Programming Windows程式开发设计指南->第三章 窗口和消息

3. 窗口和消息

在前两章,程序使用了同一个函数MessageBox来向用户输出文本。MessageBox函数会建立一个「窗口」。在Windows中,「窗口」一词有确切的含义。一个窗口就是屏幕上的一个矩形区域,它接收用户的输入并以文本或图形的格式显示输出内容。

MessageBox函数建立一个窗口,但这只是一个功能有限的特殊窗口。消息窗口有一个带关闭按钮的标题列、一个选项图标、一行或多行文本,以及最多四个按钮。当然,必须选择Windows提供给您的图标与按钮。

MessageBox函数非常有用,但下面不会过多地使用它。我们不能在消息框(方块)中显示图形,而且也不能在消息框(方块)中添加菜单。要添加这些对象,就需要建立自己的窗口,现在就开始。

自己的窗口
 

建立窗口很简单,只需调用CreateWindow函数即可。

好啦,虽然建立窗口的函数的确名为CreateWindow,而且您也能在/Platform SDK/User Interface Services/Windowing/Windows/Window Reference/Window Functions找到此文件,但您将发现CreateWindow的第一个参数就是所谓的「窗口类别名称」,并且该窗口类别连接所谓的「窗口消息处理程序」。在我们调用CreateWindow之前,有一点背景知识会对您大有帮助。

总体结构
 

进行Windows程序设计,实际上是在进行一种对象导向的程序设计(OOP)。这一点在Windows中使用得最多的对象上表现最为明显。这种对象正是Windows之所以命名为「Windows」的原因,它具有人格化的特徵,甚至可能会在您的梦中出现,这就是那个叫做「窗口」的东西。

桌面上最明显的窗口就是应用程序窗口。这些窗口含有显示程序名称的标题列、菜单甚至可能还有工具栏和滚动条。另一类窗口是对话框,它可以有标题列也可以没有标题列。

装饰对话框表面的还有各式各样的按键、单选按钮、核取框(方块)、列表框、滚动条和文本输入区域。其中每一个小的视觉对象都是一个窗口。更确切地说,这些都称为「子窗口」或「控件窗口」或「子窗口控件」。

作为对象,用户会在屏幕上看到这些窗口,并通过键盘和鼠标直接与它们进行交互操作。更有趣的是,程序写作者的观点与用户的观点极其类似。窗口以「消息」的形式接收窗口的输入,窗口也用消息与其他窗口通信。对消息的理解将是学习如何写作Windows程序所必须越过的障碍之一。

这有一个Windows的消息范例:我们知道,大多数的Windows程序都有大小合适的应用程序窗口。也就是说,您能够通过鼠标拖动窗口的边框来改变窗口的大小。通常,程序将通过改变窗口中的内容来回应这种大小的变化。您可能会猜测(并且您也是正确的),是Windows本身而不是应用程序在处理与用户重新调整窗口大小相关的全部杂乱程序。由于应用程序能改变其显示的样子,所以它也「知道」窗口大小改变了。

应用程序是如何知道用户改变了窗口的大小的呢?由于程序写作者习惯了往常的文本模式程序,操作系统没有设置将此类消息通知给用户的机制。问题的关键在于理解Windows所使用的架构。当用户改变窗口的大小时,Window给程序发送一个消息指出新窗口的大小。然后程序就可以调整窗口中的内容,以回应大小的变化。

Windows给程序发送消息。」我们希望读者不要对这句话视而不见。它到底表达了什么意思呢?我们在这里讨论的是程序码,而不是一个电子邮件系统。操作系统怎么给程序发送消息呢?

其实,所谓「Windows给程序发送消息」,是指Windows调用程序中的一个函数,该函数的参数描述了这个特定消息。这种位于Windows程序中的函数称为「窗口消息处理程序」。

无疑,读者对程序调用操作系统的做法是很熟悉的。例如,程序在打开磁片文件时就要使用有关的系统调用。读者所不习惯的,可能是操作系统调用程序,而这正是Windows对象导向架构的基础。

程序建立的每一个窗口都有相关的窗口消息处理程序。这个窗口消息处理程序是一个函数,既可以在程序中,也可以在动态链接库中。Windows通过调用窗口消息处理程序来给窗口发送消息。窗口消息处理程序根据此消息进行处理,然后将控制传回给Windows

更确切地说,窗口通常是在「窗口类别」的基础上建立的。窗口类别标识了处理窗口消息的窗口消息处理程序。使用窗口类别使多个窗口能够属于同一个窗口类别,并使用同一个窗口消息处理程序。例如,所有Windows程序中的所有按钮均依据同一个窗口类别。这个窗口类别与一个处理所有按钮消息的窗口消息处理程序(位于Windows的动态链接库中)联结。

在对象导向的程序设计中,对象是程序与数据的组合。窗口是一种对象,其程序是窗口消息处理程序。数据是窗口消息处理程序保存的信息和Windows为每个窗口以及系统中那个窗口类别保存的信息。

窗口消息处理程序处理给窗口发送消息。这些消息经常是告知窗口,用户正使用键盘或者鼠标进行输入。这正是按键窗口知道它被「按下」的奥妙所在。在窗口大小改变,或者窗口表面需要重画时,由其他消息通知窗口。

Windows程序开始运行后,Windows为该程序建立一个「消息伫列」。这个消息伫列用来存放该程序可能建立的各种不同窗口的消息。程序中有一小段程序码,叫做「消息回圈」,用来从伫列中取出消息,并且将它们发送给相应的窗口消息处理程序。有些消息直接发送给窗口消息处理程序,不用放入消息伫列中。

如果您对这段Windows架构过于简略的描述将信将疑,就让我们去看看在实际的程序中,窗口、窗口类别、窗口消息处理程序、消息伫列、消息回圈和窗口消息是如何相互配合的。这或许会对您有些帮助。

HELLOWIN程序
 

建立一个窗口首先需要注册一个窗口类别,那需要一个窗口消息处理程序来处理窗口消息。处理窗口消息对每个Windows程序都带来了些负担。程序3-1所示的HELLOWIN程序中整个做的事情差不多就是料理这些事情。

 程序3-1  HELLOWIN

HELLOWIN.C

/*------------------------------------------------------------------------

     HELLOWIN.C -- Displays "Hello, Windows 98!" in client area

                    (c) Charles Petzold, 1998

 -----------------------------------------------------------------------*/

 

#include <windows.h>

 

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

 

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,

                    PSTR szCmdLine, int iCmdShow)

{

       static TCHAR szAppName[] = TEXT ("HelloWin") ;

       HWND    hwnd ;

       MSG       msg ;

       WNDCLAS    wndclass ;

 

      wndclass.style                = CS_HREDRAW | CS_VREDRAW ;

      wndclass.lpfnWndProc  = WndProc ;

       wndclass.cbClsExtra       = 0 ;

       wndclass.cbWndExtra     = 0 ;

       wndclass.hInstance         = hInstance ;

       wndclass.hIcon               = LoadIcon (NULL, IDI_APPLICATION) ;

      wndclass.hCursor    = LoadCursor (NULL, IDC_ARROW) ;

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

      wndclass.lpszMenuNam = NULL ;

       wndclass.lpszClassName       = szAppName ;

 

       if (!RegisterClass (&wndclass))

     {

              MessageBox ( NULL, TEXT ("This program requires Windows NT!"),

                                   szAppName, MB_ICONERROR) ;

              return 0 ;

     }

       hwnd = CreateWindow( szAppName,   // window class name

                     TEXT ("The Hello Program"),      // window caption

                     WS_OVERLAPPEDWINDOW,    // window style

                     CW_USEDEFAULT,      // initial x position

                     CW_USEDEFAULT,      // initial y position

                     CW_USEDEFAULT,      // initial x size

                     CW_USEDEFAULT,      // initial y size

                     NULL,                  // parent window handle

                  NULL,            // window menu handle

                  hInstance,          // program instance handle

                  NULL) ;            // creation parameters

    

       ShowWindow (hwnd, iCmdShow) ;

       UpdateWindow (hwnd) ;

    

       while (GetMessage (&msg, NULL, 0, 0))

     {

              TranslateMessage (&msg) ;

             DispatchMessage (&msg) ;

     }

       return msg.wParam ;

}

 

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

