The Windows portion of the code for a typical application consists of the following code blocks:
Let's walk through the implementation of each of these components of our Windows-related code. If you installed the sample code from the companion CD and want to follow the code that this chapter discusses, select Open Workspace from the File menu in Microsoft Visual C++, find and select roadrage.dsw, and click Open. Roadrage.dsw is the workspace for the entire book. To make Chapter 2's code the active project, select the FileView tab in the Workspace window, right-click Chap2 Files, and select Set As Active Project from the context menu.
Programs that use DirectX need certain GUIDs (globally unique IDs, such as IID_IDirectDraw7) to be defined so that they can compile successfully. GUIDs are global variables rather than constants, so you must define storage for them. One way to incorporate the required GUIDs into your program is to include dxguid.lib in your libraries when building the project. If you prefer, you can instead define the symbol INITGUID as follows in one of your source modules before you include the header files windows.h, ddraw.h, or d3d.h, and before you use any other #define directives:
#define INITGUID |
If you forget to define INITGUID or include the dxguid.lib library, you'll be deluged with errors when you attempt to compile your DirectX code.
TIP
To compile a program that uses DirectX, you need to have your compiler use the directories containing the latest DirectX include and library files. To verify that Visual C++ is configured correctly, choose Options from the Tools menu and then click the Directories tab. Make sure that the path to the DirectX include files (something like C:\mssdk\include) is at the top of the directory list. Then use the Show Directories For control to switch to the Library Files section and make sure that the path to the DirectX library files (something like C:\mssdk\lib) is at the top of the directory list. If you don't do this, Visual C++ will use the DirectX header files and library files that shipped with Visual C++, which will usually be the wrong version.
The include files are specified at the top of the roadrage.cpp file (as they are in any C++ module). You can find the header files resource.h, d3dapp.h, and roadrage.hpp in this chapter's project directory. Resource.h defines the Windows resources that the RoadRage program uses. D3dapp.h is the header file for the d3dapp.cpp file that contains most of the code presented in this chapter and that will contain the main-event-handling code and rendering-related code added to RoadRage in later chapters.
The class CMyD3DApplication, defined in roadrage.hpp, will encapsulate the RoadRage code that handles all the main tasks, including 3D world creation and rendering as well as user control of the 3D world, sound, and multiplayer game play. CMyD3DApplication uses the DirectInput, DirectSound, and DirectPlay components of DirectX and inherits its basic attributes from the CD3DApplication class.
The following code (from roadrage.hpp and roadrage.cpp) defines the CMyD3DApplication class, a default constructor for the class, and the WinMain function (the main entry point for the application).
#ifndef __ROADRAGE_H #define __ROADRAGE_H //------------------------------------------------------------------- // Name: class CMyD3DApplication // Desc: Application class. The base class provides just about all // the functionality we want, so we're just supplying stubs to // interface with the non-C++ functions of the application. //------------------------------------------------------------------- class CMyD3DApplication : public CD3DApplication { public: CMyD3DApplication(); void DisplayCredits(HWND hwnd); void DisplayRRStats(HWND hwnd); void DisplayLegalInfo(HWND hwnd); LRESULT MsgProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ); static HRESULT hr; HINSTANCE hInstApp; }; #endif //__ROADRAGE_H ///------------------------------------------------------------------ // File: RoadRage.cpp // // Desc: The main RoadRage code. The CMyD3DApplication class handles // most of the RoadRage functionality. // // Copyright (c) 1999 William Chin and Peter Kovach. All rights // reserved. //------------------------------------------------------------------- #define STRICT #define D3D_OVERLOADS #include <math.h> #include <time.h> #include <stdio.h> #include "D3DApp.h" //------------------------------------------------------------------- // Name: WinMain // Desc: Entry point to the program. Initializes everything, and goes // into a message-processing loop. Idle time is used to render // the scene. //------------------------------------------------------------------- INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT ) { CMyD3DApplication d3dApp; d3dApp.hInstApp = hInst; if( FAILED( d3dApp.Create( hInst, strCmdLine ) ) ) return 0; d3dApp.Run(); CoUninitialize(); return TRUE; } //------------------------------------------------------------------- // Name: CMyD3DApplication // Desc: Application constructor. Sets attributes for the // application. //------------------------------------------------------------------- CMyD3DApplication::CMyD3DApplication() { m_strWindowTitle = TEXT( "Chapter 2" ); pCMyApp = this; } |
The CD3DApplication class from which the CMyD3DApplication class is derived is part of the Direct3D Framework and provides several member variables we'll use. CD3DApplication also supplies methods for creating the new application and handling many of the Windows messages the application will receive. In this chapter, we'll use a simplified version of the CD3DApplication class rather than the full version as specified in the standard Direct3D Framework. The CD3DApplication class is defined in file d3dapp.h, which you'll find in this chapter's project folder, as follows:
//------------------------------------------------------------------- // File: D3DApp.h // // Desc: Application class for the Direct3D samples framework // library // //------------------------------------------------------------------- #ifndef D3DAPP_H #define D3DAPP_H #define D3D_OVERLOADS #include <d3d.h> //------------------------------------------------------------------- // Name: Class CD3DApplication // Desc: //------------------------------------------------------------------- class CD3DApplication { // Internal variables and member functions BOOL m_bActive; BOOL m_bReady; protected: HWND m_hWnd; // Overridable variables for the application TCHAR* m_strWindowTitle; // Overridable power management (APM) functions virtual LRESULT OnQuerySuspend( DWORD dwFlags ); virtual LRESULT OnResumeSuspend( DWORD dwData ); public: // Functions to create, run, pause, and clean up the application virtual HRESULT Create( HINSTANCE, LPSTR ); virtual INT Run(); virtual LRESULT MsgProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ); // Accessor functions HWND Get_hWnd() { return m_hWnd; }; BOOL GetbActive() { return m_bActive; }; BOOL GetbReady() { return m_bReady; }; VOID SetbReady(BOOL val) { m_bReady = val; }; VOID SetbActive(BOOL val) { m_bActive = val; }; // Class constructor CD3DApplication(); }; #endif // D3DAPP_H |
The WinMain routine is always the main function of a Windows-based application. The system calls this function as the initial entry point of the application.
Our implementation of WinMain uses CMyD3DApplication's default constructor to create the application's instance of the CMyD3DApplication class and then calls the class's Create method to generate an instance of the application's main window. CMyD3DApplication inherits its Create method from CD3DApplication and uses CD3DApplication's constructor. Here's the constructor for the CD3DApplication class:
//------------------------------------------------------------------- // Name: CD3DApplication // Desc: Constructor for class CD3DApplication //------------------------------------------------------------------- CD3DApplication::CD3DApplication() { m_hWnd = NULL; m_bActive = FALSE; m_bReady = FALSE; m_strWindowTitle = _T( "Direct3D Application" ); g_pD3DApp = this; } |
This constructor initializes the member variables for our application whenever we create an instance of this class.
The CD3DApplication::Create method is defined as follows:
//------------------------------------------------------------------- // Name: Create // Desc: Creates the application's main window //------------------------------------------------------------------- HRESULT CD3DApplication::Create( HINSTANCE hInst, CHAR* strCmdLine ) { // Register the window class. WNDCLASS wndClass = { 0, WndProc, 0, 0, hInst, LoadIcon( hInst, MAKEINTRESOURCE(IDI_MAIN_ICON) ), LoadCursor( NULL, IDC_ARROW ), (HBRUSH)GetStockObject(WHITE_BRUSH), NULL, _T("D3D Window") }; RegisterClass( &wndClass ); // Create the render window. m_hWnd = CreateWindow( _T("D3D Window"), m_strWindowTitle, WS_OVERLAPPEDWINDOW|WS_VISIBLE, CW_USEDEFAULT,CW_USEDEFAULT, 640, 480, 0L, LoadMenu(hInst,MAKEINTRESOURCE(IDR_MENU)), hInst, 0L ); if (!m_hWnd) { LPVOID lpMsgBuf; FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, GetLastError(), // Default language MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMsgBuf, 0, NULL); // // Process any inserts in lpMsgBuf. // ... // Display the string. // MessageBox( NULL, (LPCTSTR)lpMsgBuf, "Error", MB_OK | MB_ICONINFORMATION ); // Free the buffer. LocalFree( lpMsgBuf ); } UpdateWindow( m_hWnd ); // The application is ready to go. m_bReady = TRUE; return S_OK; } |
This Create method uses the WNDCLASS type to create a window class, specifies attributes for the window class, uses the RegisterClass function to register the window class, and creates a window based on the registered window class by using the CreateWindow function. The window class's attributes include items such as the window's cursor, its icon, and its menu. CD3DApplication::Create uses the following functions to fill in and register the WNDCLASS structure:
TIP
You don't need to delete stock objects using a call to DeleteObject, but if you do, no error will occur. Also keep in mind that you can't adjust stock brush origins and that NULL_BRUSH and HOLLOW_BRUSH objects are the same.
With the window class defined, the next step is to create the window. Creating a window is fairly straightforward, though the creation command includes a variety of parameters. CD3DApplication::Create uses the Windows function CreateWindow to create the main window. The CreateWindow function is defined as follows:
HWND CreateWindow( LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam ); |
Parameter | Description |
---|---|
lpClassName | Pointer to a null-terminated string containing a registered class name |
lpWindowName | Pointer to a null-terminated string containing the window name |
dwStyle | Window style |
x | Horizontal position of window |
y | Vertical position of window |
nWidth | Window width |
nHeight | Window height |
hWndParent | Handle to parent or owner window |
hMenu | Menu handle or child identifier |
hInstance | Handle to application instance (ignored in Windows 2000) |
lpParam | Window-creation data |
In any Windows-based application, you need to define the message-processing loop that the application uses to process any Windows-based messages it receives. The routine that does this processing for RoadRage, CD3DApplication::Run, follows:
//------------------------------------------------------------------- // Name: Run // Desc: Message-processing loop. Uses idle time to render the scene. //------------------------------------------------------------------- INT CD3DApplication::Run() { // Load keyboard accelerators. HACCEL hAccel = LoadAccelerators( NULL, MAKEINTRESOURCE( IDR_MAIN_ACCEL ) ); // Now we're ready to receive and process Windows messages. BOOL bGotMsg; MSG msg; PeekMessage( &msg, NULL, 0U, 0U, PM_NOREMOVE ); while( WM_QUIT != msg.message ) { // Use PeekMessage if the application is active so that we // can use idle time to render the scene. If the application // isn't active, use GetMessage to avoid eating CPU time. if( m_bActive ) bGotMsg = PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ); else bGotMsg = GetMessage( &msg, NULL, 0U, 0U ); if( bGotMsg ) { // Translate and dispatch the message. if( 0 == TranslateAccelerator( m_hWnd, hAccel, &msg ) ) { TranslateMessage( &msg ); DispatchMessage( &msg ); } } } return msg.wParam; } |
The first call in this routine, made before entering the message-processing loop, is to LoadAccelerators. Here's the function declaration for LoadAccelators:
HACCEL LoadAccelerators( HINSTANCE hInstance, LPCTSTR lpTableName ); |
Parameter | Description |
---|---|
hInstance | The handle of the application instance |
lpTableName | The address of the table name string |
This routine is used to load the application's accelerator table. In Windows, an accelerator table defines the shortcuts used to access the various menu options.
Figure 2-1 shows the Microsoft Visual C++ resource editor window presented when you're constructing a menu.
Figure 2-1 Building the application's menu
To add a new menu in Visual C++, select Resource from the Insert menu; the Insert Resource dialog box shown in Figure 2-2 appears. In the Insert Resource dialog box, select Menu from the Resource Type control and click the New button. You can name the menu whatever you want.
Figure 2-2 Inserting a menu resource into an application's menu
Figure 2-3 shows the Visual C++ resource editor displaying the accelerator table defined for the RoadRage application. You can add or modify these entries if you want to. In the right pane, the ID field displays the names of the menu items that can be activated, the Key field shows the keystroke sequence for the shortcut, and the Type field displays the key type (either ASCII or VIRTKEY).
Figure 2-3 Setting the menu's shortcuts
After you've created the menu, you need to define how to handle the messages that this menu and the various window objects generate. The message retrieval and dispatch loop, which acts on each event that occurs, is the heart of the program. When the system receives the WM_QUIT message, it terminates the program, with WinMain returning the value passed in the message's wParam parameter. If the application terminates before entering the message loop, it returns a value of FALSE.
Once you've defined the accelerator table, you can begin the message-handling loop, which consists of the following steps:
Let's look at each of these commands individually to make sure that you understand their usage and the reason they're executed in this sequence.
The PeekMessage function is declared as follows:
BOOL PeekMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg ); |
Parameter | Description |
---|---|
lpMsg | A pointer from the message queue to the message's structure. |
hWnd | The handle to the window whose message you want. |
wMsgFilterMin | The value of the first message in the range of messages you want to examine. |
wMsgFilterMax | The value of the last message in the range of messages you want to examine. |
wRemoveMsg | The removal flag: PM_NOREMOVE indicates that the message shouldn't be removed after the call to PeekMessage, and PM_REMOVE indicates that it should be removed. |
PeekMessage is used to acquire a message from the message queue. The one argument that might need a bit of extra explanation is the final wRemoveMsg parameter. For nearly every application, you'll use the PM_REMOVE flag, which requests that the message be removed once it's handled. This flag is used to make sure that once a message is handled, it's removed from the queue so that the next message can be received. You'd rarely choose not to have the handled message removed. However, imagine that you've written an application such that when the code determines that it can't immediately process a message, but expects to be able to process the message within a short period of time, the code doesn't remove the message from the queue and simply tries to process the message again later (in a few milliseconds). That code would need to use the PM_NOREMOVE flag. GetMessage is just like PeekMessage except that if no messages are in the queue the function waits until a message appears. The reasoning for using both functions in this way will become clear in later chapters.
The TranslateAccelerator function declaration follows:
int TranslateAccelerator( HWND hWnd, HACCEL hAccTable, LPMSG lpMsg ); |
Parameter | Description |
---|---|
hWnd | The window whose message will be translated |
hAccTable | The handle of the accelerator table loaded with LoadAccelerators |
lpMsg | Pointer to the MSG structure that holds the message acquired from the calling thread's message queue using PeekMessage (or GetMessage) |
This function processes the accelerator keys for the menu commands. TranslateAccelerator translates a WM_KEYDOWN or WM_SYSKEYDOWN message to a WM_COMMAND or WM_SYSCOMMAND message if an entry in the accelerator table indicates that this action should be taken. TranslateAccelerator then passes this translated message to the appropriate window procedure.
The function declaration for TranslateMessage follows:
BOOL TranslateMessage( CONST MSG * lpMsg ); |
This function is used to translate the virtual-key messages into character messages. These character messages are then posted to the calling thread's message queue, which is read the next time the thread calls PeekMessage or GetMessage. If the message is translated (so that a character message is posted to the thread's message queue), the function's return value will be nonzero; if it isn't translated, the return value will be 0. If the message is WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, or WM_SYSKEYUP, the return value will be 0 no matter what the translation is. When TranslateAccelerator returns a nonzero value, indicating that the message passed to it in the lpMsg parameter was processed, your application shouldn't process the message again with TranslateMessage.
The DispatchMessage function has a single parameter, lpMsg, which is a pointer to the MSG structure that holds the message acquired from the calling thread's message queue.
LONG DispatchMessage( CONST MSG * lpMsg ); |
This function dispatches the translated message to the window procedure. The return value of this function is the value the window procedure returns.
When creating the WNDCLASS structure, you specify a pointer to a window procedure in the structure's lpfnWndProc member. This procedure, which is named WndProc in our application (Microsoft documents it as WindowProc), is an application-defined callback function that processes messages sent to the window. The WindowProc function is declared as follows:
LRESULT CALLBACK WindowProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ); |
Parameter | Description |
---|---|
hWnd | The handle to the window |
uMsg | The message identifier |
wParam | The first message parameter |
lParam | The second message parameter |
D3dapp.cpp has a short WndProc function. It simply calls into g_pD3DApp->MsgProc to handle the message:
//------------------------------------------------------------------- // Name: WndProc // Desc: Static message handler that passes messages to the // application class //------------------------------------------------------------------- LRESULT CALLBACK WndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) { if( g_pD3DApp ) return g_pD3DApp->MsgProc( hWnd, uMsg, wParam, lParam ); return DefWindowProc( hWnd, uMsg, wParam, lParam ); } |
Since g_pD3DApp is of type CD3DApplication, you might think that this code would call CD3DApplication::MsgProc. But CMyD3DApplication has overridden the virtual function MsgProc, so CMyD3DApplication::MsgProc (in roadrage.cpp) gets called instead. CMyD3DApplication::MsgProc handles a few messages that are particular to RoadRage and passes the rest to CD3DApplication::MsgProc, which handles messages that are common to all programs that use the Direct3D Framework. Here's CMyD3DApplication::MsgProc:
LRESULT CMyD3DApplication::MsgProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) { HMENU hMenu; m_hWnd = hWnd; hMenu = GetMenu( hWnd ); switch( uMsg ) { case WM_COMMAND: switch( LOWORD(wParam) ) { case MENU_ABOUT: DialogBox(hInstApp, MAKEINTRESOURCE(IDD_ABOUT), hWnd, (DLGPROC)AppAbout); break; case IDM_EXIT: SendMessage( hWnd, WM_CLOSE, 0, 0 ); DestroyWindow( hWnd ); PostQuitMessage(0); exit(0); default: return CD3DApplication::MsgProc( hWnd, uMsg, wParam, lParam ); } break; case WM_GETMINMAXINFO: ((MINMAXINFO*)lParam)->ptMinTrackSize.x = 100; ((MINMAXINFO*)lParam)->ptMinTrackSize.y = 100; break; case WM_CLOSE: DestroyWindow( hWnd ); PostQuitMessage(0); return 0; default: return CD3DApplication::MsgProc( hWnd, uMsg, wParam, lParam ); } return DefWindowProc( hWnd, uMsg, wParam, lParam ); } |
The CD3DApplication::MsgProc method is defined as shown here:
//------------------------------------------------------------------- // Name: MsgProc // Desc: Message-handling function //------------------------------------------------------------------- LRESULT CD3DApplication::MsgProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) { HRESULT hr; switch( uMsg ) { case WM_PAINT: // Handle paint messages when the application isn't // ready. break; case WM_MOVE: // If in windowed mode, move the Direct3D Framework's // window. break; case WM_SIZE: // Check to see whether you're losing the window. if( SIZE_MAXHIDE==wParam || SIZE_MINIMIZED==wParam ) m_bActive = FALSE; else m_bActive = TRUE; // A new window size requires a new back-buffer size, // so you must change the 3D structures accordingly. break; case WM_SETCURSOR: // Prevent a cursor in full-screen mode. break; case WM_ENTERMENULOOP: // Pause the application when menus are displayed. Pause(TRUE); break; case WM_EXITMENULOOP: Pause(FALSE); break; case WM_ENTERSIZEMOVE: // Halt frame movement while the application is sizing // or moving. if( m_bFrameMoving ) m_dwStopTime = timeGetTime(); break; case WM_EXITSIZEMOVE: if( m_bFrameMoving ) m_dwBaseTime += timeGetTime() - m_dwStopTime; break; case WM_CONTEXTMENU: // Handle the application's context menu (via a right // mouse click). TrackPopupMenuEx( GetSubMenu( LoadMenu( 0, MAKEINTRESOURCE(IDR_POPUP) ), 0 ), TPM_VERTICAL, LOWORD(lParam), HIWORD(lParam), hWnd, NULL ); break; case WM_NCHITTEST: // Prevent the user from selecting the menu in // full-screen mode. break; case WM_POWERBROADCAST: switch( wParam ) { case PBT_APMQUERYSUSPEND: // At this point, the application should save any // data for open network connections, files, and // so on and prepare to go into suspended mode. return OnQuerySuspend( (DWORD)lParam ); case PBT_APMRESUMESUSPEND: // At this point, the application should recover // any data, network connections, files, and so // on and resume running from the point at which // the application was suspended. return OnResumeSuspend( (DWORD)lParam ); } break; case WM_SYSCOMMAND: // Prevent moving or sizing and power loss in // full-screen mode. switch( wParam ) { case SC_MOVE: case SC_SIZE: case SC_MAXIMIZE: case SC_MONITORPOWER: // If not in windowed mode, return 1. break; } break; case WM_COMMAND: switch( LOWORD(wParam) ) { case IDM_TOGGLESTART: // Toggle frame movement. break; case IDM_SINGLESTEP: // Single-step frame movement. break; case IDM_CHANGEDEVICE: // Display the device-selection dialog box. return 0; case IDM_TOGGLEFULLSCREEN: // Toggle between full-screen and windowed mode. return 0; case IDM_ABOUT: // Display the About dialog box. Pause(TRUE); DialogBox( (HINSTANCE)GetWindowLong( hWnd, GWL_HINSTANCE ), MAKEINTRESOURCE(IDD_ABOUT), hWnd, AboutProc ); Pause(FALSE); return 0; case IDM_EXIT: // Received key/menu command to exit application SendMessage( hWnd, WM_CLOSE, 0, 0 ); return 0; } break; case WM_GETMINMAXINFO: ((MINMAXINFO*)lParam)->ptMinTrackSize.x = 100; ((MINMAXINFO*)lParam)->ptMinTrackSize.y = 100; break; // Close the window. case WM_CLOSE: DestroyWindow( hWnd ); return 0; // The WM_DESTROY message is sent to the window procedure // of the window being destroyed after the window is removed // from the screen. // // This message is sent first to the window being destroyed // and then to the child windows (if any) as they are // destroyed. You can assume that while the message is being // processed, all child windows still exist. case WM_DESTROY: // Clean up 3D environment stuff here. PostQuitMessage(0); return 0; } // // The DefWindowProc function calls the default window procedure // to provide default processing for any window messages that an // application doesn't process. This function ensures that every // message is processed. DefWindowProc is called with the same // parameters received by the window procedure. return DefWindowProc( hWnd, uMsg, wParam, lParam ); } |
The CD3DApplication::MsgProc arguments describe the window (hWnd), the message received (uMsg), and any parameters associated with the message (wParam and lParam). The messages that follow are a few of the messages that the MsgProc method handles:
The last step in the CD3DApplication's MsgProc method is to perform default processing of any window message that the application doesn't process. MsgProc calls DefWindowProc (with the same parameters that MsgProc received) for this default processing.
Figure 2-4 The About dialog box
The final step is to destroy the window when the code is complete, which usually occurs when the user selects Escape from the menu. To destroy the window, the DestroyWindow command is called. This command has one parameter, hWnd, which is the handle to the window to be destroyed. The DestroyWindow command is defined as follows:
BOOL DestroyWindow( HWND hWnd ); |
DestroyWindow does what it sounds like: destroys the window you pass to it. It also sends a WM_DESTROY message to the window procedure, so your application can destroy any resources that depend on the window.