摘要:讨论Active Template Library (ATL) 3.0中的一些类,这些类围绕着Windows API建立了一个面向对象的编程框架,使用这个框架,可以简化Microsoft® Windows®编程并且只需要很少的系统开销。内容包括:考察对窗口做了简单封装的CWindow类;使用CWindowImpl进行消息处理和消息映射;使用ATL中的对话框类以及扩展现有窗口类的功能的方法。
简介:虽然Active Template Library (ATL)主要是为了支持COM开发而设计的,但它确实包含了很多可用于窗口设计的类。这些窗口类和ATL中的其它类一样,都是基于模版的,并且只需要花费很少系统开销。这篇文章就向我们演示了使用ATL创建窗口和对话框并进行消息处理的基本方法。
这篇文章假设读者熟悉C++语言和Windows程序设计;但是并不一定要求读者具有COM方面的知识。
CWindow:在ATL窗口类中,CWindow是最基本的。这个类对Windows API进行了面向对象的包装,它封装了一个窗口句柄,并提供一些成员函数来操作它,这些函数包装了相应的Windows API。
标准的Windows程序设计看起来象这样:
HWND hWnd = ::CreateWindow( "button", "Click me",
WS_CHILD, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, NULL, NULL, hInstance, NULL );
::ShowWindow( hWnd, nCmdShow );
::UpdateWindow( hWnd );
使用ATL中的CWindow类后,等效代码如下:
CWindow win;
win.Create( "button", NULL, CWindow::rcDefault, "Click me",
WS_CHILD );
win.ShowWindow( nCmdShow );
win.UpdateWindow();
我们应该在我们的大脑中我们应该保持这样一个概念:ATL的窗口对象与Windows系统中的窗口是不同的。Windows系统中的窗口指的是操作系统中维持的一块数据,操作系统靠这块数据来操作屏幕上的一块区域。而一个ATL窗口对象,是CWindow类的一个实例,它是一个C++对象,它的内部没有保存任何有关屏幕区域或者窗口数据结构的内容,只保存了一个窗口的句柄,这个句柄保存在它的数据成员m_hWnd中,CWindow对象和它在屏幕上显示出来的窗口就是靠这个句柄联系起来的。
理解了ATL中的窗口对象和Windows系统中窗口的区别,就更加容易理解CWindow对象的构造与窗口的创建是两个分开的过程。我们再看看前面的代码,就会发现,首先是一个CWindow对象被构造:
CWindow win;
然后创建它的窗口:
win.Create( "button", NULL, CWindow::rcDefault, "Click me",
WS_CHILD );
我们也可以构造一个CWindow对象,然后把它和一个已经存在的窗口关联起来,这样我们就可以通过CWindow类的成员函数来操作这个已经存在的窗口。这种方法非常有用,因为CWindow类提供的函数都是封装好了的,用起来很方便,比如CWindow类中的CenterWindow, GetDescendantWindow等函数用起来就比直接使用Windows API方便得多。
HWND hWnd = CreateWindow( szWndClass, "Main window",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, NULL, NULL, hInstance, NULL );
// 下面的方法中可以任选一种:
// CWindow win( hWnd ); // 通过构造函数关联
// 或
// CWindow win;
// win = hWnd; // 通过赋值操作符关联
// 或
// CWindow win;
// win.Attach( hWnd ); // 使用Attach()方法关联
win.CenterWindow(); // 现在可以使用win对象来代替hWnd进行操作
win.ShowWindow( nCmdShow );
win.UpdateWindow();
CWindow类也提供了一个HWND操作符,可以把CWindow类的对象转化为窗口句柄,这样,任何要求使用HWND的地方都可以使用CWindow类的对象代替:
::ShowWindow( win, nCmdShow ); // 此API函数本来要求HWND类型的参数
CWindow类使得对窗口的操作更简单,而且不会增加系统开销——它经过编译和优化后的代码与使用纯API编程的代码是等价的。
不幸的是,CWindow类不能让我们自己决定窗口如何响应消息。当然,我们可以使用CWindow类提供的方法来使一个窗口居中或隐藏,甚至可以向一个窗口发送消息,但是当窗口收到消息后怎么处理则取决于创建这个窗口时使用的窗口类,如果我们是创建的是”button”类的窗口,那么它的表现就象个按钮,如果用”listbox”类创建,那它就具有跟列表框相同的行为,使用CWindow类我们没有办法改变这点。幸好,ATL为我们提供了另外一个类CWindowImpl,它允许我们指定窗口的新行为。
CWindowImpl:CWindowImpl类是从CWindow类派生的,所以我们依然可以使用CWindow类中的成员函数,但是CWindowImpl类的功能更强大,它允许我们指定窗口怎样处理消息。在传统的窗口编程中,如果我们要处理窗口消息,我们必须使用窗口函数;但是使用ATL,我们只需要在我们的ATL窗口类中定义一个消息映射。
首先,从CWindowImpl类派生自己的窗口类,如下:
class CMyWindow : public CWindowImpl
{
注意,我们自己的类名必须作为一个模版参数传递给CWindowImpl类。
然后在类的定义里面定义如下的消息映射:
BEGIN_MSG_MAP(CMyWindow)
MESSAGE_HANDLER(WM_PAINT,OnPaint)
MESSAGE_HANDLER(WM_CREATE,OnCreate)
MESSAGE_HANDLER(WM_DESTROY,OnDestroy)
END_MSG_MAP()
下面这句
MESSAGE_HANDLER(WM_PAINT,OnPaint)
的意思是,当WM_PAINT消息到达时,将调用CMyWindow::OnPaint成员函数。
最后就是定义处理消息的函数了,如下:
LRESULT OnPaint(
UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{ ...
}
LRESULT OnCreate(
UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{ ...
}
LRESULT OnDestroy(
UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
{ ...
}
}; // CmyWindow
这些函数中的参数意义为:第一个是消息ID,中间的两个参数的意义取决于消息类型,第四个参数是一个标志,用它来决定这个消息是已经处理完了还是需要进一步的处理。关于这些参数,我们在Message Map小结有更详细的讨论。
当窗口收到一个消息,它将从消息映射表的顶部开始查找匹配的消息处理函数,因此把最常用的消息放在消息映射表的前面是个不错的注意。如果没有找到匹配的消息处理函数,则这个消息被发送到默认的窗口过程进行处理。
ATL的消息映射表封装了Windows的消息处理过程,它比传统的窗口函数中的大量switch分支或者if语句看起来更加直观。
要创建一个基于CWindowImpl派生类的窗口,请调用CWindowImpl类的Create方法:
CMyWindow wnd; // 构造一个 CMyWindow 类的对象
wnd.Create( NULL, CWindow::rcDefault, _T("Hello"),
WS_OVERLAPPEDWINDOW|WS_VISIBLE );
注意,CWindowImpl类的Create方法与CWindow类的Create方法略有不同,在CWindow类的Create中,我们必须指定一个注册了的窗口类,但是CWindowImpl则不同,它创建一个新的窗口类,因此,不需要为它指定窗口类。
一个简单而完整的示例:
这篇文章中的大部分示例都只是代码片段,但是下面列出的是一个完整的Hello world的示例程序。虽然我们使用的是ATL,但是没有涉及到COM,因此在使用Visual C++®建立项目的时候,我们选择Win32® application而不是ATL COM:
在stdafx.h文件中,加入下面几行:
#include <atlbase.h>
extern CComModule _Module;
#include <atlwin.h>
在hello.cpp文件中,写如下代码:
#include "stdafx.h"
CComModule _Module;
class CMyWindow : public CWindowImpl<CMyWindow> {
BEGIN_MSG_MAP( CMyWindow )
MESSAGE_HANDLER( WM_PAINT, OnPaint )
MESSAGE_HANDLER( WM_DESTROY, OnDestroy )
END_MSG_MAP()
LRESULT OnPaint( UINT, WPARAM, LPARAM, BOOL& ){
PAINTSTRUCT ps;
HDC hDC = GetDC();
BeginPaint( &ps );
TextOut( hDC, 0, 0, _T("Hello world"), 11 );
EndPaint( &ps );
return 0;
}
LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& ){
PostQuitMessage( 0 );
return 0;
}
};
int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE, LPSTR, int )
{
_Module.Init( NULL, hInstance );
CMyWindow wnd;
wnd.Create( NULL, CWindow::rcDefault, _T("Hello"),
WS_OVERLAPPEDWINDOW|WS_VISIBLE );
MSG msg;
while( GetMessage( &msg, NULL, 0, 0 ) ){
TranslateMessage( &msg );
DispatchMessage( &msg );
}
_Module.Term();
return msg.wParam;
}
在这个示例程序中,CmyWindow是从CWindowImpl派生的,它的消息映射捕获了两个消息WM_PAINT和WM_DESTROY,当收到WM_PAINT消息时,它的成员函数OnPaint处理这个消息并在窗口上输出“Hello world”,当收到WM_DESTROY消息时,也就是当用户关闭这个窗口的时候,调用OnDestroy函数处理这个消息,在OnDestroy函数中调用PostQuitMessage来结束消息循环。
WinMain函数中创建了一个CmyWindow类的实例并实现了一个标准的消息循环。(有一些地方,我们必须遵循ATL的规范,比如在这里我们必须使用_Module。)
消息映射:
有三组用于消息映射的宏,他们分别是:
- 窗口消息映射宏,用于所有的窗口消息(如WM_CREATE、WM_PAINT等);
- 命令消息映射宏,专用于WM_COMMAND消息(比如由控件或菜单发出的消息);
- 通知消息映射宏,专用于WM_NOTUFY消息(通常由通用控件发出此消息,比如工具栏控件或列表视图控件)
窗口消息映射宏:
有两个窗口消息映射宏,他们分别是:
- MESSAGE_HANDLER
- MESSAGE_RANGE_HANDLER
第一个宏将一个特定的消息映射到相应的处理函数;第二个宏将一组消息映射到一个处理函数。消息处理函数都要求具有如下的原形:
LRESULT MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
其中,参数uMsg是消息标识,wParam和lParam是两个附加与消息的参数,(他们的具体意义取决与消息类别。)
消息处理函数使用bHandled来标志消息是否已经被完全捕获,如果bHandled被设置成FALSE,程序将继续在消息映射表的后续部分查找这个消息的其它处理函数。这个特性使得我们对一个消息使用多个处理函数成为可能。什么时候需要对一个消息使用多个处理函数呢?可能是在对多个类链接时,也可能是我们只想对一个消息做出响应但是并不真正捕获它。在处理函数被调用之前,bHandled被置为TRUE,所以如果我们不在函数的结尾显式地将它置为FALSE,则消息映射表的后续部分不会被继续查找,也不会有其它的处理函数被调用。
命令消息映射宏:
命令消息映射宏只处理命令消息(WM_COMMAND消息),但是它能让我们根据消息类型或者发送命令消息的控件ID来指定消息处理函数。
- COMMAND_HANDLER映射一个特定控件的一条特定消息到一个处理函数;
- COMMAND_ID_HANDLER映射一个特定控件的所有消息到一个处理函数;
- COMMAND_CODE_HANDLER映射任意控件的一个特定消息到一个处理函数;
- COMMAND_RANGE_HANDLER映射一定范围内的控件的所有消息到一个处理函数;
- COMMAND_RANGE_CODE_HANDLER映射一定范围内的控件的一条特定消息到一个处理函数。
命令消息处理函数应该具有如下的原形:
LRESULT CommandHandler(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled);
其中,参数wNotifyCode代表消息代码,wID代表发送消息的控件的ID,hWndCtl代表发送消息的控件的窗口句柄,bHandled的意义如前所述。
通知消息映射宏:
通知消息映射宏用来处理通知消息(WM_NOTUFY消息),它根据通知消息的类型和发送通知消息的控件的不同将消息映射到不同的处理函数,这些宏与前面讲的命令消息映射宏是等价的,唯一的不同就是它处理的是通知消息而不是命令消息。
- NOTIFY_HANDLER
- NOTIFY_ID_HANDLER
- NOTIFY_CODE_HANDLER
- NOTIFY_RANGE_HANDLER
- NOTIFY_RANGE_CODE_HANDLER
通知消息处理函数都需要如下的原形:
LRESULT NotifyHandler(int idCtrl, LPNMHDR pnmh, BOOL& bHandled);
其中,参数idCtrl代表发送通知消息的控件的ID,参数pnmh是指向一个NMHDR结构的指针,bHandled的意义如前所述。
通知消息包含了一个指向消息细节的结构的指针,例如,当一个列表视图控件发送一个通知消息,这个消息就包含了一个指向NMLVDISPINFO结构的指针,所有类似于NMLVDISPINFO的结构都包含一个NMHDR结构的头,pnmh就指向这个头,如果需要访问这种结构中头部以外的其它数据成员,可以将pnmh转化成相应类型的指针。
例如,我们如果要处理列表视图控件发出的LVN_ENDLABELEDIT通知消息,我们可以把下面这行代码放到消息映射表中:
NOTIFY_HANDLER( ID_LISTVIEW, LVN_ENDLABELEDIT, OnEndLabelEdit)
这个通知消息附带的额外信息包含在一个NMLVDISPINFO结构中,因此,消息处理函数看起来应该象下面这个样子:
LRESULT OnEndLabelEdit(int idCtrl, LPNMHDR pnmh, BOOL& bHandled)
{
// The item is -1 if editing is being canceled.
if ( ((NMLVDISPINFO*)pnmh)->item.iItem == -1) return FALSE;
...
可以看出,pnmh指针被转化成NMLVDISPINFO*类型,以便访问头部结构以外的数据。
为现有的窗口类添加功能:有许多向现有的窗口添加功能的方法。如果这个类是ATL窗口类,我们可以从这个窗口类派生自己的类,就象Base Class Chaining中描述的一样。这种方法主要是一个C++类的继承加上一点消息映射的链接。
如果我们想扩展一个预定义的窗口类(如按纽类或列表框类)的功能,我们可以超类化它。就是创建一个基于这个预定义类的新类,并在消息映射表中添加消息映射以增强它的功能。
有些时候,我们需要改变一个已经存在的窗口实例的行为,而不是一个窗口类——或许我们要让一个对话框上的编辑框做点什么特别的事情。在这种情况下,我们可以写一个新的ATL窗口类,并子类化这个已经存在的编辑框。任何本该发送到这个编辑框的消息都会先被发送到这个子类的对象。
另外一种可选的方法:我们也可以让这个编辑框成为一个被包含的窗口,所有发送到这个编辑框的消息都会经过它的容器窗口;我们可以在这个容器窗口中为这个被包含的窗口实现特殊的消息处理。
最后的一种方法就是消息反射,当一个窗口收到一个消息后不处理它,而是反射给发送这个消息的窗口自己处理,这种技术可以用来创建自包含的控件。
基类消息链(Base Class Chaining):如果我们已经有一些实现了特定功能的ATL窗口类,我们可以从它们派生新类以充分利用继承的优点。比如:
class CBase: public CWindowImpl< CBase >
// simple base window class: shuts down app when closed
{
BEGIN_MSG_MAP( CBase )
MESSAGE_HANDLER( WM_DESTROY, OnDestroy )
END_MSG_MAP()
LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& )
{
PostQuitMessage( 0 );
return 0;
}
};
class CDerived: public CBase
// derived from CBase; handles mouse button events
{
BEGIN_MSG_MAP( CDerived )
MESSAGE_HANDLER( WM_LBUTTONDOWN, OnButtonDown )
END_MSG_MAP()
LRESULT OnButtonDown( UINT, WPARAM, LPARAM, BOOL& )
{
ATLTRACE( "button down\n" );
return 0;
}
};
// in WinMain():
...
CDerived win;
win.Create( NULL, CWindow::rcDefault, "derived window" );
可是,上面的代码有一个问题。当我们在调试模式下运行这个程序,一个窗口出现了,如果我们在这个窗口中单击,“button down”将出现在输出窗口中,这是CDrived类的功能,可是,当我们关闭这个窗口的时候,程序并不退出,尽管CBase类处理了WM_DESTROY消息并且CDrived类是从CBase类派生的。
Why?因为我们必须明确地将一个消息映射表链接到另外一个。如下:
BEGIN_MSG_MAP( CDerived )
MESSAGE_HANDLER( WM_LBUTTONDOWN, OnButtonDown )
CHAIN_MSG_MAP( CBase ) // 链接到基类
END_MSG_MAP()
现在,任何在CDrived类中没有被处理的消息都会被传到CBase类中。
为什么不自动将派生类的消息映射和它的基类的消息映射链接起来呢?这是因为在ATL的体系结构中有很多多重继承的情况,这种情况下没有办法知道究竟应该链接到哪个基类,所以只好让程序员自己来做决定。
可选的消息映射:
消息映射链允许多个类同时进行消息处理,同时也带来了问题:如果我们在多个类中都要响应WM_CREATE消息,但是不同的类需要基类提供不同的处理,怎么办呢?为了解决这个问题,ATL使用了可选的消息映射:将消息映射表分成很多节,每一节用不同的数字标识,每一节都是一个可选的消息映射表。
// in class CBase:
BEGIN_MSG_MAP( CBase )
MESSAGE_HANDLER( WM_CREATE, OnCreate1 )
MESSAGE_HANDLER( WM_PAINT, OnPaint1 )
ALT_MSG_MAP( 100 )
MESSAGE_HANDLER( WM_CREATE, OnCreate2 )
MESSAGE_HANDLER( WM_PAINT, OnPaint2 )
ALT_MSG_MAP( 101)
MESSAGE_HANDLER( WM_CREATE, OnCreate3 )
MESSAGE_HANDLER( WM_PAINT, OnPaint3 )
END_MSG_MAP()
如上,基类的消息映射表由3节组成:一个默认的消息映射表(隐含的标识为0)和两个可选的消息映射表(标识为100和101)。
当你链接消息映射表时,指定你所希望的方案的标识,如下:
class CDerived: public CBase {
BEGIN_MSG_MAP( CDerived )
CHAIN_MSG_MAP_ALT( CBase, 100 )
END_MSG_MAP()
...
CDrived类的消息映射表链接到CBase类中标识号为100的可选节,因此当WM_PAINT到达时,CBase::OnPaint2被调用。
(译者注:我觉得这种方法不太合乎C++的思想,基类的编写者不一定总能知道派生自它的类会有哪些需求,而且把所有不同的版本都在基类中实现,基类中无用的代码量会大大增加。更好的办法应该是把基类中的消息处理函数声明为虚函数。总之,我觉得这一小节并不能体现出可选消息映射的真正用途。)
其它类型的链:
除了基类消息映射链,ATL也提供了成员链(member chaining)和动态链(dynamic chaining),这些很少使用到的链技术超出了我们这篇文章的讨论范围,但是可以简单提一下。成员链允许把消息映射链接到一个类的成员变量,动态链允许在运行时进行动态链接。如果你想了解更多,请参考ATL文档中的CHAIN_MSG_MAP_DYNAMIC 和CHAIN_MSG_MAP_MEMBER的相关内容。
窗口的超类化:超类化定义一个类,并为预定义的窗口类(如按钮类或列表框类)添加新的功能,下面的例子超类化一个按钮,让这个按钮在被单击的时候发出蜂鸣。
class CBeepButton: public CWindowImpl< CBeepButton >
{
public:
DECLARE_WND_SUPERCLASS( _T("BeepButton"), _T("Button") )
BEGIN_MSG_MAP( CBeepButton )
MESSAGE_HANDLER( WM_LBUTTONDOWN, OnLButtonDown )
END_MSG_MAP()
LRESULT OnLButtonDown( UINT, WPARAM, LPARAM, BOOL& bHandled )
{
MessageBeep( MB_ICONASTERISK );
bHandled = FALSE; // alternatively: DefWindowProc()
return 0;
}
}; // CBeepButton
DECLARE_WND_SUPERCLASS宏声明了这个窗口的类名(“BeepButton”)和被超类化的类名(“Button”)。它的消息映射表只有一个入口项,将WM_LBUTTONDOWN消息映射到OnLButtonDown函数。其余的消息都让默认的窗口过程处理,除了可以发出蜂鸣外,CbeepButton需要和其它的按钮表现相同,因此在OnLButtonDown函数的最后,需要将bHandled设置为FALSE,让默认的窗口过程在OnLButtonDown函数完成后对WM_LBUTTONDOWN消息进行其它的处理。(另外的一种方法是直接调用DefWindowProc函数。)
到目前为止,我们所做的只是定义了一个新类;我们依然需要创建一些真正的CbeepButton窗口,下面的类定义了两个CbeepButton类型的成员变量,因此,当这个类的窗口被创建时,将会创建两个CbeepButton类型的子窗口。
const int ID_BUTTON1 = 101;
const int ID_BUTTON2 = 102;
class CMyWindow: public CWindowImpl< CMyWindow, CWindow,
CWinTraits<WS_OVERLAPPEDWINDOW|WS_VISIBLE> >
{
CBeepButton b1, b2;
BEGIN_MSG_MAP( CMyWindow )
MESSAGE_HANDLER( WM_CREATE, OnCreate )
COMMAND_CODE_HANDLER( BN_CLICKED, onClick )
END_MSG_MAP()
LRESULT onClick(WORD wNotifyCode, WORD wID, HWND hWndCtl,
BOOL& bHandled)
{
ATLTRACE( "Control %d clicked\n", wID );
return 0;
}
LRESULT OnCreate( UINT, WPARAM, LPARAM, BOOL& )
{
RECT r1 = { 10, 10, 250, 80 };
b1.Create(*this, r1, "beep1", WS_CHILD|WS_VISIBLE, 0, ID_BUTTON1);
RECT r2 = { 10, 110, 250, 180 };
b2.Create(*this, r2, "beep2", WS_CHILD|WS_VISIBLE, 0, ID_BUTTON2);
return 0;
}
}; // CMyWindow
窗口的子类化:子类化允许我们改变一个已经存在的窗口的行为,我们经常用它来改变控件的行为。它的实现机制是插入一个消息映射表来截取发向控件的消息。举例说明:假设有一个对话框,对话框上有一个编辑框控件,我们想让这个控件只接受不是数字的字符。我们可以截获发往这个控件的WM_CHAR消息并抛弃接收到的数字字符。下面的类实现这个功能:
class CNoNumEdit: public CWindowImpl< CNoNumEdit >
{
BEGIN_MSG_MAP( CNoNumEdit )
MESSAGE_HANDLER( WM_CHAR, OnChar )
END_MSG_MAP()
LRESULT OnChar( UINT, WPARAM wParam, LPARAM, BOOL& bHandled )
{
TCHAR ch = wParam;
if( _T(''0'') <= ch && ch <= _T(''9'') )
MessageBeep( 0 );
else
bHandled = FALSE;
return 0;
}
};
这个类只处理一个消息WM_CHAR,如果这个字符是数字的话,则调用MessageBeep( 0 )并返回,这样可以有效地忽略这个字符。如果不是数字,则将bHandled设置为FALSE,指明默认的窗口过程这个消息需要进一步处理。
现在我们将子类化一个编辑框控件,以便CnoNumEdit能够抢先处理发到这个编辑框得消息。(下面得例子用到了CdialogImpl类,这个类我们将在ATL中的对话框类一节中介绍。)在这个例子中,CmyDialog类中用到了一个对话框资源(ID号为IDD_DIALOG1),对话框中有一个编辑框控件(ID号为IDC_EDIT1),当对话框初始化的时候,编辑框经过SubclassWindow而变成一个不接受数字的编辑框:
class CMyDialog: public CDialogImpl<CMyDialog>
{
public:
enum { IDD = IDD_DIALOG1 };
BEGIN_MSG_MAP( CMyDialog )
MESSAGE_HANDLER( WM_INITDIALOG, OnInitDialog )
END_MSG_MAP()
LRESULT OnInitDialog( UINT, WPARAM, LPARAM, BOOL& )
{
ed.SubclassWindow( GetDlgItem( IDC_EDIT1 ) );
return 0;
}
CNoNumEdit ed;
};