{

       HDC                     hdc ;

       PAINTSTRUCT ps ;

       RECT            rect ;

    

       switch (message)

     {

       case WM_CREATE:

       PlaySound (TEXT ("hellowin.wav"), NULL, SND_FILENAME | SND_ASYNC) ;

              return 0 ;

 

       case       WM_PAINT:

              hdc = BeginPaint (hwnd, &ps) ;

         

              GetClientRect (hwnd, &rect) ;

         

              DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect,

                     DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;

                    EndPaint (hwnd, &ps) ;

                     return 0 ;

         

       case WM_DESTROY:

              PostQuitMessage (0) ;

              return 0 ;

     }

   return DefWindowProc (hwnd, message, wParam, lParam) ;

}

程序建立一个普通的应用程序窗口,如图3-1所示。在窗口客户区的中央显示「Hello, Windows 98!」。如果安装了音效卡,那么您还可以听到相应的朗读声音。

Click to view at full size. 


3-1 HELLOWIN窗口

提醒您注意:如果您使用Microsoft Visual C++ 为此程序建立新工程,那么您得加上连结程序所需的程序库文件。从 Project 菜单选择 Setting 选项,然后选取 Link 页面标签。从 Category 列表框中选择 General ,然后在 Object/Library Modules 文本框(方块)添加 WINMM.LIB  Windows multimedia  Windows多媒体 )。您这样做是因为HELLOWIN将使用多媒体功能调用,而默认的工程中又不包括多媒体程序库文件。不然连结程序报告了错误信息,表明PlaySound函数不可用。

HELLOWIN将存取文件HELLOWIN.WAV,该文件在本书所附光碟的HELLOWIN目录中。运行HELLOWIN.EXE时,默认的目录必须是HELLOWIN。在Visual C++中运行此程序时,虽然运行档会产生在HELLOWINRELEASEDEBUG子目录中,但运行程序的目录还是必须在HELLOWIN中。

通盘考量
 

实际上,每一个Windows程序码中都包括HELLOWIN.C程序的大部分。没人能真正记住此程序的全部写法;通常,Windows程序写作者在开始写一个新程序时总是会复制一个现有的程序,然后再做相应的修改。您可以按此习惯自由使用本书附带光碟中的程序。

上面提到,HELLOWIN将在其窗口的中央显示字符串。这种说法不是完全正确的。文本实际显示在程序客户区的中央,它在图3-1中是标题列和边界范围内的大片白色区域。这区别对我们来说很重要;客户区就是程序自由绘图并且向用户显示输出结果的窗口区域。

如果您认真思考一下,将会发现虽然只有80行程序码,这个窗口却令人惊讶地具有许多功能。您可以用鼠标按住标题列,在屏幕上移动窗口;可以按住大小边框,改变窗口的大小。在窗口大小改变时,程序自动地将「Hello, Windows 98!」字符串重新定位在客户区的中央。您可以按最大化按钮,放大HELLOWIN以充满整个屏幕;也可以按最小化按钮,将程序缩小成一个图标。您可以在系统菜单中运行所有选项(就是按下在标题列最左端的小图标);也可以从系统菜单中选择 Close 选项,或者单击标题列最右端的关闭按钮,或者双击标题列最左端的图标,来关闭窗口以终止程序的运行。

我们将在本章的余下部分对此程序作一详细的检查。当然,我们首先要从整体上看一下。

与前两章中的范例程序一样,HELLOWIN.C也有一个WinMain函数,但它还有另外一个函数,名为WndProc。这就是窗口消息处理程序。注意,在HELLOWIN.C中没有调用WndProc的程序码。当然,在WinMain中有对WndProc的参考,而这就是该函数要在程序开头附近声明的原因。

Windows函数调用
 

HELLOWIN至少调用了18Windows函数。下面以它们在HELLOWIN中出现的次序列出这些函数以及各自的简明描述:

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头文件中定义的。有些标帜符含有两个字母或者三个字母的字头,这些字头后头接著一个底线:

CS_HREDRAW

DT_VCENTER

SND_FILENAME

CS_VREDRAW

IDC_ARROW

WM_CREATE

CW_USEDEFAULT

IDI_APPLICATION

WM_DESTROY

DT_CENTER

MB_ICONERROR

WM_PAINT

DT_SINGLELINE

SND_ASYNC

WS_OVERLAPPEDWINDOW

这些是简单的数值常量。字头指示该常量所属的类别,如表3-1所示。

3-1

 

字头

类别

CS

窗口类别样式

CW

建立窗口

DT

绘制文本

IDI

图标ID

IDC

光标ID

MB

消息框(方块)

SND

声音

WM

窗口消息

WS

窗口样式

奉劝程序写作者不要费力气去记忆Windows程序设计中的数值常量。实际上,Windows中使用的每个数值常量在头文件中均有相应的标帜符定义。

新的数据类型
 

HELLOWIN.C中的其他标帜符是新的数据类型,也在Windows头文件中使用typedef叙述或者#define叙述加以定义了。最初是为了便于将Windows程序从原来的16位系统上移植到未来的使用32(或者其他)技术的操作系统上。这种作法并不如当时每个人想像的那样顺利,但是这种概念基本上是正确的。

有时这些新的数据类型只是为了方便缩写。例如,用于WndProc的第二个参数的UINT数据类型只是一个unsigned int (无正负号整数),在Windows 98中,这是一个32位的值。用于WinMain的第三个参数的PSTR数据类型是指向一个字符串的指针,即是一个char *

其他数据类型的含义不太明显。例如,WndProc的第三和第四个参数分别被定义为WPARAMLPARAM,这些名字的来源有点历史背景:当Windows还是16位系统时,WndProc的第三个参数被定义为一个WORD,这是一个16位的 无正负号短 unsigned short)整数,而第四个参数被定义为一个LONG,这是一个32位有正负号长整数,从而导致了文本「PARAM」前面加上了前置字头「W」和「L」。当然,在32位的Windows中,WPARAM被定义为一个UINT,而LPARAM被定义为一个LONG(这就是C中的long整数类型),因此窗口消息处理程序的这两个参数都是32位的值。这也许有点奇怪,因为WORD数据类型在Windows98中仍然被定义为一种16位的 无正负号 整数,因此「PARAM」前的「W」就有点误用了。

WndProc函数传回一个类型为LRESULT的值,该值简单地被定义为一个LONGWinMain函数被指定了一个WINAPI类型(在头文件中定义的所有Windows函数都被指定这种类型),而WndProc函数被指定一个CALLBACK类型。这两个标帜符都被定义为_stdcall,表示在Windows本身和用户的应用程序之间发生的函数调用的调用参数传递方式。

HELLOWIN还使用了Windows头文件中定义的四种数据结构(我们将在本章稍后加以讨论)。这些数据结构如表3-2所示。

3-2

 

结构

含义

MSG

