Page Contents
Project: Win32 Window
This is a simple project, which consists of using the Win32 API to create a window and then displaying this window at the centre of the screen. I have used the Win32 API to create a window in previous projects using the Visual Studio Windows Desktop Application template. I wanted to create this project using CMake. While I have used CMake with console applications before, I have not used it with GUI applications.
CMake
CMakeLists.txt
This is a simple CMake project so I only have a single CMakeLists.txt
file which is in the below code fence. The file consists of the usual CMake setup except I specify the WIN32
property when calling the add_executable
command. This makes Window
a GUI executable instead of a console application. And the entry point to the program will now be WinMain
or wWinMain
rather than main
.
It is possible to create a window within a console application, which can be beneficial during development for error output and debugging. I opted to develop it as a GUI application and manage error output independently.
cmake_minimum_required (VERSION 3.8)
project ("Window")
set(CMAKE_CXX_STANDARD 17) #Use C++17
add_executable (Window WIN32 "Window.cpp") #Make a Windows GUI application
option(USE_UNICODE "Support Unicode." OFF)
if(USE_UNICODE)
target_compile_definitions(Window PUBLIC UNICODE)
target_compile_definitions(Window PUBLIC _UNICODE)
endif()
Additionally, I create a CMake option USE_UNICODE
. This option once built is stored in the CMakeCache.txt
. Once built if we want to change the value of the CMake option we cannot just change the default value in the CMakeLists.txt
. We either have to manually edit the CMakeCache.txt
or delete it.
Alternatively, we can change the value of the option by specifying it in the CMake command line, a process which is simplified by using presets.
When USE_UNICODE
is ON
we set two compiler definitions UNICODE
and _UNICODE
. These compiler definitions will set both the Windows API and C runtime respectively to map functions to their wide-character versions. When USE_UNICODE
is OFF
then the functions will be mapped to narrow character versions. I use the PUBLIC
specifier when adding the compile definition to the executable, ensuring that it is applied when linking with the Windows API and C runtime.
Running the CMake
In order to get an executable to run on our OS we first need to choose a generator. A CMake generator is responsible for translating the platform independent CMakeLists.txt
configuration files into platform-specific build files.
For this project I used Ninja for my generator. To aid with the building, I created a CMakeUserPresets.json
where I created two presets. The presets allow me to set USE_UNICODE
without having to manually edit the CMakeCache.txt
and allow me to set the generator and compilers I am using, without having to use the CMake command line. The difference between the two presets is the value of the Cmake variable CMAKE_BUILD_TYPE
. This variable determines whether the compiler is running in debug or release mode.
{
"version": 3,
"configurePresets": [
{
"name": "debug",
"generator": "Ninja",
"binaryDir": "${sourceDir}/out/build/debug",
"cacheVariables": {
"CMAKE_C_COMPILER": "C:/Program Files/LLVM/bin/clang.exe",
"CMAKE_CXX_COMPILER": "C:/Program Files/LLVM/bin/clang++.exe",
"CMAKE_BUILD_TYPE": "Debug",
"USE_UNICODE" :false
}
},
{
"name": "release",
"generator": "Ninja",
"binaryDir": "${sourceDir}/out/build/release",
"cacheVariables": {
"CMAKE_C_COMPILER": "C:/Program Files/LLVM/bin/clang.exe",
"CMAKE_CXX_COMPILER": "C:/Program Files/LLVM/bin/clang++.exe",
"CMAKE_BUILD_TYPE": "Release",
"USE_UNICODE" :false
}
}
]
}
To generate the build files, I use the command-line --preset <preset-name>
. Then, I navigate to the build directory and use the ninja
command.
The Window
Project setup
Given that I am only creating a window, I only require the Windows.h
header file, which is required for accessing a number of Windows APIs. Furthermore, since I am only using the Win32 API, I define WIN32_LEAN_AND_MEAN
. This definition excludes a number unrequired APIs. To assist with error logging, I also include a C++ standard library, sstream
. I also include the Windows header tchar.h
in order to use the _T
macro. This macro switches string literals to be wide strings and narrow strings based on the value of __UNICODE
.
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <sstream>
#include <tchar.h>
In the CMake section I don't explictly link any Win32 library files. Most C++ compilers that support Windows, including Clang++, MSVC, and GCC (when used for Windows development), are configured to automatically link against these standard Windows libraries.
Debugging
The first step I took was handling any potential future run-time errors generated by the Win32 API. The API doesn't use C++ exceptions for run-time errors so we must manually check for them the using function FormatMessage
.
To determine if an error has occured I created the preprocessor macro HANDLE_RETURN
which takes a Boolean indicating whether an error has occured. I use the predefined macros __FILE__
and __LINE__
to identify the file and line the macro was called from. Given that it is no longer a console application we can't output debug information to console. Instead I log any errors in the string stream Win32ErrorLog
. Given that the language of the messages are English by default there is no need to use Unicode for the error messages and I can use the narrow version FormatMessageA
.
#define HANDLE_RETURN(err) LogIfFailed(err, __FILE__, __LINE__)
std::stringstream Win32ErrorLog;
inline void LogIfFailed(bool err, const char* file, int line)
{
if (err)
{
DWORD error = GetLastError();
LPVOID lpMsgBuf;
FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
error,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPSTR)&lpMsgBuf,
0, NULL);
std::string message = "File: " + std::string(file) + "\n\nMessage: " + std::string((char*)lpMsgBuf) + "\n\nLine: " + std::to_string(line);
LocalFree(lpMsgBuf);
Win32ErrorLog << message;
}
}
At the end of the application I then call displayErrorMessage
,which creates an ANSI Windows dialog box with displaying any error messages.
if (!Win32ErrorLog.str().empty()) {
displayErrorMessage("Win32 Errors:\n\n" + Win32ErrorLog.str());
}
In future I could also choose to log the messages in a file in case the application never reaches displayErrorMessage
.
Creating the window
The first step when creating a window using the Win32 API is to create a window class. A window class is represented as an instance of the WNDCLASSEX
struct. One of the main fields in this struct is lpfnWndProc
where we specify the window procedure. For the hCursor
and hIconSm
I specify default values, such that it will use the default window icon and cursor icon. And I set the class name to be "window"
. The class is registered using RegisterClassEx
which will return 0
if it fails. I use the HANDLE_RETURN
macro to check this.
WNDCLASSEX winClass = {};
winClass.cbSize = sizeof(WNDCLASSEX);
winClass.style = CS_HREDRAW | CS_VREDRAW;
winClass.lpfnWndProc = WndProc;
winClass.cbClsExtra = 0;
winClass.cbWndExtra = 0;
winClass.hInstance = hInstance;
winClass.hIcon = NULL;
winClass.hCursor = LoadCursor(hInstance, NULL);
winClass.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
winClass.lpszMenuName = NULL;
winClass.lpszClassName = _T("window");
winClass.hIconSm = LoadIcon(hInstance, NULL);
HANDLE_RETURN(RegisterClassEx(&winClass) == 0);
The client area is the part of the window where the application can render content. The window area includes the client area and other parts of the window such as the title bar, border, and menu bar. I have defined the client area to have a width of 990
and a height of 540
pixels. The function AdjustWindowRect
takes the client area and based on the desired style of the window, in this case WS_OVERLAPPEDWINDOW
, and returns the window area. Based on the returned RECT
I could now calculate the width and height of the window area.
I wanted the position of the window when it is created to appear in the centre of the screen. Centring the window requires calculating the coordinates of the top left corner based on the centered window. Using the area of the primary display, which I got using GetSystemMetrics
and the window area I calculated centreX
and centreY
. If I had used the client area in this calculation, these coordinates would be slightly incorrect.
constexpr LONG defaultWindowWidth = 990;
constexpr LONG defaultWindowHeight = 540;
RECT windowArea = {};
windowArea.left = 0;
windowArea.top = 0;
windowArea.right = defaultWindowWidth;
windowArea.bottom = defaultWindowHeight;
//Find window area from the client
HANDLE_RETURN(AdjustWindowRect(&windowArea, WS_OVERLAPPEDWINDOW, TRUE) == 0);
int windowAreaWidth = static_cast<int>(windowArea.right - windowArea.left);
int windowAreaHeight = static_cast<int>(windowArea.bottom - windowArea.top);
//Find centre of screen coordinates
int displayWidth = GetSystemMetrics(SM_CXSCREEN);
int displayHeight = GetSystemMetrics(SM_CYSCREEN);
int centreX = (displayWidth - windowAreaWidth) / 2;
int centreY = (displayHeight - windowAreaHeight) / 2;
The window is created using the function CreateWindowEx
which takes the class name, and the window name. The window name what is displayed in title bar if the style includes one. It also takes the style of the window, the location of where it starts and the window area. This function does not activate the widow nor does it display the window. To activate the window and display it in its current size I called the function ShowWindow
with a value of SW_SHOW
.
HWND windowHandle = CreateWindowEx(NULL, _T("window"), _T("My Window"), WS_OVERLAPPEDWINDOW, centreX, centreY,
windowAreaWidth, windowAreaHeight, NULL, NULL, hInstance, nullptr);
HANDLE_RETURN(windowHandle == NULL);
ShowWindow(windowHandle, SW_SHOW);
Even if we have declared the correct prototype for window procedure lpfnWndProc
, running this code currently would cause a Win32 error. This is because when creating the window a valid window procedure is required.
Window procedure
The window procedure is a callback that handles window messages for a particular window. When we create a window class we specify a window procedure, lpfnWndProc
. Before we can successfully create a window instance we must create a valid window procedure. To be a valid window procedure there are a minimal amount of message codes that it must deal with. If we don’t wish to manually implement the required message codes we can call the Win32 function DefWindowProc
. In the below code fence my window procedure deals with only one window message WM_DESTROY
and returns the default behaviour for every other window message.
The window message WM_DESTORY
is sent to the window procedure of the window being destroyed after the window is removed from the screen. I handle the message by sending the thread message WM_QUIT
using the function PostQuitMessage
.
LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
return 0;
}
Message Loop
The message loop continually reads messages from the message queue dispatching window messages to their relevant window procedures and dealing with any thread messages. In the below code fence I continually loop until the thread message WM_QUIT
is read from the message queue. Messages are read from the message queue using the function GetMessage
. Since GetMessage
in this instance is called with NULL
for the window handle, it retrieves messages for any window in the current thread and also thread messages. The function is also blocking, meaning that it waits for a message to be available before returning. If a window message has been retrieved, DispatchMessage
dispatches it to the procedure associated with the message's target window
MSG windowMsg= {};
while (windowMsg.message != WM_QUIT) {
GetMessage(&windowMsg, NULL, 0, 0);
DispatchMessage(&windowMsg);
}
Without completing the message loop, the window will appear, but then the program will finish executing, and the window will instantly close. Now, with the message loop, here is the created window. It's not much, but it is centered (Note: it is not centered in the screenshot, as the taskbar has been cut off).