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

原作 :Michael Dunn [英文原文]
翻译 :Orbit(桔皮干了) [http://www.winmsg.com/cn/orbit.htm]

下载演示程序代码

本章内容

对第二部分的介绍

好了,现在正式开始介绍WTL!在这一部分我讲的内容包括生成一个基本的主窗口和WTL提供的一些友好的改进,比如UI界面的更新(如菜单上的选择标记)和更好的消息映射机制。为了更好地掌握本章的内容,你应该安装WTL并将WTL库的头文件目录添加到VC的搜索目录中,还要将WTL的应用程序生成向导复制到正确的位置。WTL的发布版本中有文档具体介绍如何做这些设置,如果遇到困难可以查看这些文档。

WTL 总体印象

WTL的类大致可以分为几种类型:

  1. 主框架窗口的实现- CFrameWindowImpl, CMDIFrameWindowImpl
  2. 控件的封装- CButton, CListViewCtrl
  3. GDI 对象的封装- CDC, CMenu
  4. 一些特殊的界面特性 - CSplitterWindow, CUpdateUI, CDialogResize, CCustomDraw
  5. 实用的工具类和宏- CString, CRect, BEGIN_MSG_MAP_EX

本篇文章将深入地介绍框架窗口类,还将简要地讲一下有关的界面特性类和工具类,这些界面特性类和工具类中绝大多数都是独立的类,尽管有一些是嵌入类,例如:CDialogResize。

开始写WTL程序

如果你没有用WTL的应用程序生成向导也没关系(我将在后面介绍这个向导的用法), WTL的程序的代码结构很像ATL的程序,本章使用的例子代码有别于第一章的例子,主要是为了显示WTL的特性,没有什么实用价值。

这一节我们将在WTL生成的代码基础上添加代码,生成一个新的程序,程序主窗口的客户区显示当前的时间。stdafx.h的代码如下:

#define STRICT
#define WIN32_LEAN_AND_MEAN
#define _WTL_USE_CSTRING
 
#include <atlbase.h>       // 基本的ATL类
#include <atlapp.h>        // 基本的WTL类
extern CAppModule _Module; // WTL 派生的CComModule版本
#include <atlwin.h>        // ATL 窗口类
#include <atlframe.h>      // WTL 主框架窗口类
#include <atlmisc.h>       // WTL 实用工具类,例如:CString
#include <atlcrack.h>      // WTL 增强的消息宏

atlapp.h 是你的工程中第一个包含的头文件,这个文件内定义了有关消息处理的类和CAppModule,CAppModule是从CComModule派生的类。如果你打算使用CString类,你需要手工定义_WTL_USE_CSTRING标号,因为CString类是在atlmisc.h中定义的,而许多包含在atlmisc.h之前的头文件都会用到CString,定义_WTL_USE_CSTRING之后,atlapp.h就会向前声明CString类,其他的头文件就知道CString类的存在,从而避免编译器为此大惊小怪。

接下来定义框架窗口。我们的SDI窗口是从CFrameWindowImpl派生的,在定义窗口类时使用DECLARE_FRAME_WND_CLASS代替前面使用的DECLARE_WND_CLASS。下面时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有两个参数,窗口类名(类名可以是NULL,ATL会替你生成一个类名)和资源ID,创建窗口时WTL用这个ID装载图标,菜单和加速键表。我们还要象CFrameWindowImpl中的消息处理(例如WM_SIZE和WM_DESTROY消息)那样将消息链入窗口的消息中。

现在来看看WinMain()函数,它和第一部分中的例子代码中的WinMain()函数几乎一样,只是创建窗口部分的代码略微不同。

// 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中的CreateEx()函数的参数使用了常用的默认值,所以我们不需要特别指定任何参数。正如前面介绍的,CFrameWindowImpl会处理资源的装载,你只需要使用IDR_MAINFRAME作为ID定义你的资源就行了(译者注:主要是图标,菜单和加速键表),你也可以直接使用本章的例子代码。

如果你现在就运行程序,你会看到主框架窗口,事实上它没有做任何事情。我们需要手工添加一些消息处理,所以现在是介绍WTL的消息映射宏的最佳时间。

WTL 对消息映射的增强

将Win32 API通过消息传递过来的WPARAM和LPARAM数据还原出来是一件麻烦的事情并且很容易出错,不幸得是ATL并没有为我们提供更多的帮助,我们仍然需要从消息中还原这些数据,当然WM_COMMAND和WM_NOTIFY消息除外。但是WTL的出现拯救了这一切!

WTL的增强消息映射宏定义在atlcrack.h中。(这个名字来源于“消息解密者”,是一个与windowsx.h的宏所使用的相同术语)首先将BEGIN_MSG_MAP改为BEGIN_MSG_MAP_EX,带_EX的版本产生“解密”消息的代码。

class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
    BEGIN_MSG_MAP_EX(CMyWindow)
        CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
    END_MSG_MAP()
};

