Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WTL for MFC Programmers, Part II - WTL GUI Base Classes

0.00/5 (No votes)
22 Dec 2005 1  
WTL programming for MFC developers - frame windows.

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:

  1. Frame window implementation - CFrameWindowImpl, CMDIFrameWindowImpl
  2. Control wrappers - CButton, CListViewCtrl
  3. GDI wrappers - CDC, CMenu
  4. Special UI features - CSplitterWindow, CUpdateUI, CDialogResize, CCustomDraw
  5. 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>       // base ATL classes
#include <atlapp.h>        // base WTL classes
extern CAppModule _Module; // WTL version of CComModule
#include <atlwin.h>        // ATL GUI classes
#include <atlframe.h>      // WTL frame window classes
#include <atlmisc.h>       // WTL utility classes like CString
#include <atlcrack.h>      // WTL enhanced msg map macros

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:

// 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.

// main.cpp:
#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;
 
    // Create the main window
    if ( NULL == wndMain.CreateEx() )
        return 1;       // Window creation failed
 
    // Show the window
    wndMain.ShowWindow ( nCmdShow );
    wndMain.UpdateWindow();
 
    // Standard Win32 message loop
    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:

// MyWindow.h, VC6 only:
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()
 
    // OnCreate(...) ?
};

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;
 
        // Get our window's client area.
        GetClientRect ( rc );
 
        // Build the string to show in the window.
        GetLocalTime ( &st );
        sTime.Format ( _T("The time is %d:%02d:%02d"), 
                       st.wHour, st.wMinute, st.wSecond );
 
        // Set up the DC and draw the text.
        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 );
 
        // Restore the DC.
        dc.RestoreDC(-1);
        return 1;    // We erased the background (ExtTextOut did it)
    }
};

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:

 [clock window - 4K]

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:

 [AppWiz screen 1 - 14K]

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:

 [AppWiz screen 2 - 22K]

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.

 [AppWiz screen 3 - 21K]

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:

 [VC7 AppWiz screen 1 - 29K]

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:

 [VC7 AppWiz screen 2 - 25K]

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.

 [VC7 AppWiz screen 3 - 23K]

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 /*uMsg*/, WPARAM /*wParam*/, 
                             LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
    m_hWndClient = m_view.Create(m_hWnd, rcDefault, NULL, |
                                 WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS |
                                   WS_CLIPCHILDREN, WS_EX_CLIENTEDGE);
 
    // register object for message filtering and idle updates
    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;    // WM_QUIT retrieved from the queue
 
        if ( !CallPreTranslateMessageFilters(&msg) )
            {
            // if we get here, message was not filtered out
            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 /*uMsg*/, WPARAM wParam, LPARAM /*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);
 
    // position bars and offset their dimensions
    UpdateBarsPosition(rect, bResizeBars);
 
    // resize client window
    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:

 [Clock app w/view window - 3K]

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:

  1. Derive the frame window class from CUpdateUI and CIdleHandler
  2. Chain messages from CMainFrame to CUpdateUI
  3. Add the frame window to the module's list of idle handlers
  4. 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:

 [Clock menu - 2K]

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 /*uMsg*/, WPARAM /*wParam*/, 
                             LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
    m_hWndClient = m_view.Create(...);
 
    // register object for message filtering and idle updates
    // [omitted for clarity]
 
    // Set the initial state of the Clock menu items:
    UIEnable ( IDC_START, false );
    UIEnable ( IDC_STOP, true );
 
    return 0;
}

And here's what the Clock menu looks like when the app is started:

 [Start item disabled - 4K]

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)
{
    // Enable Stop and disable Start
    UIEnable ( IDC_START, false );
    UIEnable ( IDC_STOP, true );
 
    // Tell the view to start its clock.
    m_view.StartClock();
}
 
void CMainFrame::OnStop(UINT uCode, int nID, HWND hwndCtrl)
{
    // Enable Start and disable Stop
    UIEnable ( IDC_START, true );
    UIEnable ( IDC_STOP, false );
 
    // Tell the view to stop its clock.
    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:

 [Messed-up ClassView - 6K]

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)

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here