Giáo trình C++ P4

Chia sẻ: Hoang Nguyen | Ngày: | Loại File: PDF | Số trang:34

0
108
lượt xem
70
download

Giáo trình C++ P4

Mô tả tài liệu
  Download Vui lòng tải xuống để xem tài liệu đầy đủ
Lưu

Nội dung Text: Giáo trình C++ P4

  1. Week 4 Game Institute Introduction to C and C++ by Stan Trujillo www.gameinstitute.com Introduction to C and C++ : Week 4: Page 1 of 34
  2. © 2001, eInstitute, Inc. You may print one copy of this document for your own personal use. You agree to destroy any worn copy prior to printing another. You may not distribute this document in paper, fax, magnetic, electronic or other telecommunications format to anyone else. This is the companion text to the www.gameinstitute.com course of the same title. With minor modifications made for print formatting, it is identical to the viewable text, but without the audio. www.gameinstitute.com Introduction to C and C++ : Week 4: Page 2 of 34
  3. Table of Contents Introduction to C++ ...................................................................................................................................... 4 Lesson 4 – Application Architecture ........................................................................................................ 4 The typedef Keyword ........................................................................................................................... 5 Handles ................................................................................................................................................. 7 Macros .................................................................................................................................................. 8 The HelloWin32 Sample....................................................................................................................... 8 Windows Messaging ........................................................................................................................... 11 The Message Pump ............................................................................................................................. 12 Callback Functions.............................................................................................................................. 13 Window Creation ................................................................................................................................ 15 The SimpleWindow Sample ............................................................................................................... 16 Class Frameworks ............................................................................................................................... 21 The GameApplication Class ............................................................................................................... 21 The WinMain function ........................................................................................................................ 27 The WndProc Function ....................................................................................................................... 28 The SimpleWindowClass Sample ....................................................................................................... 31 Message Pump Modifications ............................................................................................................. 32 What’s next? ....................................................................................................................................... 33 www.gameinstitute.com Introduction to C and C++ : Week 4: Page 3 of 34
  4. Introduction to C++ A GameInstitute course by Stan Trujillo The console applications that we’ve been written so far are useful for game-related tools and utilities (and instructional samples), but console applications are too limited for the game application itself. What we need is a windowed application. This will let us create a window in which to display graphics, and provide access to some important Windows-specific features. But this isn’t the only thing that a game requires. Although games have a lot in common with most windowed applications, there are some important differences. In this lesson we learn about windowed applications and high-level game application design, and a few C++ topics that we still need to cover. This lesson is different than the previous lessons. It introduces some real-world programming issues that we haven’t had to use thus far, and it focuses on building a body of code that we can use for the remaining lessons. It also moves more briskly than the previous lessons—meaning that not all of the code presented is discussed in detail. The reason for the increased pace is that this lesson is designed to bridge the gap between the simple and educational samples from the previous lessons and the game that we’re building up to. This bridge takes the form of a small class framework that encapsulates some of the more mundane elements of Windows programming. The advantage is that once we’ve written this framework, we don’t have to worry about how it works. This lesson moves quickly because there is a lot to cover, and because it is not necessary for you to master the Windows programming details required to implement this framework. On the other hand, it’s not enough to be given a class framework and being told not to worry how it works. That’s why we’ll spend this lesson building the framework from the bottom up. What is important to take from this lesson is the basics of Windows programming, the high-level design of a game application, and some techniques that go into good class design. Lesson 4 – Application Architecture The console applications used in the previous lessons each had two portions: those that we provided, and those and were provided in the form of functions from the standard libraries. The standard library provided us with helpful features such as user input support, screen output support, and string manipulation functions. All of these features are still available to us now that we’re going to be writing windowed applications, but we’ll need more than just the standard libraries, which are largely platform independent. What we need is Windows-specific features. Microsoft provides access to Windows-specific features through Win32, which is essentially a huge set of functions. Win32 is the Application Programming Interface (API) that is used to write Windows applications. (The ‘32’ referrers to the fact that Win32 is specific to 32 bit versions of Windows.) There are other APIs for writing Windows applications, but Win32 is the most direct way to access Windows features. Win32 is the product of an evolution that began with the first version of Windows. At the time, C was the language of choice, so Microsoft designed Win32 based on C programming techniques. Win32 is still a C-style, function based API, partly because there are so many existing applications that Microsoft can’t change the underlying API without upsetting thousands of developers, and partly because Microsoft has been relatively slow to adopt C++ in general. www.gameinstitute.com Introduction to C and C++ : Week 4: Page 4 of 34
  5. 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; www.gameinstitute.com Introduction to C and C++ : Week 4: Page 5 of 34
  6. 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”; www.gameinstitute.com Introduction to C and C++ : Week 4: Page 6 of 34
  7. 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. www.gameinstitute.com Introduction to C and C++ : Week 4: Page 7 of 34
  8. 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 int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { // all processing occurs here return 0; www.gameinstitute.com Introduction to C and C++ : Week 4: Page 8 of 34
  9. } 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 2nd and 3rd 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); www.gameinstitute.com Introduction to C and C++ : Week 4: Page 9 of 34
  10. 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: www.gameinstitute.com Introduction to C and C++ : Week 4: Page 10 of 34
  11. 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 Sent whenever the mouse is moved—includes WM_MOUSEMOVE the new location of the mouse Sent whenever the left mouse button is WM_LBUTTONDOWN pressed Sent whenever the left mouse button is WM_LBUTTONDBLCLK double-clicked Sent whenever the left mouse button is WM_LBUTTONUP released Sent whenever the right mouse button is WM_RBUTTONDOWN pressed Sent whenever the right mouse button is WM_RBUTTONDBLCLK double-clicked Sent whenever the right mouse button is WM_RBUTTONUP released Sent whenever a key is pressed—includes the WM_KEYDOWN ID of the key involved www.gameinstitute.com Introduction to C and C++ : Week 4: Page 11 of 34
  12. Sent whenever a key is released—includes WM_KEYUP the ID of the key involved Sent to indicate any change in the application WM_ACTIVATE window state, such as when the window gains or loses focus Sent when the application requests that a new WM_CREATE window be created Sent immediately before the window is WM_CLOSE 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. www.gameinstitute.com Introduction to C and C++ : Week 4: Page 12 of 34
  13. 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
  14. } 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
  15. LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam ); The callback function that we provide must have the return type and argument list shown, but can be named whatever we want. It is the address of this function that we’ll provide to Win32, not the name. The name used here, WndProc, is arbitrary, but this is the name we’ll use in this lesson. Window Creation The final item that we need to construct a windowed application is the window itself. Because of how Win32 works, creating a window actually involves two steps: registering a window class, and creating the window. Win32 requires that in order for a new window to be created, a window class—or window type—be defined. This can be done with the Win32 RegisterClass function, which is declared like this: ATOM RegisterClass( CONST WNDCLASS *lpWndClass ); The RegisterClass function takes the address of a variable of the WNDCLASS structure type. This structure contains the properties and options for the new window class. The WNDCLASS structure looks like this: typedef struct _WNDCLASS { UINT style; WNDPROC lpfnWndProc; int cbClsExtra; int cbWndExtra; HINSTANCE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPCTSTR lpszMenuName; LPCTSTR lpszClassName; } WNDCLASS, *PWNDCLASS; To register a new window class, applications create an instance of the WNDCLASS structure, initialize each member with the desired settings, and then pass the address of the structure to RegisterClass. Because our goal is to create a very simple window, we don’t need to use all of the members provided by the WNDCLASS structure. We can leave several of them assigned to zero to indicate default settings. An important WNDCLASS data member is the lpfnWndProc member, which is a pointer to the message handling callback function that Windows will invoke each time a message is to be delivered to this window class. The RegisterClass function returns a data item called an ATOM. As long was this value is non-zero, the call to RegisterClass has succeeded. It is not necessary to save this value for later use. For our purposes, we’ll register a window class with this code: WNDCLASS wc; ZeroMemory( &wc, sizeof(wc) ); wc.style = CS_HREDRAW | CS_VREDRAW; www.gameinstitute.com Introduction to C and C++ : Week 4: Page 15 of 34
  16. wc.lpfnWndProc = WndProc; wc.hInstance = hInstance; wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wc.lpszClassName = szWindowClass; ATOM r = RegisterClass(&wc); Notice that after the wc structure is declared, its address is passed to the ZeroMemory function along with the size of the structure. Win32 provides the ZeroMemory function for the purpose of assigning regions of memory to zero. The usage of ZeroMemory shown here is typical. A structure is declared, and passed to ZeroMemory so that every member is set to zero. Then just the data members of interest are assigned. Using ZeroMemory to initialize structures is similar to initializing arrays to zero using an initializer list, except that zero is the only value supported. The code above assigns the lpfnWndProc member of the WNDCLASS structure with WndProc. This is the name of the message handling callback function that we’ll define later. After a window class has been defined, the application is ready to create the actual window. This is done with the CreateWindow function, which is declared like this: HWND CreateWindow( LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HANDLE hInstance, PVOID lpParam ); The CreateWindow function takes 11 arguments, the first of which is most important. This is a const string containing the name of the window class. This must be the same name previously provided to the RegisterClass function (as the lpszClassName member of the WNDCLASS structure). Notice also that the position and size of the new window are determined by the values passed as the x, y, nWidth, and nHeight arguments. We’ll call CreateWindow like this: hwnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, 100, 100, 320, 240, NULL, NULL, hInst, NULL); CreateWindow returns a window handle (an HWND) that represents the new window. A handle value of zero indicates that CreateWindow has failed. The SimpleWindow Sample Now that we know what goes into a windowed application, we’re ready to put it all together into a sample. Actually, we’re going to write two versions of this sample. First, we’ll write a version that does not use classes or inheritance. Then, after a discussion of how classes can be used to improve upon this version, we’ll write a class-based version. As a function-based application, the SimpleWindow sample is broken into four functions: • WinMain • RegisterWindowClass • CreateSimpleWindow www.gameinstitute.com Introduction to C and C++ : Week 4: Page 16 of 34
  17. • WndProc The WinMain function is the application entry point function. The RegisterWindowClass function performs the Win32 window type registration, and the CreateSimpleWindow function creates the actual window. The WndProc function is the message-handling callback function. In addition to these four functions, this sample uses two global variables, declared like this: HINSTANCE hInst = 0; HWND hwnd = 0 The hInst variable is used to store the application handle provided to WinMain, and the hwnd variable stores the window handle. As global variables, these values are available to all of the functions in a program. As we discussed in Lesson 2 using global variables isn’t an ideal solution, so we’ll find an alternative when we write a class-based version. We’ll start our look at the SimpleWindow sample with the first function that gets called: WinMain: int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow) { hInst = hInstance; if ( ! RegisterWindowClass()) { MessageBox( 0, "Failed to register window class", "Error", MB_ICONEXCLAMATION |MB_ICONWARNING ); return FALSE; } if ( ! CreateSimpleWindow( nCmdShow )) { MessageBox( 0, "Failed to create window", "Error", MB_ICONEXCLAMATION | MB_ICONWARNING ); return FALSE; } MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } The WinMain function begins by assigning the global variable hInst with the value provided by Windows as the hInstance parameter. This makes the application handle available globally. Next the RegisterWindowClass function is called. If this function fails and returns false, a message box is displayed with an error message, and WinMain returns. Otherwise, WinMain calls the CreateSimpleWindow function, which creates the application window and displays it. If this operation fails, the problem is reported with a message box, and WinMain returns. www.gameinstitute.com Introduction to C and C++ : Week 4: Page 17 of 34
  18. If the registration and window creation succeed, WinMain begins the message retrieval and dispatching performed by the message pump discussed earlier. The message pump runs until GetMessage returns zero. What is interesting about the WinMain function is that it is completely generic, meaning that it can be used—pretty much exactly as it appears here—in virtually any application. Except for some performance tweaking that we’ve yet to discuss, we can easily use this version of WinMain for the duration of the course. This leaves us with two choices: we can copy this function into each new program that we write, or we can package it in a library of our own creation so that all of our programs can use the exact same version. This latter option provides two advantages. By centralizing WinMain, we can simplify all of the programs we’ll write. Each program can use the packaged version instead of providing its own version. Also, if we later discover a better way to implement WinMain and decide to change it, this change will automatically propagate to each program that uses the centralized version. We’ll explore the idea of centralizing WinMain further when we discuss the class-based version of this sample. The next function is the RegisterWindowClass function, which looks like this: bool RegisterWindowClass() { WNDCLASS wc; ZeroMemory( &wc, sizeof(wc) ); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WndProc; wc.hInstance = hInst; wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wc.lpszClassName = szWindowClass; if ( RegisterClass( &wc ) == 0 ) return false; return true; } Using the Win32 WNDCLASS structure and RegisterClass function discussed earlier, the RegisterWindowClass function registers a window class, using a name provided by the global constant string szWindowClass. (The sz prefix is a common naming convention for strings that are zero, or null- terminated.) The SimpleWindow sample defines this string like this: const char* szWindowClass = "SimpleWindowClass"; The actual name used for the window class isn’t very important. This string doesn’t get displayed—it just serves to identify the window type that being registered. After calling RegisterWindowClass, WinMain calls the CreateSimpleWindow function: bool CreateSimpleWindow(int nCmdShow) { hwnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, www.gameinstitute.com Introduction to C and C++ : Week 4: Page 18 of 34
  19. 100, 100, 320, 240, NULL, NULL, hInst, NULL); if ( ! hwnd) return false; ShowWindow( hwnd, nCmdShow ); UpdateWindow( hwnd ); return true; } This function begins by calling the CreateWindow function discussed previously. The first argument is the szWindowClass constant, indicating the name of the window class to use as a template for the new window. The szTitle string (the 2nd argument) is the text that will appear in the window title bar. szTitle is defined this way: const char* szTitle = "SimpleWindow Sample"; The 4th and 5th CreateWindow arguments indicate the desired position for the upper left-hand corner of the new window. The coordinate system that Windows uses places 0, 0 at the upper left-hand corner of the screen, so by using the value 100 for both the x and y position, we’re requesting that the corner of the window be placed 100 pixels from the left of the screen, and 100 pixels from the top of the screen. The following two arguments indicate the width and height of the new window. The CreateWindow return value is stored in the global variable hwnd, making the handle of the new window available to all functions. If CreateWindow succeeds, two additional Win32 functions are called. The ShowWindow function can be used to hide or show any window. The first argument is the handle of the window to be affected. We’re using the HWND returned from CreateWindow, and the window state integer passed to WinMain as the second argument. This value determines the initial state of the window. Finally, the UpdateWindow function is called with the window handle as a single argument. This function cause Windows to sent a WM_PAINT message to the new window—which is necessary in order for the window to appear properly. Like the WinMain function, much of the tasks performed by RegisterWindowClass and CreateSimpleWindow are generic. All of our applications will need to register window classes and create windows. These functions are therefore also candidates for centralization in our class framework. However, there are some issues that we’ll have to address. For example, we don’t want to centralize these functions as they are because they use settings that may not be appropriate for every application. The initial window position and size, for example, is something that the application should be able to specify. The last function in the SimpleWindow sample is the WndProc function. This is the callback function that Windows will call each time a message is dispatched to our application. For a simple application like this one, we can get away with handling only two Windows messages, but full-scale applications are likely to provide custom handling for one or two dozen messages. Our version looks like this: LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { www.gameinstitute.com Introduction to C and C++ : Week 4: Page 19 of 34
  20. case WM_PAINT: PAINTSTRUCT ps; HDC hdc; hdc = BeginPaint( hWnd, &ps ); RECT rt; GetClientRect( hWnd, &rt ); DrawText( hdc, szHello, strlen(szHello), &rt, DT_CENTER ); EndPaint( hWnd, &ps ); break; case WM_DESTROY: PostQuitMessage( 0 ); break; default: return DefWindowProc( hWnd, message, wParam, lParam ); } return 0; } The message handling callback function in Win32 applications is almost always implemented using a switch statement. In fact, one of the trademarks of Win32 applications are lengthy switch statements. Our version provides custom handling for just two messages: WM_PAINT and WM_DESTROY. The WM_PAINT message is sent by Windows whenever a window must be re-drawn. Win32 provides a number of functions for performing the actual update, or “painting” of the window, such as the BeginPaint and EndPaint functions. We won’t delve into the details of using these functions, however, because this is not how modern games draw or update their windows. We’ll talk about how this is done in the next lesson. However, just to prove to ourselves that our code is working, we display some text on the sample window using the DrawText function. The text displayed is determined by the szHello string, defined like this: const char* szHello = "hello!"; The WM_DESTROY message is sent to a window whenever the user attempts to destroy the window, usually by pressing the “X”, or “dismiss” button in the upper-right hand corner or by pressing ALT-F4. In our case either action is sufficient evidence that the user wants to exit the application, so we call PostQuitMessage, which causes the WM_QUIT message to be posted. This message causes the GetMessage function to return false, terminating the message pump in WinMain. All other messages are handled by the default case in the switch statement. These messages are passed to the DefWindowProc function, which Win32 provides as the default message handler. Passing all un- handled message to DefWindowProc is very important. Failing to do so will cause run-time errors. Compiled and executed, the SimpleWindow sample looks like this: www.gameinstitute.com Introduction to C and C++ : Week 4: Page 20 of 34
Đồng bộ tài khoản