消息结构

WNDCLASS

窗口类别结构

PAINTSTRUCT

绘图结构

RECT

矩形结构

前面两个数据结构在WinMain中使用,分别定义了两个名为msgwndclass的结构,后面两个数据结构在WndProc中使用,分别定义了psrect结构。

句柄简介
 

最后,还有三个大写标帜符(见表3-3),用于不同类型的「句柄」:

3-3

 

标帜符

含义

HINSTANCE

实例(程序自身)句柄

HWND

窗口句柄

HDC

设备环境句柄

句柄在Windows中使用非常频繁。在本章结束之前,我们将遇到HICON(图标句柄)、HCURSOR(鼠标光标句柄)和HBRUSH(画刷句柄)。

句柄是一个(通常为32位的)整数,它代表一个对象。Windows中的句柄类似传统C或者MS-DOS程序设计中使用的文件句柄。程序几乎总是通过调用Windows函数取得句柄。程序在其他Windows函数中使用这个句柄,以使用它代表的对象。句柄的实际值对程序来说是无关紧要的。但是,向您的程序提供句柄的Windows模块知道如何利用它来使用相对应的对象。

匈牙利表示法
 

读者可能注意到,HELLOWIN.C中有一些变量的名字显得很古怪。如szCmdLine,它是传递给WinMain的参数。

许多Windows程序写作者使用一种叫做「匈牙利表示法」的变量命名通则。这是为了纪念传奇性的Microsoft程序写作者Charles Simonyi。非常简单,变量名以一个或者多个小写字母开始,这些字母表示变量的数据类型。例如,szCmdLine中的sz代表「以0结尾的字符串」。在hInstancehPrevInstance中的h字头表示「句柄」;在iCmdShow中的i字头表示「整数」。 WndProc的后两个参数也使用匈牙利表示法。正如我在前面已经解释过的,尽管wParam应该更适当地被命名为uiParam(代表「无正负号整数」),但是因为这两个参数是使用数据类型WPARAMLPARAM定义的,因此保留它们传统的名字。

在命名结构变量时,可以用结构名(或者结构名的一种缩写)的小写作为变量名的字头,或者用作整个变量名。例如,在HELLOWIN. CWinMain函数中,msg变量是MSG类型的结构;wndclassWNDCLASSEX类型的一个结构。在WndPmc函数中,ps是一个PAINTSTRUCT结构,rect是一个RECT结构。

匈牙利表示法能够帮助程序写作者及早发现并避免程序中的错误。由于变量名既描述了变量的作用,又描述了其数据类型,就比较容易避免产生数据类型不合的错误。

3-4列出了在本书中经常用到的变量字头。

3-4

 

字头

数据类型

c

charWCHARTCHAR

by

BYTE (无正负号字符)

n

short

i

int

x, y

int分别用作x座标和y座标

cx, cy

int分别用作x长度和y长度;C代表「计数器」

bf

BOOL (int)f代表「旗标」

w

WORD (无正负号短整数)

l

LONG (长整数)

dw

DWORD (无正负号长整数)

fn

function(函数)

s

string(字符串)

sz

以字节值0结尾的字符串

h

句柄

p

指针

注册窗口类别
 

窗口依照某一窗口类别建立,窗口类别用以标识处理窗口消息的窗口消息处理程序。

不同窗口可以依照同一种窗口类别建立。例如,Windows中的所有按钮窗口-包括按键、核取框(方块),以及单选按钮-都是依据同一种窗口类别建立的。窗口类别定义了窗口消息处理程序和依据此类别建立的窗口的其他特徵。在建立窗口时,要定义一些该窗口所独有的特徵。

在为程序建立窗口之前,必须首先调用RegisterClass注册一个窗口类别。该函数只需要一个参数,即一个指向类型为WNDCLASS的结构指针。此结构包括两个指向字符串的栏位,因此结构在WINUSER.H头文件中定义了两种不同的方式,第一个是ASCII版的WNDCLASSA

typedef struct tagWNDCLASSA

{

       UINT           style ;

       WNDPROC         lpfnWndProc ;

       int        cbClsExtra ;

       int             cbWndExtra ;

       HINSTANCE         hInstance ;

       HICON              hIcon ;

       HCURSOR      hCursor ;

       HBRUSH            hbrBackground ;

       LPCSTR        lpszMenuName ;

       LPCSTR        lpszClassName ;

}

WNDCLASSA, * PWNDCLASSA, NEAR * NPWNDCLASSA, FAR * LPWNDCLASSA ;

在这里提示一下数据类型和匈牙利表示法:其中的lpfn字头代表「指向函数的长指针」。(在Win32 API中,长指针和短指针(或者近程指针)没有区别。这只是16Windows的遗物。)cb字头代表「字节数」而且通常作为一个常量来表示一个字节的大小。h字头是一个句柄,而hbr字头代表「一个画刷的句柄」。lpsz字头代表「指向以0结尾字符串的指针」。

Unicode版的结构定义如下:

typedef struct tagWNDCLASSW

{

          UINT               style ;

          WNDPROC       lpfnWndProc ;

          int           cbClsExtra ;

          int           cbWndExtra ;

          HINSTANCE       hInstance ;

          HICON            hIcon ;

          HCURSOR         hCursor ;

          HBRUSH           hbrBackground ;

          LPCWSTR        lpszMenuName ;

          LPCWSTR        lpszClassName ;

}

WNDCLASSW, * PWNDCLASSW, NEAR * NPWNDCLASSW, FAR * LPWNDCLASSW ;

与前者唯一的区别在于最后两个栏位定义为指向宽字符串常量,而不是指向ASCII字符串常量。

WINUSER.H定义了WNDCLASSAWNDCLASSW结构(以及指向结构的指针)以后,头文件依据对UNICODE标帜符的解释,定义了WNDCLASS和指向WNDCLASS的指针(包括一些向后兼容的程序码):

#ifdef UNICODE

typedef   WNDCLASSW         WNDCLASS ;

typedef   PWNDCLASSW       PWNDCLASS ;

typedef   NPWNDCLASSW        NPWNDCLASS ;

typedef   LPWNDCLASSW        LPWNDCLASS ;

#else

typedef   WNDCLASSA          WNDCLASS ;

typedef   PWNDCLASSA        PWNDCLASS ;

typedef   NPWNDCLASSA    NPWNDCLASS ;

typedef   LPWNDCLASSA    LPWNDCLASS ;

#endif

本书后面列出结构时,将只列出功用相同的结构定义,对WNDCLASS就像这样:

typedef struct

{

    UINT              style ;

       WNDPROC            lpfnWndProc ;

       int                   cbClsExtra ;

       int              cbWndExtra ;

       HINSTANCE       hInstance ;

       HICON            hIcon ;

       HCURSOR         hCursor ;

       HBRUSH           hbrBackground ;

       LPCTSTR         lpszMenuName ;

       LPCTSTR         lpszClassName ;

}

WNDCLASS, * PWNDCLASS ;

我也不再著重说明指针的定义。一个程序写作者的程序不应该因为使用以LPNP为字头的不同指针类型而被搅乱。

WinMain中为WNDCLASS定义一个结构,通常像这样:

WNDCLASS wndclass ;

然后,你就可以初始化该结构的10个栏位,并调用RegisterClass

WNDCLASS结构中最重要的两个栏位是第二个和最后一个,第二个栏位(lpfnWndProc) 是依据这个类别来建立的所有窗口所使用的窗口消息处理程序的地址。在HELLOWIN.C中,这个是WndProc函数。最后一个栏位是窗口类别的文本名称。程序写作者可以随意定义其名称。在只建立一个窗口的程序中,窗口类别名称通常设定为程序名称。

