Creating a Window on Microsoft Windows
In order to create a window, we're going to be using platform specific code. Please note that this tutorial is for Windows only. None of this will apply to Linux, Android, GLFW, etc. I'll make the warning ahead of time: this chapter makes use of a lot of functions. Their definitions are not super relevant to you as a Vulkan developer because this will be written once.
Including Headers
Because we're going to be writing this from scratch, we're going to have to interact with Windows directly. Before we can do anything, we need to include the Windows header. If you're only targeting Windows, you can write:
#include <windows.h>
If you're targeting Linux as well, you should surround both by the #if defined(_WIN32) directives.
Setting Up a Console Window
Because we're now switching from a Windows Console Application to a Windows Application, we'll need to make sure we have a console to view the output of stdout and stderr. Also, because we're exiting right after we encounter an error, we should:
- Show a message box
- Wait for user input (keypress)
- Close after the user has acknowledged the error
We'll be using four methods to do this work.
Definition for AllocConsole:
BOOL WINAPI AllocConsole(void);
Documentation for AllocConsole:
- This function takes no arguments
Usage for AllocConsole:
AllocConsole();
Definition for AttachConsole:
BOOL WINAPI AttachConsole(
DWORD dwProcessId
);
Documentation for AttachConsole:
dwProcessIdis the identifier of the process whose console is to be used.
Usage for AttachConsole:
AttachConsole(GetCurrentProcessId());
Definition for freopen:
FILE * freopen (
const char * filename,
const char * mode,
FILE * stream );
Documentation for freopen:
fileNameis a C string containing the name of the file to be opened.modeis a C string containing a file access mode. It can be:"r""w""a"- etc.
streamis a pointer to aFILEobject that identifies the stream to be reopened.
Usage for freopen:
freopen("CON", "w", stdout);
freopen("CON", "w", stderr);
Definition for SetConsoleTitle:
BOOL WINAPI SetConsoleTitle(
LPCTSTR lpConsoleTitle
);
Documentation for SetConsoleTitle:
lpConsoleTitleis the string to be displayed in the title bar of the console window. The total size must be less than 64K.
Usage for SetConsoleTitle:
SetConsoleTitle(TEXT(applicationName));
If you put these methods together you can:
- Allocate a console
- Attach the console to the current process
- Redirect
stdoutandstderrto said console - Set the title of the console window
Now, let's modify our exitOnError method to show a error message box. We'll need to use the MessageBox method.
Definition for MessageBox:
int WINAPI MessageBox(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
);
Documentation for MessageBox:
hWndis a handle to the owner window of the message box to be created. If this parameter isNULL, the message box has no owner window.lpTextis the message to be displayed. If the string consists of more than one line, you can separate the lines using a carriage return and/or linefeed character between each line.lpCaptionis the dialog box title. If this parameter isNULL, the default title is "Error".uTypeis the contents and behavior of the dialog box. This parameter can be a combination of flags.
Usage for MessageBox:
MessageBox(NULL, msg, applicationName, MB_ICONERROR);
Creating a Window
As mentioned before, because we're writing this without any windowing libraries, we'll have to use the Win32 API. I won't go too much into detail because our focus is Vulkan and not Windows. Let's go ahead and list the variables we'll be using:
const uint32_t windowWidth = 1280;
const uint32_t windowHeight = 720;
HINSTANCE windowInstance;
HWND window;
In this section we'll be writing the body this method:
void createWindow(HINSTANCE hInstance) {}
Don't worry about hInstance for now. It is passed from the WinMain method we'll write later on. To setup our window, we'll need to register it with Windows, but first, we need to create a WNDCLASSEX object to pass during registration.
typedef struct WNDCLASSEX {
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEX, *PWNDCLASSEX;
Documentation for WNDCLASSEX:
cbSizeis the size, in bytes, of this structure. Set this member tosizeof(WNDCLASSEX). Be sure to set this member before calling theGetClassInfoExfunction.styleis the class style(s). This member can be any combination of the Class Styles.lpfnWndProcis a pointer to the window procedure. You must use theCallWindowProcfunction to call the window procedure.cbClsExtrais the number of extra bytes to allocate following the window-class structure.cbWndExtrais the number of extra bytes to allocate following the window instance.hInstanceis a handle to the instance that contains the window procedure for the class.hIconis a handle to the class icon. This member must be a handle to an icon resource. If this member isNULL, the system provides a default icon.hCursoris a handle to the class cursor.hbrBackgroundA handle to the class background brush.lpszMenuNameis a pointer to a null-terminated character string that specifies the resource name of the class menu, as the name appears in the resource file. If you use an integer to identify the menu, use theMAKEINTRESOURCEmacro.lpszClassNameis a pointer to a null-terminated string or is an atom.hIconSmis a handle to a small icon that is associated with the window class.
Usage for WNDCLASSEX:
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wcex.lpszMenuName = NULL;
wcex.lpszClassName = applicationName;
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
Now, we can make a call to RegisterClassEx to get Windowss to register the Window class.
Definition for RegisterClassEx:
ATOM WINAPI RegisterClassEx(
const WNDCLASSEX *lpwcx
);
Documentation for RegisterClassEx:
lpwcxis a pointer to aWNDCLASSEXstructure. You must fill the structure with the appropriate class attributes before passing it to the function.
Usage for RegisterClassEx:
Calling RegisterClassEx returns NULL upon failure so we should make sure we check for that.
if (!RegisterClassEx(&wcex))
exitOnError("Failed to register window");
Assuming all has gone well, we can set our windowInstance variable.
windowInstance = hInstance;
While it's not required, I'd recommend centering the window on the screen upon showing it. We'll need to use the GetSystemMetrics method to get two values:
- Screen width (
SM_CXSCREEN) - Screen height (
SM_CYSCREEN)
Once we have these, we can do the math required to center the window:
$$W{left} = S{width} / 2 - W_{width} / 2$$
$$W{top} = S{height} / 2 - W_{height} / 2$$
To get this information and compute the window's left and top positions, we can write the following code:
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
int windowLeft = screenWidth / 2 - windowWidth / 2;
int windowTop = screenHeight / 2 - windowHeight / 2;
Finally, we can call Window's CreateWindow method. This will, as the name suggests, create the window like we want. We'll specify dimensions, location, and other parameters.
Definition for CreateWindow:
HWND WINAPI CreateWindow(
LPCTSTR lpClassName,
LPCTSTR lpWindowName,
DWORD dwStyle,
int x,
int y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hMenu,
HINSTANCE hInstance,
LPVOID lpParam
);
Documentation for CreateWindow:
lpClassNameis a null-terminated string or a class atom created by a previous call to theRegisterClassorRegisterClassExfunction. The atom must be in the low-order word oflpClassName; the high-order word must be zero. IflpClassNameis a string, it specifies the window class name.lpWindowNameis the window name. If the window style specifies a title bar, the window title pointed to by lpWindowName is displayed in the title bar.dwStyleis the style of the window being created. This parameter can be a combination of the window style values.xis the initial horizontal position of the window.yis the initial vertical position of the window.nWidthis the width, in device units, of the window.nHeightis the height, in device units, of the window.hWndParentis a handle to the parent or owner window of the window being created orNULLin our case.hMenuis a handle to a menu, or specifies a child-window identifier depending on the window style orNULLin our case.hInstanceis a handle to the instance of the module to be associated with the window.lpParamis a pointer to a value to be passed to the window through theCREATESTRUCTstructure (lpCreateParamsmember) pointed to by thelParamparam of theWM_CREATEmessage orNULLin our case.
Usage for CreateWindow:
window = CreateWindow(
applicationName,
applicationName,
WS_OVERLAPPEDWINDOW | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
windowX,
windowY,
windowWidth,
windowHeight,
NULL,
NULL,
windowInstance,
NULL);
The CreateWindow method returns NULL upon failure. Let's deal with that possibility before we move on:
if (!window)
exitOnError("Failed to create window");
Last, but not least, we should show the window, set it in the foreground, and focus it. Windows provides three methods that do exactly that. These are very self explanatory:
ShowWindow(window, SW_SHOW);
SetForegroundWindow(window);
SetFocus(window);
Window Process
For this section, we'll be writing the body of a method called WndProc. This is required by Windows to process window events.
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {}
We need to destroy the window and tell Windows we quit if the user attempted to close the window. If we're told we need to paint, we'll simply update the window. If neither of those cases we're met, we'll use the default procedure to handle events we didn't process. You can do this like so:
switch (message) {
case WM_DESTROY:
DestroyWindow(hWnd);
PostQuitMessage(0);
break;
case WM_PAINT:
ValidateRect(hWnd, NULL);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
break;
}
Our Update Loop
For this section, we'll write the body of this method:
void VulkanExample::renderLoop() {}
We're calling it renderLoop because later we'll make calls to rendering functions within it. For now, however, we're going to:
- Create a message, loop while we have Windows set it
- Windows translate it into a character message then add it to the thread queue
- Dispatch the message to the windows procedure.
While that sounds complicated, it can be done with just a few lines of code:
MSG message;
while (GetMessage(&message, NULL, 0, 0)) {
TranslateMessage(&message);
DispatchMessage(&message);
}
A New Entry-Point
This is our application's new entry-point. We will not be using your typical int main() entry-point. This is because Windows requires GUI applications to enter at WinMain. We need to:
- Create an instance of our class
- Call our
createWindowmethod - Call our
renderLoop
Definition for WinMain:
int CALLBACK WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow
);
Documentation for WinMain:
hInstanceis a handle to the current instance of the application.hPrevInstanceis a handle to the previous instance of the application. This parameter is alwaysNULL.lpCmdLineis the command line for the application, excluding the program name.nCmdShowcontrols how the window is to be shown.
Usage for WinMain:
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow) {
VulkanSwapchain ve = VulkanSwapchain();
ve.createWindow(hInstance);
ve.renderLoop();
}