Chapter 03: Fundamentals of Windows Programming

 

It would be great if we could cut to the chase and simply jump straight into some 3D action, but Direct3D deals with 3D related matters only, and can’t accomplish much on its own without some generic Windows code to support it. With that said, the samples for this book are all written as simple Win32 console applications, which I generated using Visual C++ 6.0. If you’re already an OpenGL programmer, you may be crying foul at reading this because you have most likely become accustomed to using GLUT for all your samples. GLUT definitely produces cleaner looking code for samples as it tends to hide sticky issues such as Window class creation and message loops, but GLUT is not an option when working with Direct3D, thus we begin our brief detour into Windows programming.

 

Fundamentals Only

 

First off, I’m not going to teach you Windows programming in the traditional sense. It’s far too difficult a subject to cram into a single chapter of a book. Instead, I’m going to expose you to just enough to get our test applications working, but hopefully not enough to swear you off Windows programming all together. Windows programming by itself is a difficult subject to learn and takes years to fully understand, let alone master. Fortunately for us, we only need to know a trivial amount to get our samples up and running.

 

Visual C++ 6.0 Work Spaces

 

The source code for the projects discussed in each chapter can be found on the CD as a collection of Visual C++ 6.0 workspaces and while you can simply copy the entire directory to your hard drive when you want to build and run them, it’s good to know how to create your own Win32 projects or workspaces from scratch.

 

Here are the steps for creating an empty workspace for your own Win32 projects.

 

  1. Launch the Visual C++ 6.0 compiler and select New from the File menu.
  2. Select the Projects tab and highlight the Win32 application selection.
  3. Fill in the Project name: edit box with whatever you what to call your project.
  4. If you don’t like the specified location of your future project, click the browse button next to the Location: edit box and select a new path.
  5. If everything looks fine, click the Ok button to proceed to the next dialog.
  6. When the next dialog pops up, select An empty project. And click Finish.
  7. Finally, a dialog describing what the compiler is about to do is displayed and if everything checks out, click the Ok button again.

 

When we’re done, we’ll have an empty Win32 project with no source files. At first, it may appear the we haven’t done much, but several important project settings have been set that will make it possible for us to later compile and run a Win32 Application.

 

The only thing that’s left to do now is to add at least one source file (i.e. a .cpp file) so we can start coding our new Win32 application. To see the files currently associated with our new workspace, click on the File View tab of the compiler’s Workspace window. You should find three empty directories there titled, Source Files, Header Files, and Resource Files.

 

Here’s how to add a new source file to our project:

 

  1. Select New from the File menu and the select the Files tab from the dialog.
  2. Select C++ Source file and fill in the File name: edit box.
  3. The Location: edit box should already be set to the proper directory, so click Ok.

 

If we go to the Source File directory in our Workspace window, we should find our new source file listed there. We now have the bare minimum for creating a Win32 application from scratch. If we wanted, the compiler could have created for us a workspace complete with automatically generated source and header files for a generic do-nothing application. That would be fine for a typical Win32 application, but this approach would’ve created a bloated window application filled with useless code that our samples don’t really require and would’ve only confused matters more.

 

Now, once you have your workspace setup, you can actually begin constructing a Win32 application by typing the code listed through out this chapter into your new source file and compiling it into a Win32 executable. Of course, if you’re lazy like me; you’ll just copy the sample projects onto your hard drive and bypass the rewarding experience of hand coding a Windows application.

 

A Simple Windows Application

 

At a minimum, a Windows application needs at least two functions to operate; WinMain, which acts like main in a standard C or C++ program, and WindowProc, a call-back function that the operating system calls when it needs to send our application a window message. As you’ll soon learn, Window messages are the key to understanding Windows Programming, but let’s not get too far ahead of ourselves.  The following code shows the steps performed in our WinMain function for creating a simple windows application.

 

Listing 3-1: WinMain function of the “simple_window” sample

 

//-----------------------------------------------------------------------------

// Name: WinMain()

// Desc: Unlike a regular C or C++ program that starts with main(), Window

//       applications start with WinMain.

//-----------------------------------------------------------------------------

int WINAPI WinMain( HINSTANCE hInstance,     // handle to current instance

                    HINSTANCE hPrevInstance, // handle to previous instance

                    LPSTR     lpCmdLine,     // pointer to command line

                    int       nCmdShow )     // show state of window

