sunlight

Generic Windows

Introduction

In this part of the tutorial, we will cover:

  • Modeless dialog boxes
  • Message loops
  • Generic windows

Previous View Code Download Code

Modeless Dialog Boxes

From time to time, you may see dialog boxes that still allow you to interact with the rest of the application. Until now, the dialog boxes we have created have been modal: they disable any other windows created by your application. We haven't had any other windows available, so this hasn't mattered.

However, this is not the normal window style. Most windows are not modal - it's only the dialog boxes (like File Open and Options, etc.) that are modal. For instance, controls are child windows - and they certainly aren't modal!

It is possible to create dialog boxes as modeless dialogs. Modeless dialogs do not disable anything when they are created. The Find dialog box in many applications is modeless; property sheets are usually modeless, and so on. To create a modeless dialog box, all you have to do is replace the call to DialogBoxParam with CreateDialogParam, and you're away.

If we do this in our program, there is a problem - CreateDialogParam returns as soon as the dialog is created, as you would expect. What is missing is a loop to collect and dispatch messages - the message loop.

The Message Loop

The message loop is a simple thing - all it does (in its simplest form) is GetMessage then DispatchMessage:

	while (GetMessage(&msg, NULL, 0, 0))
		DispatchMessage(&msg);

GetMessage returns 0 (breaking the loop) when a WM_QUIT message is returned. Since you may have several main windows, this doesn't happen until you explicitly send one: using the PostQuitMessage function. This is typically done in the WM_DESTROY message handler of your main window, but can be done anywhere:

	case WM_DESTROY:
		PostQuitMessage(0);
		return TRUE;

The parameter of PostQuitMessage is accessible as msg.wParam after GetMessage returns.

So far, so good. There are a few more modifications to make to support the modeless dialog box, though.

The first is that a modeless dialog box is different to a standard dialog. There is no need to return a value or enable the other windows. We must then use the DestroyWindow function instead of an EndDialog call:

	case WM_CLOSE:
		DestroyWindow(hWnd);
		return TRUE;

and

	case IDCANCEL:
		DestroyWindow(hWnd);
		break;

Next, the dialog is created invisible. We should then call ShowWindow once the dialog is created.

The last is due to the extra features of a dialog box. A standard window will receive Tab keystrokes (as well as others) and will receive them as WM_KEYDOWN messages. A dialog box, on the other hand, takes these messages and interprets them as commands to move to the next control. We need to cause this processing to take place, and it is done by the IsDialogMessage function.

	while (GetMessage(&msg, NULL, 0, 0))
	{
		if (!IsDialogMessage(hWndMain, &msg))
			DispatchMessage(&msg);
	}

The IsDialogMessage takes the handle to our dialog box as a parameter.

Our WinMain now looks like:

HWND hWndMain;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int) 
{
	MSG	msg;

	hWndMain = CreateDialogParam(hInstance, MAKEINTRESOURCE(IDD_BITMAPVIEWER),
		NULL, (DLGPROC)MainDialogProc, 0);
	ShowWindow(hWndMain, SW_SHOW);

	while (GetMessage(&msg, NULL, 0, 0))
	{
		if (!IsDialogMessage(hWndMain, &msg))
			DispatchMessage(&msg);
	}
	return 0;
}

It does exactly the same as before. Not much improvement, you're thinking? Well, not quite. What we have done is expose the message loop to the application. This is valuable, since at times when no message arrives, we can run an idle-time process. In a game or multimedia application, this is the time when display frame updates occur. Processes with low-priority background tasks are often better off doing things in the idle message time than creating a separate thread.

The second advantage is that now we have the capability to add generic windows to our application - windows without an underlying dialog template, windows that have full access to the capabilities of Windows.

Creating a Generic Window

Creating windows is a two-step process that dates back to Windows 2.x. The first step is to define a window class, a structure that sets attributes like the window message procedure, the default menu, redraw flags, icon and cursor. The class defines the window without specifying flags like size and position. The standard Windows controls are defined as global classes, allowing you to create them with styles and positions that you specify but retaining their basic attributes.

Creating the Window Class

Registering a window class consists of filling in a WNDCLASSEX structure and calling RegisterClassEx:

void RegisterWindowClass()
{
	WNDCLASSEX	wcx;

	ZeroMemory(&wcx, sizeof(WNDCLASSEX));
	wcx.cbSize = sizeof(WNDCLASSEX);
	wcx.lpfnWndProc = MainWindowProc;
	wcx.hInstance = GetModuleHandle(NULL);
	wcx.hIcon = LoadIcon(wcx.hInstance, MAKEINTRESOURCE(IDR_MAIN));
	wcx.hCursor = LoadCursor(NULL, IDC_ARROW);
	wcx.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wcx.lpszClassName = "SampleWindowClass";
	wcx.lpszMenuName = MAKEINTRESOURCE(IDR_MAIN);
	RegisterClassEx(&wcx);
}