对于我们的时钟程序,我们需要处理WM_CREATE消息来设置定时器,WTL的消息处理使用MSG_作为前缀,后面是消息名称,例如MSG_WM_CREATE。这些宏只是代表消息响应处理的名称,现在我们来添加对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的消息响应处理看起来有点象MFC,每一个处理函数根据消息传递的参数不同也有不同的原型。由于我们没有向导自动添加消息响应,所以我们需要自己查找正确的消息处理函数。幸运的是VC可以帮我们的忙,将鼠标光标移到“MSG_WM_CREATE”宏的文字上按F12键就可以来到这个宏的定义代码处。如果是第一次使用这个功能,VC会要求从新编译全部文件以建立浏览信息数据库(browse info database),建立了这个数据库之后,VC会打开atlcrack.h并将代码定位到MSG_WM_CREATE的定义位置:

#define MSG_WM_CREATE(func) \
    if (uMsg == WM_CREATE) \
    { \
        SetMsgHandled(TRUE); \
        lResult = (LRESULT)func((LPCREATESTRUCT)lParam); \
        if(IsMsgHandled()) \
            return TRUE; \
    }

标记为红色的那一行非常重要,就是在这里调用实际的消息响应函数,他告诉我们消息响应函数有一个LPCREATESTRUCT类型的参数,返回值的类型是LRESULT。请注意这里没有ATL的宏所用的 bHandled 参数,SetMsgHandled()函数代替了这个参数,我会对此作些简要的介绍。

现在为我们的窗口类添加OnCreate()响应函数:

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 是直接从CWindow类派生的, 所以它继承了所有CWindow类的方法,如SetTimer()。这使得对窗口API的调用有点象MFC的代码,只是MFC使用CWnd类包装这些API。

我们使用SetTimer()函数创建一个定时器,它每隔一秒钟(1000毫秒)触发一次。由于我们需要让CFrameWindowImpl也处理WM_CREATE消息,所以我们调用SetMsgHandled(false),让消息通过CHAIN_MSG_MAP宏链入基类,这个调用代替了ATL宏使用的bHandled参数。(即使CFrameWindowImpl类不需要处理WM_CREATE消息,调用SetMsgHandled(false)让消息流入基类是个好的习惯,因为这样我们就不必总是记着哪个消息需要基类处理那些消息不需要基类处理,这和VC的类向导产生的代码相似,多数的派生类的消息处理函数的开始或结尾都会调用基类的消息处理函数)

为了能够停止定时器我们还需要响应WM_DESTROY消息,添加消息响应的过程和前面一样,MSG_WM_DESTROY宏的定义是这样的:

#define MSG_WM_DESTROY(func) \
    if (uMsg == WM_DESTROY) \
    { \
        SetMsgHandled(TRUE); \
        func(); \
        lResult = 0; \
        if(IsMsgHandled()) \
            return TRUE; \
    }

OnDestroy()函数没有参数也没有返回值,CFrameWindowImpl也要处理WM_DESTROY消息,所以还要调用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); } };

接下来是响应WM_TIMER消息的处理函数,它每秒钟被调用一次。你应该知道怎样使用F12键的窍门了,所以我直接给出响应函数的代码:

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();
    }
};

