特别注:由于本页内容栏宽度不够,会导致部分内容看不见,请点击这里以获得最佳浏览效果。
第二部分 – WTL 中的 GUI 基础类
内容
- 第二部分介绍
- WTL 综述
- 开始一个 WTL EXE
- WTL 消息映射的增强
- 使用 WTL AppWizard 可以得到什么
- 通历向导(VC 6)
- 通历向导(VC 7)
- 检查生成的代码
- CMessageLoop 内幕
- CFrameWindowImpl 内幕
- 回到时钟程序
- UI 更新
- 控制时钟的新菜单项
- 调用 UIEnable()
- 关于消息映射的最后注意事项
- 下一站,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 这样的嵌入类(mix-in)。
开始一个 WTL EXE
如果你不使用 WTL AppWizard (稍后我们就会提到它),那么一个 WTL EXE 一开始会很像一个 ATL EXE。如同第一部分中的那样,本文中的示例代码是另一个框架窗口,不过为了展示一些 WTL 的特性,较之前者不再那么微不足道。
在本节里,我们会从头开始一个新的 EXE。主窗口会在其客户区显示当前的时间。下面是一个基本的 stdafx.h:
1 2 3 4 5 6 7 8 9 10 11 |
#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 头文件。它包含了用于消息处理的类和一个继承自 CComModule 的类 CAppModule。如果你计划使用 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 里我们窗口定义的开头:
1 2 3 4 5 6 7 8 9 10 |
// 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() 极其类似,只是创建主窗口的调用存在差异。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// 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 文件中(此名字来源于 “message cracker”,是一个应用于 windowsx.h 中类似的宏的术语)。要使用这些宏的第一个步骤在 VC 6 和 VC 7 里是不一样的,在 atlcrack.h 中的以下提示解释了这一不同:
对于 ATL 3.0,使用了解拆处理器的消息映射必须使用 BEGIN_MSG_MAP_EX。
对于 ATL 7.0/7.1,你可以为 CWindowImpl/ CDialogImpl 的派生类使用 BEGIN_MSG_MAP,但是对于不是派生于 CWindowImpl/ CDialogImpl 的类则必须使用 BEGIN_MSG_MAP_EX。
所以,如果你在使用 VC 6,你需要这样改动你的 MyWindow.h:
1 2 3 4 5 6 7 8 |
// MyWindow.h, VC6 only: class CMyWindow : public CFrameWindowImpl<CMyWindow> { public: <span style="color: red;">BEGIN_MSG_MAP_EX(CMyWindow)</span> CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>) END_MSG_MAP() }; |
( _EX 宏对于 VC 6 来讲是必需的,因为包含于其中的某些代码是消息处理器宏需要使用的。出于可读性的原因,这里就不列出 VC 6 和 VC 7 版本的头文件了,因为它们仅仅是一个宏上面的不同。只需记住 _EX 宏在 VC 7 里是不需要的即可。)
对我们的时钟程序来说,我们需要处理 WM_CREATE 并设置一个定时器。WTL 把针对一个消息的消息处理器命名为 MSG_ 后随消息名,比如 MSG_WM_CREATE。这些宏仅接受处理器的名字。我们来为 WM_CREATE 添加一个处理器:
1 2 3 4 5 6 7 8 9 10 |
class CMyWindow : public CFrameWindowImpl<CMyWindow> { public: BEGIN_MSG_MAP_EX(CMyWindow) <span style="color: red;">MSG_WM_CREATE(OnCreate)</span> CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>) END_MSG_MAP() // OnCreate(...) ? }; |
WTL 的消息处理器看起来很像 MFC,每个处理器都根据随消息传入的参数有一个不同的原型。不过,由于没有向导来写处理器,我们不得不自己来查找原型。幸运的是 VC 可以帮上忙。将光标(注:此处原文错误,不应该是光标[cursor],而应该是插入符[caret])放在 “MSG_WM_CREATE” 文本上再按 F12 会转到宏的定义处。在 VC 6 里,VC 会先重新编译工程以构建浏览信息数据库。这一工作一旦完成,VC 就会在 MSG_WM_CREATE 的定义处打开 atlcrack.h:
1 2 3 4 5 6 7 8 |
#define MSG_WM_CREATE(func) \ if (uMsg == WM_CREATE) \ { \ SetMsgHandled(TRUE); \ <span style="text-decoration: underline;">lResult = (LRESULT)func((LPCREATESTRUCT)lParam);</span> \ if(IsMsgHandled()) \ return TRUE; \ } |
带下划线的是最重要的一行,那是对处理器的实际调用,它告诉我们处理器会返回一个 LRESULT 并接受一个 LPCREATESTRUCT 类型的参数。注意,没有像 ATL 的宏所使用的 bHandled 参数。 SetMsgHandled() 函数替代了该参数,很快我们就要解释这件事情。
现在我们可以为窗口类添加一个 OnCreate() 处理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class CMyWindow : public CFrameWindowImpl<CMyWindow> { public: BEGIN_MSG_MAP_EX(CMyWindow) MSG_WM_CREATE(OnCreate) CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>) END_MSG_MAP() <span style="color: red;">LRESULT OnCreate(LPCREATESTRUCT lpcs) { SetTimer ( 1, 1000 ); SetMsgHandled(false); return 0; }</span> }; |
CFrameWindowImpl 间接地从 CWindow 派生而来,因此它具有所有 CWindow 的函数,例如 SetTimer()。这使得调用窗口 API 看起来很像 MFC 代码,在 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 宏,看起来就是这样:
1 2 3 4 5 6 7 8 9 |
#define MSG_WM_DESTROY(func) \ if (uMsg == WM_DESTROY) \ { \ SetMsgHandled(TRUE); \ <span style="text-decoration: underline;">func();</span> \ lResult = 0; \ if(IsMsgHandled()) \ return TRUE; \ } |
因此我们的 OnDestroy() 处理器既没有参数也没有返回值。 CFrameWindowImpl 的确也处理了 WM_DESTROY,所以这儿仍然需要调用 SetMsgHandled(false):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class CMyWindow : public CFrameWindowImpl<CMyWindow> { public: BEGIN_MSG_MAP_EX(CMyWindow) MSG_WM_CREATE(OnCreate) <span style="color: red;">MSG_WM_DESTROY(OnDestroy)</span> CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>) END_MSG_MAP() <span style="color: red;">void OnDestroy() { KillTimer(1); SetMsgHandled(false); }</span> }; |
接着是每秒钟调用一次的 WM_TIMER 处理器。现在你应该已经对 F12 这一技巧很熟悉了,所以我们只呈现处理器本身:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class CMyWindow : public CFrameWindowImpl<CMyWindow> { public: BEGIN_MSG_MAP_EX(CMyWindow) MSG_WM_CREATE(OnCreate) MSG_WM_DESTROY(OnDestroy) <span style="color: red;">MSG_WM_TIMER(OnTimer)</span> CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>) END_MSG_MAP() <span style="color: red;">void OnTimer ( UINT uTimerID, TIMERPROC pTimerProc ) { if ( 1 != uTimerID ) SetMsgHandled(false); else RedrawWindow(); }</span> }; |
这一处理器仅仅重绘窗口以使新的时间显示在客户区内。最后,我们来处理 WM_ERASEBKGND,在相应的处理器中,把当前时间绘制在客户区的左上角。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
class CMyWindow : public CFrameWindowImpl<CMyWindow> { public: BEGIN_MSG_MAP_EX(CMyWindow) MSG_WM_CREATE(OnCreate) MSG_WM_DESTROY(OnDestroy) MSG_WM_TIMER(OnTimer) <span style="color: red;">MSG_WM_ERASEBKGND(OnEraseBkgnd)</span> CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>) END_MSG_MAP() <span style="color: red;">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) }</span> }; |
此处理器演示了 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:
最后一页里我们可以选择拥有工具栏,复用栏以及状态栏。为了保证应用的简单,去掉所有这些选择并点击 Finish。
通历向导(VC 7)
点击 VC 的 File|New 并在列表里选择 ATL/WTL AppWizard。我们来重写时钟程序,输入 WTLClock 作为工程名字:
当 AppWizard 界面出来后,点击 Application Type。在本页里,你可以选择是 SDI、MDI 还是基于对话框的应用,以及一些其他的选项。选择下面显示的选项并点击 User Interface Features:
最后一页里我们可以选择拥有工具栏,复用栏以及状态栏。为了保证应用的简单,去掉所有这些选择并点击 Finish。
检查生成的代码
向导结束后,在生成的代码里你会看到三个类: CMainFrame、 CAboutDlg 和 CWTLClockView。从名字里你就可以猜出每个类的作用。尽管有一个“view”类,不过它却是从 CWindowImpl 派生而来的一个“普通”窗口,而没有像 MFC 的文档/视图架构中的框架窗口。
还有一个函数是 _tWinMain(),它初始化 COM、公用控件以及 _Module,之后再调用一个全局的 Run() 函数。 Run() 会创建主窗口并开始消息泵,它还使用了一个新类 CMessageLoop。 Run() 调用 CMessageLoop::Run(),确切地说是后者包含了消息泵。在下一节里我们将了解 CMessageLoop 的更多细节。
CAboutDlg 是一个简单的 CDialogImpl 派生类,它关联到一个 ID 为 IDD_ABOUTBOX 的对话框上。我在第一部分里谈到了对话框,所以你应该能够理解 CAboutDlg 的代码。
CWTLClockView 是我们这一应用的“view”类。它工作起来像是一个 MFC 视图,没有标题栏,占据着主框架的客户区。 CWTLClockView 有一个 PreTranslateMessage() 函数,它工作起来也像是 MFC 中的同名函数。再有就是 WM_PAINT 处理器。目前还没有哪个函数在做举足轻重的事情,但我们即将填写 OnPaint() 方法来显示时间。
最后,我们还有 CMainFrame,它有许多有趣的新东西。下面是类定义的一个简化版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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 维护的消息过滤器列表和空闲处理列表中。稍后会介绍这些内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 为我们的应用程序提供了消息泵。除标准的 DispatchMessage/ TranslateMessage 循环之外,它还通过 PreTranslateMessage() 提供了消息过滤功能,通过 OnIdle() 提供了空闲处理功能。以下是 Run() 逻辑伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
int Run() { MSG msg; for(;;) { while ( !PeekMessage(&msg) ) CallIdleHandlers(); if ( 0 == GetMessage(&msg) ) break; // WM_QUIT retrieved from the queue if ( !CallTranslateMessageFilters(&msg) ) { // if we get here, message was not filtered out TranslateMessage(&msg); DispatchMessage(&msg); } } return msg.wParam; } |
CMessageLoop 知道要调用哪一个 PreTranslateMessage() 函数是因为每个需要过滤消息的类都要像 CMainFrame::OnCreate() 所做的那样调用 CMessageLoop::AddMessageFilter()。与之相仿,需要进行空闲处理的类要调用 CMessageLoop::AddIdleHandler()。
注意,在消息循环中没有对 TranslateAccelerator() 或者 IsDialogMessage() 进行调用。 CFrameWindowImpl 处理了前者,不过,如果你要在应用里添加任何非模态对话框,你就需要在 CMainFrame::PreTranslateMessage() 中增加对 IsDialogMessage() 的调用。
CFrameWindowImpl 内幕
CFrameWindowImpl 及其基类 CFrameWindowImplBase 提供了许多 MFC 的 CFrameWnd 具有的特性:工具栏、复用栏(Rebar)、状态栏、用于工具栏按钮的工具提示(Tooltip)以及针对菜单项的动态帮助。我会逐渐将到这些特性,因为完整地讨论 CFrameWindowImpl 类需要占用整整两篇文章!至于眼下,看看 CFrameWindowImpl 是如何处理 WM_SIZE 和客户区就足够了。在此,请记住 m_hWndClient 是 CFrameWindowImplBase 的一个成员,用来存放位于框架中的“view”的 HWND。
CFrameWindowImpl 有 WM_SIZE 的一个处理器:
1 2 3 4 5 6 7 8 9 10 11 |
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():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 一样,视图窗口可以处理定时器和绘制。以下是类的部分定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 改成了 BEGIN_MSG_MAP_EX,那你就可以将 ATL 的消息映射宏和 WTL 版本的混合起来使用。 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 中添加一个入口:
1 2 3 4 5 6 7 8 9 10 |
class CMainFrame : public ... { public: // ... BEGIN_UPDATE_UI_MAP(CMainFrame) <span style="color: red;">UPDATE_ELEMENT(IDC_START, UPDUI_MENUPOPUP) UPDATE_ELEMENT(IDC_STOP, UPDUI_MENUPOPUP)</span> 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 菜单项的初始状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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] <span style="color: red;">// Set the initial state of the Clock menu items: UIEnable ( IDC_START, false ); UIEnable ( IDC_STOP, true );</span> return 0; } |
下面是 Clock 菜单在应用刚开始时后的样子:
CMainFrame 现在需要这两个新项的处理器。处理器会倒换菜单项的状态,然后再调用视图类的方法开始或者停止时钟。此处是 MFC 的内建消息路由严重遗漏的领域,如果这是一个 MFC 应用,所有的 UI 更新和命令处理可能会被完全放到视图类中。但是在 WTL 里,框架和视图类必须用某种方法相互通讯,菜单为框架所有,因此框架会收到菜单相关的消息并且有责任响应它们,要么自己处理,要么发给视图类。
通讯可以通过 PreTranslateMessage() 完成,不过 UIEnable() 的调用还必须由 CMainFrame 完成。 CMainFrame 可以通过将自己的 this 指针传递给视图类而逃避责任,于是视图类可以使用该指针来调用 UIEnable()。在本例中,我选择的方案会导致框架与视图类的紧密耦合,但是我发现它既易于理解又易于解释!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
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 的末尾即可:
1 2 3 4 |
#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() 来看看改变菜单项的不同方法。
修订历史
2003 年 3 月 26 日:首次发布
2005 年 12 月 15 日:更新,包括了 VC 7.1 里 ATL 的改动