Concurrent Programming on Windows

Creating a window is as easy as calling the CreateWindow function.

Well, not really. Although the function to create a window is indeed named CreateWindow and you can find documentation for this function at /Platform SDK/User Interface Services/Windowing/Windows/Window Reference/Window Functions, you'll discover that the first argument to CreateWindow is something called a "window class name" and that a window class is connected to something called a "window procedure." Perhaps before we try calling CreateWindow, a little background information might prove helpful.

An Architectural Overview

When programming for Windows, you're really engaged in a type of object-oriented programming. This is most evident in the object you'll be working with most in Windows, the object that gives Windows its name, the object that will soon seem to take on anthropomorphic characteristics, the object that might even show up in your dreams: the object known as the "window."

The most obvious windows adorning your desktop are application windows. These windows contain a title bar that shows the program's name, a menu, and perhaps a toolbar and a scroll bar. Another type of window is the dialog box, which may or may not have a title bar.

Less obvious are the various push buttons, radio buttons, check boxes, list boxes, scroll bars, and text-entry fields that adorn the surfaces of dialog boxes. Each of these little visual objects is a window. More specifically, these are called "child windows" or "control windows" or "child window controls."

The user sees these windows as objects on the screen and interacts directly with them using the keyboard or the mouse. Interestingly enough, the programmer's perspective is analogous to the user's perspective. The window receives the user input in the form of "messages" to the window. A window also uses messages to communicate with other windows. Getting a good feel for messages is an important part of learning how to write programs for Windows.

Here's an example of Windows messages: As you know, most Windows programs have sizeable application windows. That is, you can grab the window's border with the mouse and change the window's size. Often the program will respond to this change in size by altering the contents of its window. You might guess (and you would be correct) that Windows itself rather than the application is handling all the messy code involved with letting the user resize the window. Yet the application "knows" that the window has been resized because it can change the format of what it displays.

How does the application know that the user has changed the window's size? For programmers accustomed to only conventional character-mode programming, there is no mechanism for the operating system to convey information of this sort to the user. It turns out that the answer to this question is central to understanding the architecture of Windows. When a user resizes a window, Windows sends a message to the program indicating the new window size. The program can then adjust the contents of its window to reflect the new size.

"Windows sends a message to the program." I hope you didn't read that statement without blinking. What on earth could it mean? We're talking about program code here, not a telegraph system. How can an operating system send a message to a program?

When I say that "Windows sends a message to the program" I mean that Windows calls a function within the program—a function that you write and which is an essential part of your program's code. The parameters to this function describe the particular message that is being sent by Windows and received by your program. This function in your program is known as the "window procedure."

You are undoubtedly accustomed to the idea of a program making calls to the operating system. This is how a program opens a disk file, for example. What you may not be accustomed to is the idea of an operating system making calls to a program. Yet this is fundamental to Windows' architecture.

Every window that a program creates has an associated window procedure. This window procedure is a function that could be either in the program itself or in a dynamic-link library. Windows sends a message to a window by calling the window procedure. The window procedure does some processing based on the message and then returns control to Windows.

More precisely, a window is always created based on a "window class." The window class identifies the window procedure that processes messages to the window. The use of a window class allows multiple windows to be based on the same window class and hence use the same window procedure. For example, all buttons in all Windows programs are based on the same window class. This window class is associated with a window procedure located in a Windows dynamic-link library that processes messages to all the button windows.

In object-oriented programming, an object is a combination of code and data. A window is an object. The code is the window procedure. The data is information retained by the window procedure and information retained by Windows for each window and window class that exists in the system.

A window procedure processes messages to the window. Very often these messages inform a window of user input from the keyboard or the mouse. For example, this is how a push-button window knows that it's being "clicked." Other messages tell a window when it is being resized or when the surface of the window needs to be redrawn.

When a Windows program begins execution, Windows creates a "message queue" for the program. This message queue stores messages to all the windows a program might create. A Windows application includes a short chunk of code called the "message loop" to retrieve these messages from the queue and dispatch them to the appropriate window procedure. Other messages are sent directly to the window procedure without being placed in the message queue.

If your eyes are beginning to glaze over with this excessively abstract description of the Windows architecture, maybe it will help to see how the window, the window class, the window procedure, the message queue, the message loop, and the window messages all fit together in the context of a real program.

The HELLOWIN Program

Creating a window first requires registering a window class, and that requires a window procedure to process messages to the window. This involves a bit of overhead that appears in almost every Windows program. The HELLOWIN program, shown in Figure 3-1, is a simple program showing mostly that overhead.

Figure 3-1. The HELLOWIN program.

HELLOWIN.C

