现在的位置: 首页 > 综合 > 正文

第三章 窗口与消息

2014年04月05日 ⁄ 综合 ⁄ 共 7860字 ⁄ 字号 评论关闭

Windows中,“窗口”这个词具有明确的含义。窗口是位于屏幕中的一个矩形区域,它用于接收用户的输入,然后以文本的或图形的形式显示输出。

在进行Windows程序设计时,其实就是在进行一种面向对象的编程。有一种对象我们接触得最频繁的就是窗口。

滚动条:scrollbar 注意与滑块(scrollbox)的区别。

应用程序如何获知用户对窗口尺寸已做调整的呢?

对于习惯于传统字符模式编程的程序员来说,操作系统不具备将这类信息传递给用户的机制。因此这个问题是理解Windows体系结构的关键。当用户改变窗口的尺寸时,Windows便向应用程序发送一条携带新窗口尺寸相关的信息的消息(实际上是Windows调用了改程序内部的一个函数--窗口过程,改程序的核心),接着应用程序(窗口过程)对自身的内容进行调整调整以反映出窗口尺寸的变化。

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

窗口 和 程序实例 的区别?

窗口是依据窗口类进行创建的,允许多个窗口共享同一窗口类。

程序实例唯一的标识当前的应用程序,例如我们双击hello_world.exe文件,在任务管理器上我们就看到有一个hello_world.exe的进程在运行。再双击这个执行文件,就会再次创建一个进程(这样解析不知是否正确,实际如何暂时未知?)。

很直观的感觉就是窗口在前台,程序实例在后台。还有就是有时候一个窗口(顶级窗口)对应一个程序实例,有时候却不是,例如窗口上的按钮,窗口上也许有多个按钮(按钮也是窗口的一种),但是也许整个系统中就只存在一个程序实例。

这里需要特别注意的是:当创建工程并且正确地敲入本章的HELLOWIN.C程序,却发现链接时一条错误提示,指出无法对PlaySound函数进行解析。原因是:HELLOWIN程序调用了一个多媒体函数,而多媒体对象库并未包含在默认项目内。解决办法是,从project菜单中选择Setting,然后选择Link选项卡。从Category列表框中选择General,并添加WINMM.LIBObject/Library Modules文本框里。

实际上,任何Windows 程序的结构都与HELLO.C大体类似。没有人会真正记住这种框架的所有细节。通常,Windows程序员都是将已有的程序代码复制到新程序中,然后加以修改。你也可以按照这种方式自由使用配套光盘中的代码。

/*------------------------------------------------------------
   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 ;
     WNDCLASS     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.lpszMenuName  = 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) ;
}

客户区:是一块应用程序可以自由绘图并向用户传达可是输出的区域。

前缀

常量

CS

类风格选项

CW

创建窗口选项

DT

文本绘制选项

IDI

图标的ID

IDC

光标的ID

MB

消息框选项

SND

声音选项

WM

窗口消息

WS

窗口风格

在进行Windows编程时,这些数值常量是没必要强记下来的。实际上,每一个数值常量在头文件仲都定义有一个标识符。

P43看了多次后,终于明白了这个“典故”。

当Windows还是16位系统时,WndProc的第三个参数类型被定义为WORD,表示一个16位的无符号短整型,而第4个参数类型被定义成LONG,表示一个32位的有符号长整形,”PARAM“ 的前缀“W”和“L”正是由此而来。但是在32位版本的Windows中,WPARAM被定义为UNIT(unsigned int),而LPARAM被定义成LONG(long),因此窗口过程的这两个参数都是32位的。这就有些令人疑惑,因为WORD类型在Windows98(32位系统)中仍然被定义为16位无符号短整型,因此“WPARAM”的前缀“W”有些取名不当。

WINAPI CALLBACK都被定义成__stdcall,它指定了在Windows 和应用程序之间函数调用的特定次序。具体是怎样?

匈牙利标记法:

1、变量名以表明该变量数据类型的小写字母开始。例如,szCmdLine的前缀sz表示“以零结束的字符串”。

2、当命名结构体变量时,可使用结构名(或结构名称缩写)的小写形式作为变量名名称或整个变量名。例如msg变量是一个MSG类型的结构,而ps是一个PAINTSTRUCT结构。

3、由于变量的名称描述了该变量的用法及其数据类型,在编程时就不大可能犯数据类型不匹配的错误,从而帮助你远离一些bug

本书中使用的变量名前缀如下表所示。

前缀

数据类型

c

char 或 WCHAR 或 TCHAR

by

BYTE(无符号字符)

n

short(短整型)

i

int(整形)

xy

int, 表示x坐标和y坐标

cx,cy

int, 表示xy的长度,c表示“count”(计数)

或 f

BOOL(int); f表示“flag

w

WORD(无符号短整型),在Windows 98 中依然为16

l

LONG(长整形)

dw

DWORD(无符号长整形)

fn

函数

s

字符串

sz

以零结束的字符串

h

句柄

p

指针

cb:字节数(count of byte

hbr: 画刷的句柄(handle to a brush

lpsz: 指向以零结束的字符串的长指针(long pointer to a string terminated with a zero

为了获取预定义图标的句柄,需要调用函数LoadIcon,并将函数的第一个参数设为NULL。而在从保存在磁盘中的应用程序的可执行文件中加载自定义图标时,该参数必须设为hInstance, 即相应程序的实例句柄。对于预定义的图标来说,该参数是一个前缀为IDIID for an icon)的标识符。

实际上,系统存在两种不同的用于注册窗口类的函数:RegisterClassA 和 RegisterClassW。程序利用哪一个函数注册窗口类,决定着传递给窗口的消息是包含ASCII文本还是UNICODE文本。

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

  if(!RegisterClass(&wndclass))
  {
  //MessageBoxW是Windows 98中所实现的少数几个UNICODE函数之一
  	MessageBoxW(NULL, TEXT("This program requires Windows NT!"),
  		szAppName,MB_ICONERROR);
  	return 0;
  }

层叠窗口:overlapped window.

WS_OVERLAPPEDWINDOW风格的窗口:该窗口有一个标题栏、一个位于标题栏左边的系统菜单按钮、一个窗口尺寸调整边框以及位于标题栏右方的单个按钮(分别用于最小化、最大化和关闭窗口)。查看头文件WINUSER.H,会发现该风格其实是由几个位标记通过按位或组合而成。

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

擦除和重绘的区别?

例如,函数ShowWindow用于将窗口显示在屏幕中。如果该函数的第二个参数是SW_SHOWNORMAL,则该窗口的客户区将被在窗口类中所指定的背景画刷擦除(即粉刷一遍)。然后下面的调用将使客户区重绘(即在另一块布上作图...):

UpdateWindow(hwnd);

这是通过向窗口过程发送一条WM_PAINT消息而完成的。

Windows为当前在其中运行的每一个Windows程序都维护了一个“消息队列”。当输入事件发生后,Windows会自动将这些事件转换为“消息”,并将其放置在应用程序的消息队列当中。

GetMessage(&msg, NULL, 0, 0);//用于从消息队列中对消息进行检索

该调用将一个指向名称为msgMSG结构变量的指针传给Windows。其中后面的参数分别被设为NULL0,表明该程序希望获取由该程序所创建的所有窗口的消息。Windows用从消息队列中得到的下一条消息来填充消息结构的各个字段。其中message消息标识符,是一个用于标识消息的数字。对于每条消息,在Windows的头文件中都为其定义了一个以WM_为前缀的标识符。例如WM_LBUTTONDOWN(值为0x0201)

TranslateMessage(&msg);

msg结构返还给Windows以进行某些键盘消息的转换。

DispatchMessage(&msg);

msg结构再次返回给Windows。接着,Windows会将这条消息发送给合适的窗口过程来处理。

总结一下:应用程序调用了Windows,即应用程序传递一个消息给系统。换句话说就是应用程序调用了系统函数(API)。即控制权转到了Windows上。

当窗口过程对消息进行处理后,应返回0(此时再不用纠结到底在case语句后是用break还是return了)。所有窗口过程不进行处理的消息都必须传给名称为DefWindowProcWindows函数。DefWindowsProc的返回值必须从窗口过程中返回(此时也不用纠结为什么在switch-case语句的最后还要返回一个值了)。

WindowsWinMain函数中处理CreateWinndow函数调用时,WndProc将接收到WM_CREAT的消息。也就是说,当HELLOWIN调用CreateWindow时,Windows完成必要的操作,同时在此过程中,WindowsWndProc进行调用(即在CreateWindow中包含调用窗口过程的代码),接着,WndProcWM_CREATE消息进行处理,并将控制权返还给Windows(即继续执行CreateWindow的代码)。然后WindowsCreateWindow调用返回到HELLOWIN中,并继续执行WinMain中的其他步骤。

PlaySound以异步方式播放声音,即当所指定的声音文件开始播放时PlaySound函数便立即返回,而无需等待该文件播放结束。按照这种方式,应用程序可以继续完成其他初始化工作。

当窗口的客户区的全部或部分“无效”且必须“更新”时,应用程序将得到WM_PAINT消息。但是何种情况下客户区会变成无效?

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

(2)、在调整窗口的尺寸时,客户区也会变得无效。

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

(4)当被覆盖的区域在后来不在被遮盖时,窗口被标记为无效。

BeginPaint调用期间,如果客户区的背景尚未被擦除,则Windows会对其进行擦除。擦除背景时使用的画刷是在用于注册窗口类的WNDCLASS结构中的hbrBackground字段中指定的。在HELLOWIN这个例子中,所使用的画刷是一个库存的白色画刷,即Windows会将窗口的背景清除为白色。BeginPaint调用将使这个客户区有效。

问题:如果不擦除背景会出现什么状况?

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

return msg.wParam;

msg结构的wParam字段是传递给PostQuitMessage函数的值(通常情况下为0)。该返回语句将从WinMain中退出并将程序关闭。 

窗口所发生的一切都通过消息的形式传给窗口过程。然后窗口过程以某种方式对消息作出反应,或者把消息传递给DefWindowProc以进行默认处理。

由于对DefWindowProc的调用,一些消息还会产生其他消息。例如,

1)假定你运行HELLOWIN,并且用鼠标单击了【关闭】按钮,或者假定使用键盘或鼠标选择了系统菜单中的【关闭】选项,那么DefWindowProc会对该键盘或鼠标输入进行处理。

2)当该函数检测到你已选择【关闭】,便会向窗口过程发送一条WM_SYSCOMMAND的消息。

3)WndProc会将该消息传给DefWindowProc

4)作为响应,DefWindowProc又会给窗口过程发送一条WM_CLOSE消息。

5)WndProc再次将该消息传给DefWindowProc

6)此时DefWindowProc会通过调用DestroyWindow来对WM_CLOSE作出响应。DestroyWindow则会导致Windows向窗口过程发送一条WM_DESTROY消息。

7)WndProc最终又会通过调用PostQuitMessage将一条WM_QUIT消息投递到消息队列中来作为对该消息的响应。这条消息会使WinMain中的消息循环结束,并使程序终止。

总结一下:从这个过程可以看出,WndProc不进行处理的消息确实是交给DefWindowProc进行处理。那么实际上,上面的消息都可以由WndProc窗口过程进行拦截处理。只需在switch-case语句中加上响应的消息选项即可。

队列消息:是指那些由Windows放入程序的消息队列的消息。主要由用户的输入产生,主要形式为按键消息、由按键产生的字符消息、鼠标移动、鼠标单击。此外还包括定时器消息、重绘消息和退出消息。

非队列消息:是由Windows对窗口过程的直接调用而产生的。通常由调用特定的Windows函数引起。例如,当WinMain调用CreateWindow函数时,Windows就会创建窗口,并在创建过程中向窗口过程发送WM_CREATE消息。更多请参看P60

我们一般说队列消息是被“投递(post)”到消息队列中,而非队列消息则是被“发送(send)”到窗口过程。

从窗口过程的视角看,这些信息是以有序、同步的方式到来的。这句话的第一层含义是指消息与硬件中断不同。在窗口过程处理某一消息的过程中,程序不会被其他消息突然中断。

窗口过程与多线程的关系?第20

抱歉!评论已关闭.