WTL 7 心得筆記 (2): WTL 初探

Last Update: 06/17/2004

WTL 的構成

WTL 7.5 中包含的類別已經遠超過 Michael Dunn 撰寫文章時的數量了,在 WTL 的 Readme.txt 檔案中有詳細的介紹。大致上所有的 MFC control 都可以在 WTL 中找到對應的東西。

BEGIN_MSG_MAP_EX 這個 macro 在 ATL 7 中是沒有作用的,它會直接被替代為 BEGIN_MSG_MAP,因此底下的文章中我們都是以最新的 WTL 7.5 加 ATL 7.1 為準。

另外在 atlmisc.h 中有四個類別是 ATL 7.1 中已有的了:CRect, CSize, CPoint 以及 CString。使用時必須用 scope operator 來指明 (i.e. ATL::* 或 WTL::*),不然也可以用

#define _WTL_NO_WTYPES		// disable CRect, CSize, CPoint
#define _WTL_NO_CSTRING // disable CString

看看 WTL AppWizard 產生的程式碼

我們用 WTL AppWizard 產生一個基本的 SDI 程式,叫做 WTLTest2,照預設值完全不改的話可以得到一個簡單的 SDI frame window application。先來看看這個自動產生的程式長什麼樣子:

// stdafx.h

#pragma once

// Change these values to use different versions
#define WINVER		0x0400
//#define _WIN32_WINNT	0x0400
#define _WIN32_IE	0x0400
#define _RICHEDIT_VER	0x0100

#include <atlbase.h>
#include <atlapp.h>

extern CAppModule _Module;

#include <atlwin.h>

乍看下並沒有很大的不同,前頭的 #define 其實是為了配合某些特定的類別或 API 而使用的,例如 _WIN32_WINNT 的值就會影響 CImage 的行為 (Windows 2000 以上才可以使用 AlphaBlend() 成員函數)。atlapp.h 則是 WTL 的基本檔,要使用 WTL 的東西就必須含入。

// stdafx.cpp

#include "stdafx.h"

#if (_ATL_VER < 0x0700)
#include <atlimpl.cpp>
#endif //(_ATL_VER < 0x0700)

stdafx.cpp 只有一些與 ATL 3.0 相容的東西,並沒有什麼用處。

AppWizard 產生的 WTL 程式將 function body 都放在 .h 檔中,有些類似 Java 的做法。我是對這種方法沒什麼意見,但 VS.Net 的 Intellisense 在 .h 檔中是不會動的,所以必須修改一下將 function body 移動到 .cpp 檔去才行。修改過後的 MainFrm.h 如下:

// MainFrm.h : interface of the CMainFrame class

#pragma once

class CMainFrame : public CFrameWindowImpl<CMainFrame>,
		   public CUpdateUI<CMainFrame>,
		   public CMessageFilter, 
		   public CIdleHandler
{
public:
	DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME)

	CWTLTest2View m_view;

	CCommandBarCtrl m_CmdBar;

	virtual BOOL PreTranslateMessage(MSG* pMsg);
	virtual BOOL OnIdle();

	BEGIN_UPDATE_UI_MAP(CMainFrame)
		UPDATE_ELEMENT(ID_VIEW_TOOLBAR, UPDUI_MENUPOPUP)
		UPDATE_ELEMENT(ID_VIEW_STATUS_BAR, UPDUI_MENUPOPUP)
	END_UPDATE_UI_MAP()

	BEGIN_MSG_MAP(CMainFrame)
		MESSAGE_HANDLER(WM_CREATE, OnCreate)
		COMMAND_ID_HANDLER(ID_APP_EXIT, OnFileExit)
		COMMAND_ID_HANDLER(ID_FILE_NEW, OnFileNew)
		COMMAND_ID_HANDLER(ID_VIEW_TOOLBAR, OnViewToolBar)
		COMMAND_ID_HANDLER(ID_VIEW_STATUS_BAR, OnViewStatusBar)
		COMMAND_ID_HANDLER(ID_APP_ABOUT, OnAppAbout)
		CHAIN_MSG_MAP(CUpdateUI<CMainFrame>)
		CHAIN_MSG_MAP(CFrameWindowImpl<CMainFrame>)
	END_MSG_MAP()

