Tải bản đầy đủ (.pdf) (34 trang)

Tài liệu Giáo trình C++ P4 docx

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (575.64 KB, 34 trang )


www.gameinstitute.com

Week
4










Game Institute
Introduction to C and C++



































by Stan Trujillo



Introduction to C and C++ : Week 4: Page 1 of 34

www.gameinstitute.com






© 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.











Introduction to C and C++ : Week 4: Page 2 of 34

www.gameinstitute.com



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
Introduction to C and C++ : Week 4: Page 3 of 34

www.gameinstitute.com

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.

Introduction to C and C++ : Week 4: Page 4 of 34

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

www.gameinstitute.com
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;
Introduction to C and C++ : Week 4: Page 15 of 34

www.gameinstitute.com
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
Introduction to C and C++ : Week 4: Page 16 of 34

www.gameinstitute.com
• 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.
Introduction to C and C++ : Week 4: Page 17 of 34


www.gameinstitute.com

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 s
trings 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,
Introduction to C and C++ : Week 4: Page 18 of 34

www.gameinstitute.com
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 2
nd
argument) is the text that will appear in the window title bar. szTitle
is defined this way:

const char* szTitle = "SimpleWindow Sample";

The 4
th
and 5
th
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)
{
Introduction to C and C++ : Week 4: Page 19 of 34

www.gameinstitute.com

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:

Introduction to C and C++ : Week 4: Page 20 of 34

www.gameinstitute.com

Class Frameworks
The SimpleWindow sample accomplishes the goal of displaying a window and processing messages
received from Windows, but much of the code is generic code that can be shared among applications.
Furthermore, the code is lacking some features that we’ll need if we intend to build a game.

Both of these issues can be addressed by constructing a class framework. This involves designing one or
more classes that provide default behavior and functionality for any application. The idea is to extract the
generic functionality into reusable classes. This makes the application code simpler, because only
application-specific behaviors and parameters need appear in the code.

A good class framework also provides features that are specific to the type of application being written

without being overly specific. In our case we’re writing a game, so we can equip our class framework
with features that all games require, but without specifying how each game uses the feature. This is a task
well suited for polymorphism.
The GameApplication Class
Our class framework will consist of just one class, which represents the entire application. We’ll call this
class GameApplication, and it will serve as a base-class for classes that represent specific applications.
We’ll begin with this definition as a starting point:

class GameApplication
{
public:
GameApplication()
{
}
~GameApplication();
{
}
};

As a base class, we’ll equip the GameApplication class with as many features as possible while keeping
it generic. For example, this class will provide the WinMain function, making it unnecessary for the
application code to provide one. Instead, the application code will derive a class from GameApplication,
and thereby inherit a version of WinMain.

Introduction to C and C++ : Week 4: Page 21 of 34

www.gameinstitute.com
Before we add any functionality to this class, however, we need to consider how it will be used. As a
class that represents the entire application, it makes sense that each application should create one and only
one instance of this class. It doesn’t make sense for someone to create two application objects in a single

application. We might leave this to chance, but if someone does create multiple instance of the application
object, the likely result is unpredictable behavior. This is a bug best avoided, so we should work to
prevent this situation.

How do you prevent a programmer from creating objects? There are ways to prevent any objects based on
a specific class from being created, but in this case we want to allow one object—no more and no less. A
class that limits object creation in this fashion is called a singleton.

One strategy you might immediately consider is to add a Boolean to the class that indicates whether an
object has been created, like this:

class GameApplication
{
public:
GameApplication()
{
assert( objectPresent == false );
objectPresent = true;
}
~GameApplication();
{
objectPresent = false;
}
private:
bool objectPresent;
};

The idea here is that the class constructor asserts that no other objects exist by insisting that
objectPresent is false. The constructor then assigns objectPresent to true. If another object is created,
objectPresent will already be true, so the assert will fail, halting execution and indicating the location of

the problem to the programmer.

There are two problems with this idea. First, each object has it’s own copy of the data member in the
class. The state of the objectPresent data member in one object has no bearing on its state in another
object. Also, the objectPresent data member is un-initialized when an object is created. It may very well
be non-zero (true) when the first GameApplication object is created, causing an unwarranted assertion
failure.

The solution to both of these problems is the static keyword. Static has two different uses in C++,
depending on whether it appears inside a class definition. For our purposes we’ll concentrate on how
static affects data members and member functions when used with a class. Consider this modification:

class GameApplication
{
public:
GameApplication()
{
assert( objectPresent == false );
objectPresent = true;
}
Introduction to C and C++ : Week 4: Page 22 of 34

www.gameinstitute.com
~GameApplication();
{
objectPresent = false;
}
private:
static bool objectPresent;
};


This definition makes the objectPresent data member static. The effect is similar to that of a global
variable. Now, all GameApplication objects share a single copy of objectPresent. The Boolean now
exists at the class-level rather than at the individual object-level. When one object sets the value of this
data member, the changes will affect all other instances of the class (i.e. all other objects), so the code
above will have the desired affect of preventing multiple GameApplication objects from being created.

There is actually one more thing that is needed: static data members must be explicitly declared outside
of the class, like this:

bool GameApplication::objectPresent = false;

This declaration reserves the actual memory required to represent the objectPresent data member.
Normally, the memory required to store each data member is allocated when an object is created, but with
static data members, this isn’t true because the static member isn’t a true data member. The memory used
to represent a static data member is global. This distinction is illustrated here:



Before we finalize the objectPresent Boolean, let’s discuss another issue that will affect the design of the
GameApplication class.

Introduction to C and C++ : Week 4: Page 23 of 34

www.gameinstitute.com
Our goal is that the GameApplication class will provide the functionality performed by the WinMain
and WndProc functions. But there’s a problem: Win32 requires that both of these functions have specific
prototypes, and these prototypes preclude these from being a member functions. For example, Win32
insists that WinMain have this prototype:


int WINAPI WinMain( HINSTANCE, HINSTANCE, LPWSTR, int );

But, if we simply added a member function to the GameApplication class, the result would be a function
with this prototype:

int WINAPI GameApplication::WinMain( HINSTANCE, HINSTANCE, LPWSTR, int );

The class to which a member function belongs is part of the function’s prototype. These two prototypes
are not compatible, because member functions have access to the current object’s data member through
the this pointer. Global functions, like the version of WinMain that Win32 requires, don’t have access to
the this pointer, because they are not part of a class. If we make either WinMain or WndProc member
functions of GameApplication, they won’t meet the requirements mandated by Win32.

There are two solutions to this problem. We’ll use one for each function. One solution is to define a
global function (not a member function) and give it access to the class to which it would normally have
been a member function through the use of the friend keyword. This keyword can be used to grant access
to the non-public portion of a class. This access can be granted to other classes or global functions. To
enable a global version of WinMain to access the GameApplication class, we can add this declaration to
the GameApplication class:

class GameApplication
{
public:
GameApplication()
{
assert( objectPresent == false );
objectPresent = true;
}
~GameApplication();
{

objectPresent = false;
}
private:
static bool objectPresent;

friend int APIENTRY WinMain( HINSTANCE, HINSTANCE, TCHAR *, int );
};

Now the global WinMain function has unrestricted access to the GameApplcation class. It can access
private data members and functions as if it were a member of the class.

The friend keyword can grant special access, but it can’t be used to force access to a class. The reason the
WinMain function has access to the GameApplication class is because the GameApplication itself
class has been granted the permission. As the class designers, we have opted to allow WinMain special
access by placing the friend usage inside that class.

Introduction to C and C++ : Week 4: Page 24 of 34

www.gameinstitute.com
There is a catch. The WinMain function, despite the fact that it now has access to the GameApplication
class members, is still not an actual member function; it doesn’t have a this pointer. This means that each
usage of a member function or a data member must be preceded by a pointer to the current object.

The same is true for the other solution, which we’ll use to implement the WndProc function. We’ll use
the static keyword to make the WndProc function a static member function. This has the same effect as
it does for data members: it makes the member global. A static member function, like a global function,
must provide an explicit object address for each data member and member function when accessing true
members of a class.

This requirement is a good reason to reconsider the use of a static Boolean for the purposes of restricting

object creation. Instead, we can use a pointer to the GameApplication class. If this pointer is zero, then
no objects have been created, but if it is non-zero, it points to the current (and only) application object.
This modification looks like this:

class GameApplication
{
public:
GameApplication()
{
assert( pApp == 0 );
pApp = this;
}
~GameApplication();
{
pApp = 0;
}
private:
static GameApplication* pApp;
};

Now the GameApplication class uses a static pointer instead of a Boolean. This pointer is used to assert
that only one instance of this class is created, and can also be used by the global WinMain function and
the static WndProc member function to access members. With the declarations for these two functions in
place, our class now looks like this;

class GameApplication
{
public:
GameApplication()
{

assert( pApp == 0 );
pApp = this;
}
~GameApplication();
{
pApp = 0;
}
private:
static GameApplication* pApp;

friend int APIENTRY WinMain( HINSTANCE, HINSTANCE, TCHAR *, int );
static LRESULT CALLBACK WndProc( HWND, UINT, WPARAM, LPARAM );
};
Introduction to C and C++ : Week 4: Page 25 of 34

×