Win32 - Window Creation Tutorial (C)
How to create a window using Windows API.
Updated Mar 16, 2024

Build tools

Install Visual Studio and select the C++ Workload in the installation options to install Microsoft command line build tools (or download the standalone Build Tools for Visual Studio from here: https://visualstudio.microsoft.com/downloads/.

Hello, World!

Create main.c and add these lines to it:

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

Compile the program:

cl main.c

Execute main and you should see "Hello, World!" printed on the screen:

$ main
Hello, World!

You can also use a script to build the program. Create build.bat and add these lines to it:

@echo off
cl main.c /nologo /Zi /Fea.exe
  • @echo off turns off the echoing of commands to the command prompt. The @ symbol is used to prevent the echoing of the echo off command itself.
  • cl.exe is the command-line interface to the Microsoft Visual C++ compiler. You can use it to compile C and C++ source code into executable programs.
  • /nologo suppresses the display of copyright and version information.
  • /Zi generates debugging information for later use with a debugger.
  • /Fe specifies the name of the output file.

In many programs, such as Emacs and 4Coder, you can run the script using a keyboard shortcut (such as Alt + M).

With VSCode, you can create a folder named .vscode in the project root and create a file named tasks.json in it:

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "type": "shell",
            "command": "${workspaceFolder}/build.bat",
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}

And assign a shortcut (such as Alt + M) to Tasks: Run Build Task.

WinMain()

For GUI applications, we should use the WinMain function as the entry point. It is called by the system when the application starts and serves as the starting point for the application's execution. Replace the contents of main.c with these lines:

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#define Assert(c) do { if (!(c)) __debugbreak(); } while (0)

#pragma comment (lib, "user32.lib")

int WINAPI WinMain(
    HINSTANCE instance,
    HINSTANCE prev_instance,
    LPSTR     cmd,
    int       cmd_show) {

    OutputDebugString("Hello World!\n");

    return 0;
}
  • WIN32_LEAN_AND_MEAN excludes some of the less commonly used parts of the Windows API.
  • #define Assert(c) do { if (!(c)) __debugbreak(); } while (0) macro provides a simple mechanism for incorporating runtime assertions into the code. The do-while loop wraps the macro body to ensure that the macro behaves like a single statement. while (0) is there to create a loop that executes only once. If the assertion fails, it triggers a debugger breakpoint.
  • #pragma comment (lib, "user32.lib") is a preprocessor directive that instructs the linker to include the user32.lib library during the linking phase. You can also link it like this: cl main.c /nologo /Zi /link user32.lib.
  • int WINAPI specifies the return type and calling convention of the WinMain function. A return value of 0 usually signifies successful execution.
  • instance is a handle to the current instance of the application. It is essentially a pointer to a structure that contains information about the program's executable. The actual structure and content of the internal data referred to by HINSTANCE are not part of the public Windows API.
  • prev_instance is a legacy parameter that allowed a new instance of the application to communicate with the previous one.
  • cmd is a pointer to a null-terminated string that represents the command-line arguments passed to the program. LPSTR stands for "long pointer to a string".
  • cmd_show specifies the initial appearance of the application's main window. SW_SHOWNORMAL is the default state, while SW_SHOWMAXIMIZED would display the window in a maximized state. We will use this later with the ShowWindow(window, cmd_show); command.

The value of cmd_show can be influenced by the user's preferences like this:

With GUI programs, we typically use OutputDebugString() to print to the debugger's output window:

Window class

A window class serves as a template for creating windows with similar characteristics. Add the WNDCLASSW struct, which defines the attributes for a window class:

int WINAPI WinMain(
    HINSTANCE instance,
    HINSTANCE prev_Instance,
    LPSTR     cmd,
    int       cmd_show) {

    OutputDebugString("Hello World!\n");

    // Here.
    WNDCLASSW wc = {
        .lpszClassName = L"MyWindowClass",
        .lpfnWndProc = win_proc,
        .hInstance = instance,
        .hCursor = LoadCursor(NULL, IDC_CROSS),
    };

    ATOM atom = RegisterClassW(&wc);
    Assert(atom && "Failed to register a window");

    return 0;
}
  • lpszClassName is a unique identifier for the type of window we are creating. The L before the string indicates that it's a wide string (Unicode).
  • lpfnWndProc is a function pointer to the window procedure, which is a function that processes messages sent to a window.
  • hCursor represents the handle to the cursor that will be used as the default cursor for windows of this class. The NULL parameter indicates that the cursor is a standard system cursor, and IDC_CROSS specifies the crosshair cursor type.
  • RegisterClassW() informs the OS about the window class. The W indicates that it deals with wide-character strings, as opposed to the ANSI version, which is RegisterClassA.
  • An ATOM is a small, unique numerical identifier associated with a character string. The use of atoms allows for efficient string lookups. Instead of working with entire strings, applications can work with atoms.
  • Assert(atom && "Failed to register a window"); triggers when atom is zero (indicating failure).

WindowProc()

When an event occurs in a window (such as a button click, keypress, or mouse movement), Windows sends a message to the window. Every window has an associated window procedure that handles messages related to that window.

// Add this above the WinMain function:

LRESULT CALLBACK win_proc(HWND window, UINT msg, 
                          WPARAM w_param, LPARAM l_param) {
    switch(msg) {
        case WM_KEYDOWN: {
            switch(w_param) {
                case 'O': { 
                    DestroyWindow(window); 
                } break;
            }
        } break;
        
        case WM_DESTROY: {
            PostQuitMessage(0);
        } break;
        
        default: {
            return DefWindowProcW(window, msg, w_param, l_param);
        }
    }
    
    return 0;
}
  • LRESULT is usually a 32-bit signed value that is used to indicate the result of an operation.
  • CALLBACK is a macro that defines the stdcall calling convention.
  • w_param and l_param parameters are used to provide additional information about a message sent to a window. The interpretation of these parameters depends on the type of message being sent. For example, in the case of WM_KEYDOWN, w_param contains the virtual-key code of the pressed key.
  • WM_DESTROY is sent when the window is closed. This allows us to gracefully exit when the user closes the window.
  • PostQuitMessage(0) posts a quit message to the application's message queue, indicating that the application should exit and return the specified exit code (in this case, 0). We use this message to exit the main loop.
  • WM_KEYDOWN is sent when a key on the keyboard is pressed.
  • DestroyWindow(window) is used to close and destroy a window. It performs various cleanup tasks associated with the window. Before the window is actually destroyed, the system sends a series of messages to the window procedure. These messages include WM_DESTROY, which we use to post a quit message.
  • DefWindowProcW processes the rest of the messages we don't explicitly handle.

Window creation

Add this after Assert(atom && "Failed to register a window");:

    HWND window = CreateWindowW(
        wc.lpszClassName, 
        L"Title", 
        WS_OVERLAPPEDWINDOW, 
        CW_USEDEFAULT, CW_USEDEFAULT, // position (x, y)
        CW_USEDEFAULT, CW_USEDEFAULT, // size (width, height)
        NULL, // parent window handle 
        NULL, // menu handle
        instance, // handle to the application instance
        NULL); // additional data to window procedure

    Assert(window && "Failed to create a window");
    
    ShowWindow(window, cmd_show);
  • Each window in a Windows application is assigned a unique HWND (handle to a window) that distinguishes it from other windows. It is a pointer to a structure that contains information about the window.
  • wc.lpszClassName specifies the class of the window.
  • L"Title" specifies the window caption. It appears in the title bar of the window.
  • WS_OVERLAPPEDWINDOW is a combination of styles that gives the window a standard window appearance. It includes a title bar, a system menu, a minimize button, a maximize button, and a close button.
  • Using the CW_USEDEFAULT constant instructs the system to choose the default position and size for the window.
  • We can use the last parameter to pass additional information to win_proc through the WM_CREATE message when the window is being created. The WM_CREATE message is sent to the window procedure immediately after the window is created but before it is shown.
  • ShowWindow(window, cmd_show); sets the window's show state.

Loop

Add this after ShowWindow(window, cmd_show);:

    for(;;) {
        MSG msg;
        if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
            if(msg.message == WM_QUIT) break;
            TranslateMessage(&msg);
            DispatchMessage(&msg);
            continue;
        }    
    }
    return 0;
  • for (;;) continues to execute until explicitly broken out of with the break statement.
  • PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) checks if there is a message in the application's message queue. If a message is available, it removes the message from the queue and stores it in the msg variable.
  • if (msg.message == WM_QUIT) break; checks if the message is WM_QUIT and, if true, exits the loop. This is the message sent to the window via PostQuitMessage(0).
  • TranslateMessage(&msg) is used to translate virtual-key messages into character messages.
  • DispatchMessage(&msg) dispatches the message to win_proc.

Complete example

// cl main.c

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#define Assert(c) do { if (!(c)) __debugbreak(); } while (0)

#pragma comment (lib, "user32.lib")

LRESULT CALLBACK win_proc(HWND window, UINT msg, 
                          WPARAM w_param, LPARAM l_param) {
    switch(msg) {
        case WM_KEYDOWN: {
            switch(w_param) {
                case 'O': { 
                    DestroyWindow(window); 
                } break;
            }
        } break;
        
        case WM_DESTROY: {
            PostQuitMessage(0);
        } break;
        
        default: {
            return DefWindowProcW(window, msg, w_param, l_param);
        }
    }
    
    return 0;
}

int WINAPI WinMain(HINSTANCE instance, HINSTANCE prev_instance, 
                   PSTR cmd, int cmd_show) {
    
    WNDCLASSW wc = {
        .lpszClassName = L"MyWindowClass",
        .lpfnWndProc = win_proc,
        .hInstance = instance,
        .hCursor = LoadCursor(0, IDC_CROSS)
    };
    
    ATOM atom = RegisterClassW(&wc);
    Assert(atom && "Failed to register a window");
    
    HWND window = CreateWindowW(wc.lpszClassName, 
                                L"Title", 
                                WS_OVERLAPPEDWINDOW, 
                                CW_USEDEFAULT, CW_USEDEFAULT, 
                                CW_USEDEFAULT, CW_USEDEFAULT, 
                                NULL, NULL, instance, NULL);
    Assert(window && "Failed to create a window");
    
    ShowWindow(window, cmd_show);
    
    for(;;) {
        MSG msg;
        if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
            if(msg.message == WM_QUIT) break;
            TranslateMessage(&msg);
            DispatchMessage(&msg);
            continue;
        }    
    }
    return 0;
}