65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (157投票s)

2003年3月27日

CPOL

18分钟阅读

viewsIcon

656851

downloadIcon

6105

WTL for MFC Developers - Frame Windows.

目录

第二部分简介

好了,是时候真正开始谈论WTL了!在这一部分,我将介绍编写主框架窗口的基础知识,并介绍WTL提供的一些令人欢迎的改进,例如UI更新和更好的消息映射。为了最大限度地利用这一部分,您应该安装WTL,以便头文件在VC的搜索路径中,并且AppWizard位于适当的目录中。WTL发行版附带有关如何安装AppWizard的说明,请查阅文档。

请记住,如果您在安装WTL或编译演示代码时遇到任何问题,请在在此处提问之前,阅读第一部分的README部分

WTL概述

WTL类可以分为几个主要类别

  1. 框架窗口实现 - CFrameWindowImpl, CMDIFrameWindowImpl
  2. 控件包装器 - CButton, CListViewCtrl
  3. GDI包装器 - CDC, CMenu
  4. 特殊UI功能 - CSplitterWindow, CUpdateUI, CDialogResize, CCustomDraw
  5. 实用类和宏 - CString, CRect, BEGIN_MSG_MAP_EX

本文将深入探讨框架窗口,并触及一些UI功能和实用类。大多数类都是独立的类,尽管像CDialogResize这样的少数类是混合类。

创建WTL EXE

如果您不使用WTL AppWizard(稍后会介绍),那么WTL EXE的起始方式与ATL EXE非常相似。本文中的示例代码将是第一个部分中的另一个框架窗口,但会稍微复杂一些,以便展示一些WTL功能。

在本节中,我们将从头开始创建一个新的EXE。主窗口将在其客户端区域显示当前时间。这是一个基本的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是您包含的第一个WTL头文件。它包含消息处理类和CAppModule,一个派生自CComModule的类。如果您打算使用CString,还应该定义_WTL_USE_CSTRING,因为CString定义在atlmisc.h中,但还有其他在atlmisc.h之前的头文件具有使用CString的功能。定义_WTL_USE_CSTRING使atlapp.h前向声明CString类,以便其他头文件知道CString是什么。

(请注意,我们需要一个全局的CAppModule变量,尽管在第一部分中这并非必需。CAppModule有一些与空闲处理和UI更新相关的我们需要的特性,所以我们需要CAppModule存在。)

接下来定义我们的框架窗口。SDI窗口(如我们的)派生自CFrameWindowImpl。窗口类使用DECLARE_FRAME_WND_CLASS而不是DECLARE_WND_CLASS来定义。这是我们窗口定义在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接受两个参数,窗口类名(可以是NULL,让ATL为您生成一个名称),以及一个资源ID。WTL将查找具有该ID的图标、菜单和加速器表,并在创建窗口时加载它们。它还将查找具有该ID的字符串,并将其用作窗口标题。我们还将消息传递给CFrameWindowImpl,因为它本身包含一些消息处理程序(最值得注意的是WM_SIZEWM_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时,一个繁琐且容易出错的操作是从随消息发送的WPARAMLPARAM数据中解包参数。不幸的是,ATL在这方面帮助不大,除了WM_COMMANDWM_NOTIFY之外,我们仍然需要从所有消息中解包数据。但WTL在这里挺身而出!

WTL增强的消息映射宏在atlcrack.h中。(该名称来自“消息解包器”,这是windowsx.h中类似宏的术语。)使用这些宏的初始步骤在VC 6和VC 7上有所不同,atlcrack.h中的这个注释对此进行了说明

对于ATL 3.0,使用解包处理程序的消息映射必须使用BEGIN_MSG_MAP_EX

对于ATL 7.0/7.1,您可以将BEGIN_MSG_MAP用于CWindowImpl/CDialogImpl派生类,但必须为不派生自CWindowImpl/CDialogImpl的类使用BEGIN_MSG_MAP_EX

因此,如果您正在使用VC 6,您将在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()
};

(VC 6中需要_EX宏,因为它包含消息处理宏使用的某些代码。为了简洁起见,我在此处不展示头文件的VC 6和VC 7版本,因为它们只在那个宏上有所不同。请记住,VC 7中不需要_EX宏。)

对于我们的时钟程序,我们需要处理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 6中,VC将首先重新构建项目以构建其浏览信息数据库。完成后,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; \
    }

带下划线的代码是重要部分,它是对处理程序的实际调用,它告诉我们处理程序返回LRESULT并接受一个参数LPCREATESTRUCT。请注意,不像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代码,其中您使用封装API的各种CWnd方法。

我们调用SetTimer()来创建一个每秒(1000毫秒)触发一次的计时器。由于我们想让CFrameWindowImpl也处理WM_CREATE,我们调用SetMsgHandled(false),以便消息通过CHAIN_MSG_MAP宏传递给基类。此调用取代了ATL宏使用的bHandled参数。(尽管CFrameWindowImpl不处理WM_CREATE,但养成在使用基类时调用SetMsgHandled(false)的习惯是一个好习惯,这样您就不必记住基类处理了哪些消息。这与ClassWizard生成的代码类似;大多数处理程序以调用基类处理程序开始或结束。)

