第一篇 看透 Windows“看透Windows”前言
终于开始Windows编程了。
我们也无可避免地,开始和Windows打交道。记得在《白话 C++》里,我们试图用“程序员”的眼光来重新认识什么叫电脑,认为电脑中的一切都是0和1。现在,更艰巨的任务摆在我们面前。本课程前些章节的内容就是:看透Windows。 (不要和我抬杠,说Windows那么复杂,你怎么能看得透,你明白,我们的任务主要是尽量从程序的角度来看Windows这个操作系统。)
从 Windows 3.1 到 Windows 95、98、Me、及今天的 Windows XP,其间还有Windows NT,Windows 2000。Windows这一操作系统,早已经遍布世界上各个角落上的电脑。所以你应该对Windows并不陌生。
但是你以前不是程序员,或者说不是“Windows 程序员”。所以,你看到的Windows,一定和我看到的,不太一样。
当我把自己的身份切换“最终的电脑用户”时,我也和许多人一样,骂过Windows,骂它体积太大,速度太慢,容易死机,很不安全。 而当我意识到自己是一个程序员时,我就闭嘴了。
我可以保证,学习所有的“看透Windows”,你就是一个标准的“Windows"程序员——因为你立可就可以写出多个真正的Windows程序。并且这些程序个个直达 Windows世界的要点。到时,你也会我一样,明白“Windows 不是一日可以写成的”。从而对Windows不再有抱怨。 当然,我们的身份是双重的,你随时可以让把自己的身份强行切换到“最终用户”,然后结合你所学的程序知识,在Windows身上出点气。而且,其实我也很乐意教这些的:D 你看看这张图,一会儿我们就要写这么一个程序:
(图1:在Windows桌面上打个大叉) 想否认Windows的价值?最直观而又无恶意的作法之一,就是在它的桌面上打个红叉吧。
言归正传,我们要看透Windows什么?首要任务是:
1、GUI (Graphic User Interface 图形用户界面)
在Windows 之前,我们用的是DOS操作系统。在学习《白话 C++》时,其实我一直在和那个黑乎乎的窗口打交道。看一眼现在WindowsXP华丽的外表,再看一眼那个DOS窗口吧。Windows之所以成为Windows,最根基的一点就是它为用户提供了一个图形的世界。你现在所看到的一切,开始按钮,桌面上那个“我的电脑”的图标,还有一切文字,图形,它们都是画出来的。Windows一代比一代画得好,不过是为了让我看得舒服点。 Windows是一个GUI操作系统。
2、API (Application Programming Interface 应用编程接口)
GUI 是给普通电脑用户看的。而这个API则仅仅给Windows的程序员准备。通过图形界面,最终用户可以通过手抓着鼠标或按键盘,来操作,控制Windows,比如点“开始”按钮,然后选择关机菜单项来关闭电脑。但如果要通过程序来实现关机该怎么办?那就需要API了。API就是程序才能“看”到“界面(Interface)”。另外的如用户可以拖一个文件到“回收站”,程序也可以“拖动”一个文件到“回收站”。只是前者通过GUI,而后者通过API。再有,你有没遇到过IE的默认主页及窗口标题被人家改了的臭事?那些经验文章教我们如何通过注册表编辑器恢复,并再三强调说:要小心啊,千万不要误操作……,对一个程序员来说,手动改注册表的讨厌之处不再于它的危险,而在于它的麻烦,累眼睛啊!好吧,程序来做,程序如何做?用API。 程序员通过API来控制Windows。
3、 Messages Loop (消息循环)
GUI 和 API 看起来是如此的不同。似乎一个是走阳光道一个走独木桥。虽然不是全部,但在很多地方,二者其实走得很近。在Windows深处的一条繁忙的路上,来自GUI的“人物”和来自“API”的人物可能就一前一后地走着!那就这里所说“消息循环”。这里的“循环”并不是说某个消息一定就要“绕一个圈”;而是说千千万万个消息不停息地走在Windows建立的“消息通道”上,来自不同的地方,最终又走向各自的归宿…… 如果按“来自”哪里分,最粗的分法就是:消息可以来自程序,也可以来自用户。 你理解“消息”吗?《白话 C++》的第三章曾经讲到,鼠标处理的是人手移动的信息。这些信息最后打包成一个“消息”,发送Windows,告诉Windows:“那个用户双击了‘我的电脑’”。 鼠标和键盘之类的外设所产生的消息不过是Windows消息中很小的一部分。更多的消息是留给程序员用的,留给程序用的。想不想做这么一个调皮的程序:想像你的同事上班时双击桌面的“我的电脑”,“我的电脑”窗口刚一闪就自行关闭!他双击!双击!!再双击!!!就是打不开“他的电脑”。最后一定会向你要杀毒盘的。赶在愚人节之前“看透消息”吧。
在开始本部课程之前,我假设你已经学好C++。如果没有,你应该先学习《白话 C++》。另外,你应该一点点的英语基础,这有助于从程序代码中诸名变量的名字这一最基本的水平上理解代码的功能。
第一章 GUI的世界(一)一切都是画出来的1.1 “一切都是画出来的”"Windows",中文翻译为“窗口”。为什么叫“窗口”?可能你知道,像下面的界面,微软公司称它为一个“窗口”:
(图2 :一个"Windows")
上面是我双击“我的电脑”,出现的的窗口。不仅仅是Windows自带的,诸如“资源管理器”,或“IE”等应用程序的带有“窗口”,所有在Windows上跑的程序,除非它确实不需要任何可操作的界面,否则,它就至少必须一个窗口(称为主窗口)。比如我们常见的QQ:
(图3: 您的 QQ 可能很漂亮,但也是窗口)
不仅仅这种可变大小的窗口称为窗口,一些对话框大小固定,但在程序上也叫窗口。不仅仅大大的,有内容的窗口是窗口,一个小小的按钮,在程序上也称为“窗口”。
这些窗口哪来的?这些窗口是由Windows画出来的。我们举一个小小按钮,来看看。为了看得更清楚,我把按钮所在的网页位置背景设置为红色。 (图4: 一个开始按钮也是一个窗口)
按钮具有3维效果。其实就是画出来的。一个长方形,左、上边画各画一条白边线;而右下则最外层是一条黑色线,内层则是灰色线。而按钮的表面则是银色。这样就构成一种立体效果(如果你不信,可以在画图程序里试试)。
窗口是画出来的。单就为了这么一个小小按钮的三维效果,就得至少画上6条线。一个很复杂的窗口呢?更可怕的是,窗口里的所有画像,文字,线条,就算是填充成一片空白,统统是画画的效果。
有些人可能要放弃了:学习编程,不仅要学逻辑,要英文,没想到还要会画画?
事情当然没有这么糟!事实让,这一切,Windows操作系统都会帮我们来画(但在DOS年代,就得由程序员自己画了)。不过,如果统统交给操作系统,那么写程序似乎也就没有什么意思了?所以操作系统在不顾劳累地画啊画的同时,仍然提供了接口来让程序员在认为合适的时候,合适的地方,画上合适的东东。
1.2 第一个程序:把Windows打个大红叉!
当然“合适不合适”,Windows并不太管。也管不了。所以,我们的第一个程序就是:在Windows桌面上画一个大叉。如果让比尔盖咨看到了,他可能会认为我们做的真的有那么一点点不合适。
怎么在屏幕上出现那个大红叉呢?我刚问到这里,一个女学员说:“其实也不是太难”。她说对了!这就是C++ Builder的功劳。本来,一切都要通过前面所说的API来打交道。但CB对相关的这些API做了很好的封装,使得我们能以极其自然(面向对象)的,简短的程序代码来实现。(我说完这些,把赞许的目光投向那位女学员,却发现她已经拿出口红,在屏幕上迅速地打了一个叉)。
运行 C++ Builder 6,菜单:File | New | Appication。 CB6为我们新建一个空白的Windows程序工程(Project)。并且自动生成了一个表单(Form)。我们知道,当程序运行之后,这个表单就被称为窗口(Window)。(如果你对这些及后面一些CB相关操作不熟,请复习《白话C++》的第二章)。
把这个表单拉小点,然后,从控件栏的“Standard”页中选 Button (控钮)
现在效果应如下:
双击放在表单上的按钮。出现代码编辑窗口。现在的代码中应有这些内容:
void __fastcall TForm1::Button1Click(TObject *Sender) {
} 在上面的一对 { } 之间填写以下内容:(行号不要键入!)
//先得到屏幕的长宽: 1) int ScreenWidth = Screen->Width; 2) int ScreenHeight = Screen->Height;
3) HDC hDC = ::GetDC(0);
//得到“画布(Canvas)” 4) TCanvas* ScreenCanvas = new TCanvas; 5) ScreenCanvas->Handle = hDC;
//设置画布的画笔(Pen) 6) ScreenCanvas->Pen->Color = clRed; //画笔的颜色为红色(Red) 7) ScreenCanvas->Pen->Width = 10; //画笔的粗细
//开始画叉啦,需要画两笔,这是第一笔:左上到右下 8) ScreenCanvas->MoveTo(0,0); //把画笔移到(Move to)坐标0,0处,即屏幕最左上角 9) ScreenCanvas->LineTo(ScreenWidth,ScreenHeight); //从当前位置画一条线(Line to)到屏幕的右下角。
//第二笔:右上到左下: 10) ScreenCanvas->MoveTo(ScreenWidth,0); 11) ScreenCanvas->LineTo(0,ScreenHeight); //释放“画布”: 12) ScreenCanvas->Handle = 0; 13) delete ScreenCanvas; 14) ::ReleaseDC(0,hDC);
保存工程。 编译: Ctrl + F9。如果报错,说明你代码有误。 运行:F9。窗口出现了。上面有一个按钮。如果你的CB6正在显示,把它最小化以露出Windows的桌面。然后,点按钮。本章最前头的图片效果就应该出来了。
不知道你在想什么?但我想通过这个程序来告诉你:你每天都在面对的Windows,都在使用的Windows,它的一切都是画出来的。普通用户没有“权力”去参与这件工作。但程序员有权参与——仅管我们看上去是在搞破坏。
代码解释:
我们并不想在"看透"的章节里,就讲太多的技术细节与实现原理。但幸好上面的代码都很自然。如果你不懂什么叫“类(class)”,那么需要继续学习《白话 C++》。
上面用到了两个类:TScreen 和 TCanvas。
Screen 变量 CB 的程序会在程序运行时,就自动产生一个 Screen 变量,它的数据类型为 TScreen 。如其名,它是封装了“屏幕”的一些信息和功能。我们这里仅仅想得知屏幕的分辨率是多少?我的电脑是(1024 * 768),别的电脑可能也是,或者是800 * 600,最惨就是 640 * 480了。(注 1) 得到屏幕长宽后(第1、2行),我们也就没有在使用Screen了。Screen由CB自动生成,是一个全局变量,它的释放工作不归我们管。
ScreenCanvas 变量 TCanvas 类,它封装有关Windows绘画操作的绝大部分数据和功能。所以虽然它的名字为“画布”,但其实包含了“画笔(Pen)、画刷(Brush)”等等。
在CB中,窗口会自带一个Canvas变量,但可惜我们现在是要在屏幕上的任何地方画。(第一道线就从从屏幕的左上角一直穿过整个桌面,直到落上右下角的时间区)。这是一种很霸道的行为。CB当然不会为我们这种行为事先准备一个这样的“画布”。所以我们需要生成一个Canvas变量,我命名它为:ScreenCanvas (屏幕画布)。这是第4行做的事:生成一个画布变量。
虽然取名为“屏幕画布(ScreenCanvas)”。但事实上新生成的画布都是“空画布”。因为程序这里还不知道你到底想画到哪里。 我们想画到整个屏幕上(事实上,如果你有其它程序正在屏幕上显示着,则会画到那个程序的界面)。这就需要向Windows提出申请,请它把整个电脑屏幕的“画布”都交给我们。
Windows 是用C和汇编写的,二者都不是面向对象的语言。Windwos大多数数据和功能本身也没做面向对象的封装(后来的COM对象开始有了面向对象的特征,这是后话)。在Windows里,类型画布的东西,称为DC。
第三行:HDC hDC = ::GetDC(0); 得到了屏幕DC。 参数 0 正是指明要的是整个屏幕的画布。 第五行:ScreenCanvas->Handle = hDC; 通过ScreenCanvas 的 Handle属性,得到了整个屏幕的画布。 (这些解释,你可能觉得还有不少地方没讲到,但没关系。)
其它的,就是画线的过程。代码中已有详细的注释。 最后12,13,14三行,用于返还、释放屏幕画布。
这就是我们的第一个例子。在屏幕上打一个叉。下一章我们仍然是要画画。不过我们将会规矩一点,只在合适地方,在合适的时间,画上合适的内容。
猜一猜,我们会画什么内容?是一个在编程界流传很广,也很古董的内容。当初学习《白话 C++》时,我们就做过了!——此言一出,台下一片失望之色。
|
注一: 可以这样查看你的电脑屏幕分辨率:在Windows桌面点右键,出现的菜单中选“属性”,然后选择到“设置”页。应该就能找到分辨率,如果没有,可以找一个“高级”按钮,点击后在出现的各页里查找。 |
第二章 GUI的世界(二) 在何时何处画
上一章我们讲到,在Windows的世界里,我们所看到的一切,都是“画”出来的。并且,我们大胆地在Windows的桌面画了一个红叉。画红叉似乎很爽,不过那个程序更像是一个恶作剧。只是用来证明我们可以画。这一章,我们将要学习如何“正经地”画。
每一个“正经地”的Windows应用程序,往往都是相似的,它们只在合适的时机,合适的位置上画需要的东西。我们首先来看看合适的位置是哪里。
2.1 在自己的窗口上画
我们说过,在Windows世界里,有无数的窗口(请复习第一章).现在,让我们也来制造一个窗口——通过CB,这是一件再容易不过的事了。
2.1.1 准备工作:创建一个窗口
同时在学习《白话 C++》的学员注意了,我们在《白话 C++》里,经常创建“控制台工程”,就是那个“黑黑的”的窗口。但在《白话 Windows编程》,我们更多的是以如下步骤创建一个正常的Windows应用程序工程。提醒您不要顺手就来一个控制台工程,否则,你会永远找不到“窗口”。
运行 C++ Builder 6,菜单:File | New | Appication。 CB6为我们新建一个空白的Windows程序工程(Project)。并且自动生成了一个表单(Form)。我们知道,当程序运行之后,这个表单就被称为窗口(Window)。(如果你对这些及后面一些CB相关操作不熟,请复习《白话C++》的第二章)。
(对于CB5,在菜单File里,可以直接到的New Application。)
这个工程很重要,马上保存工程!按一下Ctrl + Shift + S 。建议在磁盘上建立一个专门的文件夹来保存这个工程。比如,建立:bhwin/ls2/prj1/ 这样层次的文件夹。至于CPP文件名和工程名字,我都只是取默认的Unit1.cpp和Project1.bpr。
2.1.2 观察一个窗口的蛛丝马迹!
2.1.2.1 第一个问题
没错,这一小节里,我们将像一个神探一样去观察一个窗口,并且提出一个个问题。要撕开一匹布,最难的在于撕开最开始的一个口子,下面的一个个问题,就是我们撕裂Windows面纱的第一道口子。希望你能有一付好眼力。
保存之后,我们还是什么代码也不必写,直接按F9,编译,运行。在我的电脑上,显示了一个窗口。就是设计时的那个表单Form1。为了不占用太大版面,我把窗口拉得很小。
(图一:一个“光秃秃”的窗口)
这是一个光秃秃的窗口……这真的是一个“光秃秃”的窗口吗?仔细看这个窗口,你能发现什么?有个同学特深沉,他说看出这窗口上隐含着一幅3维立体图,并且他建议大家最好用“斗鸡眼”的方式来观察………拷!真是太夸张了,有这样的学生,我觉得大家不要叫我老师,还是叫我“大师”吧。
事实上,这个窗口并非空无一物,它有一个标题栏,标题是:Form1,在右方,还有三个小按钮,分别表示“最小化”、“最大化”、“关闭”……听到这里,你们千万不要准备退学啊!虽然这些你们早就知道,可是我们的第一个问题也出来了:
你有没有想过,标题栏,及窗口边框等,它们也是画出来的!可是,它们是由谁画出来的呢?
第一个问题:窗口默认的图形元素,是谁在为我们画的呢?
2.1.2.2 第二个问题
退出 Form1,我们在该表单上放一个Button(按钮)。
首先,在CB的“Standard”页(也就是第一页),找到按钮控件(TButton):
(Standard页上 TButton 控件)
鼠标选中图示中有个“OK”的按钮,然后在当前表单上放置一个按钮,尽量放在表单的中间。
(在表单上放置一个按钮)
双击Button1,出现代码编辑窗口,输入以下代码(黑体部分):
void __fastcall TForm1::Button1Click(TObject *Sender)
{
//设置当前客户区的画笔(Pen)
Canvas->Pen->Color = clRed; //画笔的颜色为红色(Red)
Canvas->Pen->Width = 10; //画笔的粗细
//开始画叉啦,需要画两笔,
//这是第一笔:左上到右下
Canvas->MoveTo(0,0); //把画笔移到(Move to)坐标0,0处,即 客户区的原点
Canvas->LineTo(ClientWidth,ClientHeight); //从当前位置画一条线到客户区的右下角。
//第二笔:右上到左下:
Canvas->MoveTo(ClientWidth,0);
Canvas->LineTo(0,ClientHeight);
}
//---------------------------------------------------------------------------
同样是画一个叉,和第一章在屏幕上相比,这里的代码省去了不少。这是因为当我们在一个自己生成的窗口上作画时,很多东西都由CB默认提供了。比如:“画布(Canvas)”,还有当前客户区的长宽:ClientWidth,ClientHeight。(Client即客户的意思)。MoveTo是“移动到…”,LineTo是“画线到…”这两个函数在上一章我们用过了。
(学员会问了:老师你怎么知道变量Canvas,ClientWidth,ClientHeight;又怎么知道函数MoveTo,LineTo?嗯,这些东东你们很快也会知道它们的出处,但现在不是解释这些的时候。白话 C++的课程不是到了结构了吗?很快后面要学习“类”的知识,只有在学习了“类”的基础上,我们才可能去解释这一切。)
言归正传,我们保存工程,按F9,编译,执行。然后按一下Button1,结果如下:
(图二:在客户区上画叉)
按完Button1后,千万什么也不要动,不过我猜想,你很可能已经动了什么了,没关系,再按一次Button1就是,保证你看到的图和上面的图一个样。
然后我们还是来仔细观察这个窗口!看到什么了吗?红叉“经过”了按钮,可是竟然没有“画过”按钮的表面。这是为什么?
再者,我们是从窗口的坐标原点(0,0)起笔,你注意了吗?这个坐标原点,是在窗口的左上角,而不是我们中学学笛卡尔坐标系的左下角。另外,这个绘图区域,并不位于标题栏之内,而是紧挨在标题栏之下。这就说明了,在默认情况下,我们是无法在标题上画画的。
第二个问题:窗口在绘画时,Windows是如何决定它要在哪里画,不在哪里画?
2.1.2.3 第三个问题
继续我们的福尔摩斯之旅!
再按一次Button1,保证那个红叉的完好。然后,我来拖一个别的窗口(比如你聊天用的QQ窗口什么的),小心地“擦过”带叉窗口的右下角……看:
(我拖着WinAMP的窗口,“擦过”Form1)
擦过之后:
(擦过之后,红叉短了一腿)
我们发现,另外一个窗口,居然像个“橡皮擦”一样,擦去了我在Form1这个窗口上的画的红叉的一部分。
再做一个试验,还是先按一个Button1画出红叉,然后小心翼翼地用鼠标在窗口边缘拉大窗口,你会发现此时红叉不受损伤。但是如果我们把窗口先缩小,然后再拉回原来大小,这时会发现,红叉又有一部分消失了。
第三个问题: 在窗口上画的内容,为什么会被“擦除”?
2.2 在需要的时候画
所有三个问题,我都没准备回答。当你学完课程,这些问题必将不再是问题。
如果程序还在运行着,关闭它,回到CB。
在Form1表单上,选中那个Button1,然后删除它(没错,我们不要它了)。
鼠标单击一下Form1,用以确保选中Form1本身,然后在控件属性窗口,选Event(事件)页;最后找到OnPaint这一行,如图:
(找到OnPaint事件)
在OnPaint右边的编辑框内,双击鼠标,或者按下Ctrl + 回车键。进入OnPaint事件响应函数的代码编辑处。
//--------------------------------------------------------------------------- void __fastcall TForm1::FormPaint(TObject *Sender) {
} //--------------------------------------------------------------------------- |
(Form1 的 OnPaint 事件响应函数)
(小心,这时候,你千万不要——没错,是不要——存盘,不然,CB会因为这个事件响应函数是空的,而自动删除它。)
然后,我们把前面Button1的OnClick函数里的代码,拷贝到FormPaint函数体内:
//--------------------------------------------------------------------------- void __fastcall TForm1::FormPaint(TObject *Sender) { //设置当前客户区的画笔(Pen) Canvas->Pen->Color = clRed; //画笔的颜色为红色(Red) Canvas->Pen->Width = 10; //画笔的粗细
//开始画叉啦,需要画两笔, //这是第一笔:左上到右下 Canvas->MoveTo(0,0); //把画笔移到(Move to)坐标0,0处,即 客户区的原点 Canvas->LineTo(ClientWidth,ClientHeight); //从当前位置画一条线到客户区的右下角。
//第二笔:右上到左下: Canvas->MoveTo(ClientWidth,0); Canvas->LineTo(0,ClientHeight); } //--------------------------------------------------------------------------- |
(代码Copy自前面Button1OnClick)
这时再存盘。
确保你把代码都拷对了吗?如果你是网页上拷来的,那有时会夹带一个汉字的空格,这样就会编译不过去。不管如何,按下F9键,跑跑吧。
(又是一个红叉)
又是一个红叉,但这回,你再用别的窗口去擦除,你会发现这个红叉擦不掉了。
不过,当你调整该窗口的大小……发现什么?咦世界有些乱套:
(将窗口拉小再拉大,红叉乱掉了)
此时要想恢复红叉,最直接的方法是把该窗口最小化,再恢复。你试试,并找找其它方法。
或许还有很多疑问,但关于“红叉”,我们的课程完了,下面要做的,算是一个扩展。
2.3 Hello Windows
Canvas 不仅可以用来画画,也可以用来写字。严格地讲,在Windows这个一切都是画出来的世界里,写字何尝不是涂鸦?当然,Windows不会发神经到让我一笔一划“画”出字来。在Windows的安装目录下,有一个Fonts(字体)目录,里面放了很多字体。所谓的字体,就是事先建立如何画出某个字来的信息。有了字体,我们可以很轻松的在窗口上写字。
去掉前面FormPaint函数体内的代码,换成以下几句:
//--------------------------------------------------------------------------- void __fastcall TForm1::FormPaint(TObject *Sender) { Canvas->Font->Color = clGreen; //画布的字体的颜色=绿色 Canvas->Font->Size = 24; //画布的字体的大小=24 (你现在看的课程,字体大小是9)
Canvas->TextOut(10,10,"你好,Windows世界!"); } //--------------------------------------------------------------------------- |
(更改 FormPaint里的代码,让我们向Windows世界问个好。)
按F9后,运行结果如图:
(呵呵,Windows,老朋友了。)
(也许,有些人对Windows没什么感情?如果你愿意,你可以把这句话改为“安红,俺想你!”,我知道你做得到。)
这节课就到这里吧。记住,更精采,更实用的Windows编程世界,在后面等着我们。
第三章 消息的世界(一) 什么是消息
什么是消息?真是无法解释。如果问什么是“短消息”,那倒是好回答一点:就是你在手机上七按八摁,最后按“发送”键以后,能导致中国移动或联通扣掉你1毛或1.5毛不等的东西。或者,就是当 你从北京坐火车到福建,一路上手机收到的各省人民对你的路过所表达的,七七八八的致意:
“河北移动欢迎你!”、“欢迎来到江西省,江西移动随时为您服务!”……最后搞到我手机没电,结果没收到福建移动对我的欢迎。我在想会是什么呢?估计是一句:“嘿嘿,你总算到了!”。
当然,这些都是垃圾信息。删掉就是。不过,如果收到的是其它信息呢?我想起一个笑话。说是一个家伙在国庆长假时闲着,突发奇想要问候一下自己的老板,于是发过去短信一则:
“老板,节日快乐!”
很快,老板回短信了:
“谢谢,也祝你快乐。对了,今天公司没人值班,你能不能过去盯一盯?”
现在可以提“消息驱动”这个术语了。其实很简单不是吗?那家伙之所以在假日暴走街头,赶往公司,就是被那条老板回来的消息给“驱动”的。任何标准的Windows程序都要受各种各样的消息“驱动”,同时,也会发出各种各样的消息去驱动别的程序,或驱动自己。
下图示例了标准Windows程序最上层的逻辑——即一个消息接收与处理的循环。
(任何Windows程序,最上层都是一个消息循环)
解疑: 一些已经写过不少程序的程序员,可能会对“任何Windows程序都是一个消息循环”这种说法表示质疑:“我写了好多程序,可是什么消息也没发现啊?”。事实上,这是因为,当我们用VB,PB,JAVA,Delphi或我们在用的CB写Windows程序时,平常许多消息处理的过程,都被这些工具背后提供的代码封装起来,消息处理多数以一种称为“事件”的形式展现。但这并不等于我们就不需要去了解Windows更本质的内容,否则,我们的课题也不会叫做“看透”了。
理论上的东东总是不直观。来一个例子,相信大家会有点感觉。
3.1 向“画图”程序发消息
请按以下两个步骤做一遍:
第一步:鼠标点击任务栏上“开始”按钮,“所有程序”或“程序”-> “附件” -> “画图”。会看到下面这个Windows老朋友。自打有了Windows,就有这个程序。
(画图程序)
确保: 你所看到的“画图”程序的标题文字,必须如上图所示的“未命名 - 画图”。如果不是,请点击画图的主菜单“文件”,然后选“新建”。
第二步:打开C++ Builder。新建一个Windows应用程序工程。
2.1 在默认创建的TForm1里,放一个TButton。(如果不会,看看上一章)。
2.2 双击Button1,在编辑窗口里写入以下代码(粗体部分):
void __fastcall TForm1::Button1Click(TObject *Sender) { /*免费版本不提供*/ } |
(Button1 OnClick 事件代码)
解释:
a)NULL 是一个宏,即: #define NULL (void* 0)。 你可以理解它为:0,或才一个空的地址。(关于地址,请参见《白话C++》指针章节)。
b) HWND 也是一个类型定义别名,即:typedef void* HWND。即,它是一个无类型的指针。你可以理解它只是一个编号:用于给各个程序里的各个窗口编号。我们也习惯称这种编号为“句柄”。
辅助:本章辅助教材将提供如何查看这些宏或别名原始定义的方法。
b)FindWindow 是 Windows提供的API函数(见《白话 C++》函数章节)。该函数需要两个参数:要查找的窗口的“类名”和“标题”。“类名”我们暂不解释,这里我们也不按“类名”查找,所以我给了一个NULL。标题则为“未命名 - 画图”。请注意其间的2个空格。如果竟然是用非中文版的Windows。那只好自己去看标题是什么了。FindWindows的意思是“查找Window”。顾名思义,它会去找指定的窗口。我们这里要让它找到画图程序主窗口的编号(句柄)。
辅助:本章辅助教材将提供如何查找API函数说明的方法,同时教学员配置一个更加得心应手C++Builder集成环境。
c)FindWindows 有没有找到我们想要的“画图”程序的主窗口呢?我们用这句话来判断:
if (hWnd != NULL)
即,如果找到的编号不是一个0,就是找到了。
d) 找到以后做什么?这是这个程序的关键。我们将向找到的窗口发一个消息。SendMessage也是一个API函数。同样顾名思义:它的功能是“发送消息”。它需要4个参数:
第一个参数指明“向谁发消息”,即目标窗口,这里是我们找到的,画图程序的主窗口。
第二到四个参数,都用于表明“发送什么消息”。WM_CLOSE 也是一个宏。其实它肯定整数,可能是101,也可能是777?具体是多少,如果你用VB或PB编号,恐怕是要费一番力气去查一查。而像我们用C++,就不必了,因为Windows本身是用C写的,我们直接就用Windows提供的宏即可。不过这个宏是什么意思呢?在Windows,“消息”由三个整数组成:第一个整数是消息的编号,其余两个是消息附加的参数。“WM_CLOSE”表示让一个窗口关闭/Close的消息。这个消息不需要什么参数,所以剩下的两个参数,我们都填0,即:
SendMessage(hWnd, WM_CLOSE, 0,0) //表示:向hWnd这个窗口,发送一个WM_CLOSE消息,附带参数都为0。
e)想一想,我们的程序现在是不是有些像“中国移动”了?人家画图程序好好地呆在屏幕上,一动也不动,正休息呢,我们就不管不顾地发给它一个消息。更过份的是,中国移动好歹是发个“欢迎”消息,而我们发的呢,一会儿你就会明白,这是一个“滚蛋”消息。
一个程序,接收到另一程序强制发来的“滚蛋”消息。结果会如何呢?有两种:第一种是“逆来顺受”型,即默默地真的消失了。另一种是“坚决不理”型。
画图程序,这个Windows的老员工,就是一个典型的逆来顺受型。废话少说。保存工程,代码。按F9吧!确保刚才你的画图还在桌面上运行着。然后按我们的程序的Button1。你就会发现画图程序自动退出了。
尝试:在电脑桌面上双击“我的电脑”,打开“我的电脑”窗口。然后修改前面代码中的要找的窗口标题为“我的电脑”。重新编译,运行,点击Button1……结果是什么?曾经,我记得Windows95吧?这是可以的,那时的Windows的“我的电脑”窗口。也可以用这种方法关闭。而现在,我只听到“当”一声……(这“当”一声表示什么?表示“我的电脑”对我们试图通过发“短消息”来当掉它的做法表示不满吗?)
辅助:辅助教材将提供以同样的原理实现的“密码查看器”。我和很多网民一样,曾经在网上申请好多免费email。然后在机器上装一个Foxmail.登录这些信箱都需要密码,可我在Foxmail时一一设好以后,用上1个月,我就忘了这些密码是什么了。Foxmail密码一栏里显示的已经是“*******”了。怎么办?还是通过向Foxmail强行发一个“我要看你的文字”的消息,然后就看Foxmail是“逆来顺受”还是“坚决不理”了。
3.2 在VCL里处理自定义消息
未经商量,就向另一个程序(比如“画图”)发消息,怎么说也显得不礼貌。
下面我们自己往自己写的窗口发消息。
Borland C++ Builder 提供一套主要用于封门Windows各种窗口的形为的程序框架,称为VCL(可视化元件库)。当然也包括了处理各种Windows常见消息的方法。因此,多数情况下,我们并不用去操心如何处理那些所有应用程序都不得不处理的消息。事实上,这个处理过程是相当无趣的,因为千篇一律。不过,Windows还允许我们自定义消息,然后在程序中发送和处理这些消息。
有了更有趣,也为了更好的了解一个“消息”从生到死的完整过程,才有了这一节。
3.2.1 自定义消息
前面说了,消息需要三个无符号整数:消息的编号,消息的两个参数。
为了不和Windows预定义的消息重复,Windows规定我们自定义的消息必须从一个基数开始。这个基数,同样被定义为一个宏:WM_USER。
至于消息的两个参数,完全由我们来自定义了。事实上这只是一个“约定”,也就是说,你必须在消息的“发送者”和“接收者”之间约定好这两个参数的意义。想像一下,如果你发给你老婆一个号“7521”,你的本意是“妻我爱你”,多甜蜜啊……可是如果没有事先约定,谁敢保证你老婆不会理解为“欺我儿女”呢?所以保证这个约定不出错很重要。就算是像我们现在这样,集消息的“定义者”、“发送者”、“接受者”于一身,也得凡事小心。如何保证不出错呢?当然也是用宏定义了。将整数定义成一个个有字面意义的宏,是个比较好的办法。
由于Windows很多消息的宏,都是以“WM_”开头,为了表示区别,用户自定义消息的宏,通过以"CM_"开头。
先说说我们要做些什么?假设我们要发给窗口一个消息,窗口接到这个消息后,可以往“上、下、左、右”某个方向移动指定的距离。
//自定义消息编号,基于WM_USER,我们就从加1天始吧。
#define CM_MOVEWINDOW (WM_USER + 1)
接下来,我们做个约定:第一个参数,用于指明往哪个方向移动,为了不出错,我们定义一个枚举类型表示:
enum TMoveDir {mdLeft, mdBottom, mdRight, mdUp}; //左,下,右,上
第二个参数,用来表示要移动的距离。
也就是说,假如,某个窗口收到这样一条信息:
CM_MOVEWINDOW, mdLeft, 2
那么,这个窗口应该主动向左,移动2个像素。
约定好后,可以开始写程序了。让我们在CB里新建一个Windows应用工程(原来的工程记得保存)。
3.2.2 准备两个窗口
一、新工程将默认产生一个主表单,在表单上放一个标签控件:TLabel。名称为默认的Label1.
二、选择主菜单“File”->“New”->“Form”,让我们再建一个表单,新建的表单默认名为Form2,我们用它来发送消息。而原来的表单(主窗口),用来接收消息。
三、为了确保Form2能被自动创建,在完成第二步之后,请点击主菜单“Project”->“Options”,找到“Forms”,请确保Form1和Form2都出现左边的列表中。如果不是,通过界面上的“<”按钮移到左边。
(确保两个Form都在左边的列表)
确认退出第三步的对话框。
四、改变,Form1,Form2的位置,大小,并在Form2上放置四个按钮,注意它们在图上摆的位置与次序。最终如下图。
(左上是Form1, 右下是Form2)
3.2.3 消息发送
五、鼠标点一下Form2的标题,按F12,打开Unit2.cpp;再按Ctrl+F6,或者点击编辑框下面的,打开Unit2的头文件:“Unit2.h”。输入以下加粗代码,即我们前面的约定。
... #include <StdCtrls.hpp> #include <Forms.hpp> //---------------------------------------------------------------------------
/*免费版本不提供*/
class TForm2 : public TForm { ... |
(Unit2.h 文件内添加代码)
六、切换回Unit2.cpp,代码上部加入以下加粗的一行,即引用Unit1.h。因为我们需要Unit2.cpp里用到Form1,而Form1在Unit1.h里定义。
//--------------------------------------------------------------------------- #include <vcl.h> #pragma hdrstop
#include "Unit2.h" #include "Unit1.h" //加入这一行 //--------------------------------------------------------------------------- |
(在Unit2.cpp中,include Unit1.h)
七、在Form2上,双击Button1,在代码编辑窗口输入以下加粗部分代码。
void __fastcall TForm2::Button1Click(TObject *Sender) { //发送“向上移”的消息,要求向上移动10个像素位置: /*免费版本不提供*/ } |
(Button1 的 OnClick代码:发送向上移动10个象素的消息)
八、和第七步类似,分别单击Button2,Button3,Button4,完成以下加粗部分代码的输入:
void __fastcall TForm2::Button2Click(TObject *Sender) { //发送“向左移”的消息,要求向左移动10个像素位置: /*免费版本不提供*/ } //--------------------------------------------------------------------------- void __fastcall TForm2::Button3Click(TObject *Sender) { //发送“向右移”的消息,要求向右移动10个像素位置: /*免费版本不提供*/ } //--------------------------------------------------------------------------- void __fastcall TForm2::Button4Click(TObject *Sender) { //发送“向下移”的消息,要求向下移动10个像素位置: /*免费版本不提供*/ } //---------------------------------------------------------------------------- |
(完成剩余的三个方向)
现在我们完成了发送消息部分的代码。下面该处理接收消息了。
从上面的代码我们看到,消息是发送到“Form1->Handle”.Form1是新建工程时,默认就创建的那个表单,而Handle,是表单的窗口句柄。因此,消息的接受者,是Form1.
切换到Unit1.cpp,再按Ctrl+F6,切换到Unit1.h。
辅助:C++ Builder 集成环境的基本使用方法。
3.2.4 消息接收方的处理
九、在Unit1.h 里。加上对Unit2.h的引用:
#include <StdCtrls.hpp> #include <Forms.hpp> //--------------------------------------------------------------------------- #include <Unit2.h> //--------------------------------------------------------------------------- |
十、在VCL里,有专门的方法,来完成消息响应。请在Unit1.h里,点击鼠标右键,在弹出菜单中,选择“View Explorer”,或者直接按Ctrl + Shift + E。出现“类专家”窗口:
(类专家窗口,该窗口出现时,有可能是“嵌套”在代码窗口里)
十一、如上图,选中TForm1。然后点击鼠标右键。在弹出菜单中,选择“New Method...”。我们将为TForm1新建一个成员函数。
(New Method: 为指定类新建一个成员方法)
十二、在出现的对话框中,作出以下红圈标明的改动:
/*免费版本不提供*/
(Add Method :添加方法)
十三、点击OK退出后,CB会自在Unit1.cpp里,添加一个函数。请加入以下加粗部分的代码。
void TForm1::CmMoveWindow(TMessage& msg) { //TODO: Add your source code here if (msg.Msg != CM_MOVEWINDOW) return; //LParam 为消息第2个参数,约定要移动的距离: int len = msg.LParam; //WParam为消息的第1个参数,约定用于指示移动的方向: switch(msg.WParam) { case mdLeft : Left = Left - len; Label1->Caption = "向左移"; break; case mdRight : Left = Left + len; Label1->Caption = "向右移"; break; case mdUp : Top = Top - len; Label1->Caption = "向上移"; break; case mdBottom : Top = Top + len; Label1->Caption = "向下移"; break; } } |
(接收,处理消息的代码)
3.2.5 让Form2和Form1一起显示
十四、现在编译并运行程序,你会发现只看到Form1,但找不到Form2. 这是因为只有默认产生的主窗口,也就是Form1,才会自动显示。 所以,我们希望在Form1显示时,也自动显示Form2.请用鼠标单击一下Form1.然后按F11,找到控件属性窗口,并切换到Event页:
(找到Form1的 OnShow 事件)
如上图,找到OnShow,然后用鼠标双击其右的空白。
十五、在出现的代码编辑窗口中,加入下面加粗的一行代码:
void __fastcall TForm1::FormShow(TObject *Sender) { /*免费版本不提供*/ } //--------------------------------------------------------------------------- |
3.2.6 运行
十六、到现在还没有保存过代码吗?这可很危险。保存(Ctrl + Shift S)。然后按F9编译并运行。分别按Form2的四个按钮,看Form1的形为是否正确。下面是在我机器上运行的一个截图:
(最终运行截图)
看不去很长的一过程,我们完成了“定义、发送、处理”消息三大步骤。事实上,要完成同样的功能,完全可以不通过自定义消息来处理(辅助:本例相同功能的“非自定义消息”版)。但是通过消息处理,我们可以使程序在设计上变得更灵活,而本章的重点,正是在于建立“消息处理”的概念。
在本例中,按中Form2中的四个按钮,都将向Form1发出CM_MOVEWINDOW这个消息。记住:CM_MOVEWINDOW只是一个整数。程序并不会因为这个宏含有“Move Window”的文字,就自动将Form1上下左右的移!要想完成相关的处理,完全需要靠我们代码来工作。消息带有两个参数,这两个参数也是整数,它们的具体含意,如何使用,同样要依赖于实际代码来处理。
3.3 小结
本章只有两节。
第一节我们尝试向一个外部的程序:“画图”,发送一个Windows预定的消息:WM_CLOSE。“画图”程序的主窗口接收到该消息后,会做出预定义的动作:退出。但对于其它程序,可能会做出判断,发现该消息来自外部的程序,而不做处理。
第二节,我们在同一个程序内部的两个不同的窗口之间,发送自定义的消息。由此,我们了解到,原始的Windows消息由三部分组成:消息编号、及两个消息参数。
这就是本章的要点。如果你原来不知道Windows世界里有“消息”这一回事,现在你应该有所了解了。不过,如果你刚才在看课程时,只一个劲地Copy代码,那么现在是你回头再重新看一遍的时候了。
下一章见。