{

    WNDCLASSEX winClass;

    HWND       hwnd;

    MSG        uMsg;

 

    memset(&uMsg,0,sizeof(uMsg));

 

    // The winClass structure will hold the information about our

    // new Window class that we plan on creating.

 

    winClass.lpszClassName = "MY_WINDOWS_CLASS";

    winClass.cbSize        = sizeof(WNDCLASSEX);

    winClass.style         = CS_HREDRAW | CS_VREDRAW;

    winClass.lpfnWndProc   = WindowProc;

    winClass.hInstance     = hInstance;

    winClass.hIcon         = LoadIcon(NULL, IDI_APPLICATION);

    winClass.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);

    winClass.hCursor       = LoadCursor(NULL, IDC_ARROW);

    winClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);

    winClass.lpszMenuName  = NULL;

    winClass.cbClsExtra    = 0;

    winClass.cbWndExtra    = 0;

 

    // Register our new declared window class

    if( RegisterClassEx( &winClass) == 0 )

    return E_FAIL;

 

    // Create the window!

    hwnd = CreateWindowEx( NULL,

                           "MY_WINDOWS_CLASS",

                           "Simple Window",

                           WS_OVERLAPPEDWINDOW | WS_VISIBLE,

                           0,0,

                           640,480,

                           NULL,

                           NULL,

                           hInstance,

                           NULL );

 

    if( hwnd == NULL )

    return E_FAIL;

 

    ShowWindow( hwnd, nCmdShow );

    UpdateWindow( hwnd );

 

    // The following while loop represents the main message loop

    // for our application. Every time our Window receives a windows

    // message, this loop will detect it, perform some preprocessing

    // on it and then dispatch it to our application's message handler

    // function, WindowProc().

 

    while( uMsg.message != WM_QUIT )

    {

        GetMessage( &uMsg, NULL, 0, 0 );

        TranslateMessage( &uMsg );

        DispatchMessage( &uMsg );

    }

 

    UnregisterClass( "MY_WINDOWS_CLASS", winClass.hInstance );

 

    return uMsg.wParam;

}

 

While the SDK functions for creating a window look complicated and take a ridiculous number of arguments, the general steps for creating your own window are fairly straightforward:

 

  1. Create an instance of the WNDCLASSEX structure and describe your windows abilities by setting its data members.
  2. Use RegisterClassEx to register your new window class with the operating system so the OS will know how your new window should be constructed when an instance of it is needed.
  3. Call CreateWindowEx to create an actual instance of your new window.

 

Creating The Window’s Class

 

To create our window’s class (not the same as a C++ class), we start off by creating an instance of the WNDCLASSEX structure and filling in its members. As we fill in these members, we are in fact describing the look and feel of our desired window to the operating system. The most important of these is the lpfnWndProc member, which identifies the function that we must provide if we hope to receive any messages from the operating system when users interact with our new window. The lpszClassName member allows us to actually name my new window class so we can differentiate it from other registered classes when we later call CreateWindowEx to create an instance of it.

 

The style argument identifies all the styles that we want applied to our new class. As the code shows, the requested styles can be combined together using the bitwise OR operator. For example, our class sets CS_HREDRAW | CS_VREDRAW as its only two styles. These two style flags simply request that the window be completely redrawn if a movement or size adjustment changes the width or height of our window’s client area.

 

The rest of the members are either very obvious or deal with items that our simple samples don’t require such as specifying icon resources, identifying the type of cursor to use, or what color should be selected as the window’s fill color. If you’re interested in such additions to your own windows, please refer to the documentation that ships with Visual Studio or go online and search Microsoft’s MSDN website for more information.

 

Creating the Window

 

With our WNDCLASSEX structure filled in and ready to go, we can proceed forward and call RegisterClassEx to register our new class with the operating system followed by a call to CreateWindowEx (making sure to pass in the correct class name) to actually create an instance of it. In this sample, CreateWindowEx has a number of arguments actually Nulled out because we simply don’t need them for such a simple window, but the rest of the arguments do some important tasks such as giving the window a title for its title bar and setting the size of the window to 640x480. We can also tell the operating system where we want our new window to show up on the user’s desktop by setting the X and Y arguments. Our window’s default position on the desktop will simply be at X = 0 and Y = 0, which in screen coordinates is in the window’s upper left-hand corner with the X and Y values increasing as you move along the monitor’s top and down the side respectively. Note how the window is positioned in screen-coordinates by using its upper-left hand corner.

 