其他栏位依照下面的方法描述了窗口类别的一些特徵。让我们依次看看WNDCLASS结构中的每个栏位。

叙述

wndclass.style = CS_HREDRAW | CS_VREDRAW ;

使用C的位「或」运算子结合了两个「窗口类别样式」标帜符。在头文件WINUSER.H中,已定义了一整组以CS为字头的标帜符:

#define   CS_VREDRAW                 0x0001

#define   CS_HREDRAW            0x0002

#define   CS_KEYCVTWINDOW           0x0004

#define   CS_DBLCLKS              0x0008

#define   CS_OWNDC              0x0020

#define   CS_CLASSDC             0x0040

#define   CS_PARENTDC            0x0080

#define   CS_NOKEYCVT                 0x0100

#define   CS_NOCLOSE             0x0200

#define   CS_SAVEBITS              0x0800

#define   CS_BYTEALIGNCLIENT         0x1000

#define   CS_BYTEALIGNWINDOW            0x2000

#define   CS_GLOBALCLASS             0x4000

#define   CS_IME                        0x00010000

由于每个标帜符都可以在一个复合值中设置一个位的值,所以按这种方式定义的标帜符通常称为「位旗标」。通常我们只使用少数的窗口类别样式。HELLOWIN中用到的这两个标帜符表示,所有依据此类别建立的窗口,每当窗口的水平方向大小(CS_HREDRAW)或者垂直方向大小(CS_VREDRAW)改变之后,窗口要完全重画。改变HELLOWIN的窗口大小,可以看到字符串仍然显示在窗口的中央,这两个标帜符确保了这一点。不久我们就将看到窗口消息处理程序是如何得知这种窗口大小的变化的。

WNDCLASS结构的第二个栏位由以下叙述进行初始化:

wndclass.lpfnWndProc = WndProc ;

这条叙述将这个窗口类别的窗口消息处理程序设定为WndProc,即HELLOWIN.C中的第二个函数。这个过程将处理依据这个窗口类别建立的所有窗口的全部消息。在C语言中,像这样在结构中使用函数名时,真正提供的是指向函数的指针。

下面两个栏位用于在窗口类别结构和Windows内部保存的窗口结构中预留一些额外空间:

wndclass.cbClsExtra = 0 ;

wndclass.cbWndExtra = 0 ;

程序可以根据需要来使用预留的空间。HELLOWIN没有使用它们,所以设定值为0。否则,和匈牙利表示法所指示的一样,这个栏位将被当成「预留的字节数」。(在第七章的程序CHECKER3将使用cbWndExtra栏位。)

下一个栏位就是程序的实例句柄(它也是WinMain的参数之一):

wndclass.hInstance = hInstance ;

叙述

wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;

为所有依据这个窗口类别建立的窗口设置一个图标。图标是一个小的位图图像,它对用户代表程序,将出现在Windows任务栏中和窗口的标题列的左端。在本书的后面,您将学习如何为您的Windows程序自订图标。现在,为了方便起见,我们将使用预先定义的图标。

要取得预先定义图标的句柄,可以将第一个参数设定为NULL来调用LoadIcon。在载入程序写作者自订的图标时(图标应该存放在磁片上的.EXE程序文件中),这个参数应该被设定为程序的实例句柄hInstance。第二个参数代表图标。对于预先定义图标,此参数是以IDI开始的标帜符(「ID代表图标」),标帜符在WINUSER.H中定义。IDI_APPLICATION图标是一个简单的窗口小图形。LoadIcon函数传回该图标的句柄。我们并不关心这个句柄的实际值,它只用于设置hIcon栏位的值。该栏位在WNDCLASS结构中定义为HICON类型,此类型名的含义为「handle to an icon(图标句柄)」。

叙述

wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;

与前一条叙述非常相似。LoadCursor函数载入一个预先定义的鼠标光标(命名为IDC_ARROW),并传回该光标的句柄。该句柄被设定给WNDCLASS结构的hCursor栏位。当鼠标光标在依据这个类别建立的窗口的客户区上出现时,它变成一个小箭头。

下一个栏位指定依据这个类别建立的窗口背景颜色。hbrBackground栏位名称中的hbr字头代表「handle to a brush(画刷句柄)」。画刷是个绘图词汇,指用来填充一个区域的著色样式。Windows有几个标准画刷,也称为「备用(stock)」画刷。这里所示的GetStockObject调用将传回一个白色画刷的句柄:

wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;

这意味著窗口客户区的背景完全为白色,这是一种极其普遍的做法。

下一个栏位指定窗口类别菜单。HElLOWIN没有应用程序菜单,所以该栏位被设定为NULL

wndclass.lpszMenuName = NULL ;

最后,必须给出一个类别名称。对于小程序,类别名称可以与程序名相同,即存放在szAppName变量中的「HelloWin」字符串。

wndclass.lpszClassName = szAppName ;

至于该字符串由ASCII字节成或由Unicode字节成,取决于是否定义了UNICODE标帜符。

在初始化该结构的10个栏位后,HELLOWIN调用RegisterClass来注册这个窗口类别。该函数只有一个参数,即指向WNDCLASS结构的指针。实际上,RegisterClassA函数将获得一个指向WNDCLASSA结构的指针,而RegisterClassW函数将获得一个指向WNDCLASSW结构的指针。程序要使用哪个函数来注册窗口类别,取决于发送给窗口的消息包含ASCII文本还是Unicode文本。

现在有一个问题:如果用定义的UNICODE标帜符编译了程序,程序将调用RegisterClassW。该程序可以在Microsoft Windows NT中运行良好。但如果此程序在Windows 98上运行,RegisterClassW函数并未真地被运行到。函数有一个入口,但函数调用后只传回0,表明错误。对于在Windows 98下运行的Unicode程序来说,这是一个通知用户有问题并终止运行的好机会。这是本书中多数程序处理RegisterClass函数调用的方法:

if (!RegisterClass (&wndclass))

{

       MessageBox ( NULL, TEXT ("This program requires Windows NT!"),

                    szAppName, MB_ICONERROR) ;

       return 0 ;

}

由于MessageBoxW是可在Windows 98环境下运行的几个Unicode函数之一,所以其运行正常。

当然,这段程序假定RegisterClass不会因为其他原因而调用失败,诸如WNDCLASS结构中lpfnWndProc栏位被设定成NULL之类的错误。GetLastError函数会帮助您确定在这样的情况下产生错误的原因。GetLastErrorWindows中常用的函数,它可以在函数调用失败时获得更多错误信息。不同函数的文件将指出您是否能够用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查看错误。

一些Windows程序写作者喜欢检查所有可能发生错误的函数调用的传回值。这么做确实有点道理,相信您也非常习惯在配置内存后检查错误。而许多Windows函数需要配置内存。例如,RegisterClass需要配置内存,以保存窗口类别的信息。如此一来,您就应该要检查这个函数的运行结果。另一方面说来,如果由于RegisterClass不能得到所需要的内存,它会声明调用失败,而Windows大概也快当掉了。

在本书的范例程序中,我做了最少的错误检查。这不是因为我认为错误检查不是一个好方法,而是因为这会让我们在程序举例中分心。

最后,一个老经验是:在一些Windows范例程序中,您可能在WinMain中看到以下程序码:

if (!hPrevInstance)

{

       wndclass.cbStyle = CS_HREDRAW | CS_VREDRAW ;

 

             初始化其他 wndclass

 

       RegisterClass (&wndclass) ;

}