我们还需要一个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)
    }
};

此处理程序演示了GDI包装器CDCHandle之一,以及CRectCString。关于CString我只需要说的是,它与MFC的CString完全相同。我稍后会介绍包装器类,但现在您可以将CDCHandle视为HDC的简单包装器,类似于MFC的CDC,尽管当CDCHandle超出作用域时,它不会销毁底层设备上下文。

所以,经过这一切,我们的窗口看起来是这样的

 [clock window - 4K]

示例代码还包含菜单项的WM_COMMAND处理程序;我不会在这里讨论它们,但您可以查看示例项目并看到WTL宏COMMAND_ID_HANDLER_EX的实际应用。

如果您正在使用VC 7.1,请查看Sergey Solozhentsev的WTL Helper插件,它将为您完成添加消息映射宏的繁重工作。

WTL AppWizard提供了什么

WTL发行版附带一个非常好的AppWizard,所以让我们看看它为SDI应用程序提供了哪些功能。

通过向导 (VC 6)

在VC中点击File|New,然后从列表中选择ATL/WTL AppWizard。我们将重写时钟应用程序,所以输入WTLClock作为项目名称

 [AppWiz screen 1 - 14K]

下一页是选择SDI、MDI或基于对话框的应用程序,以及其他一些选项。选择图中所示的选项,然后点击Next

 [AppWiz screen 2 - 22K]

最后一页是我们选择是否需要工具栏、rebar和状态栏。为了保持应用程序简单,取消选中所有这些选项,然后点击Finish

 [AppWiz screen 3 - 21K]

通过向导 (VC 7)

在VC中点击File|New|Project,然后从列表中选择ATL/WTL AppWizard。我们将重写时钟应用程序,所以输入WTLClock作为项目名称,然后点击OK

 [VC7 AppWiz screen 1 - 29K]

当AppWizard屏幕出现时,点击Application Type。此页面是选择SDI、MDI或基于对话框的应用程序,以及其他一些选项。选择图中所示的选项,然后点击User Interface Features

 [VC7 AppWiz screen 2 - 25K]

最后一页是我们选择是否需要工具栏、rebar和状态栏。为了保持应用程序简单,取消选中所有这些选项,然后点击Finish

 [VC7 AppWiz screen 3 - 23K]

检查生成的代码

完成向导后,您将在生成的代码中看到三个类:CMainFrameCAboutDlgCWTLClockView。从名称上,您可以猜到每个类的作用。虽然有一个“view”类,但它只是一个“纯粹”的窗口,派生自CWindowImpl;它没有任何像MFC的文档/视图架构那样的框架。

还有一个_tWinMain(),它初始化COM、公共控件和_Module,然后调用全局的Run()函数。Run()负责创建主窗口并启动消息循环。它还使用了一个新类CMessageLoopRun()调用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)
 
    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是一个混合类,提供PreTranslateMessage(),而CIdleHandler是另一个混合类,提供OnIdle()CMessageLoopCIdleHandlerCUpdateUI协同工作,提供类似于MFC中的ON_UPDATE_COMMAND_UI的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_hWndClientCFrameWindowImpl的一个成员;这是当框架被调整大小时将被调整大小的窗口。

生成的CMainFrame还包含File|NewFile|ExitHelp|About的处理程序。我们的时钟程序不需要大多数默认菜单项,但暂时保留它们不会有坏处。您现在可以构建并运行向导生成的代码,尽管该应用程序目前还不是很实用。您可能对逐步执行全局Run()中的CMainFrame::CreateEx()调用感兴趣,以确切了解框架窗口及其资源是如何加载和创建的。

我们在WTL之旅的下一站是CMessageLoop,它负责消息循环和空闲处理。

CMessageLoop 内部机制

CMessageLoop为我们的应用程序提供了一个消息循环。除了标准的TranslateMessage/DispatchMessage循环之外,它还通过PreTranslateMessage()提供消息过滤,并通过OnIdle()提供空闲处理。这是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知道要调用哪些PreTranslateMessage()函数,因为需要过滤消息的对象会调用CMessageLoop::AddMessageFilter(),就像CMainFrame::OnCreate()所做的那样。同样,需要执行空闲处理的对象会调用CMessageLoop::AddIdleHandler()

请注意,消息循环中没有调用TranslateAccelerator()IsDialogMessage()。前者由CFrameWindowImpl处理,但如果您向应用程序添加任何无模式对话框,您需要在CMainFrame::PreTranslateMessage()中添加一个IsDialogMessage()调用。

CFrameWindowImpl 内部机制

