WTL 7 心得筆記 (1): ATL 的基本 UI 能力

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 的。底下是學習過程中的一些心得筆記。

原始 ATL 提供的 window 支援

我們先從十分基本的 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 的繁瑣工作。

Chaining Message Loop

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 找,這是必須注意的地方。

使用 Resource

接下來我們想為這個 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。

使用 Dialog

我們剛剛是使用 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。