OpenGL Tutorial

- Lesson Two -

Abstract: In this lesson, we will learn some basic concepts about the Windows' Graphics Device Interface. This is the last pre-OpenGL lesson of the tutorial.

1. Return of the "Hello world!"

Our "Hello world!" example is only half-way done, the window we created so far is a blank one. To finish the example, it is necessary to introduce some basic concepts about the Windows Graphics Device Interface (GDI). Such an introduction will also serve as a good take-off point for entering the OpenGL space.

Windows GDI is a dynamic-link library (DLL library) that processes graphics function calls from a Windows-based application and passes those calls to the appropriate device driver. GDI is a software layer between programmers and the display devices such as a video display or a printer, it hides the complexity of these devices from the programmers, and makes graphic programming device-independent.

One of the most important data structure in GDI is the so called Device Context structure - DC. Device context is a data structure internally maintained by GDI. What "internally" means is you can't access data members of a device context directly. A device context is associated with a particular display device, in the case the device is the video display, the device context is usually associated with a particular window on the display. Every GDI drawing function requires a handle to a device context to work. The device context determines how GDI drawing functions work, for instance, what color should be used in drawing, what font should be used for text, which area should be painted, etc. Device context is a system resource, therefore you should always release device context to the system once you are done with it - similar to what you would do for memory in normal C/C++ programming.

Win32 uses the teminology of "painting" an area when something needs to be displayed. Whenever a part of the client area of a window needs to be painted (becaused user resized or moved a window for instance), the operating system will send a WM_PAINT message to the program that owns the window, the program's window procedure will handle the message and do the necessary painting in the handler. So we will place our drawing code in the WM_PAINT handler of the window procedure. The area or region that needs to be painted is called the invalid region or update region. As a program runs, the operating system keeps track of many paint information for each window the program created, invalid region is one such piece of information. If, for some reason, you need to manually add an area into the invalid region (so a painting will be done for that region), you can use the InvalidateRect function. Another thing that is very important about the invalid region is you must validate the invalid region in the WM_PAINT handler. Windows does not place multiple WM_PAINT messages to the message queue, but once a WM_PAINT message is retrived from the message queue, Windows will send another WM_PAINT message to the queue (and keep doing so) until the program validates the invalid region (which basically tells Windows that the invalid area is updated). To validate an area, one usually uses the ValidateRect function.

In summerize, the steps required to finish our "Hello world!" example (namely to draw the "Hello world!" string on the window) are the following:

  • Get a handle to the device context
  • Draw the text string
  • Validate the invalid region
  • Release the device context

The following code is the new window procedure that - in addition to what we had before - does the drawing in the WM_PAINT handler. The old code is gray out so we can focus on the new part. The comments in the code are in one-to-one correpondence with the steps listed above.

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    HDC         hdc;
    PAINTSTRUCT ps;

    switch (msg) {
        case WM_PAINT:
            // Get a handle to the device context & Validate the invalid region
            hdc = BeginPaint(hwnd, &ps);
            // Draw the text string
            TextOut(hdc, 50, 50, "Hello world!", 12);
            // Release the device context
            EndPaint(hwnd, &ps);
            break;
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }

    return 0;
}

The WinMain function - which we didn't include here - remains the same as in Lesson One. We use the function BeginPaint to get the device context associated with the window whose handle - hwnd - is passed as the first argument. BeginPaint takes a second argument - &ps - which is a reference to a PAINTSTRUCT structure. As we mentioned before, the operating system keeps track of many paint information for the window, these information will be stored in ps. For most simple tasks, including our example, the information stored in ps is not used (but is required to keep the function signature correct). Before returning a handle to the device context, the BeginPaint function does the following:

  • Sets the clipping region of the device context to exclude any area outside the update region (all the painting controlled by a device context is restricted to the clipping region of that device context).
  • Validates the entire client area.

From here we see that the BeginPaint function alone accomplishes two steps for the drawing task (Gets a handle and validates the region). Notice that we haven't done any painting yet, it might seem strange that the validation is done so early. Logically one would think that painting should be done before the validation (after all validating an area is to tell Windows that the area is painted!). But in reality it is not very important to keep the order since validation is nothing but an announcement to the operating system. Normally you would announce the finishing of a task after it's done, but sometimes you can announce it in advance, so long as you do the task right after the announcement and finish it before your boss gets a chance to check it!

Once the device context is ready, we then use the TextOut function to draw the text string. The TextOut function takes - in addition to the device context that determines the color, font, etc (all in system default values) - the x, y coordinates at which the string is to be drawn, the string itself, and the total number of characters in the string (instead of counting the number yourself and passing it directly as we did in the example, you can use the strlen() function to do the counting).

Finally we use the EndPaint function to release the device context. EndPaint takes the same arguments as BeginPaint (you may wonder that in order to release the device context pointed by hdc, why didn't we pass hdc to the EndPaint function? This is because &ps - the pointer to a PAINTSTRUCT - that we pass to EndPaint already has hdc as a data field). BeginPaint and EndPaint should always be used in pairs (similar to the new and free pair in C/C++).

If a window procedure doesn't handle the WM_PAINT message manually, the default handler will simply call the BeginPaint and EndPaint pair in succession which does nothing by validate the invalid region (so Windows will not keep sending WM_PAINT messages).

2. Another method

There is another popular function people often use to get a device context handle. That is the GetDC function. To get a handle to the device context associated with a window, GetDC takes the handle to that window as argument (and that's all it takes). The following code demonstrates the using of the GetDC function in our "Hello world!" example:

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    HDC         hdc;
     
    switch (msg) {
        case WM_PAINT:
            // Get a handle to the device context
            hdc = GetDC(hwnd);
            // Draw the text string
            TextOut(hdc, 50, 50, "Hello world!", 12);
            // Validate the invalid region
            ValidateRect(hwnd, NULL);
            // Release the device context
            ReleaseDC(hwnd, hdc);
            break;
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
        default:
            return DefWindowProc(hwnd, msg, wParam, lParam);
    }

    return 0;
}

Notice that we used the ValidateRect function in this code, because the GetDC function - unlike BeginPaint - will NOT validate the invalid region therefore it is necessary that we do the validation ourselves. Another feature that is different from the BeginPaint function is that GetDC sets the clipping region of the device context equal to the whole client area, therefore any painting using the device context returned by GetDC will not be restricted to the invalid region (but will still be restricted to the client area unless you do what described in the next paragraph).

One thing interesting about the GetDC function is that if - instead of passing a handle to the window - you pass a NULL pointer to GetDC, you will get the device context for the whole primary display, which means you can draw anywhere on the screen with this device context. To test this, replace the line hdc = GetDC(hwnd); by hdc = GetDC(NULL); in the code, and change the coordinates in the TextOut function from 50, 50 to 500, 500. Run the modified program you will see a "Hello world!" string painted at screen coordinate 500, 500, which is far beyond the application window (of size 200 × 200).

Similar to the using of the EndPaint function to release the device context obtained by BeginPaint, There is a ReleaseDC function one should always use to release the device context obtained by GetDC. The ReleaseDC function takes two arguments: the handle to the window - hwnd and the handle to the device context - hdc (GetDC -unlike EndPaint - doesn't take &ps, therefore hdc is needed).

3. Change device context - an example

As we said before, the device context is maintained by GDI internally and we don't have a direct access to its data members. On the other hand, device context controls the appearance of the painting therefore it's quite obvious that users will constantly need to change its properties (i.e. data members). To fullfill such demand, Windows GDI provides many functions to perform those changes. All these functions take the handle to the device context and manipulate it. For instance, if you want to draw the text in red as oppose to the default black color, you can use the SetTextColor function to set the color property for the device context before calling the TextOut function. The code segment is as follows:

        case WM_PAINT:
            hdc = BeginPaint(hwnd, &ps);
            SetTextColor(hdc, RGB(255, 0, 0));
            TextOut(hdc, 50, 50, "Hello world!", 12);
            EndPaint(hwnd, &ps);
            break;

What the SetTextColor function takes are a handle to the device context and the new color. What it does is set the color of that device context to the new color. The return value of SetTextColor (which we discarded) is the previous color used in the device context. You should save that value if you ever want to restore the original color later on.

Ok, Let's end our Windows GDI tour here, many tasks done by Windows GDI can also be done using OpenGL.

What's next?

OpenGL (finally)!

2002-09-19


Glossary

ValidateRect function - The ValidateRect function validates the client area within a rectangle by removing the rectangle from the update region of the specified window. The arguments this function takes are:

  • hWnd is a handle to the window the validation applies. If this parameter is NULL, the system invalidates and redraws all windows.
  • lpRect is a pointer to a rectangle - a RECT structure - that is to be removed. If this parameter is NULL, the entire client window will be validated.

Once the rectangle region is removed, any queued WM_PAINT message (if exists) whose invalid region is within the rectangle will be removed from the queue, and no further WM_PAINT message will be sent until a new invalid region occurs.

References

  1. Microsoft Corporation, MSDN Library, comes with Visual Studio 6.0, also available on the Microsoft website.
  2. Charles Petzold, Programming Windows (5th edition), Microsoft Press, 1998.
  3. André Lamothe, Tricks of the Windows Game Programming Gurus, Sams, 1999.