Contents
Introduction to Part II
OK, time to actually start talking about WTL! In this part I'll cover the basics of writing a main frame window and cover some of the welcome improvements WTL gives, such as UI updating and better message maps. To get the most out of this part, you should have WTL installed so the header files are in the VC search path, and the AppWizard is in the appropriate directory. The WTL distribution comes with instructions on how to install the AppWizard, so consult the docs.
Remember, if you encounter any problems installing WTL or compiling the demo code, read the readme section of Part I before posting questions here.
WTL Overview
The WTL classes can be divided into a few major categories:
- Frame window implementation -
CFrameWindowImpl
, CMDIFrameWindowImpl
- Control wrappers -
CButton
, CListViewCtrl
- GDI wrappers -
CDC
, CMenu
- Special UI features -
CSplitterWindow
, CUpdateUI
, CDialogResize
, CCustomDraw
- Utility classes and macros -
CString
, CRect
, BEGIN_MSG_MAP_EX
This article will cover frame windows in depth and touch on some of the UI features and utility classes. Most of the classes are stand-alone classes, although a few such as CDialogResize
are mix-ins.
Beginning a WTL EXE
If you don't use the WTL AppWizard (we'll get to that later), a WTL EXE begins much like an ATL EXE. The sample code in this article will be another frame window as in Part I, but will be a bit less trivial in order to show some WTL features.
For this section, we'll start a new EXE from scratch. The main window will show the current time in its client area. Here is a basic stdafx.h:
#define STRICT
#define WIN32_LEAN_AND_MEAN
#define _WTL_USE_CSTRING
#include <atlbase.h>
#include <atlapp.h>
extern CAppModule _Module;
#include <atlwin.h>
#include <atlframe.h>
#include <atlmisc.h>
#include <atlcrack.h>
atlapp.h is the first WTL header you include. It contains classes for message handling and CAppModule
, a class derived from CComModule
. You should also define _WTL_USE_CSTRING
if you plan on using CString
, because CString
is defined in atlmisc.h yet there are other headers that come before atlmisc.h which have features that use CString
. Defining _WTL_USE_CSTRING
makes atlapp.h forward-declare the CString
class so those other headers know what a CString
is.
(Note that we need a global CAppModule
variable, even though in Part I this was not necessary. CAppModule
has some features relating to idle processing and UI updating that we need, so we need that CAppModule
to be present.)
Next let's define our frame window. SDI windows like ours are derived from CFrameWindowImpl
. The window class is defined with DECLARE_FRAME_WND_CLASS
instead of DECLARE_WND_CLASS
. Here's the beginning of our window definition in MyWindow.h:
class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
DECLARE_FRAME_WND_CLASS(_T("First WTL window"), IDR_MAINFRAME);
BEGIN_MSG_MAP(CMyWindow)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
};
DECLARE_FRAME_WND_CLASS
takes two parameters, the window class name (which can be NULL to have ATL generate a name for you), and a resource ID. WTL will look for an icon, menu, and accelerator table with that ID, and load them when the window is created. It will also look for a string with that ID and use it as the window title. We also chain messages to CFrameWindowImpl
as it has some message handlers of its own (most notably WM_SIZE
and WM_DESTROY
).
Now let's look at WinMain()
. It is pretty similar to the WinMain()
we had in Part I, the difference is in the call to create the main window.
#include "stdafx.h"
#include "MyWindow.h"
CAppModule _Module;
int APIENTRY WinMain ( HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow )
{
_Module.Init ( NULL, hInstance );
CMyWindow wndMain;
MSG msg;
if ( NULL == wndMain.CreateEx() )
return 1;
wndMain.ShowWindow ( nCmdShow );
wndMain.UpdateWindow();
while ( GetMessage ( &msg, NULL, 0, 0 ) > 0 )
{
TranslateMessage ( &msg );
DispatchMessage ( &msg );
}
_Module.Term();
return msg.wParam;
}
CFrameWindowImpl
has a CreateEx()
method that has the most common default values, so we don't need to specify any parameters. CFrameWindowImpl
will also handle loading resources as explained earlier, so you should make some dummy resources now with IDs of IDR_MAINFRAME
, or check out the sample code accompanying the article.
If you run this now, you'll see the main frame window, but of course it doesn't actually do anything yet. We'll need to add some message handlers to do stuff, so now is a good time to cover the WTL message map macros.
WTL Message Map Enhancements
One of the cumbersome and error-prone things you do when using the Win32 API is unpacking parameters from the WPARAM
and LPARAM
data sent with a message. Unfortunately, ATL doesn't help much, and we still have to unpack data from all messages aside from WM_COMMAND
and WM_NOTIFY
. But WTL comes to the rescue here!
WTL's enhanced message map macros are in atlcrack.h. (The name comes from "message cracker", a term used for similar macros in windowsx.h.) The initial step in using these macros differs on VC 6 and VC 7, this note in atlcrack.h explains it:
For ATL 3.0, a message map using cracked handlers must use BEGIN_MSG_MAP_EX
.
For ATL 7.0/7.1, you can use BEGIN_MSG_MAP
for CWindowImpl
/CDialogImpl
derived classes, but must use BEGIN_MSG_MAP_EX
for classes that don't derive from CWindowImpl
/CDialogImpl
.
So if you are using VC 6, you would make the change in MyWindow.h:
class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
BEGIN_MSG_MAP_EX(CMyWindow)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
};
(The _EX
macro is needed in VC 6 because it contains some code that the message handler macros use. For the sake of readability, I won't show the VC 6 and VC 7 versions of the header file here, since they only differ in that one macro. Just keep in mind that the _EX
macro is not needed in VC 7.)
For our clock program, we'll need to handle WM_CREATE
and set a timer. The WTL message handler for a message is called MSG_
followed by the message name, for example MSG_WM_CREATE
. These macros take just the name of the handler, so let's add one for WM_CREATE
:
class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
BEGIN_MSG_MAP_EX(CMyWindow)
MSG_WM_CREATE(OnCreate)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
};
WTL message handlers look a lot like MFC's, where each handler has a different prototype depending on what parameters are passed with the message. But since we don't have a wizard to write the handler, we'll have to find the prototype ourselves. Fortunately, VC can help out. Put the cursor on the "MSG_WM_CREATE" text and press F12 to go to the definition of that macro. On VC 6, VC will rebuild the project first to build its browse info database. Once that's done, VC will open atlcrack.h at the definition of MSG_WM_CREATE
:
#define MSG_WM_CREATE(func) \
if (uMsg == WM_CREATE) \
{ \
SetMsgHandled(TRUE); \
lResult = (LRESULT)func((LPCREATESTRUCT)lParam); \
if(IsMsgHandled()) \
return TRUE; \
}
The underlined code is the important part, it's the actual call to the handler, and it tells us the handler returns an LRESULT
and takes one parameter, a LPCREATESTRUCT
. Notice that there is no bHandled
parameter like the ATL macros use. The SetMsgHandled()
function replaces that parameter; I'll explain this more shortly.
Now we can add an OnCreate()
handler to our window class:
class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
BEGIN_MSG_MAP_EX(CMyWindow)
MSG_WM_CREATE(OnCreate)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
LRESULT OnCreate(LPCREATESTRUCT lpcs)
{
SetTimer ( 1, 1000 );
SetMsgHandled(false);
return 0;
}
};
CFrameWindowImpl
inherits indirectly from CWindow
, so it has all the CWindow
functions like SetTimer()
. This makes windowing API calls look a lot like MFC code, where you use the various CWnd
methods that wrap APIs.
We call SetTimer()
to create a timer that fires every second (1000 ms). Since we want to let CFrameWindowImpl
handle WM_CREATE
as well, we call SetMsgHandled(false)
so that the message gets chained to base classes via the CHAIN_MSG_MAP
macro. This call replaces the bHandled
parameter that the ATL macros use. (Even though CFrameWindowImpl
doesn't handle WM_CREATE
, calling SetMsgHandled(false)
is a good habit to get into when using base classes, so you don't have to remember which messages the base classes handle. This is similar to the code that ClassWizard generates; most handlers start or end with a call to the base class handler.)
We'll also need a WM_DESTROY
handler so we can stop the timer. Going through the same process as before, we find the MSG_WM_DESTROY
macro looks like this:
#define MSG_WM_DESTROY(func) \
if (uMsg == WM_DESTROY) \
{ \
SetMsgHandled(TRUE); \
func(); \
lResult = 0; \
if(IsMsgHandled()) \
return TRUE; \
}
So our OnDestroy()
handler takes no parameters and returns nothing. CFrameWindowImpl
does handle WM_DESTROY
as well, so in this case we definitely need to call SetMsgHandled(false)
:
class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
BEGIN_MSG_MAP_EX(CMyWindow)
MSG_WM_CREATE(OnCreate)
MSG_WM_DESTROY(OnDestroy)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
void OnDestroy()
{
KillTimer(1);
SetMsgHandled(false);
}
};
Next up is our WM_TIMER
handler, which is called every second. You should have the hang of the F12 trick by now, so I'll just show the handler:
class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
BEGIN_MSG_MAP_EX(CMyWindow)
MSG_WM_CREATE(OnCreate)
MSG_WM_DESTROY(OnDestroy)
MSG_WM_TIMER(OnTimer)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
void OnTimer ( UINT uTimerID, TIMERPROC pTimerProc )
{
if ( 1 != uTimerID )
SetMsgHandled(false);
else
RedrawWindow();
}
};
This handler just redraws the window so the new time appears in the client area. Finally, we handle WM_ERASEBKGND
and in that handler, we draw the current time in the upper-left corner of the client area.
class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
BEGIN_MSG_MAP_EX(CMyWindow)
MSG_WM_CREATE(OnCreate)
MSG_WM_DESTROY(OnDestroy)
MSG_WM_TIMER(OnTimer)
MSG_WM_ERASEBKGND(OnEraseBkgnd)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
LRESULT OnEraseBkgnd ( HDC hdc )
{
CDCHandle dc(hdc);
CRect rc;
SYSTEMTIME st;
CString sTime;
GetClientRect ( rc );
GetLocalTime ( &st );
sTime.Format ( _T("The time is %d:%02d:%02d"),
st.wHour, st.wMinute, st.wSecond );
dc.SaveDC();
dc.SetBkColor ( RGB(255,153,0) );
dc.SetTextColor ( RGB(0,0,0) );
dc.ExtTextOut ( 0, 0, ETO_OPAQUE, rc, sTime,
sTime.GetLength(), NULL );
dc.RestoreDC(-1);
return 1;
}
};
This handler demonstrates one of the GDI wrappers, CDCHandle
, as well as CRect
and CString
. All I need to say about CString
is, it's identical to MFC's CString
. I'll cover the wrapper classes later on, but for now you can think of CDCHandle
as a simple wrapper around an HDC
, similar to MFC's CDC
, although when the CDCHandle
goes out of scope, it doesn't destroy the underlying device context.
So after all that, here's what our window looks like:
The sample code also has WM_COMMAND
handlers for the menu items; I won't cover those here, but you can check out the sample project and see the WTL macro COMMAND_ID_HANDLER_EX
in action.
If you're using VC 7.1, check out the WTL Helper plug-in by Sergey Solozhentsev, which will do the grunt work of adding message map macros for you.
What You Get with the WTL AppWizard
The WTL distribution comes with a very nice AppWizard, so let's see what features it puts in an SDI app.
Going through the wizard (VC 6)
Click File|New in VC and select ATL/WTL AppWizard from the list. We'll be rewriting the clock app, so enter WTLClock for the project name:
The next page is where you select an SDI, MDI, or dialog-based app, along with some other options. Select the options as shown here and click Next:
The last page is where we can choose to have a toolbar, rebar, and status bar. To keep this app simple, uncheck all those and click Finish.
Going through the wizard (VC 7)
Click File|New|Project in VC and select ATL/WTL AppWizard from the list. We'll be rewriting the clock app, so enter WTLClock for the project name and click OK:
When the AppWizard screen comes up, click Application Type. This page is where you select an SDI, MDI, or dialog-based app, along with some other options. Select the options as shown here and click User Interface Features:
The last page is where we can choose to have a toolbar, rebar, and status bar. To keep this app simple, uncheck all those and click Finish.
Examining the generated code
After finishing the wizard, you'll see three classes in the generated code: CMainFrame
, CAboutDlg
, and CWTLClockView
. From the names, you can guess what the purpose of each class is. While there is a "view" class, it is strictly a "plain" window derived from CWindowImpl
; there is no framework at all like MFC's doc/view architecture.
There is also a _tWinMain()
, which initializes COM, the common controls, and _Module
, and then calls a global Run()
function. Run()
handles creating the main window and starting the message pump. It also uses a new class, CMessageLoop
. Run()
calls CMessageLoop::Run()
which actually contains the message pump. I'll cover CMessageLoop
in more detail in the next section.
CAboutDlg
is a simple CDialogImpl
-derived class that is associated with a dialog with ID IDD_ABOUTBOX
. I covered dialogs in Part I, so you should be able to understand the code in CAboutDlg
.
CWTLClockView
is our app's "view" class. This works like an MFC view, in that it is a captionless window that occupies the client area of the main frame. CWTLClockView
has a PreTranslateMessage()
function, which works just like the MFC function of the same name, and a WM_PAINT
handler. Neither function does anything significant yet, but we'll fill in the OnPaint()
method to show the time.
Finally, we have CMainFrame
, which has lots of interesting new stuff. Here is an abbreviated version of the class definition:
class CMainFrame : public CFrameWindowImpl<CMainFrame>,
public CUpdateUI<CMainFrame>,
public CMessageFilter,
public CIdleHandler
{
public:
DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME)
BEGIN_UPDATE_UI_MAP(CMainFrame)
END_UPDATE_UI_MAP()
BEGIN_MSG_MAP(CMainFrame)
CHAIN_MSG_MAP(CUpdateUI<CMainFrame>)
CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>)
END_MSG_MAP()
BOOL PreTranslateMessage(MSG* pMsg);
BOOL OnIdle();
protected:
CWTLClockView m_view;
};
CMessageFilter
is a mix-in class that provides PreTranslateMessage()
, and CIdleHandler
is another mix-in that provides OnIdle()
. CMessageLoop
, CIdleHandler
, and CUpdateUI
work together to provide UI updating that works like ON_UPDATE_COMMAND_UI
in MFC.
CMainFrame::OnCreate()
creates the view window and saves its window handle, so that the view will be resized when the frame window is resized. OnCreate()
also adds the CMainFrame
object to lists of message filters and idle handlers kept by the CAppModule
; I will cover those later.
LRESULT CMainFrame::OnCreate(UINT , WPARAM ,
LPARAM , BOOL& )
{
m_hWndClient = m_view.Create(m_hWnd, rcDefault, NULL, |
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS |
WS_CLIPCHILDREN, WS_EX_CLIENTEDGE);
CMessageLoop* pLoop = _Module.GetMessageLoop();
pLoop->AddMessageFilter(this);
pLoop->AddIdleHandler(this);
return 0;
}
m_hWndClient
is a member of CFrameWindowImpl
; this is the window that will be resized when the frame is resized.
The generated CMainFrame
also has handlers for File|New, File|Exit, and Help|About. We won't need most of the default menu items for our clock, but it won't do any harm to leave them in for now. You can build and run the wizard-generated code now, although the app won't be very useful yet. You might be interested in stepping through the CMainFrame::CreateEx()
call in the global Run()
to see exactly how the frame window and its resources are loaded and created.
Our next stop on the WTL tour is CMessageLoop
, which handles the message pump and idle processing.
CMessageLoop Internals
CMessageLoop
provides a message pump for our app. Aside from a standard TranslateMessage
/DispatchMessage
loop, it also provides message filtering via PreTranslateMessage()
and idle processing via OnIdle()
. Here is pseudo code for the logic in Run()
:
int Run()
{
MSG msg;
for(;;)
{
while ( !PeekMessage(&msg) )
CallIdleHandlers();
if ( 0 == GetMessage(&msg) )
break;
if ( !CallPreTranslateMessageFilters(&msg) )
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
}
CMessageLoop
knows which PreTranslateMessage()
functions to call because objects that need to filter messages call CMessageLoop::AddMessageFilter()
, just like CMainFrame::OnCreate()
does. And similarly, objects that need to do idle processing call CMessageLoop::AddIdleHandler()
.
Notice that there are no calls to TranslateAccelerator()
or IsDialogMessage()
in the message loop. CFrameWindowImpl
handles the former, but if you add any modeless dialogs to your app, you'll need to add a call to IsDialogMessage()
in CMainFrame::PreTranslateMessage()
.
CFrameWindowImpl Internals
CFrameWindowImpl
and its base class CFrameWindowImplBase
provide a lot of the features you're used to having in MFC's CFrameWnd
: toolbars, rebars, status bars, tooltips for toolbar buttons, and flyby help for menu items. I'll be covering all those features gradually, since discussing the entire CFrameWindowImpl
class could take up two whole articles! For now, it will be sufficient to see how CFrameWindowImpl
handles WM_SIZE
and its client area. For this discussion, remember that m_hWndClient
is a member of CFrameWindowImplBase
that holds the HWND
of the "view" window inside the frame.
CFrameWindowImpl
has a handler for WM_SIZE
:
LRESULT OnSize(UINT , WPARAM wParam, LPARAM , BOOL& bHandled)
{
if(wParam != SIZE_MINIMIZED)
{
T* pT = static_cast<T*>(this);
pT->UpdateLayout();
}
bHandled = FALSE;
return 1;
}
This checks that the window is not being minimized. If not, it delegates to UpdateLayout()
. Here is UpdateLayout()
:
void UpdateLayout(BOOL bResizeBars = TRUE)
{
RECT rect;
GetClientRect(&rect);
UpdateBarsPosition(rect, bResizeBars);
if(m_hWndClient != NULL)
::SetWindowPos(m_hWndClient, NULL, rect.left, rect.top,
rect.right - rect.left, rect.bottom - rect.top,
SWP_NOZORDER | SWP_NOACTIVATE);
}
Notice how the code is referencing m_hWndClient
. Since m_hWndClient
is a plain HWND
, it can be any window at all. There is no restriction on what kind of window it can be, unlike MFC where some features (like splitter windows) require a CView
-derived class. If you go back to CMainFrame::OnCreate()
, you'll see that it creates a view window and stores its handle in m_hWndClient
, which ensures that the view will be resized properly.
Back to the Clock Program
Now that we've seen some of the frame window class details, let's get back to our clock app. The view window can handle the timer and drawing, just as CMyWindow
did in the previous sample. Here's a partial class definition:
class CWTLClockView : public CWindowImpl<CWTLClockView>
{
public:
DECLARE_WND_CLASS(NULL)
BOOL PreTranslateMessage(MSG* pMsg);
BEGIN_MSG_MAP_EX(CWTLClockView)
MESSAGE_HANDLER(WM_PAINT, OnPaint)
MSG_WM_CREATE(OnCreate)
MSG_WM_DESTROY(OnDestroy)
MSG_WM_TIMER(OnTimer)
MSG_WM_ERASEBKGND(OnEraseBkgnd)
END_MSG_MAP()
};
Note that it's fine to mix the ATL message map macros with the WTL versions, as long as you change BEGIN_MSG_MAP
to BEGIN_MSG_MAP_EX
when necessary. OnPaint()
has all the drawing code that was in OnEraseBkgnd()
in the previous sample. Here's what the new window looks like:
The last thing we'll add to this app is UI updating. To demonstrate this, we'll add a new top-level menu item called Clock that has Start and Stop commands to stop and start the clock. The Start and Stop menu items will be enabled and disabled as appropriate.
UI Updating
Several things work together to provide idle-time UI updating: a CMessageLoop
object, the mix-in classes CIdleHandler
and CUpdateUI
that CMainFrame
inherits from, and the UPDATE_UI_MAP
in CMainFrame
. CUpdateUI
can operate on five different types of elements: top-level menu items (in the menu bar itself), menu items in popup menus, toolbar buttons, status bar panes, and child windows (such as dialog controls). Each type of element has a corresponding constant in CUpdateUIBase
:
- menu bar items:
UPDUI_MENUBAR
- popup menu items:
UPDUI_MENUPOPUP
- toolbar buttons:
UPDUI_TOOLBAR
- status bar panes:
UPDUI_STATUSBAR
- child windows:
UPDUI_CHILDWINDOW
CUpdateUI
can set the enabled state, checked state, and text of items (but of course not all items support all states; you can't put a check on an edit box). It can also set a popup menu item to the default state so the text appears in bold.
To hook up UI updating, we need to do four things:
- Derive the frame window class from
CUpdateUI
and CIdleHandler
- Chain messages from
CMainFrame
to CUpdateUI
- Add the frame window to the module's list of idle handlers
- Fill in the frame window's
UPDATE_UI_MAP
The AppWizard-generated code takes care of the first three parts for us, so all that's left is to decide which menu items we'll update, and when they will be enabled or disabled.
New menu items to control the clock
Let's add a new Clock menu to the menu bar, with two items: IDC_START
and IDC_STOP
:
Then we add an entry in the UPDATE_UI_MAP
for each menu item:
class CMainFrame : public ...
{
public:
BEGIN_UPDATE_UI_MAP(CMainFrame)
UPDATE_ELEMENT(IDC_START, UPDUI_MENUPOPUP)
UPDATE_ELEMENT(IDC_STOP, UPDUI_MENUPOPUP)
END_UPDATE_UI_MAP()
};
Then whenever we want to change the enabled state of either item, we call CUpdateUI::UIEnable()
. UIEnable()
takes the ID of the item, and a bool
that indicates the enabled state (true
for enabled, false
for disabled).
This system works differently from MFC's ON_UPDATE_COMMAND_UI
system. In MFC, we write update UI handlers for any UI elements that need to have their state updated. MFC then calls the handlers either at idle time, or when it is about to show a menu. In WTL, we call CUpdateUI
methods when the state of an item changes. CUpdateUI
keeps track of the UI elements and their states, and updates the elements at idle time, or before a menu is shown.
Calling UIEnable()
Let's go back to the OnCreate()
function and see how we set up the initial state of the Clock menu items.
LRESULT CMainFrame::OnCreate(UINT , WPARAM ,
LPARAM , BOOL& )
{
m_hWndClient = m_view.Create(...);
UIEnable ( IDC_START, false );
UIEnable ( IDC_STOP, true );
return 0;
}
And here's what the Clock menu looks like when the app is started:
CMainFrame
now needs handlers for our two new items. The handlers will toggle the states of the menu items, then call methods in the view class to start and stop the clock. This is one area where MFC's built-in message routing is sorely missed; if this were an MFC app, all the UI updating and command handling could be put entirely in the view class. However, in WTL, the frame and view classes have to communicate in some way; the menu is owned by the frame window, so the frame gets menu-related messages and is responsible for either acting on them or sending them to the view class.
The communication could be done through PreTranslateMessage()
, however the UIEnable()
calls still have to be done from CMainFrame
. CMainFrame
could get around this by passing its this
pointer to the view class, so the view could call UIEnable()
through that pointer. For this sample, I have chosen the solution that results in a tightly-coupled frame and view, since I find it easier to understand (and explain!).
class CMainFrame : public ...
{
public:
BEGIN_MSG_MAP_EX(CMainFrame)
COMMAND_ID_HANDLER_EX(IDC_START, OnStart)
COMMAND_ID_HANDLER_EX(IDC_STOP, OnStop)
END_MSG_MAP()
void OnStart(UINT uCode, int nID, HWND hwndCtrl);
void OnStop(UINT uCode, int nID, HWND hwndCtrl);
};
void CMainFrame::OnStart(UINT uCode, int nID, HWND hwndCtrl)
{
UIEnable ( IDC_START, false );
UIEnable ( IDC_STOP, true );
m_view.StartClock();
}
void CMainFrame::OnStop(UINT uCode, int nID, HWND hwndCtrl)
{
UIEnable ( IDC_START, true );
UIEnable ( IDC_STOP, false );
m_view.StopClock();
}
Each handler updates the Clock menu, then calls a method in the view, since the view is the class that controls the clock. The StartClock()
and StopClock()
methods are not shown here, but you can find them in the sample project.
One Last Note on Message Maps
If you're using VC 6, you might have noticed that when you change BEGIN_MSG_MAP
to BEGIN_MSG_MAP_EX
, ClassView gets a bit confused:
This happens because ClassView doesn't recognize BEGIN_MSG_MAP_EX
as something it should parse specially, and it thinks that all the WTL message map macros are actually functions. You can fix this by changing the macros back to BEGIN_MSG_MAP
and adding these lines at the end of stdafx.h:
#if (ATL_VER < 0x0700)
#undef BEGIN_MSG_MAP
#define BEGIN_MSG_MAP(x) BEGIN_MSG_MAP_EX(x)
#endif
Next Stop, 1995
We've only just begun to scratch the surface of WTL. In the next article, I'll bring our sample clock app up to 1995 UI standards and introduce toolbars and status bars. In the meantime, experiment a bit with the CUpdateUI
methods; for example, try calling UISetCheck()
instead of UIEnable()
to see the different ways the menu items can be changed.
Copyright and license
This article is copyrighted material, (c)2003-2005 by Michael Dunn. I realize this isn't going to stop people from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation, I would just like to be aware of the translation so I can post a link to it here.
The demo code that accompanies this article is released to the public domain. I release it this way so that the code can benefit everyone. (I don't make the article itself public domain because having the article available only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are benefiting from my code) but is not required. Attribution in your own source code is also appreciated but not required.
Revision History
- March 26, 2003: Article first published.
- December 15, 2005: Updated to cover ATL changes in VC 7.1.
Series Navigation: « Part I (ATL GUI Classes) | » Part III (Toolbars and Status Bars)