第四天课程 — 消息、WindowsX 和绘制正文
本章主要介绍两个内容:
1.借助于 WINDOWSX.H,如何写出可读性较强、面向事件的可移植代码?
2.最基本的 I/O 服务:如何显示正文。
由于 Windows 是面向事件(或基于消息)的系统,所以它才有许多特征并形成特殊的风格。如果你不 理解消息。你也就不能理解 Windows。
在本章你有机会学习面向事件、基于消息的系统。你将会对窗口过程、消息、消息分析器作深入了解。 这些内容在 Windows 中处于非常核心的位置。
在本章重点介绍如下内容:
- 消息
- WindowsX
- 消息分析器
- 对 WM_PAINT 消息的响应
- 设备描述表
- 在窗口上输出正文的两种方法
4.1 WindowsX 和 STRICT
有两种有点独特的技巧帮你编写清晰的可移植代码。WINDOWSX.H 包含一系列宏,它有助于用一种简单 易读的方式处理消息,并且能同时产生可以在 WIN16 和 WIN32 中移植的代码。
由操作系统发送的消息带有两个参数,其类型为 wParam 和 lParam。在 WIN16 中 wParam 为一个 16 位的值,而在 WIN32 中 wParam 为一个 32 位的值。lParam 在两种操作系统中都是 32 位的值。 这种不同意味着在 WIN32 和 WIN16 中使用了不同的编码。WindowsX 中的消息分析器为你把这些实现的细 节隐藏起来。
WindowsX 还提供了很多宏,有助于你书写清晰且简单易懂的 WndProc。这些宏使你的程序变得简洁优 美。而这些好处通常只有通过复杂的面向对象的系统如 OWL 或 MFC 才能取得。没有 WindowsX,即使最好的 WndProc 也可能会把那些难以控制的代码拉长好几页。
WINDOWSX.H 帮你避开对大多数的 Windows API 调用要做的类型转换。一般说来,Windows 的程序 设计需要做大量的类型转换。所有的这些都避免了常规的类型检查过程,而这些检查是书写安全的代码所不 可或缺的。WINDOWSX.H 让你避免类型转换并坚持把类型检查放在首位。
WINDOWSX.H 包含整整一系列子控制宏,它们使你很容易地书写单行过程而不再用复杂的 SendMessage 命令。这些单行命令在 WIN16 和 WIN32 可移植,而等价的 SendMessage 或 PostMessage 语法不需 要是可移植的。传送给 SendMessage 的参数通常需要把信息包装在 wParam 和 lParam 中,这是一个 复杂而易错的过程。WindowsX 避开了所有这些麻烦事,用一个清晰、易读的界面取代了使用 SendMessage。
最后,WINDOWSX.H 还包含了一系列 API 宏,它可以让你使用标准的 Windows 函数(诸如 SelectObject 函数和 GetStockObject 函数)更容易。每次使用这些函数,你都要最少做两次艰难的类 型转换。WindowsX 允许你跳过类型转换,从而写出简单、清晰的函数,这些函数使用诸如 SelectFont 函 数和 GetStockBrush 函数。
总之,WindowsX 提供了三个工具:
1.消息分析器。
2.子控制宏。
3.API 宏。
这三个工具能帮助你写出清晰、可移植的、可读性好的代码。
STRICT 伪指令帮你记住传给函数的参数类型。例如,有时会在代码中用了 HWND 类型,而此处 Windows 期望的是 HDC 类型。如果你定义了 STRICT,你就不会发生这种情况。STRICT 强制你要遵守很多规则来帮你 建立可移植的正确的程序。
4.2 消息是什么
在第三章中,我们浏览了一个标准的 Windows 程序,匆匆地了解了一下 Windows 程序所有主要的部 分。这些主要部分包含有 WinMain,Register,Create 和 WndProc 以及消息循环。下一步是缩小视 野,从更近的距离去看看窗口过程和发给它的消息。
现在你应该对消息(或事件驱动程序)和标准的过程式程序之间的差别有了一定感受。对事件驱动程 序,由操作系统告诉程序有一个事件发生了。而对过程式程序来说是由程序向系统查询才能知道发生了什 么。
从下面两点可以更进一步看到传统的 DOS 和传统的 Windows 程序设计之间的不同:
·Windows 基于消息的模式: 一旦程序启动,它只是简单地等待发送给它的消息,然后作出相应的响应。由 Windows 本身去检测是否有 一个键被按下或是否有鼠标移动。当这类事件发生时,Windows 就向程序发送一条预定义的消息,告诉它发 生了什么。程序一般都有响应消息或不理睬消息的选项。
·DOS 过程模式: C++ 代码通常是线性地执行,也就是从程序的开头到结束,每个时刻执行一行代码,或者转移到某行代 码,或者循环执行,以这几种方式运行各段代码。程序要想知道发生了什么,必须通过调用基于中断的子程 序。这些子程序是操作系统内部建立或由硬件实现的。子程序返回时,报告是否有一个键被按下或鼠标做了 移动。
消息实际上只是在 WINDOWS.H 文件复合体中定义的常量,现在对此应不感到奇怪了。作为例子,下 面列出有关键盘处理和鼠标移动的消息说明:
// Keyboard messages #define WM_KEYDOWN 0x0100 // Key was pressed #define WM_KEYUP 0x0101 // Key was released #define WM_CHAR 0x0102 // Processed keystroke #define WM_DEADCHAR 0x0103 // Composite key #define WM_SYSKEYDOWN 0x0104 // Alt key was pressed #define WM_SYSKEYUP 0x0105 // Alt key was released #define WM_SYSCHAR 0x0106 // Processed system keystroke #define WM_SYSDEADCHAR 0x0107 // Composite system keystroke // Mouse input messages #define WM_MOUSEMOVE 0x0200 // Mouse was moved #define WM_LBUTTONDOWN 0x0201 // Left button pressed #define WM_LBUTTONUP 0x0202 // Left button released #define WM_LBUTTONDBLCLK 0x0203 // Double click of left button #define WM_RBUTTONDOWN 0x0204 // Right button pressed #define WM_RBUTTONUP 0x0205 // Right button released #define WM_RBUTTONDBLCLK 0x0206 // Double click of right button #define WM_MBUTTONDOWN 0x0207 // Middle button down #define WM_MBUTTONUP 0x0208 // Middle button up #define WM_MBUTTONDBLCLK 0x0209 // Double click of middle button
不要试图记住这些消息。只要大致看一看,熟悉一下它们的外观和所提供的服务类型就可以了。
显然,关于消息本身并没有什么神秘之处。它们只是具有有用名字的简单常量,用来通知程序系统当前 的状态。当一个事件发生时,这些消息与其它有用的信息捆绑在一起发送给一个或多个应用程序窗口过程。 本章的主题就是介绍对这些消息确切地做什么。
4.3 第二个完整的 Wnidows 程序
本章下面几节将集中介绍由三部分构成的过程,它确切地勾画出消息在一个 WndProc 中是如何处理的:
·第一步是准备好一个例程并运行它。
·第二步是讨论 WindowsX 和消息分析器。讨论主要集中在两个特定的消息 WM_DESTROY 和 WM_CREATE。
·第三步是简明扼要地介绍 WindowsX 是如何对待默认的窗口过程。
我准备让你看的程序的大部分代码与你在第三章中看到的代码很相似。大多数 Windows 程序都基于一个 公共的模板,从一个程序变到另一个程序只需要做些很小的改动。这样,你就可以把生成 WINDOW1.EXE 所用到的文件复制到一个新的子目录 EASYTXET 中。把这些文件中的“Window1”这个字统统改为“EASYTEXT”,并 把文件名从 WINDOW1.* 改为 EASYTEXT.*。
-
注意事项:
- 应该用已测试过的代码作为每个你要写的程序的基础。
- 不要从头开始编写每个 Windows 程序。
上述这些步骤是十分重要的。你要写的每一段 Windows 代码几乎都可以用第三章所建立的基本内容为基 础,也就是说这些代码的可重用性很强。你可能知道,在大多数现代化的程序设计的努力中,重用代码的思想 是十分重要的。
现在我们看一看 EasyText 程序,它在程序清单 4.1 中列出。将这个程序准备好运行,它将帮你理解 本章的其余内容。
程序清单 4.1 EasyText.cpp /// // Program Name:EASYTEXT.cpp // Programer: skyline // Description: Demonstrate simple text I/O /// #define STRICT #include <windows.h> #include <windowsx.h> #include <string.h> #pragma warning (disable:4068) #pragma warning (disable:4100) static char szAppName[]="EasyText"; #define skyline_DefProc DefWindowProc BOOL skyline_OnCreate(HWND hwnd,CREATESTRUCT FAR * IpCreateStruct); void skyline_OnDestroy(HWND hwnd); void skyline_OnPaint(HWND hwnd); char buf[100]; static HWND MainWindow; static HINSTANCE hInst; BOOL Create(HINSTANCE hInst,int nCmdShow); BOOL Register(HINSTANCE hInst); LRESULT CALLBACK WndProc(HWND hwnd,UINT Message, WPARAM wParam,LPARAM lParam); // // The WinMain is the program entry point. // #pragma argsused int WINAPI WinMain(HINSTANCE hInst,HINSTANCE hPrevInstance, LPSTR lpszCmdParam,int nCmdShow) { MSG Msg; if (! hPrevInstance) if (! Register(hInst)) return FALSE; if(! Create(hInst,nCmdShow)) return FALSE; while(GetMessage(&Msg,NULL,0,0)) { TranslateMessage(&Msg); DispatchMessage(&Msg); } return Msg.wParam; } // Register the window BOOL Register(HINSTANCE hInst) { WNDCLASS WndClass; WndClass.style = CS_HREDRAW|CS_VREDRAW; WndClass.lpfnWndProc = WndProc; WndClass.cbClsExtra = 0; WndClass.cbWndExtra = 0; WndClass.hInstance = hInst; WndClass.hIcon = LoadIcon(NULL,IDI_APPLICATION); WndClass.hCursor = LoadCursor(NULL,IDC_ARROW); WndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); WndClass.lpszMenuName = NULL; WndClass.lpszClassName = szAppName; return (RegisterClass(&WndClass)!=0); } // Creat the Window and show it. BOOL Create(HINSTANCE hInst,int nCmdShow) { HWND hwnd = CreateWindowEx (0,szAppName,szAppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT, NULL,NULL,hInst,NULL); if(hwnd == NULL) return FALSE; ShowWindow(hwnd,nCmdShow); UpdateWindow(hwnd); return TRUE; } // ------------------------------- // WndProc and Implementation // ------------------------------- / // Thw Window Procedure LRESULT CALLBACK WndProc(HWND hwnd,UINT Message, WPARAM wParam,LPARAM lParam) { switch(Message) { HANDLE_MSG(hwnd,WM_CREATE,skyline_OnCreate); HANDLE_MSG(hwnd,WM_DESTROY,skyline_OnDestroy); HANDLE_MSG(hwnd,WM_PAINT,skyline_OnPaint); default: return skyline_DefProc(hwnd,Message,wParam,lParam); } } /// // Create window // Load the bitmap from resource /// #pragma argsused BOOL skyline_OnCreate(HWND hwnd, CREATESTRUCT FAR * lpCreateStruct) { strcpy(buf,"Try resizing this window."); return TRUE; } // The destructor Handles WM_DESTROY /// #pragma argsused void skyline_OnDestroy(HWND hwnd) { PostQuitMessage(0); } // Handle WM_PAINT Messages // Show how to use TextOut and DrawText #pragma argsused void skyline_OnPaint(HWND hwnd) { PAINTSTRUCT PaintStruct; RECT rect; HDC PaintDC = BeginPaint(hwnd,&PaintStruct); SetBkMode(PaintDC,TRANSPARENT); TextOut(PaintDC,10,10,buf,lstrlen(buf)); GetClientRect(hwnd,&rect); DrawText(PaintDC,"The middle of the window",-1,&rect, DT_SINGLELINE|DT_CENTER|DT_VCENTER); EndPaint(hwnd,&PaintStruct); }
这个程序向你显示了如何使用消息分析器和如何响应 WM_PAINT 消息,并在窗口中打印两段正文。注意 其中一段正文始终处于窗口的中央,即使改变了窗口的大小也是如此。
4.4 Switch 语句、WindowsX 和消息分析器
这个程序最明显的新特点是在窗口的客户区出现正文。在介绍如何在屏幕上绘制正文之前,理解 WndProc 函数采用的机制是很重要的。
LRESULT CALLBACK WndProc(HWND hwnd,UINT Message, WPARAM wParam,LPARAM lParam) { switch(Message) { HANDLE_MSG(hwnd,WM_CREATE,skyline_OnCreate); HANDLE_MSG(hwnd,WM_DESTROY,skyline_OnDestroy); HANDLE_MSG(hwnd,WM_PAINT,skyline_OnPaint); default: return skyline_DefProc(hwnd,Message,wParam,lParam); } }
回想起 WINDOWSX.H 出现以前的那些糟糕的日子,那时窗口过程可能被看成极可怕的敌人,Windows 程 序设计人员必须每天与之奋战。麻烦的根源是窗口过程声名狼藉的 switch 语句,甚至中等复杂程度的程序所 包含的 WndProc 都具有延伸许多页、让人摸不着边际的倾向。
现在有了 WINDOWSX.H,而 switch 语句仍然存在。但它被一系列消息分析器十分有效的驯服了。这就是 按照结构化程序设计的规则,把对消息响应的那段程序转移到 WndProc 之外的函数中。当然,使用消息分析 器不是一个绝对不费力的过程。但我相信你会发现它们比长长的 switch 语句容易许多。
EasyText 程序明显地处理三个消息:
HANDLE_MSG(hwnd,WM_CREATE,skyline_OnCreate);
HANDLE_MSG(hwnd,WM_DESTROY,skyline_OnDestroy);
HANDLE_MSG(hwnd,WM_PAINT,skyline_OnPaint);
正如你所看到的,这些消息是 WM_CREATE,WM_DESTROY 和 WM_PAINT。在下面几节中,你会看到消息 分析器不仅对这些消息,而且对所有标准的 API 消息的使用都进行了简化。
在阅读有关这些问题的介绍时,应该记住 WindowsX 帮助你避开复杂性。不幸的是,为了解释清楚消息 分析器是如何工作的,我必须深入到这个复杂的核心中去。但是,在你的脑子中对一些基本概念清楚之后,你 会发现消息分析器其实在为你一次又一次铺平道路。
好了,现在你应该集中精神,下面几节包含了大量的重要信息。
首先,你应该知道 WM_DESTROY 消息送给一个窗口是在窗口准备关闭的时候。在一个应用程序主窗口,你 要记得调用 PostQuitMessage 函数以响应一条 WM_DESTROY 消息,这是关键的一点。如果不这么做,你马 上就会卷进一大堆麻烦中永不抽身,除非你能想出错误的根源所在。
附注:
PostQuitMessage 函数发送一条 WM_QUIT 消息给 Windows,这是打断 WinMain 中消息循环的信号。 也就是说,如果不调用 PostQuitMessage 函数,消息循环就会无限期继续下去。因此,对于正常终止的应用 程序来说,这个调用是绝对需要的。
在 EasyText 程序中,PostQuitMessage 的调用出现在 skyline_OnDestroy 函数中:
void skyline_OnDestroy(HWND hwnd) { PostQuitMessage(0); }
如果没有使用 WindowsX,这整个过程将在 WndProc 中处理:
switch(Message) { case WM_DESTROY: PostQuitMessage(0); break; case WM_PAINT: etc ... }
有了 WindowsX,程序员就可以做另一种选择,把 WM_DESTROY 消息放在一个单独的函数中去处理,这 有助于设计出良好的结构化程序,
在 WINDOWSX.H 源代码中,你可以看到消息分析器宏。这些代码对传给 WndProc 的参数进行分解(Pick apart)。然后,这些宏把找出的重要参数传给消息处理函数。传送给一个窗口的消息可以伴随一些附加的信 息,这些信息通常以 hwnd,wParam 和 lParam 参数的形式出现。
用 hwnd,wParam 和 lParam 参数发送消息的结果带来三个问题:
1.并不是所有的消息都用到这三个参数。这样,程序设计人员必须去找参考书看一看,以便了解哪个消 息会用到哪些参数。
2.不同的信息常常会被装进一个特定的参数。这样,一个程序员可能需要从 lParam 的第一个字节中取 得一些信息,又要从 wParam 的第一个和第二个字节中取得另外一些信息,在作这些工作的过程中,程序员 还常常需要在使用这些信息之前对它们作类型转换。
3.装入 lParam 和 wParam 中的信息的方式有时也是不同的,这取决于你所使用的是 16 位的 Windows 还是 WIN32。
通过学习我们已经了解到,消息分析器实际上是一系列的宏,用来把传给 WndProc 的各种参数分离出 来。这些宏通常由两部分,第一部分是处理消息本身,第二部分则有选择地把消息传给 DefWindowProc。例 如,下列是 WM_DESTROY 消息宏,它们列在 WindowsX 中:
/* void Cls_OnDestroy(HWND hwnd) */ #define HANDLE_WM_DESTROY(hwnd, wParam, lParam, fn) / ((fn)(hwnd), 0L) #define FORWARD_WM_DESTROY(hwnd, fn) / (void)(fn)((hwnd), WM_DESTROY, 0L, 0L)
语法
处理消息的函数说明
ClassName_OnMessageName
从 WindowsX 中摘抄的 WM_DESTROY 的开学有一句注释,它显示了怎样说明一个函数来响应 WM_DESTROY 消息。也就是说,你只要把这句注释复制下来:
/* void Cls_OnDestroy(HWND hwnd); */
然后把注释符号去掉,再把代码中的第一个字符改为你的“类”名:
void skyline_OnDestroy(HWND hwnd);
你复制的函数的原型叫做消息处理函数或者消息响应函数。一个消息处理函数的命名习惯是:首先是类 名,之后跟一个下划线,然后是“On”,最后是消息名字。
例如,在 EasyText 中,按上述命名规则,一个函数名为:
skyline_OnDestroy
你可以把这个名字读作“skyline 类在收到一条 WM_DESTROY 消息时执行这个函数”。当然,它们 不是真正的面向对象代码处理的“类”。不过 WindowsX 提供一些同样的句法上的改进,使你书写的代码 更清晰。
从 WindowsX 中摘录的 WM_DESTROY 最重要的部分是 HANDEL_WM_DESTROY 宏的定义。这个代码段 把传给 WndProc 的参数分离出来,在这个特定情况下,只挑出 hwnd 作为重要的东西:
((fn)(hwnd),0L)
我重申一下,一定要搞清楚这个十分重要的概念。这个宏的目的是把传给 WndProc 的重要参数赋为 0,这就是消息分析器的工作,在上述例子中,lParam 和 wParam 参数中都不包含任何有用的信息,而 只有一个与信息相关的重要参数是 hwnd,它由 WM_DESTROY 消息分析器及时传出。
消息分析器的第二部分是调用 FORWARD_WM_DESTROY:
#define FORWARD_WM_DESTROY(hwnd,fn)/
(void)(fn)((hwnd), WM_DESTROY, 0L, 0L)
只有你想把消息传给另一个函数,比如将处理过的消息传给默认的窗口过程时,才会用到宏 FORWARD_WM_DESTROY。为了使这部分处理工作能够正确进行,按上面所说的那样,把默认的窗口过程包装 在一个包含类名字的宏中。
WM_DESTROY 消息分析器是开始学习消息分析器的最理想的例子,因为它太简单了。这个例子也能帮助 你了解比较复杂的宏。而且,下面介绍的 WM_CREATE 消息分析器也会很好地说明了 WindowsX 如何何分离 更复杂的宏。
下面是 WindowsX 中的 WM_CREATE 消息分析器:
/* BOOL Cls_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct) */ #define HANDLE_WM_CREATE(hwnd, wParam, lParam, fn) / ((fn)((hwnd), (LPCREATESTRUCT)(lParam)) ? 0L : (LRESULT)-1L) #define FORWARD_WM_CREATE(hwnd, lpCreateStruct, fn) / (BOOL)(DWORD)(fn)((hwnd), WM_CREATE, 0L, (LPARAM)(LPCREATESTRUCT)(lpCreateStruct))
正如你看到的那样,HANDLE_WM_CREATE 宏将 lParam 转换成一个指向 CREATESTRUCT 的指针,这 样保证这个参数的正确类型传给 skyline_OnCreate 函数:
BOOL skyline_OnCreate(HWND hwnd,
CREATESTRUCT FAR * lpCreateStruct)
此时,你不必仔细考查 CREATESTRUCT 结构的各个域,所以我也不在此将它们列出。如果你对此感兴 趣,可以在 WINDOWSX.H 中找到。
如果没有消息分析器,你就必须像下面一样自己处理类型转换:
lpCreateStruct = (CREATESTRUCT FAR *)lParam;
这是一件令人讨厌的工作,因为它是很容易出错的过程,而且对于初学 Windows 环境的人来说尤其感 到棘手和容易出错。
你可能注意到这个宏不是返回 0 就是返回 -1。如果一个程序响应 WM_CREATE 消息则返回 0,这个 程序就正常继续运行。如果它返回 -1,窗口就会立即关闭,CreateWindow 函数将返回 NULL。
这类细节常常让人糊涂。因此 WindowsX 采用把 Cls_OnCreate 说明为 BOOL,就把这些问题一下 子解决了。这样,根据你能否对程序要访问的内存域进行初始化的情况,你所要做的是返回 TRUE 或 FALSE。如果返回 FALSE,窗口就关闭了并且你的应用程序也终止了。
4.5 HANDLE_MSG 宏
我希望前几节的介绍已向你说明了消息分析器是如何消除了复杂性。下面要介绍的宏 HANDLE_MSG 可以更进一步简化你的程序。它在 WINDOWSX.H 中说明如下:
#define HANDLE_MSG(hwnd, message, fn) /
case (message): return HANDLE_##message((hwnd),
(wParam), (lParam), (fn))
这个宏的作用是双重的:
1.它消除了冗长的 case 语句,这种 case 语句在标准的 WndProc 中实在令人烦恼。
2.使你摆脱必须传送消息处理函数返回值的责任。(这个函数用 Cls_OnXXXX 模式说明。)
返回你的函数的结果是一项重要责任。事实上你必须传送这个值,甚至你的消息处理函数说明为 Void 也必须如此。在这种情况下,宏总是返回 0L。不过,如果你使用 HANDLE_MSG 宏,你再也不必为返回值担心了。
如果你不用 HANDLE_MSG,你必须使你的宏返回一个值。更明显的是,对 WM_CREATE 消息,每次使用消息处理函数时,在你的 WndProc 中都要写下列代码:
switch(Message) { case WM_CREATE: return HANDLE_WM_CREATE(hwnd,wParam,skyline_OmCreate); etc ... }
HANDLE_MSG 让你书写下列代码就摆脱了这个责任:
switch(Message) { HANDLE_MSG(hwnd,WM_CREATE,Cls_OmCreate); etc ... }
每个程序员在编码时似乎都有不同的重点。对我来说,我非常重视能帮我书写清晰易读的代码的技术。所以我求助于 HANDLE_MSG 宏。它帮我写出了尽量简单和直接了当的代码。
4.6 WindowsX 和默认的窗口过程
你可以回顾一下第三章的内容。在应用程序的生存期中,WndProc 函数通常会受到大量消息的轰击。甚至会遇到这种情况,消息象暴风雨落在你头上一样倾泻而下。在许多应用程序中,大多数这样的消息是由默认窗口来处理的。这一节的目的就是介绍 WindowsX 如何处理 DefWindowProc。
在使用 WindowsX 的应用程序中,标准的操作过程是把宏包含在一个叫做 DefWindowProc 的 Windows API 函数中。
#define skyline_DefProc DefWindowProc
这样做的主要原因是防止你犯粗心的错误。
在有些情况下,窗口可能不把它的消息传给 DefWindowProc。例如,某些窗口把消息传给其他函数,例如 DefDlgProc 或 DefMDIChildProc(现在不必考虑什么时候和为什么要用到这些函数)。
问题在于,Windows 程序设计人员总是把一个应用程序剪贴到另一个程序中,这容易产生错误。比如你想调用 DefWindowProc 时却调用了 DefMDIChildProc。
为了防止这类错误,WindowsX 程序设计人员只需调用 DefWindowProc 宏时建立这个窗口类的名字。这有助于程序员集中精力在这个主要的问题上:为这个特定类找到正确的默认窗口过程。这样你只要在代码的一个地方做了改变就会波及到整个程序。
WindowsX 程序设计风格的另一个重大改进它具有面向对象的外貌。OOP 代码之所以非常有用是因为它可读性好。例如,对默认窗口过程的调用显著地标记出它是对 skyline 窗口类的:
return skyline_DefProc(hwnd,Message,wParam,lParam);
程序员要想写出清晰易懂的代码,指明类型是非常重要的。它符合现代程序设计实践的基本原则。这种实践是经历 10 年到 20 年耗费大量时间和经过许多争论之后才形成的。
注意事项:
·应该说明一个宏用来处理你的窗口的默认的窗口过程。
·在你的窗口类定义之后,不要忘记命名宏,也不要忘记确认一下你为该类调用的是正确的窗口过程。
4.7 消息分析器小结
在我们进入对重要的 WM_PAINT 消息讨论之前,对前面几节扼要回顾一下是值得的。
消息分析器有助于简化 Windows 程序和避免粗心所造成的错误。它通过三种方法达到这种目的:
1.它使 WndProc 中很典型的冗长的 case 语句解体,这有助于你的代码成为良好的结构程序设计实践。
2.它有助于分离或者说扫描 hwnd、lParam 和 wParam 参数并标出最重要的参数做适当的类型转换。
3.它把与同一个窗口类相关的所有函数统一用同样的题头,这是凭借 MYClass_OnXX 的命名规则作到的。
好了,就说到此,我不在介绍任何更多的细节问题。此后,消息分析器就会逐渐得到承认,并被当作在窗口过程中处理消息使用的唯一的合法的非 OOP 方法。我相信事实上也是如此。
下面显示的 EASYTEXT.CPP 中的一小段代码可能很好地概括了上述讨论中的大多数内容:
#define skyline_DefProc DefWindowProc BOOL skyline_OnCreate(HWND hwnd,CREATESTRUCT FAR * IpCreateStruct); void skyline_OnDestroy(HWND hwnd); void skyline_OnPaint(HWND hwnd);
这五行代码对 EasyText 窗口类的全部功能做了概括。当第一次读 EASYTEXT.CPP 程序或其他的一些程序时,首先要对程序中与上述代码类似的部分浏览一下,它们会立即向你指出程序中哪些地方是你注意的重点。如果在这个模块中有三、四个不同的“类”,你只要对这些类的每一个定义都看看,就可以立即知道它们是什么以及彼此之间差别所在了。
这种简明性帮助你尽快地组织代码和使代码变清晰。让其他程序员继续在 Void 中挣扎吧,WindowsX 不管这些。
4.8 WM_PAINT 消息的发送
发给一个窗口过程的每条消息都是重要的。但无论如何 WM_PAINT 消息给我留下如此深刻的印象——它处于消息中突出的地位,很多应用程序围着它转。当然,没有人否认在几乎所有 Windows 应用程序的生存期中,WM_PAINT 消息是一个充满生命活力的程序。
每次需要重新绘制窗口的客户区时,都要向窗口发送一条 WM_PAINT 消息。程序员说的一个窗口的客户区,通常是指窗口边界之内标题栏之下的那块区域。程序员通常在这部分客户区中绘制,而 Windows 则关心其余部分。
如果你对客户区域这部分空间没有特别进行控制,Windows 则用特定的背景色把它填满。也就是说,如果你不能从默认的窗口过程得到所要的消息,就不能阻止它这么做。如果你没有传送一些消息给 DefWindowProc 指出需要系统做附加的处理,就可能会出现麻烦。这是后面要介绍的内容。
当一个窗口需要绘制或重新绘制时,操作系统将发送一条 WM_PAINT 消息给这个窗口。如果一个窗口被另一个窗口遮住了,突然间去掉了遮盖物,它收到一条 WM_PAINT 消息。它得到这条消息是因为它需要重新绘制被另一个窗口遮住的部分。如果一个窗口为最大化或最小化,当它恢复正常大小时收到一条 WM_PAINT 消息。如果用户从一个全屏幕的 DOS 窗口回到 Windows 环境中,在桌面上所有可见的窗口都收到 WM_PAINT 消息。
4.9 BeginPaint,EndPaint 和设备描述表
让我们更近一步看看 EasyText 程序的 WM_PAINT 消息处理函数:
void skyline_OnPaint(HWND hwnd) { PAINTSTRUCT PaintStruct; RECT rect; HDC PaintDC = BeginPaint(hwnd,&PaintStruct); SetBkMode(PaintDC,TRANSPARENT); TextOut(PaintDC,10,10,buf,lstrlen(buf)); GetClientRect(hwnd,&rect); DrawText(PaintDC,"The middle of the window",-1,&rect, DT_SINGLELINE|DT_CENTER|DT_VCENTER); EndPaint(hwnd,&PaintStruct); }
为了准确地帮助你理解这个函数是如何工作的,我把其中某些东西去掉,只留下最基本的部分:
void skyline_OnPaint(HWND hwnd) { PAINTSTRUCT PaintStruct; HDC PaintDC = BeginPaint(hwnd,&PaintStruct); EndPaint(hwnd,&PaintStruct); }
这个例子可以作为你要建立的所有绘制例程的样板。这里的基本成分是调用 BeginPaint 和调用 EndPaint。还要注意变量 PaintDC 和 PaintStruct。
BeginPaint 函数的关键作用是让你访问一个窗口的设备描述表(device context),即 HDC。对那些从 DOS 转到 Windows 的人来说,设备描述表是一个全新的概念。因此,你应该在此处的简单介绍中特别注意 Windows 程序设计的这个关键概念。
设备描述表将你的程序与某些安装在计算机上的外部输出设备连接起来。尤其是许多设备描述表形成你的应用程序和打印机或显示卡之间的接口。
可以肯定地说,如果现在还没有出现设备描述表,总会有人发明它。它们成为任何健壮的程序设计环境的最基本部分。一直到 Windows 出现时,所有的 DOS 程序设计人员才自觉或不自觉地感到他们没有设备描述表的不足。
设备描述表使得处理各种各样的显示卡或打印机变得很容易。过去,每个应用程序需要它自己的驱动程序来运行这些不同的设备。为了解决这个问题,Windows 使得硬件输出设备制造商有可能开发出它的驱动程序,这种驱动程序可以挂到任何 Windwos 程序中去。这种处理的关键是设备描述表的开发,它将你的应用程序、设备驱动程序和实际的硬件设备连接起来。硬件设备(例如打印机或显示卡)由设备驱动程序控制,Windows 程序通过设备描述表与设备驱动程序对话。由于设备描述表的存在,使得你写出的一组代码在(至少理论上如此)多种设备上运行成为可能。
现在,你应该明白每次绘制过程都从调用 BeginPaint 开始,这一点为什么如此重要。BeginPaint 是一个函数,它为一个特定的窗口去检索设备描述表。当你试图显示图形时,如果没有设备描述表,就什么也做不成。
警告:
可以用几种不同的方法从系统中得到设备描述表。最常用的是调用 GetDC 去得到。在我看来,GetDC 的名字很能说明它的功能而且又容易使用,因此就存在着用 BeginPaint 的地方用了 GetDC 的诱惑。但是,永远不要这么做——这将引起各种费解和不确定的行为。BeginPaint 是专门设计用来配合 WM_PAINT 消息来使用。如果你试图不用它进行工作,当你要把一个窗口从另一个窗口后面放到它前面时或者要重新绘制窗口时,你的窗口就有可能不会正确地响应。
注意事项:
·为 WM_PAINT 消息处理函数检索设备描述表应该用 BeginPaint。
·在 WM_PAINT 消息处理函数内部不要使用 GetDC。
·在你需要访问当前设备描述表但不是用来响应 WM_PAINT 消息时应该用 GetDC。
只有把 BeginPaint 作为一个绘制函数的开始才是最适合的,EndPaint 最适合用来关掉它,应确保对每个 WM_PAINT 消息的响应中都用这两个函数包括起来。
BeginPaint 和 EndPaint 函数都用 HWND 和 PAINTSTRUCT 的地址作为参数。下面是一个 PAINTSTRUCT 结构所包含的字段:
typedef struct tagPAINTSTRUCT { HDC hdc; BOOL fErase; RECT rcPaint; BOOL fRestore; BOOL fIncUpdate; BYTE rgbReserved[16]; } PAINTSTRUCT;
PAINTSTRUCT 结构的各个字段说明如下:
·HDC:窗口的设备描述表的副本。
·fErase:表明是否要重新绘制背景。
·rcPaint:指明要绘制的矩形区域。这里指的矩形和客户区域的矩形并不是同义语,理解这一点十分重要。这样做的理由是 Windows 是足够灵活的,可以告诉它一个窗口需要重新绘制的是哪一部分。当一个处在背景中的窗口由于前面遮住它的窗口移动而露出一部分时,这种功能是很重要的。rcPaint 字段似乎说:“不必费心把整个表面都重新绘制,只要绘制向用户显露出来的这一小部分区域。”
·PAINTSTRUCT 结构的其它三个字段为 Windows 内部使用,用户不必关心它们。
4.10 TextOut 和 DrawText 函数
下面列出的是绘制例程的核心,它们被 BeginPaint 和 EndPaint 函数把核心段包含在其中:
SetBkMode(PaintDC,TRANSPARENT); TextOut(PaintDC,10,10,buf,lstrlen(buf)); GetClientRect(hwnd,&rect); DrawText(PaintDC,"The middle of the window",-1,&rect, DT_SINGLELINE|DT_CENTER|DT_VCENTER);
这段代码首先设置背景模式为 TRANSPARENT(透明的)。这是必要的,因为窗口的背景可以是任何不同颜色的一种,例如可以为下面一行 Register 函数中的代码产生的颜色:
WndClass.hbrBackGround=(HBRUSH)(COLOR_WINDOW+1);
这个代码使用选定的系统范围内使用的颜色作为主窗口的背景色。如果你右击桌面,你可以访问 Display Properties 对话框。使用其中的 Item 组合框来选择 Window 选项,然后把它设置成你所挑选的颜色。此后,在你的桌面上所有设计完的窗口都有你所选择的这种颜色。作为一个程序员,你的工作是尊重用户在 Display Properties 对话框中所做的选择。要作到这一点,你只要把你的窗口的背景色置为 COLOR_WINDOW 常量,设置输出正文的背景颜色为透明的:
SetBkMode(PaintDC,TRANSPARENT);
如果你忽略了调用 SetBkMode,则你的正文将出现在白色背景中,即使周围的窗口是兰色、灰色或其它颜色也是如此。
在 EasyText 程序中,你可以看到在 Windows 程序中显示正文的两种常用的方法。第一种是调用 TextOut,第二种是调用 DrawText。
TextOut 函数是这两种函数中比较容易的一种。它所要求的仅仅是设备描述表、一些起始坐标、一个字符串以及这个字符串的长度。确定字符串长度的最普通的方法调用一个 C 语言的内置函数,例如 strlen 或 Windows 自己的 lstrlen。
而更高级的 DrawText 函数要求设备描述表,字符串,字符串长度,RECT 和一些标志作为参数。如果你向 DRAWTEXT 传送的字符串是以 NULL 作为终止符的,你就必须给第三个参数传送 -1,这样才能让 Windows 自己去计算该字符串的长度。RECT 参数指定一个区域,字符串在这个区域内绘制。在这种情况下,我用 GetClientRect 函数找到整个客户窗口的区域,然后传入三个标志,它指示 Windows 把字符串放在 RECT 的中间位置。无论窗口的外形是什么样子,无论你如何拉拽其边界,Windows 总能把字符串放在中间位置上。注意,由 TextOut 函数输出的字符串,无论窗口外形和尺寸如何变化,字符串始终留在一个地方。
在结束这个题目之前,我应该指出前面只是对一个 Windows 应用程序如何输出正文的简单介绍,更多的内容,特别是 Windows 不可思议的强大的字体函数将在以后介绍。
4.11 疑 难 解 答
- 我怎样才能记住传给一个消息处理函数的参数?
要想记住一种程序语言几乎所有的重要内容是可能的,但你必须是一个真正的天才才能做这种艰巨的工作。我的忠告是只要记住许多参数中很少几个参数,而且在你的桌面上简单地保持一个 WINDOWSX.H 的副本,当你需要时只要从中复制相应的头部信息。尤其是如果要想知道一个 Cls_OnCreate 函数是怎么定义的,只要打开 WindoesX 对 WM_CREATE 做全局搜索,就能让你直接得到你需要看的代码。
- 什么时候发送 WM_CREATE 消息?发送的频度有多大?
WM_CREATE 消息通常在程序的主窗口的生命周期中只出现一次。每当你调用 CreateWindow 函数时就发送它们。对 CreateWindow 函数的调用出现在 WinMain 中开始处,在进入消息循环之前。这个消息发送给窗口,使得你有机会执行你希望在窗口变得可见之前要做的任何初始化工作。
- HANDLE_MSG 宏的作用是什么?
HANDLE_MSG 宏让程序员避免建立像下面这样的 csae 语句:
case WM_CREATE:
return HANDLE_WM_CREATE(hwnd,wParam,lParam,MyCls_OnCreate);
你可以用下面这样容易理解的代码代替上面一行代码:
HANDLE_MSG(hwnd,WM_CREATE,,MyCls_OnCreate);