MFC 程序员的 WTL 教程 ( 2 ) — WTL 中的 GUI 基础类
第二部分 – WTL 中的 GUI 基础类
内容
第二部分介绍好,是实实在在地讲述 WTL 的时候了!在这部分里,我会介绍写一个主框架窗口的基础知识,以及 WTL 引入的比较受欢迎的改进,比如 UI 更新和更好的消息影射。为了最大程度地掌握本部分的内容,你应该安装 WTL 以使其头文件处于 VC 的搜索路径中,而且 AppWizard 也在适当的目录下。WTL 的分发包中附有如何安装 AppWizard 的说明,请参考该文档。
记住,如果你安装 WTL 或者编译示例代码时遇到了任何问题,请在张贴你的问题之前阅读第一部分的 ReadMe 一节。 WTL 综述WTL 的类可以分为几个主要的类别:
本文将深入到框架窗口中去,顺便提及一些 UI 特性和工具类。大多数的类都是独立的,不过也有一些像 开始一个 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 头文件。它包含了用于消息处理的类和一个继承自 (注意,我们需要一个全局的 接下来我们来定义我们的框架窗口。像我们这样的 SDI 窗口继承自 // 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() };
现在我们来看 // 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; }
如果你马上运行,就可以看到主框架窗口了,当然,它实际上还没有做任何事情。我们需要加入一些消息处理器来干活儿,所以现在是研究 WTL 消息映射宏的好时机。 WTL 消息映射的增强在使用 Win32 API 时,既令人讨厌又易于出错的事情之一就是从随消息一起发送过来的 WTL 的增强消息映射宏在 atlcrack.h 文件中(此名字来源于 “message cracker”,是一个应用于 windowsx.h 中类似的宏的术语)。要使用这些宏的第一个步骤在 VC 6 和 VC 7 里是不一样的,在 atlcrack.h 中的以下提示解释了这一不同:
所以,如果你在使用 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() }; ( 对我们的时钟程序来说,我们需要处理 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 可以帮上忙。将光标(注:此处原文错误,不应该是光标[cursor],而应该是插入符[caret])放在 “MSG_WM_CREATE” 文本上再按 F12 会转到宏的定义处。在 VC 6 里,VC 会先重新编译工程以构建浏览信息数据库。这一工作一旦完成,VC 就会在 #define MSG_WM_CREATE(func) \ if (uMsg == WM_CREATE) \ { \ SetMsgHandled(TRUE); \ lResult = (LRESULT)func((LPCREATESTRUCT)lParam); \ if(IsMsgHandled()) \ return TRUE; \ } 带下划线的是最重要的一行,那是对处理器的实际调用,它告诉我们处理器会返回一个 现在我们可以为窗口类添加一个 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; } };
我们调用 我们还需要一个 #define MSG_WM_DESTROY(func) \ if (uMsg == WM_DESTROY) \ { \ SetMsgHandled(TRUE); \ func(); \ lResult = 0; \ if(IsMsgHandled()) \ return TRUE; \ } 因此我们的 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); } }; 接着是每秒钟调用一次的 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(); } }; 这一处理器仅仅重绘窗口以使新的时间显示在客户区内。最后,我们来处理 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 的封装类之一, 最后,这就是我们的窗口: 示例代码中还有为菜单项加入的 如果你在使用 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。 检查生成的代码向导结束后,在生成的代码里你会看到三个类: 还有一个函数是
最后,我们还有 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; };
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; }
生成的 我们的 WTL 游览的下一站是 CMessageLoop 内幕
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; }
注意,在消息循环中没有对 CFrameWindowImpl 内幕
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; } 此函数检查了窗口是不是要被最小化。如果不是,它就派发到 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); } 注意代码是如何引用 回到时钟程序现在,在看完了框架窗口类的一些细节之后,让我们回到时钟程序上来。就像前例中的 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() }; 注意,只要把 我们要加到应用中的最后一样东西是 UI 更新。出于演示目的,我们要添加一个 Clock 顶级菜单项,并具有 Start 和 Stop 两个命令以开始或者停止时钟。Start 和 Stop 菜单项将被适时地启用或者禁用。 UI 更新空闲时的 UI 更新是由好几件东西一起工作来提供的:一个
要挂接 UI 更新,我们需要作四件事情:
AppWizard 生成的代码已经为我们照顾到了前三项,剩下的事情就是决定哪个菜单项要更新,以及要在什么时候启用或者禁用。 控制时钟的新菜单项我们来在菜单栏上添加一个新的 Clock 菜单,包括两项: 然后我们为每一项在 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() // ... }; 之后无论何时我们要改变任一项的启用状态,我们就调用 这套系统工作起来不同于 MFC 的 调用 UIEnable()让我们回到 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 菜单在应用刚开始时后的样子:
通讯可以通过 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 菜单,然后调用视图的方法,因为视图是控制时钟的类。 关于消息映射的最后注意事项如果你在使用 VC 6,你可能会注意到:当你把 这是因为 ClassView 不能像理解别的一些它能够特殊分析的东西一样理解 #if (ATL_VER < 0x0700) #undef BEGIN_MSG_MAP #define BEGIN_MSG_MAP(x) BEGIN_MSG_MAP_EX(x) #endif 下一站,1995我们仅仅触及到 WTL 的皮毛。在下一篇文章中,我将给我们的示例时钟程序带来 1995 UI 标准并介绍工具栏和状态栏。同时,对 修订历史2003 年 3 月 26 日:首次发布 |

近期评论