Programming Windows with MFC, Second Edition
Programs written for traditional operating environments use a procedural programming model in which programs execute from top to bottom in an orderly fashion. The path taken from start to finish may vary with each invocation of the program depending on the input it receives or the conditions under which it is run, but the path remains fairly predictable. In a C program, execution begins with the first line in the function named main and ends when main returns. In between, main might call other functions and these functions might call even more functions, but ultimately it is the program—not the operating system—that determines what gets called and when.
Windows programs operate differently. They use the event-driven programming model illustrated in Figure 1-1, in which applications respond to events by processing messages sent by the operating system. An event could be a keystroke, a mouse click, or a command for a window to repaint itself, among other things. The entry point for a Windows program is a function named WinMain, but most of the action takes place in a function known as the window procedure. The window procedure processes messages sent to the window. WinMain creates that window and then enters a message loop, alternately retrieving messages and dispatching them to the window procedure. Messages wait in a message queue until they are retrieved. A typical Windows application performs the bulk of its processing in response to the messages it receives, and in between messages, it does little except wait for the next message to arrive.
The message loop ends when a WM_QUIT message is retrieved from the message queue, signaling that it's time for the application to end. This message usually appears because the user selected Exit from the File menu, clicked the close button (the small button with an X in the window's upper right corner), or selected Close from the window's system menu. When the message loop ends, WinMain returns and the application terminates.
Figure 1-1. The Windows programming model.
The window procedure typically calls other functions to help process the messages it receives. It can call functions local to the application, or it can call API functions provided by Windows. API functions are contained in special modules known as dynamic-link libraries, or DLLs. The Win32 API includes hundreds of functions that an application can call to perform various tasks such as creating a window, drawing a line, and performing file input and output. In C, the window procedure is typically implemented as a monolithic function containing a large switch statement with cases for individual messages. The code provided to process a particular message is known as a message handler. Messages that an application doesn't process are passed on to an API function named DefWindowProc, which provides default responses to unprocessed messages.
Messages, Messages, and More Messages
Where do messages come from, and what kinds of information do they convey? Windows defines hundreds of different message types. Most messages have names that begin with the letters "WM" and an underscore, as in WM_CREATE and WM_PAINT. These messages can be classified in various ways, but for the moment classification is not nearly as important as realizing the critical role messages play in the operation of an application. The following table shows 10 of the most common messages. A window receives a WM_PAINT message, for example, when its interior needs repainting. One way to characterize a Windows program is to think of it as a collection of message handlers. To a large extent, it is a program's unique way of responding to messages that gives it its personality.
Common Windows Messages
Message | Sent When |
---|---|
WM_CHAR | A character is input from the keyboard. |
WM_COMMAND | The user selects an item from a menu, or a control sends a notification to its parent. |
WM_CREATE | A window is created. |
WM_DESTROY | A window is destroyed. |
WM_LBUTTONDOWN | The left mouse button is pressed. |
WM_LBUTTONUP | The left mouse button is released. |
WM_MOUSEMOVE | The mouse pointer is moved. |
WM_PAINT | A window needs repainting. |
WM_QUIT | The application is about to terminate. |
WM_SIZE | A window is resized. |
A message manifests itself in the form of a call to a window's window procedure. Bundled with the call are four input parameters: the handle of the window to which the message is directed, a message ID, and two 32-bit parameters known as wParam and lParam. The window handle is a 32-bit value that uniquely identifies a window. Internally, the value references a data structure in which Windows stores relevant information about the window such as its size, style, and location on the screen. The message ID is a numeric value that identifies the message type: WM_CREATE, WM_PAINT, and so on. wParam and lParam contain information specific to the message type. When a WM_LBUTTONDOWN message arrives, for example, wParam holds a series of bit flags identifying the state of the Ctrl and Shift keys and of the mouse buttons. lParam holds two 16-bit values identifying the location of the mouse pointer when the click occurred. Together, these parameters provide the window procedure with all the information it needs to process the WM_LBUTTONDOWN message.
Windows Programming, SDK-Style
If you haven't programmed Windows in C before, it's instructive to see what the source code for a simple program looks like. The program listed in Figure 1-2 creates a window and responds to WM_PAINT messages by drawing an ellipse in the window's upper left corner. This code is similar to the source code you'll find in books such as Charles Petzold's Programming Windows (1998, Microsoft Press) and other books that teach Windows programming in C.
Figure 1-2. C source code for a simple Windows program.
#include <windows.h> LONG WINAPI WndProc (HWND, UINT, WPARAM, LPARAM); int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow) { WNDCLASS wc; HWND hwnd; MSG msg; wc.style = 0; // Class style wc.lpfnWndProc = (WNDPROC) WndProc; // Window procedure address wc.cbClsExtra = 0; // Class extra bytes wc.cbWndExtra = 0; // Window extra bytes wc.hInstance = hInstance; // Instance handle wc.hIcon = LoadIcon (NULL, IDI_WINLOGO); // Icon handle wc.hCursor = LoadCursor (NULL, IDC_ARROW); // Cursor handle wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1); // Background color wc.lpszMenuName = NULL; // Menu name wc.lpszClassName = "MyWndClass"; // WNDCLASS name RegisterClass (&wc); hwnd = CreateWindow ( "MyWndClass", // WNDCLASS name "SDK Application", // Window title WS_OVERLAPPEDWINDOW, // Window style CW_USEDEFAULT, // Horizontal position CW_USEDEFAULT, // Vertical position CW_USEDEFAULT, // Initial width CW_USEDEFAULT, // Initial height HWND_DESKTOP, // Handle of parent window NULL, // Menu handle hInstance, // Application's instance handle NULL // Window-creation data ); ShowWindow (hwnd, nCmdShow); 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) { PAINTSTRUCT ps; HDC hdc; switch (message) { case WM_PAINT: hdc = BeginPaint (hwnd, &ps); Ellipse (hdc, 0, 0, 200, 100); EndPaint (hwnd, &ps); return 0; case WM_DESTROY: PostQuitMessage (0); return 0; } return DefWindowProc (hwnd, message, wParam, lParam); } |
WinMain begins by calling the API function RegisterClass to register a window class. The window class defines important characteristics of a window such as its window procedure address, its default background color, and its icon. These and other properties are defined by filling in the fields of a WNDCLASS structure, which is subsequently passed to RegisterClass. An application must specify a window class when it creates a window, and a class must be registered before it can be used. That's why RegisterClass is called at the outset of the program. Keep in mind that a WNDCLASS-type window class is not the same as a C++ window class. To avoid confusion, I'll use the term WNDCLASS throughout this book to refer to classes registered with RegisterClass. The term window class will refer to C++ classes derived from MFC's CWnd class.
Once the WNDCLASS is registered, WinMain calls the all-important CreateWindow function to create the application's window. The first parameter to CreateWindow is the name of the WNDCLASS from which the window will be created. The second parameter is the text that will appear in the window's title bar. The third specifies the window style. WS_OVERLAPPEDWINDOW is a commonly used style that creates a top-level window with a resizing border, a title bar, a system menu, and buttons for minimizing, maximizing, and closing the window.
The next four parameters specify the window's initial position and size. CW_USEDEFAULT tells Windows to use default values for both. The final four parameters specify, in order, the handle of the window's parent window (HWND_DESKTOP for an application's main window); the handle of the menu associated with the window, if any; the application's instance handle (a value that lets the programmer differentiate between the program itself and the modules—that is, DLLs—that it loads); and a pointer to application-specific window-creation data. I could easily devote a section of this book to CreateWindow and its parameters, but as you'll see later, MFC hides much of this detail inside the class library. A typical MFC application doesn't have a WinMain function (at least not one you can see), and it doesn't call RegisterClass or CreateWindow.
The window that CreateWindow creates is not initially visible on the screen because it was not created with the WS_VISIBLE style. (Had it been used, WS_VISIBLE would have been combined with WS_OVERLAPPEDWINDOW in the call to CreateWindow.) Therefore, WinMain follows CreateWindow with calls to ShowWindow and UpdateWindow, which make the window visible and ensure that its WM_PAINT handler is called immediately.
Next comes the message loop. In order to retrieve and dispatch messages, WinMain executes a simple while loop that calls the GetMessage, TranslateMessage, and DispatchMessage API functions repeatedly. GetMessage checks the message queue. If a message is available, it is removed from the queue and copied to msg; otherwise, GetMessage blocks on the empty message queue until a message is available. msg is an instance of the structure MSG, whose fields contain pertinent message parameters such as the message ID and the time at which the message was placed in the queue. TranslateMessage converts a keyboard message denoting a character key to an easier-to-use WM_CHAR message, and DispatchMessage dispatches the message to the window procedure. The message loop executes until GetMessage returns 0, which happens only when a WM_QUIT message is retrieved from the message queue. When this occurs, WinMain ends and the program terminates.
Messages dispatched with DispatchMessage generate calls to the window procedure WndProc. The sample program in Figure 1-2 processes just two message types, WM_PAINT and WM_DESTROY; all other messages are passed to DefWindowProc for default processing. A switch-case block inspects the message ID passed in the message parameter and executes the appropriate message handler. The WM_PAINT handler calls the BeginPaint API function to obtain a device context handle before painting begins and the EndPaint API function to release the handle when painting is finished. In between, the Ellipse API function draws an ellipse that is 200 pixels wide and 100 pixels high. A device context handle is the "magic cookie" that permits a Windows application to draw on the screen. Without it, functions such as Ellipse won't work.
The WM_DESTROY handler calls the PostQuitMessage API function to post a WM_QUIT message to the message queue and ultimately cause the program to terminate. The WM_DESTROY message is sent to a window just before it is destroyed. A top-level window must call PostQuitMessage when it receives a WM_DESTROY message, or else the message loop will not fall through and the program will never end.
Hungarian Notation and Windows Data Types
Another aspect of Figure 1-2 that deserves mentioning is the variable naming convention that it uses. Veteran Windows programmers know it as Hungarian notation, in which each variable name begins with one or more lowercase characters identifying the variable's type: h for handle, n for integer, and so on. The table below lists some of the commonly used Hungarian prefixes. Prefixes are often combined to form other prefixes, as when p and sz are joined to form psz, which stands for "pointer to zero-terminated string."
Many of the data types shown in this table aren't standard C/C++ data types but rather are "special" data types defined in the Windows header files. COLORREF, for example, is the Windows data type for 24-bit RGB color values. A BOOL is a Boolean data type that stores TRUE/FALSE values, while a DWORD is a 32-bit unsigned integer. Over time, you'll come to know these data types as well as you know your compiler's native data types.
Common Hungarian Notation Prefixes
Prefix | Data Type |
---|---|
b | BOOL |
c or ch | char |
clr | COLORREF |
cx, cy | Horizontal or vertical distance |
dw | DWORD |
h | Handle |
l | LONG |
n | int |
p | Pointer |
sz | Zero-terminated string |
w | WORD |
Most MFC programmers use Hungarian notation, too. Glance through the source code for a typical MFC program and you'll see hundreds of hs and lps and other familiar prefixes as well as prefixes representing MFC's own data types (for example, wnd for CWnd variables). It's also common to prefix member variables with m_ so that it's obvious whether a variable is a member of a class. A temporary CString variable created on the stack might have the name strWndClass, but if it's a member variable it will probably be called m_strWndClass. You don't have to abide by these rules yourself, of course, but observing established naming conventions will make your code more readable to other programmers who do.
SDK Programming in Perspective
All this is a lot to digest if you've never programmed Windows before, but it brings to light a few very important concepts. First, Windows is an event-driven, message-based operating system. Messages are the key to everything that goes on in the system, and for an application, very few things happen that aren't the direct result of receiving a message. Second, there are many different API functions and many different message types, which complicates application development and makes it hard to predict all of the scenarios an application might encounter. Third, seeing how Windows programming is done the hard way provides a baseline for evaluating MFC and other class libraries. MFC is not the panacea some of its proponents would have you believe, but it undeniably makes certain aspects of Windows programming easier. And the higher order it lends to Windows programs frees programmers to spend more time developing the structural components of a program and less time worrying about the style bits passed to CreateWindow and other nuances of the API. If you haven't given MFC a look, now is the time to consider it. Windows programming isn't getting any easier, and MFC lets you benefit from tens of thousands of lines of code already written and tested by Microsoft.