/*------------------------------------------------------------ HELLOWIN.C -- Displays "Hello, Windows 98!" in client area (c) Charles Petzold, 1998 ------------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("HelloWin") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, // window class name TEXT ("The Hello Program"), // window caption WS_OVERLAPPEDWINDOW, // window style CW_USEDEFAULT, // initial x position CW_USEDEFAULT, // initial y position CW_USEDEFAULT, // initial x size CW_USEDEFAULT, // initial y size NULL, // parent window handle NULL, // window menu handle hInstance, // program instance handle NULL) ; // creation parameters ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) { case WM_CREATE: PlaySound (TEXT ("hellowin.wav"), NULL, SND_FILENAME | SND_ASYNC) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

This program creates a normal application window, as shown in Figure 3-2, and displays, "Hello, Windows 98!" in the center of that window. If you have a sound board installed, you will also hear me saying the same thing.

Figure 3-2. The HELLOWIN window.

A couple of warnings: If you use Microsoft Visual C++ to create a new project for this program, you need to make an addition to the object libraries the linker uses. Select the Settings option from the Project menu, and pick the Link tab. Select General from the Category list box, and add WINMM.LIB ("Windows multimedia") to the Object/Library Modules text box. You need to do this because HELLOWIN makes use of a multimedia function call, and the multimedia object library isn't included in a default project. Otherwise you'll get an error message from the linker indicating that the PlaySound function is unresolved.

HELLOWIN accesses a file named HELLOWIN.WAV, which is on the companion CD-ROM in the HELLOWIN directory. When you execute HELLOWIN.EXE, the default directory must be HELLOWIN. This is the case when you execute the program within Visual C++, even though the executable will be in the RELEASE or DEBUG subdirectory of HELLOWIN.

Thinking Globally

Most of HELLOWIN.C is overhead found in virtually every Windows program. Nobody really memorizes all the syntax to write this overhead; generally, Windows programmers begin a new program by copying an existing program and making appropriate changes to it. You're free to use the programs on the companion CD-ROM in this manner.

I mentioned above that HELLOWIN displays the text string in the center of its window. That's not precisely true. The text is actually displayed in the center of the program's "client area," which in Figure 3-2 is the large white area within the title bar and the sizing border. This distinction will be important to us; the client area is that area of the window in which a program is free to draw and deliver visual output to the user.

When you think about it, this program has an amazing amount of functionality in its 80-odd lines of code. You can grab the title bar with the mouse and move the window around the screen. You can grab the sizing borders and resize the window. When the window changes size, the program automatically repositions the text string in the center of its client area. You can click the maximize button and zoom HELLOWIN to fill the screen. You can click the minimize button and clear it from the screen. You can invoke all these options from the system menu (the small icon at the far left of the title bar). You can also close the window to terminate the program by selecting the Close option from the system menu, by clicking the close button at the far right of the title bar, or by double-clicking the system menu icon.

We'll be examining this program in detail for much of the remainder of the chapter. First, however, let's take a more global look.

HELLOWIN.C has a WinMain function like the sample programs in the first two chapters, but it also has a second function named WndProc. This is the window procedure. (In conversation among Windows programmers, it's called the "win prock.") Notice that there's no code in HELLOWIN.C that calls WndProc. However, there is a reference to WndProc in WinMain, which is why the function is declared near the top of the program.

The Windows Function Calls

HELLOWIN makes calls to no fewer than 18 Windows functions. In the order they occur, these functions (with a brief description) are:

These functions are described in the Platform SDK documentation, and they are declared in various header files, mostly in WINUSER.H.

Uppercase Identifiers

You'll notice the use of quite a few uppercase identifiers in HELLOWIN.C. These identifiers are defined in the Windows header files. Several of these identifiers contain a two-letter or three-letter prefix followed by an underscore:

CS_HREDRAW DT_VCENTER SND_FILENAME
CS_VREDRAW IDC_ARROW WM_CREATE
CW_USEDEFAULT IDI_APPLICATION WM_DESTROY
DT_CENTER MB_ICONERROR WM_PAINT
DT_SINGLELINE SND_ASYNC WS_OVERLAPPEDWINDOW

These are simply numeric constants. The prefix indicates a general category to which the constant belongs, as indicated in this table:

Prefix Constant
CS Class style option
CW Create window option
DT Draw text option
IDI ID number for an icon
IDC ID number for a cursor
MB Message box options
SND Sound option
WM Window message
WS Window style

You almost never need to remember numeric constants when programming for Windows. Virtually every numeric constant has an identifier defined in the header files.

New Data Types

Some other identifiers used in HELLOWIN.C are new data types, also defined in the Windows header files using either typedef or #define statements. This was originally done to ease the transition of Windows programs from the original 16-bit system to future operating systems that would be based on 32-bit technology. This didn't quite work as smoothly and transparently as everyone thought at the time, but the concept was fundamentally sound.

Sometimes these new data types are just convenient abbreviations. For example, the UINT data type used for the second parameter to WndProc is simply an unsigned int, which in Windows 98 is a 32-bit value. The PSTR data type used for the third parameter to WinMain is a pointer to a nonwide character string, that is, a char *.

Others are less obvious. For example, the third and fourth parameters to WndProc are defined as WPARAM and LPARAM, respectively. The origin of these names requires a bit of history. When Windows was a 16-bit system, the third parameter to WndProc was defined as a WORD, which was a 16-bit unsigned short integer, and the fourth parameter was defined as a LONG, which was a 32-bit signed long integer. That's the reason for the "W" and "L" prefixes on the word "PARAM." In the 32-bit versions of Windows, however, WPARAM is defined as a UINT and LPARAM is defined as a LONG (which is still the C long data type), so both parameters to the window procedure are 32-bit values. This may be a little confusing because the WORD data type is still defined as a 16-bit unsigned short integer in Windows 98, so the "W" prefix to "PARAM" creates somewhat of a misnomer.

The WndProc function returns a value of type LRESULT. That's simply defined as a LONG. The WinMain function is given a type of WINAPI (as is every Windows function call defined in the header files), and the WndProc function is given a type of CALLBACK. Both these identifiers are defined as __stdcall, which refers to a special calling sequence for function calls that occur between Windows itself and your application.

HELLOWIN also uses four data structures (which I'll discuss later in this chapter) defined in the Windows header files. These data structures are shown in the table below.

Structure Meaning
MSG Message structure
WNDCLASS Window class structure
PAINTSTRUCT Paint structure
RECT Rectangle structure

The first two data structures are used in WinMain to define two structures named msg and wndclass. The second two are used in WndProc to define two structures named ps and rect.

Getting a Handle on Handles

Finally, there are three uppercase identifiers for various types of "handles":

Identifier Meaning
HINSTANCE Handle to an "instance"—the program itself
HWND Handle to a window
HDC Handle to a device context

Handles are used quite frequently in Windows. Before the chapter is over, you will also encounter HICON (a handle to an icon), HCURSOR (a handle to a mouse cursor), and HBRUSH (a handle to a graphics brush).

A handle is simply a number (usually 32 bits in size) that refers to an object. The handles in Windows are similar to file handles used in conventional C or MS-DOS programming. A program almost always obtains a handle by calling a Windows function. The program uses the handle in other Windows functions to refer to the object. The actual value of the handle is unimportant to your program, but the Windows module that gives your program the handle knows how to use it to reference the object.

Hungarian Notation

You might also notice that some of the variables in HELLOWIN.C have peculiar-looking names. One example is szCmdLine, passed as a parameter to WinMain.

Many Windows programmers use a variable-naming convention known as "Hungarian Notation," in honor of the legendary Microsoft programmer Charles Simonyi. Very simply, the variable name begins with a lowercase letter or letters that denote the data type of the variable. For example, the sz prefix in szCmdLine stands for "string terminated by zero." The h prefix in hInstance and hPrevInstance stands for "handle;" the i prefix in iCmdShow stands for "integer." The last two parameters to WndProc also use Hungarian notation, although, as I explained before, wParam should more properly be named uiParam (ui for "unsigned integer"). But because these two parameters are defined using the data types WPARAM and LPARAM, I've chosen to retain their traditional names.

When naming structure variables, you can use the structure name (or an abbreviation of the structure name) in lowercase either as a prefix to the variable name or as the entire variable name. For example, in the WinMain function in HELLOWIN.C, the msg variable is a structure of the MSG type; wndclass is a structure of the WNDCLASS type. In the WndProc function, ps is a PAINTSTRUCT structure and rect is a RECT structure.

Hungarian notation helps you avoid errors in your code before they turn into bugs. Because the name of a variable describes both the use of a variable and its data type, you are much less likely to make coding errors involving mismatched data types.

The variable name prefixes I'll generally be using in this book are shown in the following table.

Prefix Data Type
c char or WCHAR or TCHAR
by BYTE (unsigned char)
n short
i int
x, y int used as x-coordinate or y-coordinate
cx, cy int used as x or y length; c stands for "count"
b or f BOOL (int); f stands for "flag"
w WORD (unsigned short)
l LONG (long)
dw DWORD (unsigned long)
fn function
s string
sz string terminated by 0 character
h handle
p pointer

Registering the Window Class

A window is always created based on a window class. The window class identifies the window procedure that processes messages to the window.

More than one window can be created based on a single window class. For example, all button windows—including push buttons, check boxes, and radio buttons—are created based on the same window class. The window class defines the window procedure and some other characteristics of the windows that are created based on that class. When you create a window, you define additional characteristics of the window that are unique to that window.

Before you create an application window, you must register a window class by calling RegisterClass. This function requires a single parameter, which is a pointer to a structure of type WNDCLASS. This structure includes two fields that are pointers to character strings, so the structure is defined two different ways in the WINUSER.H header file. First, there's the ASCII version, WNDCLASSA:

typedef struct tagWNDCLASSA { UINT style ; WNDPROC lpfnWndProc ; int cbClsExtra ; int cbWndExtra ; HINSTANCE hInstance ; HICON hIcon ; HCURSOR hCursor ; HBRUSH hbrBackground ; LPCSTR lpszMenuName ; LPCSTR lpszClassName ; } WNDCLASSA, * PWNDCLASSA, NEAR * NPWNDCLASSA, FAR * LPWNDCLASSA ;

Notice some uses of Hungarian notation here: The lpfn prefix means "long pointer to a function." (Recall that in the Win32 API there is no distinction between long pointers and near pointers. This is a remnant of 16-bit Windows.) The cb prefix stands for "count of bytes" and is often used for a variable that denotes a byte size. The h prefix is a handle, and the hbr prefix means "handle to a brush." The lpsz prefix is a "long pointer to a string terminated with a zero."

The Unicode version of the structure is defined like so:

typedef struct tagWNDCLASSW { UINT style ; WNDPROC lpfnWndProc ; int cbClsExtra ; int cbWndExtra ; HINSTANCE hInstance ; HICON hIcon ; HCURSOR hCursor ; HBRUSH hbrBackground ; LPCWSTR lpszMenuName ; LPCWSTR lpszClassName ; } WNDCLASSW, * PWNDCLASSW, NEAR * NPWNDCLASSW, FAR * LPWNDCLASSW ;

The only difference is that the last two fields are defined as pointers to constant wide-character strings rather than pointers to constant ASCII character strings.

After WINUSER.H defines the WNDCLASSA and WNDCLASSW structures (and pointers to the structures), the header file defines WNDCLASS and pointers to WNDCLASS (some included for backward compatibility) based on the definition of the UNICODE identifier:

#ifdef UNICODE typedef WNDCLASSW WNDCLASS ; typedef PWNDCLASSW PWNDCLASS ; typedef NPWNDCLASSW NPWNDCLASS ; typedef LPWNDCLASSW LPWNDCLASS ; #else typedef WNDCLASSA WNDCLASS ; typedef PWNDCLASSA PWNDCLASS ; typedef NPWNDCLASSA NPWNDCLASS ; typedef LPWNDCLASSA LPWNDCLASS ; #endif

When I show subsequent structures in this book, I'll just show the functionally equivalent definition of the structure, which for WNDCLASS is this:

typedef struct { UINT style ; WNDPROC lpfnWndProc ; int cbClsExtra ; int cbWndExtra ; HINSTANCE hInstance ; HICON hIcon ; HCURSOR hCursor ; HBRUSH hbrBackground ; LPCTSTR lpszMenuName ; LPCTSTR lpszClassName ; } WNDCLASS, * PWNDCLASS ;

I'll also go easy on the various pointer definitions. There's no reason for you to clutter up your code with variable types beginning with LP and NP.

In WinMain, you define a structure of type WNDCLASS, generally like this:

WNDCLASS wndclass ;

You then initialize the 10 fields of the structure and call RegisterClass.

The two most important fields in the WNDCLASS structure are the second and the last. The second field (lpfnWndProc) is the address of a window procedure used for all windows based on this class. In HELLOWIN.C, this window procedure is WndProc. The last field is the text name of the window class. This can be whatever you want. In programs that create only one window, the window class name is commonly set to the name of the program.

The other fields describe some characteristics of the window class, as described below. Let's take a look at each field of the WNDCLASS structure in order.

The statement

wndclass.style = CS_HREDRAW | CS_VREDRAW ;

combines two 32-bit "class style" identifiers with a C bitwise OR operator. The WINUSER.H header files defines a whole collection of identifiers with the CS prefix:

#define CS_VREDRAW 0x0001 #define CS_HREDRAW 0x0002 #define CS_KEYCVTWINDOW 0x0004 #define CS_DBLCLKS 0x0008 #define CS_OWNDC 0x0020 #define CS_CLASSDC 0x0040 #define CS_PARENTDC 0x0080 #define CS_NOKEYCVT 0x0100 #define CS_NOCLOSE 0x0200 #define CS_SAVEBITS 0x0800 #define CS_BYTEALIGNCLIENT 0x1000 #define CS_BYTEALIGNWINDOW 0x2000 #define CS_GLOBALCLASS 0x4000 #define CS_IME 0x00010000

Identifiers defined in this way are often called "bit flags" because each identifier sets a single bit in a composite value. Only a few of these class styles are commonly used. The two identifiers used in HELLOWIN indicate that all windows created based on this class are to be completely repainted whenever the horizontal window size (CS_HREDRAW) or the vertical window size (CS_VREDRAW) changes. If you resize HELLOWIN's window, you'll see that the text string is redrawn to be in the new center of the window. These two identifiers ensure that this happens. We'll see shortly how the window procedure is notified of this change in window size.

The second field of the WNDCLASS structure is initialized by the statement:

wndclass.lpfnWndProc = WndProc ;

This sets the window procedure for this window class to WndProc, which is the second function in HELLOWIN.C. This window procedure will process all messages to all windows created based on this window class. In C, when you use a function name in a statement like this, you're really referring to a pointer to a function.

The next two fields are used to reserve some extra space in the class structure and the window structure that Windows maintains internally:

wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ;

A program can use this extra space for its own purposes. HELLOWIN does not use this feature, so 0 is specified. Otherwise, as the Hungarian notation indicates, the field would be set to a "count of bytes." (I'll use the cbWndExtra field in the CHECKER3 program shown in Chapter 7.)

The next field is simply the instance handle of the program (which is one of the parameters to WinMain):

wndclass.hInstance = hInstance ;

The statement

wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;

sets an icon for all windows created based on this window class. The icon is a small bitmap picture that represents the program to the user. When the program is running, the icon appears in the Windows taskbar and at the left side of the program window's title bar. Later in this book, you'll learn how to create customized icons for your Windows programs. Right now, we'll take an easy approach and use a predefined icon.

To obtain a handle to a predefined icon, you call LoadIcon with the first argument set to NULL. When you're loading your own customized icons that are stored in your program's .EXE file on disk, this argument would be set to hInstance, the instance handle of the program. The second argument identifies the icon. For the predefined icons, this argument is an identifier beginning with the prefix IDI ("ID for an icon") defined in WINUSER.H. The IDI_APPLICATION icon is simply a little picture of a window. The LoadIcon function returns a handle to this icon. We don't really care about the actual value of the handle. It's simply used to set the value of the hIcon field. This field is defined in the WNDCLASS structure to be of type HICON, which stands for "handle to an icon."

The statement

wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;

is similar to the previous statement. The LoadCursor function loads a predefined mouse cursor known as IDC_ARROW and returns a handle to the cursor. This handle is assigned to the bCursor field of the WNDCLASS structure. When the mouse cursor appears over the client area of a window that is created based on this class, the cursor becomes a small arrow.

The next field specifies the background color of the client area of windows created based on this class. The hbr prefix of the hbrBackground field name stands for "handle to a brush." A brush is a graphics term that refers to a colored pattern of pixels used to fill an area. Windows has several standard, or "stock," brushes. The GetStockObject call shown here returns a handle to a white brush:

wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;

This means that the background of the client area of the window will be solid white, which is a common choice.

The next field specifies the window class menu. HELLOWIN has no application menu, so the field is set to NULL:

wndclass.lpszMenuName = NULL ;

Finally the class must be given a name. For a small program, this can be simply the name of the program, which is the "HelloWin" string stored in the szAppName variable.

wndclass.lpszClassName = szAppName ;

This string is composed of either ASCII characters or Unicode characters depending on whether the UNICODE identifier has been defined.

When all 10 fields of the structure have been initialized, HELLOWIN registers the window class by calling RegisterClass. The only argument to the function is a pointer to the WNDCLASS structure. Actually, there's a RegisterClassA function that takes a pointer to the WNDCLASSA structure, and a RegisterClassW function that takes a pointer to the WNDCLASSW structure. Which function the program uses to register the window class determines whether messages sent to the window will contain ASCII text or Unicode text.

Now here's a problem: If you have compiled the program with the UNICODE identifier defined, your program will call RegisterClassW. That's fine if you're running the program on Microsoft Windows NT. But if you're running the program on Windows 98, the RegisterClassW function is not really implemented. There's an entry point for the function, but it just returns a zero from the function call, indicating an error. This is a good opportunity for a Unicode program running under Windows 98 to inform the user of the problem and terminate. Here's the way most of the programs in this book will handle the RegisterClass function call:

if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; }

The MessageBoxW function works properly because it is one of the few Unicode functions implemented in Windows 98.

This code fragment assumes, of course, that RegisterClass is not failing for some other reason, such as a NULL lpfnWndProc field of the WNDCLASS structure. The GetLastError function helps you determine the cause of the error in cases like this. GetLastError is a general-purpose function in Windows to get extended error information when a function call fails. The documentation of the various functions will indicate whether you can use GetLastError to obtain this information. In the case of calling RegisterClassW in Windows 98, GetLastError returns 120. You can look in WINERROR.H to see that the value 120 corresponds to the identifier ERROR_CALL_NOT_IMPLEMENTED. You can also look up the error in /Platform SDK/Windows Base Services/Debugging and Error Handling/Error Codes/System Errors - Numerical Order.

Some Windows programmers like to check the return value of every function call for errors. This certainly makes some sense, and here's why: I'm sure you're familiar with the rule that you always, always check for an error when you're allocating memory. Well, many Windows functions need to allocate some memory. For example, RegisterClass needs to allocate memory to store information about the window class. So you should be checking the function regardless. On the other hand, if RegisterClass fails because it can't allocate the memory it needs, Windows has probably already ground to a halt.

I do a minimum of error checking in the sample programs in this book. This is not because I don't think error checking is a good idea, but because it would distract from what the programs are supposed to illustrate.

Finally, a historical note: In some sample Windows programs, you might see the following code in WinMain:

if (!hPrevInstance) { wndclass.cbStyle = CS_HREDRAW | CS_VREDRAW ; [other wndclass initialization] RegisterClass (&wndclass) ; }

This comes under the category of "old habits die hard." In 16-bit versions of Windows, if you started up a new instance of a program that was already running, the hPrevInstance parameter to WinMain would be the instance handle of the previous instance. To save memory, two or more instances were allowed to share the same window class. Thus, the window class was registered only if hPrevInstance was NULL, indicating that no other instances of the program were running.

In 32-bit versions of Windows, hPrevInstance is always NULL. This code will still work properly, but it's not necessary to check hPrevInstance.

Creating the Window

The window class defines general characteristics of a window, thus allowing the same window class to be used for creating many different windows. When you go ahead and create a window by calling CreateWindow, you specify more detailed information about the window.

Programmers new to Windows are sometimes confused about the distinction between the window class and the window and why all the characteristics of a window can't be specified in one shot. Actually, dividing the information in this way is quite convenient. For example, all push-button windows are created based on the same window class. The window procedure associated with this window class is located inside Windows itself, and it is responsible for processing keyboard and mouse input to the push button and defining the button's visual appearance on the screen. All push buttons work the same way in this respect. But not all push buttons are the same. They almost certainly have different sizes, different locations on the screen, and different text strings. These latter characteristics are part of the window definition rather than the window class definition.

While the information passed to the RegisterClass function is specified in a data structure, the information passed to the CreateWindow function is specified as separate arguments to the function. Here's the CreateWindow call in HELLOWIN.C, complete with comments identifying the fields:

hwnd = CreateWindow (szAppName, // window class name TEXT ("The Hello Program"), // window caption WS_OVERLAPPEDWINDOW, // window style CW_USEDEFAULT, // initial x position CW_USEDEFAULT, // initial y position CW_USEDEFAULT, // initial x size CW_USEDEFAULT, // initial y size NULL, // parent window handle NULL, // window menu handle hInstance, // program instance handle NULL) ; // creation parameters

At this point I won't bother to mention that there are actually a CreateWindowA function and a CreateWindowW function, which treat the first two parameters to the function as ASCII or Unicode, respectively.

The argument marked "window class name" is szAppName, which contains the string "HelloWin"—the name of the window class the program just registered. This is how the window we're creating is associated with a window class.

The window created by this program is a normal overlapped window. It will have a title bar; a system menu button to the left of the title bar; a thick window-sizing border; and minimize, maximize, and close buttons to the right of the title bar. That's a standard style for windows, and it has the name WS_OVERLAPPEDWINDOW, which appears as the "window style" parameter in CreateWindow. If you look in WINUSER.H, you'll find that this style is a combination of several bit flags:

#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | \ WS_CAPTION | \ WS_SYSMENU | \ WS_THICKFRAME | \ WS_MINIMIZEBOX | \ WS_MAXIMIZEBOX)

The "window caption" is the text that will appear in the title bar of the window.

The arguments marked "initial x position" and "initial y position" specify the initial position of the upper left corner of the window relative to the upper left corner of the screen. By using the identifier CW_USEDEFAULT for these parameters, we are indicating that we want Windows to use the default position for an overlapped window. (CW_USEDEFAULT is defined as 0x80000000.) By default, Windows positions successive newly created windows at stepped horizontal and vertical offsets from the upper left corner of the display. Similarly, the "initial x size" and "initial y size" arguments specify the initial width and height of the window. The CW_USEDEFAULT identifier again indicates that we want Windows to use a default size for the window.

The argument marked "parent window handle" is set to NULL when creating a "top-level" window, such as an application window. Normally, when a parent-child relationship exists between two windows, the child window always appears on the surface of its parent. An application window appears on the surface of the desktop window, but you don't need to find out the desktop window's handle to call CreateWindow.

The "window menu handle" is also set to NULL because the window has no menu. The "program instance handle" is set to the instance handle passed to the program as a parameter of WinMain. Finally, a "creation parameters" pointer is set to NULL. You could use this parameter to point to some data that you might later want to reference in your program.

The CreateWindow call returns a handle to the created window. This handle is saved in the variable hwnd, which is defined to be of type HWND ("handle to a window"). Every window in Windows has a handle. Your program uses the handle to refer to the window. Many Windows functions require hwnd as an argument so that Windows knows which window the function applies to. If a program creates many windows, each has a different handle. The handle to a window is one of the most important handles that a Windows program (pardon the expression) handles.

Displaying the Window

After the CreateWindow call returns, the window has been created internally in Windows. What this means basically is that Windows has allocated a block of memory to hold all the information about the window that you specified in the CreateWindow call, plus some other information, all of which Windows can find later based on the window handle.

However, the window does not yet appear on the video display. Two more calls are needed. The first is

ShowWindow (hwnd, iCmdShow) ;

The first argument is the handle to the window just created by CreateWindow. The second argument is the iCmdShow value passed as a parameter to WinMain. This determines how the window is to be initially displayed on the screen, whether it's normal, minimized, or maximized. The user probably selected a preference when adding the program to the Start menu. The value you receive from WinMain and pass to ShowWindow is SW_SHOWNORMAL if the window is displayed normally, SW_SHOWMAXIMIZED if the window is to be maximized, and SW_SHOWMINNOACTIVE if the window is just to be displayed in the taskbar.

The ShowWindow function puts the window on the display. If the second argument to ShowWindow is SW_SHOWNORMAL, the client area of the window is erased with the background brush specified in the window class. The function call

UpdateWindow (hwnd) ;

then causes the client area to be painted. It accomplishes this by sending the window procedure (that is, the WndProc function in HELLOWIN.C) a WM_PAINT message. We'll soon examine how WndProc deals with this message.

The Message Loop

After the UpdateWindow call, the window is fully visible on the video display. The program must now make itself ready to read keyboard and mouse input from the user. Windows maintains a "message queue" for each Windows program currently running under Windows. When an input event occurs, Windows translates the event into a "message" that it places in the program's message queue.

A program retrieves these messages from the message queue by executing a block of code known as the "message loop":

while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; }

The msg variable is a structure of type MSG, which is defined in the WINUSER.H header file like this:

typedef struct tagMSG { HWND hwnd ; UINT message ; WPARAM wParam ; LPARAM lParam ; DWORD time ; POINT pt ; } MSG, * PMSG ;

The POINT data type is yet another structure, defined in the WINDEF.H header file like this:

typedef struct tagPOINT { LONG x ; LONG y ; } POINT, * PPOINT;

The GetMessage call that begins the message loop retrieves a message from the message queue:

GetMessage (&msg, NULL, 0, 0)

This call passes to Windows a pointer to a MSG structure named msg. The second, third, and fourth arguments are set to NULL or 0 to indicate that the program wants all messages for all windows created by the program. Windows fills in the fields of the message structure with the next message from the message queue. The fields of this structure are:

If the message field of the message retrieved from the message queue is anything except WM_QUIT (which equals 0x0012), GetMessage returns a nonzero value. A WM_QUIT message causes GetMessage to return 0.

The statement:

TranslateMessage (&msg) ;

passes the msg structure back to Windows for some keyboard translation. (I'll discuss this more in Chapter 6.) The statement

DispatchMessage (&msg) ;

again passes the msg structure back to Windows. Windows then sends the message to the appropriate window procedure for processing. What this means is that Windows calls the window procedure. In HELLOWIN, the window procedure is WndProc. After WndProc processes the message, it returns control to Windows, which is still servicing the DispatchMessage call. When Windows returns to HELLOWIN following the DispatchMessage call, the message loop continues with the next GetMessage call.

The Window Procedure

All that I've described so far is really just overhead. The window class has been registered, the window has been created, the window has been displayed on the screen, and the program has entered a message loop to retrieve messages from the message queue.

The real action occurs in the window procedure. The window procedure determines what the window displays in its client area and how the window responds to user input.

In HELLOWIN, the window procedure is the function named WndProc. A window procedure can have any name (as long as it doesn't conflict with some other name, of course). A Windows program can contain more than one window procedure. A window procedure is always associated with a particular window class that you register by calling RegisterClass. The CreateWindow function creates a window based on a particular window class. More than one window can be created based on the same window class.

A window procedure is always defined like this:

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

The four parameters to the window procedure are identical to the first four fields of the MSG structure. The first parameter is hwnd, the handle to the window receiving the message. This is the same handle returned from the CreateWindow function. For a program like HELLOWIN, which creates only one window, this is the only window handle the program knows about. If a program creates multiple windows based on the same window class (and hence the same window procedure), hwnd identifies the particular window receiving the message.

The second parameter is the same as the message field in the MSG structure. It's a number that identifies the message. The last two parameters are 32-bit message parameters that provide more information about the message. What these parameters contain is specific to each type of message. Sometimes a message parameter is two 16-bit values stuck together, and sometimes a message parameter is a pointer to a text string or to a data structure.

Programs generally don't call window procedures directly. The window procedure is almost always called from Windows itself. A program can indirectly call its own window procedure by calling a function named SendMessage, which we'll examine in later chapters.

Processing the Messages

Every message that a window procedure receives is identified by a number, which is the message parameter to the window procedure. The Windows header file WINUSER.H defines identifiers beginning with the prefix WM ("window message") for each type of message.

Generally, Windows programmers use a switch and case construction to determine what message the window procedure is receiving and how to process it accordingly. When a window procedure processes a message, it should return 0 from the window procedure. All messages that a window procedure chooses not to process must be passed to a Windows function named DefWindowProc. The value returned from DefWindowProc must be returned from the window procedure.

In HELLOWIN, WndProc chooses to process only three messages: WM_CREATE, WM_PAINT, and WM_DESTROY. The window procedure is structured like this:

switch (iMsg) { case WM_CREATE : [process WM_CREATE message] return 0 ; case WM_PAINT : [process WM_PAINT message] return 0 ; case WM_DESTROY : [process WM_DESTROY message] return 0 ; } return DefWindowProc (hwnd, iMsg, wParam, lParam) ;

It is important to call DefWindowProc for default processing of all messages that your window procedure does not process. Otherwise behavior regarded as normal, such as being able to terminate the program, will not work.

Playing a Sound File

The very first message that a window procedure receives—and the first that HELLOWIN's WndProc chooses to process—is WM_CREATE. WndProc receives this message while Windows is processing the CreateWindow function in WinMain. That is, when HELLOWIN calls CreateWindow, Windows does what it has to do and, in the process, Windows calls WndProc with the first argument set to the window handle and the second argument set to WM_CREATE (the value 1). WndProc processes the WM_CREATE message and returns controls back to Windows. Windows can then return to HELLOWIN from the CreateWindow call to continue further progress in WinMain.

Often a window procedure performs one-time window initialization during WM_CREATE processing. HELLOWIN chooses to process this message by playing a waveform sound file named HELLOWIN.WAV. It does this using the simple PlaySound function, which is described in /Platform SDK/Graphics and Multimedia Services/Multimedia Audio/Waveform Audio and documented in /Platform SDK/Graphics and Multimedia Services/Multimedia Reference/Multimedia Functions.

The first argument to PlaySound is the name of a waveform file. (It could also be a sound alias name defined in the Sounds section of the Control Panel or a program resource.) The second argument is used only if the sound file is a resource. The third argument specifies a couple of options. In this case, I've indicated that the first argument is a filename and that the sound is to be played asynchronously—that is, the PlaySound function call is to return as soon as the sound file starts playing without waiting for it to complete. That way the program can continue with its initialization.

WndProc concludes WM_CREATE processing by returning 0 from the window procedure.

The WM_PAINT Message

The second message that WndProc processes is WM_PAINT. This message is extremely important in Windows programming. It informs a program when part or all of the window's client area is "invalid" and must be "updated," which means that it must be redrawn or "painted."

How does a client area become invalid? When the window is first created, the entire client area is invalid because the program has not yet drawn anything on the window. The first WM_PAINT message (which normally occurs when the program calls UpdateWindow in WinMain) directs the window procedure to draw something on the client area.

When you resize HELLOWIN's window, the client area becomes invalid. You'll recall that the style field of HELLOWIN's wndclass structure was set to the flags CS_HREDRAW and CS_VREDRAW. This directs Windows to invalidate the whole window when the size changes. The window procedure then receives a WM_PAINT message.

When you minimize HELLOWIN and then restore the window again to its previous size, Windows does not save the contents of the client area. Under a graphical environment, this would be too much data to retain. Instead, Windows invalidates the window. The window procedure receives a WM_PAINT message and itself restores the contents of its window.

When you move windows around the screen so that they overlap, Windows does not save the area of a window covered by another window. When that area of the window is later uncovered, it is flagged as invalid. The window procedure receives a WM_PAINT message to repaint the contents of the window.

WM_PAINT processing almost always begins with a call to BeginPaint:

hdc = BeginPaint (hwnd, &ps) ;

and ends with a call to EndPaint:

EndPaint (hwnd, &ps) ;

In both cases, the first argument is a handle to the program's window, and the second argument is a pointer to a structure of type PAINTSTRUCT. The PAINTSTRUCT structure contains some information that a window procedure can use for painting the client area. I'll discuss the fields of this structure in the next chapter; for now, we'll just use it in the BeginPaint and EndPaint functions.

During the BeginPaint call, Windows erases the background of the client area if it hasn't been erased already. It erases the background using the brush specified in the hbrBackground field of the WNDCLASS structure used to register the window class. In the case of HELLOWIN, this is a stock white brush, which means that Windows erases the background of the window by coloring it white. The BeginPaint call validates the entire client area and returns a "handle to a device context." A device context refers to a physical output device (such as a video display) and its device driver. You need the device context handle to display text and graphics in the client area of a window. Using the device context handle returned from BeginPaint, you cannot draw outside the client area, even if you try. EndPaint releases the device context handle so that it is no longer valid.

If a window procedure does not process WM_PAINT messages (which is very rare), they must be passed on to DefWindowProc. DefWindowProc simply calls BeginPaint and EndPaint in succession so that the client area is validated.

After WndProc calls BeginPaint, it calls GetClientRect:

GetClientRect (hwnd, &rect) ;

The first argument is the handle to the program's window. The second argument is a pointer to a rectangle structure of type RECT. This structure has four LONG fields named left, top, right, and bottom. The GetClientRect function sets these four fields to the dimensions of the client area of the window. The left and top fields are always set to 0. Thus, the right and bottom fields represent the width and height of the client area in pixels.

WndProc doesn't do anything with this RECT structure except pass a pointer to it as the fourth argument to DrawText:

DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;

DrawText, as the name implies, draws text. Because this function draws something, the first argument is a handle to the device context returned from BeginPaint. The second argument is the text to draw, and the third argument is set to -1 to indicate that the text string is terminated with a zero character.

The last argument to DrawText is a series of bit flags defined in WINUSER.H. (Although DrawText seems to be a GDI function call because it displays output, it's actually considered part of the User module because it's a fairly high-level drawing function. The function is documented in /Platform SDK/Graphics and Multimedia Services/GDI/Fonts and Text.) The flags indicate that the text should be displayed as a single line centered horizontally and vertically within the rectangle specified by the fourth argument. This function call thus causes the string "Hello, Windows 98!" to be displayed centered in the client area.

Whenever the client area becomes invalid (as it does when you change the size of the window), WndProc receives a new WM_PAINT message. WndProc obtains the updated window size by calling GetClientRect and again displays the text in the next center of the window.

The WM_DESTROY Message

The WM_DESTROY message is another important message. This message indicates that Windows is in the process of destroying a window based on a command from the user. The message is a result of the user clicking on the Close button or selecting Close from the program's system menu. (Later in this chapter, I'll discuss in more detail how the WM_DESTROY message gets generated.)

HELLOWIN responds to the WM_DESTROY message in a standard way by calling

PostQuitMessage (0) ;

This function inserts a WM_QUIT message in the program's message queue. I mentioned earlier that GetMessage returns nonzero for any message other than WM_QUIT that it retrieves from the message queue. When GetMessage retrieves a WM_QUIT message, GetMessage returns 0. This causes WinMain to drop out of the message loop. The program then executes the following statement:

return msg.wParam ;

The wParam field of the structure is the value passed to the PostQuitMessage function (generally 0). The return statement exits from WinMain and terminates the program.

Категории