这是出于「旧习难改」的原因。在16位的Windows中,如果您启动正在运行的程序的一个新实例,WinMainhPrevInstance参数将是前一个实例的实例句柄。为节省内存,两个或多个实例就可能会共用相同的窗口类别。这样,窗口类别就只在hPrevInstanceNULL的时候才注册,这表明程序没有其他实例。

32位的Windows中,hPrevInstance总是NULL。此程序码会正常运行,而实际上也没必要检查hPrevInstance

建立窗口
 

窗口类别定义了窗口的一般特徵,因此可以使用同一窗口类别建立许多不同的窗口。实际调用CreateWindow建立窗口时,可能指定有关窗口的更详细的信息。

Windows程序设计新手有时会混淆窗口类别和窗口之间的区别,以及为什么一个窗口的所有特徵不能被一次设定好。实际上,以这种方式分开这些样式信息是非常方便的。例如,所有的按钮窗口都可以依据同样的窗口类别来建立,与这个窗口类别相关的窗口消息处理程序位于Windows内部。由窗口类别来负责处理按钮的键盘和鼠标输入,并定义按钮在屏幕上的外观形象。从这一点看来,所有的按钮都是以同样的方式工作的。但是并非所有的按钮都是一样的。它们可以有不同的大小,不同的屏幕位置,以及不同的字符串。后面的这样一些特徵是窗口定义的一部分,而不是窗口类别定义的。

传递给RegisterClass函数的信息会在一个数据结构中设定好,而传递给CreateWindow函数的信息会在函数单独的参数中设定好。下面是HELLOWIN.C中的CreateWindows调用,每一个栏位都做了完整的说明:

hwnd = CreateWindow (szAppName, // window class name

       TEXT (   "The Hello Program"),   // window caption

              WS_OVERLAPPEDWINDOW,        // window style

          CW_USEDEFAULT,                  // initial x position

         CW_USEDEFAULT,                  // initial y position

              CW_USEDEFAULT,                 // initial x size

             CW_USEDEFAULT,                  // initial y size

              NULL,                        // parent window handle

             NULL,                        // window menu handle

           hInstance,                      // program instance handle

           NULL) ;                        // creation parameters

在这里,我不想提实际上有CreateWindowA函数和CreateWindowW函数,两个函数分别将前两个参数当成ASCII或者Unicode字符串来处理。

标记为「window class name」的参数是szAppName,它含有字符串「HelloWin」-这是程序注册的窗口类别名称。这就是我们建立的窗口联结窗口类别的方式。

此程序建立的窗口是一个普通的重叠式窗口。它含有一个标题列,标题列左边有一个系统菜单按钮,标题列右边有缩小、放大和关闭图标,四周还有一个表示窗口大小的边框。这是标准样式的窗口,名为WS_OVERLAPPEDWINDOW,出现在CreateWindow的「窗口样式」参数中。如果看一下WINUSER.H,您将会发现此样式是几种位旗标的组合:

#define   WS_OVERLAPPEDWINDOW (WS_OVERLAPPED  | /

                 WS_CAPTION                                | /

                 WS_SYSMENU                               | /

                 WS_THICKFRAME                             | /

                 WS_MINIMIZEBOX                           | /

                 WS_MAXIMIZEBOX)

「窗口标题」是显示在标题列中的文本。

注释著「initial x position」和「initial y position」的参数指定了窗口左上角相对于屏幕左上角的初始位置。由于这些参数使用CW_USEDEFAULT标帜符,指示Windows使用重叠窗口的默认位置。(CW_USEDEFAULT定义为0x80000000。)默认情况下,Windows依次对新建立的窗口定位,使各窗口左上角的垂直和水平距离在屏幕上按一定的大小递增。与此类似,注释著「initial x size」和「initial y size」的参数分别指定窗口的宽度和高度。同样使用了CW_USEDEFAULT标帜符,表明希望Windows使用默认尺寸。

在建立一个「最上层」窗口,如应用程序窗口时,注释为「父窗口句柄」的参数设定为NULL。通常,如果窗口之间存在有父子关系,则子窗口总是出现在父窗口的上面。应用程序窗口出现在桌面窗口的上面,但不必为调用CreateWindow而找出桌面窗口的句柄。

因为窗口没有菜单,所以「窗口菜单句柄」也设定为NULL。「程序实例句柄」设定为实例句柄,它是作为WinMain的参数传递给这个程序的。最后,「建立参数」指针设定为NULL,可以用这个参数存取稍后程序中可能引用到的数据。

CreateWindow传回被建立的窗口的句柄,该句柄存放在变量hwnd中,后者被定义为HWND类型(「窗口句柄类型」)。Windows中的每个窗口都有一个句柄,程序用句柄来使用窗口。许多Windows函数需要使用hwnd作为参数,这样,Windows才能知道函数是针对哪个窗口的。如果一个程序建立了许多窗口,则每个窗口均有一个句柄。窗口句柄是Windows程序所处理最重要的句柄之一。

显示窗口
 

CreateWindow调用传回之后,Windows内部已经建立了这个窗口。这就是说,Windows已经配置了一块内存,用来保存在CreateWindow调用中指定窗口的全部信息跟一些其他信息,而Windows稍后就是依据窗口句柄找到这些信息的。

然而,光是这样子,窗口并不会出现在视频显示器上。您还需要两个函数调用,一个是:

ShowWindow (hwnd, iCmdShow) ;

第一个参数是刚刚用CreateWindow建立的窗口句柄。第二个参数是作为参数传给WinMainiCmdShow。它确定最初如何在屏幕上显示窗口,是一般大小、最小化还是最大化。在开始菜单中安装程序时,用户可能做出最佳选择。如果窗口按一般大小显示,那么WinMain接收到后传递给ShowWindow的就是SW_SHOWNORMAL;如果窗口是最大化显示的,则为SW_SHOWMAXIMIZED。而如果窗口只显示在任务栏上,则是SW_SHOWMINNOACTIVE

ShowWindow函数在显示器上显示窗口。如果ShowWindow的第二个参数是SW_SHOWNORMAL,则窗口的客户区就会被窗口类别中定义的背景画刷所覆盖。函数调用

UpdateWindow (hwnd) ;

会重画客户区。它经由发送给窗口消息处理程序(即HELLOWIN.C中的WndProc函数)一个WM_PAINT消息做到这一点。后面,我们将说明WndProc如何处理这个消息。

消息回圈
 

调用UpdateWindow之后,窗口就出现在视频显示器上。程序现在必须准备读入用户用键盘和鼠标输入的数据。Windows为当前运行的每个Windows程序维护一个「消息伫列」。在发生输入事件之后,Windows将事件转换为一个「消息」并将消息放入程序的消息伫列中。

程序通过运行一块称之为「消息回圈」的程序码从消息伫列中取出消息:

while       (GetMessage (&msg, NULL, 0, 0))

{

       TranslateMessage (&msg) ;

       DispatchMessage (&msg) ;

}

msg变量是类型为MSG的结构,类型MSGWINUSER.H中定义如下:

typedef struct tagMSG

{

       HWND    hwnd ;

       UINT      message ;

       WPARAM      wParam ;

       LPARAM       lParam ;

       DWORD       time ;

       POINT         pt ;

}

MSG, * PMSG ;

POINT数据类型也是一个结构,它在WINDEF.H中定义如下:

typedef struct tagPOINT

{

       LONG  x ;

       LONG  y ;

}

POINT, * PPOINT;

消息回圈以GetMessage调用开始,它从消息伫列中取出一个消息:

