Win32学习笔记
作者: 姜学哲(netsail0@163.net)
教材: Windows程序设计(第五版)北京大学出版社
[美]Charles Petzold 著
北京博彦科技发展有限公司 译 ¥:160
参考资料:
Windows 应用程序设计原理_方法_技术(因为是PDF格式的EBOOK,作者等不详)
新编Windows API 参考大全 电子工业出版社 ¥:98
C++ Primer(第三版)中国电力出版社 Stanley B.Lippman & Josee Lajoie 著 潘爱民 张丽 译 ¥:128
TURBO C实用大全 徐金梧 杨德斌 徐科 编 ¥:42
环境: windows2000 Pro + Internet Explorer 6.0 + DirectX8.1 + Visual C++ 6.0
图们江计算机程序编制小组(http://chulsoft.xiloo.com)版权所有,转载请说明出处
--------------------------------------------------------------------
【第三章 HelloWin】
4月16日是值得纪念的日子。主页上传第十六天,我们的网站访问量是19500。我们什么都没有做,连搜索引擎都没有提交。所以感到很意外。怎么会有这么多人访问的,访客是怎么知道我们的网站的呢?真是匪夷所思。
经过了前两章的准备活动后要正式看WIN32代码了。在这之前我假设您已经具有C语言的知识。如果您没有C方面的知识,那么抓紧时间学吧。第三章的重点就是第三十九页开始的HelloWin了。我们的全部任务就是看懂它。可能是我特别低能+没耐心,几个月前第一次看到第三章的时候我选择了放弃,那时候觉得我这辈子都看不懂它,真是惭愧。
对我来讲它实在是太难了。因为很多新知识点对我来讲都是空白。头脑中一点概念都没有,真的是十足的“一头雾水”。因为感到WIN32太难,我又转向了DOS下的C,也就是TC2环境下的编程,学了一会儿,又觉得TC2也很难,又回到了WIN32,我也知道这种学习态度是不好的,虽然我这次从TC2转到WIN32的原因又是避重就轻,但是这次一定要学好WIN32,这再也不能像前几次那样了。
其实现在想起来,我缺的就是耐心,如果当时多看几遍就能看懂的,白白浪费了那么多时间。所以,如果您觉得这个程序太难,您一定要坚持多读几遍。过程是比较枯燥的。从一开始您就会碰到一些困难,比如Win32专用的术语。有些术语的含义简单,却很不好解释。
我们需要一段时间来适应。开头处您会碰到“窗口过程”,“窗口类”等术语。因为头脑中完全没有概念,所以当您看到这些术语的时候可能产生对人生的绝望。请珍稀您的生命,坚强一点,对付这些术语的方法是硬着头皮往下读。可以先不管具体的意思。只要在脑子里留点印象就可以了。等您看懂了HelloWin代码后再回过头来重读 N 遍,您会有意外的收获。
我是农民出身嘛!再加上智商也不是很高,读书只能用这种笨办法了。这种事要靠天份的。可能您看一遍就懂了也说不定。
当您读完了第三章后可能还有很多疑点。其中的相当一部分将在第四章中解释。所以当您看到了一个疑点,书中又没有详细说明时可以先跳过那段。学过后面几章后再回过头来看一看。这样做绝对不是浪费时间。
为了学好WIN32我曾经想过放弃"帝国时代"和"星际争霸",不不!!我死也不放弃!!我的最爱啊!可能随着学习的深入这些游戏会渐渐离我远去。对了,不知道星际什么时候出2代啊?
在前两章示例程序使用了MessageBox()来向用户输出文本。MessageBox()函数创建一个消息框窗口。在Windows中"窗口"一词有确切的含义。一个窗口就是屏幕上的一个矩形区域。它接收用户的输入,并以文本或图形的格式显示输出内容。
MessageBox()创建一个消息框窗口,但这只是一个功能有限的特殊窗口。这个窗口有一个带关闭按钮的标题栏、一个可选的图标、一行或多行文本,以及最多4个按钮。
MessageBox()虽然有用,但我们不能在消息框中显示图形,也不能在消息框中添加菜单。想要添加这些东东就需要创建自己的窗口,现在就开始。
书上说创建窗口很简单,只需要调用CreateWindow()即可。实际上Windows已经做好了大部分事情,我们所要做的只是记住API函数的名字和功能,然后到了实际的开发过程中照搬就可以了。这听起来似乎很简单,但是事实上还有很多工作需要程序员来做。
零件虽然做好了,但是组装起来还是得费一番功夫的。另外如果您的英语水平过得去,将对学习有很大的帮助。微软的MSDN是很好的东东。可惜我不会看啊。
进行Windows程序设计,实际上是进行一种面向对象的程序设计(OOP)。不要跟我说C不支持面向对象,我什么都不知道。
桌面上最明显的窗口就是应用程序窗口。这些窗口含有显示程序名称的标题栏,菜单,工具栏,滚动条。装饰对话框表面的还有各式各样的按钮,单选框,复选框,列表框,滚动条和文本输入区域。这些都是窗口。更确切地说这些都称为"子窗口","控件窗口","子窗口控件"。
其实按钮也是一种窗口。比如Windows的“开始”菜单按钮,它就是一个窗口,叫做“按钮窗口”。如果在不使用Windows API的情况下以C语言编写滚动条最快也得两个星期,而且写出来的滚动条一定是很难看的那种。对于像我们这样的菜鸟来说自己写滚动条函数并不是令人Happy的事情。非常Lucky的是Windows已经提供了一大串现成的滚动条函数。
当我们移动鼠标的时候,当我们敲键盘的时候,当我们单击鼠标左右键的时候…当…当…当…,会产生一种信号。这种信号代表某种操作。我们可以用一种信号表示鼠标的移动,用另一种信号表示鼠标左键的按下等等等等。Windows把这种信号称为“消息”。
当用户点击一个窗口时这个窗口就会收到一个“消息”。窗口以“消息”形式接收窗口的输入,窗口也用消息与其它窗口通讯。对于消息的理解将是学习编写Windows程序必须逾越的障碍之一。很重要啊!虽然消息这个东东并不是很难理解,但是确实是很陌生的东东。
按一个键盘会产生一个消息,移动鼠标产生一个消息,点击鼠标左键产生消息……总之Windows是靠消息来驱动的。到底什么是消息呢?等您看懂HelloWin程序之后大概也会明白。我只能说,多看几遍吧!!
我们可以用鼠标拖动窗口边框来改变窗口大小。程序会改变窗口中的内容来响应这种变化。重新调整窗口尺寸的工作是Windows处理的。应用程序没有这种负担。应用程序所要做的只是改变窗口的内容来响应这种变化。
应用程序是如何知道用户改变了窗口的尺寸的呢?有很多程序员已经习惯了字符模式程序。在字符模式下操作系统没有将此类消息通知给应用程序的机制。问题的关键在于理解Windows使用的体系结构。当用户改变窗口尺寸时Windows给程序发送一条消息,指出窗口的新尺寸。然后程序调整窗口中的内容,以反映尺寸的变化。
“Windows给程序发送消息”。这对于有过字符模式下编程经验的一些人来说有点不好理解。操作系统怎么会给程序发送消息呢?其实“Windows给程序发送消息”是指Windows调用程序中的一个函数,该函数的参数被设计为接收Windows和用户发出的消息。这种位于应用程序中的,被系统调用的函数被称为“窗口过程”。
我们对于应用程序调用操作系统是很好理解的。比如说在DOS下应用程序经常调用DOS中断。但是对于操作系统调用应用程序中的函数可能很不习惯。而这正是Windows面向对象体系结构的基础。
程序创建的每一个窗口都有一个相关的窗口过程。窗口过程是一个函数。这个函数可以在程序中,也可以在动态链接库中。Windows通过调用窗口过程来处理窗口发送的消息。窗口过程根据此消息进行处理,然后将控制返回给Windows。
窗口通常是在窗口类的基础上创建的。在阅读HelloWin的时候您会了解到什么是窗口类。窗口类中指定了处理该窗口的消息的窗口过程。多个窗口能够基于同一个窗口类,并且使用同一个窗口过程。例如,所有Windows程序中的所有按钮均基于同一个窗口类。我们所见过的所有的Windows按钮都使用一个窗口过程。
在窗口尺寸改变或窗口表面需要重画时由一种消息通知窗口。向窗口发送的消息由该窗口的窗口过程函数处理。
Windows程序开始执行后,Windows为该程序创建一个“消息队例”。这个消息队列用来存放该程序创建的各种不同的窗口消息。程序中有一小段代码,叫做“消息循环”,用来从队列中取出消息,并且将它们发送给相应的窗口过程。有些消息直接发送给窗口过程,不用放入消息队列中。
虽然我叽叽歪歪say了一堆,但是您可能还是不太明白。没关系,接下来我们看第一个Win32程序。这是本章的重点,而且是整个Win32的真正开始。这个代码具有代表性,您最好是手工敲几遍,这绝对有必要!这样脑子会留下一些印象。千万不要偷懒哦……
◎第三十九页:
/*------------------------------------------------------------
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);//执行默认的消息处理
}
-----------------------------------------------------------------------------
整个程序由两个函数组成。WinMain()是程序的入口。而WndProc()就是窗口过程。Windows系统会调用它。
WinMain()前面有WINAPI,这个东东是以前从来没见过的。所以特地跑到论坛问了一下。有人说这是定义参数的调用顺序的。到底是什么意思我真的不明白。但是我知道在WINDEF.H中它的定义如下:
#define WINAPI __stdcall
在窗口过程函数前面有一个CALLBACK,他的定义如下:
#define CALLBACK __stdcall
窗口过程函数的类型是LRESULT,它的定义如下:
typedef long LRESULT;
但是请注意!千万不要把
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
写成:
long CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
虽然LRESULT的确是long型,但是在定义窗口过程函数时的格式不能随便改。
WPARAM LPARAM 是 long
#define PASCAL pascal
#define NEAR near
#define FAR far
typedef unsigned int UINT;
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef unsigned long DWORD;
typedef long LONG;
typedef char *PSTR;
typedef char NEAR *NPSTR;
typedef char FAR *LPSTR
typedef void VOID;
typedef int *LPINT;
typedef LONG (PASCAL FAR* FARPROC)();
随书光盘中有源代码和HelloWin声音文件。把整个HelloWin复制到您的硬盘中,然后去掉只读属性。然后编译运行一次程序。一定要去掉只读属性。
程序创建一个普通的应用程序窗口。在窗口客户区的中央显示"Hello, Windows98!"。程序的WinMain()部分具有代表性,在以后的很多程序中会重复出现N次,而且几乎是原封不动的。您会发现各个程序间不同的是窗口过程函数部分。
HelloWin程序在窗口客户区中央显示文本串。客户区就是程序自由绘图并且向用户交付可视输出的窗口区域。HelloWin程序中有一大片白色区域,那就是客户区。
和其它的程序一样,HelloWin创建的窗口可以移动,可以改变窗口尺寸。右上角有“最大化”,“最小化”和“关闭”按钮。无论用户如何改变窗口尺寸,程序都会自动将"Hello, Winodws98!"文本串重新定位在客户区域的中央。我将对实现上述功能的代码一一做介绍。
HelloWin调用了18个函数。这18个函数都是Windows API函数。如果您想了解这18个函数的细节,可以查阅Windows API参考手册。我们绝对有必要买一本Windows API的参考资料。如果您有好的API参考手册别忘了告诉我书名。如果您的英语水平很不错,也可以去看MSDN。
程序中有很多大写的标识符。这些标识符都是在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
以上都是简单的数值常量。标识符的前缀表示该常量所属的类别。
CS --- 类风格选项
CW --- 创建窗口选项
DT --- 绘制文本选项
IDI---图标ID号
IDC---光标ID号
MB ---消息框选项
SND---声音选项
WM ---窗口消息
WS ---窗口风格
MSG -- 消息结构
WNDCLASS -- 窗口类结构
PAINTSTRUCT -- 绘图结构
RECT -- 矩形结构
最后还有三个大写标识符,用于不同类型的“句柄”。
HINSTANCE --实例(程序自身)句柄
HWND -- 窗口句柄
HDC -- 设备描述表句柄
句柄在Windows中使用非常频繁。也是非常重要的一个概念。我们还将遇到图标句柄HICON、鼠标指针句柄HCURSOR、图形刷句柄HBRUSH。到底什么是句柄呢?
句柄是一种新的数据类型。菜单,窗口,图标,内存,设备,程序,位图等都被称为“对象”。也就是说菜单是菜单对象,窗口是窗口对象,内存是内存对象。对于这样的称呼您一定要习惯。句柄可以代表一个对象。我们用句柄引用一个对象。例如我们可以用设备描述表句柄引用一个设备(比如显示器)。当我们用一个设备描述表句柄引用显示器时,这个句柄代表的就是显示器。我们通过这个句柄使用显示器。
■常用的句柄类型
HANDLE------------------通用句柄类型
HWND--------------------标识一个窗口对象
HDC---------------------标识一个设备对象
HMENU-------------------标识一个菜单对象
HICON-------------------标识一个图标对象
HCURSOR-----------------标识一个光标对象
HBRUSH------------------标识一个刷子对象
HPEN--------------------标识一个笔对象
HFONT-------------------标识一个字体对象
HINSTANCE---------------标识一个应用程序模块的一个实例
HLOCAL------------------标识一个局部内存对象
HGLOBAL-----------------标识一个全局内存对象
也就是说HWND代表的是一个窗口对象,HDC代表的是一个设备对象。这个设备有可能是内存,也有可能是磁盘。句柄是一个数,通常为32位数,以十六进制形式表示。您明白我在说什么吗?不明白?看完这本书后可能就明白了,请坚持下去吧。
进入程序后第一个碰到的是一组变量定义。
static TCHAR szAppName[] = TEXT ("HelloWin") ;
HWND hwnd ; //窗口句柄
MSG msg ; //消息结构
WNDCLASS wndclass ; //窗口类结构
第二个变量是一个窗口句柄。它代表一个窗口。那么它代表的是哪个窗口呢?现在还没有赋值啊,所以它哪个窗口都没有代表。
当然,如果您高兴第二个变量也可以定义成:
HWND aaa; //或者:
HWND bbb;
和int, float等标准数据类型一样,变量名可以随便起。下面的两个也是同样道理:
MSG addf;
WNDCLASS sdakd221;
首先要定义窗口类,也就是给窗口类结构赋值。窗口类只是定义窗口大概的样子。so,所有基于此窗口类创建的窗口对象都会有窗口类中给出的特点。下面我们一个一个地分析窗口类的每一个域。
wndclass.style = CS_HREDRAW | CS_VREDRAW;
上面的语句表示:每当窗口的水平方向尺寸(CS_HREDRAW)或者垂直方向尺寸(CS_VREDRAW)改变后,要完全刷新窗口(刷新就是在屏幕上重画。我们所看到的所有图形界面都是"画"出来的。)。无论如何改变Hellowin的窗口尺寸我们都可以看到文本串“Hello Windows98!”仍然显示在窗口的中央,这两个标识符确保了这一点。
wndclass.lpfnWndProc = WndProc;
这条语句将这个窗口类的窗口过程函数指定为WndProc()。以后凡是基于此窗口类创建的所有新窗口都会把WndProc()当作自己的窗口过程函数。这一条很重要哦,一定要记住!
wndclass.cbClsExtra = 0;
基于同一个窗口类创建的窗口对象的公共数据区大小。不太明白这是什么意思,我是菜鸟嘛。看字面上的意思好像是说所有基于此窗口类创建的窗口对象都会拥有一个公共的内存区。我的理解正确吗?有没有高手教我啊?
wndclass.cbWndExtra = 0;
当前窗口对象私有的数据区大小。
wndclass.hInstance = hInstance; //当前进程对象实例句柄
本程序的句柄。也就是代表HelloWin程序的句柄。如果让我细细讲来,可就难为我了。我还是个菜鸟啊!可能看完了整个书就会懂吧?这个是WinMain()的参数之一。
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
为所有基于此窗口类创建的窗口设置一个图标。想要加载自己画的图标时这个参数应该被设置为程序的实例句柄hInstance。如果想要获取预先定义的图标句柄我们可以把LoadIcon()的第一个参数设置为NULL。当第一个参数为NULL时,第二个参数有以下选项:
IDI_APPLICATION 默认的应用程序图标。您可以看看HelloWin的样子,那个方框就是了。
IDI_ASTERISK 星号
IDI_EXCLAMATION 惊叹号
IDI_HAND 手形图标
IDI_QUESTION 问号
IDI_WINLOGO Windows徽标
以上的标识符可以在WINUSER.H中找到。函数返回代表该图标对象的句柄。
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
LoadCursor()函数加载一个预先定义的光标,并返回该项光标的句柄。该项句柄被赋给WNDCLASS结构的hCursor域。当鼠标经过"基于此窗口类创建的窗口"时,它变成一个小箭头。不过因为之前的光标也是小箭头,所以我们看不出光标的变化。下面是预定义的鼠标指针标识符:
IDC_APPSTARTING 标准箭头及小沙漏
IDC_ARROW 标准箭头
IDC_CROSS 十字交叉
IDC_HAND (Windows2000)手形
IDC_HELP 箭头和问号
IDC_IBEAM 文本I形
IDC_ICON 空图标
IDC_NO 斜杠圈
IDC_SIZE 四向箭头
IDC_SIZEALL 四向箭头
IDC_SIZENESW 指向东北和西南的双向箭头
IDC_SIZENS 指向南北的双向箭头
IDC_SIZENWSE 指向西北和东南的双向箭头
IDC_SIZEWE 指向东西的双向箭头
IDC_UPARROW 垂直箭头
IDC_WAIT 沙漏
您可以把语句中的IDC_ARROW换成别的,然后再编译运行一次。当您把鼠标移到窗口上面时会看到不同的结果。试一试啊!!你可以先试一试IDC_CROSS。
wndclass.hCursor = LoadCursor(NULL, IDC_CROSS);
然后编译运行一次。把鼠标移到窗口对象上面,看看鼠标有什么变化?鼠标是不是变成十字形了?之前的LoadIcon()也是同样道理,您可以试着换一下第二个参数。看看有什么变化。
wndclass.hbrBackground = GetStockObject(WHITE_BRUSH);
设定基于此窗口类创建的窗口对象的背景颜色。hbr代表“handle to a brush(刷子句柄)”。刷子是图形学上的术语。指用来填充一个区域的着色像素模式。Windows有几个标准刷子,也称为备用(stock)刷子。上面所示的GetStockObject()调用将返回一个白色刷子的句柄。窗口客户区将完全为白色。这是一种及其普遍的做法。
wndclass.lpszMenuName = NULL;
指定窗口类菜单。HelloWin没有菜单,所以这项为NULL。
wndclass.lpszClassName = szAppName;
给这个窗口类取名字。以后就用这个名字认它了。
if (!RegisterClass (&wndclass))//为程序窗口注册窗口类
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
创建一个窗口首先需要注册一个窗口类。RegisterClass()的功能是注册窗口类。但是注册窗口类并不是每次都能成功。所以应该有一个查错机制。
RegisterClass()注册失败后会返回0。所以当窗口类注册失败后if为真,接着执行MessageBox()。执行MessageBox()的结果是显示一个消息框,并显示"This program requires Windows NT!"。然后退出整个程序。这段英文的大概意思是说"本程序需要Windows NT的支持!"。如果您使用的是Windows98,当您运行附书光盘中的HelloWin.exe时可能有机会看到这个消息框。我的是Win2k,所以看不到啊。出现这个消息框表明注册窗口类失败。
窗口类定义了窗口的一般特征,可以使用同一窗口类创建许多不同的窗口。调用CreateWindow()创建窗口时指定有关窗口的更详细的信息。
为什么一个窗口的所有特征不能被一步到位指定呢?实际上以这种方式分开这些风格信息是非常方便的。例如,所有的按钮窗口都可以基于同样的窗口类来创建。与这个窗口类相关的窗口过程位于Windows内部。所有的按钮都是以同样的方式工作的。但是每一个按钮都有不同的尺寸,不同的屏幕位置,以及不同的文本串。这些不同的特征是CreateWindow()定义的一部分,而不是窗口类定义的。
hwnd = CreateWindow (szAppName, // 指定一个窗口类,基于该窗口类创建窗口
TEXT ("The Hello Program"), // 这个字符串会出现在标题栏中
WS_OVERLAPPEDWINDOW, // 本窗口风格
CW_USEDEFAULT, // 窗口的X坐标,更准确地说是窗口左上角的X坐标
CW_USEDEFAULT, // 窗口的Y坐标
CW_USEDEFAULT, // 窗口的宽度
CW_USEDEFAULT, // 窗口的高度
NULL, // 窗口对象的父窗口句柄
NULL, // 窗口对象的菜单句柄或者子窗口编号
hInstance, // 当前进程的实例句柄
NULL) ; // 窗口对象的参数指针句柄(我也不知道这是什么意思)
我们要关注的是第三个参数WS_OVERLAPPEDWINDOW
WINUSER.H中对WS_OVERLAPPDWINDOW定义如下。
#define WS_OVERLAPPDWINDOW (WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX)
您想搞清楚这一长串到底是什么东东吗?很…很…很Easy!这是定义该窗口对象的风格。从上面的语句里可以看出第三行的真正的内容是:
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
问题是书上没有说明上面六个WS_XXXX都是干什么用的,所以只能由我们自己想办法。
请将第三个参数WS_OVERLAPPDWINDOW换成:
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
接下来要做的就是把六个WS_XXXX一个一个地去掉,每去掉一次就重新编译一次,看看窗口会变成什么样子。首先去掉WS_SYSMENU,也就是说第三个参数变成了:
WS_OVERLAPPED | WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
然后编译一次,您会Lucky地看到您的HelloWin.exe已经是面目全非了,窗口右上角的三个按钮不见了,左上角的小图标也不见了,用鼠标右击标题栏时不会出现系统菜单。由此我们已经知道WS_SYSMENU的含义了。接下来让我们看看WS_MINIMIZEBOX,WS_MAXIMIZEBOX是干什么用的,改成如下所示后,再编译
WS_OVERLAPPED |WS_CAPTION | WS_SYSMENU | WS_THICKFRAME
您会High地看到右上角只有一个按钮了,最大化,最小化两个按钮消失于时间和空间的尽头……以此类推,您可以一个一个的试。
第四,第五个参数分别是窗口对象在屏幕上的x,y坐标,准确地说是窗口对象的左上角的坐标。
第六,第七个参数分别是窗口对象的宽度和高度。
第八个参数父窗口句柄。您不知道父窗口是什么东东?好,请打开记事本程序,按CTRL+O,会发现出现一个窗口,这个窗口总是在记事本上面 ,这个窗口是记事本的子窗口,而记事本就是父窗口。OK?还不知道?………………HelloWin没有父窗口,也就没有子窗口。所以这一项选的是NULL。
HelloWin没有菜单,所以第九项也是NULL。
第十项,书上写的是,hInstance是程序的实例句柄,也就是说这个句柄代表的是程序自身。同时它也是WinMain()的参数之一。
第十一项,窗口对象的参数指针句柄。看到这么长的名字是不是头晕了?我不知道具体含义sorry......
CreateWindow()返回被创建的窗口的句柄,该句柄被存放在变量hwnd中。Windows中的每个窗口都有一个句柄,程序用句柄来引用窗口。许多Windows函数都需要使用hwnd作为参数,这样,Windows才能知道函数是针对哪个窗口的。如果一个程序创建了许多窗口,则每个窗口都有一个句柄。窗口句柄是Windows程序处理的最重要的句柄之一。
就这样,CreateWindows()调用返回后窗口已经创建完毕。Windows已经分配了一块内存,用来保存在创建窗口过程中设置的全部信息。
但是我们仍然无法在屏幕上看到窗口。还需要两个函数显示窗口。一个是:
ShowWindow(hwnd, iCmdShow);
hwnd是刚刚用CreateWindow()创建的窗口对象的句柄。我们要好好看一看第二个参数。这也是WinMain()的第四个参数,int类型。那么,这是干什么用的呢?Come on Baby!
第二个参数有三个常量可供选择。
SW_SHOWNORMAL SW_SHOWMAXIMIZED SW_SHOWMINNOACTIVE
这些都是干什么用的???跟上次一样,改代码!请把
ShowWindow(hwnd, iCmdShow);
改成
ShowWindow(hwnd, SW_SHOWMAXIMIZED) 或是 ShowWindow(hwnd, SW_SHOWMINNOACTIVE)
运行一次您就Know了。虽然名字是ShowWindow(),但是我们还是不能Show到HelloWin生成的窗口。因为我们需要重新画屏幕,也就是刷新。
UpdateWindow(hwnd);
该函数的功能是刷新窗口。现在终于可以看见窗口了。
while (GetMessage (&msg, NULL, 0, 0)) //从消息队列中获取消息
{
TranslateMessage (&msg) ; //转换某些键盘消息
DispatchMessage (&msg) ; //将消息发送给窗口过程
}
Windows为当前运行的每个程序维护一个消息队列。这一点很重要。当您点击鼠标左右键的时候会发生消息,当您点了某个按钮的时候也会发生消息。总之,不管您干什么,都会产生消息。这些消息会进入消息队列中。GetMessage()负责从消息队列中取出消息。
TranslateMessage()在按键时系统产生虚拟键消息(VK_TAB等等)。在接收虚拟键代码时该函数将相应的WM_CHAR代码发送到应用程序消息队列中。具体的意思嘛我不懂啊。不过这个东东暂时好像不是很重要。
DispatchMessage()从应用程序的消息循环中发送消息至相应的窗口过程。
消息有入队与不入队之分。入队消息按照顺序一个一个送到窗口过程,就像排队买票一样。不入队消息的优先级是比较高的,可以直接发送给窗口过程。就好像领导不用排队买电影票一样。while循环处理的都是入队的消息。
msg变量是类型为MSG的结构,类型MSG在WINUSER.H中定义如下:
typedef struct tagMSG
{
HWND hwnd; //向窗口过程函数发送该消息的窗口对象的句柄,也就是该消息的来源。
UINT message; //消息标识符,也就是消息内容。
WPARAM wParam; //消息的附加信息,取决于message的值。
LPARAM lParam; //消息的附加信息,取决于message的值。
DWORD time; //发送消息的时间。
POINT pt; //发送消息时屏幕上鼠标的位置。
}
MSG, *PMSG;
POINT数据类型也是一个结构,它在WINDEF.H中定义如下:
typedef struct tagPOINT
{
LONG x;
LONG y;
}
POINT, *PPOINT;
消息循环以GetMessage()调用开始,它从消息队列中取出一个消息:
GetMessage(&msg, NULL , 0, 0);
在这里我们有必要了解一下GetMessage()。它的原型是:
GetMessage(LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax);
GetMessage()检索消息队列,然后把消息放入lpMsg指向的MSG结构中。让我们来LOOK一下函数的四个参数。
lpmsg --> 指向MSG结构的指针
hWnd --> 接收消息的窗口句柄。通常设置为NULL,检索属于当前程序的窗口消息以及使用PostThredMessage()调用产生的消息。有关PostThredMessage()以后再说。
wMsgFilterMin --> 检索的最小消息值。一般情况下设置为0。
wMsgFilterMax --> 检索的最大消息值。如果wMsgFilterMin和wMsgFilterMax都设置为0,则检索所有消息。一般情况下设置为0。
返回值 BOOL型。检索到WM_QUIT消息时返回FALSE,否则为TRUE。应该继续消息循环直到GetMessage()返回FALSE以退出while循环,终止程序。
之前我们所讨论的都是准备性工作。注册窗口类,创建窗口,然后在屏幕上显示窗口,程序进入消息循环,从消息队列中取出一条消息,然后由DispatchMessage()将消息发送到相应的窗口过程中,WinMain()所做的只有这些。HelloWin实际的动作都是在它的窗口过程WndProc()中进行的。当用户改变了窗口的大小,或者移动了窗口,所有的这些消息都是由窗口过程来处理的。所以HelloWin的学习重点是它的窗口过程WndProc()。没有搞明白WndProc()等于没有学HelloWin。窗口过程函数确定了在窗口的客户区域中显示的内容,以及窗口怎样响应用户输入。
窗口过程说到底只是一个函数,所以我们可以任意给它取名字。在HelloWin中窗口过程的名字是WndProc()。一个Windows程序可以包含多个窗口过程,但是一个窗口类只能有一个窗口过程。
窗口过程总是定义为如下形式:
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
在定义窗口过程的时候除了函数名可以变之外其它的一定要原封不动。大家可以看到窗口过程的四个参数跟MSG结构的前四个域是一样的。DispatchMessage()从应用程序的消息循环中发送消息至相应的窗口过程。因为窗口过程是用来接收消息的,所以它的四个参数跟MSG结构的前四个域是一样的。
第一个参数是窗口句柄。这个句柄代表的是HelloWin的窗口。因为:
hwnd = CreateWindow( /* 此处省略…… */ );
CreateWindow()的返回值是新建窗口的句柄。上面的语句将新建窗口的句柄赋给了hwnd。所以hwnd代表的是HelloWin窗口。如果没有这个参数,鬼才知道窗口过程要处理的是哪个窗口的消息。前面已经提过,因为多个窗口使用同一个窗口过程,所以用第一个参数说明窗口过程要处理的是哪一个窗口发出的消息。HelloWin只有一个窗口对象。所以第一个参数只能是hwnd。
第二个参数message是消息的内容,比如WM_CREATE。WM_CREATE消息是CreateWindow()发出的。最后两个参数是32位的消息参数,它提供关于消息的更多信息。这些参数包含每个消息类型的详细信息。
一般来说,Windows程序员使用switch语句来处理窗口过程接收到的消息。窗口过程在处理完消息后必须返回0。窗口过程不予处理的其它所有的消息应该被传给DefWindowProc()。从DefWindowProc()返回的值必须由窗口过程返回。这句话可能有点不好理解。可以Look一下HelloWin的最后一个语句:
return DefWindowProc(hwnd, message, wParam, lParam);
现在明白了吗?好好想一想。这个应该很简单。
在HelloWin中WndProc()只选择处理三种消息:WM_CREATE, WM_PAINT, WM_DESTROY。窗口过程的结构如下:
switch(message)
{
case WM_CREATE:
[处理 WM_CREATE 消息]
return 0;
case WM_PAINT:
[处理 WM_PAINT 消息]
return 0;
case WM_DESTROY:
[处理 WM_DESTROY 消息]
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
调用DefWindowProc()来为窗口过程不予处理的所有消息提供默认处理,也就是说除了上述三种消息之外的所有消息都是由DefWindowProc()来处理。这是很重要的。
case WM_CREATE: //播放一个声音文件
PlaySound(TEXT ("HelloWin.wav"), NULL, SND_FILENAME | SND_ASYNC) ;
return 0 ;
窗口过程收到的第一个消息----也是WndProc()选择处理的第一个消息是----WM_CREATE。当Windows在WinMain()中处理CreateWindow()时WndProc()接收这个消息。更准确地讲,这个WM_CREATE是由CreateWindow()发出的。
当WM_CREATE产生后Windows调用WndProc(),将WndProc()的第一个参数设置为HelloWin窗口对象的窗口句柄。第二个参数设置为WM_CREATE消息。WndProc()处理完WM_CREATE之后将控制权返回给Windows。然后Windows继续执行WinMain()中CreateWindow()后面的语句。
在HelloWin程序中窗口过程收到WM_CREATE消息后播放HelloWin.wav。使用的是PlaySound()。从函数的名字上我们就大概可以看出它是用来播放声音文件的。
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 ;
WndProc()处理的第二个消息是WM_PAINT消息。这个消息在Windows程序设计中很重要。当窗口客户区域的部分或者全部变为“无效”,以致于必须“刷新”时,将由这个消息通知程序。
这里又出现了新名词---“无效”。客户区域怎么会变得无效呢?比如说,假设HelloWin窗口的坐标原来是(300, 400)。用户使用鼠标拖动了窗口,使窗口的坐标变成了(301,400)。既然窗口的坐标已经发生了变化,那么显示器上也必须重画这个窗口。我已经说过,屏幕上的图形界面都是画出来的,是位图文件(跟BMP一样)。新画出来的窗口坐标是(301,400)。用户又使用鼠标改变了窗口的位置,使得坐标变成了(302,400),这个时候显示器又得重画这个窗口,新画的窗口坐标当然是(302,400)。这一切都是一瞬间的事情。
当原来在显示器上显示的内容已经过时了,需要重画时,这个时候就说窗口的客户区域变得“无效”了。Windows必须重新在显示器上画窗口,以符合新的位置。
在最初创建窗口的时候整个客户区都是无效的。因为程序在显示器上没有画任何东东。当程序运行到UpdateWindow()时这个函数发出第一个WM_PAINT消息。
对WM_PAINT消息的处理几乎总是从一个BeginPaint()调用开始:
hdc = BeginPaint(hwnd, &ps);
以一个EndPaint()调用结束:
EndPaint (hwnd, &ps);
在BeginPaint()调用中如果客户区域背景还没有被删除,Windows会负责删除。它使用注册窗口类的wndclass.hbrBackground域中指定的刷子来删除背景。在HelloWin中这是一个白色的备用刷子。所以Windows将把窗口背景设置为白色。BeginPaint()调用使得整个客户区有效,并返回一个“设备描述表句柄”。设备描述表句柄是指物理输出设备。比如显示器或者是打印机。在窗口的客户区域显示文本和图形需要设备描述表句柄。不能用BeginPaint()返回的设备描述表句柄在客户区之外绘图。想要在客户区外绘图需要其它函数返回的设备描述表句柄。
EndPaint()释放设备描述表句柄,使之该设备描述表句柄不再有效。
在用户改变HelloWin的尺寸后客户区变得无效。读者应该还记得在wndclass.style域设置为标志CS_HREDRAW和CS_VREDRAW,这一风格指示Windows当窗口尺寸发生变化后使得整个窗口无效。然后窗口过程接收到一个WM_PAINT消息。
在移动窗口使几个窗口互相重叠时Windows不保存一个窗口中被另一个窗口所遮盖的部分。当原先被遮盖的部分又露出来以后它就被标志为无效,窗口过程接收到一个WM_PAINT消息,以刷新窗口的内容。
调用完BeginPaint()之后WndProc()接着调用GetClientRect():
GetClientRect(hwnd, &rect); //获取窗口客户区的尺寸
函数的功能是获取窗口客户区的尺寸。第一个参数是程序窗口的句柄,第二个参数是一个指针,指向一个RECT类型的rect结构。该结构有四个LONG域,分别为left、top、right、bottom。这四个域用来保存窗口客户区的尺寸。left和top域通常设置为0,right和bottom域设置为客户区域的宽度和高度(单位是像素点数)。
DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER) ; //显示文本串
该函数在指定的矩形里写入格式化文本,根据指定的方法对文本格式。
第一个参数是设备描述表句柄。第二个参数就是显示在客户区中央的文本串。第三个参数是 -1,表示在客户区显示的文本串是以'/0'结束的。
第四个参数客户区的尺寸和位置。表示在这个矩形内显示格式化文本。
第五个参数:
DT_SINGLELINE 表示文本串必须显示在同一行。即使是插入了回车和换行符都不能折行。
DT_CENTER 表示文本串在客户区水平居中。
DT_VCENTER 表示文本串在客户区垂直居中。当使用这个标志时必须同时指定DT_SINGLELINE。
一旦客户区域变得无效,WndProc()就接收一个新的WM_PAINT消息。WndProc()通过调用GetClientRect()获取变化后的窗口尺寸,并使用DrawText()在窗口中央显示文本。
WM_DESTROY消息是另一个重要消息。这一个消息指示Windows正在根据用户输入的命令清除窗口。该消息是用户单击"关闭"按钮或者在程序的系统菜单上选择"关闭"时发生的。
HelloWin通过调用PostQuitMessage()以标准方式响应WM_DESTROY消息。
PostQuitMessage(0);
该函数在程序的消息队列中插入一个WM_QUIT消息。前面提到过GetMessage()对于除了WM_QUIT之外的从消息队列中取出的所有消息都返回非0值。而当GetMessage()取到一个WM_QUIT消息时它返回0。这将导致WinMain()退出消息循环,并终止程序。然后程序执行下面的语句:
return msg.wParam;
我不知道这个时候返回msg.wParam的意义,以后会明白的。总之整个程序就这样结束了。终于可放松一下了,接下来的内容是对第三章的总结。
即使有了对HelloWin的说明,读者可能仍然对程序的结构和原理觉得神秘。在为传统环境编写简单的C程序时整个程序可能包含在main()中,而在HelloWin中WinMain()只包含了注册窗口类,创建窗口,从消息队列中取出消息和发送消息所必需的代码。
程序的所有实际动作均在窗口过程中发生。在HelloWin中这些动作不多。WndProc()只是简单地播放了一个声音文件,并在窗口中显示一个文本串。
在后面的章节中读者将发现Windows程序所做的一切都是响应发送给窗口过程的消息。这是概念上的主要难点之一,在开始编写Windows程序之前必须先搞清楚。
程序员已经习惯了使用操作系统调用的思路。例如C程序员使用fopen()打开文件。fopen()最终通过调用操作系统来打开文件,这毫无疑问。
但是Windows不同,尽管Windows有1000多个函数供程序调用,但是Windows也调用用户程序。比如前面定义的窗口过程WndProc()。窗口过程与一个窗口类相关联,窗口类是程序调用RegisterClass()注册的。基于该窗口类创建的所有窗口使用窗口类指定的窗口过程处理所有消息。
在第一次创建窗口时Windows调用WndProc(),窗口被消除时Windows调用WndProc(),窗口改变尺寸、移动或者变成图标时、从菜单选择某一项、滚动滚动条、按下鼠标或者从键盘输入字符时、以及窗口客户区域必须被刷新时Windows都要调用WndProc()。
所有这些WndProc()调用都以消息形式进行。在大多数Windows程序中程序的主要部分都用来处理消息。Windows发送给窗口过程的消息通常都以 WM 打头的名字标识,并且都在WINUSER.H头文件中定义。
Windows程序有一个消息循环。它使用GetMessage从消息队列中取出消息,并且调用DispatchMessage()将消息发送给窗口过程。
Windows程序是按照顺序将消息发送给窗口过程的?还是直接从程序外面接收消息的?实际上这两种情况都存在。消息被分为“进队的”和“不进队的”。进队的消息是Windows放入消息队列中的。在程序的消息队列中排队的消息被GetMessage()依次取出并传给窗口过程。不进队的消息在Windows调用窗口时直接发送给窗口过程。也就是说“进队”的消息被发送给消息队列,“不进队”的消息发送给窗口过程。任何情况下窗口过程都将获得窗口所有的消息--包括进队的和不进队的。窗口过程是窗口的“消息中心”。
进队消息基本上是用户输入的结果,击键(如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消息。
这一过程很复杂,非常Lucky的是其中大部分是由Windows解决的,不关程序员的事情。当我看到第四章的内容时才发现第三章简直就是小儿科。第三章的HelloWin只是准备运动。