Last Update: 06/17/2004
我用的 WTL 是 7.5 版,基於 ATL 7.1 和 Visual Studio.Net 2003。WTL 是 ATL 的一個 extension,可用來開發 GUI,與 MFC 也可和平共存。WTL 可以在 Source Forge 下載,欲使用 WTL 者請先確認自己是否已下載並安裝最新的 Platform SDK。
Michael Dunn (Norton Antivirus 2000 GUI 的作者,現在在 Napster) 在 Code Project 上有貼了一系列的 WTL 簡介文章,基本上大家都是靠它學 WTL 的。底下是學習過程中的一些心得筆記。
我們先從十分基本的 Hello World! 程式開始玩起,順道看看 ATL 對於 window 的原始支援如何。首先在 VS.Net 中開一個 empty 的 Win32 project,然後加入三個檔案:
// stdafx.h
#define STRICT
#define VC_EXTRALEAN
#include <atlbase.h> // Base ATL classes
extern CComModule _Module; // Global _Module
#include <atlwin.h> // ATL windowing classes
<atlwin.h> 是 ATL 內建的對 Window 的簡單支援,也是 WTL 的基礎。
ATL 將 HWND 包裝成 CWindow 類別,並將大部份基本的 Window 操作都包含進去了。之所以不是 IWindow 而是 CWindow 的原因是因為 CWindow 是一個 client-side Windows window object,它只有一個 member (HWND),所以會比 MFC 從 CWnd 一路長下來的東西要有效率的多。
// main.h class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits> { public: DECLARE_WND_CLASS(_T("My Window Class")) BEGIN_MSG_MAP(CMyWindow) MESSAGE_HANDLER(WM_CLOSE, OnClose) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) END_MSG_MAP() LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { DestroyWindow(); return 0; } LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { PostQuitMessage(0); return 0; } };
CWindowImpl 則是 server side 的物件,意即實際處理 window message loop 的地方。原本 ATL 對於 window 的支援就僅止於此了,所以 WTL 就增加了許多不同的 window impl 如 CFrameWindowImpl 等,此是後話。這個 template class 需要三個參數,詳細的說明請參考 VS.NET 中的 help。
最後你必須要有一個 WinMain 來生成 window:
#include "main.h" CComModule _Module; int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR szCmdLine, int nCmdShow) { _Module.Init(NULL, hInst); CMyWindow wndMain; MSG msg; // Create & show our main window if ( NULL == wndMain.Create ( NULL, CWindow::rcDefault, _T("Hello world!") ))
{ // Bad news, window creation failed return 1; } wndMain.ShowWindow(nCmdShow); wndMain.UpdateWindow(); // Run the message loop while ( GetMessage(&msg, NULL, 0, 0) > 0 ) { TranslateMessage(&msg); DispatchMessage(&msg); } _Module.Term(); return msg.wParam; }
這個 CWindowImpl 和 Platform SDK tutorial 中提供的 CVirWindow 非常的相似,也就是它們都只是把傳統的 Window programming 以 C++ class 做較有組織化的呈現,差別在於 ATL 本來就是設計要使 ActiveX control 更易使用,比起 CVirWindow 那套純硬派的做法要容易一些。另外,CComModule 也幫你處理了許多 COM component initialization / finalization 的繁瑣工作。
ATL window 最大的特色是它的 message loop 可以被串聯起來,因此可將 message 處理的部份放到其他的 class 去。比如我們現在若打算做一個廣用的 WM_ERASEBKGND 處理類別:
template <class T, COLORREF t_crBrushColor> class CPaintBkgnd : public CMessageMap { protected: HBRUSH m_hbrBkgnd; public: CPaintBkgnd() { m_hbrBkgnd = CreateSolidBrush(t_crBrushColor); } ~CPaintBkgnd() { DeleteObject ( m_hbrBkgnd ); } BEGIN_MSG_MAP(CPaintBkgnd) MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBkgnd) END_MSG_MAP() LRESULT OnEraseBkgnd(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { T* pT = static_cast(this); HDC dc = (HDC) wParam; RECT rcClient; pT->GetClientRect ( &rcClient ); FillRect ( dc, &rcClient, m_hbrBkgnd ); return 1; // we painted the background } };
若要套用在我們之前造的 CMyWindow 類別,則需做以下的修改
class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>, public CPaintBkgnd<CMyWindow, RGB(0,0,255)> { public: typedef CPaintBkgnd<CMyWindow, RGB(0,0,255)> CPaintBkgndBase; DECLARE_WND_CLASS(_T("My Window Class")) BEGIN_MSG_MAP(CMyWindow) MESSAGE_HANDLER(WM_CLOSE, OnClose) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) CHAIN_MSG_MAP(CPaintBkgndBase) END_MSG_MAP() ... };
看起來真帥,不是嗎?以往 MFC 時代的 copy/paste 終於獲得了解決。不過呢,既然它叫 chain,意即是個 link list。若 WM_CLOSE 先來的話,自然不會跑去 chain 找,這是必須注意的地方。
接下來我們想為這個 frame window 加點東西,就加個 menu 好了。假設我們已用 Resource editor 做好一個 IDR_MENU1,上頭只有一個 About 選項 (IDM_ABOUT)。要如何將它載入 MyWindow 呢?首先你得 #include "resource.h" 這是不在話下,接下來當然是處理 WM_CREATE 和 WM_COMMAND 這兩個 message:
class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>, public CPaintBkgnd<CMyWindow, RGB(0,0,255)> { public: typedef CPaintBkgnd<CMyWindow, RGB(0,0,255)> CPaintBkgndBase; DECLARE_WND_CLASS(_T("My Window Class")) BEGIN_MSG_MAP(CMyWindow) MESSAGE_HANDLER(WM_CREATE, OnCreate) MESSAGE_HANDLER(WM_CLOSE, OnClose) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) COMMAND_ID_HANDLER(IDM_ABOUT, OnAbout) CHAIN_MSG_MAP(CPaintBkgndBase) END_MSG_MAP() LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { HMENU hmenu = LoadMenu ( _Module.GetResourceInstance(), MAKEINTRESOURCE(IDR_MENU1) ); SetMenu ( hmenu ); return 0; } LRESULT OnAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { MessageBox ( _T("Hello World!"), _T("About MyWindow") ); return 0; } ... };
比 MFC 繁瑣是必然得付出的代價,因為也沒有臃腫的 vtable 和討厭的 central message loop。
我們剛剛是使用 MessageBox 來做 About 的功能。若我想用 dialog resource,那要怎麼做呢?若我們已用 resource editor 做好一個 IDD_ABOUT 的 dialog,在 ATL 中便可以下面的程式碼引用之:
class CAboutDlg : public CDialogImpl<CAboutDlg> { public: enum { IDD = IDD_ABOUT }; BEGIN_MSG_MAP(CAboutDlg) MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog) MESSAGE_HANDLER(WM_CLOSE, OnClose) COMMAND_ID_HANDLER(IDOK, OnOK) END_MSG_MAP() LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { CenterWindow(); return TRUE; // let the system set the focus } LRESULT OnClose(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { EndDialog(IDCANCEL); return 0; } LRESULT OnOK(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { EndDialog(wID); return 0; } };
接下來便是修改 CMyWindow 中的 OnAbout() 函式:
LRESULT OnAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled) { CAboutDlg dlg; dlg.DoModal(); return 0; }
到此最基本的操作已經都 ok 了,接下來就要看看 WTL 對 ATL 做了哪些 extension。