..
#gamedev

(Win) 002 - How to Create a Game Window using Windows API

In the next lines I'll show you how to create a window and set up the main game loop.

All these steps require documentation, I'll just show the code and explain only the key points, assuming you'll refer to the documentation alongside.

It's so important to follow the documentation side by side with this post.

The Windows documentation is called MSDN.

Register a Class

So, the first step is to create a WindowClass (a struct) called WNDCLASS.

This struct requires a callback function to handle window-related tasks, including Keyboard events, etc.

We must register this struct to create a Window properly.

Let's see the code:

int APIENTRY
WinMain(HINSTANCE instance,
        HINSTANCE prev_instance,
        LPSTR cmd_line,
        int show_cmd)
{
        WNDCLASSA window_class = {};
        window_class.style         = CS_VREDRAW|CS_HREDRAW|CS_OWNDC;
        window_class.lpfnWndProc   = window_callback;
        window_class.hInstance     = instance;
        window_class.lpszClassName = "GameWindowClass";
        // window_class.hIcon -> load icon
        // window_class.hCursor -> load cursor
        // TODO: Add Icon to title bar

        if (!RegisterClassA(&window_class)) {
                OutputDebugStringA("Failed to register a class.\n");
                return -1;
        }
}

The key points is:

  • style: Bitwise information determining how the window operates;
  • lpfnWndProc: A function (that we'll create later) to handle window events like resize, create, destroy, etc;
  • hInstance: Handle instance of the application;
  • lpszClassName: Class name for the window;
  • CS_VREDRAW: Redraw the window vertically if needed when the size changes.
  • CS_HREDRAW: Redraw the window horizontally if needed when the size changes.
  • CS_OWNDC: Allocate only one DeviceContext to the window. Windows uses the DC to keep the state of a drawing while we're drawing to the window. It was initially intended to be used in conjunction with the brushes. Normally, Windows would have several device contexts available. When a program needs to draw, it gets one, uses it and then gives it back;
  • RegisterClassA: Function required for the Window to work properly;

We also can retrieve the instance handle calling GetModuleHandle(NULL).

The OutputDebugStringA function serves as the standard output for debug mode, visible only within VisualStudio. If you need to see the output on console, switch to the printf function from the stdio.h file, which is part of the C standard library.

Create a Window

Let's create the Window handle before defining the callback function.

HWND window = CreateWindowA(
    window_class.lpszClassName,
        "Game Title",
        WS_OVERLAPPEDWINDOW|WS_VISIBLE,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        960,
        540,
        NULL,
        NULL,
        instance,
        NULL);

if (!window) {
        OutputDebugStringA("Failed to create a window.\n");
        return -1;
}

The three main parameters are the className windowClass.lpszClassName, the Window title, and the instance itself.

CW_USEDEFAULT is used to replace the coordinates (x, y, width, height) with default value based on user's screen.

See the documentation to know what is the others parameters and how to used them (optional).

Window Procedure

Now, let's see how to implement the callback function.

LRESULT window_callback(HWND window, UINT message, WPARAM l_param, LPARAM w_param) {
        LRESULT result = 0;
        switch (message) {
                case WM_CREATE: {
                        OutputDebugStringA("Window Created.\n");
                } break;

                case WM_PAINT: {
                        // OutputDebugStringA("Window painted.\n");
                } break;

                case WM_CLOSE: {
                        DestroyWindow(window);
                } break;

                case WM_DESTROY: {
                        PostQuitMessage(0);
                } break;

                default: {
                        result = DefWindowProcA(window, message, l_param, w_param);
                }
        }
        return result;
}

The callback gives back sort of generic parameters. I mean, each situation of window we can get the specific values based on events.

This values is inside WPARAM and LPARAM.

In the future we'll utilize these parameters to processs keyboard message events.

The last parameter of the CreateWindow function is a void pointer where we can set values and retrieve them from LPARAM during the WM_CREATE state.

Process Messages For Game-Loop

The next step is to translate and dispatch messages from the message queue.

These messages include window messages, keyboard input, mouse input, and others.

We'll use the PeekMessage to retrieve messages from the queue.

Of course, we must perform this entire process inside a main loop controlled by a boolean variable. Otherwise, the window will flash and close instantly.

I declare this bool variable named should_quit.

Static Keyword And Process Message Queue

The variable is global scope and is declared with the static keyword.

The meaning of static varies depending on the context in which it appears.

For example, if static is used on local scope variable. It behaves like a persistent local variable.

When used within a function, it restricts to the file of translation unit. In the others words, it becomes an internal function.

Let's take a look at the code:

At the top of the file, we have a global variable named should_quit.

static bool should_quit;
static void process_message_queue() {
        MSG msg;
        while(PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) {
                if (msg.message == WM_QUIT) {
                        should_quit = true;
                } 
                TranslateMessage(&msg);
                DispatchMessageA(&msg);
        }
}

Inside the main function, we have:

// ...
while(!should_quit) {
        process_message_queue();
 }

OutputDebugStringA("end program!\n");

return 0;

Reading the code, we can observe that the PeekMessageA function populates the Message struct for us, and if this struct is not zero, we process it.

When users hit the close button, the window callback handles the WM_CLOSE message. Inside that block, we call DestroyWindow which in turns triggers WM_DESTROY to force a PostQuitMessage.

As a result, the next message is a WM_QUIT message, which breaks the loop, thus ending the game.

The final step is to add a static library gdi32.lib to the build.bat file.

Conclusion

To create a window we need to determine a window class that contains title and icons for the window. It also includes the window callback procedure to handle the painting step, creating and closing the window.

After define the window class, we need to register this class for using the window.

Now, create a window with coordinates, title and some flags to define window properties like borders, visible, etc.

Create a main-loop to process current thread message queue and window message queue.

This process allow us to translate the virtual-key into character message and dispatch this message to the the window callback procedure.

As you can see below, the second parameter of PeekMessageA should be null to both window messages and thread messages to be processed (WM_QUIT is thread message). Otherwise, we cannot close the program, even the window was closed.

The next step is to Allocate a Back Buffer.

Review

To create a window we need to determine a window class that contains title and icons for the window. It also includes the window callback procedure to handle the painting step, creating and closing the window.

After define the window class, we need to register this class for using the window.

Now, create a window with coordinates, title and some flags to define window properties like borders, visible, etc.

Create a main-loop to process current thread message queue and window message queue.

This process allow us to translate the virtual-key into character message and dispatch this message to the the window callback procedure.

As you can see below, the second parameter of PeekMessageA should be null to both window messages and thread messages are processed (WM_QUIT is thread message). Otherwise, we cannot close the program, even the window was closed.

CS_OWNDC: Allocates a unique device context for each window in the class.

IMPORTANT: hnwd must be NULL to both window messages and thread messages are processed.

My Quick Access

  • Create WNDCLASSA
  • Register a Class
  • Create Window Callback Prodecure
  • Create Window Ex
  • Create GameLoop
  • Process Event Queue with Peek Message
  • Extra: Paint the whole Window with PatBlit, BeginPaint and EndPaint

References

  • HandmadeHero - Opening a Win32 Window
  • WNDCLASSA - Contains the window class attributes that are registered by the RegisterClass function
  • DefWindowProcA - Calls the default window procedure to provide default processing for any window messages that an application does not process.
  • RegisterClassA - Registers a window class for subsequent use in calls to the CreateWindow
  • CreateWindowA - Creates an overlapped, pop-up, or child window
  • PeekMessageA - Dispatches incoming nonqueued messages, checks the thread message queue for a posted message, and retrieves the message (if any exist). Used for lengthy operation.
    • IMPORTANT: hnwd must be NULL to both window messages and thread messages are processed.
  • DispatchMessageA - Dispatches a message to a window callback procedure.
  • TranslateMessage - Translate the virtual key into character messages. The character messages are posted to the calling thread's message queue, to be read the next time the thread calls PeekMessage.
  • DestroyWindow - Send message to the window to deactivate it and remove the keyboard focus from it.
  • PostQuitMessage - Indicates to the system that a thread has made a request to terminate.