尊天命,尽人事
分类: LINUX
2013-07-20 17:13:55
窗口是 MiniGUI 当中最基本的 GUI 元素,一旦窗口建立之后,窗口就会从消息队列当中获取属于自己的消息,然后交由它的窗口过程进行处理。这些消息当中,有一些是基本的输入设备事件,而有一 些则是与窗口管理相关的逻辑消息。本文将讲述 MiniGUI 中的窗口建立和销毁过程,并解释了窗口过程的概念以及对一些重要消息的处理。
2 消息和消息循环
在 MiniGUI 中,消息被如下定义(include/window.h):
352 typedef struct _MSG 353 { 354 HWND hwnd; 355 int message; 356 WPARAM wParam; 357 LPARAM lParam; 358 #ifdef _LITE_VERSION 359 unsigned int time; 360 #else 361 struct timeval time; 362 #endif 363 POINT pt; 364 #ifndef _LITE_VERSION 365 void* pAdd; 366 #endif 367 }MSG; 368 typedef MSG* PMSG; |
一个消息由该消息所属的窗口(hwnd)、消息编号(message)、消息的 WPARAM 型参数(wParam)以及消息的 LPARAM 型参数(lParam)组成。消息的两个参数中包含了重要的内容。比如,对鼠标消息而言,lParam 中一般包含鼠标的位置信息,而 wParam 参数中则包含发生该消息时,对应的 SHIFT 键的状态信息等。对其他不同的消息类型来讲,wParam 和 lParam 也具有明确的定义。当然,用户也可以自定义消息,并定义消息的 wParam 和 lParam 意义。为了用户能够自定义消息,MiniGUI 定义了 MSG_USER 宏,可如下定义自己的消息:
#define MSG_MYMESSAGE1 (MSG_USER + 1) #define MSG_MYMESSAGE2 (MSG_USER + 2) |
在理解消息之后,我们看消息循环。简而言之,消息循环就是一个循环体,在这个循环体中,程序利用 GetMessage 函数不停地从消息队列中获得消息,然后利用 DispatchMessage 函数将消息发送到指定的窗口,也就是调用指定窗口的窗口过程,并传递消息及其参数。典型的消息循环如下所示:
while (GetMessage (&Msg, hMainWnd)) { TranslateMessage (&Msg); DispatchMessage (&Msg); } |
在 MiniGUI-Threads 版本中,每个建立有窗口的 GUI 线程有自己的消息队列,而且,所有属于同一线程的窗口共享同一个消息队列。因此,GetMessage 函数将获得所有与 hMainWnd 窗口在同一线程中的窗口的消息。
而在 MiniGUI-Lite 版本中,只有一个消息队列,GetMessage 将从该消息队列当中获得所有的消息,而忽略 hMainWnd 参数。
3 几个重要的消息处理函数
除了上面提到的 GetMessage 和 TranslateMessage、DispatchMessage 函数以外,MiniGUI 支持如下几个消息处理函数。
PostMessage:该函数将消息放到指定窗口的消息队列后立即返回。这种发送方式称为"邮寄"消息。如果 消息队列中的邮寄消息 缓冲区已满,则该函数返回错误值。在下一个消息循环中,由 GetMessage 函数获得这个消息之后,窗口才会处理该消息。PostMessage 一般用于发送一些非关键性的消息。比如在 MiniGUI 中,鼠标和键盘消息就是通过 PostMessage 函数发送的。
SendMessage:该函数和 PostMessage 函数不同,它在发送一条消息给指定窗口时,将等待该消息被处理之后才会返回。当需要知道某个消息的处理结果时,使用该函数发送消息,然后根据其返回值进行 处理。在 MiniGUI-Threads 当中,如果发送消息的线程和接收消息的线程不是同一个线程,发送消息的线程将阻塞并等待另一个线程的处理结果,然后继续运行;否则, SendMessage 函数将直接调用接收消息窗口的窗口过程函数。MiniGUI-Lite 则和上面的第二种情况一样,直接调用接收消息窗口的窗口过程函数。
SendNotifyMessage:该函数和 PostMessage 消息类似,也是不等待消息被处理即返回。但和 PostMessage 消息不同,通过该函数发送的消息不会因为缓冲区满而丢失,因为系统采用链表的形式处理这种消息。通过该函数发送的消息一般称为"通知消息",一般用来从控 件向其父窗口发送通知消息。
PostQuitMessage:该消息在消息队列中设置一个 QS_QUIT 标志。GetMessage 在从指定消息队列中获取消息时,会检查该标志,如果有 QS_QUIT 标志,GetMessage 消息将返回 FALSE,从而可以利用该返回值终止消息循环。
4 MiniGUI-Threads 和 MiniGUI-Lite 在消息处理上的不同
表 1 总结了 MiniGUI-Threads 和 MiniGUI-Lite 在消息处理上的不同
表 1 MiniGUI-Threads 和 MiniGUI-Lite 在消息处理上的不同
MiniGUI-Threads | MiniGUI-Lite | |
多消息队列 | 每个创建窗口的线程拥有独立的消息队列 | 只有一个消息队列。所有窗口共享一个消息队列。除非嵌套消息循环,否则一个程序中只有一个消息循环。 |
内建多线程处理 | 是。可以自动处理跨线程的消息传递 | 不能。从一个线程向另外一个线程发送或者邮寄消息时,必须通过互斥处理保护消息队列。 |
其他 | 可以利用 PostSyncMessage 函数跨线程发送消息,并等待消息的处理结果 | 不能使用 PostSyncMessage、SendAsynMessage 等消息。 |
5 窗口的建立和销毁
5.1 窗口的建立
我们知道,MiniGUI 的 API 类似 Win32 的 API。因此,窗口的建立过程和 Windows 程序基本类似。不过也有一些差别。首先我们回顾一下 Windows 应用程序的框架:
在 Win32 程序中,在建立一个主窗口之前,程序首先要注册一个窗口类,然后创建一个属于该窗口类的主窗口。MiniGUI 却没有在主窗口中使用窗口类的概念。在 MiniGUI 程序中,首先初始化一个 MAINWINCREATE 结构,该结构中元素的含义是:
CreateInfo.dwStyle: | 窗口风格 |
CreateInfo.spCaption: | 窗口的标题 |
CreateInfo.dwExStyle : | 窗口的附加风格 |
CreateInfo.hMenu: | 附加在窗口上的菜单句柄 |
CreateInfo.hCursor: | 在窗口中所使用的鼠标光标句柄 |
CreateInfo.hIcon: | 程序的图标 |
CreateInfo.MainWindowProc: | 该窗口的消息处理函数指针 |
CreateInfo.lx: | 窗口左上角相对屏幕的绝对横坐标,以象素点表示 |
CreateInfo.ty: | 窗口左上角相对屏幕的绝对纵坐标,以象素点表示 |
CreateInfo.rx: | 窗口的长,以象素点表示 |
CreateInfo.by: | 窗口的高,以象素点表示 |
CreateInfo.iBkColor: | 窗口背景颜色 |
CreateInfo.dwAddData: | 附带给窗口的一个 32 位值 |
CreateInfo.hHosting: | 窗口消息队列所属 |
其中有如下几点要特别说明:
在准备好MAINWINCREATE 结构之后,就可以调用 CreateMainWindow 函数建立主窗口了。在建立主窗口之后,典型的程序将进入消息循环。如下所示:
int MiniGUIMain (int args, const char* arg[]) { MSG Msg; MAINWINCREATE CreateInfo; HWND hWnd; // 初始化 MAINWINCREATE 结构 CreateInfo.dwStyle = WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | WS_CAPTION; CreateInfo.spCaption= "MiniGUI step three"; CreateInfo.dwExStyle = WS_EX_NONE; CreateInfo.hMenu = createmenu(); CreateInfo.hCursor = GetSystemCursor(0); CreateInfo.hIcon = 0; CreateInfo.MainWindowProc = MainWinProc; CreateInfo.lx = 0; CreateInfo.ty = 0; CreateInfo.rx = 640; CreateInfo.by = 480; CreateInfo.iBkColor = COLOR_lightwhite; CreateInfo.dwAddData = 0; CreateInfo.hHosting = HWND_DESKTOP; // 建立主窗口 hWnd = CreateMainWindow(&CreateInfo); if (hWnd == HWND_INVALID) return 0; // 显示主窗口 ShowWindow (hWnd, SW_SHOWNORMAL); // 进入消息循环 while (GetMessage(&Msg, hWnd)) { TranslateMessage (&Msg); DispatchMessage(&Msg); } MainWindowThreadCleanup (hWnd); return 0; } |
5.2 窗口的销毁
要销毁一个主窗口,可以利用 DestroyMainWindow (hWnd) 函数。该函数将销毁主窗口,但不会销毁主窗口所使用的消息队列,而要使用MainWindowThreadCleaup 最终清除主窗口所使用的消息队列。
一般而言,一个主窗口过程在接收到 MSG_CLOSE 消息之后会销毁主窗口,并调用 PostQuitMessage 消息终止消息循环。如下所示:
case MSG_CLOSE: // 销毁窗口使用的资源 DestroyLogFont (logfont1); DestroyLogFont (logfont2); DestroyLogFont (logfont3); // 销毁子窗口 DestroyWindow(hWndButton); DestroyWindow(hWndEdit); // 销毁主窗口 DestroyMainWindow (hWnd); // 发送 MSG_QUIT 消息 PostQuitMessage(hWnd); return 0; |
6 几个重要消息
在窗口(包括主窗口和子窗口在内)的生存周期当中,有几个重要的消息需要仔细处理。下面描述这些消息的概念和典型处理。
6.1 MSG_NCCREATE
该消息在 MiniGUI 建立主窗口的过程中发送到窗口过程。lParam 中包含了由 CreateMainWindow 传递进入的 pCreateInfo 结构指针。您可以在该消息的处理过程中修改 pCreateInfo 结构中的某些值。
6.2 MSG_SIZECHANGING
该消息窗口尺寸发生变化时,或者建立窗口时发送到窗口过程,用来确定窗口大小。wParam 包含预期的窗口尺寸值,而 lParam 用来保存结果值。MiniGUI 的默认处理是,
case MSG_SIZECHANGING: memcpy ((PRECT)lParam, (PRECT)wParam, sizeof (RECT)); return 0; |
你可以截获该消息的处理,从而让即将创建的窗口位于指定的位置,或者具有固定的大小,比如在 SPINBOX 控件中,就处理了该消息,使之具有固定的大小:
case MSG_SIZECHANGING: { const RECT* rcExpect = (const RECT*) wParam; RECT* rcResult = (RECT*) lPraram; rcResult->left = rcExpect->left; rcResult->top = rcExpect->top; rcResult->right = rcExpect->left + _WIDTH; rcResult->bottom = rcExpect->left + _HEIGHT; return 0; } |
6.3 MSG_CHANGESIZE
在确立窗口大小之后,该消息被发送到窗口过程,用来通知确定之后的窗口大小。wParam 包含了窗口大小 RECT 的指针。注意应用程序应该将该消息传递给 MiniGUI 进行默认处理。
6.4 MSG_SIZECHANGED
该消息用来确定窗口客户区的大
小,和 MSG_SIZECHANGING 消息类似。wParam 参数包含窗口大小信息,lParam 参数是用来保存窗口客户区大小的 RECT
指针,并且具有默认值。如果该消息的处理返回非零值,则将采用 lParam 当中包含的大小值作为客户区的大小;否则,将忽略该消息的处理。比如在
SPINBOX 控件中,就处理了该消息,并使客户区占具所有的窗口范围:
case MSG_SIZECHANGED { RECT* rcClient = (RECT*) lPraram; rcClient->right = rcClient->left + _WIDTH; rcClient->bottom = rcClient->top + _HEIGHT; return 0; } |
6.5 MSG_CREATE
该消息在建立好的窗口成功添加到
MiniGUI 的窗口管理器之后发送到窗口过程。这时,应用程序可以在其中创建子窗口。如果该消息返回非零值,则将销毁新建的窗口。注意,在
MSG_NCCREATE 消息被发送时,窗口尚未正常建立,所以不能在 MSG_NCCREATE 消息中建立子窗口。
6.6 MSG_PAINT
该消息在需要进行窗口重绘时发送到窗口过程。
MiniGUI
通过判断窗口是否含有无效区域来确定是否需要重绘。当窗口在初始显示、从隐藏状态变化为显示状态、从部分不可见到可见状态,或者应用程序调用
InvalidateRect 函数使某个矩形区域变成无效时,窗口将具有特定的无效区域。这时,MiniGUI
将在处理完所有的邮寄消息、通知消息之后处理无效区域,并向窗口过程发送 MSG_PAINT 消息。该消息的典型处理如下:
case MSG_PAINT: { HDC hdc; hdc = BeginPaint (hWnd); // 使用 hdc 绘制窗口 ... EndPaint (hWnd, hdc); break; } |
6.7 MSG_DESTROY
该消息在应用程序调用 DestroyMainWindow 或者 DestroyWindow 时发送到窗口过程当中,用来通知系统即将销毁一个窗口。如果该消息的处理返回非零值,则将取消销毁过程。
7 Hello, World
在这个小节当中,我们给出一个简单的示例程序,该程序在窗口中打印"Hello, world!":
#include |