// Handler prototypes (uncomment arguments if needed):
//	LRESULT MessageHandler(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
//	LRESULT CommandHandler(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/)
//	LRESULT NotifyHandler(int /*idCtrl*/, LPNMHDR /*pnmh*/, BOOL& /*bHandled*/)

	LRESULT OnCreate(UINT, WPARAM, LPARAM, BOOL&);
	LRESULT OnFileExit(WORD, WORD, HWND, BOOL&);
	LRESULT OnFileNew(WORD, WORD, HWND, BOOL&);
	LRESULT OnViewToolBar(WORD, WORD, HWND, BOOL&);
	LRESULT OnViewStatusBar(WORD, WORD, HWND, BOOL&);
	LRESULT OnAppAbout(WORD, WORD, HWND, BOOL&);
};

CFrameWindowImpl<> 是用來造 frame window 用的。CMessageFilter 是用來提供 PreTranslateMessage() 用的,和 MFC 中的作用相同,都是在 message 進入 TranslateMessage() 之前加以攔截並做處理之用。CIdleHandler 則是提供 OnIdle 用的,也與 MFC 裡的 OnIdle() 一樣,都是提供在 UI thread idle 時可做一些背景處理的動作。

CUpdateUI<> 則提供了一個較有效率的 UI update 機制,它會記住 BEGIN_UPDATE_UI_MAP ... END_UPDATE_UI_MAP 中的 control 是否需要重畫 (亦即處理麻煩的 WM_PAINT),至於 client area 的 WM_PAINT,則由 view class 來負責 (本例中是 CWTLTest2View)。

在 message map 的部份,現在的 WTL programming 已不流行使用 atlhack.h 中那一堆 macro 了,而是直接歸納成三種類型:

macro 對應的 message 類型 對應的 message handler prototype
MESSAGE_HANDLER 一般的 window message MessageHandler
COMMAND_ID_HANDLER WM_COMMAND CommandHandler
NOTIFY_ID_HANDLER WM_NOTIFY NotifyHandler

底下是修改過的 MainFrm.cpp:

#include "stdafx.h"

#include <atlframe.h>
#include <atlctrls.h>
#include <atldlgs.h>
#include <atlctrlw.h>

#include "resource.h"

#include "AboutDlg.h"
#include "WTLTest2View.h"
#include "MainFrm.h"

BOOL CMainFrame::PreTranslateMessage(MSG* pMsg)
{
    if(CFrameWindowImpl<CMainFrame>::PreTranslateMessage(pMsg))
	return TRUE;

    return m_view.PreTranslateMessage(pMsg);
}

BOOL CMainFrame::OnIdle()
{
    UIUpdateToolBar();
    return FALSE;
}

LRESULT CMainFrame::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
    // create command bar window
    HWND hWndCmdBar = m_CmdBar.Create(m_hWnd, rcDefault, NULL, ATL_SIMPLE_CMDBAR_PANE_STYLE);
    // attach menu
    m_CmdBar.AttachMenu(GetMenu());
    // load command bar images
    m_CmdBar.LoadImages(IDR_MAINFRAME);
    // remove old menu
    SetMenu(NULL);

    HWND hWndToolBar = CreateSimpleToolBarCtrl(m_hWnd, IDR_MAINFRAME, FALSE, ATL_SIMPLE_TOOLBAR_PANE_STYLE);

    CreateSimpleReBar(ATL_SIMPLE_REBAR_NOBORDER_STYLE);
    AddSimpleReBarBand(hWndCmdBar);
    AddSimpleReBarBand(hWndToolBar, NULL, TRUE);

    CreateSimpleStatusBar();

    m_hWndClient = m_view.Create(m_hWnd, rcDefault, NULL, WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, WS_EX_CLIENTEDGE);

    UIAddToolBar(hWndToolBar);
    UISetCheck(ID_VIEW_TOOLBAR, 1);
    UISetCheck(ID_VIEW_STATUS_BAR, 1);

    // register object for message filtering and idle updates
    CMessageLoop* pLoop = _Module.GetMessageLoop();
    ATLASSERT(pLoop != NULL);
    pLoop->AddMessageFilter(this);
    pLoop->AddIdleHandler(this);

    return 0;
}

LRESULT CMainFrame::OnFileExit(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
    PostMessage(WM_CLOSE);
    return 0;
}

LRESULT CMainFrame::OnFileNew(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
    // TODO: add code to initialize document

    return 0;
}