GetMessage (&msg, NULL, 0, 0)

这一调用传给Windows一个指针,指向名为msgMSG结构。第二、第三和第四个参数设定为NULL或者0,表示程序接收它自己建立的所有窗口的所有消息。Windows用从消息伫列中取出的下一个消息来填充消息结构的各个栏位,结构的各个栏位包括:

hwnd 接收消息的窗口句柄。在HELLOWIN程序中,这一参数与CreateWindow传回的hwnd值相同,因为这是该程序拥有的唯一窗口。
 

message 消息标帜符。这是一个数值,用以标识消息。对于每个消息,均有一个对应的标帜符,这些标帜符定义于Windows头文件(其中大多数在WINUSER.H中),以字头WM(「window message」,窗口消息)开头。例如,用户将鼠标光标放在HELLOWIN客户区之内,并按下鼠标左按钮,Windows就在消息伫列中放入一个消息,该消息的message栏位等于WM_LBUTTONDOWN。这是一个常量,其值为0x0201
 

wParam 一个32位的「message parameter(消息参数)」,其含义和数值根据消息的不同而不同。
 

lParam 一个32位的消息参数,其值与消息有关。
 

time 消息放入消息伫列中的时间。
 

pt 消息放入消息伫列时的鼠标座标。
 

只要从消息伫列中取出消息的message栏位不为WM_QUIT(其值为0x0012),GetMessage就传回一个非零值。WM_QUIT消息将导致GetMessage传回0

叙述

TranslateMessage (&msg) ;

msg结构传给Windows,进行一些键盘转换。(关于这一点,我们将在第六章中深入讨论。)

叙述

DispatchMessage (&msg) ;

又将msg结构回传给Windows。然后,Windows将该消息发送给适当的窗口消息处理程序,让它进行处理。这也就是说,Windows将调用窗口消息处理程序。在HELLOWIN中,这个窗口消息处理程序就是WndProe函数。处理完消息之后,WndProc传回到Windows。此时,Windows还停留在DispatchMessage调用中。在结束DispatchMessage调用的处理之后,Windows回到HELLOWIN,并且接著从下一个GetMessage调用开始消息回圈。

窗口消息处理程序
 

以上我们所讨论的都是必要的负担:注册窗口类别,建立窗口,然后在屏幕上显示窗口,程序进入消息回圈,然后不断从消息伫列中取出消息来处理。

实际的动作发生在窗口消息处理程序中。窗口消息处理程序确定了在窗口的客户区中显示些什么以及窗口怎样回应用户输入。

HELLOWIN中,窗口消息处理程序是命名为WndProc的函数。窗口消息处理程序可任意命名(只要求不和其他名字发生冲突)。一个Windows程序可以包含多个窗口消息处理程序。一个窗口消息处理程序总是与调用RegisterClass注册的特定窗口类别相关联。CreateWindow函数根据特定窗口类别建立一个窗口。但依据一个窗口类别,可以建立多个窗口。

窗口消息处理程序总是定义为如下形式:

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

注意,窗口消息处理程序的四个参数与MSG结构的前四个栏位是相同的。第一个参数hwnd是接收消息的窗口的句柄,它与CreateWindow函数的传回值相同。对于与HELLOWIN相似的程序(只建立一个窗口),这个参数是程序所知道的唯一窗口句柄。如果程序是依据同一窗口类别(同时也是同一窗口消息处理程序)建立多个窗口,则hwnd标识接收消息的特定窗口。

第二个参数与MSG结构中的message栏位相同,它是标识消息的数值。最后两个参数都是32位的消息参数,提供关于消息的更多信息。这些参数包含每个消息类型的详细信息。有时消息参数是两个存放在一起的16位值,而有时消息参数又是一个指向字符串或数据结构的指针。

程序通常不直接调用窗口消息处理程序,窗口消息处理程序通常由Windows本身调用。通过调用SendMessage函数,程序能够直接调用它自己的窗口消息处理程序。我们将在后面的章节讨论SendMessage函数。

处理消息
 

窗口消息处理程序所接受的每个消息均是用一个数值来标识的,也就是传给窗口消息处理程序的message参数。Windows头文件WINUSER.H为每个消息参数定义以「WM」(窗口消息)为字头开头的标帜符。

一般来说,Windows程序写作者使用switchcase结构来确定窗口消息处理程序接收的是什么消息,以及如何适当地处理它。窗口消息处理程序在处理消息时,必须传回0。窗口消息处理程序不予处理的所有消息应该被传给名为DefWindowProcWindows函数。从DefWindowProc传回的值必须由窗口消息处理程序传回。

HELLOWIN中,WndProc只选择处理三种消息:WM_CREATEWM_PAINTWM_DESTROY。窗口消息处理程序的结构如下:

switch (iMsg)

{

case WM_CREATE :

      处理WM_CREATE消息

       return 0 ;

         

case WM_PAINT :

      处理WM_PAINT消息

       return 0 ;

         

case WM_DESTROY :

       处理WM_DESTROY消息

       return 0 ;

}

return DefWindowProc (hwnd, iMsg, wParam, lParam) ;

调用DefWindowProc来为窗口消息处理程序不予处理的所有消息提供默认处理,这是很重要的。不然一般动作,如终止程序,将不会正常运行。

播放音效文件
 

窗口消息处理程序接收的第一个消息-也是WndProc选择处理的第一个消息-是WM_CREATE。当WindowsWinMain中处理CreateWindow函数时,WndProc接收这个消息。就是说,在HELLOWIN调用CreateWindow时,Windows将做一些它必须做的工作。在这些工作中,Windows调用WndProc,将第一个参数设定为窗口句柄,第二个参数设定为WM_CREATEWndProc处理WM_CREATE消息并将控制传回给Windows Windows然后可以从CreateWindow调用中传回到HELLOWIN中,继续在WinMain中进行下一步的处理。

通常,窗口消息处理程序在WM_CREATE处理期间进行一次窗口初始化。HELLOWIN对这个消息的处理中播放一个名为HELLOWIN.WAV的音效文件。它使用简单的PlaySound函数来做到这一点。该函数说明在/Platform SDK/Graphics and Multimedia Services/Multimedia Audio/Waveform Audio中,而文件在/Platform SDK/Graphics and Multimedia Services/Multimedia Reference/Multimedia Functions中。

PlaySound的第一个参数是音效文件的名称(它也可能是在Control PanelSounds中定义的一种声音的别名,或者是一个程序资源)。第二个参数只有当音效文件是一种资源时才被使用。第三个参数指定一些选项。在这个例子中,我指定第一个参数是一个文件名,并且非同步地播放声音,即PlaySound函数调用在音效文件开始播放时立即传回,而不会等待它的完成。在这种方法下,程序能够继续初始化。

WndProc通过从窗口消息处理程序中传回0,结束了整个WM_CREATE的处理。

WM_PAINT消息
 

WndProc处理的第二个消息为WM_PAINT。这个消息在Windows程序设计中是很重要的。当窗口客户区的一部分显示内容或者全部变为「无效」,以致于必须「更新画面」时,将由这个消息通知程序。

客户区的显示内容怎么会变得无效呢?在最初建立窗口的时候,整个客户区都是无效的,因为程序还没有在窗口上画什么东西。第一条WM_PAINT消息(通常发生在WinMain中调用UpdateWindow时)指示窗口消息处理程序在客户区上画一些东西。