这个响应函数只是在每次定时器触发时重画窗口的客户区。最后我们要响应WM_ERASEBKGND消息,在窗口客户区的左上角显示当前的时间。

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)
    }
};

这个消息处理函数不仅使用了CRect和CString类,还使用了一个GDI包装类CDCHandle。对于CString类我想说的是它等同与MFC的CString类,我在后面的文章中还会介绍这些包装类,现在你只需要知道CDCHandle是对HDC的简单封装就行了,使用方法与MFC的CDC类相似,只是CDCHandle的实例在超出作用域后不会销毁它所操作的设备上下文。

所有的工作完成了,现在看看我们的窗口是什么样子:

 [clock window - 4K]

例子代码中还使用了WM_COMMAND响应菜单消息,在这里我不作介绍,但是你可以查看例子代码,看看WTL的COMMAND_ID_HANDLER_EX宏是如何工作的。

WTL的应用程序生成向导能得到什么

WTL的发布版本附带一个很棒的应用程序生成向导,让我们以一个SDI 应用为例看看它有什么特性。

使用向导的整个过程

在VC的IDE环境下单击File|New菜单,从列表中选择ATL/WTL AppWizard,我们要重写时钟程序,所以用WTLClock作为项目的名字:

 [AppWiz screen 1 - 14K]

在下一页你可以选择项目的类型,SDI,MDI或者是基于对话框的应用,当然还有其它选项,如下图所示设置这些选项,然后点击“下一步”:

 [AppWiz screen 2 - 22K]

在最后一页你可以选择是否使用toolbar,rebar和status bar,为了简单起见,取消这些选项并单击“结束”。

 [AppWiz screen 3 - 21K]

查看生成的代码

向导完成后,在生成的代码中有三个类:CMainFrame, CAboutDlg, 和CWTLClockView,从名字上就可以猜出这些类的作用。虽然也有一个是视图类,但它仅仅是从CWindowImpl派生出来的一个简单的窗口类,没有象MFC那样的文档/视图结构。

还有一个_tWinMain()函数,它先初始化COM环境,公用控件和_Module,然后调用全局函数Run()。Run()函数创建主窗口并开始消息循环,Run()调用CMessageLoop::Run(),消息泵实际上是位于CMessageLoop::Run()内,我将在下一个章节介绍CMessageLoop的更多细节。

CAboutDlg是CDialogImpl的派生类,它对应于ID IDD_ABOUTBOX资源,我在第一部分已经介绍过对话框,所以你应该能看懂CAboutDlg的代码。

CWTLClockView是我们的程序的视图类,它的作用和MFC的视图类一样,没有标题栏,覆盖整个主窗口的客户区。CWTLClockView类有一个PreTranslateMessage()函数,也和MFC中的同名函数作用相同,还有一个WM_PAINT的消息响应函数。这两个函数都没有什么特别之处,只是我们会填写OnPaint()函数来显示时间。

最后是我们的CMainFrame类,它有许多有趣的新东西,这是这个类的定义缩略版本:

class CMainFrame : public CFrameWindowImpl<CMainFrame>,
                   public CUpdateUI<CMainFrame>,
                   public CMessageFilter,
                   public CIdleHandler
{
public:
    DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME)

    CWTLClockView m_view;

    virtual BOOL PreTranslateMessage(MSG* pMsg);
    virtual BOOL OnIdle();

    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()
};

CMessageFilter是一个嵌入类,它提供PreTranslateMessage()函数,CIdleHandler也是一个嵌入类,它提供了OnIdle()函数。CMessageLoop, CIdleHandler 和 CUpdateUI三个类互相协同完成界面元素的状态更新(UI update),就像MFC中的ON_UPDATE_COMMAND_UI宏一样。

CMainFrame::OnCreate()中创建了视图窗口并保存这个窗口的句柄,当主窗口改变大小时视图窗口的大小也会随之改变。OnCreate()函数还将CMainFrame对象添加到由CAppModule维持的消息过滤器队列和空闲处理队列,我将在稍后介绍这些。

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是CFrameWindowImpl对象的一个成员变量,当主窗口大小改变时此窗口的大小也将改变。

