WTL自画按钮的实现

来源:百度文库 编辑:神马文学网 时间:2024/04/28 21:24:59
WTL自画按钮的实现 2003-01-14··Bjarne··yesky

一.思路:
Windows 为控件提供了自画(owner draw)的能力,程序员可以通过这一机制实现非常酷的控件外观。WTL(Windows Template Library)提供了一个CownerDraw模板,用来对控件的自画操作提供支持。
COwnerDraw 的声明为如下形式:
template <class T>
class CownerDraw

……
};
从上面的代码可以看出,它没有从任何基类或模板派生,它并不是一个窗口类。它只为参数T(T必须是一个支持自画的控件类)提供自画支持。除了自画以外,我们也许还想让按钮具有ToolTip功能,或者看起来象一个位图按钮,最好还能在位图的背景下显示文字,或者上面显示位图下面显示文字。这些功能我们都可以通过自画操作来实现,但是那样会很麻烦,利用WTL提供的CbitmapButtonImpl模板,我们只需要简单地继承再加上自画能力就可以实现上述功能。现在看一看自画按钮的声明:
class CownerDrawButton : public CbitmapButtonImpl<CownerDrawButton>,
public CownerDraw<CownerDrawButton>
……
它采用多继承的方式从两个模板派生,从而不但具有了自画的能力,而且也是一个位图按钮。
二、COwnerDraw 模板
CownerDraw模板提供了一组消息映射宏和相应的响应函数。如:
BEGIN_MSG_MAP(COwnerDraw< T >)
MESSAGE_HANDLER(WM_DRAWITEM, OnDrawItem)
……
ALT_MSG_MAP(1)
MESSAGE_HANDLER(OCM_DRAWITEM, OnDrawItem)
……
END_MSG_MAP()
因为CownerDrawButton从两个基类派生,在响应WM_DRAWITEM消息时,它需要把消息链接到CownerDraw模板,在响应其它消息(如WM_CREATE)时,需要把消息链接到CBitmapButtonImpl模板。ATL(注意是ATL而不WTL,WTL构建在ATL之上)不知道哪一个消息要对应到哪个父类的处理函数,如果两个父类都响应相同的消息,那么崎义就会产生,所以这一操作必须由程序员来完成。
使用基类链接(base classchaining)机制用CHAIN_MSG_MAP宏,虽然可以将消息导向父类,但是如果父类派生了多个子类,而每个子类对相同的消息又有不同的处理要求时, CHAIN_MSG_MAP就无能为力了。所以ATL又提供了另一个机制:消息分割(Alternate messagemaps),消息分割可以在父类的消息映射中,将相同的消息分割放置在不同的区域,CownerDraw模板就是采用了这一机制。
在CownerDraw模板的消息映射表里,消息映射被分为两个区域,0号和1号区域。子类要链接到不同区域,需要使用CHAIN_MSG_MAP_ALT宏。CownerDrawButton需要响应CownerDraw的1号区域中的OCM_DRAWITEM消息,就可以在它自己的消息映射表中加入这样一条宏:CHAIN_MSG_MAP_ALT(COwnerDraw<CownerDrawButton>,1)
而其余的消息,它希望由CbitmapButtonImpl模板来处理,仍然可以使用CHAIN_MSG_MAP做基类链接(后面我会提到,实际上不能用CHAIN_MSG_MAP简单地做基类链接)。
CownerDraw模板的这两个消息映射区域唯一的不同是1号区域的消息是以OCM开头的。这就涉及到了ATL的消息反射(MessageReflection)机制。所谓消息反射,就是指窗口类在收到消息时可以将消息反传回去给发出消息的窗口类。比如对于一个自画样式的按钮,它会发出WM_DRAWITEM消息通知父窗口,而父窗口并不处理这个消息而是将它反传回去,让按钮自己处理。显而易见,这种机制更符合面向对象的要求,减少了按钮和父窗口之间的依赖关系。
被父窗口返回的消息代号都是以OCM开头,当我们在父窗口的消息映射表中加入一条REFLECT_NOTIFICATIONS()宏时,父窗口就能够将支持消息反射的控件所发出的消息反传回去,如果控件类或其父类(前提是已经做了基类链接)的消息映射表中有相应消息的反射处理宏,那么控件就会在自己或父类的消息响应函数中处理这条消息。下面让我们来看一看消息分割及反射的具体实现方法。首先在CownerDrawButton的消息映射表中加入如下宏:
CHAIN_MSG_MAP_ALT(COwnerDraw<CownerDrawButton>,1)
然后在框架类的消息映射表中加入REFLECT_NOTIFICATIONS()宏,这样就完成了消息映射。但是需要注意的是,REFLECT_NOTIFICATIONS必须放在消息映射表的最后,否则所有通知消息都将被返回,窗口本身得不任何通知消息,如果你在REFLECT_NOTIFICATIONS宏后面添加一条COMMAND_HANDLER(IDC_BUTTON1, BN_CLICKED,OnClickedButton1) ,那么OnClickedButton1是永远也不会被触发的。当按钮发出WM_DRAWITEM消息时,框架类接到后,先检查自己的消息映射表里是否有相对应的消息处理函数,如果没有那么REFLECT_NOTIFICATIONS就将消息反回给按钮,按钮在消息映射表中找到MESSAGE_HANDLER(OCM_DRAWITEM,OnDrawItem)这一项,宏会将消息映射到OnDrawItem函数,通过调用OnDrawItem函数,完成绘制工作。CownerDraw模板已经为我们实现了OnDrawItem函数,这个函数很简单,代码如下:
LRESULT OnDrawItem(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& bHandled)
{
T* pT = static_cast<T*>(this);
pT->SetMsgHandled(TRUE);
pT->DrawItem((LPDRAWITEMSTRUCT)lParam);
bHandled = pT->IsMsgHandled();
return (LRESULT)TRUE;
}
OnDrawItem函数通过static_cast运算符(静态强制转换)将基类指针转换到派生类指针,然后调用派生的成员函数DrawItem来完成绘制任务。DrawItem是实现自画的关键所在,CownerDraw并没有提供DrawItem的实现,因为它没有办法知道派生类的具体绘制要求,所以DrawItem必须由派生类去实现。CownerDraw模板只提供了一个接口,如果你在派生类中不提供DrawItem的实现,那么在调试的时候,将引发一ATL assert。
三、CBitmapButtonImpl模板
CbitmapButtonImpl为位图按钮提供了支持,使我们不必了解太多实现细节,就可以做漂亮的位图按钮。它还提供了对ToolTip的支持。我们还可以通过重载DoPaint函数来实现个性化。CbitmapButtonImpl定义了一个重要的成员变量m_ImageList,这个成员主要用于位图或图标的管理和绘制。我们将在例程中看到它的使用方法。
四、COwnerDrawButton 类
前面曾经说过,CownerDrawButton类可以通过基类链接的机制,将消息导向其基类,但是如果简单地使用CHAIN_MSG_MAP(CbitmapButtonImpl<CownerDrawButton>)宏,就会出现问题。因为我们实现是的“自画”按钮,所有的绘制工作都应该在DrawItem函数里完成,但是CbitmapButtonImpl并不知道这种情况,所以它仍然响应WM_PAINT、WM_PRINTCLIENT和WM_ERASEBKGND以及其它有关绘制操作的消息,并调用DoPaint等函数进行绘制工作,可想而知这会造成极大的混乱。因此我们必须屏蔽掉CownerDrawButton对这些消息的响应。拷贝CbitmapButtonImpl的所有消息映射表项到CownerDrawButton的消息消息映射表中,然后删除这三行:
MESSAGE_HANDLER(WM_ERASEBKGND, OnEraseBackground)
MESSAGE_HANDLER(WM_PAINT, OnPaint)
MESSAGE_HANDLER(WM_PRINTCLIENT, OnPaint)
再添加CHAIN_MSG_MAP_ALT(COwnerDraw<COwnerDrawButton>,1)这一项,这样窗口默认的绘制消息就不会被触发,问题也得到了解决。但是不要忘记,CownerDrawButton也是一个位图按钮,而CbitmapButtonImpl是在DoPaint函数中实现位图的显示,如果不响应WM_PAINT或者WM_PAINTCLIENT消息,DoPaint是不会被调用的。显然如果CownerDrawButton的m_ImageList成员包含图片的话,我们就需要自己在DrawItem函数里实现位图的显示,当然我们也可以在需要显示位图的时候简单地调用DoPaint函数,只要为它传递一个CDCHandle,DoPaint就会非常好地完成任务,实际上我就是这么做的。不过要想实现图1所显示的按钮,CbitmapButtonImpl提供的DoPaint函数是没有办法办到的。为CownerDrawButton声明一个成员变量m_uBmpPosStyle,当m_ImageList包含图像时,这个变量就被设置,用于存储图像的具体位置。图像的位置被声明为五个无符号的整型常量如下所示:
unsigned int const IMAGEPOS_TOP   = 1 ;
unsigned int const IMAGEPOS_BOTTOM = 2 ;
unsigned int const IMAGEPOS_LEFT  = 3 ;
unsigned int const IMAGEPOS_RIGHT  = 4 ;
unsigned int const IMAGEPOS_CENTER  = 5 ;
只有当m_ImageList包含图像,图像的扩展样式不是自动尺寸(BMPBTN_AUTOSIZE),并且图像尺寸小于按钮客户区域时,这些样式才有效。还要声明一个CRect类型的成员变量m_ClientRect,用于记录客户区域的尺寸,每当我们显示完位图之后,就对m_ClientRect区域进行裁剪,以便于文本的排布。为了应用这些样式,以及对m_ClientRect成员进行修改,必须对DoPaint进行重载。把CbitmapButtonImpl模板的DoPaint源码拷贝到CownerDrawButton的DoPaint中,然后此基础上进行修改。修改后的DoPaint函数对位图尺寸和客户区域进行比较,如果图片尺寸小于客户区域,则再根据m_uBmpPosStyle设置的样式绘制位图,最后对m_ClientRect进行裁剪,以便于文本布局。在DrawItem函数中,通过对m_ImageList 是否包含图片及是否设置主图进行判断,来决定是否调用DoPaint进行图片的显示。如果没有图片则执行缺省的绘制,并在客户区域中央显示文本。
为了便于位图资源的导入,CownerDrawButton提供了一个LoadImageFromID函数,原型为:
BOOL LoadImageFromID(UINT IDBitmap ,UINT IDMask, const IMGINFOS & imgno);
其第三个参数是一个自定义类型的结构,包含了按钮的图像列表成员中包含的各个图像的状态信息、图像的尺寸、图像类型标志、图像列表中初始图像个数和最大图像个数等。结构的声明及函数实现如下:
typedef struct _imageinfo{
int Normal ;
int Pushed ;
int Hover ;
int Disabled ;
int cx;
int cy;
UINT flags;
int cInitial;
int cGrow;
}IMAGELISTINFOSTRUCT;
typedef IMAGELISTINFOSTRUCT IMGINFOS;
file://Load bitmap from resource Id
BOOL LoadImageFromID(UINT IDBitmap ,UINT IDMask, const IMGINFOS & imgno)
{
if(!m_ImageList.Create( imgno.cx,imgno.cy,imgno.flags ,imgno.cInitial,imgno.cGrow))
return FALSE;
CBitmap m_Mask,m_bbmp;
if(!m_bbmp.LoadBitmap(IDBitmap))
return FALSE;
if(!m_Mask.LoadBitmap(IDMask))
return FALSE;
if((m_ImageList.Add(m_bbmp,m_Mask) == -1))
return FALSE;
SetImages(imgno.Normal,imgno.Pushed ,imgno.Hover,imgno.Disabled);
return TRUE;
}
五、例程
启动VC++6.0,创建一个基于WTL对话框的应用程序,工程名为OwnerDrawDemo创建完成后,打开ClassView,选择CmainDlg,单击鼠标右键,选择Add Member Variable为CmainDlg类添加一个CownerDrawButton成员变量。打开ResourceView,在对话框资源模板上添加一个按钮,调整到合适尺寸,ID为IDC_BUTTON1,Caption 为Help。导入一幅位图和一幅相应的Mask图,修改ID分别为:IDB_BUTTON、IDB_MASK。
打开FileView,打开OwnerDrawDemo.cpp在其顶部依次添加#include <atlctrlx.h>、
#include <atlgdi.h> 和 #include <atlmisc.h>。打开maindlg.h文件,在OnInitDialog函数中添加如下代码:
DWORD style =  BMPBTN_AUTO3D_SINGLE|BMPBTN_SHAREIMAGELISTS|
BMPBTN_HOVER;
IMGINFOS imgis = {0,1,1,-1,30,30,ILC_COLOR24|ILC_MASK,0,2};
if(m_Button.LoadImageFromID(IDB_BUTTON,IDB_MASK,imgis))
{
m_Button.SetBitmapButtonExtendedStyle(style);
m_Button.SetBitmapPosStyle(IMAGEPOS_TOP);
}
m_Button.SubclassWindow(GetDlgItem(IDC_BUTTON1));
m_Button.SetToolTipText(_T("WTL_OwnerDrawButton!"));
现在就可以按F7构建或者Ctrl + F5执行了。
程序在Windows2000 + VC++6.0 +WTL 7.0 环境下编译通过,在Windows98、Windows 2000及WindowsXP下运行通过。