Building a Win32 App Part 3: a Simple Window
Purpose
The purpose of this tutorial is to learn the process of constructing a Win32 application window at it's simplest form. By the end of this tutorial, readers should be able to create a window from scratch, understand basics and flow of the message loop, as well as the procedure associated to this window.
Part 1: Intro to Visual Studio
Intended audience
This tutorial requires basic knowledge of Visual Studio, proficiency in C/C++, as this tutorial does not cover the C++ programming language.
Objectives
- Window class registration; initialization; creation.
- Comprehending and building a basic message loop.
- Analyse and create a window procedure.
Workspace Setup
Creating the Project
Launch Visual Studio, which will bring the start page.
Go to the menu bar and select new project.
You will be prompted to choose a project type. Select Win32 Project.
If you wish, you can choose a different project name than Win32Project1. I will use MyFirstWindow for clarity purposes.
Once you are done, simply click OK. The Win32 project wizard will launch to help you setup your new project.
Win32 Project Wizard
In the Win32 project wizard, simply click Next.
After this comes the application settings, where you choose whether or not you may want to start from a particular template. Since the purpose of this tutorial is to create a window from scratch, we do not need a template, but rather an empty project. In Additional options, check **Empty project **, then click OK.
Source Files
In the solution explorer, expand the project to see it's content, by clicking on the arrow.
Right-click Source Files and add a new source file item. Name it main.cpp and click OK.
The Code
Windows Headers
In order to gain access to Win32 API functions, we need a file that references them. By convention, this is done by including Windows.h.
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <Windows.h>
Macro WIN32_LEAN_AND_MEAN is used to reduce unnecessary function parsing, as Windows.h contains several references to other rarely used includes.
In Windows.h
#ifndef WIN32_LEAN_AND_MEAN
#include <cderr.h>
#include <dde.h>
#include <ddeml.h>
#include <dlgs.h>
#ifndef _MAC
#include <lzexpand.h>
#include <mmsystem.h>
#include <nb30.h>
#include <rpc.h>
#endif
#include <shellapi.h>
#ifndef _MAC
#include <winperf.h>
#include <winsock.h>
#endif
#ifndef NOCRYPT
#include <wincrypt.h>
#include <winefs.h>
#include <winscard.h>
#endif
#ifndef NOGDI
#ifndef _MAC
#include <winspool.h>
#ifdef INC_OLE1
#include <ole.h>
#else
#include <ole2.h>
#endif /* !INC_OLE1 */
#endif /* !MAC */
#include <commdlg.h>
#endif /* !NOGDI */
#endif /* WIN32_LEAN_AND_MEAN */
WinMain
Entry Point
Unlike console applications, Win32 applications need to use a specific function named WinMain. This serves as the program entry point.
CALLBACK is a macro that serves as an alias for __stdcall, an exclusive Windows calling convention.
int CALLBACK WinMain(
_In_ HINSTANCE hInstance,
_In_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nCmdShow
);
Function parameters
hInstance A handle to the current application instance
hPrevInstance A handle to a previous application instance. This only occurs if the current application is the nth instance higher than one.
lpCmdLine The command line arguments, if any exist.
nCmdShow Specifies the way the window will be shown. .i.e: minimized, maximized
Setup Window Class Parameters
// Setup window class attributes.
WNDCLASSEX wcex;
ZeroMemory(&wcex, sizeof(wcex));
wcex.cbSize = sizeof(wcex); // WNDCLASSEX size in bytes
wcex.style = CS_HREDRAW | CS_VREDRAW; // Window class styles
wcex.lpszClassName = TEXT("MYFIRSTWINDOWCLASS"); // Window class name
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // Window background brush color.
wcex.hCursor = LoadCursor(hInstance, IDC_ARROW); // Window cursor
wcex.lpfnWndProc = WndProc; // Window procedure associated to this window
At first, we must declare a WNDCLASSEX instance. This instance will represent the window class.
Since filling all attributes is not required to register a basic window class, we will first set all attributes to zero and only initialize those that are needed.
Note:
Assumptions are made that the reader understands both
reasons and motivations behind clearing values upon
initialization, as C++ assigns arbitrary values to
uninitialized data types. Class registration may fail
if instance contains invalid values. More precisely,
values that are outside the function's input range.
ZeroMemory is a macro used to clear the WNDCLASSEX instance, wcex with NULL values.
Precisely, ZeroMemory is defined as:
#define ZeroMemory(Destination,Length) memset((Destination),0,(Length))
As for the window attributes, here are the main attributes we will be using.
wcex.cbSize The size in bytes of the WNDCLASSEX structure.
wcex.style The window class style. This is a combination of one or more Window Class Styles.
wcex.lpszClassName A unique class name for the window. This class name is used by several functions to retrieve window information at run time.
wcex.hdrBackground A brush handle representing the background color.
wcex.lpfnWndProc A pointer to a window procedure function. This function is used for message processing.
Window Class Registration
After setting attributes, a window class must be registered before it can be used. To register a class, we use RegisterClassEx. It is important to note that each window class must be unique, as such attempting to register an already registered class or system class will result in failure.
// Register window and ensure registration success.
if (!RegisterClassEx(&wcex))
return 1;
Setup Window Initialization Parameters
Once registration complete, we need to specify how to display the window, it's style; dimensions; title; class name;
// Setup window initialization attributes.
CREATESTRUCT cs;
ZeroMemory(&cs, sizeof(cs));
cs.x = 0; // Window X position
cs.y = 0; // Window Y position
cs.cx = 640; // Window width
cs.cy = 480; // Window height
cs.hInstance = hInstance; // Window instance.
cs.lpszClass = wcex.lpszClassName; // Window class name
cs.lpszName = TEXT("My First Window"); // Window title
cs.style = WS_OVERLAPPEDWINDOW; // Window style
We need a CREATESTRUCT instance to specify these properties.
cs.x The left position of the window.
cs.y The top position of the window.
cs.cx The width of the window.
cs.cy The height of the window.
cs.hInstance The application instance to which, this window is associated.
cs.lpszClassName The window's class name. This is the type of window to be created. We use the one we created, previously.
cs.lpszName The window's name or title, precisely.
cs.style The window's style. The style is a combination of one or more window styles. For a complete list of styles, readers should refer to Window Styles.
Window Creation
To create a window, we need to use the function CreateWindowEx. While we can directly assign values in function arguments, the reason behind using CREATESTRUCT is to demonstrate the relation between this function and this structure.
// Create the window.
HWND hWnd = ::CreateWindowEx(
cs.dwExStyle,
cs.lpszClass,
cs.lpszName,
cs.style,
cs.x,
cs.y,
cs.cx,
cs.cy,
cs.hwndParent,
cs.hMenu,
cs.hInstance,
cs.lpCreateParams);
Display the Window
To display the window, we need to call ShowWindow with argument nCmdShow as SW_SHOWDEFAULT. nCmdShow specifies how the window will open, we could specify SW_MAXIMIZE to make the window maximized, SW_MINIMIZE for minimization. Since we simply want to illustrate the main use of ShowWindow, SW_SHOWDEFAULT is used as the default argument.
The next function, UpdateWindow, is used to send a paint message, WM_PAINT, to the window, to ensure the window gets redrawn at least once, after being displayed. It is usually a good convention to call UpdateWindow after ShowWindow or any other operations involved on client area modification/display.
// Display the window.
::ShowWindow(hWnd, SW_SHOWDEFAULT);
::UpdateWindow(hWnd);
The Message Loop
The message loop is fairly simple, As stated in Windows and messages, the system checks in the message queue to see if there are pending messages. If any message is found, then the system will dispatch it to the appropriate window. Here is a simplified version of the main loop with no TranslateAccelerator or TranslateMessage.
GetMessage
The GetMessage function is used to retrieve pending messages from the main message queue.
BOOL WINAPI GetMessage(
_Out_ LPMSG lpMsg,
_In_opt_ HWND hWnd,
_In_ UINT wMsgFilterMin,
_In_ UINT wMsgFilterMax
);
lpMsg A pointer to a MSG structure, to store information about message queues associated to the current window.
hWnd A window handle to retrieve messages from. In this case, it is our previously created window.
wMsgFilterMin The minimal message value to be retrieved.
wMsgFilterMax The maximal message value to be retrieved.
DispatchMessage
The dispatch function is used to dispatch the message that was received to the associated window.
LRESULT WINAPI DispatchMessage(
_In_ const MSG *lpmsg
);
Putting it all together
// Main message loop.
MSG msg;
while (::GetMessage(&msg, hWnd, 0, 0) > 0)
::DispatchMessage(&msg);
One particular difference with this version and the one seen previously is the conditional expression within while loop. The expression returns true when GetMessage > 0. The reason behind using > 0 comes from the fact that if GetMessage encounters an error, the result is negative. This to ensure that the loop will be able to terminate. I will describe error handling in further tutorials, as Win32 errors and exceptions are not part of this tutorial.
Unregister window class
This is an optional function call, which consists of unregistering the windows class we created when we no longer need it. UnregisterClass checks if a window class, specified by wcex.lpszClassName, already exists and frees memory that was allocated for this one.
// Unregister window class, freeing the memory that was
// previously allocated for this window.
::UnregisterClass(wcex.lpszClassName, hInstance);
By default, application defined classes are automatically unregistered when the process terminates, therefore making this line a bit redundant. Nevertheless, it does not mean it should never be used. There might be situations where an application has a special window that is only available when a specific option is enabled. The window is registered before use and unregistered in order to free memory, since this class is unique and only has utility when visible.
Window procedure
The last function to implement is the window procedure. A general naming convention for the default procedure is WndProc. If you want, you can use a different name, additionally, you need to ensure that the function used in the WNDCLASSEX, wcex.lpfnWndProc matches both signature and name of your function.
LRESULT CALLBACK 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;
}
Parameter | Description |
---|---|
hWnd | A handle representing the window that is currently being processed. |
uMsg | the message ID that was dispatched in main loop. |
wParam | A parameter whose value may change based on uMsg value. |
lParam | A parameter whose value may change based on uMsg value. |
If no message is handled, the default case will simply call the default window procedure to process all messages that were not processed by Wndproc.
In order to be able to return from the main loop, a request is needed to let the main thread know that the application is terminating. This is the reason for using PostQuitMessage. This function takes an integer which is used as an exit code, then sends a WM_QUIT message to the thread's message queue. When GetMessage retrieves the quit message, and places the exit code value in the MSG structure, ends the message loop. The return value of the application can be retrieved from the wParam, from MSG and used as the return value in WinMain.
return (int)msg.wParam;
Building and launching the project
When you have multiple projects in one workspace, and are willing to automatically start building from a specific project, you need to set it as the default startup project.
To set a startup project, in solution explorer, right-click on the desired project, MyFirstWindow in this case, and select Set as StartUp Project.
Then click the green arrow button, Local Windows Debugger, or press Ctrl+F5.
The result should be similar to the following:
Conclusion
Readers should be able to setup, register, initialize, create a window from scratch, without the need of a template. Readers should now be familiar with the basics of event-driven architecture, as well as a comprehension of the window and the procedure, to which is associated. Finally, readers are expected to have acquired new techniques and skills in Visual Studio, which will be used in future tutorials. For future tutorials, readers should be comfortable with the creation of new projects if necessary, as well as building, and adding new source files and items to them.
Readers are encouraged to read this tutorial again to ensure correct understanding, as further tutorials will cover and expand concepts viewed throughout this lecture.
Complete Example
main.cpp
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <Windows.h>
//
//
// WndProc - Window procedure
//
//
LRESULT
CALLBACK
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;
}
//
//
// WinMain - Win32 application entry point.
//
//
int
APIENTRY
wWinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPWSTR lpCmdLine,
int nShowCmd)
{
// Setup window class attributes.
WNDCLASSEX wcex;
ZeroMemory(&wcex, sizeof(wcex));
wcex.cbSize = sizeof(wcex); // WNDCLASSEX size in bytes
wcex.style = CS_HREDRAW | CS_VREDRAW; // Window class styles
wcex.lpszClassName = TEXT("MYFIRSTWINDOWCLASS"); // Window class name
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); // Window background brush color.
wcex.hCursor = LoadCursor(hInstance, IDC_ARROW); // Window cursor
wcex.lpfnWndProc = WndProc; // Window procedure associated to this window class.
wcex.hInstance = hInstance; // The application instance.
// Register window and ensure registration success.
if (!RegisterClassEx(&wcex))
return 1;
// Setup window initialization attributes.
CREATESTRUCT cs;
ZeroMemory(&cs, sizeof(cs));
cs.x = 0; // Window X position
cs.y = 0; // Window Y position
cs.cx = 640; // Window width
cs.cy = 480; // Window height
cs.hInstance = hInstance; // Window instance.
cs.lpszClass = wcex.lpszClassName; // Window class name
cs.lpszName = TEXT("My First Window"); // Window title
cs.style = WS_OVERLAPPEDWINDOW; // Window style
// Create the window.
HWND hWnd = ::CreateWindowEx(
cs.dwExStyle,
cs.lpszClass,
cs.lpszName,
cs.style,
cs.x,
cs.y,
cs.cx,
cs.cy,
cs.hwndParent,
cs.hMenu,
cs.hInstance,
cs.lpCreateParams);
// Validate window.
if (!hWnd)
return 1;
// Display the window.
::ShowWindow(hWnd, SW_SHOWDEFAULT);
::UpdateWindow(hWnd);
// Main message loop.
MSG msg;
while (::GetMessage(&msg, hWnd, 0, 0) > 0)
::DispatchMessage(&msg);
// Unregister window class, freeing the memory that was
// previously allocated for this window.
::UnregisterClass(wcex.lpszClassName, hInstance);
return (int)msg.wParam;
}
Great tutorial!
I am using VS 2019. To make the sample code run I had to set under the
Project properties dialog: Configuration properties -> Linker -> System -> SubSystem to
Windows (/SUBSYSTEM:WINDOWS).
Very short and precise example, thank you for the code summary at the end!