WTL的类可以分为以下几类:
1)窗口实现类 - CFrameWindowImpl, CMDIFrameWindowImpl …
2)控件封装类 - CButton, CListViewCtrl …
3)GDI封装类 - CDC, CMenu …
4)特殊的UI特性 - CSplitterWindow, CUpdateUI, CDialogResize, CCustomDraw …
5)基础类和宏 - CString, CRect, BEGIN_MSG_MAP_EX …
我们从零开始不通过WTL AppWizard创建一个WTL项目。
1)在Stdafx.h添加头文件和定义宏
// stdafx.h
#define STRICT
#define WIN32_LEAN_AND_MEAN
#define _WTL_USE_CSTRING // 如果计划使用WTL封装的CString类,需要定义此宏。
// CString是在atlmisc.h中定义的,但是在其他的头文件中可能会用到此类,尤其在atlmisc.h之前被包含的头文件如AtlApp.h
// 因此,通过这种方法可以预声明一个CString类,以告诉其他头文件CString是个什么东东
#include <atlbase.h> // base ATL classes
#include <atlapp.h> // base WTL classes,包含消息处理和CAppModule的定义(派生于CComModule类)
extern CAppModule _Module; // WTL version of CComModule,它包含我们所需要的空闲进程和UI更新等特性
#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
2)定义一个Frame Window,其派生于CFrameWindowImpl。并且用DECLARE_FRAME_WND_CLASS宏定义窗口类。
// MyWindow.h:
class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
// 两个参数,前者表示窗口名字,如果为NULL,WTL将自动生成一个
// 后者表示窗口的资源ID。
// WTL将会根据此ID搜索图标、菜单、快捷键表等资源,并在窗口创建时加载它们。
// 同样也通过此ID搜索一个字符串用于窗口的Title
DECLARE_FRAME_WND_CLASS(_T("First WTL window"), IDR_MAINFRAME);
BEGIN_MSG_MAP(CMyWindow)
// 用这种方法将消息链到其基类中(值得注意的是WM_SIZE和WM_DESTORY)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
}
3)WinMain函数,与上一节相似:
#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;
}
此时运行程序,将会起动一个空窗口的。
ATL中需要对消息参数WPARAM和LPARAM进行解包的,但是WTL中增强的消息路由机制已经帮我们解决了此问题,
对于ATL3.0,必须用宏BEGIN_MSG_MAP_EX定义消息路由
对于ATL7.0/7.1,从CwindowImpl和CDialogImpl派生的类可以直接使用 宏BEGIN_MSG_MAP定义消息路由,其他类必须使用宏BEGIN_MSG_MAP_EX定义消息路由。
4)添加WM_CREATE消息处理
class CMyWindow : public CFrameWindowImpl<CMyWindow>
{
public:
BEGIN_MSG_MAP_EX(CMyWindow)
// 用这种方法声明WM_CREATE消息的处理
MSG_WM_CREATE(OnCreate)
CHAIN_MSG_MAP(CFrameWindowImpl<CMyWindow>)
END_MSG_MAP()
// OnCreate(...) ?
};
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对象。同时没有了bHandle参数,通过SetMsgHandled()代替此参数。
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,故其有SetTimer()函数。设置每1s触发一个计时消息的计时器。同时用SetMsgHandled(false);设置其基类也能处理该消息。这是一个好的习惯。
5)添加WM_DESTORY处理,销毁计时器
MSG_WM_DESTORY宏的定义如下:
#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)
}
};
1) 自动生成了三个类:CMainFrame,CAboutDlg,CxxxView。
2) 自动生成了程序入口函数_tWinMain(),用于初始化COM、普通控件以及_module,然后调用一个全局的Run()。Run()里创建消息循环类CMessageLoop,并且创建窗口。
3) CMainFrame类:
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;
}
CMessageLoop提供程序的消息泵。不仅提供TranslateMessage/DispatchMessage,还提供消息过滤PreTranslateMessage()和空闲处理OnIdle()。下面是CMessageLoop::Run()的伪代码:
int Run()
{
MSG msg;
for(;;)
{
while ( !PeekMessage(&msg) )
CallIdleHandlers();
if ( 0 == GetMessage(&msg) )
break; // WM_QUIT retrieved from the queue
if ( !CallPreTranslateMessageFilters(&msg) )
{
// if we get here, message was not filtered out
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return msg.wParam;
}
CFrameWindowImpl和其基类CFrameWindowImplBase提供了大量的特性,如工具条、状态条、tooltips,toolButton等。
1)WM_SIZE消息。CFrameWindowImplBase的成员m_hWndClient存储Frame的view视图句柄。因此:
LRESULT OnSize(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled)
{
// 检查是否最小化,如果不是,调用UpdateLayout()
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 Updating
处理UI的更新需要三个类的结合:CMessageLoop,CIdleHandler,CUpdateUI。CUpdateUIBase中存储5个常量对应于5中控件的更新。
CUpdateUI可以设置控件的enabled state, checked state, and text of items等。
关于UI更新,我们有四件事情要做:
1) 从CUpdateUI和CIdleHandler派生Frame窗口类
2) 从CFrameWindow链接消息到CUpdateUI
3) 添加Frame窗口类到_module的Idle handles的列表中
4) 填充Frame窗口下的UPDATE_UI_MAP宏
添加两个菜单项 ID_CLOCK_START, ID_CLOCK_STOP,用来控制时钟。
class CMainFrame : public ...
{
public:
// ...
BEGIN_UPDATE_UI_MAP(CMainFrame)
UPDATE_ELEMENT(ID_CLOCK_START, UPDUI_MENUPOPUP)
UPDATE_ELEMENT(ID_CLOCK_STOP, UPDUI_MENUPOPUP)
END_UPDATE_UI_MAP()
// ...
};
通过上面的方法,来控制UI的可操作性。
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 (ID_CLOCK_START, false );
UIEnable (ID_CLOCK_STOP, true );
return 0;
}