注:本文是袁晓辉根据Eamon O’ Tuathail的WTL Developer’s Guide(www.clipcode.biz ) 翻译而来,发布在www.farproc.com。作者翻译本文仅仅是为自己和其他WTL爱好者学习之用,请勿用于商业用途。你可以转载本文,但必须保证本文的完整性,并保留该声明!
第2章 Win32 SDK windowing
目标
Ÿ 本章的目标为:
Ÿ 回顾windowing的基本概念
Ÿ 引入windowing术语
Ÿ 解释在Windows下用户界面是如何构建的
Ÿ 描述Windows操作系统如何处理消息队列、子类、超类、消息循环和如何进行窗口管理等
Ÿ 分析线程和窗口如何交互
Ÿ 讲解简原始绘画是如何工作的
Windowing基本概念
WTL是建立在ATL windowing的基础之上的,而ATL又是建立在Win32 SDK基础之上的。在探索WTL之前,回顾在Windows操作系统家族里windowing是如何工作的是很重要的,这就是我们章要探讨的问题。如果能清晰地理解一些重要的Win32概念的话,我们在来看ATL和WTL的结构就会轻松得多。
窗口的类型
从最基本的说起,一台运行着Windows操作系统的PC的屏幕上的任何东西要么是一个窗口,要么是一个窗口上的原始绘画。Win32 API提供了创建、管理和销毁窗口以及表现窗口内的图形的概念。每一个用户界面元素(控件)都存在与一个独立的窗口中,最终用户可能认为存在许多类型的窗口-图形输出窗口、用户界面标准控件、(按钮,单选框等)、通用控件(ListView,TreeView)和ActiveX控件(ActiveX的一个特例是“无窗口”,可以没有自己的窗口,而是存在于他的容器窗口中),其实它们都是遵循同一套规则的窗口的实例而已。
窗口的层次
窗口在屏幕上是按层次排放的。最顶层的是桌面窗口,一个大图标模式的listview控件。应用程序的顶级窗口和它们的子窗口组成了这个有OS维护的窗口层次树的节点。
当你创建窗口时,你必须指定它的父窗口,新的窗口将位于它的父窗口所在的节点之下,当一个窗口被销毁时它的子窗口自动被销毁。
窗口类
一个窗口类是对一个窗口如何工作的重要方面的描述。你创建的每一个窗口必须有一个窗口类。操作系统为标准控件提供了窗口类,比如Button,Edit和ListBox以及通用控件ListView,TreeView,CalenderPicker
窗口类是用RegisterClassEx函数注册,用UnregisterClass取消的。
ATOM RegisterClass( //返回一个标识该窗口类的ATOM
CONST WNDCLASS *lpWndClass // IN:窗口类结构指针
);
BOOL UnregisterClass( /
LPCTSTR lpClassName, // WNDCLASS中的lpszClassName
HINSTANCE hInstance // 窗口基本
);
有时一个窗口类也被叫做窗口“模板”,用编程的术语来说,一个窗口类是一个Win32结构(并非一个C++结构)。窗口类中的最重要部分是窗口过程(window procedure),还包含一个用于程序最小化时显示的图标、背景刷子、默认鼠标指针和拥有者的hInstance(包含WndProc的Exe或Dll)。一个进程可以创建一个窗口类,然后基于它实例化窗口。
typedef struct _WNDCLASS {
UINT style; // 包含更多设置的掩码
// (比如 CS_NOCLOSE – 不在控制菜单中显示关闭
WNDPROC lpfnWndProc;//窗口过程
int cbClsExtra; //窗口类的附加字节
int cbWndExtra; //每个窗口的附加字节
HINSTANCE hInstance;// 包含窗口过程的EXE/DLL 的实例句柄
HICON hIcon; // 窗口类的图标
HCURSOR hCursor; // 窗口类的鼠标指针
HBRUSH hbrBackground; // 背景如何画
LPCTSTR lpszMenuName; //菜单资源名称
LPCTSTR lpszClassName; // 类名
} WNDCLASS, *PWNDCLASS;
重要的Style包括
CS_SAVEBITS,CS_NOCLOSE,CS_CLASSDC,CS_OWNDC,CS_PARENTDC(译者注:具体解释省略,参看MSDN)
附加的字节可以存储在窗口类中和该类的每个窗口中。类名在以后窗口窗口时有用-作为CreateWindowx[Ex]的一个参数
窗口过程
窗口消息不断地发送到窗口上,窗口通过窗口过程来处理它们。窗口过程是一个程序和一个窗口类的特定函数,这个函数检测到一个特定的消息就进行相应的处理(通常要调用另外的函数来处理),对不感兴趣的消息直接转交默认的处理函数(DefWindowProc)。很正常,多个窗可能是基于同一个窗口类的,也就是说多个窗口使用一个窗口过程来处理消息(通过一个HWND类型的参数区分是属于哪个窗口的消息)。
窗口
窗口是通过调用 CreateWindowEx 创建出来的,创建时必须指定一个已经注册过的窗口类名(这个参数不能为NULL)
HWND CreateWindow(
LPCTSTR lpClassName, // 窗口类名
LPCTSTR lpWindowName, // 窗口名称,显示在标题栏
DWORD dwStyle, // 窗口的风格
int x, // 窗口相对于父窗口的位置(xy坐标)
int y,
int nWidth, nHeight, // 窗口大小
HWND hWndParent, // 父窗口
HMENU hMenu, // 菜单句柄
HINSTANCE hInstance, // 和该窗口相关联的模块(EXE/DLL)句柄
LPVOID lpParam // 自定义数据,将作为WM_CREATE的参数
);
子类
当基于一个窗口类创建了一个窗口后,这个窗口就记录了一个窗口类中的窗口过程的指针。可以用子类(subclass)在不重新编写窗口类的前提下修改窗口的行为。子类通过调用一个函数用新的窗口过程替换原来的,这样就可以在新的窗口过程里收到消息,进行处理然后再传递给原来的窗口过程。这可以用在你自己写的窗口类中或那些系统提供的预定义的控件上。
超类
超类可以用来扩充基窗口类的功能。超类自己的窗口过程收到一个消息后可以直接处理或调用基类的窗口过程。
我们通常使用子类和超类来修改一些预定义的窗口控件的行为,比如editbox和lsitbox。子类和超类只能在同一个进程中使用。
消息队列
每个调用了Win32窗口函数的线程都拥有自己的消息队列。一个创建了窗口的线程的消息队列被用来输送这个窗口的消息。
消息循环
每个拥有消息队列的线程通过消息循环来处理传递过来的消息,这个需要调用GetMessage函数,这个函数是阻塞的,直到有消息进来为止,然后调用DispatchMessage,结果是适当的窗口过程被调用。
MSG msg;
while (GetMessage(&msg, 0, 0, 0))
DispatchMessage(&msg);
这个消息循环同样是COM中单线程单位(single-threaded apartments)的基础。如果一个组件被设计为被单线程调用,然而程序内外的多个线程想调用它时,一个隐形的窗口就被创建出来了,所有对指定COM的调用都被作为消息POST到这个窗口,那个对象可以一个一个地提取并处理它们。
捕获和焦点
一般来说,消息被发送到它们所发生的窗口,对于用户输入来说,就是焦点窗口。对鼠标消息来说,如果每个程序都想在收到鼠标按下消息后检测到对应的鼠标抬起消息,可以调用SetCapture来实现。
窗口风格
程序开发者在创建窗口是指定的形形色色的窗口风格会影响窗口的行为和外观。总共有两类窗口风格,标准风格(比如WS_CHILD)和扩展风格(比如WS_EX_PALETEWINDOW)。在WTL/ATL中“窗口风格”有另外一个术语,叫“特征(traits)”。
窗口属性
操作系统为一个窗口维护多组命名的属性。属性的名称是程序定义的,属性的值是个HANDLE,通常是指向程序分配并填充的一块内存的指针。
例子-用SDK写窗口
这例子演示了使用纯Win32 API编写窗口的概念。以后当我们使用ATL和WTL时再回头看看当初是如何用SDK调用手工实现的会很有帮助。
WindowsWithSDK工程是一个自动生成的简单的“Hello world”工程。在Visual C++中用 New Workspace命令,选择Win32 Application,然后选择“A typical ‘Hello World!’”,然后自动生成代码。它只是简单地初试化了一个WNDCLASSEX结构,然后调用RegisterClassEx注册了这个窗口类。然后调用CreateWindow来窗口窗口,它有一个消息循环来处理消息,并发送到窗口过程。
InstanceSubclassing工程也是同样的方法生成后,做了些更改来演示实例子类。这个工程有多写了一个窗口过程MySubclassWndProc,它处理WM_LBUTTONDOWN消息并发送所有其他的消息给原来的窗口过程(CallWndProc)
LRESULT CALLBACK MySubclassedWndProc(HWND hWnd, UINT message, WPARAM
wParam, LPARAM lParam){
switch (message) {
case WM_LBUTTONDOWN:
OutputDebugString(
TEXT("Mouse Down detected in subclassed WndProc\n"));
break;
default:
return CallWindowProc(MyWndProc, hWnd,
message, wParam, lParam);
}
return 0;
}
子类是通过在窗口创建后调用SetWindowLongPtr实现的。
hWnd = CreateWindow(szWindowClass, szTitle,
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, 0,
CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
SetWindowLongPtr(hWnd, GWL_WNDPROC,
(LONG_PTR)MySubclassedWndProc);
GlobalSubclassing工程演示了改变一个注册后的窗口类的窗口过程。
首先像以前一样注册窗口类,用SetClassLongPtr来改变窗口类设置。第一个窗口创建了,然后调用SetClassLongPtr来改变所有基于该类的新窗口的窗口过程(注意,第一个窗口仍然会使用原来的窗口过程,并没有受到SetClassLongPtr的影响)。
hWnd1 = CreateWindow(szWindowClass, szTitle,
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0,
CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
SetClassLongPtr(hWnd1, GCLP_WNDPROC,
(LONG_PTR)MySubclassedWndProc);
hWnd2 = CreateWindow(szWindowClass, szTitle,
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, 0,
CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
SuperClassing工程演示了超类化已经存在的窗口类。首先WNDCLASSEX结构的大小域被正确地初始化,然后用GetClassInfoEx提取出了一个已经存在的窗口类的信息,复制一些域到新的WNDCLASSEX结构并设置窗口过程指向一个新的函数,并调用RegisterClassEx注册了这个新窗口类。在新的窗口过程里,在调用原来的窗口过程之前加了一些代码。
wcex_base.cbSize = sizeof(WNDCLASSEX);
GetClassInfoEx(hInstance, szWindowClass, &wcex_base);
CopyMemory(&wcex_superclass, &wcex_base, sizeof(WNDCLASSEX));
wcex_superclass.lpfnWndProc =
(WNDPROC)MySuperclassWndProc;
wcex_superclass.lpszClassName =
TEXT("SuperClassName");
RegisterClassEx(&wcex_superclass);
hWnd = CreateWindow(TEXT("SuperClassName"), szTitle,
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0,
CW_USEDEFAULT, 0, NULL, NULL, hInstance,
NULL);
MessagesAndThreads工程在原来的线程里创建了一个窗口然后有开了一个线程。
hWnd = CreateWindow(szWindowClass, szTitle,
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, 0,
CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
HANDLE hMyThread;
DWORD MyThreadID;
hMyThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE) MyThreadStartProc, hInstance,
0, &MyThreadID);
在新的线程里创建了一个新的窗口,两个线程都拥有自己的消息循环。规则为创建窗口的线程被用来给属于自己窗口的消息排队,如果一个线程停止了(调用一个sleep)那么发送到该线程的窗口的消息将不被处理。
DWORD MyThreadStartProc(LPVOID hInstance){
HWND hWnd;
MSG msg;
hWnd = CreateWindow(szWindowClass, szTitle,
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0,
CW_USEDEFAULT, 0, NULL, NULL,
(HINSTANCE)hInstance, NULL);
ShowWindow(hWnd, SW_SHOWNORMAL);
UpdateWindow(hWnd);
// Main message loop:
while (GetMessage(&msg, NULL, 0, 0)) {
if (!TranslateAccelerator(msg.hwnd,
hAccelTable, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return 0;// thread functioned correctly
}
消息
放在消息队列里的消息通过整数ID标识,并带有两个参数,wParam和lParam,消息的发送者可以是系统的鼠标键盘驱动、窗口控件的实现代码、你的程序代码或ActiveX控件,消息的接收者可以一般是发生该消息的窗口的窗口过程,也有可能是它的父窗口的窗口过程。
消息ID
Windows的平台SDK定义了一组和平台窗口相关的消息,它们的名字都是WM_XXX的形式,这些消息对普通窗口和标准/通用控件(它们是基于普通窗口的)都有效。所以当鼠标在一个窗口内移动时,它的窗口过程就会收到一个WM_MOUSEMOVE消息,几乎所有的WM_消息都是这样,只有三个例外:WM_COMMAND,WM_NOTIFY和WM_CTLCOLORXXX这三个消息是发送到发生动作的窗口的父窗口的。
程序可以发送自定义的消息来实现程序通讯,自定义消息的值应该和WM_USER相加来形成一个有效的消息ID。接收消息的窗口过程应该把消息值减去WM_USER进行处理。
要像关闭一个程序,发送WM_QUIT消息。
标准控件和通用控件
Windows随核心一起提供了一套标准控件,它们总是可用的。包括按钮,组合框,编辑框,列表框,滚动条和静态组件。
注意,富文本框(Rich-Edit)控件是标准控件,不是通用控件,分三个版本发布,每个都比前一个支持更多的富文本特性。(省略部分对Rich-Edit的版本介绍)
Windows通用提供了更多的高级控件,叫做通用控件,只要程序调用了InitCommonControls/InitCommonControlsEx API并且系统安装了相应版本的Comctl32.dll就可以使用它们。
“经典的”Windows95提供了通用控件的核心,比如Treeview,listview和property page。后来随IE4(4.71)发布的Comctl32.dll提供了更多的控件,比如IPAddress,DateTime Picker,MonthCalendar。IE5没有添加通用控件,但是为这些控件增加了一些新的消息,比如Treeview的TVM_GETLINECOLOR
获取和设置信息
系统没提供真正用于操作这些控件的API,而是让程序通过发送消息来操作它们。比如给一个Edit发送EM_SETLIMITTEXT消息,并在wParam中指定具体的限制;发送EM_GETLIMITTEXT就可以从SendMessage的返回值得到这个限制的数目。
命令和通知
程序在使用标准/通用控件时往往想在控件收到特定小时时得到通知,我们已经看到了,可以通过子类/超类的方法让程序代码比默认的窗口过程更早地得到消息,处理特定的消息,传递不感兴趣的给默认窗口过程。对于特定的情况这很好,但是如果你的程序有很多个对话框,每个对话框上又有许多控件时,你的工作量就大得难以接受了。我们需要更简单地使用这些控件的方法。如果一个对话框(控件的父窗口)能得知它里面的控件的一些重要的动作,比如在edit box里输入了一个字符等。幸运的是,操作系统就是这样设计的,它发送WM_COMMAND消息和WM_NOTIFY消息给父窗口,并在参数里指明子窗口上发生了什么。
对于每个标准/通用控件,系统指定了当一些特定的动作发生是,父窗口得到通知。控件自身可能会收到WM_LBUTTONDOWN消息,但是它发送一个WM_COMMAND消息给父窗口,包含了一个BN_CLICKED通知消息在里面。标准控件和Animation通用控件使用WM_COMMAND,所有其他的都使用WM_NOTIFY。
窗口位于哪个层次
对Windows编程的一个好建议是心中要时刻清晰地知道你正在处理的窗口层次。通常的情况是我们无法正确估计窗口的数量,并且应该发送给父窗口的消息却发送到了另外一个窗口。这也许发生在ActiveX控件上,这些控件可以被设计为“无窗口的”,控件本身并没有自己的窗口而是对话框中工作的。当这个控件在一个编程环境中“安家”时,它被窗口类所封装,这个窗口类管理了一个“地址(site)”窗口,这个地址窗口在窗口层次中位于对话框窗口之下(它是对话框窗口的子窗口)和控件窗口(它是控件窗口的父窗口)之上
充分利用Spy++工具可以帮你理清窗口的层次关系,减少出错的机会和调试的时间。
线程和窗口
线程和窗口在很多的层次上存在交互,清楚理解这种交互可以避免许多潜在的问题并优化性能。
在Win32上,只有唯一一种线程,它执行threadproc中的用户界面或非界面程序代码。在操作系统的观点上没有“用户界面线程”这种东西,用户界面线程和工作者线程的差别只存在于程序层次上,因为你是程序的开发者,你决定了那些和UI有关的代码该在一个线程里,另外的UI无关的代码应该在另一个线程中调用。这你你做出的决定,操作系统并不关心这个。(这个观点同样适用于COM线程。没有“COM线程”这东西,它仅仅是一个恰巧执行了COM的Win32线程)在Win32里你可以在任何线程中执行UI调用,你可能是出于设计的考虑把UI调用集中到某些线程中。为了做出更好的设计,必须深入了解windows操作在线程上下文中是如何执行的。
下面是三本讲线程的经典书籍:
Richter的“Programming Applications for Microsoft Windows” – ISBN: 1-57231-996-8 第26章;
Berveridge’s/Wiener的“Multithreading Applications in Win32” ISBN: 0-201-44234-5, 第11章;
Cohen’s/Woodring的“Win32 Multithreaded Programming” ISBN: 1-56592-296-4, 第12章.
谁拥有这个对象?
一个程序可以创建许多对象,比如窗口,pen,
brush等等。一个好的程序员有义务在不再需要这些对象时删除它们。及时没有这么做,核心也会在线程终结时或进程终结时介入,删除那些依然存在的对象。Window对象(和Hook对象)是被创建它们的线程所拥有,当线程退出时,该线程创建的每个窗口都会被关闭。其他的对象(pen,
brush,region,DC,bitmap,font等等)是被进程拥有,不管它们是由哪个线程创建,它们都将存活知道被显式地删除或进程(非线程)终结。
线程和窗口消息队列
窗口/线程交互的一条黄金定律是创建窗口的线程(通过CreateWindow[Ex])也是处理该窗口消息的线程。其他大部分的Win32窗口/线程交互都遵循此定律。
为了理解此定律的含义,我们必须先回答几个问题。如果一个线程将会接受窗口消息,那么它有没有一个消息队列?如果有,是如何创建的?消息是如何放置到这个队列中的?如果一个线程有消息队列,它就需要一个消息泵来从队列中提取并处理消息,这又是怎么实现的?当一个线程死亡了,与之相关的窗口也会死亡,消息也就不会再发送给它们了,这怎么处理?如果一个拥有消息泵的线程同样等待一个系统核心对象(比如Mutex)会怎么样?
一些线程需要消息队列,一些不需要。还有一些线程在窗口运行中从来就没有用到,为它们分配内存构建一个从来都不用的消息队列是不划算的。隐藏,当OS创建一个线程是,并不会自动为它创建消息队列。这个线程一调用Win32窗口API,系统就会为它创建消息队列,这对程序代码来说是透明的。消息队列一旦存在,就一直存活到线程终结为止。
程序需要知道哪个线程有消息队列,以便为它添加熟悉的GetMessage/DispatchMessage消息泵。程序知道这个,因为消息队列仅仅在一个线程调用了窗口API后才被创建。因此在什么地方添加消息泵是设计者的问题。
发送(Send)消息和投递(Post)消息
导致窗口过程被调用的两种主要技术是发送消息和投递消息。发送是这么做的:
LRESULT SendMessage( //消息处理后的返回值
HWND hWnd, // 接受消息的窗口
UINT Msg, // 发送的消息
WPARAM wParam, //消息参数
LPARAM lParam // 消息参数
};
你可以认为Send是同步的,调用线程会阻塞知道消息真的被处理了,下一行代码才会被执行。hWnd参数指定了需要执行的窗口过程所在的线程。如果这个线程就是调用SendMessage的线程,窗口过程直接-没有线程上下文切换,消息也不放到消息队列里-被执行。如果接收消息的窗口在其他线程,一个消息被放置在那个线程的消息队列中,接下来的某个时候会被处理,并返回执行结果。然后调用SendMessage的线程“苏醒”了,继续执行。调用SendMessage从一个线程发送消息到另一个线程中的窗口是可行的,但是主要的问题是如果那个线程没有正常地处理消息泵,第一个线程将被阻塞。
投递可以用一下三个函数之一完成:
BOOL PostMessage(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam);
这个函数把消息放置到指定的窗口所被创建的线程的消息队列中。
BOOL PostThreadMessage(
DWORD idThread,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);
这个函数把消息放置到指定的窗口所被创建的线程的消息队列中。接收时窗口句柄可能为0。VOID PostQuitMessage( // no return
int nExitCode // Exit-code
);
这个函数把一个WM_QUIT消息放置到调用该函数的线程的消息队列中。过些时候线程接收到WM_QUIT小时时应该正常地退出。
投递是异步的,消息被投递到队列,函数立即返回,不等待。消息可能被正确处理也可能不被。这个函数通常被用来实现线程间不需要同步的通讯。接收线程在适当的时候会从队列中取出这个消息并按顺序处理。
需要注意的一点是如果消息队列不存在会怎么样?调用Post的线程会在接收线程中创建吗?不!消息队列只会在该线程调用了任何窗口API是在自身中被创建。这点在WTL多线程SDI程序中是非常主要的,我们会在WTL AppWizard一章详细讨论。
线程和内核对象
GetMessage API是一个阻塞的调用,等待消息到达队列。WaitForSingleObject 或 WaitForMultipleObject 也是阻塞调用,等到一个或多个内核对象被通知(signaled)。如果不但要等到核心对象也要同时等待消息到达该怎么办?可以用MsgWaitForMultipleObjects和 MsgWaitForMultipleObjectsEx。
DWORD MsgWaitForMultipleObjects(
DWORD nCount, // 句柄个数
CONST HANDLE pHandles, //句柄数组
BOOL fWaitAll, // 是否等待全部
DWORD dwMilliseconds, // 超时
DWORD dwWakeMask // 检测什么类型的输入
);
DWORD MsgWaitForMultipleObjectsEx(
DWORD nCount,
CONST HANDLE pHandles,
DWORD dwMilliseconds,
DWORD dwWakeMask,
DWORD dwFlags //其他标志
);
它们将等到若干个内核对象并检测消息到达,并有超时设置。
<待续>