Updated Sep 20, 2020

C - Window Creation (Win32)

How to use Windows API (Win32) to create a window.

Watch video on YouTube: https://www.youtube.com/watch?v=6JJ8e5IZ8S

Command line build tools

Install Visual Studio. It comes with the Microsoft C++ (MSVC) compiler toolset that allows us to build programs on the command line. The actual IDE is only used for debugging. For text editing I use https://4coder.net/.

Note: make sure to select Desktop development with C++ workload when installing Visual Studio. If you already have Visual Studio installed, open the Visual Studio Installer and select modify to add the workload.

Developer command prompt

Option 1

  • Open Start menu and search for "developer". You should a see shortcut named Developer Command Prompt for VS [version].
  • Click Open file location and open the VC directory. Double-click the x64 Native Tools Command Prompt for VS [version] shortcut.

Option 2

This is how I do it. I have a shortcut on the desktop with Target set to this:

%comspec% /k c:\Users\samul\stuff\setup.bat

This will open a new cmd.exe interpreter instance and run the setup.bat script.

  • %comspec locates the command-interpreter (cmd.exe).
  • /k option runs the setup.bat script and leaves the command prompt open.

The actual target will be set to this when you save the shortcut:

%windir%\system32\cmd.exe /k c:\Users\samul\stuff\setup.bat

setup.bat has these lines in it:

@echo off
call "c:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat"
set path=C:\Users\samul\stuff;%path%
  • echo off prevents commands from echoing on the screen. @ prevents the echo off command itself from echoing.
  • call calls a batch program from a batch program without stopping the parent batch. The set path.. line doesn't have any effect unless you use the call command.
  • vcvars64.bat sets the environment so we can use the cl tool to build 64-bit code.
  • set path=C:\Users\samul\stuff;%path% adds the stuff directory to the path variable.

I use the stuff directory to store batch files for various shortcuts. For example, there is a file called 4.bat with these lines:

@echo off
start C:\Users\samul\Programs\4coder-4-1-1\4ed.exe -W

That allows me to open 4coder in the current directory by typing "4" and hitting enter.

  • start runs a specific program or command.
  • -W flag starts the editor maximized (-F would make it fullscreen).

Test the compiler

Run cl on the command line and you should see something like this:

Microsoft (R) C/C++ Optimizing Compiler Version 19.27.29111 for x64

Build script

Let's create a simple test program. Create a file called main.c somewhere and add these lines to it:

#include <stdio.h>

int main()
{
    printf("Hello");
    return 0;
}

Create a file called build.bat and add these lines to it:

@echo off
cl main.c /Zi /nologo /DUNICODE /D_UNICODE user32.lib
  • /Zi option produces debugging data for debuggers.
  • /nologo disables the Microsoft copyright banner.
  • Some functions are implemented in multiple formats: one that deals with UNICODE (wide) characters and one that doesn't. These UNICODE options makes sure that the wide version is called. /DUNICODE is for the Windows API: CreateWindowEx() gets replaced with CreateWindowExW(). /D_UNICODE is for the C runtime: _TEXT(..) maps to L"..", not ".." and so on.
  • CreateWindowEx() and bunch of other Windows functions require the user32.lib dependency.

In 4coder you can use alt+m to run the build.bat file. You should see something like this:

main.c
exited with code 0

Run this on the command line and you should see Hello:

main

You can also open (and debug) the program with Visual Studio like this:

devenv main.exe

Some debugging shortcuts:

  • f11 steps into the program.
  • f10 steps forward one step.
  • f9 toggles a breakpoint.
  • f5 runs the program.
  • shift + f5 stops the program. This is a clean way to close it.

wWinMain

Edit main.c and replace the contents with these lines:

#include <windows.h>

int WINAPI 
wWinMain(HINSTANCE instance, 
         HINSTANCE prev_instance, 
         PWSTR cmd_line, 
         int cmd_show)
{
    return 0;
}
  • windows.h includes the Windows API declarations.
  • WINAPI is a calling convention. This affects at least these things: 1) in which order (right to left or left to right) parameters are pushed to the memory 2) who cleans the parameters from memory (caller or callee).
  • wWinMain is the program entry point. w means that the command line arguments are passed to this function as a wide (UNICODE) string.
  • instance is a handle to the application instance. A handle is an abstracted reference to some internal resource.
  • prev_instance is a legacy parameter that is always zero.
  • cmd_line contains the command line arguments.
  • With cmd_show you can pass a display hint to the application. For example, a shortcut has a Run value (Normal window, minimized, maximized) that is passed in with this parameter. You can do something with that if you want.

WNDCLASS

Every window has to be associated with a window class struct that defines a set of behaviours for it:

#include <windows.h>

