Michael Park
Microsoft Corporation
July 1999
Summary: Discusses classes in Active Template Library (ATL) 3.0, which simplify Microsoft® Windows® programming by creating an object-oriented framework around the Windows API, while imposing very little overhead. Examines CWindow, a thin wrapper class; message handling with CWindowImpl and message maps; ATL dialog box classes; and techniques for extending the functionality of existing window classes.
Contents
Introduction
CWindow
CWindowImpl
A Simple but Complete Example
Message Maps
Adding Functionality to Existing Window Classes
Base Class Chaining
Window Superclassing
Window Subclassing
Contained Windows
Message Reflection
ATL Dialog Box Classes
Specifying Window Class Information
Conclusion
Introduction
Although the Active Template Library (ATL) is known primarily for its COM support, it also provides several classes that simplify Microsoft Windows programming. These classes, like the rest of ATL, are template-based and have very low overhead. This article demonstrates the basics of using ATL to create windows and dialog boxes and to handle messages.
This article assumes you are familiar with C++ and Windows programming; it does not require that you have any knowledge of COM.
CWindow
The most basic ATL window class is CWindow, an object-oriented wrapper for the Windows API. It encapsulates a window handle and provides member functions that wrap the API.
Standard Windows programming looks something like this:
HWND hWnd = ::CreateWindow( "button", "Click me", WS_CHILD, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL ); ::ShowWindow( hWnd, nCmdShow ); ::UpdateWindow( hWnd );
The equivalent code using ATL's CWindow class is
CWindow win; win.Create( "button", NULL, CWindow::rcDefault, "Click me", WS_CHILD ); win.ShowWindow( nCmdShow ); win.UpdateWindow();
It is useful to keep in mind that an ATL window object is not the same thing as a window. A window, in this context, is a set of data that the Windows operating system uses to manage an area on a screen. An ATL window object, such as an instance of CWindow, is a C++ object, and by itself has no notion of screens or window data structures. Instead, it has one data member, m_hWnd, a window handle. The window handle is the link between a CWindow object and its corresponding window on the screen.
One consequence of the division between ATL window objects and windows is that the construction of a CWindow object is distinct from the creation of its window. Referring to the previous example, we see that first the CWindow object is constructed:
CWindow win;
Then its window is created:
win.Create( "button", NULL, CWindow::rcDefault, "Howdy", WS_OVERLAPPEDWINDOW );
You can also construct a CWindow object and attach it to an existing window, thereby allowing you to manipulate the existing window with the member functions of CWindow. This can be very useful, since CWindow actually provides more than just convenient wrapper functions; CenterWindow, GetDescendantWindow, and others provide functionality beyond the Windows API.
HWND hWnd = CreateWindow( szWndClass, "Main window", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL ); // choose one of // CWindow win( hWnd ); // attach via constructor // or // CWindow win; // win = hWnd; // attach via operator= // or // CWindow win; // win.Attach( hWnd ); // attach via Attach() win.CenterWindow(); // now can work with win in place of hWnd win.ShowWindow( nCmdShow ); win.UpdateWindow();
CWindow also has an HWND conversion operator so you can use a CWindow object where an HWND is expected:
::ShowWindow( win, nCmdShow ); // API call expects HWND
CWindow makes window manipulation easier, and remarkably, adds no overhead—the optimized compiled code is exactly equivalent to pure Windows API programming using window handles.
Unfortunately, CWindow does not let you define how a window responds to messages. Sure, you can use CWindow functions to center a window or hide a window, even send a message to a window, but what happens when the message reaches the window depends on its window class, and that was determined when the window was created. If it was created as an instance of the "button" class, it acts as a button; if it's a "list box" then that's how it behaves. There is no way, using CWindow, to alter that. Fortunately, ATL has another class, CWindowImpl, which does allow you to specify new behavior for a window.
CWindowImpl
CWindowImpl inherits from CWindow, so you can still use all those handy CWindow member functions, but what makes CWindowImpl special is that it also lets you define message-handling behavior. In traditional Windows programming, when you want to specify a window's response to messages, you write a window procedure; in ATL, you define a "message map" in your ATL window class.
First, derive your class from CWindowImpl, like this:
class CMyWindow : public CWindowImpl<CMyWindow> {
Note that your new class's name must be passed as an argument to the CWindowImpl template.
Within the class definition, define the message map:
BEGIN_MSG_MAP(CMyWindow) MESSAGE_HANDLER(WM_PAINT,OnPaint) MESSAGE_HANDLER(WM_CREATE,OnCreate) MESSAGE_HANDLER(WM_DESTROY,OnDestroy) END_MSG_MAP()
The following line:
MESSAGE_HANDLER(WM_PAINT,OnPaint)
means "When the window receives a WM_PAINT message, invoke member function CMyWindow::OnPaint."
Define the member functions that handle the messages:
LRESULT OnPaint( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ) { ... } LRESULT OnCreate( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ) { ... } LRESULT OnDestroy( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ) { ... } }; // CMyWindow
The four arguments to the handler functions are the message identifier, two parameters whose contents depends on the message, and a flag that the handler can use to indicate that the message has been dealt with or needs further processing. These arguments are examined in more detail in Message Maps.
When the window receives a message, the message map entries are examined starting at the top, so it's a good idea to put the most frequent messages first. If no matching entry is found in the message map, the message is passed to the default window procedure.
The ATL message map encapsulates the window's message-handling behavior within the class. It is also more readable than a traditional window procedure's nested switch and if statements.
To create a window based on a CWindowImpl-derived class, invoke the CWindowImpl Create member function:
CMyWindow wnd; // constructs a CMyWindow object wnd.Create( NULL, CWindow::rcDefault, _T("Hello"), WS_OVERLAPPEDWINDOW|WS_VISIBLE );
Note that this Create differs from the Create of CWindow. When creating a window for a CWindow object, you have to specify a registered window class; in contrast, a CWindowImpl-derived class defines a new window class, so no class name need be specified in the call to Create.
A Simple but Complete Example
Most of the examples in this article are just code fragments, but this one is a complete "Hello world" program written with ATL window classes. Although it uses ATL, it does not use COM, so it can be built in Visual C++® as a Win32® application rather than as an ATL COM project.
In stdafx.h, put these lines:
#include <atlbase.h> extern CComModule _Module; #include <atlwin.h>
In hello.cpp, put these:
#include "stdafx.h" CComModule _Module; class CMyWindow : public CWindowImpl<CMyWindow> { BEGIN_MSG_MAP( CMyWindow ) MESSAGE_HANDLER( WM_PAINT, OnPaint ) MESSAGE_HANDLER( WM_DESTROY, OnDestroy ) END_MSG_MAP() LRESULT OnPaint( UINT, WPARAM, LPARAM, BOOL& ){ PAINTSTRUCT ps; HDC hDC = GetDC(); BeginPaint( &ps ); TextOut( hDC, 0, 0, _T("Hello world"), 11 ); EndPaint( &ps ); return 0; } LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& ){ PostQuitMessage( 0 ); return 0; } }; int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE, LPSTR, int ) { _Module.Init( NULL, hInstance ); CMyWindow wnd; wnd.Create( NULL, CWindow::rcDefault, _T("Hello"), WS_OVERLAPPEDWINDOW|WS_VISIBLE ); MSG msg; while( GetMessage( &msg, NULL, 0, 0 ) ){ TranslateMessage( &msg ); DispatchMessage( &msg ); } _Module.Term(); return msg.wParam; }
CMyWindow is derived from CWindowImpl. Its message map handles two messages, WM_PAINT and WM_DESTROY. When a WM_PAINT message is received, member function OnPaint handles it by drawing "Hello world" in the window. When a WM_DESTROY message is received, signaling that the user has closed the window, OnDestroy calls PostQuitMessage to terminate the application's message loop.
WinMain creates an instance of the CMyWindow class and implements a standard message loop. (There is also a small amount of ATL bookkeeping required with respect to _Module.)
Message Maps
There are three groups of message handling macros:
- Message handlers, for all messages (such as WM_CREATE, WM_PAINT, and so on).
- Command handlers, specifically for WM_COMMAND messages (typically sent by predefined child window controls, such as buttons or menu items).
- Notification handlers, specifically for WM_NOTIFY messages (typically sent by common controls, such as status bars or list view controls).
Message Handler Macros
There are two message handler macros:
- MESSAGE_HANDLER
- MESSAGE_RANGE_HANDLER
The first macro maps a specific message to a handler function; the second maps a range of messages to a handler.
Message handler functions have the following prototype:
LRESULT MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
where uMsg
identifies the message, and wParam
and lParam
are the message parameters (their contents depend on the message).
A handler function uses the bHandled
flag to indicate whether it handled the message. If bhandled
is set to FALSE in the handler function, the rest of the message map will be searched for another handler for the message. This can be useful when you want to have multiple handlers for the same message, perhaps in different classes using chaining, or if you just want to do something in response to a message but not actually handle it. The bHandled
flag is set to TRUE before the function is called, so unless the function explicitly sets bHandle
d to FALSE, no further message map processing will be performed.
Command Handler Macros
Command handler macros only handle commands (WM_COMMAND messages), but they allow you to specify the mapping in terms of the command code or the identifier of the control sending the command.
- COMMAND_HANDLER maps commands with a specified command code from a specified control to a handler function.
- COMMAND_ID_HANDLER maps commands with any command code from a specified control to a handler function.
- COMMAND_CODE_HANDLER maps commands with a specified command code from any control to a handler function.
- COMMAND_RANGE_HANDLER maps commands with any command code from a range of controls to a handler function.
- COMMAND_RANGE_CODE_HANDLER maps commands with a specified command code from a range controls to a handler function.
Command handler functions have the following prototype:
LRESULT CommandHandler(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled);
where wNotifyCode
is the notification code, wID
is the identifier of the control sending the command, hWndCtl
is the handle of the control sending the command, and bHandled
is as described previously.
Notification Handler Macros
Notification handler macros map notifications (WM_NOTIFY messages) to functions, depending on the notification code and the identifier of the control sending the notification. These macros are equivalent to the command handler macros described in the preceding section, except that these handle notifications, not commands.
- NOTIFY_HANDLER
- NOTIFY_ID_HANDLER
- NOTIFY_CODE_HANDLER
- NOTIFY_RANGE_HANDLER
- NOTIFY_RANGE_CODE_HANDLER
Notification handler functions have the following prototype:
LRESULT NotifyHandler(int idCtrl, LPNMHDR pnmh, BOOL& bHandled);
where idCtrl
is the identifier of the control sending the notification, pnmh is a pointer to an NMHDR structure, and bHandled
is as described previously.
Notifications include a pointer to a notification-specific structure; for example, when a list view sends a notification, it includes a pointer to a NMLVDISPINFO structure. All such structures have an NMHDR header, which is what pnmh
points to. To access other members of the structure, cast pnmh
to a pointer to the specific structure.
For example, to handle an LVN_ENDLABELEDIT notification from a list view, put the following into the message map of the parent window:
NOTIFY_HANDLER( ID_LISTVIEW, LVN_ENDLABELEDIT, OnEndLabelEdit)
This notification sends additional information in an NMLVDISPINFO structure. The notification handler function might look something like this:
LRESULT OnEndLabelEdit(int idCtrl, LPNMHDR pnmh, BOOL& bHandled) { // The item is -1 if editing is being canceled. if ( ((NMLVDISPINFO*)pnmh)->item.iItem == -1) return FALSE; ...
Note how pnmh
is cast to an NMLVDISPINFO* to allow access to members beyond the NMHDR header.
Adding Functionality to Existing Window Classes
There are several ways to add functionality to an existing window class. If the class is an ATL window class, you can derive a new class from it, as described in Base Class Chaining. This is basically C++ inheritance with just a small twist because of message maps.
If you want to extend the capabilities of a predefined window class, such as the button or list box controls, you can superclass it. This defines a new class that is based on the predefined class, but with a message map that adds to the functionality of the underlying window class.
Sometimes you'll want to modify the behavior of a window instance, rather than a class—perhaps you need an edit control on a dialog box to do something special. In that case you can write an ATL window class that subclasses the existing edit control. Messages directed to the edit control are routed first through the message map of the subclassing object.
Alternatively, you could make the edit control a contained window, a window that passes the messages it receives to the message map of its containing class for processing; the containing class can implement special message handling behavior for the contained window.
Finally, there is message reflection, in which the window receiving a message does not handle it, but instead reflects it back to the sending window, which must then handle the message itself. This technique can help make controls more self-contained.
Base Class Chaining
Having defined an ATL windowing class with some functionality, you can derive a new class from it to take advantage of inheritance. For example:
class CBase: public CWindowImpl< CBase > // simple base window class: shuts down app when closed { BEGIN_MSG_MAP( CBase ) MESSAGE_HANDLER( WM_DESTROY, OnDestroy ) END_MSG_MAP() LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& ) { PostQuitMessage( 0 ); return 0; } }; class CDerived: public CBase // derived from CBase; handles mouse button events { BEGIN_MSG_MAP( CDerived ) MESSAGE_HANDLER( WM_LBUTTONDOWN, OnButtonDown ) END_MSG_MAP() LRESULT OnButtonDown( UINT, WPARAM, LPARAM, BOOL& ) { ATLTRACE( "button down/n" ); return 0; } }; // in WinMain(): ... CDerived win; win.Create( NULL, CWindow::rcDefault, "derived window" );
However, there is a problem with this example. If you run this program in the debugger, a window will appear. If you click inside the window, "button down" will appear in the Debug output window. This is behavior of CDerived. However, when you close the CDerived window, the application will not stop executing, even though CBase handles WM_DESTROY messages and CDerived inherits from CBase.
Why not? Because you have to explicitly "chain" one message map to another:
BEGIN_MSG_MAP( CDerived )
MESSAGE_HANDLER( WM_LBUTTONDOWN, OnButtonDown )
CHAIN_MSG_MAP( CBase ) // chain to base class
END_MSG_MAP()
Now any messages not handled in the message map of CDerived will be passed to the message map of CBase for processing.
Why isn't chaining from a class to its base class done automatically? That's because of multiple inheritance, which is fundamental to ATL's architecture. In general, there is no way to know which one of multiple base classes to chain to, so the decision is left to the programmer.
Alternate Message Maps
Message map chaining allows one message map to handle messages from multiple window classes, which creates a problem: The same WM_PAINT handler (for instance) is called for all window classes in the chain, even though you presumably want different painting behavior depending on the class. To solve this problem, ATL uses alternate message maps: You divide a message map into sections, each identified by a number. Each such section is an alternate message map:
// in class CBase: BEGIN_MSG_MAP( CBase ) MESSAGE_HANDLER( WM_CREATE, OnCreate1 ) MESSAGE_HANDLER( WM_PAINT, OnPaint1 )ALT_MSG_MAP( 100 )
MESSAGE_HANDLER( WM_CREATE, OnCreate2 ) MESSAGE_HANDLER( WM_PAINT, OnPaint2 )ALT_MSG_MAP( 101)
MESSAGE_HANDLER( WM_CREATE, OnCreate3 ) MESSAGE_HANDLER( WM_PAINT, OnPaint3 ) END_MSG_MAP()
The message map of CBase consists of three sections: the default message map (implicitly numbered 0) and two alternate message maps (numbered 100 and 101).
When you chain to the message map, specify the identifier of the desired alternate message map. For example:
class CDerived: public CBase {
BEGIN_MSG_MAP( CDerived )
CHAIN_MSG_MAP_ALT( CBase, 100
)
END_MSG_MAP()
...
The message map of CDerived chains to alternate message map 100 in CBase, so when a CDerived window receives a WM_PAINT message, handler function CBase::OnPaint2 will be invoked.
Other Kinds of Chaining
In addition to base class chaining, ATL also supports member chaining and dynamic chaining. These rarely used chaining techniques are outside the scope of this paper, but to cover them briefly, member chaining allows you to chain to the message map of a member variable, and dynamic chaining makes it possible to chain message maps at run time. For more information, see CHAIN_MSG_MAP_DYNAMIC and CHAIN_MSG_MAP_MEMBER in the online ATL documentation.
Window Superclassing
Superclassing defines a class that adds new functionality to a predefined window class, such as the button or list box controls. The following example superclasses the button control to define a button that beeps when clicked:
class CBeepButton: public CWindowImpl< CBeepButton >
{
public:
DECLARE_WND_SUPERCLASS( _T("BeepButton"), _T("Button") )
BEGIN_MSG_MAP( CBeepButton )
MESSAGE_HANDLER( WM_LBUTTONDOWN, OnLButtonDown )
END_MSG_MAP()
LRESULT OnLButtonDown( UINT, WPARAM, LPARAM, BOOL& bHandled )
{
MessageBeep( MB_ICONASTERISK );
bHandled = FALSE; // alternatively: DefWindowProc()
return 0;
}
}; // CBeepButton
The DECLARE_WND_SUPERCLASS macro declares the name of the class ("MyButton") and the name of the class being superclassed ("Button"). The message map has one entry, mapping WM_LBUTTONDOWN to OnLButtonDown. All other messages are handled by the default window procedure. Other than beeping, a CBeepButton window should behave as a normal button, so after OnLButtonDown calls MessageBeep, it sets bHandled to FALSE to let the default window procedure handle the message after OnLButtonDown is finished. (Alternatively, it could call DefWindowProc directly.)
So far, all we've done is to define a new class; we still have to create some actual CBeepButton windows. The following class has two data members of type CBeepButton; when an instance of this class is created, it creates two CBeepButton child windows.
const int ID_BUTTON1 = 101; const int ID_BUTTON2 = 102; class CMyWindow: public CWindowImpl< CMyWindow, CWindow, CWinTraits<WS_OVERLAPPEDWINDOW|WS_VISIBLE> > { CBeepButton b1, b2; BEGIN_MSG_MAP( CMyWindow ) MESSAGE_HANDLER( WM_CREATE, OnCreate ) COMMAND_CODE_HANDLER( BN_CLICKED, OnClick ) END_MSG_MAP() LRESULT OnClick(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { ATLTRACE( "Control %d clicked/n", wID ); return 0; } LRESULT OnCreate( UINT, WPARAM, LPARAM, BOOL& ) { RECT r1 = { 10, 10, 250, 80 }; b1.Create(*this, r1, "beep1", WS_CHILD|WS_VISIBLE, 0, ID_BUTTON1); RECT r2 = { 10, 110, 250, 180 }; b2.Create(*this, r2, "beep2", WS_CHILD|WS_VISIBLE, 0, ID_BUTTON2); return 0; } }; // CMyWindow
Window Subclassing
Subclassing allows you to change the behavior of an existing window, typically a control, by inserting a message map to intercept the window's messages. For example, suppose you have a dialog box with an edit control that you want to accept only non-numeric characters. You could do this by intercepting WM_CHAR messages destined for the edit control and discarding any messages indicating that a numeric character has been entered. Here is a class that does just that:
class CNoNumEdit: public CWindowImpl< CNoNumEdit > { BEGIN_MSG_MAP( CNoNumEdit ) MESSAGE_HANDLER( WM_CHAR, OnChar ) END_MSG_MAP() LRESULT OnChar( UINT, WPARAM wParam, LPARAM, BOOL& bHandled ) { TCHAR ch = wParam; if( _T('0') <= ch && ch <= _T('9') ) MessageBeep( 0 ); else bHandled = FALSE; return 0; } };
This class handles one message, WM_CHAR. If the character typed is a numeral, it is handled by calling MessageBeep and returning, effectively ignoring the character. For non-numeric characters, bHandled is set to FALSE, indicating that the default window procedure should handle the character.
Now we have to subclass an edit control so that CNonNumEdit gets first crack at the control's messages. (The following example uses CDialogImpl, which will be described in the section ATL Dialog Box Classes.) In this example, CMyDialog represents a dialog box resource (identified by IDD_DIALOG1) with an edit control (IDC_EDIT1). When the dialog box is initialized, the edit control is turned into a non-numeric edit control via SubclassWindow:
class CMyDialog: public CDialogImpl<CMyDialog>
{
public:
enum { IDD = IDD_DIALOG1 };
BEGIN_MSG_MAP( CMyDialog )
MESSAGE_HANDLER( WM_INITDIALOG, OnInitDialog )
END_MSG_MAP()
LRESULT OnInitDialog( UINT, WPARAM, LPARAM, BOOL& )
{
ed.SubclassWindow
( GetDlgItem( IDC_EDIT1 ) );
return 0;
}
CNoNumEdit ed;
};
Contained Windows
A contained window is a window that doesn't handle any messages; instead, it passes all messages it receives to the message map of another window, its container. Typically the contained window is a child window and its container is its parent window, but that is not always the case. The containing window is not necessarily the same as the parent window. The container–contained window relationship is defined in C++: the contained window is a data member of the container class. The relationship between parent and child windows on the screen is determined when the windows are created.
A contained window is based on a registered window class, such as an edit control. If an edit control is contained, the messages that are sent to it are actually handled in the containing window. In this way, the container can make the edit control behave in nonstandard ways. This is similar to subclassing but differs in that no new class need be defined to subclass the control. Compare the previous example, which defined the class CNoNumEdit to handle WM_CHAR messages, with the following, which has the WM_CHAR handler in the containing window class:
class CMyWindow: public CWindowImpl<CMyWindow> { CContainedWindow m_contained; public: CMyWindow(): m_contained( _T("edit"), this, 99 ) { } ...
CMyWindow is the container window class. The constructor initializes the CContainedWindow member as follows: The contained window is based on the edit control, and it forwards its messages to "this" (the parent window), using alternate message map 99.
BEGIN_MSG_MAP( CMyWindow ) MESSAGE_HANDLER( WM_CREATE, OnCreate ) MESSAGE_HANDLER( WM_DESTROY, OnDestroy ) ALT_MSG_MAP( 99 ) // contained window's messages come here... MESSAGE_HANDLER( WM_CHAR, OnChar ) END_MSG_MAP()
The parent window, when it is created, creates the contained window (in the WM_CREATE message handler). Since the contained window is based on the edit control, it will look like an edit control on the screen:
LRESULT OnCreate( UINT, WPARAM, LPARAM, BOOL& ) { RECT rc = { 10, 10, 200, 35 }; m_contained.Create( *this, rc, _T("non-numeric edit"), WS_CHILD|WS_VISIBLE|WS_BORDER, 0, 666 ); return 0; }
In this example, the container is also the parent window of the contained window.
The OnChar member function of the containing window is called when the contained window receives a WM_CHAR message. This is the same function as in the CNoNumEdit example, but in this case, it's a member of the container:
LRESULT OnChar( UINT, WPARAM wParam, LPARAM, BOOL& bHandled ) { TCHAR ch = wParam; if( _T('0') <= ch && ch <= _T('9') ) MessageBeep( 0 ); else bHandled = FALSE; return 0; } LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& ) { PostQuitMessage( 0 ); return 0; } };
You can also use a contained window to subclass an existing child window control, such as a control on a dialog box; again, unlike regular subclassing, the subclassed window's messages are handled by its containing window. In this next example, a dialog box subclasses an edit control, turning it into a contained window; the dialog box (container) handles WM_CHAR messages sent to the edit control and causes numeric characters to be ignored. (CDialogImpl is described in ATL Dialog Box Classes.)
class CMyDialog: public CDialogImpl<CMyDialog>
{
public:
enum { IDD = IDD_DIALOG1 };
// contained window is an edit control:
CMyDialog(): m_contained( "edit", this, 123 )
{
}
BEGIN_MSG_MAP( CMyDialog )
MESSAGE_HANDLER( WM_INITDIALOG, OnInitDialog )
ALT_MSG_MAP( 123 ) // contained window's messages come here...
MESSAGE_HANDLER( WM_CHAR, OnChar )
END_MSG_MAP()
LRESULT OnInitDialog( UINT, WPARAM, LPARAM, BOOL& bHandled )
{
// when the dialog box is created, subclass its edit control:
m_contained.SubclassWindow
( GetDlgItem(IDC_EDIT1) );
bHandled = FALSE;
return 0;
}
LRESULT OnChar( UINT, WPARAM wParam, LPARAM, BOOL& bHandled )
{
TCHAR ch = wParam;
if( _T('0') <= ch && ch <= _T('9') )
MessageBeep( 0 );
else
bHandled = FALSE;
return 0;
}
CContainedWindow m_contained;
};
Message Reflection
The previous sections present several techniques for extending the functionality of a window class by changing how a window responds to messages sent to it. In contrast, message reflection involves getting a window to respond to messages that it sends out.
When the user interacts with a control, the control typically informs its parent window by sending it a WM_COMMAND or WM_NOTIFY message; the parent window then takes some action in response. For example:
class CParentWindow: CWindowImpl<CParentWindow>
{
// assume that this window will have a child
// button control with an ID of ID_BUTTON
BEGIN_MSG_MAP( CParentWindow )
COMMAND_ID_HANDLER( ID_BUTTON, OnButton )
MESSAGE_HANDLER( WM_CTLCOLORBUTTON, OnColorButton )
...
When the button is clicked, it sends a command to the parent window, and CParentWindow::OnButton is invoked. Similarly, when the button is about to be drawn, it sends a WM_CTLCOLORBUTTON message to the parent; CParentWindow::OnColorButton responds with the handle of a brush.
In some cases, it is useful to have the control itself respond to the messages it sends, rather than the parent window. ATL provides a facility for message reflection: when a control sends its parent a message, the parent can reflect the message back to the control.
class CParentWindow: CWindowImpl<CParentWindow>
{
BEGIN_MSG_MAP( CParentWindow )
MESSAGE_HANDLER( WM_CREATE, OnCreate )
MESSAGE_HANDLER( WM_DESTROY, OnDestroy )
...other messages that CParentWindow will handle...
REFLECT_NOTIFICATIONS()
END_MSG_MAP()
...
When the parent window receives a message, it examines its message map; if none of the message map entries match the message, the REFLECT_NOTIFICATIONS macro causes the message to be sent back to the control that sent it. The control provides handlers for the reflected messages, like this:
class CHandlesItsOwnMessages: CWindowImpl<CHandlesItsOwnMessage> { public: DECLARE_WND_SUPERCLASS( _T("Superbutton"), _T("button") ) BEGIN_MSG_MAP( CHandlesItsOwnMessage ) MESSAGE_HANDLER(OCM_COMMAND
, OnCommand ) MESSAGE_HANDLER(OCM_CTLCOLORBUTTON
, OnColorButton )DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP() ...
Note that the message identifiers for reflected messages start with OCM_, not WM_. This allows you to distinguish between messages originally intended for the control and messages that have been reflected back to the control. The DEFAULT_REFLECTION_HANDLER macro takes care of unhandled reflected messages.
The originating child control must either be an instance of this class or a normal button control that has been subclassed. For example:
// in CParentWindow: CHandlesItsOwnMessages m_button; LRESULT OnCreate( UINT, WPARAM, LPARAM, BOOL& ) { RECT rc; // initialize appropriately m_button.Create( *this, rc, _T("click me"), WS_CHILD|WS_VISIBLE ); ...
Or, if there is already a button control (for example, the parent window could be a dialog box):
m_button.SubclassWindow( GetDlgItem(ID_BUTTON) );
The following example defines CStaticLink, a static control that, when clicked, opens the Web page that it names. All messages from a CStaticLink control are reflected back to it by the parent window (in this case, a dialog box—see ATL Dialog Box Classes). In addition to handling reflected WM_COMMAND messages, CStaticLink handles reflected WM_CTLCOLORSTATIC messages so that it can color itself differently, depending on whether it has been clicked or not.
#include "stdafx.h" #include "resource.h" CComModule _Module; class CStaticLink : public CWindowImpl<CStaticLink> { /* Based on CStaticLink by Paul DiLascia, C++ Q&A, Microsoft Systems Journal 12/1997. Turns static controls into clickable "links" -- when the control is clicked, the file/program/webpage named in the control's text (or set by SetLinkText()) is opened via ShellExecute(). Static control can be either text or graphic (bitmap, icon, etc.). */ public: DECLARE_WND_SUPERCLASS( _T("StaticLink"), _T("Static") ) CStaticLink() : m_colorUnvisited( RGB(0,0,255) ), m_colorVisited( RGB(128,0,128) ), m_bVisited( FALSE ), m_hFont( NULL ) { } void SetLinkText( LPCTSTR szLink ) { USES_CONVERSION; m_bstrLink = T2OLE( szLink ); } BEGIN_MSG_MAP(CStaticLink) // uses message reflection: WM_* comes back as OCM_* MESSAGE_HANDLER( OCM_COMMAND, OnCommand ) MESSAGE_HANDLER( OCM_CTLCOLORSTATIC, OnCtlColor ) MESSAGE_HANDLER( WM_DESTROY, OnDestroy ) // not a reflected message DEFAULT_REFLECTION_HANDLER() END_MSG_MAP() LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& ) { if( m_hFont ) DeleteObject( m_hFont ); return 0; } LRESULT OnCommand( UINT, WPARAM wParam, LPARAM, BOOL& ) { USES_CONVERSION; int code = HIWORD( wParam ); if( code == STN_CLICKED || code == STN_DBLCLK ){ if( m_bstrLink.Length() == 0 ){ GetWindowText( &m_bstrLink ); } if( (int)ShellExecute( *this, _T("open"), OLE2T(m_bstrLink), NULL, NULL, SW_SHOWNORMAL ) > 32 ){ m_bVisited = TRUE; // return codes > 32 => success Invalidate(); }else{ MessageBeep( 0 ); ATLTRACE( _T("Error: CStaticLink couldn't open file") ); } } return 0; } LRESULT OnCtlColor( UINT, WPARAM wParam, LPARAM, BOOL& ) { // notify bit must be set to get STN_* notifications ModifyStyle( 0, SS_NOTIFY ); HBRUSH hBr = NULL; if( (GetStyle() & 0xff) <= SS_RIGHT ){ // it's a text control: set up font and colors if( !m_hFont ){ LOGFONT lf; GetObject( GetFont(), sizeof(lf), &lf ); lf.lfUnderline = TRUE; m_hFont = CreateFontIndirect( &lf ); } HDC hDC = (HDC)wParam; SelectObject( hDC, m_hFont ); SetTextColor( hDC, m_bVisited ? m_colorVisited : m_colorUnvisited ); SetBkMode( hDC, TRANSPARENT ); hBr = (HBRUSH)GetStockObject( HOLLOW_BRUSH ); } return (LRESULT)hBr; } private: COLORREF m_colorUnvisited; COLORREF m_colorVisited; BOOL m_bVisited; HFONT m_hFont; CComBSTR m_bstrLink; }; // CStaticLink class CReflectDlg : public CDialogImpl<CReflectDlg> { public: enum { IDD = IDD_DIALOG1 }; BEGIN_MSG_MAP(CReflectDlg) COMMAND_RANGE_HANDLER( IDOK, IDCANCEL, OnClose ) MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog) REFLECT_NOTIFICATIONS() // reflect messages back to static links END_MSG_MAP() LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) { CenterWindow( GetParent() ); // a textual static control: s1.SubclassWindow( GetDlgItem(IDS_TEST1) ); // a static control displaying an icon s2.SubclassWindow( GetDlgItem(IDS_TEST2) ); // set the icon's link s2.SetLinkText( _T("http://www.microsoft.com") ); return 1; } LRESULT OnClose(UINT, WPARAM wID, HWND, BOOL& ) { ::EndDialog( m_hWnd, wID ); return 0; } private: CStaticLink s1, s2; }; // CReflectDlg int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow) { _Module.Init( NULL, hInstance ); CReflectDlg dlg; dlg.DoModal(); _Module.Term(); return 0; }
ATL Dialog Box Classes
Now that you know some of the capabilities of ATL's window classes, it's time to consider dialog boxes. Your projects will probably contain several dialog box resources, perhaps ranging from simple modal "About boxes" to control-laden modeless dialog boxes. ATL provides CSimpleDialog and CDialogImpl to simplify using your dialog box resources from within your application.
CSimpleDialog
CSimpleDialog is a class that creates modal dialog boxes from templates. It provides command handlers for standard buttons such as OK and Cancel. You can think of CSimpleDialog as a kind of message box, except that you design the "message box" in the dialog box editor so you have control over its layout.
To display such a dialog box in response to, say, a user clicking About in your application's Help menu, you would add the following command handler to your main window's class:
BEGIN_MSG_MAP( CMyMainWindow )
COMMAND_ID_HANDLER( ID_HELP_ABOUT, OnHelpAbout )
...
LRESULT OnHelpAbout( WORD, WORD, HWND, BOOL& )
{
CSimpleDialog<IDD_DIALOG1> dlg;
int ret = dlg.DoModal();
return 0;
}
Note that the ID of the dialog box resource (IDD_DIALOG1) is passed to the CSimpleDialog template. DoModal displays the dialog box. When the user clicks OK, CSimpleDialog closes the dialog box and returns the ID of the button. (CSimpleDialog has command handlers for IDOK, IDCANCEL, IDABORT, IDRETRY, IDIGNORE, IDYES, and IDNO.)
CDialogImpl
CSimpleDialog only handles simple modal dialog boxes. For more complicated dialog boxes, or for modeless dialog boxes, there is CDialogImpl. (CSimpleDialog is actually a limited kind of CDialogImpl.)
If you want to implement a modeless dialog box, you have to derive a class from CDialogImpl. Pass the name of your new class as a template argument to CDialogImpl, as you would for CWindowImpl:
class CMyModelessDialog: public CDialogImpl<CMyModelessDialog> {
Unlike CSimpleDialog, you don't pass the dialog box resource identifier as a template argument; however, you still have to "connect" this class with a dialog box resource, and you do that by defining an enum in the class:
public: enum { IDD = IDD_DIALOG1 };
Now declare a message map:
BEGIN_MSG_MAP( CMyDialog ) MESSAGE_HANDLER( WM_INITDIALOG, OnInitDialog ) MESSAGE_HANDLER( WM_CLOSE, OnClose ) ... END_MSG_MAP()
The handler function definitions are up to you, but one thing you have to do, since you are implementing a modeless dialog box, is to call DestroyWindow in response to a WM_CLOSE message:
LRESULT OnClose( UINT, WPARAM, LPARAM, BOOL& ) { DestroyWindow(); return 0; } ... }; // CMyModelessDialog
To get this dialog box on the screen, you have to construct an instance of the class and call its Create function:
CMyModelessDialog dlg; dlg.Create( wndParent );
If the dialog box resource does not have the WS_VISIBLE style, you have to make the dialog box visible:
dlg.ShowWindow( SW_SHOW );
The following example has a modeless dialog box that accepts a string from the user, and a main window that displays the strings. The dialog box has an edit box and a button; when the button is clicked, the dialog box calls its owner window's DoSomething member function with the text from the edit box. The owner window is a superclassed list box control, and DoSomething adds the new string to its list.
#include "atlbase.h" CComModule _Module; #include "atlwin.h" #include "resource.h" class CMyWindow: public CWindowImpl<CMyWindow> { public: DECLARE_WND_SUPERCLASS( "MyWindow", "listbox" ) BEGIN_MSG_MAP( CMyWindow ) MESSAGE_HANDLER( WM_DESTROY, OnDestroy ) END_MSG_MAP() LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& ) { PostQuitMessage( 0 ); return 0; } void DoSomething( LPCTSTR s ) { SendMessage( LB_ADDSTRING, 0, reinterpret_cast<LPARAM>(s) ); } }; class CMyDialog: public CDialogImpl<CMyDialog> { public: enum { IDD = IDD_DIALOG1 }; BEGIN_MSG_MAP( CMyDialog ) COMMAND_ID_HANDLER( IDC_BUTTON1, OnButton ) MESSAGE_HANDLER( WM_INITDIALOG, OnInitDialog ) MESSAGE_HANDLER( WM_CLOSE, OnClose ) END_MSG_MAP() LRESULT OnButton(WORD, WORD, HWND, BOOL&) { char buf[100]; m_ed.GetWindowText( buf, 100 ); m_owner.DoSomething( buf ); return 0; } LRESULT OnInitDialog( UINT, WPARAM, LPARAM, BOOL& ) { m_owner.Attach( GetParent() ); CenterWindow( m_owner ); m_ed = GetDlgItem( IDC_EDIT1 ); return TRUE; } LRESULT OnClose( UINT, WPARAM, LPARAM, BOOL& ) { DestroyWindow(); m_owner.Detach(); return 0; } CMyWindow m_owner; CWindow m_ed; }; CMyDialog dlg; int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { _Module.Init( NULL, hInstance ); CMyWindow win; win.Create( NULL, CWindow::rcDefault, _T("modeless dialog test"), WS_OVERLAPPEDWINDOW|WS_VISIBLE ); dlg.Create( win ); dlg.ShowWindow( SW_SHOW ); MSG msg; while( GetMessage( &msg, NULL, 0, 0 ) ){ if( !IsWindow(dlg) || !dlg.IsDialogMessage( &msg ) ){ DispatchMessage( &msg ); } } _Module.Term(); return 0; }
Specifying Window Class Information
A large portion of this paper has dealt with window class behavior—how a window responds to messages. Beside its behavior, a window class has other important attributes, such as its styles, class name, background color and cursor, and so on. This section demonstrates how to use the macros that ATL supplies to specify those attributes.
Using Window Traits to Specify Styles
In the examples so far, window styles have been specified in the call to Create:
CMyWindow wnd;
wnd.Create( NULL, CWindow::rcDefault, _T("Hello"),
WS_OVERLAPPEDWINDOW|WS_VISIBLE
);
If you don't specify any window styles (or extended window styles), ATL will supply defaults; these defaults are defined as traits of the window class. The default traits are CControlWinTraits, defined as follows:
typedef CWinTraits<WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN |WS_CLIPSIBLINGS, 0> CControlWinTraits;
CWinTraits is a template for traits; it takes two arguments, window styles, and extended window styles.
In the definition of CWindowImpl, CControlWinTraits is supplied as a default template parameter:
template <class T, class TBase = CWindow, class TWinTraits = CControlWinTraits> class CWindowImpl : public ...
So by default, a CWindowImpl-derived window is a visible child window that clips its children and siblings.
You can define your own window traits:
typedef CWinTraits<WS_OVERLAPPEDWINDOW|WS_VISIBLE,0> MyTraits;
Then, when you derive a class from CWindowImpl, specify your window traits:
class CMyWindow: public CWindowImpl<CMyWindow,CWindow,MyTraits
>
{...};
Or more directly:
class CMyWindow: public CWindowImpl<
CMyWindow,
CWindow,
CWinTraits<WS_OVERLAPPEDWINDOW|WS_VISIBLE,0>
>
{...};
Note that you have to supply all three template arguments: the derived class, the base class (CWindow), and the trait class.
CMyWindow windows now have the default style of "visible overlapped window," so you can omit the styles from the Create call:
CMyWindow wnd; wnd.Create( NULL, CWindow::rcDefault, _T("Hello") ); // style: WS_OVERLAPPEDWINDOW|WS_VISIBLE
You can always override traits:
ovwnd.Create( NULL, CWindow::rcDefault, _T("Hello"), WS_OVERLAPPEDWINDOW ); // not visible
Window traits can also include extended styles:
class CClientWindow: public CWindowImpl<CClientWindow, CWindow, CWinTraits< WS_OVERLAPPEDWINDOW|WS_VISIBLE, WS_EX_CLIENTEDGE > > {...};
DECLARE_WND_CLASS
Use the DECLARE_WND_CLASS macro to define just the name of the window class:
DECLARE_WND_CLASS("my window class");
This is equivalent to:
DECLARE_WND_CLASS_EX( "my window class", CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS, // default style COLOR_WINDOW // default color );
DECLARE_WND_CLASS_EX
Use the DECLARE_WND_CLASS_EX macro to name your class and specify its style and background color:
class CMyWindow: public CWindowImpl<CMyWindow> { public: DECLARE_WND_CLASS_EX( "my window class", // class name CS_HREDRAW|CS_VREDRAW, // class style COLOR_WINDOW // background color ); BEGIN_MSG_MAP(CMyWindow) ...
The class name is the name under which the class is registered. If you don't provide a name, ATL will generate one for you, but if you're using a tool such as Spy++, you'll probably find that a name you create will be more helpful than "ATL:00424bd0".
The class style is a bitwise-ORed combination of class style flags.
The background color is one of the standard system colors.
CWndClassInfo
You can customize a window class beyond what the DECLARE_WND_ macros allow. If you look at the definition of DECLARE_WND_CLASS, you see that it defines a CWndClassInfo structure and a function that returns it:
#define DECLARE_WND_CLASS(WndClassName) / static CWndClassInfo& GetWndClassInfo() / { / static CWndClassInfo wc = / { / { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, / StartWindowProc, / 0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, / WndClassName, NULL }, / NULL, NULL, IDC_ARROW, TRUE, 0, _T("") / }; / return wc; / }
CWndClassInfo is a structure that provides many customization possibilities. It looks something like this:
struct CWndClassInfo { struct WNDCLASSEX { UINT cbSize; UINT style; WNDPROC lpfnWndProc; int cbClsExtra; int cbWndExtra; HINSTANCE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPCSTR lpszMenuName; LPCSTR lpszClassName; HICON hIconSm; } m_wc; LPCSTR m_lpszOrigName; WNDPROC pWndProc; LPCSTR m_lpszCursorID; BOOL m_bSystemCursor; ATOM m_atom; CHAR m_szAutoName[13]; ATOM Register(WNDPROC* p); };
For example, to specify a window's cursor, you have to set m_lpszCursorID to the name of the cursor. If it's a system cursor, set m_bSystemCursor to TRUE, otherwise set it to FALSE. Note how DECLARE_WND_CLASS sets those two members to IDC_ARROW and TRUE, respectively. Since the DECLARE_WND_ macros don't provide any way to override those default values, you have to do it yourself:
class CMyWindow: public CWindowImpl<CMyWindow>
{
public:
static CWndClassInfo& GetWndClassInfo()
{
// a manual DECLARE_WND_CLASS macro expansion
// modified to specify an application-defined cursor:
static CWndClassInfo wc =
{
{ sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS,
StartWindowProc,
0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL,
"MyWindow", NULL },
NULL, NULL, MAKEINTRESOURCE(IDC_CURSOR1), FALSE,
0, _T("")
};
return wc;
}
...
Conclusion
ATL provides a simplified, elegant, and powerful Windows programming model. Beyond the convenience of wrapper functions, message maps, and macros, techniques such as chaining, window subclassing and superclassing, contained windows, and reflected messages afford great flexibility in the design and implementation of window and dialog box classes. Perhaps most impressive is that, even while providing all this power and flexibility, ATL imposes only minimal memory and execution time overhead.