在生成的CMainFrame中还添加了对File|New, File|Exit, 和 Help|About菜单消息的处理。我们的时钟程序不需要这些默认的菜单项,但是现在将它们留在代码中也没有害处。现在可以编译并运行向导生成的代码,不过这个程序确实没有什么用处。如果你感兴趣的话可以深入CMainFrame::CreateEx()函数的内部看看主窗口和它的资源是如何被加载和创建得。

我们的下一步WTL之旅是CMessageLoop,它掌管消息泵和空闲处理。

CMessageLoop 的内部实现

CMessageLoop为我们的应用程序提供一个消息泵,除了一个标准的DispatchMessage/TranslateMessage循环外,它还通过调用PreTranslateMessage()函数实现了消息过滤机制,通过调用OnIdle()实现了空闲处理功能。下面是Run()函数的伪代码:

int Run()
{
MSG msg;
 
    for(;;)
        {
        while ( !PeekMessage(&msg) )
            DoIdleProcessing();
 
        if ( 0 == GetMessage(&msg) )
            break;    // WM_QUIT retrieved from the queue
 
        if ( !PreTranslateMessage(&msg) )
            {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
            }
        }
 
    return msg.wParam;
}

那些需要过滤消息的类只需要象CMainFrame::OnCreate()函数那样调用CMessageLoop::AddMessageFilter()函数就行了,CMessageLoop就会知道该调用那个PreTranslateMessage()函数,同样,如果需要空闲处理就调用CMessageLoop::AddIdleHandler()函数。

需要注意得是在这个消息循环中没有调用TranslateAccelerator() 或 IsDialogMessage() 函数,因为CFrameWindowImpl在这之前已经做了处理,但是如果你在程序中使用了非模式对话框,那你就需要在CMainFrame::PreTranslateMessage()函数中添加对IsDialogMessage()函数的调用。

CFrameWindowImpl 的内部实现

CFrameWindowImpl 和它的基类 CFrameWindowImplBase提供了对toolbars,rebars, status bars,工具条按钮的工具提示和菜单项的掠过式帮助,这些也是MFC的CFrameWnd类的基本特征。我会逐步介绍这些特征,完整的讨论CFrameWindowImpl类需要再写两篇文章,但是现在看看CFrameWindowImpl是如何处理WM_SIZE和它的客户区就足够了。需要记住一点前面提到的东西,m_hWndClient是CFrameWindowImplBase类的成员变量,它存储主窗口内的“视图”窗口的句柄。

CFrameWindowImpl类处理了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;
}

它首先检查窗口是否最小化,如果不是就调用UpdateLayout(),下面是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);
}

注意这些代码是如何使用m_hWndClient得,既然m_hWndClient是一般窗口的句柄,它就可能是任何窗口,对这个窗口的类型没有限制。这一点不像MFC,MFC在很多情况下需要CView的派生类(例如分隔窗口类)。如果你回过头看看CMainFrame::OnCreate()就会看到它创建了一个视图窗口并赋值给m_hWndClient,由m_hWndClient确保视图窗口被设置为正确的大小。

回到前面的时钟程序

现在我们已经看到了主窗口类的一些细节,现在回到我们的时钟程序。视图窗口用来响应定时器消息并负责显示时钟,就像前面的CMyWindow类。下面是这个类的部分定义:

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()
};

使用BEGIN_MSG_MAP_EX代替BEGIN_MSG_MAP后,ATL的消息映射宏可以和WTL的宏混合使用,前面的例子在OnEraseBkgnd()中显示(画)时钟,现在被被搬到了OnPaint()中。新窗口看起来是这个样子的:

 [Clock app w/view window - 3K]

最后为我们的程序添加UI updating功能,为了演示这些用法,我们为窗口添加Start菜单和Stop菜单用于开始和停止时钟,Start菜单和Stop菜单将被适当的设置为可用和不可用。

界面元素的自动更新(UI Updating)

空闲时间的界面更新是几件事情协同工作的结果: CMessageLoop对象,嵌入类CIdleHandler 和 CUpdateUI,CMainFrame类继承了这两个嵌入类,当然还有CMainFrame类中的UPDATE_UI_MAP宏。CUpdateUI能够操作5种不同的界面元素:顶级菜单项(就是菜单条本身),弹出式菜单的菜单项,工具条按钮,状态条的格子和子窗口(如对话框中的控件)。每一种界面元素都对应CUpdateUIBase类的一个常量:

CUpdateUI可以设置enabled状态,checked状态和文本(当然不是所有的界面元素都支持所有状态,如果一个子窗口是编辑框它就不能被check)。菜单项可以被设置为默认状态,这样它的文字会被加重显示。

要使用UI updating需要做四件事:

  1. 主窗口需要继承CUpdateUI 和 CIdleHandler
  2. 将 CMainFrame 的消息链入 CUpdateUI
  3. 将主窗口添加到模块的空闲处理队列
  4. 在主窗口中添加 UPDATE_UI_MAP 宏

向导生成的代码已经为我们做了三件事,现在我们只需要决定那个菜单项需要更新和他们是么时候可用什么时候不可用。

添加控制时钟的新菜单项

在菜单条添加一个Clock菜单,它有两个菜单项:IDC_START and IDC_STOP:

 [Clock menu - 2K]

然后在UPDATE_UI_MAP宏中为每个菜单项添加一个入口:

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()
    // ...
};

我们只需要调用CUpdateUI::UIEnable()就可以改变这两个菜单项的任意一个的使能状态时。UIEnable()有两个参数,一个是界面元素的ID,另一个是标志界面元素是否可用的bool型变量(true表示可用,false表示不可用)。


这套体系比MFC的ON_UPDATE_COMMAND_UI体系笨拙一些,在MFC中我们只需编写处理函数,由MFC选择界面元素的显示状态,在WTL中我们需要告诉WTL界面元素的状态在何时改变。当然,这两个库都是在菜单将要显示的时候才应用菜单状态的改变。

调用 UIEnable()

现在返回到OnCreate()函数看看是如何设置Clock菜单的初始状态。

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;
}

我们的程序开始时Clock菜单是这样的:

 [Start item disabled - 4K]

CMainFrame现在需要处理两个新菜单项,在视图类调用它们开始和停止时钟时处理函数需要翻转这两个菜单项的状态。这是MFC的内建消息处理无法想象的地方之一。在MFC的程序中,所有的界面更新和命令消息处理必须完整的放在视图类中,但是在WTL中,主窗口类和视图类通过某种方式沟通;菜单由主窗口拥有,主窗口获得这些菜单消息并做相应的处理,要么响应这些消息,要么发送给视图类。

这种沟通是通过PreTranslateMessage()完成的,当然CMainFrame仍然要调用UIEnable()。CMainFrame可以将this指针传递给视图类,这样视图类也可以通过这个指针调用UIEnable()。在这个例子中我选择的这种解决方案导致主窗口和视图成为紧密耦合体,但是我发现这很容易理解(和解释!)。

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();
}

每个处理函数都更新Clock菜单,然后在视图类中调用一个方法,选择在视图类中使用是因为时钟是由视图类控制得。StartClock() 和 StopClock()得代码没有列出,但可以在这个工程得例子代码中找到它们。

消息映射链中最后需要注意的地方

如果你使用VC 6,你会注意到将BEGIN_MSG_MAP改为BEGIN_MSG_MAP_EX后ClassView显得有些杂乱无章:

 [Messed-up ClassView - 6K]

出现这种情况是因为ClassView不能解释BEGIN_MSG_MAP_EX宏,它以为所有得WTL消息映射宏是函数定义。你可以将宏改回为BEGIN_MSG_MAP并在stdafx.h文件得结尾处添加这两行代码来解决这个问题:

#undef BEGIN_MSG_MAP
#define BEGIN_MSG_MAP(x) BEGIN_MSG_MAP_EX(x)

下一站, 1995

我们现在只是掀起了WTL的一角,在下一篇文章我会为我们的时钟程序添加一些Windows 95的界面标准,比如工具条和状态条,同时体验一下CUpdateUI的新东西。例如试着用UISetCheck()代替UIEnable(),看看菜单项会有什么变化。

修改记录

2003年3月26日,本文第一次发表。