Figure 3-1: 1280 by 1024 display with a window’s initial position located at X = 25 and Y = 250

 

Finally, our call to CreateWindowEx passes WS_OVERLAPPEDWINDOW | WS_VISIBLE for the dwStyle argument of the call, which is very similar to the style member of the WNDCLASSEX structure that we discussed earlier. These two styles are very generic and typical of most windows. The first flag, WS_OVERLAPPEDWINDOW, is actually a composite of several other styles. Using this flag is the same as setting WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX, and WS_MAXIMIZEBOX. The second flag, WS_VISIBLE, simply makes the window visible after initial creation. I’m not going to cover all the possible flags and their valid combinations, so if you’re interested in them I would suggest checking out the help files that ship with the Visual C++ compiler.

 

Now that you’ve registered your class it with the operating system and created your window, you’ll need a call to ShowWindow and UpdateWindow to get it to appear on the video display. After this, it’s all about the messages.

 

Message Processing

 

After the window has actually been created, code execution falls into a special while loop where our new window will begin listening for incoming window messages. When a new message arrives, it gets placed into our window’s message queue where it will wait until it can be processed by the second function, WindowProc, which was specified when we created our window’s class.

 

Listing 3-2: WinProc function of the “simple_window” sample

 

//-----------------------------------------------------------------------------

// Name: WindowProc()

// Desc: This function will act as our main message handler for our window.

//       Any time our application receives a Windows message from the

//       operating system, this function will be called to handle it.

//-----------------------------------------------------------------------------

LRESULT CALLBACK WindowProc( HWND   hwnd,    // handle to window

                             UINT   uMsg,    // message identifier

                             WPARAM wParam,  // first message parameter

                             LPARAM lParam ) // second message parameter

{

    // All Window messages that we want our application to handle will need

    // to be added here as a new case statement, and as you'll soon discover,

    // almost all window messages are of the form "WM_???".

 

    switch( uMsg )

    {

        case WM_KEYDOWN:

        {

            switch( wParam )

            {

                case VK_ESCAPE:

                // Some one hit the Escape key!

                // Kill the application by posting a WM_QUIT message.

                PostQuitMessage(0);

                break;

            }

        }

        break;

 

        case WM_CREATE:

        {

            // Perform any window initialization here...

        }

        break;

 

        case WM_CLOSE:

        {

            // Kill the application by posting the WM_DESTROY and WM_NCDESTROY

            // messages.

            DestroyWindow( hwnd );

        }

 

        case WM_DESTROY:

        {

            // Kill the application by posting a WM_QUIT message.

            PostQuitMessage(0);

        }

        break;

 

        default:

        {

            // The DefWindowProc() function will process any messages that

            // we didn't bother to catch in the switch statement above.

            return DefWindowProc( hwnd, uMsg, wParam, lParam );

        }

        break;

    }

 

    return 0;

}

 

Of course, the WindowProc function in our first sample doesn’t do anything that’s immediately noticeable except post a quite message to our window when it receives a WM_DESTROY message or when the user hits the Escape key. We also manage to respond to the WM_CLOSE message by calling DestroyWindow, but other than that, it doesn’t do anything. The WindowProc in the next sample will do a lot more and should hopefully help to clear things up a little.

 

Window Messages

 

In contrast to most C applications, which are typically procedural in nature, Window applications use an event-driven style of programming in which the application spends the majority of its time sitting in an idle state waiting for user input such as a button click or a menu selection. When the operating system detects that the user is doing something to your window, it creates the appropriate window messages and then places the messages in our application’s message queue for the message loop to find. The message loop then dispatches these messages to our message handler where a switch statement will catch the messages that we want our application to process. To better understand this, I’ve included a sample that responds to several different messages by creating small message boxes with the MessageBoX function provided by the Win32 SDK.

 

For each new Window message that we want our application to respond to, we need to add a new case statement to catch it. This is how the next sample application will be able to respond with a small message box when the message is received. To trigger some window messages, simply launch the application and begin adjusting it and moving it around. The window will also respond to left and right mouse button clicks, but only if they take place within its client area (i.e. the empty black area of your window).

 

Listing 3-3: WinProc function of the “window_messages” sample

 

//-----------------------------------------------------------------------------

// Name: WindowProc()

// Desc: This function will act as our main message handler for our window.

//       Any time our application receives a Windows message from the

//       operating system, this function will be called to handle it.

//-----------------------------------------------------------------------------

