Developing Windows Applications in C++: Typical Windows tasks

If you’ve been following along typing and building code, you now have a Windows application that does nearly nothing. Well, it does everything a Windows application must do – it shows a window, which can be minimized, maximized, restored or resized, and it closes when you click the X. It’s just that “everything a Windows application must do” isn’t very much. You also have a Windows application generated by the Win32 Project template that adds a File, Exit menu command and a Help, About menu command, while using resources like a icons and strings to build a user interface.

Building a user interface – painting

A Windows application paints itself when it gets the WM_PAINT message. Sounds simple enough. But many developers design an application thinking “when this happens, I will draw that on the screen.” That is not how it works. An application only paints itself when it gets the WM_PAINT message. If there is something you want to add to the user interface, you don’t immediately draw it in response to some event or happening; you update some state and then your next WM_PAINT message will look at that state and use it in painting the main window.

You saw in Chapter 3 that the application generated by the Win32 Project template has some strings in its string table and some code to load them into local variables. It doesn’t have any code to paint the user interface of your application, though. Instead, the generated WndProc has this code for the WM_PAINT case:

case WM_PAINT: hdc = BeginPaint(hWnd, &ps); // TODO: Add any drawing code here... EndPaint(hWnd, &ps); break;

in this code, hWnd is the window handle passed into WndProc. hdc and ps are local variables:

PAINTSTRUCT ps; HDC hdc;

The HDC is a DC handle – a handle to a device context – that your code could use to do its drawing. BeginPaint takes ownership of the device context (and fills the PAINSTRUCT), and when the drawing is done, EndPaint lets go of the device context.

What you want to replace the boilerplate code with depends on what your application does. It’s reasonable to suggest that you want to do some combination of these three tasks:

  • Write some text
  • Draw a shape (a line, circle, rectangle etc)
  • Draw an image such as a bitmap or the contents of an image file (gif, jpg etc.)

Windows 7 provides a number of useful graphics APIs that you can use to handle these tasks and more:

  • DirectWrite is used for text
  • Direct2D is used for two dimensional shapes such as lines, circles, and rectangles
  • Direct3D is used for three dimensional shapes and is out of scope in this material, though you may want to look at some links in Chapter 6

In general, DirectWrite and Direct2D code is longer than the equivalent code would be with GDI, the older way of doing graphics under Windows. It’s a good plan to move the code out into a function of its own for readability. There is also a fair amount of setup and tear down code, which doesn’t belong in WndProc. Of course, this starts to raise questions like where to keep state which is set up, used, and later torn down. It’s time to transform the sample application from a collection of functions, like a C application, into a collection of objects, more of a C++ application.

The App class

You might end up with multiple classes as your application grows, but for the moment, one will suffice. I’ll call it App, though you can use whatever name you like. Everything except wWinMain will move into member functions of this class. Visual Studio can help you do this. First, right-click the project in Solution Explorer and choose Add, Class:

This will launch the Class Wizard. On the first screen, there are textboxes for name and location, but you can’t edit them. Just click Add. Now fill in the class name (App in this example) and the file names will be filled in for you:

You can use this wizard any time you want to add a class, by the way, and you can see that options like filling in a base class, asking for a virtual destructor to be generated and so on can all save you time. For this class, all you really need is its name. Click Finish.

The wizard generates a .h and .cpp file for a class with very little content. The next step is to move the code that is already in Second.cpp into member functions of this class. In App.h, edit the class definition to include new functions, and to change the global variables declared in Second.cpp into member variables:

#pragma once #define MAX_LOADSTRING 100 #include "resource.h" class App { public: App(void); ~App(void); bool Init(HINSTANCE instance, int cmd); int RunMessageLoop(); private: HINSTANCE hInstance; // current instance TCHAR szTitle[MAX_LOADSTRING]; // The title bar text TCHAR szWindowClass[MAX_LOADSTRING]; // the main window class name };

Then write implementations for these functions, moving the code from wWinMain into them:

#include "StdAfx.h" #include "App.h" App::App() { } App::~App(void) { } bool App::Init(HINSTANCE instance, int cmd) { hInstance = instance; // Initialize global strings LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadString(hInstance, IDC_SECOND, szWindowClass, MAX_LOADSTRING); MyRegisterClass(hInstance); // Perform application initialization: if (!InitInstance (hInstance, cmd)) { return false; } return true; } int App::RunMessageLoop() { HACCEL hAccelTable; hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_SECOND)); // Main message loop: MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } return (int) msg.wParam; }

(This won’t compile right away because you haven’t moved MyRegisterClass and InitInstance to member functions yet. One thing at a time.)

Finally, change wWinMain to call the new functions:

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); App theApp; if (theApp.Init(hInstance,nCmdShow)) { int QuitParam = theApp.RunMessageLoop(); return QuitParam; } return 0; }

When you first enter this code in wWinMain, you’ll see the red wigglies under App:

That’s because the class isn’t known in this compilation unit: you need to include the header file that declares it. One of the ways Visual Studio makes your life simpler is with header file name completion. Scroll to the top of the file and start to add the #include statement after the one that includes Second.h. As you type the file name, the completion is offered to you:

This can save not only time, but the mental effort to remember all your file names. As you can see, the matching doesn’t just depend on what the file name starts with. Press tab to accept a suggestion from the auto-complete.

Next, move the remaining functions into the class. Here are the lines for the header:

ATOM RegisterClass(); BOOL InitInstance(int); static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); static INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);

Notice some parameters have been removed since these member functions will have access to member variables. Also, the member functions that are to be used for callbacks must be static member functions. At first glance, this is terrible, since the WndProc is going to be handling WM_PAINT and this process of rearranging the global functions into member functions of an App class started with a desire to move the code for painting into member functions of an object. However, there is a trick to giving these static member functions access to an instance of App so that they can call other member functions. A window can hold a pointer, and your code can get and set this pointer, then cast it as needed. Change the call to CreateWindow in InitInstance so it passes the App pointer, this:

hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, this);

The pointer ends up coming along as one of the parameters in the WM_CREATE message. Your WndProc can handle this message, extract the pointer, and put it elsewhere in the window using SetWindowLongPtr. For all other messages, code in WndProc can get the pointer back using GetWindowLongPtr. You are now free to call member functions of App using the pointer. Here’s the code to be added to the beginning of WndProc:

App* pApp; if (message == WM_CREATE) { LPCREATESTRUCT pcs = (LPCREATESTRUCT)lParam; pApp = (App *)pcs->lpCreateParams; ::SetWindowLongPtrW(hWnd,GWLP_USERDATA,PtrToUlong(pApp)); return TRUE; } else { pApp = reinterpret_cast<App *>(static_cast<LONG_PTR>(::GetWindowLongPtrW(hWnd,GWLP_USERDATA))); if (!pApp) return DefWindowProc(hWnd, message, wParam, lParam); }

And here are the full implementations of the four new functions:

ATOM App::RegisterClass() { WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = App::WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_SECOND)); wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); wcex.lpszMenuName = MAKEINTRESOURCE(IDC_SECOND); wcex.lpszClassName = szWindowClass; wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL)); return RegisterClassEx(&wcex); } BOOL App::InitInstance(int nCmdShow) { HWND hWnd; hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, this); if (!hWnd) { return FALSE; } ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); return TRUE; } LRESULT CALLBACK App::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { App* pApp; if (message == WM_CREATE) { LPCREATESTRUCT pcs = (LPCREATESTRUCT)lParam; pApp = (App *)pcs->lpCreateParams; ::SetWindowLongPtrW(hWnd,GWLP_USERDATA,PtrToUlong(pApp)); return TRUE; } else { pApp = reinterpret_cast<App *>(static_cast<LONG_PTR>(::GetWindowLongPtrW(hWnd,GWLP_USERDATA))); if (!pApp) return DefWindowProc(hWnd, message, wParam, lParam); } int wmId, wmEvent; PAINTSTRUCT ps; HDC hdc; switch (message) { case WM_COMMAND: wmId = LOWORD(wParam); wmEvent = HIWORD(wParam); // Parse the menu selections: switch (wmId) { case IDM_ABOUT: DialogBox(pApp->getInstance(), MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, pApp->About); break; case IDM_EXIT: DestroyWindow(hWnd); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break; case WM_PAINT: hdc = BeginPaint(hWnd, &ps); // TODO: Add any drawing code here... EndPaint(hWnd, &ps); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } // Message handler for about box. INT_PTR CALLBACK App::About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { UNREFERENCED_PARAMETER(lParam); switch (message) { case WM_INITDIALOG: return (INT_PTR)TRUE; case WM_COMMAND: if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) { EndDialog(hDlg, LOWORD(wParam)); return (INT_PTR)TRUE; } break; } return (INT_PTR)FALSE; }

By and large the code for these methods was just copied from the old global functions, but look closely at the case statement for the About command:

case IDM_ABOUT: DialogBox(pApp->getInstance(), MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, pApp->About); break;

You can see this code is using the App pointer, pApp, as part of displaying the About box.

Add a public getter for the instance handle to the header for App, at the bottom of the public section:

HINSTANCE getInstance() {return hInstance;}

The signature changes mean Init() needs to change a little, no longer passing the instance to RegisterClass and InitInstance.

After all these changes the application should now build, and do just what it did before. Having been refactored into a more object-oriented application, it will be easier to adapt into an application that actually does something.

Setup for Direct2D drawing

Using Direct2D in your application is relatively straightforward. You will need to add a library dependency in your project so that your code will be linked with d2d1.lib, the library that implements Direct2D for use from C++ applications. To do this, right-click the project name in Solution Explorer and choose Properties:

Expand the tree view under Configuration Properties, and then under Linker. Select Input. On the Additional Dependencies line, add d2d1.lib before all the other entries that are there, following it with a semi-colon which is the separating character for libraries on this dialog. Do not remove any existing entries. The result should look like this:

Click OK.

One of the hallmarks of life before Windows was that you wrote entirely different code to output to different devices – color vs monochrome screens, or line printers vs plotters. Even two different brands of a similar printer might require different code. What Windows brought along was the idea of standard devices – your code writes to a device, and the device driver deals with making the connection from Windows to the screen or the printer. So in Windows a device is an abstraction that represents, say, a screen or a printer. A device context is just some information about the device that you can use to write to it. (Or, in a different situation, to read from it or otherwise interact with it.)

Your application knows how to draw its UI on a device of some kind – it is usually a screen, but it could be something else like a printer, or it could sometimes be the primary screen and sometimes a secondary one. It’s possible that between WM_PAINT messages, the user could move the application from one screen to another, or change the display resolution, or in other fairly common ways change what device the application is painting itself to. The setup for Direct2D is therefore split into device-independent setup (which can be done once, at the start of the application) and device-dependent setup (which might have to be done each time WM_PAINT is handled.)

Add these two declarations to the App class:

HRESULT CreateDeviceIndependentResources(); HRESULT CreateDeviceResources(HWND hwnd);

They can be private – there’s no need for code outside App to call them.

A great number of Windows calls, especially those related to COM, return an HRESULT, a handle to a result. The numerical value of the HRESULT contains information about whether a call succeeded or failed, and the reasons for failure may also be encoded. The macro SUCCEEDED returns true when given an HRESULT representing success, and false when given an HRESULT representing failure. The macro FAILED does the reverse. There is also an API call, FormatMessage, that takes an HRESULT and returns a string like “Access Denied” that may be useful in error messages.

Whenever you call a method that returns an HRESULT, you must check it before proceeding. Often it is enough to use the SUCCEEDED macro. A quite common pattern in code that is calling a number of methods returning HRESULT is:

HRESULT hr = S_OK; // Initialization of some kind hr = SomeFunction(args); if (SUCCEEDED(hr)) { hr = SomeOtherFunction(args); } if (SUCCEEDED(hr)) { hr = AThirdFunction(args); if (SUCCEEDED(hr)) { hr = YetAnotherFunction(args); } } hr = OneMoreFunction(args); return hr;

(Typically the nesting of the ifs controls the scope of local variables, none of which are shown in this snippet.)

If any of these calls fails, the function will return the HRESULT from the failed attempt. You could write code that was more like:

hr = SomeFunction(args); if (FAILED(hr)) return hr; hr = SomeOtherFunction(args); if (FAILED(hr)) return hr;

Each pattern results in the same behavior, but the one with repeated checks of SUCCEEDED with a single return statement at the end is the more commonly used pattern in Windows development. If you use it, you will find samples and other people’s code more familiar, and for this reason the samples in this material stick to the SUCCEEDED pattern.

Setting up Device-Independent Resources

The device independent resources are, generally speaking, the idea of your UI, your design. This sample intends to draw a circle in the client area of the main window. That circle is device-independent. To use the Direct2D terminology, you’re going to define a geometry, a shape that a device-dependent object like a brush could paint. First, you get a Direct2D factory (an object that creates resources for you) and then you use it to create an Ellipse geometry. Later, your painting code will use the geometry to draw your UI.

Start by adding two member variables to the App class (in the private section)

ID2D1Factory* m_pD2DFactory; ID2D1EllipseGeometry* m_pEllipseGeometry;

For this to compile, add this include at the top of App.h:

#include <d2d1.h> Then implement CreateDeviceIndependentResources like this: HRESULT App::CreateDeviceIndependentResources() { HRESULT hr; // Create a Direct2D factory. hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pD2DFactory); if (SUCCEEDED(hr)) { // Create an ellipse geometry. const D2D1_ELLIPSE ellipse = D2D1::Ellipse(D2D1::Point2F(105.0f, 105.0f), 25.0f, 25.0f); hr = m_pD2DFactory->CreateEllipseGeometry(ellipse, &m_pEllipseGeometry); } return hr; }

This creates an ellipse centred at (105,105) with an “X radius” and “Y radius” both of 25. An ellipse with equal X radius and Y radius is a circle.

Now add a call to this new function at the beginning of InitInstance:

HRESULT hr = CreateDeviceIndependentResources(); if FAILED(hr) { return FALSE; }

In Direct2D, a larger Y value is further down the screen than a small one and a larger X value is to the right of a small one. The origin is at the top left of the screen.  The units are device independent pixels, often called dips. If your desktop is set to 96 DPI, a dip is a pixel – other DPI settings will map a dip to a different number of pixels. This is taken care of when actually drawing, since the DPI is one of the settings that will vary from device to device. You set up your geometries and other device-independent-resources using dips.

Drawing with Direct2D

Actually painting your UI with Direct2D is done from a method, usually called OnRender, that is called by the handler for WM_PAINT and some other related messages. The first thing OnRender does is set up your device dependent resources, and this code is typically moved into a function of its own, CreateDeviceResources, whose declaration you have already added to the App class.

Setting up Device-Dependent Resources

When you actually draw (or render) your UI, you will do so to a render target. Your class should keep the render target cached within itself. If the device on which you’re drawing changes between WM_PAINT messages, the render target will be invalidated and you will need to recreate it. Add this private member variable to App:

ID2D1HwndRenderTarget* m_pRenderTarget;

The constructor for the App object should set this pointer to NULL, so you’ll know it needs to be created:

App::App() : m_pRenderTarget(NULL) { }

You will actually draw the circle with a brush, which you also keep in the class. Add this private member variable:

ID2D1SolidColorBrush* m_pBlackBrush;

Here’s the code for CreateDeviceResources:

HRESULT App::CreateDeviceResources(HWND hwnd) { HRESULT hr = S_OK; if (!m_pRenderTarget) { RECT rc; GetClientRect(hwnd, &rc); D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top); // Create a Direct2D render target. hr = m_pD2DFactory->CreateHwndRenderTarget( D2D1::RenderTargetProperties(), D2D1::HwndRenderTargetProperties(hwnd, size), &m_pRenderTarget ); if (SUCCEEDED(hr)) { // Create a black brush. hr = m_pRenderTarget->CreateSolidColorBrush( D2D1::ColorF(D2D1::ColorF::Black), &m_pBlackBrush ); } } return hr; }

This code does nothing if m_pRenderTarget is not NULL, because that means the resources are still usable from a previous rendering. If they need to be created, the code goes on to create a render target and, if that succeeded, a brush.

There are a variety of render targets in the Direct2D system. The Hwnd render target is used for drawing to an HWND, which is what we have in this application. Here the factory that is kept in the class (created as one of the device-independent resources) is used to create the render target. It needs to know the size of the render target, and that is determined by the size of the client rectangle for the window.

The OnRender method

Add this declaration to the App class:

HRESULT OnRender(HWND hwnd);

It can be private.

Implement the method like this:

HRESULT App::OnRender(HWND hwnd) { HRESULT hr; hr = CreateDeviceResources(hwnd); if (SUCCEEDED(hr)) { if ( !(m_pRenderTarget->CheckWindowState() & D2D1_WINDOW_STATE_OCCLUDED)) { m_pRenderTarget->BeginDraw(); m_pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White)); m_pRenderTarget->FillGeometry(m_pEllipseGeometry,m_pBlackBrush); hr = m_pRenderTarget->EndDraw(); if (hr == D2DERR_RECREATE_TARGET) { hr = S_OK; m_pRenderTarget->Release(); m_pRenderTarget = NULL; m_pBlackBrush->Release(); m_pBlackBrush= NULL; } } } return hr; }

After making sure that the window is visible (and so needs to be drawn), the OnRender method accomplishes three tasks: it creates the device-dependent resources, uses them to draw the circle, and then if necessary, frees them. To draw, it calls methods of the render target. First, BeginDraw, then specific methods that draw on the target (for example in this code, Fill Geometry), and finally EndDraw. EndDraw returns a special HRESULT if the render target needs to be recreated. Releasing the device dependent resources and setting the pointers back to NULL will ensure that all the device-dependent resources are recreated next time the application is drawn.

In WndProc, update the case statement for WM_PAINT so that it uses the new OnRender method:

case WM_PAINT: hdc = BeginPaint(hWnd, &ps); pApp->OnRender(hWnd); EndPaint(hWnd, &ps); break;

If you like, take a moment to build and run the application. You should see something like this:

The code is all in place to draw a black circle on a white background for this starter application, using Direct2D. There are just a few other Windows events you should handle, to make the application react properly in the rough-and-tumble of a typical Windows session, with windows being minimized and restored, hidden by other windows, moved from screen to screen, and so on.

When a user resizes the application, WM_SIZE is sent. The size of the render target was set in CreateDeviceResources, and you need to ensure it is adjusted when the user resizes the application. Add this method to the App class:

void OnResize(UINT width,UINT height); Implement the method like this: void App::OnResize(UINT width, UINT height) { if (m_pRenderTarget) { D2D1_SIZE_U size; size.width = width; size.height = height; m_pRenderTarget->Resize(size); } }

And in your WndProc, add this case statement:

case WM_SIZE: { UINT width = LOWORD(lParam); UINT height = HIWORD(lParam); pApp->OnResize(width, height); } break;

The braces are there to control the scope of width and height so that they are cleaned up after they are used.

Finally, add this case statement:

case WM_DISPLAYCHANGE: pApp->OnRender(hWnd); break;

This takes care of times when the user changes the resolution of the display or moves the app from one display to another.

More details

This chapter has only scratched the surface of working with Direct2D. What’s more, you can apply these concepts, like device-independent and dependent devices, geometries, and brushes to DirectWrite and Direct3D. Some good starting points to learn more are:

Reacting to Mouse and Keyboard Input

Often, the job of reacting to user input is encapsulated into a control. That’s a good approach for a UI of any complexity at all, but can mask what is actually happening. To demonstrate how user input works in Windows, this section will add some functionality to the “Draw a black circle” application that responds to mouse and keyboard input. You can use this knowledge to implement a user interface or just to understand what controls must be doing behind the scenes.

The functionality to be added will be this: the single circle at a fixed point will be replaced with a collection of circles, drawn at places the user has clicked. Typing the letter C will clear the collection. In this way the application will be responding to both mouse and keyboard input.

Removing the old drawing functionality

The geometry that was created as a device-independent resource can’t be used to implement this approach, since the location will not be known when the application first starts. Comment out or remove the line that declares m_pEllipseGeometry, and the block of code in CreateDeviceIndependentResources that creates it. (Do not remove the line in CreateDeviceIndependentResources that creates the factory.) Finally, comment out the line in OnDraw that fills the ellipse geometry with the black brush.

A Collection of Points

To the App class, add this private member variable:

std::vector<std::pair<float, float> > points;

Add this include statement at the top of the file:

#include <vector>

And these two public methods:

void AddPoint(float, float); void ClearPoints();

Implement the two functions in App.cpp like this:

void App::AddPoint(float x, float y) { points.push_back(std::pair<float, float>(x,y)); } void App::ClearPoints() { points.clear(); }

Now the class has a collection of points that can be used to draw circles in OnRender. It still needs code to call AddPoint and ClearPoints, and it needs to use the collection in OnRender. After the call to Clear, where the FillGeometry call was, put this block of code:

for (std::vector<std::pair<float,float> >::iterator i= points.begin(); i<points.end();i++) { D2D1_ELLIPSE ellipse = D2D1::Ellipse(D2D1::Point2F(i->first, i->second),25.f,25.f); m_pRenderTarget->FillEllipse(ellipse,m_pBlackBrush); }

This goes through the points, builds an ellipse on the fly for each point, and uses FillEllipse to fill it.

Adding Points on a Click

Points will be added to the collection when the user clicks the mouse. The co-ordinates of the mouse click will be provided in pixels. As discussed earlier, the Direct2D APIs use co-ordinates in dips, device independent pixels. To convert between them, you need to ask Windows about the DPI capabilities of the desktop and keep the scale information. Add these private member variables to App:

float xScale; float yScale;

Add these public getters:

float getXScale() {return xScale;} float getYScale() {return yScale;}

In CreateDeviceIndependentResources, after the factory is created, add these lines:

if (SUCCEEDED(hr)) { FLOAT dpiX, dpiY; m_pD2DFactory->GetDesktopDpi(&dpiX, &dpiY); xScale = dpiX/96.0f; yScale = dpiY/96.0f; }

To add points to the collection, your WndProc needs another case statement:

case WM_LBUTTONDOWN: { //ignores shift-click, ctrl-click and other modifiers - all kinds of left clicks add a point int x = LOWORD(lParam); int y = HIWORD(lParam); pApp->AddPoint((float)x/pApp->getXScale(),(float)y/pApp->getYScale()); InvalidateRect(hWnd,NULL,true); } break;

There are a number of interesting things going on here. You’ve seen mention elsewhere that messages carry parameters. The signature of WndProc is:

LRESULT CALLBACK App::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

The wParam and lParam are the parameters on the message. Their meaning varies from message to message. For WM_CREATE, lParam carried a pointer to the CREATESTRUCT used to create the window, which held inside it a pointer to the instance of App.  For WM_LBUTTONDOWN, the message sent when the user clicks the left mouse button down (another message, WM_LBUTTONUP, is sent when the click is released) the lParam actually holds two parameters packed into the double word – one is the x co-ordinate of the click and the other is the y co-ordinate. WM_SIZE uses a similar trick to carry the width and height of the window’s new size. The LOWORD and HIWORD macros just split out the two words from the parameter.

Once you have the x and y co-ordinates, using pApp to add the point to the collection is simple enough. This happens often in Windows programming – the first few cases take a bit of infrastructure to put together, but subsequent ones keep using it, which accelerates development once you’re underway. Since the co-ordinates are in pixels, each is scaled using the information that was saved during the creation of the device independent resources, and this means the points in the vector are in dips, not pixels. That’s why the code in OnRender can just use them. If you prefer, you can keep the points in pixels and do the scaling in OnRender. Most of the time, you draw your UI more often than you update it, so it makes sense to do the calculations when adding the point, not when using it, for best performance.

The last line of the block is very important. It instructs Windows that your window needs to be repainted. Without this line, even if you shrink and restore the application, or move other windows onto it and then away, a WM_PAINT message will not be sent. The state of the window is saved by Windows and redrawn for you. InvalidateRect declares that you have come to realize that your own internal state has changed and requires your window to be redrawn. The first parameter is the window handle. The second can be a specific rectangle within your window that needs to be redrawn – NULL means the whole window. The third is whether you’d like the whole window cleared before you draw or not.

Finally, this case statement has braces to control the scope and lifetime of x and y. Adopting a habit of controlling scope this way in a WndProc will reduce annoyance as the number of messages you’re handling rises. Why have to remember that you used a local x and y in some other message case statement already? Whenever you allocate a local on the stack in a message case, surround it with braces to confine its scope.

Those are all the changes needed to turn “one hardcoded black circle” in your UI into “a black circle wherever you click.” Build and run the application, click on it a few times, and you should see something like this:

Try commenting out the call to InvalidateRectangle to see the consequences of omitting it. You can put a breakpoint in your WndProc and you’ll see the WM_PAINT message does not come through. If you have your display set to something other than 96 dpi, try passing pixels instead of dips to AddPoint and see the circles appear somewhere other than right under the mouse.

Clearing the Collection

To react to the keypress of the letter C, you need to handle the WM_CHAR message. (Recall the TranslateMessage in the message loop that translates key up and key down messages to WM_CHAR for you.) Here’s what that case statement looks like:

case WM_CHAR: { char c = (TCHAR) wParam; if (c == 'c') { pApp->ClearPoints(); InvalidateRect(hWnd,NULL,true); } } break;

The actual character that was pressed is in the wParam of the message. This code extracts it, compares it to ‘c’, and if that’s what the user pressed, clears the points and calls InvalidateRect to ensure the UI is redrawn after this change in internal state.

Build and run the application, and confirm that after a few clicks, pressing ‘c’ will clear the points but pressing other letters has no effect. (It’s a good idea to also confirm that pressing letters when there are no points yet doesn’t cause any surprises.)

Adding Controls to an Application

You can imagine that building a typical “line of business” application, with buttons, text boxes, radio buttons, and other Windows controls would be very laborious using the techniques you have seen so far. It can most certainly be done. But Windows supports controls, pieces of code that know how to display themselves according to their internal state, and know how to react to particular user input – clicking for buttons, typing for text boxes, and so on. In fact, some controls were added to the starter app by the project template. The application has two menu items: File, Exit and Help, About. The generated code is a good starter point to add your own controls.

When a user clicks a button, the WM_LBUTTONDOWN message goes to the control, not to your main window. The button is a window, and as such has its own WndProc to let it handle messages it is sent. Code inside the button control (provided as part of Windows) processes the click and sends an entirely different message, WM_COMMAND, to the window that owns the button. Parameters in WM_COMMAND identify the control that is sending the message.

The starter code already contains a case statement for WM_COMMAND:

case WM_COMMAND: wmId = LOWORD(wParam); wmEvent = HIWORD(wParam); // Parse the menu selections: switch (wmId) { case IDM_ABOUT: DialogBox(pApp->getInstance(), MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, pApp->About); break; case IDM_EXIT: DestroyWindow(hWnd); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } break;

This code pulls out the ID of the control that has sent the message into wmId. Right-click IDM_ABOUT or IDM_EXIT and choose Go To Definition, and Visual Studio will open resource.h, where you will see constants declared. These control identifiers are used both when the control is created and when you process messages from the control.

To add a button to your UI, you must first choose a control identifier. For a button to clear the points, you might choose IDB_CLEAR, for example. The naming convention uses M for menu, B for button, and so on. Add a line to resource.h defining this constant:

#define IDB_CLEAR 110

It’s up to you to choose a number that doesn’t conflict with already-defined control identifiers you can see in resource.h.

With the design complete, you need to add some code that puts the button in your user interface, and some code that handles clicking the button. First, in the case statement for WM_CREATE, right before the return statement, add this:

CreateWindow(L"button", L"Clear", WS_VISIBLE | WS_CHILD , 20, 50, 80, 25, hWnd, (HMENU) IDB_CLEAR, NULL, NULL);

A control is a window, so creating one is as simple as creating a window. There are 11 parameters to this call. You may find them easier to understand if you also look in InitInstance where there is a call to CreateWindow that looks like this:

hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, this);

The parameters are:

  • The name of the window class, a struct that is registered with Windows that connects the window to code that makes it work. Since the button control comes with Windows, you don’t need to write a window class or do any work at all to make a button work. You just pass in “button” as the first parameter, the name of the window class.  A table of predefined window class names is in the remarks section of https://msdn.microsoft.com/en-us/library/ms632679(VS.85).aspx – the names are not case sensitive.
  • The window text. When creating a main window, this is used in the title bar. For a button, it’s the actual text that appears on the button.
  • The window style. For a main window, the WS_OVERLAPPEDWINDOW style gives it a title bar, a system menu, and the other features you expect in a main window. (See https://msdn.microsoft.com/en-us/library/ms632600(VS.85).aspx for details.) You don’t want any of that on a control. WS_CHILD makes it look the way a child window should look.
  • X position, Y position, width and height. For a main window, defaults work well. Users can resize the window according to their screens and the work they’re doing. For a button, you want to establish a fixed position and size that works for your UI design. You can experiment shortly with the numerical values for these 4 parameters.
  • Parent handle. For a main window, NULL because there is no parent to this window. For a control, the hWnd of your main window.
  • Control identifier. On a main window, this is the ID of a menu the window might use. For a control it’s the identifier you created, such as IDB_CLEAR. You need to cast the integer to an HMENU to keep the compiler happy.
  • The instance. Useful for a main window, not for a control, which has a parent window.
  • An extra lParam. For the main window, you use this to pass along the App pointer. Not needed for a control, which is generic and doesn’t make direct calls to any of your code.

After you create this control, you don’t need to write any code to draw it or manage it. Windows will handle that for you. And when the user clicks it, you’ll get a WM_COMMAND message. That means you need to add a case statemtn to those in the WM_COMMAND case statement:

case IDB_CLEAR: pApp->ClearPoints(); InvalidateRect(hWnd,NULL,true); break;

This is just the same code that ran when a WM_CHAR message arrived with ‘c’ as the pressed character.

Build and run the application and you’ll see a button in the UI. Click around to draw some circles, then click the button to clear them.

Showing a message box

In that WM_COMMAND case, you can see how the two menu commands work. Here’s the case for IDM_ABOUT:

case IDM_ABOUT: DialogBox(pApp->getInstance(), MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, pApp->About); break;

This displays a new modal window with information about the application:

While this window is visible, you can’t interact with the main window – resizing it, clicking to make more circles, and so on. You must first dismiss the About box by clicking OK.

The call to DialogBox passes in IDD_ABOUTBOX, the resource identifier of a resource (created by the project template) that defines this dialog, consisting of two strings and a button. It also passes a function pointer to the About method (generated by the project template and moved into the App class) that acts as a WndProc for the new window. If you want to build complex dialogs to gather information from your users, you can define dialog resources yourself and display them this way. However in many cases an ordinary message box will meet your needs. Just as a control uses a predefined window class to save you from coding the control behavior, Windows provides a message box that you can use for a very simple dialog that has only strings and buttons.

Change the case statement for the IDB_CLEAR command to read like this:

case IDB_CLEAR: { int mbResult = MessageBox(hWnd,L"Are you sure you want to clear all the points?", L"Clear Points",MB_OKCANCEL | MB_ICONWARNING); if (mbResult == IDOK) { pApp->ClearPoints(); InvalidateRect(hWnd,NULL,true); } } break;

The MessageBox function takes four parameters:

  • The window handle of the window that owns the message box.
  • The string you want displayed in the body of the box. Use \r to split lines if you want.
  • The string you want in the title of the popup window.
  • The style of the message box.

The style consists of a number of flags combined with the or operator. In this example, MB_OKCANCEL requests a box with OK and CANCEL buttons, and MB_ICONWARNING requests that it have a warning icon. You can see all the styles at https://msdn.microsoft.com/en-us/library/ms645505(VS.85).aspx – most have self explanatory names. You can also use styles to specify which button will be the default – pressing Enter or Space will have the same effect as clicking the button.

The function returns an integer that represents which button the user clicked: IDOK, IDCANCEL, or the like. So this code puts up a confirming message box, and only clears the points if the user clicks OK. Here’s how it looks:

Test this yourself, and experiment a little with styles for the message box.

Summary

You have taken the starter code generated by the new project template and adapted it in several important ways:

  • You’ve made it more object oriented by moving the code into member functions of a class and separating pieces of code into well-named functions that others can understand
  • You’ve added code to draw a user interface using Direct2D
  • You’ve added code to react to both mouse clicks and keyboard input
  • You’ve added a control, which can handle mouse and keyboard input for you
  • You’ve used a message box to display information to the user or ask the user a question

You now have the building blocks to create any Windows application and any user interface you can design. User interfaces are built from controls, and from drawing directly on the window. You can save time and effort by using controls provided to you by Windows, along with helpers like MessageBox, or you can construct your user interface yourself for maximum control.