int WINAPI 
wWinMain(HINSTANCE instance, 
         HINSTANCE prev_instance, 
         PWSTR cmd_line, 
         int cmd_show) 
{
    // START

    WNDCLASS window_class = {0};
    
    wchar_t class_name[] = L"GameWindowClass";

    window_class.lpfnWndProc = WindowProc;
    window_class.hInstance = instance;
    window_class.lpszClassName = class_name;

    RegisterClass(&window_class);
    
    // END
    
    return 0;
}
  • WNDCLASS window_class = {0}; creates the window_class structure and initializes it to zero.
  • class_name[] is a string that specifies the name of the class. wchar_t is a wide character type. We use it to store UNICODE characters. L before L"GameWindowClass" makes it a wide-character string literal.
  • lpfnWndProc defines a default procedure for the window that processes its messages.
  • lpszClassName specifies the window class name.
  • RegisterClass(&window_class) registers the window class so we can use it to create a window.

CreateWindowExW

We will be using the CreateWindowExW function to create a window:

#include <windows.h>

int WINAPI 
wWinMain(HINSTANCE instance, 
         HINSTANCE prev_instance, 
         PWSTR cmd_line, 
         int cmd_show) 
{
    WNDCLASS window_class = {0};
    
    wchar_t class_name[] = L"GameWindowClass";
    window_class.lpfnWndProc = WindowProc;
    window_class.hInstance = instance;
    window_class.lpszClassName = class_name;
    
    RegisterClass(&window_class);

    // START

    HWND window = CreateWindowEx(0,
                                 class_name,
                                 L"Game",
                                 WS_OVERLAPPEDWINDOW|WS_VISIBLE,
                                 CW_USEDEFAULT,
                                 CW_USEDEFAULT,
                                 CW_USEDEFAULT,
                                 CW_USEDEFAULT,
                                 0,
                                 0,
                                 instance,
                                 0
                                 );
    // END
    
    return 0;
}
  • L"Game" displays "Game" in the title bar.
  • WS_OVERLAPPEDWINDOW is a combination of many styles that you would usually want (title bar, sizing border, minimize / maximize button etc). WS_VISIBLE makes the window visible.
  • CW_USEDEFAULT sets default values for the position and window dimensions.

The complete parameter descriptions can be found in here.

Game loop

Add a game loop:

#include <windows.h>

// START

int running = 1;

// END

int WINAPI 
wWinMain(HINSTANCE instance, 
         HINSTANCE prev_instance, 
         PWSTR cmd_line, 
         int cmd_show)
{
    WNDCLASS window_class = {0};
    
    // ...                               
    
    // START

    while(running)
    {
        MSG message;
        while(PeekMessage(&message, window, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&message);
            DispatchMessage(&message);
        }
        
    }

    // END
    
    return 0;
}
  • PeekMessage fetches messages from the message queue.
  • TranslateMessage(&message) translates virtual-key messages into character messages.
  • DispatchMessage(&message) dispatches the message to the WindowProc function.

TranslateMessage produces WM_CHAR messages for textual characters with the character value in the WindowProc function w_param parameter. Hit the 'h' key and there will be a WM_CHAR message with the value 'h' in the w_param parameter. React to WM_KEYDOWN messages if you want to react to non-textual keys like arrow keys or numpad.

WindowProc

Add a function called WindowProc to the program. This is where we handle window messages:

#include <windows.h>

int running = 1;

// START

LRESULT CALLBACK 
WindowProc(HWND window,
           UINT message,
           WPARAM w_param,
           LPARAM l_param
           )
{
    LRESULT result = 0;
    
    switch(message)
    {
        case WM_CLOSE:
        {
            running = 0;
        } break;
        
        default:
        {
            result = DefWindowProc(window, message, w_param, l_param);
        } break;
    }
    
    return result;
}

// END

int WINAPI wWinMain(..) {..}
  • WM_CLOSE message is sent to the window when we try to close it.
  • DefWindowProc() adds default message handling behaviour for all messages that we don't handle ourselves.

Final files

build.bat

@echo off
cl main.c /Zi /nologo /DUNICODE /D_UNICODE user32.lib

main.c

#include <windows.h>

int running = 1;

LRESULT CALLBACK 
WindowProc(HWND window,
           UINT message,
           WPARAM w_param,
           LPARAM l_param
           )
{
    LRESULT result = 0;
    
    switch(message)
    {
        case WM_CLOSE:
        {
            running = 0;
        } break;
        
        default:
        {
            result = DefWindowProc(window, message, w_param, l_param);
        } break;
    }
    
    return result;
}

int WINAPI 
wWinMain(HINSTANCE instance, 
         HINSTANCE prev_instance, 
         PWSTR cmd_line, 
         int cmd_show)
{
    WNDCLASS window_class = {0};
    
    wchar_t class_name[] = L"GameWindowClass";
    window_class.lpfnWndProc = WindowProc;
    window_class.hInstance = instance;
    window_class.lpszClassName = class_name;
    
    RegisterClass(&window_class);
    
    HWND window = CreateWindowEx(0,
                                 class_name,
                                 L"Game",
                                 WS_OVERLAPPEDWINDOW|WS_VISIBLE,
                                 CW_USEDEFAULT,
                                 CW_USEDEFAULT,
                                 CW_USEDEFAULT,
                                 CW_USEDEFAULT,
                                 0,
                                 0,
                                 instance,
                                 0
                                 );
    
    while(running)
    {
        MSG message;
        while(PeekMessage(&message, window, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&message);
            DispatchMessage(&message);
        }
        
    }
    
    return 0;
}