LRESULT CALLBACK WindowProc( HWND   hwnd,    // handle to window

                             UINT   uMsg,    // message identifier

                             WPARAM wParam,  // first message parameter

                             LPARAM lParam ) // second message parameter

{

    // All Window messages that we want our application to handle

    // will need to be added here as a new case statement and as

    // you'll soon discover, almost all window messages are of the

    // form "WM_???".

 

    switch( uMsg )

    {

        case WM_KEYDOWN:

        {

            switch( wParam )

            {

                case VK_ESCAPE:

                    // The user hit the Escape key!

                    MessageBox( NULL, "I just got a WM_KEYDOWN message!",

                                "Message", MB_OK );

                    PostQuitMessage(0);

                break;

            }

        }

        break;

 

        case WM_CREATE:

        {

            // You application just got created... handle initialization here!

            MessageBox( NULL, "I just got a WM_CREATE message!",

                        "Message", MB_OK );

        }

        break;

 

        case WM_SIZE:

        {

            // You application just got its window resized... handle it here!

            MessageBox( NULL, "I just got a WM_SIZE message!",

                        "Message", MB_OK );

        }

        break;

 

        case WM_MOVE:

        {

            // You application just got its window moved... handle it here!

            MessageBox( NULL, "I just got a WM_MOVE message!",

                        "Message", MB_OK );

        }

        break;

 

        case WM_LBUTTONDOWN:

        {

            // The x/y position of the mouse click is

            // stored in the lParam value that was passed

            // to us when we received the message.

 

            int xPos = GET_X_LPARAM( lParam );

            int yPos = GET_Y_LPARAM( lParam );

 

            char buffer[255];

            sprintf( buffer, "I just got a WM_LBUTTONDOWN message at "

                             "X = %d, Y = %d!", xPos, yPos  );

 

            // Someone just clicked the left mouse button over your

            // application's window!

            MessageBox( NULL, buffer, "Message", MB_OK );

        }

        break;

 

        case WM_RBUTTONDOWN:

        {

            // The x/y position of the mouse click is

            // stored in the lParam value that was passed

            // to us when we received the message.

 

            int xPos = GET_X_LPARAM( lParam );

            int yPos = GET_Y_LPARAM( lParam );

 

            char buffer[255];

            sprintf( buffer, "I just got a WM_RBUTTONDOWN message at "

                             "X = %d, Y = %d!", xPos, yPos  );

 

            // Someone just clicked the right mouse button over your

            // application's window!

            MessageBox( NULL, buffer, "Message", MB_OK );

        }

        break;

 

        case WM_CLOSE:

        {

            // The OS is requesting that you prepare to shutdown!

            MessageBox( NULL, "I just got a WM_CLOSE message!",

                        "Message", MB_OK );

            DestroyWindow( hwnd );

        }

 

        case WM_DESTROY:

        {

            // You application just got killed! Cleanup any memory you

            // allocated here!

            MessageBox( NULL, "I just got a WM_DESTROY message!",

                        "Message", MB_OK );

            PostQuitMessage(0);

        }

        break;

 

        default:

        {

            // The DefWindowProc() function will process any messages that

            // we didn't bother to catch in the switch statement above.

            return DefWindowProc( hwnd, uMsg, wParam, lParam );

        }

        break;

    }

 

    return 0;

}

 

You should also take note of the handling of the WM_LBUTTONDOWN message. It demonstrates how to get useful information that may be in the lParam and wParam arguments, which are sent with the message. Of course, not all messages have useful information in lParam and wParam, but in the case of the mouse click messages, the lParam parameter holds the X and Y position of where the mouse click took place within you window’s client area. If this information was needed by your application, the x and y positions could be extracted by including the “windowsx.h” header file and calling the GET_X_LPARAM and GET_Y_LPARAM helper functions.

 

The Render Loop

 

The samples you just examined are fairly typical of a Windows application written with the Win32 SDK and all sample projects from now on will be using this basic framework to support our DirectX code. Of course, there’s one thing that we’ll need to tweak before we start cranking out 3D samples using this framework. The message loop will need to be modified to support the high-speed demands of 3D rendering. The typical loop uses GetMessage to retrieve messages from its message queue for eventual processing by WindowProc. The problem with GetMessage is that it blocks the while loop that it’s sitting in from looping until a new message drops into the queue.  This behavior is fine for business applications that don’t need to do anything until you click on one of their buttons, but 3D rendering is all about speed, and this is valuable time that could be used rendering the next frame into the video buffer for the next swap.

 

