The Generic Windows Program
This program usesthe basic set of classes that encapsulate the Windows API.
- Controller-- The bridge between Window Procedure and Object Oriented world.
- View-- Encapsulates the output of a Windows program.
- Canvas-- Encapsulated various Device Contexts and things you can do with them.
- Model-- The worker and the brain of your program. Doesn't deal with Windows at all.
Note: This is a Win32 program-- it will run under Windows 95 and Windows NT.
|
Note: _set_new_handler is Microsoft-specific. If you're using some other compiler, just remove this line of code. According to current C++ standard, operator new should throw exceptions anyway.
|
Note: Older compilers might have problems with templates. In such case, you may substitute the use of Win[Get/Set]Long templates with direct calls to Get/SetWindowLong. For instance, instead of calling:
Controller * pCtrl = WinGetLong (hwnd);
you can call:
Controller * pCtrl
= reinterpret_cast<Controller *> (::GetWindowLong (hwnd, GWL_USERDATA));
|
Download eneric sources (zipped file 11k).
Let's start with WinMain where we create the Window class and the top window of our application. I have encapsulated these actions inside two classes: WinClass and WinMaker. WinClass can also tell us if there already are running instances of our program. When something like that happens, in our example, we will simply activate the previously running instance of the program and exit. You should do that when you want only one instance of your program running at a time.
Once the top window is successfully created, we enter the message loop. Notice that this time we are processing keyboard shortcuts by calling TranslateMessage. That's because our program has menu items that can be accessed using Alt+key combinations.
Another interesting twist in this program is that we are no longer using strings to name our resources--we use numerical ids. More than that--even when the API's call for strings, like the name of the Windows class or the caption, we store the strings in string resources and access them through ids. Your Windows development environment most likely has a resource editor that lets you create icons, menus, and string resources and assign them appropriate numerical ids. Symbolic names for these ids are stored in a header file produced by such an editor--in our case it's called resource.h.
The constant, ID_MAIN, for instance, refers to the main program's icons (large and small in the same resource), the main menu, and the string with the Windows class name. ID_CAPTION refers to the window caption string. Such organization promotes code reusability, not to mention the ease of localization.
int WINAPI WinMain
(HINSTANCE hInst, HINSTANCE hPrevInst,
char * cmdParam, int cmdShow)
{
_set_new_handler (& NewHandler);
// Using exceptions here helps debugging your program
// and protects from unexpected incidents.
try
{
// Create top window class
TopWinClass topWinClass (ID_MAIN, hInst, MainWndProc);
// Is there a running instance of this program?
HWND hwndOther = topWinClass.GetRunningWindow ();
if (hwndOther != 0)
{
::SetForegroundWindow (hwndOther);
if (::IsIconic (hwndOther))
::ShowWindow (hwndOther, SW_RESTORE);
return 0;
}
topWinClass.Register ();
// Create top window
ResString caption (hInst, ID_CAPTION);
TopWinMaker topWin (topWinClass, caption);
topWin.Create ();
topWin.Show (cmdShow);
// The main message loop
MSG msg;
int status;
while ((status = ::GetMessage (&msg, 0, 0, 0)) != 0)
{
if (status == -1)
return -1;
::TranslateMessage (&msg);
::DispatchMessage (&msg);
}
return msg.wParam;
}
catch ( WinException e )
{
char buf [50];
wsprintf (buf, "%s, Error %d", e.GetMessage (), e.GetError ());
::MessageBox (0, buf, "Exception", MB_ICONEXCLAMATION | MB_OK);
}
catch (...)
{
::MessageBox (0, "Unknown", "Exception",
MB_ICONEXCLAMATION | MB_OK);
}
return 0;
} |
Let's have a look at the WinClass class. It encapsulates a Windows-defined structure called WNDCLASSEX and provides reasonable defaults for all its fields. It is derived from a simple WinSimpleClass class, which you might use to encapsulate some built-in Windows classes (like buttons, list views, etc.).
I have provided examples of methods that can be used to override the defaults. For instance, SetBgSysColor changes the default background color of the user area of the window to one of the predefined system colors. The method SetResIcons loads appropriate icons from resources and attaches them to the Window class. These icons will then appear in the upper left corner of the main window and on the Windows' taskbar.
TopWinClass derived from WinClass makes use of this method. It also assigns the menu to the top window class.
class WinSimpleClass
{
public:
WinSimpleClass (char const * name, HINSTANCE hInst)
: _name (name), _hInstance (hInst)
{}
WinSimpleClass (int resId, HINSTANCE hInst);
char const * GetName () const { return _name.c_str (); }
HINSTANCE GetInstance () const { return _hInstance; }
HWND GetRunningWindow ();
protected:
HINSTANCE _hInstance;
std::string _name;
};
WinSimpleClass::WinSimpleClass (int resId, HINSTANCE hInst)
: _hInstance (hInst)
{
ResString resStr (hInst, resId);
_name = resStr;
}
HWND WinSimpleClass::GetRunningWindow ()
{
HWND hwnd = ::FindWindow (GetName (), 0);
if (::IsWindow (hwnd))
{
HWND hwndPopup = ::GetLastActivePopup (hwnd);
if (::IsWindow (hwndPopup))
hwnd = hwndPopup;
}
else
hwnd = 0;
return hwnd;
}
class WinClass: public WinSimpleClass
{
public:
WinClass (char const * className, HINSTANCE hInst, WNDPROC wndProc);
WinClass (int resId, HINSTANCE hInst, WNDPROC wndProc);
void SetBgSysColor (int sysColor)
{
_class.hbrBackground = reinterpret_cast<HBRUSH> (sysColor + 1);
}
void SetResIcons (int resId);
void Register ();
protected:
void SetDefaults ();
WNDCLASSEX _class;
};
WinClass::WinClass (char const * className,
HINSTANCE hInst,
WNDPROC wndProc)
: WinSimpleClass (className, hInst)
{
_class.lpfnWndProc = wndProc;
SetDefaults ();
}
WinClass::WinClass (int resId, HINSTANCE hInst, WNDPROC wndProc)
: WinSimpleClass (resId, hInst)
{
_class.lpfnWndProc = wndProc;
SetDefaults ();
}
void WinClass::SetDefaults ()
{
// Provide reasonable default values
_class.cbSize = sizeof (WNDCLASSEX);
_class.style = 0;
_class.lpszClassName = GetName ();
_class.hInstance = GetInstance ();
_class.hIcon = 0;
_class.hIconSm = 0;
_class.lpszMenuName = 0;
_class.cbClsExtra = 0;
_class.cbWndExtra = 0;
_class.hbrBackground = reinterpret_cast<HBRUSH> (COLOR_WINDOW + 1);
_class.hCursor = ::LoadCursor (0, IDC_ARROW);
}
void WinClass::SetResIcons (int resId)
{
_class.hIcon = reinterpret_cast<HICON> (
::LoadImage (
_class.hInstance,
MAKEINTRESOURCE (resId),
IMAGE_ICON,
::GetSystemMetrics (SM_CXICON),
::GetSystemMetrics (SM_CYICON),
0));
// Small icon can be loaded from the same resource
_class.hIconSm = reinterpret_cast<HICON> (
::LoadImage (
_class.hInstance,
MAKEINTRESOURCE (resId),
IMAGE_ICON,
::GetSystemMetrics (SM_CXSMICON),
::GetSystemMetrics (SM_CYSMICON),
0));
}
void WinClass::Register ()
{
if (::RegisterClassEx (&_class) == 0)
throw WinException ("Internal error: RegisterClassEx failed.");
}
class TopWinClass: public WinClass
{
public:
TopWinClass (int resId, HINSTANCE hInst, WNDPROC wndProc);
};
TopWinClass::TopWinClass (int resId,
HINSTANCE hInst, WNDPROC wndProc)
: WinClass (resId, hInst, wndProc)
{
SetResIcons (resId);
_class.lpszMenuName = MAKEINTRESOURCE (resId);
}
|
Once the Windows class is registered with the system, you can create as many windows of this class as you wish. They will, of course, share the same Window procedure that was registered with the class. We'll see later how we can distinguish between various instances of the window inside the procedure.
The class WinMaker works in much the same way as WinClass. Its constructor provides sensible defaults that may be overriden by calling particular methods. Once everything is set, you call the Create method to create the window and the Show method to display it. Notice that the moment you call Create, your Window procedure is called with the WM_CREATE message.
The top window is created using TopWinMaker class, which provides the appropriate style and caption.
class WinMaker
{
public:
WinMaker (WinClass & winClass);
operator HWND () { return _hwnd; }
void AddCaption (char const * caption)
{
_windowName = caption;
}
void AddSysMenu () { _style |= WS_SYSMENU; }
void AddVScrollBar () { _style |= WS_VSCROLL; }
void AddHScrollBar () { _style |= WS_HSCROLL; }
void Create ();
void Show (int nCmdShow = SW_SHOWNORMAL);
protected:
WinClass & _class;
HWND _hwnd;
DWORD _exStyle; // extended window style
char const * _windowName; // pointer to window name
DWORD _style; // window style
int _x; // horizontal position of window
int _y; // vertical position of window
int _width; // window width
int _height; // window height
HWND _hWndParent; // handle to parent or owner window
HMENU _hMenu; // handle to menu, or child-window ID
void * _data; // pointer to window-creation data
};
WinMaker::WinMaker (WinClass & winClass)
: _hwnd (0),
_class (winClass),
_exStyle (0), // extended window style
_windowName (0), // pointer to window name
_style (WS_OVERLAPPED), // window style
_x (CW_USEDEFAULT), // horizontal position of window
_y (0), // vertical position of window
_width (CW_USEDEFAULT), // window width
_height (0), // window height
_hWndParent (0), // handle to parent or owner window
_hMenu (0), // handle to menu, or child-window identifier
_data (0) // pointer to window-creation data
{
}
void WinMaker::Create ()
{
_hwnd = ::CreateWindowEx (
_exStyle,
_class.GetName (),
_windowName,
_style,
_x,
_y,
_width,
_height,
_hWndParent,
_hMenu,
_class.GetInstance (),
_data);
if (_hwnd == 0)
throw WinException ("Internal error: Window Creation Failed.");
}
void WinMaker::Show (int nCmdShow)
{
::ShowWindow (_hwnd, nCmdShow);
::UpdateWindow (_hwnd);
}
// Makes top overlapped window with caption
TopWinMaker::TopWinMaker ((WinClass & winClass, char const * caption)
: WinMaker (winClass)
{
_style = WS_OVERLAPPEDWINDOW | WS_VISIBLE;
_windowName = caption;
}
|
Before we go any further, here are some light-weight classes of general interest. WinException is something that we want to throw any time a Windows API fails. It takes care of retrieving the Windows error code. (By the way, there is an easy way to convert the error code to a string using FormatMessage API.)
The class ResString is a simple encapsulation of a string stored in the string resources of your application.
// The exception class: stores the message and the error code
class WinException
{
public:
WinException (char* msg)
: _err (::GetLastError()), _msg(msg)
{}
DWORD GetError() const { return _err; }
char const * GetMessage () const { return _msg; }
private:
DWORD _err;
char * _msg;
};
// The out-of-memory handler: throws exception
int NewHandler (size_t size)
{
throw WinException ( "Out of memory" );
return 0;
}
class ResString
{
enum { MAX_RESSTRING = 255 };
public:
ResString (HINSTANCE hInst, int resId);
operator char const * () { return _buf; }
private:
char _buf [MAX_RESSTRING + 1];
};
ResString::ResString (HINSTANCE hInst, int resId)
{
if (!::LoadString (hInst, resId, _buf, MAX_RESSTRING + 1))
throw WinException ("Load String failed");
}
|
The Controller is the nervous system of a particular window instance. It is created with that window, stored with it and finally destroyed with it. You can put any state information pertaining to a particular window instance in its controller. In general, the controller has a view that deals with painting on the window's surface and it has access to the model, which is the brain of your application (this is called the MVC, or Model-View-Controller pattern invented by Smalltalk programmers).
If, as it often happens, your application has only one top-level window, you can directly embed the model in its controller. That simplifies the management of resources but at the cost of tight coupling of the controller with the model. In large projects one should avoid such couplings--the use of a (smart) pointer to the model inside the controller is then preferred.
Most controller methods require a handle to the window on which behalf they are operating. This handle is passed with every Windows message, but it is simpler to store it once inside the controller object and use it whenever necessary. Remember--there is a one-to-one correspondence between window instances (and therefore window handles) and controller objects.
class Controller
{
public:
Controller(HWND hwnd, CREATESTRUCT * pCreate);
~Controller ();
void Size (int x, int y);
void Paint ();
void Command (int cmd);
private:
HWND _hwnd;
Model _model;
View _view;
};
|
The Window Procedure is the main switchboard of a Windows application. You don't call it from your program--Windows calls it! Every time something interesting happens, Windows sends your program a message. This message is passed to your window procedure. You may deal with it, or you may pass it on to the default window procedure.
Window procedure is called with a handle to a window to which a given message is directed. This handle uniquely identifies an internal Windows' data structure that corresponds to a given window instance. It so happens that we can access this data structure and use it to store some instance-specific data. Here's the type safe way of accessing this structure. By the way, the GWL_USERDATA member of this structure is guaranteed to be present in all windows, including message boxes, dialog boxes and even buttons.
template <class T>
inline T WinGetLong (HWND hwnd, int which = GWL_USERDATA)
{
return reinterpret_cast<T> (::GetWindowLong (hwnd, which));
}
template <class T>
inline void WinSetLong (HWND hwnd, T value, int which = GWL_USERDATA)
{
::SetWindowLong (hwnd, which, reinterpret_cast<long> (value));
}
|
Every time Windows calls our window procedure, we want to first retrieve its controller object. Remember, there may be several windows sharing the same window procedure and we want to have a separate controller for each window. How do we know which controller to use when we are called? We find out by looking at the window handle. In this handle we store the pointer to this particular window's controller using the Win[Set/Get]Long trick.
The Window procedure is first called with the WM_CREATE message. At that time we create the controller object and initialize it with the window handle and a special data structure called CREATESTRUCT that is passed to us by Windows. Once we have the controller, we store the pointer to it in the corresponding internal Windows' data structure under the label of the current hwnd. Next time the Window procedure is called, with a message other than WM_CREATE, we simply retrieve the pointer to our controller using the hwnd.
The rest is easy. The Window procedure interprets message parameters and calls the appropriate methods of the controller.
LRESULT CALLBACK WndProc
(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
Controller * pCtrl = WinGetLong<Controller *> (hwnd);
switch (message)
{
case WM_CREATE:
// Have to catch exception in case new throws!
try
{
pCtrl = new Controller (hwnd,
reinterpret_cast<CREATESTRUCT *> (lParam));
WinSetLong<Controller *> (hwnd, pCtrl);
}
catch (WinException e)
{
::MessageBox (hwnd, e.GetMessage(), "Initialization",
MB_ICONEXCLAMATION | MB_OK);
return -1;
}
catch (...)
{
::MessageBox (hwnd, "Unknown Error", "Initialization",
MB_ICONEXCLAMATION | MB_OK);
return -1;
}
return 0;
case WM_SIZE:
pCtrl->Size (LOWORD(lParam), HIWORD(lParam));
return 0;
case WM_PAINT:
pCtrl->Paint ();
return 0;
case WM_COMMAND:
pCtrl->Command (LOWORD (wParam));
return 0;
case WM_DESTROY:
WinSetLong<Controller *> (hwnd, 0);
delete pCtrl;
return 0;
}
return ::DefWindowProc (hwnd, message, wParam, lParam);
}
|
Here are examples of simple implementations of a few controller methods. The constructor has to remember the handle to the window for later use, the destructor has to post the quit message, The Size method passes its argument to View, etc. We'll talk about painting the window a little later. For now, notice that the controller prepares the paint canvas for the View to work with.
Controller::Controller (HWND hwnd, CREATESTRUCT * pCreate)
:_hwnd (hwnd),
_model ("Generic")
{
}
Controller::~Controller ()
{
::PostQuitMessage(0);
}
void Controller::Size (int cx, int cy)
{
_view.SetSize (cx, cy);
}
void Controller::Paint ()
{
// prepare the canvas and let View do the rest
PaintCanvas canvas (_hwnd);
_view.Paint (canvas, _model);
// Notice: The destructor of PaintCanvas called automatically!
}
|
When the user selects one of the menu items, Window procedure is called with the message WM_COMMAND. The appropriate controller method dispatches the command based on the command id. When you create the menu using your resource editor, you pick these command ids for each menu item. They are stored in the appropriate header file (resource.h in our case) that has to be included in the controller's source file.
Our menu contains only three items with the ids IDM_EXIT, IDM_HELP, and IDM_ABOUT. The dialog box that is displayed in response to IDM_ABOUT is also created using the resource editor and given the id IDD_ABOUT. Its dialog procedure is AboutDlgProc.
Finally, in order to display a dialog box we need the handle to the application instance. The standard way to retrieve it is to access the internal Windows data structure using the appropriate hwnd.
// Menu command processing
void Controller::Command (int cmd)
{
switch (cmd)
{
case IDM_EXIT:
::SendMessage (_hwnd, WM_CLOSE, 0, 0L);
break;
case IDM_HELP:
::MessageBox (_hwnd, "Go figure!",
"Generic", MB_ICONINFORMATION | MB_OK);
break;
case IDM_ABOUT:
{
// Instance handle is available through HWND
HINSTANCE hInst = WinGetLong<HINSTANCE> (_hwnd,
GWL_HINSTANCE);
::DialogBox (hInst,
MAKEINTRESOURCE (IDD_ABOUT),
_hwnd,
AboutDlgProc);
}
break;
}
} |
The view object usually stores the dimensions of the client area. They are updated whenever the controller processes the WM_SIZE message. The first WM_SIZE message is sent during window creation and before WM_PAINT, so we can safely assume that when Paint is called, the dimensions of the client area are known.
Graphical output to the window is done by calling appropriate methods of the Canvas object. In our case, we print text obtained from the model and draw a vertical line ten pixels from the left edge of the client area.
class View
{
public:
void SetSize (int cxNew, int cyNew)
{
_cx = cxNew;
_cy = cyNew;
}
void Paint (Canvas & canvas, Model & model);
protected:
int _cx;
int _cy;
};
void View::Paint (Canvas & canvas, Model & model)
{
canvas.Text (12, 1, model.GetText(), model.GetLen());
canvas.Line (10, 0, 10, _cy);
}
|
The canvas object encapsulates what is called a Device Context in Windows parlance. Our Canvas is very simple, it only knows how to print text and draw lines, but your Canvas can have many more methods that do creative things. We'll talk about Canvas more in one of the next tutorials.
class Canvas
{
public:
operator HDC () { return _hdc; }
void Line ( int x1, int y1, int x2, int y2 )
{
::MoveToEx (_hdc, x1, y1, 0);
::LineTo (_hdc, x2, y2);
}
void Text (int x, int y, char const * buf, int cBuf)
{
::TextOut ( _hdc, x, y, buf, cBuf );
}
void Char (int x, int y, char c)
{
::TextOut (_hdc, x, y, & c, 1);
}
protected:
// Protected constructor: You can't construct
// a Canvas object, but you may be able
// to construct objects derived from it.
Canvas (HDC hdc): _hdc (hdc) {}
HDC _hdc;
};
|
The canvas that you create in response to WM_PAINT message are of special kind. They obtain the device context by calling BeginPaint and release it by calling EndPaint. PAINTSTRUCT contains additional information about which parts of the user area should be painted, etc. For now we ignore such details, but if you're serious about performance, you should learn more about it.
// Concrete example of canvas.
// Create this object after WM_PAINT message
class PaintCanvas: public Canvas
{
public:
// Constructor obtains the DC
PaintCanvas (HWND hwnd)
: Canvas (::BeginPaint (hwnd, & _paint)),
_hwnd (hwnd)
{}
// Destructor releases the DC
~PaintCanvas ()
{
::EndPaint(_hwnd, & _paint);
}
protected:
PAINTSTRUCT _paint;
HWND _hwnd;
};
|
What would Windows programming be without controls?
|