Thứ Hai, 10 tháng 2, 2014
Tài liệu Giáo trình C++ P4 docx
www.gameinstitute.com
For C++ programmers, Microsoft provides other APIs such as MFC (Microsoft Foundation Classes), and
ATL (Active Template Library). Instead of a collection of functions, these packages provide a collection
of classes that can be used for Windows application development. MFC in particular provides classes that
are designed to be used as a generic class framework from which any type of application can be written.
Both MFC and ATL are built “on top of” Win32, meaning that although they provide an alternative
interface, they are in fact written using Win32. As a result, most game programmers feel that using these
APIs for games is foolish because better performance can be gained by using Win32 directly. This is a
debatable issue, but for this course we’ll go with the status quo, and opt not to use MFC or ATL.
This doesn’t mean that we won’t be using classes to build the foundation required for games. It means
that we’ll be using Win32 to write a small and efficient class framework that is designed specifically with
games in mind. This framework can be small because, although Win32 is a massive API, most games
require the use of only a tiny fraction of the complete Win32 API.
Before we can do any of this, however, there are some topics that need to be covered first. For starters,
using Win32 requires knowledge of some language features that we haven’t discussed yet.
The typedef Keyword
C and C++ both support the typedef keyword, which is short for type definition. A type definition is
essentially a type alias. The typedef keyword can be used to define an alternative name for any data type.
typedef is used extensively in Win32. For example, Win32 defines aliases for most of the intrinsic types.
Consider these variable declarations:
INT i;
SHORT j;;
LONG l;
FLOAT f;
CHAR ch;
Each of these variables uses an intrinsic type, but through an alias that Win32 provides. The code in any
Windows application is free to use these types instead of the genuine intrinsic types. There’s no
difference between the two, because the genuine types are used with typedef to create these aliases, like
this:
typedef int INT;
typedef short SHORT;
typedef long LONG;
typedef float FLOAT;
typedef char CHAR;
The typedef keyword is followed by the name of an existing type, and ends with the desired alias and a
semicolon. typedef doesn’t create new data types—it creates new names for existing data types.
The examples shown above are of dubious value. They don’t provide any advantage over the native types
unless you happen to prefer upper-case data type names. But these are just a few of the type aliases that
Win32 provides. Some are useful merely because they are shorter than the alternatives. For example:
typedef unsigned short USHORT;
typedef unsigned char UCHAR;
typedef unsigned int UINT;
Introduction to C and C++ : Week 4: Page 5 of 34
www.gameinstitute.com
These type definitions provide shorthand syntax for the unsigned variation of the intrinsic types:
USHORT index;
Instead of
unsigned short index;
But Win32 doesn’t stop there—some new terms are used to describe familiar types. One example is the
term word. For a 32-bit operating system like Windows, a word is a 16-bit unsigned integer, and a double
word is a 32-bit unsigned integer. Hence these typedefs:
typedef unsigned short WORD;
typedef unsigned long DWORD;
Win32 also uses typedef to define names for some data types that are used for specific purposes. For
example, Windows is a message-based operating system. Messages are used to communicate with the
application code. Each message has two optional parameters, which are represented by the WPARAM
and LPARAM types, as defined here:
typedef UINT WPARAM;
typedef LONG LPARAM;
As we’ll see later, Windows messages are delivered by functions that provide the message itself, which is
represented by the UINT type alias, and is accompanied by parameters, like this:
int ProcessMessage(UINT msg, WPARAM wParam, LPARAM lParam);
At first glance, this function appears to use three exotic parameter types, but each is actually just a
typedef for an integer type.
Pointer types are also frequently assigned alternative type names by Win32. These typedefs have the
prefix “LP”, which is short for long pointer. The “long” refers to a designation made obsolete by 32-bit
operating systems, but is nevertheless still used. For example, LPDWORD is a pointer to the DWORD
type, and can be used like this:
DWORD value;
LPDWORD pValue = &value;
Despite the fact that no asterisk appears in the declaration of pValue, it is a pointer because the
LPDWORD type definition includes the asterisk. Win32 defines LPDWORD like this:
typedef DWORD* LPDWORD;
A number of similar type aliases are provided for char pointers:
typedef CHAR* LPSTR;
typedef const CHAR* LPCSTR;
These definitions make the following declarations possible:
LPSTR str = “Initial Text”;
Introduction to C and C++ : Week 4: Page 6 of 34
www.gameinstitute.com
LPCSTR const_str = “Const Text”;
Win32 uses these alternative type names extensively, so many of the function prototypes and structures
that Win32 provides for use in Windows programming look as though they are based on exotic data
types, but in fact most of the types used are simply intrinsic types. Nevertheless, the frequency at which
these types are used makes using Win32 a bit more difficult at first.
Handles
Win32 is designed around constructs that C programmers developed long before C++ was invented. One
of these constructs uses handles to represent entities that, had C++ been used, would be classes instead. A
handle is a data type that represents a specific entity such as a window, a bitmap, or an icon. When one of
these items is created, Win32 provides a handle that represents the new item. This handle can then be
used with functions that Win32 provides to manipulate the item.
A prominent example is the HWND data type, which serves as a handle for a window. (The “H” prefix
stands for handle, and WND is short for window.) When a new window is created, an instance of the
HWND type is used to store the handle value for that window. This handle can be used with a number of
functions what Win32 provides to manipulate that window. The ShowWindow function, for example, has
this prototype:
BOOL ShowWindow( HWND hWnd, int nCmdShow );
The first argument is the handle to the window that should be affected by the function call, and the second
argument is used to indicate the effect desired. The SW_SHOW symbol, for example, is provided to
indicate to ShowWindow that the window that the handle represents should be displayed if it is currently
hidden. Notice that the ShowWindow return type uses BOOL instead of the intrinsic type bool—yet
another type alias used by Win32.
Another important handle type is HINSTANCE, which essentially represents the application itself. As
we’ll see soon, Win32 provides an HINSTANCE handle to the application on startup.
The idea behind a handle is that it, together with the functions that accept handles as arguments,
encapsulate the functionality required for dealing with windows, applications, or any other programmatic
entity. Without classes, that’s the best that can be done to hide how a system works from the programmer.
If Win32 had been written using C++, then handles wouldn’t have been necessary.
Most of the time, handles are actually type aliases created with typedef that use either an integer or a
pointer as an underlying type. The HINSTANCE handle type, for example, is defined this way:
typedef void* HINSTANCE;
HINSTANCE is actually a void pointer. Pointers to the void type are generic pointers they are capable
of pointing to any type of data. The fact that Win32 defines HINSTANCE this way tells us that each
HINSTANCE is a pointer to a data type that is probably used by Win32 internally, but that we’re not
supposed to know about. The details of how Windows handles HWND and HINSTANCE are therefore
hidden from us—providing the C version of encapsulation.
Introduction to C and C++ : Week 4: Page 7 of 34
www.gameinstitute.com
Macros
We’ve intentionally avoided macros after mentioning them briefly in Lesson 1. In C++, macros can
almost always be avoided with superior features. For example, when a literal value has been required in
our programs, we’ve been using const, like this:
const int MaxPlayers = 10;
This is a better solution than the macro equivalent:
#define MaxPlayers 10
Both can be used in the same way:
SetMaxPlayers( MaxPlayers );
But the macro is a preprocessor command, whereas a constant is a C++ language construct. This means
that C++ understands constants in a way that it doesn’t understand macros. In the example above, if the
SetMaxPlayers function was modified to take a float instead of an int, the macro would cause a cryptic
compiler error, but the constant would result in type-mismatched error, more correctly reporting the
problem.
Nevertheless, Win32 uses macros liberally. Virtually every symbol used in conjunction with Win32
functions is a macro and not a constant. The SW_SHOW symbol used with the previously mentioned
ShowWindow function, for example, is defined like this:
#define SW_SHOW 5
These symbols would best be constants instead, but Win32 still uses macros. Luckily, programmers are
unaffected most of the time.
The HelloWin32 Sample
Unlike console applications, Windows applications don’t use main as a universal entry-point. Instead,
they use a similar function called WinMain. Each Win32 application must provide a version of this
function. Like main, WinMain is the first function called, and the last to exit. It also provides command-
line information to the application.
As we mentioned in Lesson 1, a windowed application that actually displays a window requires at least 50
lines of code or so. However, an application that doesn’t display a window can be much simpler. All you
need is the WinMain function. But Win32 applications don’t support the same output options available to
console applications, so, although you can send data to cout, there’s no place to display it. With no
output, there is very little point in running the resulting executable, but the code for the simplest possible
Win32 application looks like this:
#include <windows.h>
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
// all processing occurs here
return 0;
Introduction to C and C++ : Week 4: Page 8 of 34
www.gameinstitute.com
}
Using Win32 requires that the windows.h header file be included. This header file declares and defines
the core data types and functions necessary for writing Windows applications.
The WinMain function returns an int, but another symbol appears after the return type: APIENTRY.
This is a macro that Win32 defines to distinguish between the two types of function calling conventions
available in C and C++. These conventions define the exact manner in which a function call is performed
at the machine-language level. The standard method is used by default, and the pascal style is activated
by the pascal keyword. The APIENTRY macro resolves to the pascal keyword, which causes the
compiler to implement the WinMain function with the Pascal calling convention. For our purposes, all
we need to know is that APIENTRY or pascal must appear before the WinMain function name.
WinMain has four parameters. The first is the HINSTANCE that represents the application. This value
is required for some Win32 function calls, and is often stored in a global variable so that it is accessible to
all of the functions in the program. The second parameter is also an HINSTANCE, but is always zero for
Win32 applications (it was used for 16-bit versions of Windows), so it can be ignored.
The third argument is a string containing the command-line arguments used to launch the executable.
Unlike the argv parameter provided to the main function, this is not an array of strings, but a single string
containing all of the arguments as provided on the command-line. Windowed applications use command-
line arguments less frequently than console applications, so this parameter is often ignored.
The fourth and final WinMain argument is an integer that contains a value indicating the desired initial
state for the application’s window. Normally this value is equal to the SW_SHOWNORMAL symbol.
This initial version of WinMain doesn’t do anything except return zero. For the WinMain function, a
return value of zero indicates that the application is terminating without processing any messages,
which—in this case—is true.
This is the first point in this course in which we’ve had to implement a function that provides parameters
that we are unlikely to use. The second WinMain parameter in particular is useless, and we won’t need
the command-line arguments either so we can rewrite the definition of this function so that no parameter
name is given, like this:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
// all processing occurs here
return 0;
}
The parameter type is mandatory, so we’ve left the type for the 2
nd
and 3
rd
parameters, but the parameter
name is unnecessary if it is unused, so we can safely omit the names for these two parameters.
Clearly, this is a pretty boring Win32 application, even if it is our first, because there is no output. We
can’t use cout, but we can use another form of Window output: a message box. Win32 provides the
MessageBox function for situations in which an application must display a message—usually an error
message. The MessageBox function prototype looks like this:
int MessageBox( HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
Introduction to C and C++ : Week 4: Page 9 of 34
www.gameinstitute.com
The first argument that MessageBox requires is the HWND of the application window. Since we don’t
have a window yet, we’ll use zero, which instructs Win32 to use the Windows desktop as the parent
window for the message box. The second argument is the text that is to appear inside the message box and
the third argument is the text to appear on the message box title bar.
The fourth argument is an integer that is used to control which icon (if any) appears on the message box,
and which buttons should appear. For example, using the MB_OK symbol causes the message box to
display just an “OK” button. We can display a message box by adding a call to MessageBox to
WinMain, like this:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
MessageBox( 0, "Hello Win32!", "WinMain", MB_OK );
return 0;
}
This version of WinMain is used in the HelloWin32 sample, which displays this output:
Before we move on, we can use the MessageBox function to discuss the bit-wise OR operator (|). This
operator combines two integer values into a single value by setting the individual bits of the resulting
value according to the bits of the two source values. The OR operator manipulates values at the binary
level—something that we won’t get into in this course, but is an operation that is often used within an
API to allow one or more options to be enabled through a single parameter.
The MessageBox function uses this technique for the fourth parameter. Previously, we used the MB_OK
symbol to indicate that an “OK” button should be displayed, but Win32 provides more symbols for use
with the MessageBox function. The MB_ICONEXCLAMATION symbol, for example, indicates that
an icon containing an exclamation point should be displayed. We can modify our call to MessageBox to
use both of these options, like this:
MessageBox( 0, "Hello Win32!", "WinMain", MB_OK | MB_ICONEXCLAMATION );
The OR operator is used to separate the MB_OK and MB_ICONEXCLAMATION symbols. The result
is that the value of each symbol is combined into a single integer, which is provided to the MessageBox
function. Both symbols have an effect on the results, which look like this:
Introduction to C and C++ : Week 4: Page 10 of 34
www.gameinstitute.com
When an API provides symbols for use in activating options, the symbols used to represent each optional
behavior are called flags. In this case we added the MB_ICONEXCLAMATION flag to activate an
option that the MessageBox function supports.
Windows Messaging
Even with output, our first windowed application is a humble beginning. We managed to display a
message box, which is a window, but one that is provided by Windows. We need to create our own
window so that we can control its size and contents. This will require knowledge of how Win32
communicates with each window.
Windows is a message-based operating system. It uses messages to enable communication between the
operating system and each application. For example, if the user presses a key, and your application
window is currently the top-most, or active window, Windows delivers a WM_KEYDOWN message to
your application. This message is accompanied by information that indicates which key was pressed.
Likewise, when the user releases the key, a WM_KEYUP message is posted to your application.
Windows defines hundreds of different messages. Each is delivered for a different reason. Some messages
include extra information about the message, and some messages don’t. The handling of these messages
is a vital part of any windowed application, but luckily most messages can be passed back to Windows
without any processing. This is because Windows provides a default message handling mechanism that
will handle a message in a generic fashion.
This table contains some of the messages that often warrant custom processing:
WM_PAINT
Windows sends this message to any window
that is to be redrawn
WM_MOUSEMOVE
Sent whenever the mouse is moved—includes
the new location of the mouse
WM_LBUTTONDOWN
Sent whenever the left mouse button is
pressed
WM_LBUTTONDBLCLK
Sent whenever the left mouse button is
double-clicked
WM_LBUTTONUP
Sent whenever the left mouse button is
released
WM_RBUTTONDOWN
Sent whenever the right mouse button is
pressed
WM_RBUTTONDBLCLK
Sent whenever the right mouse button is
double-clicked
WM_RBUTTONUP
Sent whenever the right mouse button is
released
WM_KEYDOWN
Sent whenever a key is pressed—includes the
ID of the key involved
Introduction to C and C++ : Week 4: Page 11 of 34
www.gameinstitute.com
WM_KEYUP
Sent whenever a key is released—includes
the ID of the key involved
WM_ACTIVATE
Sent to indicate any change in the application
window state, such as when the window gains
or loses focus
WM_CREATE
Sent when the application requests that a new
window be created
WM_CLOSE
Sent immediately before the window is
destroyed
In order enable the processing of messages, a windowed application must provide three things not present
in the previous sample: a message pump, a message handling callback function, and a window.
The Message Pump
A message pump is a loop that retrieves and dispatches messages. Win32 applications are responsible for
message pumping, but this is easy to do because Win32 provides the functions that are necessary. The
standard message pump involves these three Win32 functions:
• GetMessage
• TranslateMessage
• DispatchMessage
The GetMessage function retrieves any messages that have been queued for the application. If no
messages are present, GetMessage waits until a message arrives before returning. This behavior isn’t well
suited for games—a subject we’ll return to later in this lesson.
The TranslateMessage function is used to handle accelerators, the Windows term for “hot keys” or
keyboard shortcuts. This feature allows keyboard commands to be mapped to menu commands.
Finally, the DispatchMessage function instructs Win32 to deliver the message. Messaging works on a
window basis. Each message is addressed to a window as represented by the HWND data type.
The standard message pump looks like this:
MSG msg;
while (GetMessage( &msg, NULL, 0, 0 ))
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
This code begins by declaring an instance of the MSG structure, which Win32 provides to represent each
message. Then, using a while loop, the GetMessage function is called, passing the address of the message
variable as the first argument. The second GetMessage argument is the HWND of the window for which
messages are to be retrieved, but NULL can be used instead to indicate that any messages for the current
application should be retrieved. (Win32 defines NULL as a synonym for zero.) Zero is almost always
used for the last two GetMessage arguments.
Once a message has been retrieved, it is passed first to TranslateMessage and then to DispatchMessage.
The while loop continues to pump messages until GetMessage returns false, which happens when the
WM_QUIT message is posted.
Introduction to C and C++ : Week 4: Page 12 of 34
www.gameinstitute.com
This message pump can be placed inside the WinMain function, like this:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
MSG msg;
while (GetMessage( &msg, NULL, 0, 0 ))
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
return msg.wParam;
}
However, since messages are addressed to specific windows, and we don’t have one yet, there’s no point
in compiling this code.
Callback Functions
Software programs are all composed of functions and data. In Lesson 2, when we learned about pointers,
we found that data resides in memory and can be accessed by memory address through the use of
pointers.
This is also true of functions. Like the data in a program, the series of instructions that are contained in
functions reside in memory. As such, functions have addresses, just like data items. Normally it isn’t
necessary to take the address of a function, or concern yourself with how function calls and definitions
actually work, but these subjects are necessary in order to use callback functions. A callback function is
no different than a regular function, but how
it gets called is important.
Normally, with the exception of the main or WinMain functions, you are responsible for every function
call in the program. The program entry point function (main or WinMain) is called by the startup code,
but after that, every subsequent function call is made by the entry point function, either directly or
indirectly.
A callback function is a function that is application defined, but called by system or library code. The
address of a callback function is provided to an API such as Win32, and the API implementation calls the
function at a later time. Callback functions allow a generic API to perform an application-specific task.
By calling a callback function, the API is temporarily handing control over to the application for a
specific purpose.
In the case of Windows messaging, callback functions are used to allow the application to respond to
Windows messages. The application defines a function that handles the messages of interest, and supplies
Win32 with the address of this function. Then, whenever a message is dispatched to the application,
Window calls this function via the function pointer passing to it the message information.
In order to understand how the address of a function can be determined, and how it can later be used to
make a function call without using the function name, let’s take a closer look at how function calls work.
Consider this function:
void Func(int val)
{
cout << val << endl;
Introduction to C and C++ : Week 4: Page 13 of 34
www.gameinstitute.com
}
When we compile our program, the compiler converts this function into a set of instructions, and embeds
these instructions into the final executable. When the executable is launched, these instructions are loaded
into memory at an address that is determined at run-time. Each time the function is called, the execution
flow is diverted to the address where the function happened to be loaded into memory.
Let’s assume that Func happened to be loaded into memory at the address 0x5000. This means that when
Func is called, like this:
Func( 100 );
This function call diverts the execution flow to the memory address 0x5000. This is possible because
Func is actually a pointer—a pointer to a function. The parentheses that follow each function are similar
to the de-reference operator: they access the function at the address indicated by the preceding function
name. In other words, a function name followed by an argument list is a function call, but a function name
by itself is the address where the function resides in memory:
cout << Func << endl; // displays 0x5000
This code displays the address of Func. Taking the address of a function is easy—no “address of”
operator is required because the function name is a pointer. This is actually all we need to know in order
to define a callback function, but since we’re on the subject, let’s take a look at how function pointers are
declared and used.
Declaring pointers to functions is not very intuitive because the return type and argument list are part of
the data type. Moreover, the resulting pointer name doesn’t appear after the data type, as is normally the
case:
void (*pFuncPtr)(int);
This declares a pointer named pFuncPtr that is a pointer to a function that has a return type of void, and
takes a single integer argument. This pointer can legally be assigned to any function that matches this
description, like this:
pFuncPtr = Func;
The fact that this assignment compiles demonstrates that a function name is indeed a pointer. Both
pFuncPtr and Func now contain the address where the code that implements the Func function resides.
This means that either one can be used to call it, like this:
Func( 100 ); // calls Func with 100 as an argument
pFuncPtr( 200 ); // calls Func with 200 as an argument
Both of these calls result in the execution of the same code because they both point to the same function.
Win32 uses callback functions to deliver messages to each application. Each window that an application
creates is required to define a callback function and provide its address to Win32. In the example we used
previously, the function had to have a return type of void and accept an integer argument. Win32 requires
that message handling callback functions have a return type and argument list that looks like this:
Introduction to C and C++ : Week 4: Page 14 of 34
Đăng ký:
Đăng Nhận xét (Atom)
Không có nhận xét nào:
Đăng nhận xét