Navigation: Introduction to Programming

Tutorial 6 - Windows Programming


Overview

In this tutorial I will show you how to create a window like the one below that will be our sandbox for drawing a 3 Dimensional world in for games that use a window.
Figure 4.

Setting up a Win32 Project

You may remember that in a previous tutorial you set up an empty console project. This time we will be setting up a Win32 project. Basically by doing this Visual Studio makes the entry point of the program WinMain() rather than main().

So here we go.

1. Open Visual Studio.
2. Select File > New > Project
3. Select Win32 Project (See Figure 5.)


Figure 5.

4. Enter a name for the project e.g. MyNewWindow
5. Click OK
6. Click Next
7. Make sure Windows Application and Empty Project is checked.
8. Click Finish.

Now you should have a project setup for Windows Development.

The Program Code

I'm going to show you the whole program code now and talk you through it so hopefully by the end of this tutorial you will have some understanding of how to code a windows application.

There is only one file:
  • main.cpp
You can call it something else if you want like application.cpp

main.cpp
#include <windows.h>

//Create function prototypes.
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

HWND CreateMyWindow(HINSTANCE hInstance)
{
	WNDCLASS wc;
	memset(&wc, 0, sizeof(WNDCLASS));
	
	wc.style=CS_HREDRAW | CS_VREDRAW; //Redraws the window if width or height changes.
	wc.hbrBackground = CreateSolidBrush(RGB(0,0,0));
	wc.hCursor = LoadCursor(NULL, IDC_ARROW);
	wc.lpfnWndProc=(WNDPROC)WndProc; //Associates the window procedure with the window.
	wc.hInstance=hInstance; //Record handle to this application's instance.
	wc.lpszClassName=L"My Window Class"; //Class Name.

	//Register the window class with the operating system.
	if(FAILED(RegisterClass(&wc))){
		//Failed to create window.
		return NULL;
	}

	//Create the Window.
	HWND my_window = CreateWindow(L"My Window Class", //The window class to use
			      L"Basic Window", //window title
				  WS_OVERLAPPEDWINDOW, //window style
			      200, //x
			      200, //y
			      CW_USEDEFAULT, //Default width
			      CW_USEDEFAULT, //Default height
				  NULL,
				  NULL,
			      hInstance, //Application instance
			      0); //Pointer to value parameter, lParam of WndProc

	//If window was created successfully then non-NULL
	//value will be returned.
	return my_window;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR cmdLine, int showCmd)
{
	HWND my_window = CreateMyWindow(hInstance);

	if(!my_window){return E_FAIL;}

	ShowWindow(my_window, SW_SHOW);
	UpdateWindow(my_window);

	MSG msg;
	memset(&msg, 0, sizeof(MSG));

	//Enter the message loop.
	while(msg.message != WM_QUIT)
	{
		if(PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
		{
			//Put the message in a recognizable form
			TranslateMessage(&msg);
			//Send the message to the window procedure
			DispatchMessage(&msg);
		}
		else{
			//Update the game

		}
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	//Handle window creation and destruction events
	switch(msg)
	{
	case WM_CREATE:
		break;
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	}

	//Handle all other events with default procedure
	return DefWindowProc(hWnd, msg, wParam, lParam);
}

Explanation

Ok, Every window has a window procedure, which is a function that handles messages that are sent to the window. I have called it WndProc(). It must have the return type LRESULT CALLBACK and the parameters HWND, UINT, WPARAM and LPARAM. HWND is a handle to a window, hWnd in this case. An HWND variable allows us to identify a window and use it in typical Windows functions like ShowWindow(my_window, SW_SHOW) where my_window is an HWND.

By declaring this line:
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
at the top, we can refer to the Window procedure using it's name WndProc.

If you look at the CreateMyWindow() function you can see we tell the operating system to use WndProc as the window procedure for our window:

wc.lpfnWndProc=(WNDPROC)WndProc;

...

//Register the window class with the operating system.
if(FAILED(RegisterClass(&wc))){

First we store a pointer to the function in the wc structure and then we register the window class with the operating system with RegisterClass(&wc). The operating system knows to use WndProc as the window procedure because it is stored in wc.

In the case of Windows programming you can think of a CALLBACK function as a function that is called by the operating system. WndProc() is called when the window receives a windows message like WM_CREATE and WM_DESTROY. When you interact with the window, the operating system sends other WM_ messages to the window procedure like WM_SIZE when the window is resized.

Let's examine the CreateMyWindow() function because this is where we actually create the window and get a handle to it, an HWND.

First we fill out the properties of a WNDCLASS structure. This structure is used to describe the window we want to create a bit. We initialize it's values to zero using memset, set it's members to zeros so that they each have the default values. Because wc is used to describe a window, if it's values are not zero then RegisterClass() might get the wrong idea, for example 0 typically denotes to use the default setting for something. It means we can ignore setting all the properties of wc we are not interested in because they can be safely left as 0, default, and the properties we are interested in like style, hbrBackground, hCursor we can set ourselves.

So wc is a WNDCLASS structure that describes our window. CS_HREDRAW | CS_VREDRAW means we want to redraw the contents of the window when it is resized. The | line basically adds the values together to get a new value that means use both CS_HREDRAW and CS_VREDRAW.
wc.hbrBackground = CreateSolidBrush(RGB(0,0,0)); is another way of saying make the background black. red = 0, green = 0, blue = 0. Each colour here can be 0 to 255.

LoadCursor(NULL, IDC_ARROW); returns an HCURSOR or handle to a standard arrow cursor. This makes the mouse cursor become an arrow when you hover it over the window. IDC_ARROW is simply a number that LoadCursor() uses to create a handle to an arrow cursor.

We receive an hInstance, also known as a module handle, from the WinMain() function. It represents a handle to our application and similar to other handles, HCURSOR, HWND, it can be used to identify our application and work with it. We need to pass it to RegisterClass() via wc and to CreateWindow() and that's why we need it.

Putting an L before a string L"My Window Class" indicates to make the string a unicode string rather than ASCII. You have to do this for every string if the project settings in Visual Studio are set to produce a Unicode build and not Multi-Byte.

Now if you look at this line:
if(FAILED(RegisterClass(&wc))){
FAILED() is called a macro. It is defined with some special c++ syntax and can be used like an ordinary function. RegisterClass(&wc) returns an HRESULT and FAILED turns the HRESULT into a zero or non-zero value, if the RegisterClass() failed then the result of FAILED() is above zero, true, and if it succeeded the result of FAILED() is <= 0, false.

Now we get to the code that actually creates the window.
//Create the Window.
HWND my_window = CreateWindow(L"My Window Class", //The window class to use
			      L"Basic Window", //window title
			      WS_OVERLAPPEDWINDOW, //window style
			      200, //x
			      200, //y
			      CW_USEDEFAULT, //Default width
			      CW_USEDEFAULT, //Default height
			      NULL, //Parent Window
			      NULL, //Menu
			      hInstance, //Application instance
			      0); //Pointer to value parameter, lParam of WndProc
We have set the class name to L"My Window Class" in the WNDCLASS structure so this parameter of CreateWindow() must be exactly the same as L"My Window Class" to use that window class.

The title L"Basic Window" will appear in the top menu bar of the window. Call this something that is related to whatever your application is about.

WS_OVERLAPPEDWINDOW is a style that gives the window some common styling like close, minimize and maximize buttons. WS_POPUP is another style for example.

Note: For more styles you can look up CreateWindow() for C++ in the MSDN Library, just do a search in google.

x and y is the position the window will open at on the screen, 200, 200.

CW_USEDEFAULT will let the operating system decide on the width and height of the window.

The window doesn't have a parent window, so the Parent parameter is NULL.

The window doesn't have a menu, so the Menu parameter is NULL.

We pass the module handle, hInstance, to CreateWindow().

The last parameter is not needed for these tutorials so please ignore it.

Now we have looked at HWND CreateMyWindow(HINSTANCE hInstance) in some detail, it is a good time to talk about the other parts of a windows application.

The next bit of code is:
ShowWindow(my_window, SW_SHOW);
UpdateWindow(my_window);
This opens the window and repaints it.

And now all that's left to explain is the message loop and the window procedure.
//Enter the message loop.
while(msg.message != WM_QUIT)
{
	if(PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
	{
		//Put the message in a recognizable form
		TranslateMessage(&msg);
		//Send the message to the window procedure
		DispatchMessage(&msg);
	}
	else{
		//Update the game

	}
}
As you can see there is a while loop that loops infinitely and ends when the message equals WM_QUIT. WM_ stands for Windows Message. Every time a windows message is sent to the window it gets added to a queue of messages; when the window is created for example, WM_CREATE is sent to the window and added to the queue and when the user presses the close button on the window WM_DESTROY is sent to the window and added to the queue. PeekMessage() looks at the next message on the queue, if there is one, and sets the value of msg to contain details about the message. When it finds a message on the queue it updates msg and removes the message from the queue because PM_REMOVE is used. DispatchMessage(&msg) then sends the message to the window procedure WndProc() and the window procedure deals with it.

If the window procedure receives a WM_DESTROY message, when the user presses the close button on the window, PostQuitMessage(0) sends the WM_QUIT message to the window. The WM_QUIT message is then added to the internal queue of messages and PeekMessage() finds it. Then msg.message becomes WM_QUIT because PeekMessage() has updated the value of msg and the program loop ends.

There are typically all sorts of messages being sent to the window and it would be impractical to write our own code to handle them all so in our WndProc() we pass any messages we don't handle ourselves to a Default Window Procedure, DefWindowProc(hWnd, msg, wParam, lParam), and return it's value for any messages we don't handle ourselves. The DefWindowProc() allows our window to work normally and not have any glitchy behaviour.

Conclusion

And That's how a basic windows application works.

wParam and lParam may contain arbitrary values that are associated with some messages. Their types depend on the message being received by the Window Procedure. We do not use these messages in the remaining tutorials on 3d graphics so you can ignore them. Further information can be found on the MSDN website.
Next Tutorial >>