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






4.97/5 (157投票s)
WTL for MFC Developers - Frame Windows.
目录
- 第二部分简介
- WTL概述
- 创建WTL EXE
- WTL消息映射增强
- WTL AppWizard提供了什么
- CMessageLoop 内部机制
- CFrameWindowImpl 内部机制
- 回到时钟程序
- UI更新
- 关于消息映射的最后一点说明
- 下一站,1995
- 版权和许可
- 修订历史
第二部分简介
好了,是时候真正开始谈论WTL了!在这一部分,我将介绍编写主框架窗口的基础知识,并介绍WTL提供的一些令人欢迎的改进,例如UI更新和更好的消息映射。为了最大限度地利用这一部分,您应该安装WTL,以便头文件在VC的搜索路径中,并且AppWizard位于适当的目录中。WTL发行版附带有关如何安装AppWizard的说明,请查阅文档。
请记住,如果您在安装WTL或编译演示代码时遇到任何问题,请在在此处提问之前,阅读第一部分的README部分。
WTL概述
WTL类可以分为几个主要类别
- 框架窗口实现 -
CFrameWindowImpl
,CMDIFrameWindowImpl
- 控件包装器 -
CButton
,CListViewCtrl
- GDI包装器 -
CDC
,CMenu
- 特殊UI功能 -
CSplitterWindow
,CUpdateUI
,CDialogResize
,CCustomDraw
- 实用类和宏 -
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_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中类似宏的术语。)使用这些宏的初始步骤在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
之一,以及CRect
和CString
。关于CString
我只需要说的是,它与MFC的CString
完全相同。我稍后会介绍包装器类,但现在您可以将CDCHandle
视为HDC
的简单包装器,类似于MFC的CDC
,尽管当CDCHandle
超出作用域时,它不会销毁底层设备上下文。
所以,经过这一切,我们的窗口看起来是这样的
示例代码还包含菜单项的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作为项目名称
下一页是选择SDI、MDI或基于对话框的应用程序,以及其他一些选项。选择图中所示的选项,然后点击Next
最后一页是我们选择是否需要工具栏、rebar和状态栏。为了保持应用程序简单,取消选中所有这些选项,然后点击Finish。
通过向导 (VC 7)
在VC中点击File|New|Project,然后从列表中选择ATL/WTL AppWizard。我们将重写时钟应用程序,所以输入WTLClock作为项目名称,然后点击OK
当AppWizard屏幕出现时,点击Application Type。此页面是选择SDI、MDI或基于对话框的应用程序,以及其他一些选项。选择图中所示的选项,然后点击User Interface Features
最后一页是我们选择是否需要工具栏、rebar和状态栏。为了保持应用程序简单,取消选中所有这些选项,然后点击Finish。
检查生成的代码
完成向导后,您将在生成的代码中看到三个类:CMainFrame
、CAboutDlg
和CWTLClockView
。从名称上,您可以猜到每个类的作用。虽然有一个“view”类,但它只是一个“纯粹”的窗口,派生自CWindowImpl
;它没有任何像MFC的文档/视图架构那样的框架。
还有一个_tWinMain()
,它初始化COM、公共控件和_Module
,然后调用全局的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) 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()
。CMessageLoop
、CIdleHandler
和CUpdateUI
协同工作,提供类似于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_hWndClient
是CFrameWindowImpl
的一个成员;这是当框架被调整大小时将被调整大小的窗口。
生成的CMainFrame
还包含File|New、File|Exit和Help|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_hWndClient
是CFrameWindowImplBase
的一个成员,它保存了框架内“视图”窗口的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_EX
。OnPaint()
包含前一个示例中OnEraseBkgnd()
中的所有绘图代码。这是新窗口的样子
我们要添加到此应用程序的最后一项是UI更新。为了演示这一点,我们将添加一个新的顶级菜单项,名为Clock,其中包含Start和Stop命令,用于停止和启动时钟。Start和Stop菜单项将根据需要启用和禁用。
UI更新
有几个组件协同工作来提供空闲时间UI更新:一个CMessageLoop
对象,CMainFrame
继承的混合类CIdleHandler
和CUpdateUI
,以及CMainFrame
中的UPDATE_UI_MAP
。CUpdateUI
可以操作五种不同类型的元素:顶级菜单项(在菜单栏本身中)、弹出菜单中的菜单项、工具栏按钮、状态栏窗格和子窗口(如对话框控件)。每种类型的元素在CUpdateUIBase
中都有一个对应的常量
- 菜单栏项:
UPDUI_MENUBAR
- 弹出菜单项:
UPDUI_MENUPOPUP
- 工具栏按钮:
UPDUI_TOOLBAR
- 状态栏窗格:
UPDUI_STATUSBAR
- 子窗口:
UPDUI_CHILDWINDOW
CUpdateUI
可以设置项的启用状态、选中状态和文本(当然,并非所有项都支持所有状态;您不能在编辑框上打勾)。它还可以将弹出菜单项设置为默认状态,以便文本以粗体显示。
要连接UI更新,我们需要做四件事
- 将框架窗口类从
CUpdateUI
和CIdleHandler
派生 - 将消息从
CMainFrame
传递到CUpdateUI
- 将框架窗口添加到模块的空闲处理程序列表中
- 填充框架窗口的
UPDATE_UI_MAP
AppWizard生成的代码为我们处理了前三个部分,所以我们只需要决定要更新哪些菜单项,以及何时启用或禁用它们。
用于控制时钟的新菜单项
让我们在菜单栏中添加一个新的Clock菜单,其中包含两个项目:IDC_START
和IDC_STOP
然后,我们为每个菜单项在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菜单的样子
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会有点困惑
这是因为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)