For our introduction project, our goal will not be to develop a playable game. Instead, we will focus on the basics like creating a window, drawing and animating some graphics, and perhaps play some sound effects.
In Visual Studio I will create a new project. In the project creation wizard I will filter the project templates by C++, Windows and Console, and then select the “Empty project” template. Once the project is created, I will open the project settings dialog and set the following options for all platforms and all configurations:
- General \ C++ Language Standard: ISO C++20 Standard
- Advanced \ Character Set: Use Multi-Byte Character Set
- Linker \ System \ SubSystem: Windows
Now we are ready to start coding! I will not include all the code here, but you can check out the commit named “Introduction: Creating a window” in the GitHub repository to see the complete code for this blog post.
At this point, our program only consists of one file (main.cpp) containing two functions – WindowProc()
and WinMain().
By setting the SubSystem linker option to “Windows”, we tell Visual Studio to use the WinMain()
function as the application’s entry point, instead of the usual main()
function.
In WinMain()
we basically do two things. First we create a window and display it on the screen, and then we initiate the application’s main loop, also known as the “message pump”.
Creating and displaying a window
In order to be able to create a window, we must first create a window class. This is done by calling a function of the Windows API named RegisterClassEx()
. This function takes a pointer to a WNDCLASSEX
structure as a parameter, so we must first create an instance of that structure and fill it with the necessary information.
WNDCLASSEX windowClass{ };
windowClass.cbSize = sizeof(WNDCLASSEX);
windowClass.style = CS_HREDRAW | CS_VREDRAW;
windowClass.lpfnWndProc = static_cast<WNDPROC>(WindowProc);
windowClass.hInstance = instance;
windowClass.hIcon = LoadIcon(nullptr, IDI_WINLOGO);
windowClass.hCursor = LoadCursor(nullptr, IDC_ARROW);
windowClass.lpszClassName = windowClassName.c_str();
if (!RegisterClassEx(&windowClass))
{
MessageBox(nullptr, "Unable to register window class", "Error", MB_OK);
return 0;
}
In this blog I will generally not go into details on how API functions work, nor will I describe every singIe field of the structures they take as parameters – I will simply provide a link to the documentation so you can read it yourself.
On the topic of the Windows API, we must of course include the windows.h header file to use any of its functionality. The observant reader will also notice that we define the macro WIN32_LEAN_AND_MEAN
before including the header. This macro was introduced with Windows 95 as a way to reduce build times. The Windows API header will include a number of other header files behind the scenes, but if the WIN32_LEAN_AND_MEAN
macro is defined, some of the more rarely-used ones will be excluded.
Granted, with the vastly greater computing power we have today, this doesn’t make nearly as much of a difference as it did back then, but I say we take what we can get. Enabling precompiled headers is another possible optimization that would likely have an even greater effect on our build times, so we will probably do that too when we have completed our introduction project and move on to something else. Anyway, that’s enough of a digression for now, so let’s get back to coding!
Now that we have our window class, the next step is to create our window. For this we use another Windows API function named CreateWindow()
.
HWND mainWindow = CreateWindow(windowClassName.c_str(), mainWindowTitle.c_str(), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, instance, nullptr);
if (nullptr == mainWindow)
{
MessageBox(nullptr, "Unable to create main window", "Error", MB_OK);
return 0;
}
The WS_OVERLAPPEDWINDOW
argument informs the API that we want a regular application window with a caption bar, a system menu, a thick border that allows for resizing, a minimize button and a maximize button. For more details, see the documentation page for window styles.
At this point we don’t really need our window to be of a particular size, or placed at some specific location on the screen, so the four CW_USEDEFAULT
arguments simply tells the API to use default values for the position and the size of our window.
The CreateWindow()
function returns a HWND
, which is simply a handle to the window that was created. If the value of the HWND
is nullptr
, something went wrong. If this happens, we will just display an error message and terminate the program. The Windows API has mechanisms that let us investigate why exactly the window could not be created. We will look into those at a later point.
Assuming everything went well, we now need to make sure our window is visible. So we call yet another API function named ShowWindow()
. For good measure we can also call UpdateWindow()
to ensure that the window’s client area is updated immediately.
ShowWindow(mainWindow, showMode);
UpdateWindow(mainWindow);
The message pump
Finally we are ready to enter the message pump. That is how the operating system communicates with all applications, so it’s an integral part of any Windows application – at least those with a graphical user interface. This is what we call an event-driven model, which means that any time an event is triggered that the application needs to know about, Windows will pass the information by sending a message. So instead of making explicit function calls to obtain input, applications will just sit and wait for the operating system to pass input to them.
Events that trigger such messages include, but are not limited to a key being pressed on the keyboard, a window being resized – it even happens whenever the mouse is moved. It is the application’s own responsibility to listen for these messages, and to properly act upon them.
In order to listen for messages, we repeatedly call the API function GetMessage()
. This function is blocking, meaning the application halts until the function returns. There is also a similar non-blocking alternative named PeekMessage()
, which we will get more acquainted with at a later point. But for now, GetMessage()
will do the trick.
Whenever a message is received, we pass it on to two different API calls: TranslateMessage()
and DispatchMessage()
. Without going into too much detail, this causes the application’s window procedure to be invoked. The window procedure is the function that we pointed to in the WNDCLASSEX
struct when we registered the windows class – in our case the WindowProc()
function.
MSG message;
while (GetMessage(&message, nullptr, 0, 0))
{
TranslateMessage(&message);
DispatchMessage(&message);
}
A window message consists of three components – a message ID and two parameters. The message ID tells us what type of event has occurred, and each message ID has its own way of interpreting the two parameters. For now we only need to worry about the message IDs. In this very simple version of our window procedure, we are only catching the WM_PAINT
event and the WM_DESTROY
event.
The WM_PAINT
event is triggered every time the window needs to be redrawn, which happens when the window is resized or restored from a minimized state, when the UpdateWindow()
function is called, and in a few other situations. The reason why we called UpdateWindow()
right before entering the message pump was to make sure a WM_PAINT
message is present in the message queue right from the beginning.
As you have probably already guessed, the WM_DESTROY
event is triggered when the window is destroyed. This is usually caused by the user closing the window, e.g. by clicking the close button in the upper right corner, or pressing Alt+F4. Right now all we want to do in this situation is to call the API function PostQuitMessage()
, which tells the system that the application is about to terminate. Technically, this causes Windows to send a WM_QUIT
message. That message is not something we have to explicitly catch, but it causes GetMessage()
to return false
, which is the condition for breaking out of the message pump.
case WM_DESTROY:
PostQuitMessage(0);
At this point, we only want to display a blank window with a white background. This means that when the window needs to be redrawn, all we really need to do is fill the window with white. For this, we first need what’s called a device context for the window. When we are inside a WM_PAINT
event handler, this is obtained by calling the API function BeginPaint()
. Outside of a WM_PAINT
event handler we would instead use GetDC()
, which we will probably also get back to at a later point.
The BeginPaint()
function requires a pointer to a PAINTSTRUCT
object where it can provide extra information about the paint operation that is requested. We will just create an instance and pass a pointer to it.
Next, we also need to know the extent of the area to be painted, and for this we use the API function GetClientRect()
. As arguments to this function, we pass the window handle (HWND
) which we obtained through the parameter list for WindowProc()
, and a pointer to a RECT
instance in which the function will store its output.
Now we have all the information we need, so we are ready to call another API function named FillRect()
. We pass the device context, the client rectangle and then something called a “brush”. The brush says something about what we want to fill our rectangle with, and we will discuss this in the next blog post. For now, we can get away with simply passing the predefined constant WHITE_BRUSH
.
When we are done drawing, we need to release the device context. When it was obtained using BeginPaint()
, we release it using EndPaint()
. If it were obtained using GetDC()
, we would instead call ReleaseDC()
.
case WM_PAINT:
deviceContext = BeginPaint(windowHandle, &paintStruct);
GetClientRect(windowHandle, &clientRect);
FillRect(deviceContext, &clientRect, static_cast<HBRUSH>(WHITE_BRUSH));
EndPaint(windowHandle, &paintStruct);
For both our event handlers, we make sure to return 0
, indicating to the system that the message has been successfully handled.
Even though we have written handlers for all the messages we want to respond to, there is still one more thing we need to do before our window procedure is complete. Our window will receive a multitude of other events that we are not explicitly listening for, and those need to be handled as well. The good news is that the Windows API provides a default handler for all messages. All we need to do is pass the unhandled messages on to the DefWindowProc()
function, and return the result.
Running our application
That’s it! The first version of our introduction project is ready to build and run! It’s not really the most exciting application out there, but it’s a great starting point – a blank canvas waiting for us to express our creativity. After all, our window can be moved, resized, minimized, maximized, restored and closed, so we have covered much of the basics.

Next time we will start drawing some actual graphics to the window, so stay tuned!