Table of contents
Quick Start
// 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 winproc(HWND window, UINT msg, WPARAM wparam, LPARAM lparam) {
switch(msg) {
case WM_KEYDOWN: {
switch(wparam) {
case 'O': {
DestroyWindow(window);
} break;
}
} break;
case WM_DESTROY: {
PostQuitMessage(0);
} break;
default: {
return DefWindowProcW(window, msg, wparam, lparam);
}
}
return 0;
}
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previnstance, PSTR cmd, int cmdshow) {
WNDCLASSW wc = {
.lpszClassName = L"MyWindowClass",
.lpfnWndProc = winproc,
.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, 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;
}
Tutorial
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. Thedo-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 theuser32.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 theWinMain
function. A return value of0
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 byHINSTANCE
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, whileSW_SHOWMAXIMIZED
would display the window in a maximized state. We will use this later with theShowWindow(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. TheL
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. TheNULL
parameter indicates that the cursor is a standard system cursor, andIDC_CROSS
specifies the crosshair cursor type.RegisterClassW()
informs the OS about the window class. TheW
indicates that it deals with wide-character strings, as opposed to the ANSI version, which isRegisterClassA
.- 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 thestdcall
calling convention.
w_param
andl_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 ofWM_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 includeWM_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 theWM_CREATE
message when the window is being created. TheWM_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 thebreak
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 themsg
variable.if (msg.message == WM_QUIT) break;
checks if the message isWM_QUIT
and, if true, exits the loop. This is the message sent to the window viaPostQuitMessage(0)
.TranslateMessage(&msg)
is used to translate virtual-key messages into character messages.
DispatchMessage(&msg)
dispatches the message towin_proc
.
Leave a comment