Quickstart

main.c

#include <windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, 
                            UINT msg, 
                            WPARAM wParam, 
                            LPARAM lParam)
{
    switch (msg)
    {

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

        default:
        {
            return DefWindowProc(hwnd, msg, wParam, lParam);
        } break;
    }
    return 0;
}

int WINAPI wWinMain(HINSTANCE hInstance, 
                    HINSTANCE pInstance,
                    PWSTR cmd,
                    int showCmd) 
{
    WNDCLASS wc = {0};

    const wchar_t CLASS_NAME[]  = L"Tutorial";

    wc.lpszClassName = CLASS_NAME;
    wc.lpfnWndProc   = WindowProc;
    wc.hInstance     = hInstance;

    RegisterClass(&wc);

    HWND hwnd;

    hwnd = CreateWindow(CLASS_NAME, CLASS_NAME,
                        WS_OVERLAPPEDWINDOW|WS_VISIBLE,
                        0,0,640,480,0,0,hInstance,0);

    if (hwnd == NULL)
    {
        return 0;
    }

    ShowWindow(hwnd, showCmd);

    MSG msg = {0};

    int running = 1;

    while(running) 
    {
        while(PeekMessage(&msg,0,0,0,PM_REMOVE))
        {
            if(msg.message == WM_QUIT)
            {
                running = 0;
            }

            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

        // update, render
    }

    return 0;
}

build.bat

@echo off
cl main.c /Fegame.exe /Zi /D "UNICODE" /D "_UNICODE" /nologo user32.lib gdi32.lib

Full tutorial

I'm going to use the Microsoft command line build tools to compile and link our program. An easy way to get the toolset, is to install Visual Studio: https://visualstudio.microsoft.com/. Next we need to run a script called vcvarsall.bat. It sets environment variables so we can build our program on the command line.

Let's make it easier for us and create a script that runs the vcvarsall.bat file . Create a file called setup.bat somewhere. Mine is in here: C:\Users\samul\stuff\setup.bat.

@echo off
call "c:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" x64
set path=C:\Users\samul\stuff;%path%
  • @echo off means that we don't want to print out these commands when we run the script.
  • With the call command we call a batch program from another batch program.

The path to the vcvarsall.bat file can be different in your system. The x64 argument denotes the target architecture. I'm also adding the stuff directory to the path variable so I can run things inside that directory everywhere. For example I have a file called 4.bat inside the stuff directory with these lines in it:

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

The start command launches an application. This allows me to open the 4coder editor on the command line using the number 4. The -W flag puts the editor in a windowed fullscreen mode. You can get 4coder from here: https://4coder.net/.

Create a desktop shortcut

Let's create a shortcut that opens up the Command Prompt and runs our setup.bat script. Search for Command Prompt and select Open file location. Use CTRL to make a copy of it on the desktop. Right-click the shortcut and select Properties. Write this line in the Target field:

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

The /k option runs our setup.bat script and returns to the command prompt. Double-click the shortcut to start developing. Now you should be able to build programs with the cl tool.

main.c & build.bat

Create a directory somewhere for the project and add a file called main.c to it:

#include <stdio.h>

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

Create a file called build.bat next to it:

@echo off
cl main.c

In 4coder you can run the build.bat script file using ALT + m. Run the program in the command prompt using main.exe.

There are bunch of compiler options that you might want to specify. Here is what we are going to use for our program:

@echo off
cl main.c /Fegame.exe /Zi /D "UNICODE" /D "_UNICODE" /nologo user32.lib gdi32.lib
  • /Fe specifies the output executable name.
  • /Zi generates debugging info for debuggers.
  • /D defines a preprocessing symbol. We could use the #define directive at the beginning of the code but I find this to be a cleaner approach. Windows uses UTF-16 encoding to represent Unicode characters. UTF-16 characters are called wide characters. Some functions have wide and ANSI versions. But because we define the UNICODE preprocessing symbol in the compiler options, we are automatically using the wide version of a function. For example, we can just use the function MessageBox() and it will actually use MessageBoxW(), the wide version of the function.
  • /nologo suppresses the copyright banner.
  • user32.lib provides the core functionality for creating user interfaces, like window management and input processing.
  • gdi32.lib provides functions to output graphics content.

Run the build.bat file and start debugging the program with Visual Studio using devenv game.exe. This tutorial is not about debugging but here are some things you can do:

  • Use F5 to start debugging.
  • Hit F11 to "step into it" and start adding breakpoints and watching variables with the Watch window.
  • Use Shift + F5 to stop debugging.
  • Use Ctrl + F5 to start the program without debugging.

Window Creation

Clear the main.c file and include the windows.h header file:

#include <windows.h>

The windows.h header file contains Windows API function declarations, macros and data types that allow you to create Windows applications.

Here is the general idea:

  • Add a function called wWinMain().
  • Initialize the application.
  • Create a window.
  • Add a game loop that runs until we tell it to stop.
  • We use the WindowProc() function to handle messages. In this case we use it to stop our game loop when the window is closed.
WindowProc();
wWinMain() {
    // initialize the app
    // create a window
    // game loop
}

For console programs we usually add a function named main() to serve as the program entry point. With graphical Windows-based applications we use a function called wWinMain().

int WINAPI wWinMain(HINSTANCE hInstance, 
                   HINSTANCE pInstance,
                   PWSTR cmd,
                   int showCmd) {
}
  • WINAPI is a calling convention. It determines things like how parameters to this function are managed in memory.
  • hInstance is a handle to the current instance of the application. The operating system uses this to identify the executable in memory. You can think it as the program id.
  • pInstance is a handle to the previous instance of the application. This is a legacy parameter that is always NULL in win32.
  • The cmd parameter contains any command line arguments you might provide to the program. PWSTR means that the cmd parameter is a pointer to a wide string.
  • The showCmd flag specifies how the window is displayed, like if it's maximized or not.

We will test these last two arguments after we have created a window succesfully.

Let's create a window class:

int WINAPI wWinMain(HINSTANCE hInstance, 
                   HINSTANCE pInstance,
                   PWSTR cmd,
                   int showCmd) 
{
    WNDCLASS wc = {0};

    const wchar_t CLASS_NAME[]  = L"Tutorial";

    wc.lpszClassName = CLASS_NAME;
    wc.lpfnWndProc   = WindowProc;
    wc.hInstance     = hInstance;

    RegisterClass(&wc);
}        

Every window needs to be associated with a WNDCLASS data structure.

  • CLASS_NAME[] is the name of the class. Use L before the string to declare a wide-character string literal.
  • WindowProc is a pointer to the application window procedure function that we use to process messages.
  • We also need to specify the application instance handle with wc.hInstance = hInstance.
  • Register the class with the operating system using the RegisterClass(&wc) function.

To actually create the window we use the CreateWindow() function:

int WINAPI wWinMain(HINSTANCE hInstance, 
                    HINSTANCE pInstance,
                    PWSTR cmd,
                    int showCmd) 
{
    // ...

    HWND hwnd;

    hwnd = CreateWindow(CLASS_NAME,CLASS_NAME,
                        WS_OVERLAPPEDWINDOW|WS_VISIBLE,
                        0,0,640,480,0,0,hInstance,0);

    if (hwnd == NULL)
    {
        return 0;
    }

    ShowWindow(hwnd, showCmd);

The CreateWindowW() function parameters:

void CreateWindowW(
   lpClassName,
   lpWindowName,
   dwStyle,
   x,
   y,
   nWidth,
   nHeight,
   hWndParent,
   hMenu,
   hInstance,
   lpParam
);
  • lpClassName: the class name we registered using the RegisterClass function.
  • lpWindowName: the window name. This will be displayed in the window title bar.
  • dwStyle: the style of the window. The WS_OVERLAPPEDWINDOW style enables things like minimize and maximize buttons. WS_VISIBLE makes the window visible.
  • x: initial horizontal position.
  • y: Initial vertical position.
  • nWidth: the width of the window.
  • nHeight: the height of the window.
  • hWndParent: handle to the parent or owner of the window.
  • hMenu: handle to a menu.
  • hInstance: handle to the application instance.
  • lpParam: you can use the last argument to pass additional data to the window procedure function.

The ShowWindow function sets the window's show state. Like if its maximized or not using the showCmd parameter.

Next let's add the game loop:

    MSG msg = {0};

    int running = 1;

    while(running) 
    {
        while(PeekMessage(&msg,0,0,0,PM_REMOVE))
        {
            if(msg.message == WM_QUIT)
            {
                running = 0;
            }

            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

        // update, render
    }

    return 0;

The operating system communicates with your application window by passing messages to it. For example, if you press the "a" key, a specific message is added to the message queue. The PeekMessage() function retrieves messages from that queue.

BOOL PeekMessageW(
  LPMSG lpMsg,
  HWND  hWnd,
  UINT  wMsgFilterMin,
  UINT  wMsgFilterMax,
  UINT  wRemoveMsg
);
  • lpMsg: pointer to the MSG structure to store message information.
  • hWnd: handle to a window. I use 0 here so we are retrieving messages to any window.
  • wMsgFilterMin and wMsgFilterMax allows us to filter messages. Leave these to zero to return all available messages.
  • wRemoveMsg specifies how messages are to be handled. PM_REMOVE removes the messages from the queue after they have been processed.

When you press a key, a scan code is generated by the hardware. These vary from keyboard to keyboard. The keyboard driver translates scan codes into virtual-key codes. The TranslateMessage() function translates these virtual-key messages into character messages.

The DispatchMessage() function dispatches a message to the window procedure function.

And finally let's add the WindowProc function. We typically put here a switch statement that switches on the message code. In this case we will just react to the WM_DESTROY message:

LRESULT CALLBACK WindowProc(HWND hwnd, 
                            UINT msg, 
                            WPARAM wParam, 
                            LPARAM lParam)
{
    switch (msg)
    {

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

        default:
        {
            return DefWindowProc(hwnd, msg, wParam, lParam);
        } break;
    }
    return 0;
}

WinMain(...)

LRESULT is an integer value that the program returns to Windows. The meaning of the value depends on the message code.

  • CALLBACK is the function calling convention.
  • hwnd: handle to the window.
  • msg: message code.
  • wParam and lParam contains additional data related to the message.

The WM_DESTROY message is sent to the WindowProc function after the window is removed from the screen. This is a good place to send the WM_QUIT message that exits our game loop. The PostQuitMessage() puts the WM_QUIT message on the message queue.

The DefWindowProc() function calls the default window procedure that provides default processing for messages that we don't handle in our application. This ensures that all messages are processed.

This is what happens when you close a window:

  • WM_CLOSE message is sent to the window.
  • This triggers the DestroyWindow() function that sends the WM_DESTROY message.
  • We react to the WM_DESTROY message by calling the PostQuitMessage() function.
  • This adds the WM_QUIT message to the message queue that stops our game loop.

Run the build.bat script and run the game.exe executable to open a window.

We could handle the WM_CLOSE message ourselves and for example open up a confirmation MessageBox:

switch (msg)
    {

        // START

        case WM_CLOSE:
        {
            if(MessageBox(hwnd, L"Quit?", L"App", MB_OKCANCEL) == IDOK)
            {
                DestroyWindow(hwnd);
            }
            return 0;
        } break;

        // END

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

        default:
        {
            return DefWindowProc(hwnd, msg, wParam, lParam);
        } break;
    }
  • MB_OKCANCEL is the type of the button. It contains Ok and Cancel buttons.
  • IDOK is the MessageBox() function return value that indicates that the OK button was selected.

Window client area size

At the moment, the window dimensions (640 x 480) include the title bar, menus and scroll bars. But sometimes you want to create a window client area with specific dimensions. The client area is the area where we draw things.

Let's use the AdjustWindowRect() function to do that. It calculates the correct window size based on the desired client-rectangle size. Make the following changes:

    // START

    int width = 640;
    int height = 480;
    RECT wr = {0, 0, width, height};
    AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE);

    // END

    hwnd = CreateWindow(CLASS_NAME, CLASS_NAME,
                        WS_OVERLAPPEDWINDOW|WS_VISIBLE,
                        0,0,
                        wr.right - wr.left, // HERE
                        wr.bottom - wr.top, // HERE
                        0,0,hInstance,0);
  • The RECT structure defines a rectangle by its top-left and bottom-right coordinates.

The AdjustWindowRect() function parameters:

BOOL AdjustWindowRect(
  LPRECT lpRect,
  DWORD  dwStyle,
  BOOL   bMenu
);
  • lpRect is the pointer to the RECT structure.
  • dwStyle is the style of the window we want to calculate size for.
  • bMenu indicates if the window has menu or not.

The new window size has the dimensions of our 640 x 480 pixel client area plus whatever is needed to render the non-client area. We can get width and height of the window using these calculations:

  • wr.right - wr.left gets us the width.
  • wr.bottom - wr.top gets us the height.

Testing wWinMain parameters

You can test the cmd and showCmd parameters using these lines:

int WINAPI wWinMain(...) 
{

// START

MessageBox(0, cmd, L"Test cmd parameter", 0);

// create a string with the showCmd parameter integer value using the wsprintfW() function:

wchar_t buffer[256];   
wsprintfW(buffer, 
          L"cmd: (%d)", 
          showCmd
          );
MessageBox(0, buffer, L"Test showCmd parameter", 0);

// END

WNDCLASS wc = {0};
...

Run the program with a command line argument:

game.exe hello

Create a shortcut to the game.exe file and change the Run option to "Minimized" to see the showCmd value change. Double-click the shortcut to run the program.

You can also output stuff in the Visual Studio debug window using the OutputDebugStringA() function:

...

OutputDebugStringA("\nHello\n");

WNDCLASS wc = {0};