程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> MFC程序員WTL指南(3)WTL界面基類

MFC程序員WTL指南(3)WTL界面基類

編輯:關於VC++

對第二部分的介紹

好了,現在正式開始介紹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的實例在超出作用域後不會銷毀它所操作的設備上下文。

所有的工作完成了,現在看看我們的窗口是什麼樣子:

例子代碼中還使用了WM_COMMAND響應菜單消息,在這裡我不作介紹,但是你可以查看例子代碼,看看WTL的COMMAND_ID_HANDLER_EX宏是如何工作的。

從WTL的應用程序生成向導能得到什麼

WTL的發布版本附帶一個很棒的應用程序生成向導,讓我們以一個SDI 應用為例看看它有什麼特性。

使用向導的整個過程

在VC的IDE環境下單擊File|New菜單,從列表中選擇ATL/WTL AppWizard,我們要重寫時鐘程序,所以用WTLClock作為項目的名字:

在下一頁你可以選擇項目的類型,SDI,MDI或者是基於對話框的應用,當然還有其它選項,如下圖所示設置這些選項,然後點擊“下一步”:

在最後一頁你可以選擇是否使用toolbar,rebar和status bar,為了簡單起見,取消這些選項並單擊“結束”。

查看生成的代碼

向導完成後,在生成的代碼中有三個類: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()中。新窗口看起來是這個樣子的:

最後為我們的程序添加UI updating功能,為了演示這些用法,我們為窗口添加Start菜單和Stop菜單用於開始和停止時鐘,Start菜單和Stop菜單將被適當的設置為可用和不可用。

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

空閒時間的界面更新是幾件事情協同工作的結果: CMessageLoop對象,嵌入類CIdleHandler 和 CUpdateUI,CMainFrame類繼承了這兩個嵌入類,當然還有CMainFrame類中的UPDATE_UI_MAP宏。CUpdateUI能夠操作5種不同的界面元素:頂級菜單項(就是菜單條本身),彈出式菜單的菜單項,工具條按鈕,狀態條的格子和子窗口(如對話框中的控件)。每一種界面元素都對應CUpdateUIBase類的一個常量:

  • 菜單條項: UPDUI_MENUBAR
  • 彈出式菜單項: UPDUI_MENUPOPUP
  • 工具條按鈕: UPDUI_TOOLBAR
  • 狀態條格子: UPDUI_STATUSBAR
  • 子窗口: UPDUI_CHILDWINDOW

CUpdateUI可以設置enabled狀態,checked狀態和文本(當然不是所有的界面元素都支持所有狀態,如果一個子窗口是編輯框它就不能被check)。菜單項可以被設置為默認狀態,這樣它的文字會被加重顯示。

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

  1. 主窗口需要繼承CUpdateUI 和 CIdleHandler
  2. 將 CMainFrame 的消息鏈入 CUpdateUI
  3. 將主窗口添加到模塊的空閒處理隊列
  4. 在主窗口中添加 UPDATE_UI_MAP 宏

向導生成的代碼已經為我們做了三件事,現在我們只需要決定那個菜單項需要更新和他們是麼時候可用什麼時候不可用。

添加控制時鐘的新菜單項

在菜單條添加一個Clock菜單,它有兩個菜單項:IDC_START and 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中我們只需編寫處理函數,由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菜單是這樣的:

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顯得有些雜亂無章:

出現這種情況是因為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日,本文第一次發表。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved