1.2 消息和消息循环
在Windows系列操作系统中,广泛使用了消息驱动的概念。在MiniGUI中,我们也使用了消息驱动作为应用程序的创建构架。
在消息驱动的应用程序中,计算机外设发生的事件,例如键盘键的敲击、鼠标键的按击等,都由支持系统收集,将其以事先的约定格式翻译为特定的消息。应用程序一般包含有自己的消息队列,系统将消息发送到应用程序的消息队列中。应用程序可以建立一个循环,在这个循环中读取消息并处理消息,一直处理到特定的消息传来为止。这样的循环称为消息循环。一般地,消息由代表消息的一个整型数和消息的附加参数组成。例如,鼠标左键的按下消息,可能由133这个数来表示,其附加参数可能包含按下时的鼠标所在位置信息。例如,MiniGUI中如下定义消息:
typedef struct
{
HWND hwnd;
int message;
WPARAM wParam;
LPARAM lParam;
...
}MSG;
message 指定了特定的消息类型,wParam 是以unsigned int类型定义的消息的短参数,lParam 是以 long 类型定义的消息长参数。
应用程序一般要提供一个处理消息的标准函数。在消息循环中,系统可以调用此函数,应用程序在此函数中处理相应消息。
图 1.2是一个消息驱动的应用程序的简单构架示意。
图 1.2 消息驱动的应用程序的简单构架
在 MiniGUI 中,消息分为如下几种类型:
ü 系统消息,为系统内部管理使用。
ü 鼠标消息,鼠标的点击、移动等产生的消息。
ü 键盘消息,键盘的按键消息。
ü 窗口消息,窗口管理消息。
ü 菜单消息,菜单管理消息。
ü 命令消息等。
1.3 窗口过程和窗口类
窗口过程是用来处理窗口消息的函数过程。对于同一类型的控件,其窗口过程一般是一样的。因此,系统一般利用窗口的窗口类名来区分不同的窗口类并调用不同的窗口过程。由于几乎每一个主窗口均和其他窗口有着不同的窗口过程,因此,在 MiniGUI 中,窗口类的概念只存在于控件和窗片中。对于主窗口来说,其窗口过程在建立主窗口时指定,而对控件和窗片来说,则在注册窗口类时指定,而在建立窗片或控件时指定所属窗口类。
1.4 句柄
句柄是 MiniGUI 用来标识对象的标识符。句柄和指针概念类似,但它不一定是指针值。利用句柄,MiniGUI 将系统变量从应用项目中分离了出来,因为程序员只能通过句柄访问对象,因而就没有利用指针是可能发生的因非法访问而导致的数据不一致问题。
在 MiniGUI 中,窗口、控件、设备环境、菜单、图标等均使用句柄访问。
二、窗口
2.1 应用程序和主窗口
我们将基于 MiniGUI 的一个会话(session)称为一个应用项目,而其中每个单独的线程或线程组称为应用。每个应用项目可建立多个应用。主窗口是建立在 MiniGUI 基础上的应用的主界面。MiniGUI 为每个主窗口建立单独的消息队列,在该主窗口基础上派生出的窗片、对话框及其控件均使用同一消息队列。在 MiniGUI 中,每个应用对应于一个线程。理论上讲,每个应用可以具备多个主窗口,但在 MiniGUI 中,主窗口均以单独的线程实现。但多个主窗口对应单一线程的情况也是可以在 MiniGUI 中实现的。
每个应用项目有一个 MiniGUIMain 函数,在这个函数中,可建立初始的应用线程。在调用 MiniGUIMain 之前,MiniGUI 启动自己的桌面窗口(Desktop)。桌面窗口作为 MiniGUI 的窗口管理器而存在。下面的代码段在 MiniGUIMain 中启动了三个主窗口线程:
int MiniGUIMain(int args, char* arg[])
{
pthread_t thread, thread2, thread3;
CreateThreadForMainWindow(&thread, NULL, TestWindowMain, 0);
CreateThreadForMainWindow(&thread2, NULL, TestWindowMain2, 0);
CreateThreadForMainWindow(&thread3, NULL, TestWindowMain3, 0);
return 0;
}
CreateThreadForMainWindow 函数为主窗口建立线程,并返回线程标识符。
其中的第三个参数是线程的入口函数地址。如下的代码段定义了上述代码中第一个主窗口线程的入口函数:
void InitCreateInfo(PMAINWINCREATE pCreateInfo)
{
pCreateInfo->dwStyle = WS_THICKFRAME;
pCreateInfo->spCaption = "The first main window" ;
pCreateInfo->hMenu = 0;
pCreateInfo->hCursor = GetSystemCursor(2);
pCreateInfo->hIcon = LoadIconFromFile("res/table.ico");
pCreateInfo->MainWindowProc = TestMainWinProc;
pCreateInfo->lx = 50;
pCreateInfo->ty = 50;
pCreateInfo->rx = 300;
pCreateInfo->by = 480;
pCreateInfo->iBkColor = COLOR_lightwhite;
pCreateInfo->dwAddData1 = 0;
pCreateInfo->dwAddData2 = 0;
}
void* TestWindowMain(void* data)
{
MSG Msg;
MAINWINCREATE CreateInfo;
HWND hMainWnd;
InitCreateInfo(&CreateInfo);
if( !(hMainWnd = CreateMainWindow(&CreateInfo)) )
return NULL;
ShowWindow(hMainWnd, SW_SHOWNORMAL);
while( GetMessage(&Msg, hMainWnd) ) {
DispatchMessage(&Msg);
}
MainWindowThreadCleanup(hMainWnd);
return NULL;
}
在上面的代码段中,该线程首先调用 CreateMainWindow建立了主窗口,然后调用 ShowWindow显示了主窗口,最后启动了消息循环。当消息循环因为接收到 MSG_QUIT 消息而终止时,该函数调用了 MainWindowThreadCleanup 清除了相关的线程数据。
从上述代码中可看出主函数不支持窗口类,在调用 CreateMainWindow 函数时直接指定主窗口的窗口过程地址。我们也可以从中看到主窗口所支持的其他属性:
1.窗口风格。表 2.1 给出了所支持的窗口风格
表 2.1 MiniGUI 支持的主窗口风格
风格 | 描述 |
WS_BORDER | 创建一个具有单线边框的窗口 |
WS_THICKFRAME | 创建一个具有宽边框的窗口 |
WS_THINFRAME | 创建一个具有细边框的窗口 |
WS_CAPTION | 创建一个具有标题栏的窗口 |
WS_HSCROLL | 创建一个具有水平滚动条的窗口 |
WS_MAXMIZEBOX | 创建一个具有最大化框的窗口 |
WS_MINIMIZEBOX | 创建一个具有最小化框的窗口 |
WS_SYSMENU | 创建一个具有系统菜单的窗口 |
WS_VSCROLL | 创建一个具有垂直滚动条的窗口 |
WS_DISABLED | 创建一个初始为禁止的窗口 |
WS_MAXIMIZE | 创建一个初始最大化的窗口 |
WS_MINIMIZE | 创建一个初始最小化的窗口 |
WS_VISIBLE | 创建一个初始可见的窗口 |
WS_EX_TOPMOST | 创建一个顶层窗口,这是一个 Win32 的扩展风格 |
2.窗口标题。
3.窗口菜单。
4.窗口图标。
5.窗口背景色。
理解消息循环和窗口过程
在利用 MiniGUI 开发应用程序之前,首先要理解的两个概念就是消息循环和窗口过程。消息循环是事件驱动的 GUI 编程之基础。而窗口则是图形用户界面的最基本交互元素。本文描述了 MiniGUI 中与消息相关的几个重要函数,也描述了 MiniGUI-Threads 和 MiniGUI-Lite 在消息循环实现上的几个不同。本文还讲述了在 MiniGUI 中的窗口建立和销毁过程,并解释了窗口过程的概念以及对一些重要消息的处理。
引言
我们知道,流行的 GUI 编程都有一个重要的概念与之相关,即"事件驱动编程"。事件驱动的含义就是,程序的流程不再是只有一个入口和若干个出口的串行执行线路;相反,程序会一直处于一个循环状态,在这个循环当中,程序从外部输入设备获取某些事件,比如用户的按键或者鼠标的移动,然后根据这些事件作出某种的响应,并完成一定的功能,这个循环直到程序接受到某个消息为止。"事件驱动"的底层设施,就是常说的"消息队列"和"消息循环"。本文将具体描述 MiniGUI 中用来处理消息的几个重要函数,并描述 MiniGUI-Threads 和 MiniGUI-Lite 在消息循环实现上的一些不同。
窗口是 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);
}
如上所示,GetMessage 函数从 hMainWnd 窗口所属的消息队列当中获得消息,然后调用 TranslateMessage 函数将 MSG_KEYDOWN 和 MSG_KEYUP 消息翻译成 MSG_CHAR 消息,最后调用 DispatchMessage 函数将消息发送到指定的窗口。
在 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 应用程序的框架:
在 WinMain () 中创建窗口,使用以下步骤:创建窗口类、登记窗口类、创建并显示窗口、启动消息循环。
在 WndProc () 中,负责对发到窗口中的各种消息进行响应。
在 MiniGUI 中也同样要有这两个函数。不过稍微有点不同。程序的入口函数名字叫MiniGUIMain (),它负责创建程序的主窗口。在建立主窗口之后,程序进入消息循环。
在 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: 窗口消息队列所属
其中有如下几点要特别说明:
CreateInfo.dwAddData:在程序编制过程中,应该尽量减少静态变量,但是如何不使用静态变量而给窗口传递参数呢?这时可以使用这个域。该域是一个 32 位的值,因此可以把所有需要传递给窗口的参数编制成一个结构,而将结构的指针赋予该域。在窗口过程中,可以使用 GetWindowAdditionalData 函数获取该指针,从而获得所需要传递的参数。
CreateInfo.hHosting:该域表示的是将要建立的主窗口使用哪个主窗口的消息队列。使用其他主窗口消息队列的主窗口,我们称为"被托管"的主窗口。当然,这只在 MiniGUI-Threads 版本中有效。
MainWinProc 函数负责处理窗口消息。这个函数就是主窗口的"窗口过程"。窗口过程一般有四个入口参数,第一个是窗口句柄,第二个是消息类型,第三个和第四个是消息的两个参数。
在准备好MAINWINCREATE 结构之