ATL3.0中的窗口类 原文出处: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnvc60/html/atlwindow.asp 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来结束消息循环。
窗口消息映射宏:
第一个宏将一个特定的消息映射到相应的处理函数;第二个宏将一组消息映射到一个处理函数。消息处理函数都要求具有如下的原形: LRESULT MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled); 其中,参数uMsg是消息标识,wParam和lParam是两个附加与消息的参数,(他们的具体意义取决与消息类别。)
命令消息处理函数应该具有如下的原形: LRESULT CommandHandler(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled); 其中,参数wNotifyCode代表消息代码,wID代表发送消息的控件的ID,hWndCtl代表发送消息的控件的窗口句柄,bHandled的意义如前所述。
通知消息处理函数都需要如下的原形: 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; } }; // CBeepButtonDECLARE_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; };被包含的窗口: 一个被包含的窗口是一个不响应任何消息的窗口,它将收到的所有消息重新发送到另外一个窗口的消息映射,这个另外的窗口就是它的容器窗口。通常情况下,被包含的窗口是它的容器窗口的子窗口,但情况并不是总是这样。容器窗口并不是必须等同于父窗口,包含与被包含的关系取决于C++类,被包含的窗口是容器窗口类的一个数据成员,而父窗口和子窗口的关系体现在屏幕上,它们的关系是创建窗口时确定的。 一个被包含的窗口建立在已注册的窗口类的基础之上,比如编辑框控件。如果一个编辑框被包含,那么发送到它的消息实际上被它的容器窗口的消息映射处理。使用这种方法,可以改变编辑框控件的标准行为。这有点类似于子类化但是不需要定义新类来子类化控件。和前面那个定义CnoNumEdit类响应WM_CHAR消息的例子相比,处理WM_CHAR消息的容器窗口类看起来如下: class CMyWindow: public CWindowImpl { CContainedWindow m_contained; public: CMyWindow(): m_contained( _T("edit"), this, 99 ) { } ...CmyWindow是一个容器窗口类,它的构造函数对CcontainedWindow类型的成员做这样的初始化:被包含的窗口是编辑框,发送它的消息到“this”(它的父窗口),使用可选消息映射表99。 BEGIN_MSG_MAP( CMyWindow ) MESSAGE_HANDLER( WM_CREATE, OnCreate ) MESSAGE_HANDLER( WM_DESTROY, OnDestroy ) ALT_MSG_MAP( 99 ) // contained window''s messages come here... MESSAGE_HANDLER( WM_CHAR, OnChar ) END_MSG_MAP()当父窗口被创建的时候,被包含的窗口也被创建(在WM_CREATE消息的响应函数中)。因为被包含的控件是以编辑框为基础的,所以它在屏幕上看起来象一个编辑框: LRESULT OnCreate( UINT, WPARAM, LPARAM, BOOL& ) { RECT rc = { 10, 10, 200, 35 }; m_contained.Create( *this, rc, _T("non-numeric edit"), WS_CHILD|WS_VISIBLE|WS_BORDER, 0, 666 ); return 0; }在这个例子中,容器窗口同时也是被包含窗口的父窗口。 当被包含的窗口收到WM_CHAR消息时,容器窗口的OnChar成员函数被调用。这个函数和前面的CnoNumEdit例子中的相同,但是在这个例子中,它时容器类的成员函数。 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; } LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& ) { PostQuitMessage( 0 ); return 0; } };我们同样也可以用被包含的窗口来子类化对话框中已经存在的控件,和正规的子类化不同,被子类化的窗口的消息时被容器窗口捕获的。在下面的例子中,一个对话框子类化了一个编辑框控件,把它转化成了被包含的窗口;那个对话框(容器)捕获WM_CHAR消息并忽略掉数字字符,然后在发送到编辑框控件。(CdialogImpl在ATL中的对话框类一节讲述。) class CMyDialog: public CDialogImpl<CMyDialog> { public: enum { IDD = IDD_DIALOG1 }; // contained window is an edit control: CMyDialog(): m_contained( "edit", this, 123 ) { } BEGIN_MSG_MAP( CMyDialog ) MESSAGE_HANDLER( WM_INITDIALOG, OnInitDialog ) ALT_MSG_MAP( 123 ) // contained window''s messages come here... MESSAGE_HANDLER( WM_CHAR, OnChar ) END_MSG_MAP() LRESULT OnInitDialog( UINT, WPARAM, LPARAM, BOOL& bHandled ) { // when the dialog box is created, subclass its edit control: m_contained.SubclassWindow( GetDlgItem(IDC_EDIT1) ); bHandled = FALSE; return 0; } 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; } CContainedWindow m_contained; };消息反射: 前面讲述了一些扩展窗口功能的方法,这些方法是通过使窗口响应发往窗口的消息实现的。和前面的方法相反,消息反射是使窗口能够响应从它们自己发出的消息。 当用户和控件交互的时候,控件通常使发送一个WM_COMMAND或者WM_NOTIFY消息给它的父窗口;然后父窗口做出响应,比如: class CParentWindow: CWindowImpl<CParentWindow> { // 假设这个窗口有一个按钮型的子窗口, // 并且其 ID 为 ID_BUTTON BEGIN_MSG_MAP( CParentWindow ) COMMAND_ID_HANDLER( ID_BUTTON, OnButton ) MESSAGE_HANDLER( WM_CTLCOLORBUTTON, OnColorButton ) ...当按钮被按下的时候,它发送一个命令消息给父窗口,然后CParentWindow::OnButton被调用。同理,当按钮需要被绘制的时候,它发送WM_CTLCOLORBUTTON消息给父窗口,CParentWindow::OnColorButton响应这个消息,它使用特定的画刷绘制控件。 某些情况下,让控件自己响应它发送出去的消息比让父窗口响应要好得多。ATL提供了消息反射的机制:当控件向父窗口发送消息的时候,父窗口能够将消息反射给控件。 class CParentWindow: CWindowImpl { BEGIN_MSG_MAP( CParentWindow ) MESSAGE_HANDLER( WM_CREATE, OnCreate ) MESSAGE_HANDLER( WM_DESTROY, OnDestroy ) ...other messages that CParentWindow will handle... REFLECT_NOTIFICATIONS() END_MSG_MAP() ...当父窗口收到一个消息,先查找它的消息映射表,如果没有和这个消息相匹配的入口,则REFLECT_NOTIFICATIONS宏使得该消息被反射给发送这个消息的控件。控件可以提供响应反射消息的处理函数,如下: class CHandlesItsOwnMessages: CWindowImpl<CHandlesItsOwnMessage> { public: DECLARE_WND_SUPERCLASS( _T("Superbutton"), _T("button") ) BEGIN_MSG_MAP( CHandlesItsOwnMessage ) MESSAGE_HANDLER( OCM_COMMAND, OnCommand ) MESSAGE_HANDLER( OCM_CTLCOLORBUTTON, OnColorButton ) DEFAULT_REFLECTION_HANDLER() END_MSG_MAP() ...注意,反射消息的消息标志以OCM_开头,而不是WM_。这可以让你区分这个消息究竟是否是被反射回来的。 这个控件要么是这个类的实例,要么是一个被子类化的按钮控件。例如: // in CParentWindow: CHandlesItsOwnMessages m_button; LRESULT OnCreate( UINT, WPARAM, LPARAM, BOOL& ) { RECT rc; // initialize appropriately m_button.Create( *this, rc, _T("click me"), WS_CHILD|WS_VISIBLE ); ...或者,如果这个按钮控件是已存在的(例如,父窗口是一个对话框): m_button.SubclassWindow( GetDlgItem(ID_BUTTON) );下面的例子定义了一个CstaticLink类,它是一个Static控件,当点击它的时候,将打开一个指定的网页。所有从CstaticLink发送出去的消息都被它的父窗口反射回来(在这个例子中,用到对话框,请看ATL中的对话框类这一节)。除了响应反射回的命令消息,CstaticLink还处理反射回的WM_CTLCOLORSTATIC消息以便它能够让自己在点击前和点击后显示不同的颜色。 #include "stdafx.h" #include "resource.h" CComModule _Module; class CStaticLink : public CWindowImpl<CStaticLink> { /* Based on CStaticLink by Paul DiLascia, C++ Q&A, Microsoft Systems Journal 12/1997. Turns static controls into clickable "links" -- when the control is clicked, the file/program/webpage named in the control''s text (or set by SetLinkText()) is opened via ShellExecute(). Static control can be either text or graphic (bitmap, icon, etc.). */ public: DECLARE_WND_SUPERCLASS( _T("StaticLink"), _T("Static") ) CStaticLink() : m_colorUnvisited( RGB(0,0,255) ), m_colorVisited( RGB(128,0,128) ), m_bVisited( FALSE ), m_hFont( NULL ) { } void SetLinkText( LPCTSTR szLink ) { USES_CONVERSION; m_bstrLink = T2OLE( szLink ); } BEGIN_MSG_MAP(CStaticLink) // uses message reflection: WM_* comes back as OCM_* MESSAGE_HANDLER( OCM_COMMAND, OnCommand ) MESSAGE_HANDLER( OCM_CTLCOLORSTATIC, OnCtlColor ) MESSAGE_HANDLER( WM_DESTROY, OnDestroy ) // not a reflected message DEFAULT_REFLECTION_HANDLER() END_MSG_MAP() LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& ) { if( m_hFont ) DeleteObject( m_hFont ); return 0; } LRESULT OnCommand( UINT, WPARAM wParam, LPARAM, BOOL& ) { USES_CONVERSION; int code = HIWORD( wParam ); if( code == STN_CLICKED || code == STN_DBLCLK ){ if( m_bstrLink.Length() == 0 ){ GetWindowText( &m_bstrLink ); } if( (int)ShellExecute( *this, _T("open"), OLE2T(m_bstrLink), NULL, NULL, SW_SHOWNORMAL ) > 32 ){ m_bVisited = TRUE; // return codes > 32 => success Invalidate(); }else{ MessageBeep( 0 ); ATLTRACE( _T("Error: CStaticLink couldn''t open file") ); } } return 0; } LRESULT OnCtlColor( UINT, WPARAM wParam, LPARAM, BOOL& ) { // notify bit must be set to get STN_* notifications ModifyStyle( 0, SS_NOTIFY ); HBRUSH hBr = NULL; if( (GetStyle() & 0xff) <= SS_RIGHT ){ // it''s a text control: set up font and colors if( !m_hFont ){ LOGFONT lf; GetObject( GetFont(), sizeof(lf), &lf ); lf.lfUnderline = TRUE; m_hFont = CreateFontIndirect( &lf ); } HDC hDC = (HDC)wParam; SelectObject( hDC, m_hFont ); SetTextColor( hDC, m_bVisited ? m_colorVisited : m_colorUnvisited ); SetBkMode( hDC, TRANSPARENT ); hBr = (HBRUSH)GetStockObject( HOLLOW_BRUSH ); } return (LRESULT)hBr; } private: COLORREF m_colorUnvisited; COLORREF m_colorVisited; BOOL m_bVisited; HFONT m_hFont; CComBSTR m_bstrLink; }; // CStaticLink class CReflectDlg : public CDialogImpl<CReflectDlg> { public: enum { IDD = IDD_DIALOG1 }; BEGIN_MSG_MAP(CReflectDlg) COMMAND_RANGE_HANDLER( IDOK, IDCANCEL, OnClose ) MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog) REFLECT_NOTIFICATIONS() // reflect messages back to static links END_MSG_MAP() LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&) { CenterWindow( GetParent() ); // a textual static control: s1.SubclassWindow( GetDlgItem(IDS_TEST1) ); // a static control displaying an icon s2.SubclassWindow( GetDlgItem(IDS_TEST2) ); // set the icon''s link s2.SetLinkText( _T("http://www.microsoft.com") ); return 1; } LRESULT OnClose(UINT, WPARAM wID, HWND, BOOL& ) { ::EndDialog( m_hWnd, wID ); return 0; } private: CStaticLink s1, s2; }; // CReflectDlg int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow) { _Module.Init( NULL, hInstance ); CReflectDlg dlg; dlg.DoModal(); _Module.Term(); return 0; }ATL中的对话框类: 现在我们对ATL中的窗口类有了一定的了解,接着我们来学习对话框类。在你的项目中,可能有很多对话框资源,从最简单的“关于”模式对话框到复杂的满是控件的非模式对话框。ATL提供了CSimpleDialog类和CDialogImpl类来简化我们使用对话框资源的过程。 CSimpleDialog CSimpleDialog是一个从模版创建模式对话框的类。它提供了一些标准按纽(如OK和CANCEL)的处理过程。你可以将CSimpleDialog想象成是一种消息对话框(Message Box),不同的是你可以在对话框编辑器中编辑它的外观。 要显示这样一个对话框,比如当你点击“帮助”菜单中的“关于”菜单项时显示关于对话框,你需要在主窗口类中添加如下的消息映射: BEGIN_MSG_MAP( CMyMainWindow ) COMMAND_ID_HANDLER( ID_HELP_ABOUT, OnHelpAbout ) ... LRESULT OnHelpAbout( WORD, WORD, HWND, BOOL& ) { CSimpleDialog<IDD_DIALOG1> dlg; int ret = dlg.DoModal(); return 0; }我们可以看到对话框资源的ID(IDD_DIALOG1)被作为一个模版参数传递给CSimpleDialog类,DoModal方法显示对话框。当用户点击OK按钮时,CSimpleDialog类关闭对话框并返回按钮的ID。(CSimpleDialog类实现了对按钮IDOK,IDCANCEL,IDABORT,IDRETRY,IDIGNORE,IDYES和IDNO的响应。) CDialogImpl CSimpleDialog类只能够处理简单的模式对话框,对于更加复杂的对话框或者模式对话框,就要用到CDialogImpl类。(其实CSimpleDialog是CDialogImpl中的一种特例。) 如果我们需要实现一个非模式对话框,我们必须从CDialogImpl派生出一个新类,并将新类的类名作为模板参数传递给CDialogImpl类,就象前面的CWindowImpl一样: class CMyModelessDialog: public CDialogImpl {和CSimpleDialog不同,我们不需要将对话框资源的ID作为模板参数传递给它,但是我们必须将这个类和对话框资源联系起来,我们通过在类中定义一个枚举变量实现: public: enum { IDD = IDD_DIALOG1 };然后定义消息映射表: BEGIN_MSG_MAP( CMyDialog ) MESSAGE_HANDLER( WM_INITDIALOG, OnInitDialog ) MESSAGE_HANDLER( WM_CLOSE, OnClose ) ... END_MSG_MAP()响应函数的定义和前面的一样,但是有一点需要注意,如果你实现的是一个非模式对话框,那么在WM_CLOSE消息的响应函数中必须调用DestroyWindow: LRESULT OnClose( UINT, WPARAM, LPARAM, BOOL& ) { DestroyWindow(); return 0; } ... }; // CMyModelessDialog要在屏幕上创建这样一个对话框,需要创建这个类的一个实例并调用Create方法: CMyModelessDialog dlg; dlg.Create( wndParent );如果对话框资源没有选中WS_VISIBLE属性,我们需要这样让对话框显示出来: dlg.ShowWindow( SW_SHOW );下面的例子有一个非模式的对话框可以接受用户输入的字符串,然后在主窗口中显示这个字符串。对话框中有一个编辑框控件和一个按钮;当按钮被点击时,对话框调用它所属窗口的DoSomething方法对编辑框中的字符串进行处理,它所属的窗口是一个超类化的列表框控件,DoSomething方法的功能是将字符串添加到列表框中。 #include "atlbase.h" CComModule _Module; #include "atlwin.h" #include "resource.h" class CMyWindow: public CWindowImpl<CMyWindow> { public: DECLARE_WND_SUPERCLASS( "MyWindow", "listbox" ) BEGIN_MSG_MAP( CMyWindow ) MESSAGE_HANDLER( WM_DESTROY, OnDestroy ) END_MSG_MAP() LRESULT OnDestroy( UINT, WPARAM, LPARAM, BOOL& ) { PostQuitMessage( 0 ); return 0; } void DoSomething( LPCTSTR s ) { SendMessage( LB_ADDSTRING, 0, reinterpret_cast<LPARAM>(s) ); } }; class CMyDialog: public CDialogImpl<CMyDialog> { public: enum { IDD = IDD_DIALOG1 }; BEGIN_MSG_MAP( CMyDialog ) COMMAND_ID_HANDLER( IDC_BUTTON1, OnButton ) MESSAGE_HANDLER( WM_INITDIALOG, OnInitDialog ) MESSAGE_HANDLER( WM_CLOSE, OnClose ) END_MSG_MAP() LRESULT OnButton(WORD, WORD, HWND, BOOL&) { char buf[100]; m_ed.GetWindowText( buf, 100 ); m_owner.DoSomething( buf ); return 0; } LRESULT OnInitDialog( UINT, WPARAM, LPARAM, BOOL& ) { m_owner.Attach( GetParent() ); CenterWindow( m_owner ); m_ed = GetDlgItem( IDC_EDIT1 ); return TRUE; } LRESULT OnClose( UINT, WPARAM, LPARAM, BOOL& ) { DestroyWindow(); m_owner.Detach(); return 0; } CMyWindow m_owner; CWindow m_ed; }; CMyDialog dlg; int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { _Module.Init( NULL, hInstance ); CMyWindow win; win.Create( NULL, CWindow::rcDefault, _T("modeless dialog test"), WS_OVERLAPPEDWINDOW|WS_VISIBLE ); dlg.Create( win ); dlg.ShowWindow( SW_SHOW ); MSG msg; while( GetMessage( &msg, NULL, 0, 0 ) ){ if( !IsWindow(dlg) || !dlg.IsDialogMessage( &msg ) ){ DispatchMessage( &msg ); } } _Module.Term(); return 0; }指定窗口类的信息: 这篇文章的大部分内容都是在讲述怎样处理窗口类的行为——窗口怎样响应消息。在行为之外,一个窗口类还具有一些其它的重要的属性,比如样式、类名、背景颜色和指针等等。这一节介绍怎样使用ATL中提供的宏来指定这些属性。 使用Window Traits指定窗口的样式 到目前为止,所有例子中的窗口样式都是在调用Create方法时指定的: CMyWindow wnd; wnd.Create( NULL, CWindow::rcDefault, _T("Hello"), WS_OVERLAPPEDWINDOW|WS_VISIBLE );如果你不指定任何样式和扩展样式,ATL将使用默认的样式;这些默认的样式是作为窗口的特征定义的,默认特征是CControlWinTraits,定义如下: typedef CWinTraits<WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN |WS_CLIPSIBLINGS, 0> CControlWinTraits;CWinTraits是一个模板类,它需要2个参数:窗口样式、扩展窗口样式。 在CWindowImpl的定义中,CControlWinTraits作为默认的模板参数传递给CWindowImpl: template <class T, class TBase = CWindow, class TWinTraits = CControlWinTraits> class CWindowImpl : public ...所以在默认情况下,从CWindowImpl派生的窗口都具有可视、子窗口、裁剪兄弟窗口、裁减子窗口的属性。 我们也能定义自己的窗口特征: typedef CWinTraits<WS_OVERLAPPEDWINDOW|WS_VISIBLE,0> MyTraits;然后,从CWindowImpl派生一个窗口类,指定自己的窗口特征: class CMyWindow: public CWindowImpl<CMyWindow,CWindow,MyTraits> {...};或者象下面这样更加直接: class CMyWindow: public CWindowImpl< CMyWindow, CWindow, CWinTraits<WS_OVERLAPPEDWINDOW|WS_VISIBLE,0> > {...};注意,我们必须提供全部的三个模板参数:派生类,基类(CWindow)和特征类。 CMyWindow窗口现在具有的默认的样式为“可见的弹出窗口”,所以我们可以在Create方法中省略样式参数: CMyWindow wnd; wnd.Create( NULL, CWindow::rcDefault, _T("Hello") ); // style: WS_OVERLAPPEDWINDOW|WS_VISIBLE我们也可以重写窗口特征: ovwnd.Create( NULL, CWindow::rcDefault, _T("Hello"), WS_OVERLAPPEDWINDOW ); // not visible窗口特征也可以包含扩展样式: class CClientWindow: public CWindowImpl<CClientWindow, CWindow, CWinTraits< WS_OVERLAPPEDWINDOW|WS_VISIBLE, WS_EX_CLIENTEDGE > > {...};DECLARE_WND_CLASS 使用DECLARE_WND_CLASS宏可以指定窗口的类名: DECLARE_WND_CLASS("my window class");这等价于: DECLARE_WND_CLASS_EX( "my window class", CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS, // default style COLOR_WINDOW // default color );DECLARE_WND_CLASS_EX 使用DECLARE_WND_CLASS_EX宏可以指定窗口类名、样式和背景颜色: class CMyWindow: public CWindowImpl<CMyWindow> { public: DECLARE_WND_CLASS_EX( "my window class", // class name CS_HREDRAW|CS_VREDRAW, // class style COLOR_WINDOW // background color ); BEGIN_MSG_MAP(CMyWindow) ...所谓的窗口类名是指注册的窗口类的名字,如果我们不指定窗口类名,ATL将自动生成一个,但是当我们使用Spy++之类的工具的时候,你将会发现我们自己取的类名比"ATL:00424bd0"之类的名字要有用得多。 类样式是按照按位或组合的。 背景颜色必须是标准系统颜色之一。 CWndClassInfo 我们也可以定义超出DECLARE_WND_宏能力之外的窗口类。如果你看看DECLARE_WND_CLASS的定义你就会发现它定义了一个CWndClassInfo结构,并且一个函数返回这种结构类型的值: #define DECLARE_WND_CLASS(WndClassName) / static CWndClassInfo& GetWndClassInfo() / { / static CWndClassInfo wc = / { / { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, / StartWindowProc, / 0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, / WndClassName, NULL }, / NULL, NULL, IDC_ARROW, TRUE, 0, _T("") / }; / return wc; / }CWndClassInfo结构提供了更灵活的自定义的可能,它是这样定义的: struct CWndClassInfo { struct WNDCLASSEX { UINT cbSize; UINT style; WNDPROC lpfnWndProc; int cbClsExtra; int cbWndExtra; HINSTANCE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPCSTR lpszMenuName; LPCSTR lpszClassName; HICON hIconSm; } m_wc; LPCSTR m_lpszOrigName; WNDPROC pWndProc; LPCSTR m_lpszCursorID; BOOL m_bSystemCursor; ATOM m_atom; CHAR m_szAutoName[13]; ATOM Register(WNDPROC* p); };例如,要指定一个窗口的指针,我们可以将m_lpszCursorID设置为指针的名字,如果它是一个系统指针,将m_bSystemCursor设置为TRUE,否则设置为FALSE。注意DECLARE_WND_CLASS宏是怎样将这两个成员变量分别设置为IDC_ARROW 和 TRUE的。既然DECLARE_WND_宏不能让我们改写这些默认的值,我们可以这样做: class CMyWindow: public CWindowImpl<CMyWindow> { public: static CWndClassInfo& GetWndClassInfo() { // a manual DECLARE_WND_CLASS macro expansion // modified to specify an application-defined cursor: static CWndClassInfo wc = { { sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, StartWindowProc, 0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, "MyWindow", NULL }, NULL, NULL, MAKEINTRESOURCE(IDC_CURSOR1), FALSE, 0, _T("") }; return wc; } ...结论: ATL提供了一种简单的、雅致的并且功能强大的窗口编程模式。在那些方便的封装好了的函数、消息映射和宏之外,还有一些技术诸如链接、窗口的子类化和超类化、被包含的窗口和消息反射等也使得设计和实现窗口和对话框非常灵活。或许ATL给人最深的印象就是:功能强大、灵活性好,但是不会占用太多的内存和系统开销。 |