• 《windows 程序设计》读书笔记 三


    窗口与消息

    窗口的创建:

    调用 CreateWindow 函数:CreateWindow 函数的第一个参数称为 “窗口类名称”,而窗口类又与一个称为 “窗口过程” 的东西关联在一起。

    系统结构——windows 窗口编程概述:

    进行 Windows 程序设计时,其实就是在进行一种面向对象的编程,在 Windows 环境下接触最频繁的对象就是窗口(window)。

    装饰桌面最明显的窗口莫过于应用程序窗口,这些窗口包含一个显示有程序名称的标题栏、一个菜单栏。可能还带一个工具栏 (toolbar) 和一个滚动条 (scrollbar) 。另外一种类型的窗口是对话框,这种窗口可以不带标题栏。

    没有那么明显的还包括各种各样的按钮 (push button)、单选按钮 (radio button)、复选框 (checkbox)、列表框 (listbox)、滚动条 (scroll bar)、文本框等,这些对象都可用于装饰对话框。每一个这些对象都是一个窗口。更准确地说,这些对象都被称为 “子窗口” 或 “控件窗口” 或 “子窗口控件”。

    在用户眼中,这些窗口是屏幕上的对象,并可借助键盘或鼠标直接与之进行交互。非常有趣的是,程序员的视角与用户的视角非常一致。用户对窗口的输入以 “消息” 的形式传递给窗口,而窗口也借助消息来与其他窗口进行通信。深入理解 “消息” 这个概念是学习 Windows 编程过程中的一个 重要环节。

    下面给出 Windows 消息的一个例子。众所周知,大多数 Windows 程序的窗口尺寸都是可以调整的,即用户可用鼠标抓取窗口的边框,并通过拖动操作改变窗口的尺寸。通常程序都会通过改变窗口的内容来对这种改变做出响应。

    你可能已经猜测到 (你很可能是正确的),在用户调整窗口尺寸的过程中所产生的所有琐碎的代码都是由 Windows 而非应用程序进行处理的。但是应用程序显然 “知道” 该窗口尺寸已发生变化,因为它能够对自身的显示格式加以改变。

    当用户改变窗口的尺寸时,Windows 便向应用程序发送一条携带新窗口尺寸相关信息的消息,接着应用程序对自身的内容进行调整以反映出窗口尺寸的变化。(这里应该相当于事件响应了)

    上面提到 “Windows向应用程序发送了一条消息” 这句话时,其实是在说 Windows 调用了该程序内部的一个函数,这 个函数是你写的,而且是该程序的核心。此函数的参数描述了由 Windows 所发送并由你的程序所接收的特定消息。这个函数被称为 “窗口过程”。

    应用程序所创建的每一个窗口都有一个与之相关联的窗口过程。这个窗口过程可以是应用程序中的某一个函数, 也可以位于一个动态链接库中。Windows 正是通过调用该窗口过程来向窗口传递消息的。窗口过程则依据这些消息做出相应的处理,然后将控制权返还给 Windows。

    更准确地说,窗口总是依据 “窗口类" 来创建的,窗口类标识了用于处理传递给窗口的消息的窗口过程。窗口类的使用允许多个窗口共享同一窗口类,因而多个窗口可以使用相同的窗口过程(相当于循个调用中的初始化函数,窗口类和窗口是一对多的关系)。例如,Windows 程序中的所有按钮都基于相同的窗口类,与该窗口类关联的窗口过程位于一个 Windows 动态链接库中,它可对所有传递到按钮窗口的消息进行处理。

    在面向对象编程中,对象是代码和数据的组合。一个窗口也是一个对象。其代码对应窗口过程。数据则对应窗口过程所保留的信息以及 Windows 为每个窗口和存在于系统中的窗口类所保留的信息(这里的信息就是 windows 发送信息)。

    窗口过程用于处理传递给窗口的消息。通常这些消息用于将用户的鼠标或键盘输入通知给窗口。例如,正是通过这种途径按钮窗口能够获知它被 “单击”。而当窗口尺寸被调整或当窗口表面需要重绘时,也有相应的消息来通知窗口。

    当 Windows 程序开始执行时,Windows 首先为该程序创建一个 “消息队列” (message queue)。该消息队列中存放着应用程序可能创建的所有窗口的消息。Windows 应用程序中一般都包含一小段称为 “消息循环” (message loop) 的代码,该段代码用于从消息队列中检索消息,并将其分发给相应的窗口过程。其他消息则不经过消息队列直接发送给窗口过程。

    HELLOWIN 程序及剖析:

    要创建窗口,首先需要注册一个窗口类,而窗口类又需要窗口过程来处理窗口消息。

    1. /*------------------------------------------------------------
    2. HELLOWIN.C -- Displays "Hello, Windows 98!" in client area
    3. (c) Charles Petzold, 1998
    4. ------------------------------------------------------------*/
    5. #include <windows.h>
    6. LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
    7. int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
    8. PSTR szCmdLine, int iCmdShow)
    9. {
    10. static TCHAR szAppName[] = TEXT ("HelloWin") ;
    11. HWND hwnd ;
    12. MSG msg ;
    13. WNDCLASS wndclass ;
    14. wndclass.style = CS_HREDRAW | CS_VREDRAW ;
    15. wndclass.lpfnWndProc = WndProc ;
    16. wndclass.cbClsExtra = 0 ;
    17. wndclass.cbWndExtra = 0 ;
    18. wndclass.hInstance = hInstance ;
    19. wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
    20. wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
    21. wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
    22. wndclass.lpszMenuName = NULL ;
    23. wndclass.lpszClassName = szAppName ;
    24. if (!RegisterClass (&wndclass))
    25. {
    26. MessageBox (NULL, TEXT ("This program requires Windows NT!"),
    27. szAppName, MB_ICONERROR) ;
    28. return 0 ;
    29. }
    30. hwnd = CreateWindow (szAppName, // window class name
    31. TEXT ("The Hello Program"), // window caption
    32. WS_OVERLAPPEDWINDOW, // window style
    33. CW_USEDEFAULT, // initial x position
    34. CW_USEDEFAULT, // initial y position
    35. CW_USEDEFAULT, // initial x size
    36. CW_USEDEFAULT, // initial y size
    37. NULL, // parent window handle
    38. NULL, // window menu handle
    39. hInstance, // program instance handle
    40. NULL) ; // creation parameters
    41. ShowWindow (hwnd, iCmdShow) ;
    42. UpdateWindow (hwnd) ;
    43. while (GetMessage (&msg, NULL, 0, 0))
    44. {
    45. TranslateMessage (&msg) ;
    46. DispatchMessage (&msg) ;
    47. }
    48. return msg.wParam ;
    49. }
    50. LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
    51. {
    52. HDC hdc ;
    53. PAINTSTRUCT ps ;
    54. RECT rect ;
    55. switch (message)
    56. {
    57. case WM_CREATE:
    58. PlaySound (TEXT ("hellowin.wav"), NULL, SND_FILENAME | SND_ASYNC) ;
    59. return 0 ;
    60. case WM_PAINT:
    61. hdc = BeginPaint (hwnd, &ps) ;
    62. GetClientRect (hwnd, &rect) ;
    63. DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect,
    64. DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
    65. EndPaint (hwnd, &ps) ;
    66. return 0 ;
    67. case WM_DESTROY:
    68. PostQuitMessage (0) ;
    69. return 0 ;
    70. }
    71. return DefWindowProc (hwnd, message, wParam, lParam) ;
    72. }

    上面这段程序创建了一个普通的应用程序窗口,“Hello, Windows 98!”,显示于窗口的中心。如果计算机中安装有声卡,还会听到作者的声音。

    这里有几点需要注意:如果使用 Visual C++ 为此程序创建了一个新的项目 (Projet),则需要为连接器添加一个对象库。 方法如下,从 Project 菜单中选择 Settings 菜单项,然后选择 Link 选项卡。从 Category 列表框中选择 General,并添加 WINMM.LIB 到 ObjectLibrary/Modules 文本框。我们这样做的原因是 HELLOWIN 程序调用了一个多媒体函数,而多媒体对象库并未包含在默认项目内。如果不添加该库,你将从连接器那里得到一条错误提示,

    指出无法对 PlaySound 函数进行解析。

    HELLOWIN 读取一个名为 HELLOWIN.WAV 的文件,该文件位于本书配套光盘的 HELLOWIN 目录中。在执行 HELLOWIN.exe 时,默认路径必须为 HELLOWIN。在 Visual C++ 内直接运行程序时情形也如此,无论可执行程序是位于 HELLOWIN 的 RELEASE 子目录下还是 DEBUG 子目录下。

    前面提到 HELLOWIN 在窗口中心显示了一个文本字符串。实际上,这样的表述不够准确。这行文本实际上是显示在该程序的 “客户区” (client area) 的中心,客户区对应 HELLOWIN 图中标题栏和窗口边框之间的白色区域。这种区分对我们非常重要;因为客户区是一块应用程序可以自由绘图并向用户传达可视输出的区域。

    HELLOWIN.C 中有一个 WinMain 函数 (此函数在前两章的示例程序中我们已经接触过),此外还有另外一个名为 WndProc 的函数。该函数正是窗口过程。(在 Windows 程序员的交流用语中,该函数被称为 “win prock"。)请注意,在 HELLOWIN.C 中,并未出现任何调用 WndProc 的代码。但在 WinMain 中,有一个对 WndProc 的引用,这也正是该函数在这段程序非常靠前位置声明的原因。

    调用的 Windows 函数列表:

    Windows 函数调用:

    HELLOWIN 调用了至少 18 个 Windows 函数。下面按照出现顺序一一列出这些被调用的函数 (并附有简短描述):

    • LoadIcon:加载图标,以供程序使用。

    • LoadCursor:加载鼠标光标,以供程序使用。

    • GetStockObject:获取一 一个图形对 象。在本例中是一个用来对窗口的背景进行重绘的画刷。

    • RegisterClass:为应用程序的窗口注册一个窗口类。

    • MessageBox:显示消息框。

    • CreateWindow:基于窗口类创建一一个窗口。

    • ShowWindow:在屏幕中显示窗口。

    • UpdateWindow:指示窗 口对其自身进行重绘。

    • GetMessage:从消息队列获取消息。

    • TranslateMessage:翻译一些键 盘消息。

    • DispatchMessage:将消息发送给窗口过程。

    • PlaySound:播放声音文件。

    • BeginPaint:标明窗口绘制开始。

    • GetClientRect:获取窗口客户区的尺寸。

    • DrawText:显示一个文本字符串。

    • EndPaint:结束窗口绘制。

    • PostQuitMessage:将 “退出” 消息插入消息队列。

    • DefWindowProc:执行默认的消息处理。

    这些函数在 Platform SDK 文档中都有详细说明,它们的声明位于各种头文件中,其中大部分都位于 WINUSER.H 中。

    大写标识符:

    大写标识符:

    你可能已经注意到了 HELLOWIN.C 中所使用的大量大写标识符。这些标识符都是在 Windows 头文件中定义的。这些标识符有很多都是以两个或三个字母作为前缀,且其后紧跟一个下划线:

    这些标识符其实都是数值常量,前缀表明该常量所属的一般类别,如下表所示:

    (每个数值常量在头文件都定义有一个标识符)

    新数据类型:(变量:WPARAM、LPARAM,返回值:LRESULT,调用约定类型:WINAPI、CALLBACK,结构体类型)

    新数据类型:(WPARAM 和 LPARAM)

    在 HELLOWIN.C 中使用的一些其他标识符属于新数据类型,这些类型也是在 Windows 头文件中通过 typedef 或 #define 语句定义的。

    变量类型:

    有时,这些新数据类型纯粹是出于简便的考虑而作的缩写。例如,WndProc 的第二个参数为 UINT 类型,这种类型其实就是 unsigned int,在 Windows98 中,这种类型表示一个 32 位的值。WinMain 的第三个参数类型为 PSTR,该类型表示一个指向非宽字符串的指针,即 char*。(在上面宽字符和 Windows 目录中有写)

    其余类型就不那么显而易见了。例如,WndProc 的第三个和第四个参数的类型分别为 WPARAM 和 LPARAM。这些名称的起源还有一段 “典故”。

    当 Windows 还是 16 位系统时,WndProc 的第三个参数类型被定义为 WORD,表示一个 16 位的无符号短整型,而第四个参数类型被定义为 LONG,表示一个 32 位的有符号长整型,“PARAM" 的前缀 “W” 和 “L” 正是由此而来。

    但在 32 位版本的 Windows 中,WPARAM 被定义为 UINT,而 LPARAM 被定义为 LONG (仍然为 C 语言的 long 数据类型),因此窗口过程的这两个参数都是 32 位的,这就有些令人疑惑,因为 WORD 类型在 Windows 98 中仍然被定义为 16 位无符号短整型,因此 “PARAM” 的前缀 “W” 有些取名不当。(这里的 param 是参数的意思)

    返回值类型:

    WndProc 函数的返回值类型为 LRESULT,该类型等价于 LONG。

    调用约定类型:

    WinMain 函数的类型为 WINAPI (与在头文件中定义的每一个 Windows 函数调用相同),而 WndProc 函数的类型为 CALLBACK。这两个标识符都被定义为 __stdcall,它指定了在 Windows 和应用程序之间函数调用的特定次序。

    (stdcall:(函数内平衡堆栈)

    函数的参数自右向左通过栈传递,被调用的函数在返回前清理传送参数的内存栈,通常用于 Win32 API 中。平衡堆栈是在函数内部平衡堆栈,例如:windows 的函数 messagebox)

    结构体类型:

    HELLOWIN 程序还使用了四种数据结构 ,这四种结构也是在 Windows 头文件中定义的。

    WinMain 中使用前两种数据结构定义了名称为 msg 和 wndclass 的两个结构。后两种数据结构被用于在 WndProc 中定义两个名称分别为 ps 和 rect 的结构。(大写是系统变量,小写是自己定义)

    句柄类型:(本质是数值)

    句柄类型:

    对于各种类型的句柄,有下表所示的三种大写标识符。(H 是 handle 句柄的意思)

    在 Windows 中,句柄的使用非常频繁。HELLOWIN 程序中 HICON (图标的句柄)、HCURSOR (鼠标指针的句柄) 以及 HBRUSH (图形画刷的句柄)。

    句柄本质上是引用某个对象的数值 (通常为 32 位,本质!!!),Windows 中的句柄非常类似于传统的 C 或 MS-DOS 程序中使用的文件句柄。一般情况下,应用程序几乎总是通过调用 Windows 函数来获取句柄。应用程序通过在其他 Windows 函数中使用句柄来引用相应对象。句柄的实际取值对你的程序来说并不重要,而将句柄传递给你的程序的 Windows 模块则知道如何通过句柄来引用对象。(这里的意思是 Windows 模块自己知道怎么用)

    匈牙利标记法:

    匈牙利标记法:

    许多 Windows 程序员都使用 “匈牙利标记法” 作为变量命名约定。这是为了纪念具有传奇色彩的微软程序员 Charles Simonyi。 这种标记法非常简单,即变量名以表明该变量数据类型的小写字母开始。(前面小写字母是数据类型!!!第一个大写字母开始是变量名!)

    变量的匈牙利标记:

    • 例如 szCmdLine 的前缀 sz 表示 “以零结束的字符串” 。

    • hInstance 和 hPrevInstance 中的前缀 h 表示 “句柄”;

    • iCmdShow 中的前缀 i 表示 “整型”。

    WndProc 的后两个参数也使用了匈牙利标记法。 我在前面虽然提过,wParam 取名为 uiParam (ui 表示 unsigned int )也许更合适,但由于这两个参数是使用数据类型 WPARAM 和 LPARAM 定义的,因此我保留了这些参数的传统名称。

    结构体的匈牙利标记:

    当命名结构变量时,可使用结构名 (或结构名称的缩写) 的小写形式作为变量名称的前缀或整个变量名。

    例如,在 HELLOWIN.C 的 WinMain 函数中,msg 变量是一个 MSG 类型的结构,wndclass 是一个 WNDCLASS 类型的结构。

    在 WndProc 函数中,ps 是一个 PAINTSTRUCT 结构,而 rect 是一个 RECT 结构。(结构体的匈牙利标记法就是短的小写,长的缩写)。

    匈牙利标记法可帮助你远离一些错误 (bug) 。由于变量的名称描述了该变量的用法及其数据类型,你在编程时就不大可能犯数据类型不匹配的错误。

    本节使用的变量名前缀如下表所示:

    窗口类的注册:(WNDCLASS 结构体)

    窗口总是基于窗口类来创建的,窗口类确定了处理窗口消息的窗口过程,多个窗口可以同时基于某一窗口类来创建。

    例如,所有的按钮窗口——包括下压按钮、复选框和单选按钮——都是基于同一个窗口类来创建的。窗口类为所有这些窗口定义了窗口过程和一些其他的特性。当创建一个窗口时,我们往往还需要定义一些该窗口特有的附加特性。

    在创建应用程序窗口之前,必须调用函数 RegisterClass 来注册窗口类。该函数只需要一个参数,即一个指向 WNDCLASS 类型的结构的指针。该结构中包含两个指向字符串的指针 (字段),因此在头文件 WINUSER.H 中,它有两种不同的定义方式。

    首先是 ASCII 版本的 WNDCLASSA:

    注意其中匈牙利标记法的使用:

    前缀 lpfn 表示 “指向函数的长指针” (long pointer to a function,前面曾提到,在 Win32 API 中,长指针和短指针没有任何区别:实际上,它们是 16 位 Windows 系统遗留的概念)

    前缀 cb 代表 “字节数”(count of byte),常常用在一个表示字节尺寸的变量名称中。

    前缀 h 表示一个句柄。

    前缀 hbr 表示 “画刷的句柄” (handle to a brush)。

    前缀 lpsz 表示 “指向以零结束的字符串的长指针” (long pointer to a string terminated with a zero)

    该结构的 Unicode 版本定义如下:

    这个版本与该结构的 ASCII 版本相比,唯一的区别在于最后两个字段被定义为指向宽字符的常量字符串,而不是指向 ASCII 字符的常量字符串。

    头文件 WINUSER.H 在定义了 WNDCLASSA 和 WNDCLASSW 结构之后 (另外还定义了指向这些结构的指针,这是 ASCII 的结构体),又基于 UNICODE 标识符的定义定义了 WNDCLASS 结构和指向 WNDCLASS 结构的指针 (这是 UNICODE 的结构体,目的是保持向后的兼容性):

    在 WinMain 中,我们通常用如下形式定义一个 WNDCLASS 类型的结构:

    WNDCLASS wndclass;

    然后对该结构的 10 个字段进行初始化,并调用 RegisterClass 函数:

    1. wndclass.style = CS_HREDRAW | CS_VREDRAW ;
    2. wndclass.lpfnWndProc = WndProc ;
    3. wndclass.cbClsExtra = 0 ;
    4. wndclass.cbWndExtra = 0 ;
    5. wndclass.hInstance = hInstance ;
    6. wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
    7. wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
    8. wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
    9. wndclass.lpszMenuName = NULL ;
    10. wndclass.lpszClassName = szAppName ;

    第一个字段:

    第一个字段利用 C 语言的按位或运算符,组合了两个 32 位 “类风格” (class style) 的标识符

    wndclass.style = CS_HREDRAW | CS_VREDRAW;

    在头文件 WINUSER.H 中,所有前缀为 CS 的标识符定义如下图所示:

    (以这种方式定义的标识符常常称为 “位标记” (bit flag),因为每个标识符都只能影响复合值中的一位)

    其中程序 HELLOWIN 使用了下图中两个标识符,以指定无论何时窗口的水平尺寸 (CS_HREDRAW) 或垂直尺寸 (CS_VREDRAW) 被改变,所有基于该窗口类的窗口都将被重新绘制。在调整 HELLOWIN 的窗口尺寸时,可以发现,文本字符串被重新绘制在窗口新的中心位置上。正是这两个标识符确保了这种结果。

    第二个字段:

    第二个字段 (lpfnWndProc) 是用于基于该窗口类的所有窗口的窗口过程的地址。在 HELLOWIN.C 中,该窗口过程就是 WndProc 。

    wndclass.lpfnWndProc = WndProc; (lpfn 指向函数长指针,根据前面匈牙利标记法目录可知)

    该语句将该窗口类的窗口过程设为 WndProc 函数,即 HELLOWIN.C 中的第二个函数。这个函数将处理传递给所有基于该窗口类创建的窗口的所有消息。在 C 语言中,当在语句中按这种方式使用函数名时,我们引用的实际上是指向函数的指针。

    第三第四个字段:

    下面两个字段用于在类结构和 Windows 内部维护的窗口结构中预留一些额外的空间。

    wndclass.cbClsExtra = 0; (这里的 cb 不知道应该是标识字节数,count byte)

    wndclass.cbWndExtra = 0;

    第五个字段标识应用程序的实例句柄(是 WinMain 的一个参数)

    wndclass.Instance = hInstance;

    第六个字段的语句为所有基于该窗口类的窗口设定一个图标:

    (图标是一幅位图格式的小图,用于向用户表示该程序。当程序在运行时,图标会出现在 Windows 任务栏和程序窗口的标题栏左侧)

    wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION);

    为了获取预定义图标的句柄,需要调用函数 LoadIcon, 并将函数的第一个参数设为 NULL。而在从保存在磁盘中的应用程序的可执行文件中加载自定义图标时,该参数必须设为 hInstance 即相应程序的实例句柄。第二个参数用于标识该图标。对预定义图标来说,该参数是一个前缀为 IDI 的标识符。这些标识符都在头文件 WINUSER.H 中定义,而前缀 IDI 表示 “图标的标识符” (ID for an icon)。 IDI_APPLICATION 图标只是一个窗口的小图片。

    LoadIcon 函数返回该图标的句柄。我们其实并不关心句柄的实际取值,因为它只是被用来设定 hIcon 字段。该字段的定义位于 WNDCLASS 结构中,其类型为 HICON,表示 “图标的句柄” (handle to an icon)。(句柄类型目录中提到过,句枘的本质是数值)

    第七个字段载入预定义图标指针:

    wndclass.hCursor = LoadCursor (NULL, IDC_ARROW)

    LoadCursor 函数载入一个预定义的鼠标指针 (称为 IDC_ARROW),并返回指针的句柄。该句柄被用来设定 WNDCLASS 结构的 hCursor 字段。当鼠标指针出现在这类窗口的客户区内时,将变成一个小箭头。

    第八个字段指定客户区的背景色:

    wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);

    下一个字段为这类窗口的客户区指定了置景色。字段名 hbrBackground 的前缀 hbr 表示 “画刷的句柄” (handle to a brush)。画刷是一个图形学术语,表示用于区域填充的像素着色模式。Windows 有几个标准的画刷,又称 “库存” 画刷。(上面对 GetStockObject 的调用返回一个白色画刷的句柄)

    第九个字段指定窗口类的菜单:(由于程序 HELLOWIN 不带任何菜单,所以该字段被设为 NULL)

    wndclass.lpszMenuName = NULL;

    最后一个字段赋予窗口名称:

    最后,必须为窗口类赋予一个名称,这个名称可以简单地用程序名表示,如保存在变量 szAppName 中的字符串 "HelloWin”。(这个字符串既可由 ASCII 字符组成,也可由 Unicode 字符组成,具体取决于 UNICODE 标识符是否已被定义)

    wndclass.lpszMenuName = NULL;

    调用 RegisterClass 函数进行注册:

    当 WNDCLASS 结构的所有 10 个字段完成初始化之后,程序 HELLOWIN 调用函数 RegisterClass 来完成该窗口类的注册。该函数的唯一参数是一个指向 WNDCLASS 结构的指针。

    实际上,系统存在两种不同的用于注册窗口类的函数 RegisterClassA 和 RegisterClassW,它们的参数分别是一个指向 WNDCLASSA 结构的指针和一个指向 WNDCLASSW 结构的指针。程序利用哪个函数注册窗口类,决定着传递给窗口的消息是包含 ASCII 文本还是 UNICODE 文本。

    这就引发了一个问题,如果在定义了 UNICODE 标识符的情况下编译程序,程序会调用 RegisterClassW 函数。在 Windows NT 下运行程序时,这没有问题。但是在 Windows 98 下运行程序时,RegisterClassW 函数实际上并没有真正实现。该函数虽然有一个入口,但它仅是简单地返回零值,表示出现了一个错误。这是在 Windows 98 下运行的 UNICODE 程序通知用户发生错误并终止的绝佳机会。

    (函数 MessageBoxW 能够正常运行,因为它是 Windows 98 中所实现的少有的几个 UNICODE 函数之一)

    1. if (!RegisterClass (&wndclass))
    2. {
    3. MessageBox (NULL, TEXT ("This program requires Windows NT!"),
    4. szAppName, MB_ICONERROR) ;
    5. return 0 ;
    6. }

    GetLastError 通用获取错误信息函数:

    显然,上面这段代码假定 RegisterClass 没有因其他原因而出现调用失败,如因 WNDCLASS 结构的 IpfnWndProc 字段为 NULL。利用函数 GetLastError 可以获知这类错误的原因。(就是这里有错误就崩掉了)

    GetLastError 函数是 Windows 中一个通用的函数,用于获取当函数调用失败时的扩展错误信息。各函数的说明文档中都已指明是否可以利用 GetLastError 获取这种信息。(的确各文档都有说明)

    例如,在 Windows 98 中调用 RegisterClassW 时,函数 GetLastError 的返回值将为 120。通过查看头文件 WINERROR.H 可知,值 120 对应的标识符为 ERROR_CALL_NOT_IMPLEMENTED。在 /Platform SDK/Windows Base Services/Debugging and Error Handling/Error Codes/System Errors - Numerical Order 中,也可查到各种错误类型。

    (检查函数返回值来判断是否有错误发生是很有必要的,举例分配内存时总需要进行错误检查,如果由于无法分配到所需的内存而导致 RegisterClass 调用失败,则 Windows 可能已经陷入停止状态)

    WnMain 的历史遗留代码习惯:(想节省内存)

    在一些 Windows 示例程序中,WinMain 可能包含下面这段代码:

    这实际上是因为 “旧习难改”。在 16 位 Windows 版本中,如果对已经在运行的程序启动新的实例,WinMain 的参数 hPrevInstance 将是前一个实例的实例句柄。为了节省内存,两个或多个实例允许共享同一窗口类。因此,只有当参数 hPrevInstance 为 NULL,表示程序没有其他的实例正在运行时,系统才会注册窗口类。

    而在 32 位 Windows 版本中,参数 hPrevInstance 总是 NULL。上面的代码仍然能够正常运行,但其实不必再去检查 hPrevInstance 的值。

    1. if (!hPrevInstance) //32 位 Windows 版本中,参数 hPrevInstance 总是 NULL
    2. {
    3. wndclass.cbStyle = CS_HREDRAW | CS_VREDRAW;
    4. [other wndclass initialization]
    5. RegisterClass (&wndclass);
    6. }

    开始创建窗口:(CreateWindow)

    与传给 RegisterClass 函数的信息是通过一个数据结构指定的不同,传给 CreateWindow 的信息则是通过独立参数的形式指定的。

    在 HELLOWIN.C 中,对 CreateWindow 的调用如下:

    1. hwnd = CreateWindow (szAppName, // window class name
    2. TEXT ("The Hello Program"), // window caption
    3. WS_OVERLAPPEDWINDOW, // window style
    4. CW_USEDEFAULT, // initial x position
    5. CW_USEDEFAULT, // initial y position
    6. CW_USEDEFAULT, // initial x size
    7. CW_USEDEFAULT, // initial y size
    8. NULL, // parent window handle
    9. NULL, // window menu handle
    10. hInstance, // program instance handle
    11. NULL) ; // creation parameters

    事实上还有另外两个用于创建窗口的函数,即 CreateWindowA 和 CreateWindowW 这两个函数将头两个参数分别当作 ASCII 和 UNICODE 字符来处理。

    第一个参数 szAppName:

    注释为 “窗口类名称” 的参数是 szAppName,该参数中包含字符串 “HelloWin”,即程序刚刚注册的窗口类的名称。

    第二个参数 TEXT ("The Hello Program") :

    “窗口标题” 是要出现在窗口标题栏中的那行文本。

    第三个参数 WS_OVERLAPPEDWINDOW:

    由该程序创建的窗口是一个普通的层叠 (overlapped,预定义的) 窗口。该窗口有一个标题栏、一个位于标题栏左边的系统菜单按钮、一个窗口尺寸调整边框以及位于标题栏右方的三个按钮 (分别用于最小化、最大化和关闭窗口) 。这是窗口的标准风格 (或称样式) 之一, 该风格的名称为 WS_OVERLAPPEDWINDOW,它是作为 CreateWindow 的 “窗口风格” 参数而出现的。

    如果查看一下头文件 WINUSER.H,你会发现该风格其实是由几个位标记通过按位或组合而成:

    1. #define WS_TILEDWINDOW WS_OVERLAPPEDWINDOW
    2. #define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX)

    第四第五个参数初始坐标值:

    注释为 “初始 x 坐标” 和 “初始 y 坐标” 的那两个参数指定了窗口左上角相对于屏幕左上角的初始位置。在这个程序中,我们将这些参数设为 CW_USEDEFAULT,意为我们希望 Windows 将层叠窗口的初始位置取为默认值(CW_USEDEFAULT 被定义为 0x8000000)。

    默认情况下,Windows 会将连续新建的窗口的左上角位置沿水平方向和垂直方向分别作步长为 1 的偏移(真的是这样的!!!)。

    第六、七个初始尺寸参数:

    类似地,注释为 “初始 x 方向尺寸” 和 “初始 y 方向尺寸” 的两个参数分别指定了窗口的初始宽度和高度。标识符 CW_USEDEFAULT 同样表明我们希望 Windows 将窗口的尺寸取为默认值。

    第九个参数父句柄窗口:

    如果新建窗口为顶级窗口 (例如应用程序窗口),注释为 “父窗口句柄” 的参数就应设为 NULL。通常,当两个窗口之间存在父子关系时,子窗口总是位于父窗口的前方,比如说应用程序窗口总是位于桌面窗口的前方。(但不必为了调用 CreateWindow 函数而设法获取桌面窗口的句柄)

    第十个参数的窗口菜单句柄:

    在这段程序中,“窗口菜单句柄” 也被设为 NULL,这是由于该窗口没有菜单。

    第十一个参数程序实例句柄:

    “程序实例句柄” 则被设为作为 WinMain 函数参数传入的程序实例句柄。

    最后一个创建参数:

    最后,我们将 “创建参数” 赋为了 NULL。也可将该参数指向某些数据,以便后续在程序中加以引用。

    窗口的显示 ShowWindow、UpdateWindow:

    当 CreateWindow 调用返回时,窗口已在 Windows 内部被创建。这句话的基本意思是,Windows 已经分配了一块内存来保存 CreateWindow 调用中指定的窗口信息以及一些其他信息,Windows 可通过窗口句柄来获取这些信息。

    (但是上面创建的窗口是在 "内存" 中的,我们还需调用另外两个函数帮助窗口显示出来)

    ShowWindow (hwnd, iCmdShow) 函数: (擦除)

    该函数的第一个参数是指向刚才由 CreateWindow 所创建的窗口的句柄。

    第二个参数是 WinMain 函数所接收的 iCmdShow 值。该参数决定着窗口在屏幕中的初始显示形式,即是正常显示,还是显示为最小化窗口或最大化窗口。

    将程序添加到 “开始” 菜单时,用户可能会选择一种偏好:

    • 如果窗口是正常显示,则从 WinMain 传给 ShowWindow 的参数值便为 SW_SHOWNORMAL;

    • 若窗口以最大化显示,则为 SW_SHOWMAXIMIZED;

    • 若窗口只是显示在任务栏,则为 SW_SHOWMINNOACTIVE

    函数 ShowWindow 用于将窗口显示在屏幕中,如果该函数的第二个参数是 SW_SHOWNORMAL,则该窗口的客户区将被在窗口类中所指定的背景画刷擦除,然后调用下面的 UpdateWindow (hwnd) 将使窗口客户区重绘。

    UpdateWindow (hwnd) 函数:(重绘)

    (前面窗口类注册目录中字段解析中提到过 wndclass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH) 为白色画刷)

    该函数会在收到指定消息后将窗口客户区重绘,这是通过向窗口过程 (即 HELLOWIN.C 中的 WndProc 函数) 发送一条 WM_PAINT 消息而完成的。

    消息循环:(条件检索消息(WM_QUIT 返回 0 ,条件不成立),填充 MSG 结构,两次转换消息,windows 调用窗口过程)

    在 UpdateWindow 被调用之后,新建窗口在屏幕中便完全可见了。此时该程序必须能够接收来自用户的键盘输入和鼠标输入。Windows 为当前在其中运行的每一个 Windows 程序都维护了一个 “消息队列”。当输入事件发生后,Windows 会自动将这些事件转换为 “消息”,并将其放置在应用程序的消息队列中。

    应用程序通过执行一段名为 “消息循环” 的 while 代码段来从该消息队列中获取消息:

    1. while(GetMessage (&msg, NULL, 0, 0))
    2. {
    3. TranslateMessage (&msg);
    4. DispatchMessage (&msg);
    5. }

    其中的 msg 是一个结构变量,其类型为 MSG:(该类型的定义位于头文件 WINUSER.H 中)

    1. typedef struct tagMSG {
    2. HWND hwnd;
    3. UINT message;
    4. WPARAM wParam;
    5. LPARAM lParam;
    6. DWORD time;
    7. POINT pt;
    8. } MSG,*PMSG,*NPMSG,*LPMSG;

    其中 POINT 数据类型是另外一种结构, 其定义位于头文件 WINDEF.H 中:

    1. typedef struct tagPOINT
    2. {
    3. LONG x;
    4. LONG y;
    5. }POINT, * PPOINT;

    消息循环的条件中 GetMessage 函数用于从消息队列中对消息进行检索:(条件判断是不是 WM_QUIT(返回 0 ,退出循环))

    GetMessage (&msg, NULL, 0, 0)

    该调用将一个指向名称为 msg 的 MSG 结构变量的指针传给 Windows。其第二、第三和第四个参数分别被设为 NULL 或 0,表明该程序希望获取由该程序所创建的所有窗口的消息。Windows 会自动用从消息队列中得到的下一条消息来填充消息结构 &msg 的各个字段。

    MSG 被填充的各个字段说明如下:

    • hwnd:消息所指向的窗口的句柄。

    在 HELLOWIN 程序中,它与从 CreateWindow 所返回的 hwnd 相同,因为这是该程序所拥有的唯一窗口。

    • message:消息标识符。

    这是一个用于标识消息的数字,对于每条消息,在 Windows 的头文件 (大多数位于 WINUSER.H ) 中都为其定义了一个以 WM (window message) 为前缀的标识符。比如你将鼠标放置在 HELLOWIN 程序的主窗口的客户区,并按下鼠标左键,则 Windows 会将一个message 字段为 WM_LBUTTONDOWN (值为 0x0201) 的消息放入消息队列中。

    • wParam:一个 32 位的 “消息参数”,该参数的含义和取值取决于具体的消息。(传给 DefWindowProc)

    • lParam:另外一个 32 位的 “消息参数”,该参数的含义和取值同样取决于具体消息。(传给 DefWindowProc)

    • time:消息进入消息队列的时间。

    • pt:消息进入消息队列中时鼠标指针的位置坐标。

    GetMessage 获取消息后,真正处理消息的两行代码:

    如果从消息队列中检索到的消息的 message 字段不等于 WM_QUIT (其值为 0x0012),则 GetMessage 将返回一个非 0 值,否则返回 0。

    第一行代码将 msg 结构返还给 Windows 以进行某些键盘消息的转换:(详见第 6 章)

    TranslateMessage (&msg) ;

    第二行代码将转换后的 msg 结构再次发送给 Windows 程序:

    DispatchMessage (&msg) ;

    接下来,Windows 会将这条消息发送给合适的窗口过程来处理(也就是说,Windows 调用了窗口过程,在 HELLOWIN 程序中,窗口过程为 WndProc), 当 WndProc 处理完该消息后,将控制权转回给 Windows,接着 Windows 发送完全处理好的 &msg 结构给指定注册的窗口。当 Windows 从 DispatchMessage 返回 HELLOWIN 后,消息循环又会进行下一轮的 GetMessage 调用。(从此构成了循环)

    (GetMessage——>Windows——>GetMessage——>TranslateMessage——>Windows——>WndProc ——>Windows——>DispatchMessage——>HELLOWIN)

    窗口过程 WndProc 自定义函数:

    前面的都是繁琐的常规步骤,包括注册窗口类、创建窗口、在屏幕中显示窗口、程序进入消息循环、从消息队列中检索消息。真正重要的是窗口过程,其决定了窗口客户区的显示内容以及窗口如何对用户的输入做出响应。

    在 HELLOWIN 程序中,窗口过程是一个名为 WndProc 的函数。窗口过程的名称可以任意命名 (只要不与其他名称冲突即可)。一个 Windows 程序可包含多个窗口过程,但一个窗口过程总是与一个通过调用 RegisterClass 注册的特定窗口类相关联。CreateWindow 函数基于特定的窗口类创建窗口,而基于同一个窗口类则可创建多个窗口。(一个窗口类-->多个窗口过程-->多个窗口)

    窗口过程通常按照如下方式来定义:

    LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

    窗口过程的 4 个参数与 MSG 结构的前 4 个字段是一一对应的:(MSG 结构在上一个目录)

    • 第一个参数是 hwnd,表示接收消息的窗口的句柄,它与从 CreateWindow 函数返回的句柄相同。

    对于一个像 HELLOWIN 这样只创建了一个窗口的程序,这是程序已知的唯一一个窗口句柄。如果某个应用程序基于相同的窗口类创建了多个窗口 (因而这些窗口的窗口过程均相同),则 hwnd 将标识接收消息的窗口的句柄。

    • 第二个参数与 MSG 结构的 message 字段对应,是一个标识消息的数字。最后两个参数是 32 位的消息参数,用于提供关于该消息的更丰富的信息。这些参数中所包含的内容依赖于具体的消息类型。有时一个消息参数是由两个 16 位的值组合而成,有时一个消息参数是一个指向文本字符串或一个数据结构的指针。

    (应用程序通常并不直接对窗口过程进行调用。窗口过程几乎总是由 Windows 自身调用的。应用程序如果希望调用自身的窗口过程,则可通过调用函数 SendMessage 来实现)

    • 第三四个代表具体的消息参数传递给窗口过程的 DefWindowProc 进行默认处理

    消息的处理:(WINUSER.H)

    窗口过程所接收的每条消息都由一个数字来标识,即窗口过程的 message 参数(在 MSG 结构中)。Windows 头文件 WINUSER.H 中为各种类型的消息定义了以 WM 为前缀的标识符。

    通常 Windows 程序员会使用 switch-case 结构来确定窗口过程所收到的消息的类型以及相应的处理方法。当窗口过程对消息进行处理后,应返回 0。

    所有窗口过程不进行处理的消息都必须传给名称为 DefWindowProc 的函数。DefWindowProc 的返回值必须从窗口过程返回。

    在 HELLOWIN 中,WndProc 只对 WM_CREATE、WM_PAINT 和 WM_DESTROY 进行处理:

    (用 DefWindowProc 来对所有窗口过程没有处理的消息进行默认处理非常重要,否则其他的正常行为 (如结束程序) 将无法进行)

    1. switch (message)
    2. {
    3. case WM_CREATE:
    4. PlaySound (TEXT ("hellowin.wav"), NULL, SND_FILENAME | SND_ASYNC) ;
    5. return 0 ;
    6. case WM_PAINT:
    7. hdc = BeginPaint (hwnd, &ps) ;
    8. GetClientRect (hwnd, &rect) ;
    9. DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect,
    10. DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
    11. EndPaint (hwnd, &ps) ;
    12. return 0 ;
    13. case WM_DESTROY:
    14. PostQuitMessage (0) ;
    15. return 0 ;
    16. }
    17. return DefWindowProc (hwnd, message, wParam, lParam) ;

    WM_CREATE 消息:(声音文件的播放 PlaySound 函数)

    HELLOWIN 中 WndProc 选择处理的第一条消息是 WM_CREATE,其实这也正是一个窗口过程所接收到的第一条消息,当 Windows 在 WinMain 函数中处理 CreateWindow 函数调用时,WndProc 将接收到该消息。

    也就是说,当 HELLOWIN 调用 CreateWindow 时,Windows 完成其必须的操作,而在此过程中会产生第一条消息,紧接着 Windows 对 WndProc 进行调用,并将其第一个参数设为该窗口的句柄(LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 中参数对应 MSG 结构前四个字段)将第二个参数设为 WM_CREATE (其值为 1)。

    WndProc 对 WM_CREATE 消息进行处理后会将控制权返还给 Windows, 然后 Windows 对 CreateWindow 的调用结束并返回到 HELLOWIN 中,并继续执行 WinMain 中的其他步骤。(HELLOWIN——>Windows——>CreateWindow——>WndProc——>CreateWindow——>HELLOWIN 。把 windows 函数看成 windows 的控制权,WndProc 单独分开,其它的划分给 HELLOWIN 控制权比较好理解,而且 CreateWindow 函数后没有像 DispatchMessage 一样的发送消息函数是因为其后面有 SowWindow 和 UpdateWindow 函数啊!)

    程序中实现的例子:

    1. case WM_CREATE:
    2. PlaySound (TEXT ("hellowin.wav"), NULL, SND_FILENAME | SND_ASYNC) ;
    3. return 0 ;

    通常情况下,窗口过程都会在处理 WM_CREATE 消息期间对窗口进行一次性的初始化。HELLOWIN 通过播放一个名为 HELLOWIN.WAV 的波形声音文件作为对该消息的处理,这是通过调用 PlaySound 函数而实现的(自己在官方文档中查看)。

    • PlaySound 函数的第一个参数是波形文件的名称(该参数也可以是控制面板的声音控制区定义的声音别名或应用程序资源) 。

    • 该函数的第二个参数只有当声音文件是一个资源时才有用。

    • 该函数的第三个参数指定了一组选项。在本例中,我指定了第一个参数为一个文件名且该段声音是以异步方式播放的,即当所指定的声音文件开始播放时 PlaySound 函数便立即返回,而无需等待该文件播放结束。按照这种方式,应用程序可以继续完成其初始化。

    当 WndProc 处理完 WM_CREATE 消息后,将从窗口过程返回 0。

    WM_PAINT 消息:(UpdateWindow 函数)

    (调用 CreateWindow 时会产生第一个 case 中 WM_CREATE 消息,调用 UpdateWindow 时会产生第二个 case 中 WM_PAINT 消息)

    程序中实现的例子:

    1. case WM_PAINT:
    2. hdc = BeginPaint (hwnd, &ps) ;
    3. GetClientRect (hwnd, &rect) ;
    4. DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect,
    5. DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
    6. EndPaint (hwnd, &ps) ;
    7. return 0 ;

    WndProc 所处理的第二条消息是 WM_PAINT,当窗口的客户区的部分或全部 “无效” 且必须 “更新” 时,应用程序将得到此通知,这也就意味着窗口必须被 “重绘” 。

    客户区变为无效的情况:(在后面文本输出的 WM_PAINT 消息中还有例子)

    • 当窗口被首次创建时,整个客户区都是无效的,因为此时应用程序尚未在该窗口上绘制任何东西。第一条 WM_PAINT 消息 (通常在应用程序调用 WinMain 中的 UpdateWindow 时出现,在窗口的显示目录中提到) 将指示窗口过程在窗口客户区进行绘制。

    • 在调整 HELLOWIN 窗口的尺寸时,客户区也会变为无效。前面我们有将 HELLOWIN 的 wndclass 结构的 style 字段设为了标记 CS_HREDRAW 和 CS_VREDRAW 这就指示 Windows 当窗口尺寸发生变化时,整个窗口都应宣布无效。在此之后,窗口过程将接收到一条 WM_PAINT 消息。

    • 如果先最小化 HELLOWIN 然后再将窗口恢复到原先的尺寸,Windows 并不会保存客户区的内容。在图形环境中,这种情况下需要保存的数据太多了。对此 Windows 采取的策略是宣布窗口无效。窗口过程接收到 WM_PAINT 消息后,会自行恢复窗口的内容。

    • 在屏幕中拖动窗口导致窗口之间发生重叠时,Windows 并不负责保存被另一个窗口覆盖的区域。当被覆盖的区域在后来不再被遮挡时,窗口被标记为无效。窗口过程会收到一条 WM_PAINT 消息,并对窗口的内容进行重绘。(就是重叠也会宣布无效)

    WM_PAINT 消息的处理大部分 BeginPaint 函数开始:(该函数擦去无效区域的背景并使无效区域有效化,可直接绘制并同时显示,它同时还会填充 PAINTSTRUCT 结构的各个字段,函数的返回值就是设备环境句柄)

    hdc = BeginPaint (hwnd, &ps) ; (hwnd 是窗口句柄标识符)

    以调用 EndPaint 函数结束:(释放设备环境句柄)

    EndPaint (hwnd,&ps) ;

    在这两个函数调用中,第一个参数均为程序的窗口句柄,而第二个参数均为指向一个类型为 PAINTSTRUCT 结构的指针。(PAINTSTRUCT 结构包含一些窗口过程用来对客户区进行绘制的消息,"获取设备环境句柄:方法一" 目录中讲到该结构)

    在 BeginPaint 调用期间,如果客户区的背景尚未被擦除,则 Windows 会用窗口类指定的画笔其进行擦除。在 HELLOWIN 这个例子中,所使用的画刷是一个库存的白色画刷,即 Windows 会将窗口的背景清除为白色。

    (擦除背景时使用的画刷是在用于注册窗口类的 WNDCLASS 结构中的 hbrBackground 字段中指定的,wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);)

    BeginPaint 擦除无效客户区并使其有效化,返回一个能定位该无效区的 “设备环境句柄” 以局限在该无效区内显示文本和图形,之后的 GDI 函数可以直接绘制并同时显示了!EndPaint 释放设备环境句柄(这样定位不到该窗口就无法再绘制了)。(设备环境 (devicecontext) 是指物理输出设备 (如视频显示器) 及其设备驱动程序)

    如果一个窗口过程不对 WM_PAINT 消息进行处理(这是极其罕见的),则该类消息必须交给 DefWindowProc 来处理。DefWindowProc 所做的只是简单地依次调用 BeginPaint 和 EndPaint,以使客户区被擦除并有效化显示再释放环境句柄(但是上面并没有绘制任何东西)。

    当 WndProc 调用完 BeginPaint 之后,它对 GetClientRect 进行调用:(过渡作用,获取新的矩形大小尺寸)

    GetClientRect (hwnd, &rect) ;

    该函数的第一个参数是程序的窗口句柄,第二个参数为指向类型为 RECT 的矩形结构的指针。该结构具有 4 个类型为 LONG 的字段,名称分别为 left、top、right 和 bottom。GetClientRect 函数将依据当前窗口尺寸来对这 4 个字段进行设置。其中 left 和 top 字段总是会被赋为 0,这样 right 和 bottom 字段就分别表示以像素为单位的客户区的宽度和高度。依据当前窗口尺寸来填充。(第二个参数 RECT 矩形结构是被填充的!!!依据当前窗口)

    填充完 rect 结构变量后 WndProc 将其指针作为 DrawText 的第四个参数:(限定矩形区域以便重新绘制)

    DrawText (hdc, TEXT("Hello, Windows 98!") ,-1,&rect , DT_SINGLELINE | DT_CENTER I DT_VCENTER) ;

    DrawText 函数所实现的功能是绘制文本:

    • 第一个参数为由 BeginPaint 函数所返回的设备环境句柄(用于定位可绘制客户区)。

    • 第二个参数是所要绘制的文本内容。

    • 第三个参数被设为 -1,表示该文本字符串以 0 作为结尾。

    • 最后一个参数是一组位标记,这些位标记的定义位于 WINUSER.H 中。(尽管由于 DrawText 完成的是输出的显示而看起来像是一个 GDI 函数,但由于它是一个相当高层的绘制函数,因而实际上属于用户模块)这些标记指示所显示的文本应在由第四个参数 &rect 所限定的矩形区域内单行显示,并且水平和垂直居中。按照这种方式对该函数进行调用,就会使字符串 “Hello, Windows 98!" 显示在窗口客户区的中央。

    总结:

    无论何时当客户区变为无效时 (如调整窗口尺寸时),WndProc 都将接收到一条新的 WM_PAINT 消 息。WndProc 通过调用 GetClientRect 函数可以获得更新后的窗口尺寸,并再次将这行文本显示在改变局的窗口的中央。(GetClientRect 是过渡用的)

    WM_DESTROY 消息:

    程序中实现的例子:

    1. case WM_DESTROY:
    2. PostQuitMessage (0) ;
    3. return 0 ;

    WM_DESTROY 消息表明 Windows 正处在依照用户的命令销毁窗口的过程中。当用户单击 [关闭] 按钮或从程序的系统菜单中选择 [关闭] 时,该消息将会发出。

    HELLOWIN 通过调用函数 PostQuitMessage 来对 WM_DESTROY 消息做出响应,这是一种标准的响应方式。

    PostQuitMessage(0) ;

    该函数的功能是将一个 WM_QUIT 消息插入到程序的消息队列中,如果 GetMessage 获取到的消息是 WM_QUIT, 便会退出消息循环,然后程序会执行下列语句:return msg.wParam; 该返回语句将从 WinMain 中退出并将程序结束。

    (前面说过对于所有非 WM_QUIT 消息,GetMessage 函数都将返回非零值,而对 WM_QUIT 消息,GetMessage 将返回 0(消息循环目录中))

    1. while (GetMessage (&msg, NULL, 0, 0))
    2. {
    3. TranslateMessage (&msg) ;
    4. DispatchMessage (&msg) ;
    5. }
    6. return msg.wParam ;

    若干难点:

    用户程序调用系统还是系统调用用户程序:

    (这里说的是 windows 系统能调用用户程序,所以与其它语言不同)

    平常的编程语言中都是调用操作系统来完成某种操作的做法,例如需要打开文件时,C 程序员会调用 fopen 函数,而fopen 函数是通过调用操作系统来完成打开文件这个操作的。(调用函数基本就是调用系统了)

    但 Windows 系统截然不同,虽然它也提供了成百上千个函数,但它还可以调用用户的程序,如 WndProc 窗口过程。窗口过程是与程序中 RegisterClass 注册的窗口类关联在一起的,基于该窗口类创建的窗口使用其指定的窗口过程来处理所有的消息。

    WndProc 会在类似场景下被 Windows 系统调用:

    • 新建窗口时;

    • 窗口被最终销毁时;

    • 窗口尺寸发生变化或被移动或被最小化时;

    • 用户用鼠标在窗口中执行单击或双击操作时;

    • 用户从键盘输入字符时;

    • 用户从菜单中选择某个菜单项时;

    • 用户用鼠标操作或单击滚动条时:

    • 窗口的客户区需要重绘时;

    所有这些对 WndProc 的调用都是以消息的形式出现的。大多数 Windows 程序的主要任务无一例外都致力于对各种各样的消息进行处理。这些 Windows 能够发送给程序的消息通常都带有前缀 WM,而其定义一般都位于头文 件WINUSER.H 中。

    实际上,在程序外部对程序内的例程进行调用的这种方式在字符模式的编程中也有一些例子。例如 C 语言中的 signal 函数就可获取 Ctrl-C 中断或其他来自操作系统的中断。为 MS-DOS 编写的老式程序通常都会捕获硬件中断。

    (实际上就是一种捕获)

    而在 Windows 中这一概念被扩展到了所有事件上。窗口所发生的一切都通过消息的形式转给窗口过程。然后窗口过程以某种方式对消息作出反应,或是把消息传递给 DefWindowProc 以进行默认处理。(窗口所发生的一切就是外部对窗口进行了操作)

    DefWindowProc 函数:(最后的默认处理函数)

    窗口过程的 wParam 和 IParam 参数在 HELLOWIN 中只是作为参数传给了 DefWindowProc。这些参数为窗口过程提供了关于该消息的一些附加信息。这些参数的含义与具体消息有关。

    举个例子:(下面的参数都是 MSG 结构中的字段)

    无论何时当窗口的客户区尺寸发生变化时,Windows 都会对该窗口的窗口过程进行调用。此时,窗口过程的 hwnd 参数就是尺寸发生变化的这个窗口的句柄。(注意窗口过程可以为基于同一窗口类创建的多个窗口处理消息,通过 hwnd 参数,窗口过程就可获知到底是哪一个窗口正在接收消息)

    message 参数为 WMSIZE,然后 wParam 参数取值可为 SIZE_RESTORED、SIZE_MINIMIZED、SIZE_MAXIMIZED、SIZE_MAXSHOW 或 SIZE_MAXHIDE (这些值都定义在 WINUSER.H 中,取值依次为 0 到 4 )。也就是说,wParam 参数表明窗口是将被改为非最小化或非最大化尺寸,还是将被最小化,抑或是将被最大化,或将被隐藏。

    IParam 参数中包含该窗口的新尺寸,窗口的新宽度 (一个 16 位的值) 和新高度 (也是一个 16 位的值) 被组合在 32 位的 IParam 参数中。头文件 WINDEF.H 中定义了一些宏便于从 lParam 中提取这两个值。(第 4 章将介绍这些宏的用法)

    有时,由于对 DefWindowProc 的调用,一些消息还会产生其他的消息:

    (WM_SYSCOMMAND ——>WM_PESTROY——>WM_QUIT)

    例如,假定你已运行 HELLOWIN,并且用鼠标单击了 [关闭] 按钮,或假定使用键盘或鼠标选择了系统菜单中的 [关闭] 选项,那么 DefWindowProc 会对该键盘或鼠标输入进行处理。当该函数检测到你已选择 [关闭] 选项时,便会向窗口过程发送一条 WM_SYSCOMMAND 消息,WndProc 会将该消息传给 DefWindowProc。

    作为响应 DefWindowProc 又会给窗口过程发送一条 WM_CLOSE 消息。WndProc 再次将该消息传给 DefWindowProc。 此时 DefWindowProc 会通过调用 Destroy Window 函数来对 WM_CLOSE 做出响应。Destroy Window 则会导致 Windows 向窗口过程发送一条 WM_PESTROY 消息。WndProc 最终又会通过调用 PostOuitMessage 将一 条 WM_QUIT 消息投递到消息队列中来作为对该消息的响应。这条消息会使 WinMain 中的消息循环结束,并使程序终止。(这种传来传去就当了解消息深层传递过程了)

    队列消息和非队列消息:

    一个 Windows 程序同时还具有一个消息循环用于从消息队列中检索和分发消息,其中检索消息是通过调用 GetMessage 实现的,而分发消息是通过调用 DispatchMessage 而实现的。

    那么,是 Windows 程序捕获到消息 (非常类似于字符模式程序对键盘输入的捕获) 然后将这些消息转送到某个目的地呢? 还是程序窗口直接从外部接收消息呢? 答案是两者皆有。

    消息既可以是 “队列消息”,也可以是 “非队列消息”:

    队列消息是指那些由 Windows 放入程序的消息队列中的消息。在程序的消息循环中,消息被检索,然后被投递到窗口过程中。非队列消息则是由 Windows 对窗口过程的直接调用而产生的。我们一般说队列消息被 “投递” (post) 到消息队列中,而非队列消息则是被 “发送" (send) 到窗口过程。无论在哪种情形下,窗口过程都会为窗口获取并处理所有消息——无论是队列消息还是非队列消息。因此,窗口过程实际上是窗口的 “消息中心”。

    队列消息主要由用户的输入产生:

    主要形式为按键消息 (例如 WM_KEYDOWN 和 WM_KEYUP 消息)、由按键产生的字符消息 (WM_CHAR)、鼠标移动 (WM_MOUSEMOVE)、鼠标单击 (WM_LBUTTONDOWN) 等。此外,队列消息还包括定时器消息 (WM_TIMER)、重绘消息 (WM_PAINT) 和退出消息 (WM_QUIT)。

    非队列消息则包括队列消息以外的其他所有消息:

    非队列消息通常由调用特定的 Windows 函数引起:

    例如当 WinMain 调用 CreateWindow 函数时,Windows 就会创建窗口,并在创建过程中向窗口过程发送一条 WM_CREATE 消息。

    当 WinMain 调用 ShowWindow 函数时,Windows 又会将 WM_SIZE 消息和 WM_SHOWWINDOW 消息发送给窗口过程。

    接下来,WinMain 又对 UpdateWindow 进行了调用,这就促使 Windows 向窗口过程发送一条 WM_PAINT 消息。

    有时键盘或鼠标输入的队列消息也能够产生非队列消息:

    例如,当用键盘或鼠标选择某个菜单项时,键盘或鼠标消息会进入消息队列,而最终表明有某菜单项被选中的 WM_COMMAND 消息却是一个非队列消息。(注意这里选择和选中是不同的)

    消息的有序和同步:

    从窗口口过程的视角看,消息是以有序、同步的方式到来的。窗口过程可以选择对这些消息进行某种处理或干脆直接忽略掉。有序、同步的第一层含义是指消息与硬件中断不同。在窗口过程处理某一消息的过程中,程序不会被其他消息突然中断。

    虽然 Windows 程序可有多个执行线程,但消息循环和窗口过程不是并发运行的。当一个消息循环从其自身的消息队列中检索消息,并调用 DispatchMessage 函数将检索到的消息发送给窗口过程时,只有在窗口过程将控制权返还给 Windows 后,DispatchMessage 才会返回。(就是有阻塞, 相当于同步执行)

    窗口过程的内部处理:

    窗口过程在处理信息过程中可以调用为其发送其他消息的函数。这种情形下,在该函数调用返回之前,窗口过程必须将第二个消息处理完毕,此时窗口过程才处理前一条消息。

    例如,当一个窗口过程在处理一个消息时调用 UpdateWindow 时,Windows 会以一条 WM_PAINT 消息来调用窗口过程。当窗口过程处理完 WM_PAINT 消息后,UpdateWindow 调用才将控制权返还给窗口过程来处理上一层消息。(这里的第二个消息就是 UpdateWindow 产生的消息)

    这就意味着窗口过程必须是可重入的 (reentrant):

    假定有一个静态变量决定程序跳转或其它重要信息,如果在窗口过程所调用的特定 Windows 函数产生了另外一条消息,而窗口过程在处理第二条消息期间对该变量进行了修改,则该变量的状态一定会发生改变。

    即我们无法保证变量会和先前的一样,而这也是我们在编译 Windows 程序时需要将某些编译优化关闭的原因之一。

    所以在许多情况下,窗口过程必须保留其从消息中获取的信息,并在处理其他消息时使用该信息。这种信息必须保存在窗口过程所定义的静态变量中,或保存在全局变量中。

    程序的卡死现象解析:

    Windows 98 和 Windows NT 都是抢占式多任务环境。这就意味着当一个程序完成一项非常耗时的工作时,Windows 允许用户将控制权切换给其他程序。这是一件非常好的事,而这也体现了当前版本的 Windows 相对于以前的 16 位版本的优越性。

    但是,由于 Windows 的构建方式,这种抢占式多任务模式未必总会按照你期望的方式工作。例如,假定你的程序处理某条特定消息需要花费一至两分钟的时间。的确,在此期间,用户可以切换到其他程序。但是用户无法对你的程序做任何操作。用户不能对你的程序窗口进行移动,也不能对窗口采取调整尺寸、最小化或关闭等操作,什么都不能做。这是因为你的窗口过程正在执行一项非常耗时的任务。表面上看起来窗口过程并没有执行移动或尺寸调整的操作,但实际上它确实在这样做。这部分工作由 DefWindowProc 函数负责,在定义窗口过程时必须将其考虑在内。(就是已经在队列中了)

  • 相关阅读:
    Debezium的基本使用(以MySQL为例)
    Mac命令关闭代理设置
    【bug】使用mmsegmentaion遇到的问题
    过采样、欠采样
    Flutter 3.24 更新详解
    Spring MVC里的DispatchServlet(结合Spring官网翻译)
    奋战开源操作系统二十年:为什么编程语言是突破口?
    Web服务器(01)——介绍web服务器
    银行卡四要素验证API接口文档:支持手机号归属地验证
    OpenAI将发布DALL·E3,多模态输出模式引爆热点
  • 原文地址:https://blog.csdn.net/xiao__1bai/article/details/126964097