前记
这几个嵌入类其实很早之间就完成了,2003年的时候我在CodeProject上发布了这些代码,不过当时使用了紫色作为按钮的边框,导致几个无聊的LY在哪里争吵关于Gays的问题,呵呵,看来不仅语言要接轨,颜色的认识也要和西方接轨哟。现在刚好趁这个机会整理一下代码,写篇文章纪念之。
本文的目的是使用ATL/WTL做一个与“创建有个性的对话框之MFC篇”的个性对话框一样的对话框。ATL/WTL一套模板库,创建ATL/WTL应用程序不可避免的要用到C++的模板与多继承方面的知识,在看本文之前希望您对它们有所了解。本文结尾可以下载文中介绍的例子代码,编译这些代码需要安装WTL库,对于WTL的更详细的信息请查看oRbIt翻译的“WTL for MFC Programmers”系列文章,具体位置在:http://blog.csdn.net/orbit。
ATL和WTL一起构建了一个轻量级的应用程序框架,ATL在设计时接口定义和实现是严格区分开的,这在窗口类的设计中是最明显的,这一点类似于COM,COM的接口定义和实现是完全分开的(或者可能有多个实现)。ATL有一个专门为窗口设计的接口,可以做全部的窗口操作,这就是CWindow。它实际上就是对HWND操作的包装类,对几乎所有以HWND句柄为第一个参数的窗口API的进行了封装,例如:SetWindowText() 和 DestroyWindow()。在ATL类中对窗口过程的实现是CWindowImpl。CWindowImpl 含有所有窗口实现代码,例如:窗口类的注册,窗口的子类化,消息映射以及基本的WindowProc()函数,可以看出这与MFC的设计有很大的不同,MFC将所有的代码都放在一个CWnd类中。
由于ATL和MFC都是应用于Windows平台的库,所以他们都能够响应和处理系统发送的窗口消息,只是ATL和MFC对消息的分派方式不同,从而造成编写代码方面的差异。这些差异并不是不可逾越,ATL也定义了一些与MFC类似的宏来处理和分派消息,每个ATL的窗口类都用一个消息映射表或者称其为消息映射链,将消息处理函数与特定的消息关联起来,这和MFC的做法是类似的。少量的不同之处在于消息响应函数的参数,MFC对Windows的消息参数,也就是wParam和lParam进行了内部解释,传递给消息响应函数的参数比较友好,ATL的消息响应函数则是原原本本的将wParam和lParam传递给了消息响应函数,对Windows的消息不太熟悉的程序员可能会很迷惑。如果你对C++的模板机制比较了解,并且愿意不停的查阅MSDN,那就很容易将MFC的窗口类“翻译”成ATL/WTL的窗口类,比如本文用到的CWzButtonImpl类就是从本文的“MFC姊妹篇”中例子代码的CSMButton类翻译过来的。
在开始用ATL/WTL创建个性对话框之前,还要介绍一下ATL中常用的嵌入类(Mix-in class)。ATL的另一个显著不同之处就是任何一个C++类都可以响应消息,而MFC只是将消息响应任务分给了CWnd类和CCmdTarget类,外加几个有PreTranslateMessage()方法的类。ATL的这种特性允许我们编写所谓的“嵌入类”,为我们的窗口添加特性只需将该类添加到继承列表中就行了,就这么简单!一个基本的带有消息映射链的类通常是模板类,将派生类的类名作为模板的参数,这样它就可以访问派生类中的成员,比如m_hWnd(CWindow类中的HWND成员)。让我们来看一个嵌入类的例子,这个嵌入类通过响应WM_ERASEBKGND消息来画窗口的背景:
template <class T, COLORREF t_crBrushColor>
class CPaintBkgnd : public CMessageMap
{
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<T*>(this);
HDC dc = (HDC) wParam;
RECT rcClient;
pT->GetClientRect ( &rcClient );
FillRect ( dc, &rcClient, m_hbrBkgnd );
return 1; // we painted the background
}
protected:
HBRUSH m_hbrBkgnd;
};
让我们来研究一下这个新类。首先,CPaintBkgnd有两个模板参数:使用CPaintBkgnd的派生类的名字和用来画窗口背景的颜色。(t_ 前缀通常用来作为模板类的模板参数的前缀)CPaintBkgnd也是从CMessageMap派生的,这并不是必须的,因为所有需要响应消息的类只需使用BEGIN_MSG_MAP宏就足够了,所以你可能看到其他的一些嵌入类的例子代码,它们并不是从该基类派生的。
构造函数和析构函数都相当简单,只是创建和销毁Windows画刷,这个画刷由参数t_crBrushColor决定颜色。接着是消息映射链,它响应WM_ERASEBKGND消息,最后由响应函数OnEraseBkgnd()用构造函数创建的画刷填充窗口的背景。要在我们的窗口中使用这个嵌入类需要做两件事:首先,将它加入到继承列表:
class CMyWindow : public CWindowImpl<CMyWindow, CWindow, CFrameWinTraits>,
public CPaintBkgnd<CMyWindow, RGB(0,0,255)>
其次,需要CMyWindow将消息传递给CPaintBkgnd,就是将其链入到消息映射链,在CMyWindow的消息映射链中加入CHAIN_MSG_MAP宏:
typedef CPaintBkgnd<CMyWindow, RGB(0,0,255)> CPaintBkgndBase;
BEGIN_MSG_MAP(CMyWindow)
MESSAGE_HANDLER(WM_CLOSE, OnClose)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
COMMAND_HANDLER(IDC_ABOUT, OnAbout)
CHAIN_MSG_MAP(CPaintBkgndBase)
END_MSG_MAP()
任何CMyWindow没有处理的消息都被传递给CPaintBkgnd。应该注意的是WM_CLOSE,WM_DESTROY和IDC_ABOUT消息将不会传递,因为这些消息一旦被处理消息映射链的查找就会中止。你可以在继承列表中使用多个嵌入类,每一个嵌入类使用一个CHAIN_MSG_MAP宏,这样消息映射链就会将消息传递给它。这与MFC不同,MFC的CWnd派生类只能有一个基类,MFC自动将消息传递给基类。
了解了ATL消息处理方式和嵌入类的知识之后,就可以开始创建ATL/WTL的彩色对话框了。在“创建有个性的对话框之MFC篇”中提到了一种最简单的改变对话框背景颜色的方法,就是调用CWinApp::SetDialogBkColor()函数,很不幸,ATL/WTL没有提供这个方法,至少ATL 7.1和WTL 7.1是这样的。不过没关系,我们可以向MFC那样通过处理一些特殊的窗口绘制消息来实现彩色的对话框,而且ATL/WTL的方式更加灵活,稍后就会看到。
首先是修改对话框的背景,这个只需要处理WM_ERASEBKGND消息就行了,在ATL的窗口中也可以这样做,不过ATL可以使用更加灵活的嵌入类,前面提到的CPaintBkgnd就是这样的嵌入类。使用嵌入类将WM_ERASEBKGND消息的处理函数封装到CPaintBkgnd类中,避免了CMainDlg代码的过度臃肿,同时也使得创建同样背景的对话框更加用以,不需要拷贝OnEraseBkgnd中的代码到新对话框类,只需将CPaintBkgnd加入到新类的集成列表中就行了:
class CMainDlg : public CDialogImpl<CMainDlg>,
public CPaintBkgnd<CMainDlg, RGB(0,0,255)>
这样CMainDlg就拥有了蓝色的背景,改变CPaintBkgnd的模板参数t_crBrushColor可以定制对话框不同的颜色,下面的代码使得对话框拥有了淡绿色的背景:
class CMainDlg : public CDialogImpl<CMainDlg>,
public CPaintBkgnd<CMainDlg, RGB(215,241,215)>
图1.使用了CPaintBkgnd的对话框
和在MFC中处理WM_ERASEBKGND消息一样,控件的背景颜色还没有改变,下面本文就介绍在ATL/WTL框架下如何响应WM_CTLCOLORXXX消息。
MFC将所有的WM_CTLCOLORXXX系列消息都映射为WM_CTLCOLOR消息,只是通过OnCtlColor()函数的参数区分他们,不过WM_CTLCOLORDLG消息并没有被映射为WM_CTLCOLOR,因为MFC对为WM_CTLCOLORDLG消息做了其他处理,如果你愿意追踪一下CWinApp::SetDialogBkColor()的代码你就会看到MFC是如何处理WM_CTLCOLORDLG消息的。也正式因为MFC对WM_CTLCOLORDLG“另有打算”,使得本文的“MFC姊妹篇”中的例子为了改变对话框的背景色不得不处理了WM_ERASEBKGND消息。但是使用ATL/WTL则不需要额外处理WM_ERASEBKGND消息,因为处理WM_CTLCOLORDLG通知消息更简单。ATL/WTL没有对WM_CTLCOLORXXX系列消息统一处理,不过如果集中处理这些消息能够简化代码编写,提高与MFC代码的互换性,我们也可以将其映射到一个消息处理函数中,事实上下面将要介绍的CCtrlColor就是这样做的。
下面就是使用了CCtrlColor之后的效果图,这里没有继续使用CPaintBkgnd,因为CCtrlColor中处理了WM_CTLCOLORDLG通知消息。
图2.使用了CCtrlColor的对话框
在对话框中使用位图背景也很简单,只要照着CPaintBkgnd样子写一个画位图的类就行了,以下是一个简单的例子:
template <class T, UINT uBitmapID>
class CBitmapBkgnd : public CMessageMap
{
public:
CBitmapBkgnd() { m_Bitmap.LoadBitmap(uBitmapID); }
~CBitmapBkgnd() { m_Bitmap.DeleteObject(); }
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<T*>(this);
HDC hDC = (HDC) wParam;
RECT rcClient;
pT->GetClientRect ( &rcClient );
BITMAP bm;
m_Bitmap.GetBitmap(&bm);
CDC memDC;
memDC.CreateCompatibleDC(hDC);
HBITMAP hOldBmp = memDC.SelectBitmap(m_Bitmap);
StretchBlt(hDC,0,0,rcClient.right - rcClient.left,rcClient.bottom - rcClient.top,memDC,0,0,bm.bmWidth,bm.bmHeight,SRCCOPY);
memDC.SelectBitmap(hOldBmp);
memDC.DeleteDC();
return 1; // we painted the background
}
protected:
CBitmap m_Bitmap;
};
和CPaintBkgnd相比,CBitmapBkgnd只是修改了第二个模板参数和OnEraseBkgnd()函数的代码,由于使用位图作为对话框的背景,对话框中的控件在处理WM_CTLCOLORXXX系列消息时应该返回一个空画刷而不是默认的背景颜色画刷,也就是需要一个定制的处理WM_CTLCOLORXXX系列消息的嵌入类,本文用了CCtrlHollowColor与CBitmapBkgnd配合使用,但是CCtrlHollowColor过于简单,它只能在本文的例子中使用,如果对话框中有更多的控件就需要修改CCtrlHollowColor类的代码。下面是使用位图背景的效果:
图3.使用位图背景的对话框
现在该考虑按钮的问题了,和本文的MFC姊妹篇一样,本文设计的WTL例子也使用了一个自画按钮类。前文已经提过,它其实就是从CSMButton“移植”过来的,你肯定觉得还是有些不一样,那是因为CWzButtonImpl使用了ATL的特性,那就是使用WTL的嵌入类:COwnerDraw。在MFC的按钮类CSMButton中,WM_DRAWITEM消息被MFC做了内部映射,最终的按钮绘制映工作交给了虚函数DrawItem(),这是利用了C++的多态机制。ATL/WTL不使用虚函数,多继承和嵌入类是它拥有比MFC更加灵活的方式避开了使用虚函数,这也就使得ATL/WTL的窗口对象比MFC的窗口对象占用更少的内存。只要将COwnerDraw类加入窗口类的集成列表,就可以原封不动的将CSMButton::DrawItem()搬到CWzButtonImpl中,以下是CWzButtonImpl类的完整声明,可以看到它是个窗口类,具有CButton的接口:
class CWzButtonImpl : public CWindowImpl<CWzButtonImpl, CButton>,public COwnerDraw<CWzButtonImpl>
CWzButtonImpl的使用方法可在ATL中与MFC有较大的差异,它只能通过SubclassWindow()这个方法与控件关联起来,不过有了WTL之后就和MFC一样了,因为ATL也实现了类似MFC的DDE/DDV。
如果一个对话框中有很多个按钮,一个一个添加关联也是一件很麻烦的事情,尤其是当你需要从对话框中删除某个按钮的时候。在这里我要介绍一个有用的辅助类:CButtonHelp。CButtonHelp是一个模板类,它的作用就是在对话框初始化的时候枚举对话框中所有的控件,找出其中的按钮控件并用模板参数指定的按钮类子类化(Subclassing)这个控件。以下是CButtonHelp类的声明:
template <class T, class t_ButtonClass>
class CButtonHelp
第一个模板参数是窗口类,第二个模板参数就是按钮类,通过替换模板参数t_ButtonClass可以在不同的对话框中使用不同风格的按钮,下面的例子就是本文使用的例子代码中主对话框的声明,他使用CWzButtonImpl来子类化按钮控件:
class CMainDlg : public CDialogImpl<CMainDlg>, public CCtrlColor<CMainDlg>,
public CButtonHelp<CMainDlg,CWzButtonImpl>
假设还有一个自画按钮类CBitmapButtonImpl,下面的声明将使CBmpDlg对话框拥有了另一种风格的自画按钮:
class CBmpDlg : public CDialogImpl<CBmpDlg>, public CCtrlColor<CBmpDlg>,
public CButtonHelp<CBmpDlg,CBitmapButtonImpl>
CButtonHelp类有三个主要的函数,一个是SubclassAllButtons(),这个函数遍历对话框所有的子控件,子类化其中具有pushbutton风格的按钮,SubclassButton()子类化指定的按钮,UnSubclassButton()则是SubclassButton()的逆过程。下面就是使用了CButtonHelp和CWzButtonImpl后的效果:
图4.使用自画按钮后的对话框
至此,本文已经使用ATL/WTL实现了MFC例子中的全部功能,通过对比可以看到这两个FrameWork各有各的优点,对同一个问题的解决方案也是一样的,只是编码的方式不同。本文使用的例子代码可以在文后的链接中下载。
http://blog.csdn.net/images/blog_csdn_net/orbit/demo2.zip[url=http://blog.csdn.net/images/blog_csdn_net/orbit/demo.zip][/url]