Fortunately for us, the solution to this problem is actually fairly simple: replace the call to GetMessage with another function called PeekMessage. The PeekMessage function checks to see if you have a message waiting. If there is a message in the queue, PeekMessage will return with it and you can start processing it. If no message is found, it does nothing and you can return to important rendering.

 

Listing 3-4: WinMain function of the “render_loop” sample

 

//-----------------------------------------------------------------------------

// Name: WinMain()

// Desc: A render-loop ready WinMain function...

//-----------------------------------------------------------------------------

int WINAPI WinMain( HINSTANCE hInstance,

                    HINSTANCE hPrevInstance,

                    LPSTR     lpCmdLine,

                    int       nCmdShow )

{

    WNDCLASSEX winClass;

    HWND       hwnd;

    MSG        uMsg;

 

    memset(&uMsg,0,sizeof(uMsg));

 

    winClass.lpszClassName = "MY_WINDOWS_CLASS";

    winClass.cbSize        = sizeof(WNDCLASSEX);

    winClass.style         = CS_HREDRAW | CS_VREDRAW;

    winClass.lpfnWndProc   = WindowProc;

    winClass.hInstance     = hInstance;

    winClass.hIcon         = LoadIcon(NULL, IDI_APPLICATION);

    winClass.hIconSm       = LoadIcon(NULL, IDI_APPLICATION);

    winClass.hCursor       = LoadCursor(NULL, IDC_ARROW);

    winClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);

    winClass.lpszMenuName  = NULL;

    winClass.cbClsExtra    = 0;

    winClass.cbWndExtra    = 0;

 

    if( RegisterClassEx( &winClass) == 0 )

    return E_FAIL;

 

    hwnd = CreateWindowEx( NULL, "MY_WINDOWS_CLASS", "Render Loop",

                           WS_OVERLAPPEDWINDOW | WS_VISIBLE,

                           0, 0, 640, 480, NULL, NULL, hInstance, NULL );

 

    if( hwnd == NULL )

    return E_FAIL;

 

    ShowWindow( hwnd, nCmdShow );

    UpdateWindow( hwnd );

 

    // Initialize OpenGL/Direct3D

    init();

 

    // The following while loop represents the main event loop of our new

    // window, but unlike a regular Window, we'll use PeekMessage() instead of

    // GetMessage() to retrieve our window messages for processing. This will

    // allow our application to spend more time processing 3D data and less

    // time waiting for messages.

 

    // If the message is WM_QUIT, we need to break out of the message

    // loop and shut down.

 

    while( uMsg.message != WM_QUIT )

    {

        // Test if there is a message in queue, if so - get it

        if( PeekMessage( &uMsg, NULL, 0, 0, PM_REMOVE ) )

        {

            // Translate any accelerator keys

            TranslateMessage( &uMsg );

 

            // Send the message to WindowProc() for handling

            DispatchMessage( &uMsg );

        }

        else

            render(); // If there are no messages waiting for us to process - render!

    }

 

    // Clean up and release OpenGL/Direct3D resources.

    shutDown();

 

    UnregisterClass( "MY_WINDOWS_CLASS", winClass.hInstance );

 

    return uMsg.wParam;

}

 

On top of the modifications to the message loop, you’ll notice that I’ve also added three functions, which will hopefully help to organize future samples.

 

void init(void);

void shutDown(void);

void render(void);

 

As the names indicate, these functions will be where the resources for our future samples will be initialized, used, and later cleaned up and/or released. Of course, the most important function is render. Unlike init and shutDown, which are only called once, render sits in the message loop and will be called repeatedly through out the life of the sample. All the code or function calls that perform tasks such as collecting user input, moving or transforming geometry, switching states, or swapping the video buffer will need to go here.

 

Well, that’s pretty much it for Windows programming. I know it’s a fairly short chapter, but trust me; we’ve covered everything you’ll need to know to make sense of the samples that accompany this book. The “render_loop” sample, which we just examined, is simply a starting place or framework for our future samples and you’ll see it used again and again in the chapters to come. There’s definitely a lot more to Windows programming, so if you would like to know more, I recommend reading Programming Windows 95 by Charles Petzold and Paul Yao or searching the help files that come with the Visual C++ 6.0 compiler.

 

 

© 2003 Kevin R. Harris All rights reserved.

Legal Disclaimer and Copyright Notice