LRESULT CMainFrame::OnViewToolBar(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
    static BOOL bVisible = TRUE;	// initially visible
    bVisible = !bVisible;
    CReBarCtrl rebar = m_hWndToolBar;
    int nBandIndex = rebar.IdToIndex(ATL_IDW_BAND_FIRST + 1);	// toolbar is 2nd added band
    rebar.ShowBand(nBandIndex, bVisible);
    UISetCheck(ID_VIEW_TOOLBAR, bVisible);
    UpdateLayout();
    return 0;
}

LRESULT CMainFrame::OnViewStatusBar(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
    BOOL bVisible = !::IsWindowVisible(m_hWndStatusBar);
    ::ShowWindow(m_hWndStatusBar, bVisible ? SW_SHOWNOACTIVATE : SW_HIDE);
    UISetCheck(ID_VIEW_STATUS_BAR, bVisible);
    UpdateLayout();
    return 0;
}

LRESULT CMainFrame::OnAppAbout(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
    CAboutDlg dlg;
    dlg.DoModal();
    return 0;
}

標成藍色的地方是我較感興趣的部份。frame window 的 PreTranslateMessage() 會在處理後再 pass 給 view,而 frame window 的 OnCreate() 則負責 view 的 create,並將自己的 message filter 和 idle handler 加到 module 的 message loop 中。

接下來來看看 view:

// WTLTest2View.h : interface of the CWTLTest2View class

#pragma once

class CWTLTest2View : public CWindowImpl<CWTLTest2View>
{
public:
    DECLARE_WND_CLASS(NULL)

    BOOL PreTranslateMessage(MSG* pMsg)
    {
	pMsg;
	return FALSE;
    }

    BEGIN_MSG_MAP(CWTLTest2View)
	MESSAGE_HANDLER(WM_PAINT, OnPaint)
    END_MSG_MAP()

    LRESULT OnPaint(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
    {
	CPaintDC dc(m_hWnd);

	//TODO: Add your drawing code here

	return 0;
    }
};

view 是用 CWindowImpl 來做的,但和之前的例子比起來,似乎少了些什麼:

class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>

這是因為 CWindowImpl<> 有 default parameter,所以我們的 view class 其實相當於

class CWTLTest2View : public CWindowImpl<CWTLTest2View, CWindow, CControlWinTraits>

最後來看看主程式:

// WTLTest2.cpp : main source file for WTLTest2.exe
//

#include "stdafx.h"

#include <atlframe.h>
#include <atlctrls.h>
#include <atldlgs.h>
#include <atlctrlw.h>

#include "resource.h"

#include "WTLTest2View.h"
#include "aboutdlg.h"
#include "MainFrm.h"

CAppModule _Module;

int Run(LPTSTR /*lpstrCmdLine*/ = NULL, int nCmdShow = SW_SHOWDEFAULT)
{
    CMessageLoop theLoop;
    _Module.AddMessageLoop(&theLoop);

    CMainFrame wndMain;

    if (wndMain.CreateEx() == NULL)
    {
	ATLTRACE(_T("Main window creation failed!\n"));
	return 0;
    }

    wndMain.ShowWindow(nCmdShow);

    int nRet = theLoop.Run();

    _Module.RemoveMessageLoop();
    return nRet;
}

int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR lpstrCmdLine, int nCmdShow)
{
    HRESULT hRes = ::CoInitialize(NULL);
// If you are running on NT 4.0 or higher you can use the following call instead to 
// make the EXE free threaded. This means that calls come in on a random RPC thread.
//    HRESULT hRes = ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
    ATLASSERT(SUCCEEDED(hRes));

    // this resolves ATL window thunking problem when Microsoft Layer for Unicode (MSLU) is used
    ::DefWindowProc(NULL, 0, 0, 0L);

    AtlInitCommonControls(ICC_COOL_CLASSES | ICC_BAR_CLASSES);	// add flags to support other controls

    hRes = _Module.Init(NULL, hInstance);
    ATLASSERT(SUCCEEDED(hRes));

    int nRet = Run(lpstrCmdLine, nCmdShow);

    _Module.Term();
    ::CoUninitialize();

    return nRet;
}

在這個主程式中是使用 CMessageLoop::Run() 來跑主要的 message loop,Michael Dunn 的文章中有提到這個函數的 pseudo logic,我直接抄在下面:

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;
}

其他比較特異的只有 AtlInitCommonControls(),這是 WTL 提供的函數,它直接叫用了 Platform SDK 所提供的 InitCommonControlsEx() 函數,所以該給些什麼 flag,就必須查閱 InitCommonControlsEx() 的相關說明。

各個 Control 的用法用到時再去查 Code Project 即可。