版权所有:www.d2school.com 作者:南郁

第一篇 看透 Windows

消息的世界() 什么是消息

3.1 向“画图”程序发消息

3.2 在VCL里处理自定义消息

  3.2.1 自定义消息

  3.2.2 准备两个窗口

  3.2.3 消息发送

  3.2.4 消息接收方的处理

  3.2.5 让Form2和Form1一起显示

  3.2.6 运行

3.3 小结

 

什么是消息?真是无法解释。如果问什么是“短消息”,那倒是好回答一点:就是你在手机上七按八摁,最后按“发送”键以后,能导致中国移动或联通扣掉你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代码,那么现在是你回头再重新看一遍的时候了。

下一章见。