WTL窗口
(ATL窗口 第2部分) 翻译:孙凯
---------------------------------------------------------------
这篇文章由Andrew Whitechapel所写。
---------------------------------------------------------------
绪论
在我的第一篇文章中,我讲解了ATL的窗口类。文章带来了一个问题,关于从MFC移往ATL时缺少前台GUI支持的问题。这个争论的结果,产生了Microsoft Windows Template Library(WTL)。我会带你通过十分简单的步骤创建一个基于WTL的框架-视图应用程序,让你自己写出从MFC转向ATL的感受。
WTL是ATL的扩展,也是由ATL小组开发,包含在Microsoft于2000年1月发布的开发平台SDK包中(也可以从Microsoft网站上下载),虽然Microsoft没有正式支持。WTL通过提供一个用于编写Win32应用程序和控制的轻量级的框架,一些特殊的视图,GDI对象和实用的类,来扩展了ATL窗口类。
WTL包由750KB的窗口类库头文件,三个例子和一个Visual Studio WTL AppWizard组成。当然,它还要依靠有1MB大小ATL文件。
要安装WTL,你要做以下的工作:
将WTL目录中的内容复制到你指定的位置。
将WTL\include目录加入到VC++的头文件目录(include directories)列表中。
复制文件appwiz\atlapp60.awx到VC++的定制应用程序向导(Custom App Wizard)目录中,%VCDIR%\Common\MSDev98\Template,%VCDIR%即是安装VC++时指定的目录。
WTL设计特性--附带地,相对于MFC的优势--包括:
模板化,因此有较小的代码量。例如,一个简单的“hello world”SDI应用程序,基于WTL的程序只有24KB,而MFC静态连接结果是440KB,MFC动态连接的结果是24KB+1MB。
无太多相关性,并且可以自由地和SDK代码直接混合。
不会强迫使用特定的应用程序模型,尤其相对于MFC的应用程序框架。
WTL类包括:
标准控制(编辑框,列表框,按钮等等)
公共控制(包括列表视图,树形视图,进度条,微调按钮)
IE控制(rebar,平面滚动条,日历等等)
命令条,菜单,和更新UI类
公共对话框
属性单和页类
框架窗口,MDI框架和子框架,分隔条,可滚动的窗口
设备环境(DC)和GDI对象类(笔、刷子、位图等)
打印机及其信息和设备模式类
实用工具类:包括CPoint, CRect, CSize, 和CString类
WTL AppWizard允许你生成SDI、MDI、多线程SDI和基于对话框的应用程序。多线程SDI应用程序就象IE或Windows Explorer(我的电脑),看起来象是启动了多个实例,实质上它们是同一进程的多个视图。这些视图可以是普通的基于CWindowImpl的窗口,或基于窗体、列表框、编辑框、列表视图、树形视图、丰富文本编辑框或HTML控制。你可以让你的应用程序拥有rebar、命令条(如同Windows CE)、工具条或状态条。你的应用程序可以包含ActiveX控制,甚至可以是一个COM服务器。
Hello WTL
在这个练习中,我们将创建一个基于WTL的简单的"Hello World"应用程序。
创建一个新的WTL AppWizard应用程序。取名HelloWorld。在WTL AppWizard的第一步对话框中,接受所有默认选项,单击下一步按钮。第二步对话框中,也是保留所有默认选项(包含工具条、rebar、命令条、状态条和视图窗口),单击完成按钮。现在就编译运行该程序。你会看到一个非常普通的Win32应用程序,它有标准的框架窗口和视图、菜单、工具条、状态条,以及关于对话框。File|Exit, View|Toolbar, View|Statusbar, 和Help|About菜单项/工具条按钮可以工作,尽管其它的不能。而且,菜单中的某些菜单项具有相应的工具条按钮图标:
现在来分析代码。首先,你应该注意到,在_tWinMain函数中有一个标准的ATL CComModule全局变量,它执行了初始化和终止函数。分析_tWinMain函数,你会发现它所做的其它工作仅仅是初始化公共控件(通过调用InitCommonControlsEx函数)和调用全局函数Run(处理消息循环)。Run函数创建主框架窗口和CMessageLoop对象,调用主框架窗口的ShowWindow函数,然后调用CMessageLoop::Run函数。CMessageLoop::Run函数实质上是轮流调用GetMessage和DispatchMessage函数。
然后,让我们看一看由AppWizard产生的CMainFrame类。它的所有的父类定义在WTL\ATLFrame.h或WTL\ATLApp.h中。其中主要的功能来自于CFrameWindowImpl类。
CUpdateUI通过UPDATE_UI_MAP宏被连接起来,并且最终到达了我们的派生类CMainFrame中的OnViewToolBar和OnViewStatusBar函数。它们做的操作是我们所期望的ShowWindow和SetCheck。
派生于CMessageFilter和CIdleHandler类意味着CMainFrame类必须实现一个消息过滤器。这个消息过滤器用来在分派消息之前清除某些不需处理的消息(例如,改变用户击键被处理的方式)。当消息队列中没有任何消息时,空闲处理函数被调用。
派生的视图类和CComandBarCtrl对象作为成员变量被嵌入到框架窗口类中。
class CMainFrame : public CFrameWindowImpl<CMainFrame>,
public CUpdateUI<CMainFrame>,
public CMessageFilter, //消息过滤
public CIdleHandler //空闲处理
{
public:
DECLARE_FRAME_WND_CLASS(NULL, IDR_MAINFRAME) //定义框架窗口类信息
CHelloWorldView m_view;
CCommandBarCtrl m_CmdBar;
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()
BEGIN_UPDATE_UI_MAP(CMainFrame) //菜单及工具条的更新
UPDATE_ELEMENT(ID_VIEW_TOOLBAR, UPDUI_MENUPOPUP)
UPDATE_ELEMENT(ID_VIEW_STATUS_BAR, UPDUI_MENUPOPUP)
END_UPDATE_UI_MAP()
};
在框架窗口类中唯一最重要的函数是OnCreate。它初始化CComandBarCtrl对象,并与菜单关联、装入命令条图像列表(菜单上的图标)。实际上,CComandBarCtrl类把资源文件中菜单描述转换为工具条元素,它可以很容易地为菜单项和工具条按钮关联同一个命令ID和图标。然后,框架窗口继续创建工具条、伸缩条(rebar)和状态条。接着是初始化视图。最后一步是加入框架窗口的消息过滤器(message filter)和空闲处理器(idle handler)到CComModule类的应用程序对象中。消息过滤是这样一种技术,在你的程序中,在GetMessage函数将消息成功放入你的消息队列之后Translate/DispatchMessage函数处理它之前,在各个窗口之间路由消息。
LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/,
LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
HWND hWndCmdBar = m_CmdBar.Create(m_hWnd,
rcDefault,
NULL,
ATL_SIMPLE_CMDBAR_PANE_STYLE);
m_CmdBar.AttachMenu(GetMenu());
m_CmdBar.LoadImages(IDR_MAINFRAME);
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);
CMessageLoop* pLoop = _Module.GetMessageLoop();
pLoop->AddMessageFilter(this);
pLoop->AddIdleHandler(this);
return 0;
}
视图类派生于CWindowImpl类,应用程序向导为它产生一个消息处理器 - 为WM_PAINT - 带着TODO注释:
class CHelloWorldView: public CWindowImpl<CHelloWorldView>
{
public:
DECLARE_WND_CLASS(NULL)
BOOL PreTranslateMessage(MSG* pMsg)
{
pMsg;
return FALSE;
}
BEGIN_MSG_MAP(CHelloWorldView)
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;
}
};
在这里,作一个小的改动。加入TextOut函数到OnPaint函数中,以输入“Hello World”字符串,编译运行它。
将这个WTL版的程序与前一章(ATL窗口第一部分)的程序比较。我们的轻松来自于向导为我们做了些讨厌的工作。但不要忘了所有这些帮助我们是免费获得的 - 框架+视图、漂亮的菜单、关于对话框、工具条和状态条、包括显示/隐藏功能和更新用户界面处理。同样,与MFC的等价物相比较 - MFC应用程序向导会给你菜单、工具条、状态条和关于对话框,但在MFC中把工具条按钮图标加入到菜单中容易吗?而且,再比较可执行文件的大小,特别是发行版。
在我文章的最后,我想效仿上一章中的ATL Scribble应用程序。
好,如果你在类视图(ClassView)中点击鼠标右键,选择“Add Windows Message Handler”,然后再选择WM_LBUTTONDOWN处理函数,会产生以下代码:
LRESULT OnLButtonDown(UINT uMsg,
WPARAM wParam,
LPARAM lParam,
BOOL& bHandled)
{
return 0;
}
消息映射条目如下:
MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButtonDown)
自从我们使用ATL之后,向导不再提供MFC所用的分类描述消息映射(cracked messages)的宏。“请暂停一下”我听见你说,“你的文章不是说WTL为我们提供了消息分类描述宏吗?”是的,WTL提供了类似于MFC的宏,在ATLCRACK.H文件中。如果查看源代码,你会看见与Windows消息相对应的宏。你所要做的就是使用适当的宏,并实现相同格式的消息处理函数。还有,你要使用BEGIN_MSG_MAP_EX来代替BEGIN_MSG_MAP宏。它为分类描述消息处理函数(cracked handlers)提供了重新得到当前消息和指定消息是否被处理。这是因为分类描述消息处理函数没有ATL处理函数该有的最后一个布尔(bool)参数。因此,BEGIN_MSG_MAP_EX宏定义了一个额外的函数SetMessageHandled来完成此工作。例如,响应WM_LBUTTONDOWN消息的宏如下:
#define MSG_WM_LBUTTONDOWN(func)
if (uMsg == WM_LBUTTONDOWN)
{
SetMsgHandled(TRUE);
func((UINT)wParam, CPoint(GET_X_LPARAM(lParam),
GET_Y_LPARAM(lParam)));
lResult = 0;
if(IsMsgHandled())
return TRUE;
}
注意,这里使用了CPoint,因此要包含ATLMISC.H。
因此,在你的HelloWorldView.h文件最开始处加入#include "atlmisc.h"和"atlcrack.h"。动手将你的视图的消息映射宏改为EX版本,并加下如下的分类消息宏和处理函数。记住,如果你想使用像CPen一样的GDI对象,请将#include "atlgdi.h"加入到文件中。定义两个CPoint对象m_startPoint、m_endPoint到视图中,并在构造函数中初始化为(-1,-1)。
BEGIN_MSG_MAP_EX(CHelloWorldView)
MSG_WM_LBUTTONDOWN(OnLButtonDown)
MSG_WM_LBUTTONUP(OnLButtonUp)
MSG_WM_MOUSEMOVE(OnMouseMove)
END_MSG_MAP()
LRESULT OnLButtonDown (UINT flags, CPoint point)
{
m_startPoint = point;
return 0;
}
LRESULT OnLButtonUp (UINT flags, CPoint point)
{
m_startPoint.x = m_startPoint.y = -1;
return 0;
}
LRESULT OnMouseMove (UINT flags, CPoint point)
{
m_endPoint = point;
CClientDC dc(this->m_hWnd);
CPen np;
np.CreatePen(PS_SOLID, 2, RGB(255,0,0));
HPEN op = dc.SelectPen(np.m_hPen);
if (m_startPoint.x != -1 )
{
dc.MoveTo(m_startPoint.x, m_startPoint.y, NULL);
dc.LineTo(m_endPoint.x, m_endPoint.y);
m_startPoint.x = m_endPoint.x;
m_startPoint.y = m_endPoint.y;
}
dc.SelectPen(op);
return 0;
}
为了改变颜色和画笔,象上一篇文章(ATL窗口第1部分)中一样,我们加入简单的菜单/工具条支持。加入一个新的菜单,“Color”下有三个菜单项“Red”、“Green”和“Blue”。同时,也加入相应的工具条按钮,并确信它们的ID是一致的。编写改变视图中COLORREF类型的成员变量的值的命令处理函数。加入这些命令处理函数的消息映射条目到BEGIN_MSG_MAP_EX/END_MSG_MAP中。
COMMAND_ID_HANDLER_EX(ID_COLOR_RED, OnColorRed)
COMMAND_ID_HANDLER_EX(ID_COLOR_GREEN, OnColorGreen)
COMMAND_ID_HANDLER_EX(ID_COLOR_BLUE, OnColorBlue)
LRESULT OnColorRed(UINT, int, HWND)
{
m_color = RGB(255,0,0);
return 0;
}
LRESULT OnColorGreen(UINT, int, HWND)
{
m_color = RGB(0,255,0);
return 0;
}
LRESULT OnColorBlue(UINT, int, HWND)
{
m_color = RGB(0,0,255);
return 0;
}
如果你这个时候运行程序,你会发现菜单和工具条上这些新的条目可以使用,但却没有任何结果,原来菜单产生的消息并没有传递到视图类中。为什么?哦,命令消息源自框架类,所以菜单(工具条)产生的消息只会传递给框架类。ATL/WTL使用了一种不同于MFC的消息路由策略使消息从一个类传递到另一个类,你必须加入以下的宏到框架类的消息映射中(细节请看ATL窗口第1部分)。
CHAIN_MSG_MAP_MEMBER(m_view)
有些东西是与MFC不同的,尽管你已看到这些扩展来自于ATL。重要的美景是什么?认真的开发员不必太拘泥于很小的向导支持。另一方面,漂亮的菜单(cool menu)是很漂亮,但它真的值得从MFC改变过来?是的,WTL可以说是ATL++,ATL是重要的基于COM的开发工具。这是否就是使用它的充分理由?毕竟,WTL不被微软正式支持。
对于WTL,你需要些什么支持?微软使用WTL的早期版本已经几年了,因为用它开发的软件很小很高效,而且微软ATL/WTL小组和广大的ATL/WTL社团都承诺继续支持WTL。
ATL/WTL并不会很快取代MFC,但许多项目要求更快的生产效率,更快的运行效率,更少的消耗,加上对COM的轻松支持,ATL/WTL代替MFC是可能的。我使用MFC已经10年了,但ATL/WTL组合是多么的诱人。如果保留我们对旧的技术的投资是一条标准,我们可能还在用COBOL。我们做的东西已被大家接受,你还想到哪里去呢?