Codementor Events

Building a Win32 App Part 3: a Simple Window

Published Jun 23, 2015Last updated May 26, 2020

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

Part 2: the WinMain Function

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.

Visual Studio menu bar


You will be prompted to choose a project type. Select Win32 Project.

Project selection

If you wish, you can choose a different project name than Win32Project1. I will use MyFirstWindow for clarity purposes.

Project properties

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.

Win32 project wizard

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.

Win32 project wizard properties


Source Files

In the solution explorer, expand the project to see it's content, by clicking on the arrow.

Source files

Right-click Source Files and add a new source file item. Name it main.cpp and click OK.

Solution explorer


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.

Set as StartUp Project

Then click the green arrow button, Local Windows Debugger, or press Ctrl+F5.

Visual Studio ribbon

The result should be similar to the following:

Window


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;
}

Part 4: Applying Resources to a Window

Discover and read more posts from Marc-Antoine Lortie
get started
post comments2Replies
georgoswonkos
5 years ago

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).

Marius Kugland
8 years ago

Very short and precise example, thank you for the code summary at the end!