在用户改变HELLOWIN窗口的大小后,客户区的显示内容重新变得无效。读者应该还记得,HELLOWINwndclass结构的style栏位设定为标志CS_HREDRAWCS_VREDRAW,这样的格式设定指示Windows,在窗口大小改变后,就把整个窗口显示内容当成无效。然后,窗口消息处理程序将收到一条WM_PAINT消息。

当用户将HELLOWIN最小化,然后再次将窗口恢复为以前的大小时,Windows将不会保存客户区的内容。在图形环境下,窗口客户区涉及的数据量很大。因此,Windows令窗口无效,窗口消息处理程序接收一条WM_PAINT消息,并自动恢复其窗口的内容。

在移动窗口以致其相互重叠时,Windows不保存一个窗口中被另一个窗口所遮盖的内容。在这一部分不再被遮盖之后,它就被标志为无效。窗口消息处理程序接收到一条WM_PAINT消息,以更新窗口的内容。

WM_PAINT的处理几乎总是从一个BeginPaint调用开始:

hdc = BeginPaint (hwnd, &ps) ;

而以一个EndPaint调用结束:

EndPaint (hwnd, &ps) ;

在这两个调用中,第一个参数都是程序的窗口句柄,第二个参数是指向类型为PAINTSTRUCT的结构指针。PAINTSTRUCT结构中包含一些窗口消息处理程序,可以用来更新客户区的内容。我们将在下一章中讨论该结构的各个栏位。现在我们只在BeginPaintEndPaint函数中用到它。

BeginPaint调用中,如果客户区的背景还未被删除,则由Windows来删除。它使用注册窗口类别的WNDCLASS结构的hbrBackground栏位中指定的画刷来删除背景。在HELLOWIN中, 这是一个白色备用画刷。这意味著,Windows将通过把窗口背景设定为白色来删除窗口背景。BeginPaint调用令整个客户区有效,并传回一个「设备环境句柄」。设备环境是指实体输出设备(如视频显示器)及其设备驱动程序。在窗口的客户区显示文本和图形需要设备环境句柄。但是从BeginPaint传回的设备环境句柄不能在客户区之外绘图,读者可以试一试。EndPaint释放设备环境句柄,使之不再有效。

如果窗口消息处理程序不处理WM_PAINT消息(这是很罕见的),它们必须被传送给DefWindowProcDefWindowProc只是依次调用BeginPaintEndPaint,以使客户区有效。

调用完BeginPaint之后,WndProc接著调用GetClientRect

GetClientRect (hwnd, &rect) ;

第一个参数是程序窗口的句柄。第二个参数是一个指针,指向一个RECT类型的rectangle结构。该结构有四个LONG栏位,分别为lefttoprightbottomGetClientRect将这四个栏位设定为窗口客户区的尺寸。lefttop栏位通常设定为0rightbottom栏位设定为客户区的宽度和高度(图元点数)。

WndProc除了将该RECT结构指针作为DrawText的第四个参数传递外,不再对它做其他处理:

