Making Money with Django and AI: How to Build SaaS Services Using Python

Learn how to build AI-driven websites with the Django web framework and monetize your services.
Read more
Win32 - Window Creation Tutorial (C)
How to create a window using Windows API.
Updated Nov 14, 2023

Quick Start

// cl main.c /nologo /Zi

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

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

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

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

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

Step-By-Step Tutorial

Prerequisites

  • cl.exe, the command-line compiler. To use it, you can either install Visual Studio and select the C++ Workload in the installation options 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, making the script execute more silently. 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 evoke 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 graphical user interface (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>

#pragma comment (lib, "user32.lib")
#define Assert(c) do { if (!(c)) __debugbreak(); } while (0)

int WINAPI WinMain(
    HINSTANCE instance,
    HINSTANCE pInstance,
    LPSTR     cmd,
    int       cmdShow) {

    OutputDebugString("Hello World!\n");

    return 0;
}
  • WIN32_LEAN_AND_MEAN excludes some of the less commonly used parts of the Windows API.
  • The windows.h header provides access to a variety of functions, data types, and macros necessary for developing Windows applications.
  • #pragma comment (lib, "user32.lib") is a preprocessor directive that instructs the linker to include the user32.lib library during the linking phase of compilation. You can also link it like this: cl main.c /nologo /Zi /link user32.lib.
  • #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. In development, I find this more useful than a message box.
  • int WINAPI specifies the return type and calling convention of the WinMain function. A return value of 0 often signifies successful execution, while a non-zero value may indicate an error or abnormal termination.
  • 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 module (e.g., .exe file). The actual structure and content of the internal data referred to by HINSTANCE are not part of the public Windows API. They are abstracted away from application developers.
  • pInstance (previous 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".
  • cmdShow 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, cmdShow); command.

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

With GUI programs, we typically use OutputDebugString() to print to the IDE's output window. This program will compile and run, but you won't see anything in the terminal.

But in your debugger, such as RemedyBG, you will see this:

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 pInstance,
    LPSTR     cmd,
    int       cmdShow) {

    OutputDebugString("Hello World!\n");

    // Here
    WNDCLASSW wc = {
        .lpszClassName = L"MyWindowClass",
        .lpfnWndProc = WindowProc,
        .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 (UTF-16) 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). The purpose of including a descriptive message is to provide meaningful information about why the assertion failed.

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 WindowProc(HWND window, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch(msg) {
        case WM_DESTROY: {
            PostQuitMessage(0);
        } break;
        case WM_KEYDOWN: {
            switch(wParam) {
                case 'O': {
                    DestroyWindow(window);
                } break;
            }
        } break;
        default: {
            return DefWindowProcW(window, msg, wParam, lParam);
        }
    }
    return 0;
}
  • LRESULT is usually a 32-bit signed value that is used to indicate the result or status of an operation.
  • CALLBACK is a macro that defines the stdcall calling convention.
  • wParam and lParam 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, wParam contains the virtual-key code of the pressed key. In mouse-related messages like WM_MOUSEMOVE, lParam often contains the mouse cursor's screen coordinates.
  • WM_DESTROY is typically 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"Window Creation", 
        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, cmdShow);
  • Each window in a Windows application is assigned a unique HWND (handle to a window) that distinguishes it from other windows. It is typically declared as a pointer to a structure that contains information about the window.
  • wc.lpszClassName specifies the class of the window.
  • L"Window Creation" 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 WindowProc 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. This allows you to perform initialization tasks.
  • ShowWindow(window, cmdShow); sets the window's show state using the cmdShow parameter.

The Main Loop

Add this after ShowWindow(window, cmdshow);:

    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 (;;) is an infinite loop that 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 WindowProc.
  • If a message was processed, the loop continues to the next iteration.

Source Code