第二章 一个最简单窗口程序的转型
我知道,可能会有很多朋友对上一章的“Hello, World!”ATL版不以为然,因为它并不能算是什么ATL程序——毕竟它只不过是有了个CComModule而已。不过不管怎样我还是要说,它几乎仍然拥有了一个ATL GUI程序的所有组成部分:入口、初始化、程序体、卸载……
“等等!”也许你会突然打断我,“——还有注册窗口类、消息循环呢?”
当然,对于一个完整的GUI程序来讲,这也是必要的。
貌似废话
不清楚你是否已经为本章的内容做好了准备,因为下面我们就要动真格的了。不过考虑到本书的读者群中可能会存在着相当一部分了解MFC却对Win32 GUI的基本原理和流程不甚熟悉的朋友,所以李马特别为你们准备了这一节的内容。SDK的粉丝们可以跳过这一节,如果你们觉得李马讲的有些拖沓冗长的话。
那么,我还是先以一个标准的Win32 SDK程序开始:
//////////////////////////////////////////////////////////////////////////
// ATL的GUI程序设计配套源代码
// 第二章 一个最简单窗口程序的转型
// 工程名称:HelloSDK
// 作者:李马
// http://www.titilima.cn
//////////////////////////////////////////////////////////////////////////
#include <windows.h>
#include <tchar.h>
LRESULT CALLBACK HelloWndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
switch ( uMsg )
{
case WM_DESTROY:
{
PostQuitMessage( 0 );
}
break;
case WM_PAINT:
{
HDC hdc;
PAINTSTRUCT ps;
hdc = BeginPaint( hWnd, &ps );
DrawText( hdc, _T("Hello, SDK!"), -1, &ps.rcPaint, DT_CENTER | DT_VCENTER | DT_SINGLELINE );
EndPaint( hWnd, &ps );
}
break;
default:
return DefWindowProc( hWnd, uMsg, wParam, lParam );
}
return 0;
}
BOOL InitApplication( HINSTANCE hInstance )
{
WNDCLASS wc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hbrBackground = (HBRUSH)GetStockObject( WHITE_BRUSH );
wc.hCursor = LoadCursor( NULL, IDC_ARROW );
wc.hIcon = LoadIcon( NULL, IDI_APPLICATION );
wc.hInstance = hInstance;
wc.lpfnWndProc = HelloWndProc;
wc.lpszClassName = _T("HelloSDK");
wc.lpszMenuName = NULL;
wc.style = CS_HREDRAW | CS_VREDRAW;
return RegisterClass( &wc );
}
int WINAPI _tWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nShowCmd )
{
// 注册窗口类
InitApplication( hInstance );
// 创建窗口
HWND hWnd = CreateWindow( _T("HelloSDK"), _T("Hello SDK"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL );
ShowWindow( hWnd, nShowCmd );
UpdateWindow( hWnd );
// 消息循环
MSG msg;
while ( GetMessage( &msg, NULL, 0, 0 ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
return msg.wParam;
}
不知道你是否会觉得这段代码有些冗长?事实上,这个程序已经体现了Win32 GUI程序运行的所有流程(请注意,我并不会对这些代码进行详细的解释,因为我已经假设你已经了解了这些代码具体行为的必要细节。如果不是这样的话,请参考相关的书籍或者MSDN):
注册窗口类的部分。在这个程序中,InitApplication函数完成了这一工作。窗口类的概念类似于OO(面向对象)中的类,所有你在Windows中能看到的窗口都是某个特定窗口类的一份实例。但是,窗口类并非任何一种OOP语言中的类——它所包括的并不是通称的属性和方法(在C++中称作成员变量和成员函数),而是属性和响应。这个区别可能会使你感到费解,我会在下一章中为你详细介绍——因为ATL中对窗口的封装类将这一点体现得十分淋漓尽致。
创建窗口的部分。在通常的SDK代码里,这些代码被封装在一个名为InitInstance的函数中。这段代码所做的工作一般是创建窗口并将其显示出来。
消息循环。Windows是一个基于消息机制的操作系统,各个窗口之间的通信也主要是靠Windows消息来完成的。而程序中的消息循环也就是将本程序UI线程中的消息队列中提取各种消息,进行处理(如果有必要的话)之后分发给各个消息的属主窗口(或者说是目标窗口)。
在这里需要指出的是,HelloWndProc是我们自己定义的一个函数,我们需要用它来控制我们对特定窗口消息的特定响应。我们只需要在注册窗口类之前,将这个函数的地址(也就是函数名)赋值给WNDCLASS::lpfnWndProc成员就可以了。这个函数我们自己不需要进行调用,它的调用是当我们的窗口收到窗口消息后,由Windows完成的。在这个回调函数中,我们的处理是这样的:
WM_DESTROY。在窗口被销毁的时候,窗口会收到此消息。在这里,我们会调用PostQuitMessage,用以向当前UI线程的消息队列之中发送一条WM_QUIT消息,GetMessage在收到这条消息后,会返回FALSE,也就结束了消息循环,WinMain也就结束了。
WM_PAINT。在窗口需要绘制的时候,窗口会收到此消息。在这里我们只是简单的在窗口的中间绘制了一行文字“Hello, SDK!”。
其它消息。这些消息都是我们不关心的,所以我们将其交由系统默认的窗口过程DefWindowProc来处理。
这段代码貌似冗长,但实际上还是很有条理的,你可以根据它以及我以上的解说来对照这个程序的ATL版本。
ATL等同品
在写作这本书的时候,我总是希望我每次都能够能使用让你不太陌生的代码来循序渐进地引导你。考虑再三,对于“Hello, ATL!”的这个程序,我决定先把它的WinMain展现给你:
int WINAPI _tWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nShowCmd )
{
_Module.Init( NULL, hInstance );
// 创建窗口
CHelloATLWnd wnd;
wnd.Create( NULL, CHelloATLWnd::rcDefault, _T("Hello ATL") );
wnd.ShowWindow( nShowCmd );
wnd.UpdateWindow();
// 消息循环
MSG msg;
while ( GetMessage( &msg, NULL, 0, 0 ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
_Module.Term();
return msg.wParam;
}
OK,上一章介绍过的_Module又出现在你的眼前了——不过还是没有什么特别的变化,仍然是那熟悉的Init和Term。而且,正如“山哟还是那座山”一样,消息循环哟也仍然是那个消息循环。当然,你肯定也发现了那寥寥的变化:CHelloATLWnd是什么?在我将它的代码展现给你之前,你可能会做出这样的猜想:
这是一个C++类,它对Win32窗口类进行了封装。
这个类封装了大多数窗口操作的API函数,诸如CreateWindow、ShowWindow、UpdateWindow。
窗口类的注册可能也是在这个C++类中完成的。
好,打住,这就够了。让我们来撩开CHelloATLWnd那貌似神秘的面纱吧,赶紧着。
class CHelloATLWnd : public CWindowImpl< CHelloATLWnd, CWindow, CWinTraits< WS_OVERLAPPEDWINDOW > >
{
public:
CHelloATLWnd()
{
CWndClassInfo& wci = GetWndClassInfo();
wci.m_bSystemCursor = TRUE;
wci.m_lpszCursorID = IDC_ARROW;
wci.m_wc.hbrBackground = (HBRUSH)GetStockObject( WHITE_BRUSH );
wci.m_wc.hIcon = LoadIcon( NULL, IDI_APPLICATION );
}
public:
DECLARE_WND_CLASS( _T("HelloATL") )
public:
BEGIN_MSG_MAP( CHelloATLWnd )
MESSAGE_HANDLER( WM_DESTROY, OnDestroy )
MESSAGE_HANDLER( WM_PAINT, OnPaint )
END_MSG_MAP()
public:
LRESULT OnDestroy( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& hHandled )
{
::PostQuitMessage( 0 );
return 0;
}
LRESULT OnPaint( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& hHandled )
{
HDC hdc;
PAINTSTRUCT ps;
hdc = BeginPaint( &ps );
DrawText( hdc, _T("Hello, ATL!"), -1, &ps.rcPaint, DT_CENTER | DT_VCENTER | DT_SINGLELINE );
EndPaint( &ps );
return 0;
}
};
猜想,还是猜想!
请允许我在本章中不为你解释这个类的任何具体细节,取而代之的是继续的猜想。因为,这个类中需要解释的东西太多了,以至于我必须为它单独开辟一章。
窗口类的注册是由这个C++类的构造函数与DECLARE_WND_CLASS宏一起完成的。
对于BEGIN_MSG_MAP与END_MSG_MAP这一部分,想必使用过MFC的朋友们应该更容易理解。是的,这一对宏可以算作ATL的消息映射,在其中由MESSAGE_HANDLER作为消息分流器,将各种窗口消息分配给各个处理函数。
创建窗口时指定的样式貌似和模板参数CWinTraits有关。
当然,除了这些猜想之外,你可能还会同时存在以下疑问:
CWindowImpl、CWindow、CWinTraits究竟是什么?
窗口类是在何时注册的?
消息分流器是如何实现的?
也许你还会有更多的疑问,那么就让我一并将它们留到下一章再解决吧。如果你实在等不及的话,atlwin.h的代码也会告诉你一切的。
补叙CComModule
由于这本书主要针对的是ATL 3.0/Visual C++ 6.0,所以我疏忽了对CComModule的研究。在此感谢老李老刀兄提出的一点,就是CComModule在ATL 7.0中已经不建议使用了。于是我将MSDN中的相关章节摘抄下来,权作借花献佛之用。
CComModule 替换类
ATL 的早期版本使用 CComModule。在 ATL 7.0 中,CComModule 功能被若干个类所取代:
CAtlBaseModule 包含大多数使用 ATL 的应用程序所需的信息。包含模块和资源实例的 HINSTANCE。
CAtlComModule 包含 ATL 中的 COM 类所需的信息。
CAtlWinModule 包含 ATL 中的窗口化类所需的信息。
CAtlDebugInterfacesModule 包含接口调试支持。
CAtlModule 下列 CAtlModule 派生的类被自定义为包含特定应用程序类型中所需的信息。这些类中的大部分成员都可以被重写:
CAtlDllModuleT 在 DLL 应用程序中使用。为标准导出提供代码。
CAtlExeModuleT 在 EXE 应用程序中使用。提供 EXE 中所需的代码。
CAtlServiceModuleT 为创建 Windows NT 和 Windows 2000 服务提供支持。
CComModule 仍然可用以便向后兼容。
分布 CComModule 功能的原因
由于以下原因,CComModule 的功能分布到了几个新类中:
使 CComModule 中的功能呈粒状分割。
对 COM、窗口化、接口调试和应用程序特定的(DLL 或 EXE)功能的支持现在在不同的类中。
自动为这些模块的每一个声明全局实例。
所需模块类的全局实例链接到项目中。
消除了调用 Init 和 Term 方法的必要性。
Init 和 Term 方法已移动到模块类的构造函数和析构函数中;不再需要调用 Init 和 Term。
不过,出于代码的兼容性以及WTL的内容考虑,本系列后续文章仍然将使用ATL 3.0中的CComModule。