DrawText (     hdc, TEXT ("Hello, Windows 98!"), -1, &rect,

                 DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;

DrawText可以输出文本(正如其名字所表明的一样)。由于该函数要输出文本,第一个参数是从BeginPaint传回的设备环境句柄,第二个参数是要输出的文本,第三个参数是 -1,指示字符串是以字节0终结的。

DrawText最后一个参数是一系列位旗标,它们均在WINUSER.H中定义(虽然由于其显示输出的效果,使得DrawText像一个GDI函数调用,但它确实因为相当高级的画图功能而成为User模块的一部分。此函数在/Platform SDK/Graphics and Multimedia Services/GDI/Fonts and Text中说明)。旗标指示了文本必须显示在一行上,水平方向和垂直方向都位于第四个参数指定的矩形中央。因此,这个函数调用将让字符串「Hello, Windows 98!」显示在客户区的中央。

一旦客户区变得无效(正如在改变大小时所发生的情况一样),WndProc就接收到一个新的WM_PAINT消息。WndProc通过调用GetClientRect取得变化后的窗口大小,并在新窗口的中央显示文本。

WM_DESTROY消息
 

WM_DESTROY消息是另一个重要消息。这一个消息指示,Windows正在根据用户的指示关闭窗口。该消息是用户单击Close按钮或者在程序的系统菜单上选择 Close时发生的(在本章的后面,我们将详细讨论WM_DESTROY消息是如何生效的)。

HELLOWIN通过调用PostQuitMessage以标准方式回应WM_DESTROY消息:

PostQuitMessage (0) ;

该函数在程序的消息伫列中插入一个WM_QUIT消息。前面提到过,GetMessage对于除了WM_QUIT之外的从消息伫列中取出的所有消息都传回非0值。而当GetMessage得到一个WM_QUIT消息时,它传回0。这将导致WinMain退出消息回圈,并终止程序。然后程序运行下面的叙述:

return msg.wParam ;

结构的wParam栏位是传递给PostQuitMessage函数的值(通常是0)。然后return叙述将退出WinMain并终止程序。

WINDOWS程序设计的难点
 

即使有了对HELLOWIN的说明,读者对程序的结构和原理可能仍然觉得神秘。在为传统环境编写简单的C程序时,整个程序可能包含在main函数中。而在HELLOWIN中,WinMain只包含了注册窗口类别,建立窗口,从消息伫列中取出消息和发送消息所必须的程序码。

程序的所有实际动作均在窗口消息处理程序中发生。在HELLOWIN中,这些动作不多,WndProc只是简单地播放了一个音效文件并在窗口中显示一个字符串。但是在后面的章节中,读者将发现,Windows程序所作的一切,都是回应发送给窗口消息处理程序的消息。这是概念上的主要难点之一,在开始写作Windows程序之前,必须先搞清楚。

别调用我,我会调用您
 

前面我们提到过,程序写作者已经熟悉了使用操作系统调用的做法。例如,C程序写作者使用fopen函数打开文件。fopen函数最终通过调用操作系统来打开文件,这一点问题也没有。

但是Windows不同,尽管Windows1000个以上的函数可供程序调用,但Windows也调用用户程序,比如前面定义的窗口消息处理程序WndProc。窗口消息处理程序与窗口类别相关,窗口类别是程序调用RegisterClass注册的。依据该类别建立的窗口使用这个窗口消息处理程序来处理窗口的所有消息。Windows通过调用窗口消息处理程序对窗口发送消息。

在第一次建立窗口时,Windows调用WndProc。在窗口关闭时,Windows也调用WndProc。窗口改变大小、移动或者变成图标时,从菜单中选择某一项目、挪动滚动条、按下鼠标按钮或者从键盘输入字符时,以及窗口客户区必须被更新时,Windows都要调用WndProc

所有这些WndProc调用都以消息的形式进行。在大多数Windows程序中,程序的主要部分都用来处理消息。Windows可以发送给窗口消息处理程序的消息通常都以WM开头的名字标识,并且都在WINUSER.H头文件中定义。

实际上,从程序外调用程序内的常量(式)这一种做法,在传统的程序设计中并非前所未闻。C中的signal函数可以拦截Ctrl-C中断或操作系统的其他中断。为MS-DOS编写的老程序中经常有拦截硬件中断的程序码。

但在Windows中,这种概念扩展为包括一切事件。窗口中发生的一切都以消息的形式传给窗口消息处理程序。然后,窗口消息处理程序以某种方式回应这个消息,或者将消息传给DefWindowProc,进行默认处理。

HELLOWIN中,窗口消息处理程序的wParamlParam参数除了作为传递给DefWindowProc的参数外,不再有其他用处。这些参数给出了关于消息的其他信息,参数的含义与具体消息相关。

让我们来看一个例子。一旦窗口的客户区大小发生了改变,Windows就调用窗口的窗口消息处理程序。窗口消息处理程序的hwnd参数是改变大小的窗口的句柄(请记住,一个窗口消息处理程序能处理依据同一个窗口类别建立的多个窗口的消息。参数hwnd让窗口消息处理程序知道是哪个窗口在接收消息)。参数messageWM_SIZE。消息WM_SIZE的参数wParam的值是SIZE_RESTOREDSIZE_MINIMIZEDSIZE_MAXIMIZEDSIZE_MAXSHOWSIZE_MAXHIDE (在WINUSER.H头文件中分别定义为数字04)。也就是说,参数wParam表明窗口是非最小化还是非最大化,是最小化、最大化,还是隐藏。

lParam参数包含了新窗口的大小,新宽度和新高度均为16位值,合在一起成为32位的lParamWINDEF.H中提供了帮助程序写作者从lParam中取出这两个值的宏,我们将在下一章说明这个宏。

有时候,DefWindowProc处理完消息后会产生其他的消息。例如,假设用户运行HELLOWIN,并且用户最终单击了 Close 按钮,或者假设用键盘或鼠标从系统菜单中选择了 Close  DefWindowProc处理这一键盘或者鼠标输入,在检测到用户选择了 Close 选项之后,它给窗口消息处理程序发送一条WM_SYSCOMMAND消息。WndProc将这个消息传给DefWindowProcDefWindowProc给窗口消息处理程序发送一条WM_CLOSE消息来回应之。WndProc再次将它传给DefWindowProcDestroyWindow调用DestroyWindow来回应这条WM_CLOSE消息。DestroyWindow导致Windows给窗口消息处理程序发送一条WM_DESTROY消息。WndProc再调用PostQuitMessage,将一条WM_QUIT消息放入消息伫列中,以此来回应此消息。这个消息导致WinMain中的消息回圈终止,然后程序结束。

伫列化消息与非伫列化消息
 

我们已经谈到过,Windows给窗口发送消息,这意味著Windows调用窗口消息处理程序。但是,Windows程序也有一个消息回圈,它调用GetMessage从消息伫列中取出消息,并且调用DispatchMessage将消息发送给窗口消息处理程序。

那么,Windows程序是依次等待消息(类似于普通程序中相同的键盘输入),然后将消息送到某地方去的吗?或者,它是直接从程序外面接收消息的吗?实际上,两种情况都存在。

消息能够被分为「伫列化的」和「非伫列化的」。伫列化的消息是由Windows放入程序消息伫列中的。在程序的消息回圈中,重新传回并分配给窗口消息处理程序。非伫列化的消息在Windows调用窗口时直接送给窗口消息处理程序。也就是说,伫列化的消息被「发送」给消息伫列,而非伫列化的消息则「发送」给窗口消息处理程序。任何情况下,窗口消息处理程序都将获得窗口所有的消息--包括伫列化的和非伫列化的。窗口消息处理程序是窗口的「消息中心」。

伫列化消息基本上是用户输入的结果,以击键(如WM_KEYDOWNWM_KEYUP消息)、击键产生的字符(WM_CHAR)、鼠标移动(WM_MOUSEMOVE)和鼠标按钮(WM_LBUTTONDOWN)的形式给出。伫列化消息还包含时钟消息(WM_TIMER)、更新消息(WM_PAINT)和退出消息(WM_QUIT)。

非伫列化消息则是其他消息。在许多情况下,非伫列化消息来自调用特定的Windows函数。例如,当WinMain调用CreateWindow时,Windows将建立窗口并在处理中给窗口消息处理程序发送一个WM_CREATE消息。当WinMain调用ShowWindow时,Windows将给窗口消息处理程序发送WM_SIZEWM_SHOWWINDOW消息。当WinMain调用UpdateWindow时,Windows将给窗口消息处理程序发送WM_PAINT消息。键盘或鼠标输入时发出的伫列化消息信号,也能在非伫列化消息中出现。例如,用键盘或鼠标选择了一个菜单项时,键盘或鼠标消息就是伫列化的,而说明菜单项已选中的WM_COMMAND消息则可能就是非伫列化的。

这一过程显然很复杂,但幸运的是,其中的大部分是由Windows解决的,不关我们的程序的事。从窗口消息处理程序的角度来看,这些消息是以一种有序的、同步的方式进出的。窗口消息处理程序可以处理它们,也可以不处理。

当我说消息是以一种有序的同步的方式进出时,我是说首先消息与硬件的中断不同。在一个窗口消息处理程序中处理消息时,程序不会被其他消息突然中断。

虽然Windows程序可以多线程运行,但每个线程的消息伫列只为窗口消息处理程序在该线程中运行的窗口处理消息。换句话说,消息回圈和窗口消息处理程序不是并发运行的。当一个消息回圈从其消息伫列中接收一个消息,然后调用DispatchMessage将消息发送给窗口消息处理程序时,直到窗口消息处理程序将控制传回给WindowsDispatchMessage才能结束运行。

当然,窗口消息处理程序能调用给窗口消息处理程序发送另一个消息的函数。这时,窗口消息处理程序必须在函数调用传回之前完成对第二个消息的处理。那时窗口消息处理程序将处理最初的消息。例如,当窗口程序调用UpdateWindow时,Windows将调用窗口消息处理程序来处理WM_PAINT消息。窗口消息处理程序处理WM_PAINT消息结束以后,UpdateWindow调用将把控制传回给窗口消息处理程序。

这也就是说窗口消息处理程序必须是可重入。在大多数情况下,这不会带来问题,但是程序写作者应该意识到这一点。例如,假设您在窗口消息处理程序中处理一个消息时设置了一个静态变量,然后调用了一个Windows函数。在这个函数传回时,您还能保证那个变量的值还是原来那个吗?难说--很可能您调用的Windows函数产生了另外一个消息,并且窗口消息处理程序在处理这个消息时改变了该变量的值。这也是在编译Windows程序时,有些编译最佳化选项必须关闭的原因之一。

在许多情况下,窗口消息处理程序必须保存它从消息中取得的信息,并在处理另一个消息时使用这些信息。这些信息可以储存在窗口的静态(static)变量或整体变量中。

当然,读者将在下面几章对此有一个更清楚的了解,因为窗口消息处理程序将处理更多的消息。

行动迅速
 

Windows 98Windows NT都是抢占式的多任务环境。这意味著当一个程序在进行一项长时间工作时,Windows可以允许用户将控制切换到另一个程序中。这是一件好事,也是现在的Windows优越于以前16Windows的地方。

然而,由于Windows设计的方式,这种抢占式多任务并不总是以您希望的样子工作。例如,假设您的程序花费一分钟左右来处理某一个消息。是的,用户可以将控制切换到另一个程序,但是却无法对您的程序进行任何动作。用户无法移动您的程序窗口、缩放它、最小化、关闭它、什么都不能做。这是因为您的窗口消息处理程序正忙于进行一项长时间的作业。表面上并不是窗口消息处理程序在运行它自己的移动和缩放操作,但实际上确实是它在做。这就是DefWindowProc部分的工作,它必须被考虑为您的窗口消息处理程序的一部分。

如果您的程序在处理某些消息时需要长时间的作业的话,可以选择我在第二十章里描述的那些方法来做得更有优雅一些。即使是在抢占式多任务环境中,也不应该让您的程序呆在屏幕上一动不动。这会让用户讨厌的,他们会认为您的程序中有bug、不标准的动作,说明文件没写好。最好让用户觉得程序只停了一下子就把全部消息中快速料理完了。

 

 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值