CFrameWindowImpl及其基类CFrameWindowImplBase提供了MFC中的CFrameWnd所具备的许多特性:工具栏、rebar、状态栏、工具栏按钮的工具提示以及菜单项的即时帮助。我将逐渐介绍所有这些特性,因为讨论整个CFrameWindowImpl类可能需要两整篇文章!目前,看到CFrameWindowImpl如何处理WM_SIZE及其客户端区域就足够了。在本次讨论中,请记住m_hWndClientCFrameWindowImplBase的一个成员,它保存了框架内“视图”窗口的HWND

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是一个普通的HWND,它可以是任何窗口。与MFC中某些特性(如分割窗口)需要CView派生类不同,这里对窗口类型没有限制。如果您回到CMainFrame::OnCreate(),您会看到它创建了一个视图窗口并将其句柄存储在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()
};

请注意,混合使用ATL消息映射宏和WTL版本是可以的,只要在必要时将BEGIN_MSG_MAP更改为BEGIN_MSG_MAP_EXOnPaint()包含前一个示例中OnEraseBkgnd()中的所有绘图代码。这是新窗口的样子

 [Clock app w/view window - 3K]

我们要添加到此应用程序的最后一项是UI更新。为了演示这一点,我们将添加一个新的顶级菜单项,名为Clock,其中包含StartStop命令,用于停止和启动时钟。StartStop菜单项将根据需要启用和禁用。

UI更新

有几个组件协同工作来提供空闲时间UI更新:一个CMessageLoop对象,CMainFrame继承的混合类CIdleHandlerCUpdateUI,以及CMainFrame中的UPDATE_UI_MAPCUpdateUI可以操作五种不同类型的元素:顶级菜单项(在菜单栏本身中)、弹出菜单中的菜单项、工具栏按钮、状态栏窗格和子窗口(如对话框控件)。每种类型的元素在CUpdateUIBase中都有一个对应的常量

  • 菜单栏项:UPDUI_MENUBAR
  • 弹出菜单项:UPDUI_MENUPOPUP
  • 工具栏按钮:UPDUI_TOOLBAR
  • 状态栏窗格:UPDUI_STATUSBAR
  • 子窗口:UPDUI_CHILDWINDOW

CUpdateUI可以设置项的启用状态、选中状态和文本(当然,并非所有项都支持所有状态;您不能在编辑框上打勾)。它还可以将弹出菜单项设置为默认状态,以便文本以粗体显示。

要连接UI更新,我们需要做四件事

  1. 将框架窗口类从CUpdateUICIdleHandler派生
  2. 将消息从CMainFrame传递到CUpdateUI
  3. 将框架窗口添加到模块的空闲处理程序列表中
  4. 填充框架窗口的UPDATE_UI_MAP

AppWizard生成的代码为我们处理了前三个部分,所以我们只需要决定要更新哪些菜单项,以及何时启用或禁用它们。

用于控制时钟的新菜单项

让我们在菜单栏中添加一个新的Clock菜单,其中包含两个项目:IDC_STARTIDC_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中,我们为任何需要更新其状态的UI元素编写UI更新处理程序。MFC随后在空闲时或即将显示菜单时调用处理程序。在WTL中,我们在项的状态改变时调用CUpdateUI方法。CUpdateUI跟踪UI元素及其状态,并在空闲时或菜单显示前更新元素。

调用 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应用程序,所有的UI更新和命令处理都可以完全放在视图类中。然而,在WTL中,框架和视图类必须以某种方式通信;菜单由框架窗口拥有,所以框架接收菜单相关的消息,并负责对其进行处理或将其发送给视图类。

通信可以通过PreTranslateMessage()完成,但是UIEnable()调用仍然必须从CMainFrame执行。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末尾添加以下行来解决此问题

#if (ATL_VER < 0x0700)
#undef BEGIN_MSG_MAP
#define BEGIN_MSG_MAP(x) BEGIN_MSG_MAP_EX(x)
#endif

下一站,1995

我们才刚刚开始接触WTL。在下一篇文章中,我将把我们的示例时钟应用程序升级到1995年的UI标准,并介绍工具栏和状态栏。在此期间,您可以尝试一下CUpdateUI方法;例如,尝试调用UISetCheck()而不是UIEnable(),看看菜单项可以如何改变。

版权和许可

本文是版权材料,(c)2003-2005 Michael Dunn。我知道这阻止不了人们在网上到处复制它,但我还是得这么说。如果您有兴趣翻译本文,请给我发邮件告知。我不认为会拒绝任何人翻译的许可,我只是想知道翻译的情况,以便在此处发布链接。

本文附带的演示代码已发布到公共领域。我以这种方式发布它,以便代码能使所有人受益。(我不公开本文,因为只有在CodeProject上才能找到本文有助于我自身的知名度以及CodeProject网站。)如果您在自己的应用程序中使用演示代码,写邮件告知我将不胜感激(只是为了满足我对人们是否从我的代码中受益的好奇心),但并非必需。在您自己的源代码中注明出处也同样受欢迎,但并非必需。

修订历史

  • 2003年3月26日:文章首次发布。
  • 2005年12月15日:更新以涵盖VC 7.1中的ATL更改。

系列导航:« 第一部分 (ATL GUI Classes) | » 第三部分 (Toolbars and Status Bars)

WTL for MFC Programmers, Part II - WTL GUI Base Classes - CodeProject - 代码之家
© . All rights reserved.