Getting Used to Win32
Table of Contents
One of the earlier projects I have done is writing a UI framework in C++. The oldest files of this project were last modified around the beginning of 2017. My goal was to have a completely self-built way to create windows and draw in them using the win32 API and the directX library. Among other projects, I implemented my version of Minesweeper and a game of life with this framework.
The main achievement of this is an object-oriented wrapper around the win32 API and the creation of several data classes in C++, which I will show in this series of articles. Let me start by showing how to create and use a window with the win32 API in C++.
The First Window
Spawning a Window
To spawn a window, we need to first register a window class and then create the window with this window class. The window class contains information about the events of the window by requiring a function that is called for every event the window receives. Multiple windows can have the same window class and thus the same event function. Let us start with the function for registering the window class:
void registerWindowClass(WNDPROC MSGRouter) {
WNDCLASSEX windowclass;
ZeroMemory(&windowclass, sizeof(WNDCLASSEX));
windowclass.cbSize = sizeof(WNDCLASSEX);
windowclass.hInstance = GetModuleHandle(nullptr);
windowclass.lpfnWndProc = MSGRouter;
windowclass.lpszClassName = L"MainWindowClass";
windowclass.style = CS_HREDRAW | CS_VREDRAW;
windowclass.hbrBackground = nullptr;
RegisterClassEx(&windowclass);
}The argument MSGRouter is the event-handling function. The other interesting parameters are style, hbrBackground: all values for the style can be found here usually we just need CS_HREDRAW | CS_VREDRAW to redraw when the dimensions of the window change. The hbrBackground is the background color of the window, if set to nullptr the background is transparent and needs to be colored in manually. Now for spawning the window:
HWND spawnWindow(
float positionX,
float positionY,
float width,
float height,
std::string title,
LONG style) {The RECT are the window dimensions which we adjust for the title bar, borders, etc. such that width and height are the outer dimensions of the window. This depends on the style, which is a different kind of style than the style of the window class.
RECT rect = { 0, 0, width, height };
AdjustWindowRectEx(&rect, style, false, NULL);When we create the window we pass the title, the window class, the position and dimensions. The last line sets parent handle, menu handle and additional parameters to nullptr. Additional parameters are passed to the event function at creation of the window.
We also pass two styles: dwExStyle the first argument and dwStyle the argument passed through in the style variable. For the former we hardcode WS_EX_LAYERED to support a window with transparent background.
HWND winHandle = CreateWindowExW(
WS_EX_LAYERED,
L"MainWindowClass",
std::wstring(title.begin(), title.end()).c_str(),
style,
positionX, positionY,
rect.right - rect.left, rect.bottom - rect.top,
nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
if (!winHandle)
return nullptr;For this simple example we use a green screen way to do alpha values: if at any pixel the color is RGB(255, 255, 255) (i.e. white) it is rendered as see through. If we put LWA_ALPHA instead of LWA_COLORKEY as the last function input we set the opacity of the entire window to the second last argument. It is more involved to have per pixel transparency the vanilla win32 way and we will not do it this way in the drawing engine later anyway, so I will ignore it for now.
SetLayeredWindowAttributes(
winHandle,
RGB(255, 255, 255),
0, // 0 to 255!
LWA_COLORKEY);
ShowWindow(winHandle, SW_SHOW);
UpdateWindow(winHandle);
return winHandle;
}Before we can spawn the window we need to define the event-handling function:
LRESULT MessageRouter(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam) {
switch (uMsg) {
case WM_CLOSE:
DestroyWindow(hwnd);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}We then run these functions in a separate thread to spawn the window. We could also just run them in the main thread, however for the UI framework they are separate as well so drawing the window content is not blocked by anything else.
int main()
{
std::thread windowThread = std::thread([]() {
registerWindowClass(MessageRouter);
HWND windowHandle
= spawnWindow(
50, 50,
100, 100,
"Hello World",
WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN);
if (!windowHandle)
return;
MSG msg;
while (GetMessage(&msg, windowHandle, 0, 0)
&& IsWindow(windowHandle)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
});
windowThread.join();
return 0;
}In summary, so far we register a window class this window class knows the event-handling function MessageRouter and the window itself knows its window class via the classes name. In this case L"MainWindowClass".
Drawing into the Window
We draw in this window by
- telling the window we are about to draw
HDC hdc = BeginPaint(hwnd, &ps);,- We receive a
PAINTSTRUCT psand aHDC hdc. - The paintstruct is essentially only used for staring and ending the painting process.
- The hdc is used to draw on the window.
- We receive a
- creating a brush
CreateSolidBrush(RGB(255, 255, 255)), - which we use to draw e.g.
FillRect(hdc, &cellRect, brush);.
We show this on the example of the game of life where we also add a timer to the window and then adding WM_PAINT and WM_TIMER cases to the MessageRouter function:
const int GRID_WIDTH = 50;
const int GRID_HEIGHT = 50;
const int CELL_SIZE = 10;
std::vector<std::vector<bool>> grid(GRID_WIDTH, std::vector<bool>(GRID_HEIGHT));
std::vector<std::vector<bool>> newGrid(GRID_WIDTH, std::vector<bool>(GRID_HEIGHT));
// implement these according to the game of life rules
void initializeGrid() {}
int countNeighbors(int x, int y) {}
void updateGrid() {}
void paint(HWND hwnd) {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
RECT clientRect;
GetClientRect(hwnd, &clientRect);
FillRect(hdc, &clientRect, (HBRUSH)(COLOR_WINDOW + 1));
HBRUSH brush = CreateSolidBrush(RGB(255, 255, 255));
for (int x = 0; x < GRID_WIDTH; ++x) {
for (int y = 0; y < GRID_HEIGHT; ++y) {
if (grid[x][y]) {
RECT cellRect;
cellRect.left = x * CELL_SIZE;
cellRect.top = y * CELL_SIZE;
cellRect.right = cellRect.left + CELL_SIZE;
cellRect.bottom = cellRect.top + CELL_SIZE;
FillRect(hdc, &cellRect, brush);
}
}
}
DeleteObject(brush);
EndPaint(hwnd, &ps);
}
LRESULT MessageRouter(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam) {
switch (uMsg) {
// other cases
case WM_PAINT:
paint(hwnd);
break;
case WM_TIMER:
updateGrid();
InvalidateRect(hwnd, nullptr, TRUE);
break;
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}
int main()
{
std::thread windowThread = std::thread([]() {
registerWindowClass(MessageRouter);
HWND windowHandle
= spawnWindow(
50, 50,
GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE,
"Game of Life", WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN);
if (!windowHandle) return;
initializeGrid();
SetTimer(windowHandle, 1, 100, nullptr);
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
});
windowThread.join();
return 0;
}
Summary
I think this article demonstrates well why I started this project, since working with the win32 API directly is very bloated if the goal is to “just draw in a window”. The next article will show how I wrapped this into a class that manages all spawned windows, that way we do not have to work with the win32 API directly.