Here, I've referenced an icon resource called IDR_MAIN. You will have to create an icon in your resource script with this ID.

I've also referred to a menu called IDR_MAIN. We will now create this menu.

Menus

Again, your resource editor will have menu creation facilities. Create a menu bar that contains one popup menu, 'File'. This should look like the following:

The menu should have the ID IDR_MAIN, as above. The Open command should have the ID ID_OPEN, and the Exit command ID_EXIT. These command IDs will be used later.

Creating the Window

The next step is to create the window itself. This is done using CreateWindowEx (instead of CreateDialog):

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int) 
{
	MSG	msg;

	RegisterWindowClass();

	hWndMain = CreateWindowEx(WS_EX_APPWINDOW,
		"SampleWindowClass", _T("Bitmap Viewer II"), WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 
		NULL, NULL, hInstance, NULL);
	
	ShowWindow(hWndMain, SW_SHOW);

	while (GetMessage(&msg, NULL, 0, 0))
		DispatchMessage(&msg);

	return 0;
}

I have specified:

  • a window class of "SampleWindowClass", as we specified in the call to RegisterClassEx;
  • a window title of "Bitmap Viewer II";
  • a style of WS_OVERLAPPEDWINDOW (see the CreateWindowEx documentation);
  • default size and position.

Since we no longer have a dialog, there's no need to call IsDialogMessage (although we could, if we wanted that processing).

We now need a message handling procedure, as before. Generic window message handlers are slightly different to those for dialogs. Their return value is an LRESULT. They are expected to return 0 if the message was handled, and otherwise to pass the message to a Windows function called DefWindowProc:

LRESULT MainWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	PAINTSTRUCT	ps;
	HDC		hDC;

	switch (uMsg)
	{
	case WM_COMMAND:
		MainWindow_OnCommand(hWnd, LOWORD(wParam), HIWORD(wParam), (HWND)lParam);
		return 0;

	case WM_PAINT:
		hDC = BeginPaint(hWnd, &ps);
		MainWindow_OnPaint(hWnd, hDC);
		EndPaint(hWnd, &ps);
		return 0;

	case WM_CLOSE:
		DestroyWindow(hWnd);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	}
	return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

DefWindowProc provides default handling for any messages we don't handle. If you don't call DefWindowProc, your window won't do a great deal.

We're going to draw the bitmap at the top left of the window, so our paint function becomes

void MainWindow_OnPaint(HWND hWnd, HDC hDC)
{
	if (hBitmap == NULL)
		return;

	DrawState(hDC, NULL, NULL, (LPARAM)hBitmap, 0, 0, 0, 0, 0, DST_BITMAP | DSS_NORMAL);
}

And now we need some command handling code to handle those menu items.

BOOL MainWindow_OnCommand(HWND hWnd, WORD wCommand, WORD wNotify, HWND hControl)
{
	switch (wCommand)
	{
	case ID_OPEN:

		if (hBitmap != NULL)
			DeleteObject(hBitmap);

		GetBitmapFileName(szBitmapFilename, 
			sizeof(szBitmapFilename) / sizeof(TCHAR), hWnd);
		hBitmap = LoadBitmapFile(szBitmapFilename);
		RedrawWindow(hWnd, NULL, NULL, RDW_INVALIDATE | RDW_ERASE);
		break;

	case ID_EXIT:
		DestroyWindow(hWnd);
		break;
	}
	return TRUE;
}

None of this is terribly new code. If you now compile and run this (if you have trouble, the code I used is downloadable at the end) you will find it looks a little like this:

We've broken our 100-line barrier (in fact, we did it some time ago) but it's not really complex stuff. You now can create windows, accept menu items, display bitmaps and create dialog boxes, which is worth a little sacrifice.

Where Next?

From here, there are two paths you can take.

You can go down the path of the traditional Windows application. This tutorial has brought you through the basic Windows application and further, to finally end up in something vaguely useful as a starting point. You could investigate toolbars and status bars, and do more adventurous graphics. If you want to go down this route, I would encourage you to experiment a lot... and then move to MFC and Visual C++. MFC is a lot more useful when you understand what's happening beneath the class library.

You can have a look at some Windows applications on this Web site. Have a look at the Projects section.

Or, you can go down the DirectX path, used by almost all modern games and many multimedia packages. DirectX offers fast graphics, sound and input, with much fewer restrictions: but we still need Windows. That's where we're going next.

 

Copyright © David McCabe, 1998 - 2001. All rights reserved.

You will need to download and install the m-math control to display any equations on this Web site. Without this control, you will not see most of the equations. Please do not e-mail me asking why the equations do not display!