1.1 、Windows 的介绍
Windows 是一种基于图形界面的多任务操作系统。为这个环境开发的程序有着相同的外观和命令结构。对用户 来说,这使得学习使用Windows 应用程序变得容易了。为了帮助开发Windows 应用程序,Windows 提供了大量的内建 函数以方便地使用弹出菜单、滚动条、对话框、图标和其他一些友好的用户界面应该具有的特性。
Windows 运行应用程序以硬件无关的方式来处理视频显示、键盘、鼠标、打印机、串行口以及系统时钟。最值 得注意的Windows 特性就是其标准化的图形用户界面。统一的界面使用图片或图标来代表磁盘驱动器、文件、子目 录以及其它操作系统的命令和动作。统一的用户界面也为程序员带来了好处。例如,你可以很方便地使用常见菜单和对话框的内建函数。所有的菜单都具有相同风格的键盘和鼠标接口,因为是Windows 而不是程序员在实现它。
Windows 的多任务环境允许用户在同一时刻运行多个应用程序或同一个应用程序的多个实例。一个应用程序可 能处于激活状态。激活的应用程序是指它正接收用户的输入。因为每一个瞬间仅有一个程序能够被处理,因此同一时间也只能有一个应用程序处于激活状态。但是,可以有任意个数的并行运行的任务。
1.2 、Windows 的简要历史
Windows 最初由Microsoft 公司在1983 年11 月宣布,1985 年11 月推出了第一公开发行版本,即1.01 版。此后两年,1.01 版进行了几次修改以满足国际市场的需求,并增减了一些显示器和打印机的驱动程序。1987 年11 月发行的Windows 2.0 版在用户界面上做了些改进。例如:重叠式窗口的引入,还增强了键盘和鼠标接口,特别是增强了选单和对话框的功能...(略)
1.3 、用户界面的构件
1.3.1 窗口
窗口是屏幕上与一个应用程序相关的矩形区域,它是用户与产生该窗口的应用程序之间的可视界面。对应用程序来说,窗口是应用程序控制下的屏幕上的一个矩形区域,应用程序创建并控制窗口的所有方面。当用户启动一个应用程序时,一个窗口就被创建。每当用户操作窗口中的对象时,程序就有所响应。
1.3.2 边框
绝大多数窗口都有一个环绕着它的边框,边框不仅作为窗口的边界,它也用来指明窗口的状态,即窗口是否是一个活动窗口。当我们将鼠标指针放在边框上按下鼠标键并移动鼠标时,就可以改变窗口的大小。
1.3.3 标题栏
标题栏位于窗口的顶部,其中显示的文本信息用于标注程序,一般是应用程序的名字,这便于用户知道哪个应用程序正在运行。标题栏的颜色反映一个窗口是否是一个活动窗口。
1.3.4 控制框
控制框是每个窗口左上方的小图片,每个应用程序都使用它。在控制图标上单击鼠标键会使Windows 显示系统菜单。系统菜单它提供了诸如还原、移动、大小、最小化、最大化以及关闭这样的标准操作。
1.3.5 最小化图标
每个Windows 98 及Windows NT 应用程序都在窗口的右上角显示三个图标。最左边的图标是一段短下划线,这就是最小化图标。它可以使用程序被最小化。
1.3.6 最大化图标
最大化图标是三个图标中中间的那一个,看起来象两个小窗口。使用最大化图标可以使用应用程序占满整个屏幕。如果选择了这个图标,其它应用程序窗口都会被盖住。
1.3.7 垂直滚动条
如果有必要,应用程序可以显示一个垂直滚动条。垂直流动条显示在应用程序窗口的右边,在两端有两个方向相反的箭头。它还有一个着色的棒和一个透明的窗口块。后者被用于显示当前显示内容与整个文档的关系。你可以用滚动条来选择显示哪一页。一般在任何一个箭头上单击一下会使显示内容移动一行。单击向上箭头下方的窗口块并拖动它会使屏幕输出快速更新到应用程序屏幕输出的任意位置。
1.3.8 水平滚动条
也可以显示一个水平滚动条 。水平滚动条显示在窗口的底部,具有与垂直滚动条类似的功能。你用它来选择 要显示哪些列。一般在任何一个箭头上单击一个会使显示内容移动一列。单击向左箭头右边的窗口块并拖动它会使屏幕输出快速更新到应用程序屏幕输出的任意位置。
1.3.9 选单栏
一个可选择的菜单条可以显示在标题条的下方。通过菜单条来选择菜单和子菜单。这种选择可以通过用鼠标单击,也可以用热键组合来实现。热键组合经常是ALT 与命令中带下划线的字母的组合,比如File 命令中的 “F” 。
1.3.10 用户区
通常用户区占据了窗口最大的部分。这是应用程序的基本输出区域。应当由应用程序来复杂管理用户区。另外,应用程序可以输出到用户区。
1.4 、面向对象的思维方法
对于程序员来说,面向对象意味着非常熟悉的事物:将世界看成是一组彼此相关并相互通信的实体即对象组成,每个对象有一个名字来标识,这是人们通常看待世界的方式。例如,当看见一辆汽车时,所见到的是一辆汽车,而不是一大堆原子。人们可以将汽车分解为车轮、发动机、车门、油箱等,它们都是具体的实体即对象。
对象之间的通信被称为发送消息,即一个对象请求另一个对象执行某种方式的操作。例如,交叉路口的红灯“ 请求” 驾驶员停车,驾驶员在接受到消息之后,他所执行的动作是踏下制动踏板,这有向汽车发送了一条消息,汽车在接受到此消息之后,又将该消息分解之后发送到相关的对象上:制动器作用于车轮上,将动能转变成为势能,使车速降下来;尾灯又向它后面的其它车辆的驾驶员发送消息;各种仪表盘向驾驶员反馈出所发送的消息的动作结果。
从程序员角度而言,对象是内存中一块有名的存储单元。我们通常所谓的变量就是一种数据对象,但对象的概念比变量的含义更广义,通常将对象定义成为包含有数据和代码的内存区域,数据表征对象的特征,而代码用于响应消息,使对象进行某些动作。从屏幕上显示的一个可视的窗口对象为例,我们可以对比分析一下用户心目中的对象和程序员心目中的对象的关系。窗口对象的特征,例如颜色、长度、其中显示的信息等,在程序中被表示为数据,用户对窗口对所做的操作,例如移动窗口、改变窗口的大小等,使得用户向窗口发送了消息,这些消息引起了计算机(内存中的)对象执行相应的代码,代码执行的结果改变了对象中的数据,使对应的可以视对象 的位置和大小发生了变化。
对象为响应消息所执行的代码被称为方法,对象中保存的数据构成对象的属性,对象的抽象定义就是执行某些动作,否则,没有其它途径可以使一个对象动作起来。向一个对象发送消息在程序中表示为:
functionName(id, arg1, arg2, ...);
其中,消息是functionName ,id 是标识对象的一个对象名,或称其为对象的标识符,Windows 使用某种类型的 数据来作为对象的标识符,这个标识符号常被称为对象的句柄。arg1 等为消息所带的参数。
虽然发消息类似于标准的函数调用,但消息也有函数调用中没有的特性,例如,消息始终在执行一选择机制,其参数与其它消息区别开来,从而告诉该对象完成什么样的操作。一个函数名始终指向内存中该函数的代码所位于的确定地址,消息并不指向内存中的某地址,但却告诉接受消息的对象要引用的内存地址是什么。函数不仅说明操作,而且还要执行如何完成该操作的方法。消息只说明该操作,在对象中定义的方法说明如何完成该操作。当向不同的对象发送相同的消息时,所执行的方法是不同的。
在面向对象的程序设计中,每个对象由一个类来定义,类是对一组性质相同的对象的程序描述,它是由概括了一组对象共同性质的方法和数据组成。从一组对象中抽象出公共的方法与数据,将它们保存在一个类中是面向对象程序设计的核心。
在日常生活中,我们也以类这种方式来定义客观对象。通过对客观对象进行抽象,我们将性质相同的对象归为一类,形成概念,例如,人类、苹果类、食品类等。通过对客观对象分类,我们也可以更好地认识客观对象,例如,当知道张三是一个人时,不用对张三进行更多的描述,我们已知道张三作为一个人所具有的特征和行为,因为它们已经在“ 人” 类中进行了描述。
在面向对象的程序中,类被用作样板来生产具有相同行为方式的对象。类就像是生产对象的一个工厂,在生产对象时,对象具有类中所描述的同样的数据结构和方法,同时,对象的每个数据在创立之初取得一个初始值,形成对象的初始状态。对象通过发送消息相互作用,对象的状态从一种状态过渡到另一种状态,当所有的有关对象到达某种特定的状态时就得程序的运行结果。
使用类产生对象的过程也称为生成该类的一个实例。因此,对象也可以定义为对象是类的一个实例。定义类也意味着将该类的对象公用代码放在内存的公共区域中,而不必对每个对象都将它们的代码和数据重新进行一次描述,这减轻了程序员的劳动强度。我们可以将一些常用对象定义放在一个公用库中,而在程序中需要该类的一个对象时,就创建该类的一个实例。Windows 已为程序员预定义了许多像按钮、滚动杠和对话框等对象的类,当程序员需要这些类的对象时,仅需创立该类的实例即可。对于同一个类的不同对象,在建立对象时其初始状态不同,因而这些对象在屏幕上显示的位置、大小等属性也不相同,但同类的对象的操作是相同的(因为它们共用相同的方法)。这也就是为什么不同的Windows 应用程序对用户表现出一致的操作特性的原因之一。
1.5 、句柄
Windows 应用程序中存在许多对象,例如选单、窗口、图标、内存对象、位图、刷子、设备对象和程序实例等,在Windows 中,对象使用句柄进行标识,这样,通过使用一个句柄,应用程序可以访问一个对象。
在Windows 软件开发工具中,句柄被定义为一种新的数据类型。在应用程序中,对句柄的使用一般只有赋值(句柄可以被赋以初始值、被改变为用于标识同类对象中的另一个对象和被用作函数的参数)、与NULL 进行相等比较(判定一个句柄是否为一个有效的句柄)和与标识同类对象的另一个句柄进行相等比较(判定两个句柄是否标识同一个对象),没有其它的运算。虽然在有的书中介绍说句柄是一个十六位的整数,但实际情况并不这样简单,它的长度将会随着不同的计算机平台和Windows 的发展而有所变化,例如,在32 位Windows 中,句柄将是一个32 位的数据,并且不是整数类型。
一种通用句柄类型为HANDLE ,在Windows 3.1 以前的版本中,它可被用于标识所有种类的对象,在Windows 3.1 中,部分地保留了这一特点,在本教程的程序中也有所反映。在Windows 3.1 中,从HANDLE 类型又派生出了一些新的句柄数据类型,每种类型的句柄用于标识一种类型的对象,下面是一些常见的句柄类型:
类型 | 说明 |
HANDLE | 通用句柄类型 |
HWND | 标识一个窗口对象 |
HDC | 标识一个设备对象 |
HMENU | 标识一个选单对象 |
HICON | 标识一个图标对象 |
HCURSOR | 标识一个光标对象 |
HBRUSH | 标识一个刷子对象 |
HPEN | 标识一个笔对象 |
HFONT | 标识一个字体对象 |
HINSTANCE | 标识一个应用程序模块的一个实例 |
HLOCAL | 标识一个局部内存对象 |
HGLOBAL | 标识一个全局内存对象 |
1.6 、数据类型及常量
为便于开发Windows 应用程序,Windows 的开发者新定义了一些数据类型。这些数据类型或是与C/C++ 中已有的数据类型同义,或是一些新的结构数据类型。引入这些类型的主要目的是为便于程序员开发Windows 应用程序,同时也是为了增强程序的可读性;另一个目的是为了便于程序将来能被移植到其它种类的计算机平台上或适应Windows 将来的新版本的变化。例如,本教程目前使用16 位API (Application Program Interface ),现在Windows 的版本使用32 位API ,只要将HANDLE 等句柄类型定义为32 位长,然后重新编译程序,就可以很方便地将一个使用16 位API 的Windows 应用程序改为使用32 位API 的程序,使其能运行在32 位API Windows 上。大部分的数据类型在Windows.h 中定义,下面是在这个文件中定义的部分类型:
#define PASCAL pascal
#define NEAR near
#define FAR far
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)();
在Windows.h 中,使用typedef 还定义了一些新的结构类型。这些结构类型的名字也使用大写形式的标识符:
类型 | 说明 |
MSG | 消息结构 |
WNDCLASS | 窗口的类的结构 |
PAINTSTRUCT | 绘图结构 |
POINT | 点的坐标的结构 |
RECT | 矩形结构 |
我们在这里以类型MSG 为例来说明类型的定义方法,对于其它类型,在以后用到时再作详细地说明。类型MSG 是一个消息结构,它的定义方式及其各域的含义如下:
typedef struct tagMSG {
HWND hWnd; // 窗口对象的标识符,该条消息传递到它所标识的窗口上
UINT message; // 消息标识符,标识某个特定的消息
WPARAM wParam; // 随同消息传递的16 位参数
LPARAM lParam; // 随同消息传递的32 位参数
DWORD time; // 消息产生的时间
POINT pt; // 产生消息时光标在屏幕上的坐标
} MSG;
typedef MSG FAR *LPMSG;
其中的POINT 类型的定义是:
typedef struct tagPOINT {
int x; /* X 坐标 */
int y; /* Y 坐标 */
} POINT;
typedef POINT FAR *LPPOINT;
Windows.h 在定义大部分类型的同时,还定义了该类型的指针类型。例如,上例中的LPPOINT 和LPMSG 等,其中字母前缀LP 表示远指针类型;若使用NP 作为一个类型的前缀,则表示近指针类型;若使用P 作为一个类型的前缀时,则表示一般的指针类型,这时由编译程序时所使用的内存模块决定这种指针是远指针或是近指针。在Windows.h 中说明的大部分指针类型都采用这里介绍的方法进行说明,例如,LPRECT ,它表示一个RECT 类型的远指针。
在Windows.h 中说明的大部分指针类型使用了C/C++ 的关键字const ,如果一个指针类型的名字前缀为LPC 、NPC 或PC ,则其中的字母C 表示这种类型的指针变量所指向的变量不能通过该指针变量来修改,这种指针类型一般采用下述方法进行说明:
typedef const POINT FAR * LPCPOINT;
typedef const REC FAR * LPCRECT;
一个使用const 修饰的指针(称其为const 指针)可以指向没有使用const 修饰的变量,但没有使用const 修饰的指针不能指向const 修饰的变量,例如:
const POINT pt;
LPCPOINT lpPoint = &pt; // 正确
LPPOINT lpPoint = &pt; // 错误
当向函数传递参数时,必须特别注意这个问题,例如:
void fun(LPPOINT lppt) ;
......
LPCPOINT lpPoint ;
fun(lpPoint) ;
编译器将指示这个函数调用语句是错误的。所以,在一个函数不修改一个指针参数所指向的变量的情况下,最好将该参数说明为const 指针,使const 类型的指针也能用于该函数的参数。Windows.h 中说明的大部分函数使用了const 指针参数。
在Windows.h 中,大多数语句是用于定义一个常量,例如:
#denfine WM_QUIT 0X0012
该语句用标识符WM_QUIT 来表示编号为0X0012 的消息。每个常量由一个前缀和表示其含义的单词组成的标识符组成,两者之间用下画线隔开。前缀表明这些常量所属的一般范畴。下面是一些前缀和它们所属的范畴的说明。
类型 | 说明 |
CS | 窗口类的风格(Class Style ) |
IDI | 预定义的图标对象的标识符(IDentity of Icon ) |
IDC | 预定义的光标对象的标识符(IDentity of Cursor ) |
WS | 窗口的风格(Windows Style ) |
CW | 创建窗口(Create Windows ) |
WM | 窗口消息(Windows Message ) |
DT | 绘制文本(Drawing Text ) |
在变量名的表示方法方面,Windows 推荐使用一种称为“ 匈牙利表示法” 的方法。每个变量名用小写字母或描述了变量的数据类型的字母作为前缀,变量的名字紧跟其后,且用大写字母开始的单词(一个或多个单词)表示其含义,这样每个变量都能附加上其数据类型的助记符。例如:
WORD wOffset ; /* w 表示WORD 类型 */
DWORD dwValue ; /* dw 表示DWORD 类型 */
下面是Windows 中常使用的一些字母前缀和它们所代表的数据类型:
类型 | 说明 |
b | BOOL ,布尔类型 |
by | BYTE 类型 |
c | char 类型 |
dw | DWORD 类型 |
fn | 函数类型 |
i | 整型 |
l | LONG 类型 |
lp | 远(长)指针(long pointer ) |
n | 短整型 |
np | 近(短)指针(near pointer ) |
p | 指针 |
s | 字符串 |
sz | 以'/0' 结尾的字符串 |
w | WORD 类型 |
x | short ,用于表示X 坐标时 |
y | short ,用于表示Y 坐标时 |
Windows 程序员也可以根据上述思想和使用目的,发明一些其他的前缀,但要注意,对这些前缀的使用必须保持前后一致。在Windows 中,所有的函数根据其用途来命名,它们一般由2 到3 个英文单词组成,每个单词的第一个字母大写,例如,函数CreateWindow() ,由该函数的名字可以知道它的用途是创建一个窗口。
<!-- /* Font Definitions */ @font-face {font-family:Wingdings; panose-1:5 0 0 0 0 0 0 0 0 0; mso-font-charset:2; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:0 268435456 0 0 -2147483648 0;} @font-face {font-family:宋体; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-alt:SimSun; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} @font-face {font-family:"/@宋体"; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-parent:""; margin:0cm; margin-bottom:.0001pt; text-align:justify; text-justify:inter-ideograph; mso-pagination:none; font-size:10.5pt; mso-bidi-font-size:12.0pt; font-family:"Times New Roman"; mso-fareast-font-family:宋体; mso-font-kerning:1.0pt;} /* Page Definitions */ @page {mso-page-border-surround-header:no; mso-page-border-surround-footer:no;} @page Section1 {size:612.0pt 792.0pt; margin:72.0pt 90.0pt 72.0pt 90.0pt; mso-header-margin:36.0pt; mso-footer-margin:36.0pt; mso-paper-source:0;} div.Section1 {page:Section1;} /* List Definitions */ @list l0 {mso-list-id:394162598; mso-list-template-ids:-1963032;} @list l0:level1 {mso-level-number-format:bullet; mso-level-text:; mso-level-tab-stop:36.0pt; mso-level-number-position:left; text-indent:-18.0pt; mso-ansi-font-size:10.0pt; font-family:Symbol;} ol {margin-bottom:0cm;} ul {margin-bottom:0cm;} -->
1.7 、应用程序使用的一些术语
本节介绍Winodws 应用程序使用的一些术语及其相关概念,在后面的章节中介绍有关的内容时,再对其中的概念进行详细的讨论。
1.7.1 模块
在Windows 中,术语“ 模块” 一般是指任何能被装入内存中运行的可执行代码和数据的集合。更明确地讲,模块指的就是一个.EXE 文件(又称为应用程序模块),或一个动态链接库(DLL — Dynamic Linking Library ,又被称为动态链接库模块或DLL 模块),或一个设备驱动程序,也可能是一个程序包含的能被另一个程序存取的数据资源。模块一词也被用于特指自包含的一段程序。例如,一个可单独编译的源文件,或该源文件被编译器处理之后所生成的目标程序。当制作一个程序时,模块一词用于指被连接在一起的许多模块中的某个模块。
Windows 本身由几个相关的模块组成,Windows API 函数就是在Windows 启动时装入内存中的几个动态链接库模块实现的。其中的三个主要模块是USER.EXE (用于窗口管理等)、KERNEL.EXE (用于内存管理的多任务调度)和GDI.EXE (图形设备接口,用于图形输出等)。
1.7.2 应用程序
一个Windows 应用程序是被Windows 调用或在Windows 下运行的一个程序,这个程序可以调用静态连接库(也就是C 的运行时间库)中的函数和DLL 的函数,它也可以启动其它的应用程序。一个应用程序在运行时的输入被Windows 捕获,并以消息的形式传送到应用程序的活动窗口上。一个应用程序的输出也是通过Windows 进行的,所有的输出首先被送给Windows 。许多MS-DOS 应用程序基本上占据整个计算机,并认为所有的计算机资源只属于该应用程序,应用程序告诉相对被动的MS-DOS 应做什么。在一个Windows 应用程序中,Windows 自身是非常主动的,并且和应用程序协同得非常紧密。Windows 管理着计算机的所有资源,并调度这些资源,使它们可为正在Windows 上运行的所有应用程序共享。
1.7.3 任务和实例
Windows 将运行的应用程序实例作为不同的任务。当一个应用程序的多个实例在运行时,它们也被Windows 当作不同的任务。Windows 为一个模块的每一个实例都装入一个缺省数据段,但可执行代码只能装入一次。也就是说,同一个模块的实例共享相同的代码,但有自己私用的数据段。
对每一个模块、任务或实例,Windows 分别使用一个句柄来标识它。在窗口对象的私有数据存储区存储有一个应用程序的任务句柄、实例句柄和模块句柄。任务句柄被Windows 的任务调度程序用于进行任务调度。通过模块句柄,Windows 可以知道一个模块当前有多少实例正在运行。同一个模块的不同实例有相同的模块句柄,但有不同的任务和实例句柄。当Windows 由于内存管理的需要而废弃了一个实例的代码段时,通过模块句柄,Windows 可以从模块中重新装入这个实例所需的代码。
1.7.4 动态链接库
DLL 是一种有别于MS-DOS 应用程序所使用的库模块(例如 C 的运行时间库)的一种特殊的库模块,它含有可能 被其它应用程序调用的函数。一个DLL 在运行时被动态地连接到一个应用程序中或另一个DLL 中,而不是在制作应用程序时静态地连接到应用程序中的(这种方法是在制作MS-DOS 应用程序中使用的方法,它们在Windows 应用程序中仍然可以继续被使用)。使用DLL 的好处在于,当有多个应用程序使用同一个DLL 并且同时在Windows 中运行时,该DLL 在内存中只有一个实例。
1.7.5 应用程序设计接口
应用程序设计接口(API )是应用程序用于操作周围环境的一组函数调用接口。Windows API 大约有600 多个函 数,学习Windows 程序设计的许多工作就是学习如何使用这些API 。
1.7.6 Windows 下的函数
在进行Windows 应用程序设计中,程序员除了需要知道有关一个函数的常用信息(例如函数的名字,近函数或远函数,返回类型以及应如何调用)之外,同时还要知道更多的内容:一个回调函数、引出函数或是一个引入函数。
引出函数 :这个术语与一个函数如何在一个模块中说明而在另一个模块中被调用有关。引出函数是在一个模块中定义而在这个模块之外被调用的一种函数;或是被Windows 或是被另一个模块调用。这些函数必须以一种特定的方式进行说明,并被编译器作特殊的处理。这样,当它们被调用时,它们会被正确地束定到合适的数据段上。DLL 为其它模块提供要被调用的函数,因此,每个DLL 一般都带有一个DLL 库,以便应用程序可以合法地调用DLL 中的函数。DLL 库由DLL 中每个引出函数的入口点组成。整个Windows API 就是由构成Windows 环境的不同的模块所引出的函数组成,这些API 函数的入口点在一个名为IMPORT.LIB 的DLL 库中说明。
引入函数 :在DLL 中引出的函数若要能为一个模块调用,必须在这个模块中将这个函数说明为引入函数。由此可见引出函数和引入函数表达的是从两种角度处理同一个函数的术语:引出模块中的一个函数使得这个函数能被其它模块调用;调用引出函数的模块通过引入这个函数才能调用它。在制作Windows 应用程序时,连接器自动包含一个名为IMPORT.LIB 的库文件。这个文件允许应用程序调用Windows API 中的函数。这个文件被称为引入库。引入库提供了应用程序与一个到多个DLL 中可被这个应用程序调用的函数之间的连接。
回调函数 :回调函数是一种特殊的引出函数,是由 Windows 环境直接调用的函数。一个应用程序至少要有一个回调函数。当一条消息要交给应用程序处理时,Windows 调用这个回调函数。这个函数对应于一个活动窗口,被称为这个窗口的窗口函数。因为许多应用程序至少建立一个窗口,并且Windows 需要向这个窗口发送消息,所以,处理消息的函数必须由Windows 调用。在请求Windows 枚举它所维护的对象时,例如字体或窗口,Windows 也要调用应用程序中的回调函数。当向Windows 提出这样的请求时,就必须向Windows 提供回调函数的地址。
由于引出函数是在不同的模块中被调用的,也就是说,调用者的代码段与被调用的引出函数的代码段不在同一个段中,因此,在所开发的Windows 应用程序中,引出函数都被说明为远函数。为了程序运行的效率原因,引出函数都使用Pascal 调用约定,这种调用约定不同于C 调用约定的地方在于:
- 最左边的参数先入栈:Pascal 调用约定的参数进入栈的顺序是函数调用中最左边的参数先入栈。C 的调用约定与此相反,它采用最右边的参数先入栈。
- 被调用的函数负责从展中清除参数:Pascal 调用约定的函数在返回时负责清除栈中的参数;C 调用约定的函数不作这种工作,而由调用者来作;这样,当程序中调用了大量的使用C 调用约定的函数时,为清除栈中的参数,在程序中要额外地增加许多代码。
- 全局标识符不保持原来的大小写(一般被为大写形式),也不在标识符前面加下划线。
为便于程序开发活动,在Windows.h 中定义了两个类型名,用于在程序说明引出函数:
类型 | 说明 |
WINAPI | 等价于FAR PASCAL ,说明该函数是一个引出函数,这个类型名只用于在DLL 中说明引出函数,或在应用程序中对DLL 中的引出函数进行函数说明时。 |
CALLBACK | 等价于FAR PASCAL ,说明该函数是一个回调函数,它常被用在应用程序模块中说明一个窗口函数或其它种类的回调函数。 |
1.8 、事件和消息
在Windows 中,用户或系统中所发生的任何活动被当作事件来处理,例如,用户按下了鼠标按钮,就产生一鼠标事件。对于所发生的每一个事件,Windows 将其转换成消息的形式放在一个称为消息队列的内存区中,然后由Windows 的消息发送程序选择适合的对象,将消息队列中的消息发送到欲接受消息的对象上。Windows 的消息可分为四种类型:
(1 )输入消息 :对键盘和鼠标输入作反应。这类输入消息首先放在系统消息队列中,然后Windows 将它们送入应用程序的消息队列,使消息得到处理。
(2 )控制消息 :用来与Windows 的特殊控制对象,例如,对话框、列表框、按钮等进行双向通信。这类消息一般不通过应用程序的消息队列,而是直接发送到控制对象上。
(3 )系统消息 :对程式化的事件或系统时钟中断作出反应。有些系统消息,例如大部分DDE 消息(程序间进行动态数据交换时所使用的消息)要通过Windows 的系统消息队列。而有些系统消息,例如窗口的创建及删除等消息直接送入应用程序的消息队列。
(4 )用户消息 :这些消息是程序员创建的,通常,这些消息只从应用程序的某一部分进入到该应用程序的另一部分而被处理,不会离开应用程序。用户消息经常用来处理选单操作:一个用户消息与选单中的一选项相对应,当它在应用程序队列中出现时被处理。
Windows 应用程序通过执行一段称为消息循环的代码来轮询应用程序的消息队列,从中检索出该程序要处理的消息,并立即将检索到的消息发送到有关的对象上。典型的Windows 应用程序的消息循环的形式为:
MSG msg;
while (GetMessage(&msg, NULL, 0, 0L))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
函数GetMessage 从应用程序队列中检索出一条消息,并将它存于具有MSG 类型的一个变量中,然后交由函数TranslateMessage 对该消息进行翻译,紧接着,函数DispatchMessage 将消息发送到适当的对象上。
<!-- /* Font Definitions */ @font-face {font-family:宋体; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-alt:SimSun; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} @font-face {font-family:"/@宋体"; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-parent:""; margin:0cm; margin-bottom:.0001pt; text-align:justify; text-justify:inter-ideograph; mso-pagination:none; font-size:10.5pt; mso-bidi-font-size:12.0pt; font-family:"Times New Roman"; mso-fareast-font-family:宋体; mso-font-kerning:1.0pt;} /* Page Definitions */ @page {mso-page-border-surround-header:no; mso-page-border-surround-footer:no;} @page Section1 {size:612.0pt 792.0pt; margin:72.0pt 90.0pt 72.0pt 90.0pt; mso-header-margin:36.0pt; mso-footer-margin:36.0pt; mso-paper-source:0;} div.Section1 {page:Section1;} -->
1.9 、窗口对象
对Windows 用户和程序员而言,窗口对象(简称窗口)是一类非常重要的对象。尤其对程序员,窗口的定义和创建以及对窗口的处理过程最能直观地反映出Windows 中面向对象的程序设计的四个基本机制(类、对象、方法、和消息)。
1.9.1 窗口类
如前所述,在程序中创建对象,必须先定义对象所属的类。在Windows 中,窗口类是在类型为WNDCLASS 的结构变量中定义的,在Windows.h 中,结构类型WNDCLASS 的说明为:
typedef struct tagWNDCLASS {
DWORD style; /* 窗口风格 */
WNDPROC *lpfnWndProc; /* 窗口函数 */
int cbClsExtra; /* 类变量占用的存储空间 */
int cbWndExtra; /* 实例变量占用的存储空间 */
HINSTANCE hinstance; /* 定义该类的应用程序实例的句柄 */
HICON hicon; /* 图标对象的句柄 */
HCURSOR hCursor; /* 光标对象的句柄 */
HBRUSH hbrBackground; /* 用于擦除用户区的刷子对象的句柄 */
LPCSTR lpszMenuName; /* 标识选单对象的字符串 */
LPCSTR lpszClassName; /* 标识该类的名字的字符串 */
} WNDCLASS;
WNDCLASS 类型有十个域,它描述了该类的窗口对象所具有的公共特征和方法。在程序中可以定义任意多的窗口类,每个类的窗口对象可以具有不同的特征。lpszClassName 是类的名字,在创建窗口对象时用于标识该窗口对象属于哪个类。lpfnWndProc 是指向函数的一个指针,所指向的函数应具有下述的函数原型:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam,LPARAM lParam);
该函数被称为窗口函数,其中定义了处理发送到该类的窗口对象的消息的方法。窗口函数是一个回调函数,因此在定义窗口函数时要使用CALLLBACK 类型进行说明。参数hWnd 是一个窗口对象的句柄。通过该句柄,一个窗口函数可以检测出当前正在处理哪个窗口对象的消息。参数message 是消息标识符,参数wParam 和lParam 是随同消息一起传送来的参数,随着消息的不同,这两个参数所表示的含义也不大相同,在定义消息时对这两个参数的含义一同进行定义。
域hIcon 、hCursor 和hbrBackground 分别定义窗口变成最小时所显示的图标对象的句柄,当光标进入该类的窗口对象的显示区域时所显示的光标对象的句柄,当需要擦除用户区域显示的消息时所使用的刷子对象的句柄(该刷子作用的结果形成窗口用户区的背景色)。
域style 规定窗口的风格,它可用下列常量经位或运算之后形成:
类型 | 说明 |
CS_HREDRAW | 如果窗口的水平尺寸被改变,则重画整个窗口 |
CS_VREDRAW | 如果窗口的垂直尺寸被改变,则重画整个窗口 |
CS_BYTEALIGNCLIENT | 在字节边界上(在X 方向上)定位用户区域的位置 |
CS_BYTEALIGNWINDOW | 在字节边界上(在X 方向上)定位窗口的位置 |
CS_DBLCLKS | 当连续两次按动鼠标键时向窗口发送该事件的消息 |
CS_GLOBALCLASS | 定义该窗口类是一个全局类。全局类由应用程序或库建立,并且所有的应用程序均可使用全局类 |
CS_NOCLOSE | 禁止系统选单中的Close 选项 |
还有其他一些常量,在后面的章节中介绍有关内容时再进行讨论。
域lpszMenuName 指向一个以‘/0’ 字符(称为空字符或NULL 字符)结尾的字符串,用于标识该窗口类的所有对象所使用的缺省选单对象。如果该域为NULL ,则表示没有缺省选单。
域hInstance 用于标识定义该窗口类的应用程序的实例句柄。每一个窗口类需要一个实例句柄来区分注册窗口类的应用程序或DLL ,该实例句柄用于确定类属。当注册窗口类的应用程序或DLL 被终止时,窗口类被删除。
WNDCLASS 类型规定了该类窗口对象的基本数据表示和处理消息的窗口函数,但是,在有些应用程序中,单有这些是不够的。因此,该类型提供了两个域cbClsExtra 及cbWndExtra ,指示系统分配额外的存储空间用于存储一些附加数据。其中cbClsExtra 定义可以为该类的所有对象共用的数据占用的存储空间的大小(以字节计);而cbWndExtra 用于定义该类的每个对象私用的数据占用的存储空间的大小(以字节计),一个对象可以在该私有存储空间中存储一些数据,但该类的其他对象不能访问到这个对象所存储的这些私用数据。而在公用存储空间中所存的数据可被该类的所有对象访问到。函数SetClassWord/SetClassLong 和GetClassWord/GetClassLong 用于访问公用数据,函数SetWindowWord/SetWindowLong 和函数GetWindowWord/GetWindowLong 用于访问特定对象的私用数据,这些函数在“ 窗口对象” 一章讨论。
当程序员设置了WNDCLASS 变量的各个域之后,使用函数RegisterClass 向Windows 注册这个类,至此,完成了定义一个窗口类的过程。函数RegisterClass 的原型为:
BOOL RegisterClass(LPWNDCLASS lpWndClass);
该函数唯一的一个参数是指向WNDCLASS 类型的变量的指针。函数返回非零,表示注册成功,否则注册失败。不能向Windows 注册具有相同名字(lpszClassName 域指向相同的两个字符串)的两个类,否则第二次注册失败并被忽略。下面是定义和注册窗口类的程序示例说明:
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( BLACK_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = "Window";
if (!RegisterClass(&wndclass))
... / * 处理类注册错误 * /
其中,WndProc 是一个窗口函数名,变量hInstance 存储着当前程序实例的句柄。Windows 预定义了一些图标、光标和刷子对象,函数LoadIcon 返回预定义的应用程序图标的句柄,该图标由第二个参数IDI_APPLICATION 来定义。函数LoadCursor 返回标准箭头光标(IDC_ARROW )的句柄,函数GetStockObject 返回库存对象中一个白色刷子(WHITE_BRUSH )的句柄。
1.9.2 创建窗口对象
在上一节中,我们介绍了窗口类的定义方法,窗口的某些特征(如窗口的颜色等)属于窗口类中定义的,并由该窗口 类的所有实例共享。在注册了窗口类之后,程序员使用函数CreateWindow 创建窗口,得到窗口类的一个实例(一个窗口对象)的句柄。一个窗口可以是一个重叠式窗口,或是一个弹出式窗口,或是一个隶属窗口,或是一个子窗口,这在使用CreateWindow 函数时指定。每一个子窗口都有一个父窗口,每一个隶属窗口都有一个拥有者,这个拥有者是另一个窗口对象,弹出式窗口是一种特殊的窗口,这些内容在“ 窗口对象” 一章介绍。
表1-1 CreateWindow 函数
用 途 | 创建一个重叠窗口、弹出式窗口、隶属窗口或子窗口 |
| ||||||||||||||||||||||||||
原 型 |
| |||||||||||||||||||||||||||
返回值 | 返回值是标识所创建的窗口对象的句柄,如果返回值为NULL ,则窗口没有被创建。 |
|
函数CreateWindow 的第三个参数指定窗口的风格,表1-2 是在Windows.h 中定义的一些常用到的风格常量,通过将这些常量使用位运算组合在一起,形成所要求的窗口风格。
表1-2 窗口风格
类型 | 说明 |
WS_BORDER | 创建一个有边框的窗口 |
WS_CAPTION | 创建一个有标题栏的窗口 |
WS_CHILDWINDOW (or WS_CHILD ) | 创建一个子窗口(不能与WS_POPUP 一起使用) |
WS_CLIPCHILDREN | 当在父窗口内绘制时,把子窗口占据的区域剪切在外,即不在该区域内绘图 |
WS_CLIPSIBLINGS | 裁剪相互有关系的子窗口,不在被其它子窗口覆盖的区域内绘图,仅与WS_CHILD 一起使用 |
WS_DISABLED | 创建一个初始被禁止的窗口 |
WS_DLGFRAME | 创建一个有双边框但无标题的窗口 |
WS_HSCROLL | 创建一个带水平滚动杠的窗口 |
WS_VSCROLL | 创建一个带垂直滚动杠的窗口 |
WS_ICONIC | 创建一个初始为图标的窗口,仅可以与WS_OVERLAPPEDWINDOWS 一起使用 |
WS_MAXIMIZE | 创建一个最大尺寸的窗口 |
WS_MINIMIZE | 创建一个最小尺寸的窗口(即图标) |
WS_MAXIMIZEBOX | 创建一个带有极大框的窗口 |
WS_MINIMIZEBOX | 创建一个带有极小框的窗口 |
WS_OVERLAPPED | 创建一个重叠式窗口,重叠式窗口带有标题和边框 |
WS_POPUP | 创建一个弹出式窗口,不能与WS_CHILD 一起使用 |
WS_SYSMENU | 窗口带有系统选单框,仅用于带标题栏的窗口 |
WS_THICKFRAME | 创建一个边框的窗口,使用户可以直接缩放窗口 |
WS_VISIBLE | 创建一个初始可见的窗口 |
在Windows.h 中,还定义了风格WS_OVERLAPPEDWINDOW 和WS_POPUPWINDOW 。其中,WS_OVERLAPPEDWINDOW 由下面的宏进行定义:
#define WS_OVERLAPPEDWINDOW(
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU |
WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
)
WS_POPUPWINDOW 定义为:
#define WS_POPUPWINDOW ( WS_BORDER | WS_POPUP | WS_SYSMENU )
但是,在使用WS_POPUPWINDOW 时,必须组合WS_CAPTION ,否则不能使系统选单(WS_SYSMENU )在窗口上可见。另外 两个窗口风格是WS_GROUP 和WS_TABSTOP ,这两个窗口风格的意义在介绍对话框时进行介绍,在介绍对话框时,还将介绍其它窗口风格。
CreateWindow 函数的x 和y 参数是窗口左上角相对于屏幕左上角的坐标。这两个参数可以使用常量CW_USEDFAULT ,用于表示使用缺省位置。缺省时,Windows 显示各个重叠窗口的位置在水平方向的垂直方向上均与屏幕左上角有一个相应的偏移值。nWindth 和nHeight 参数也可以使用常量CW_USEDEFAULT 来指定,这时,Windows 使用缺省的窗口尺寸。缺省的窗口尺寸在水平方向延伸到屏幕的右边界,在垂直方向延伸到屏幕底部显示图标区域的上方。
下面的程序说明在Windows 程序中创建一个窗口对象的基本方法,所创建的窗口对象所属的类为在1.9.1 节定义的“Window” 窗口类。
HWND hWnd;
hWnd = CreateWindow(
"Windows",
"Sample Program",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,CW_USEDEFAULT,
CW_USEDEFAULT,CW_USEDEFAULT,
NULL, // 没有父窗口
NULL, // 使用类选单
hInstance , // 变量hInstance 中存储有当前程序实例的句柄
NULL, // 没有额外数据
};
其中所使用的符号“//” 是C++ 语言新增加的单行注释符,它表示从“//” 开始到它所在的行的结尾所有内容都是注释。
1.9.3 窗口函数
在前面两小节中,我们介绍了定义类和创建对象的过程。本节介绍窗口对象如何接收和处理所有影响窗口的事件(如击键或按动鼠标键)的消息。一个窗口对象所接受到的消息的响应是由该对象的方法决定的,这些方法被定义在一个称为窗口函数的函数中。同一类的所有对象共用同一个窗口函数。窗口函数决定着对象如何用内部方法对消息作出响应,例如,如何在屏幕上画出窗口自身。
一个最简单的窗口函数为:
LRESULT CALLBACK WndProc(HWND hwnd, UNIT message, WPARAM wParam, LPARAM lParam)
{
return DefWindowProc (hwnd, message, wParam, lParam);
}
该窗口函数通过调用Windows 的函数DefWindowProc (缺省窗口函数),让Windows 的缺省窗口函数来处理所有发送到窗口对象上的消息。
当用户操作屏幕上的一个窗口对象时(例如用户改变了屏幕上窗口对象的位置或大小)或发生其它事件时,该事件的消息被存于应用程序的消息队列中, 消息循环首先从该队列中检索出该消息,然后将消息发送到某个对象上。发送过程由Windows 来控制,Windows 根据消息结构中的hWnd 域所指示的消息发送的目标对象,调用该对象所在类的窗口函数完成消息的发送工作。窗口函数根据消息的种类 ,选择执行一段代码(方法),对消息进行处理,并通过return 语句回送一个处理结果或状态。消息循环、Windows 和窗口函数协同配合,完成一条消息的发送和处理。在处理完一条消息之后,如果应用程序队列中还有其他消息,继续进行上述处理过程,否则,应用程序在消息循环处理进行等待。
1.9.4 处理消息
窗口对象接收到的每条消息由参数message 来标识,随同该消息一传递过来的其它数据由参数wParam 和lParam 给出。wParam 用于十六位的数据,而lParam 用于32 位的数据。
在窗口函数中,使用switch 语句来判断窗口函数接收到什么消息,通过执行相应的语句对消息进行处理。当处理完一条消息时,窗口函数要返回一个值,表示消息的处理结果,许多消息返回0 值,有些要求返回其它的值,这由具体的消息决定。窗口函数不打算处理的消息必须交由DefWindowProc() 进行处理,并且函数必须返回DefWindowProc() 的返回值。
窗口函数的基本结构为:
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
变量说明语句
初始化语句
switch (message)
{
case 消息 1:
处理“ 消息 1” 的语句序列
return 表达式 1;
case 消息 2:
处理“ 消息 2” 的语句序列
return 表达式 2;
.......
case 消息 n:
处理“ 消息 n” 的语句序列
return 表达式 n;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
Windows 为预定义的每种消息都指定了一个以WM (Window Message )为前缀的标识符常量。下面的窗口函数处理一条WM_DESTROY 消息。
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY:
PostQuitMessage(0):
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
在1.11 节,我们结合对Windows 程序结构的介绍再详细解释窗口是如何处理WM_DESTROY 消息的。
<!-- /* Font Definitions */ @font-face {font-family:Wingdings; panose-1:5 0 0 0 0 0 0 0 0 0; mso-font-charset:2; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:0 268435456 0 0 -2147483648 0;} @font-face {font-family:宋体; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-alt:SimSun; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} @font-face {font-family:"/@宋体"; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-parent:""; margin:0cm; margin-bottom:.0001pt; text-align:justify; text-justify:inter-ideograph; mso-pagination:none; font-size:10.5pt; mso-bidi-font-size:12.0pt; font-family:"Times New Roman"; mso-fareast-font-family:宋体; mso-font-kerning:1.0pt;} /* Page Definitions */ @page {mso-page-border-surround-header:no; mso-page-border-surround-footer:no;} @page Section1 {size:612.0pt 792.0pt; margin:72.0pt 90.0pt 72.0pt 90.0pt; mso-header-margin:36.0pt; mso-footer-margin:36.0pt; mso-paper-source:0;} div.Section1 {page:Section1;} /* List Definitions */ @list l0 {mso-list-id:1320115985; mso-list-template-ids:-1723421090;} @list l0:level1 {mso-level-number-format:bullet; mso-level-text:; mso-level-tab-stop:36.0pt; mso-level-number-position:left; text-indent:-18.0pt; mso-ansi-font-size:10.0pt; font-family:Symbol;} ol {margin-bottom:0cm;} ul {margin-bottom:0cm;} -->
1.10 、Windows 应用程序的面向对象认识
面向对象作为一种方法学,要求将程序中的数据和操作(代码)归结到某些对象名下,将数据看作对象的属性,要改变这些属性,必须通过操作来进行。
进行面向对象的程序设计最好使用面向对象的语言,如C++ ,SamllTalk 等。面向对象的语言的语言所起的作用,就是给程序员们提供一些进行面向对象的程序设计时必需的约束,使数据和操作的衔接有一种显式的描述,并进行一些技术性的事务管理。但是,如果我们能理解面向对象程序设计的原理和方法,即使不使用面向对象的语言,也能实现面向对象的程序设计。
Windows 本身并不是一个面向对象的程序设计环境,但Windows 的某些部分还是明显地受到面向对象的软件的概念的影响。从某种程度上说,在进行Windows 程序设计时,程序员是在进行面向对象的程序设计。理解Windows 的面向对象的思想和应用程序设计的面向对象方法对设计结构合理的应用程序会有很大的帮助。
前面已给出了对象的定义:每个对象包含有数据和代码,代码描述了对象可执行的一系列预定义的动作,而数据是对象私有的,它们由相关的可执行代码存取。预定义的动作和私有数据的结合称为封装。在C 中,我们使用一个函数来封装一个对象的私有数据和动作,使用switch 语句来定义预定义的动作,这些动作只存取为该函数本身所知道的数据。
Windows 和Windows 应用程序是怎样发送消息的呢?在Windows 及其应用程序中,消息被表示为一个数据结构,并能在对象之间传递。发送消息等价于执行其参数表示消息数据的函数调用,参数之一是一个标识该消息的预定义的消息标识符,当一个对象接受到一条消息时,消息标识符决定该对象执行何种动作。消息传递是以函数调用的形式来实现的,这种调用可以发生在程序的任何地方。
Windows 程序员必须清楚用消息引发动作的技术。不同的对象能以不同的动作响应同样的消息。这样,一个特定的消息可代表一个通用事件。例如,按键操作、移动鼠标或绘制用户区等;而任何一个特定的消息可以在不同的对象中引发不同的动作,例如,不同的窗口对象以不同的动作处理同样的WM_KEYDOWN 、WM_MOUSEMOVE 或WM_PAINT 消息。
一个消息可以有一个对象发送到另一个对象,或由Windows 发送到某个对象。例如,WM_KEY_DOWN 之类的消息是由Windows 产生的。有些消息在对象的窗口函数对其处理完毕后就消失了,而有些消息在处理时有产生新的消息:一个对象通过向其它对象或自己发送一条或多条消息来处理一条消息。这样,Windows 应用程序的控制流程不象MS-DOS 应用程序那样易于跟踪,程序的调试也比MS-DOS 应用程序困难。
除了个别消息以外,对象接受消息的顺序是不可预知的,但对象处理每条消息所采取的动作是显式定义在窗口函数中的。对象并不显式地定义所有可能消息的动作,对于不显示处理的消息,都交由DefWindowProc 进行缺省处理。
消息传递的途径很简单:从一个对象传递到另一对象,但由于DefWindowProc 对有些消息提供了缺省处理,因此,程序员在设计程序时必须考虑在一个窗口函数中捕获某条消息时是否还应交给DefWindowProc 函数作进一步的处理。DefWindowProc 能处理所有的消息,但对大部消息只是简单地废弃之,不作具有实际意义的处理,在窗口函数捕获这些废弃消息是安全的;若要捕获其它消息,则必须了解DefWindowProc 是怎样处理这条消息的,并在窗口函数的处理代码中能提供类似的处理(或将该消息交由DefWindowProc 作进一步的处理)。
现在我们讨论窗口函数对对象的私有数据的处理问题。窗口类也说明了对象的私有数据,当调用CreateWindow 创建一个窗口对象时,Windows 为创建的窗口对象分配私有数据存储区,其中存储有窗口的实例句柄、父窗口句柄、窗口函数的地址和其它Windows 用于管理窗口对象的数据。对这些私有数据的的操作只能使用GetWindowWord/GetWindowLong 等函数。对于程序中说明的变量,如何在窗口函数中将它们与相关的对象衔接在一起就比较复杂,因为窗口函数为该类的所有对象共享,该类的所有对象在接收到消息时都执行相同的代码。
在过去,Windows 推荐使用的程序设计语言是C ,由于C 语言不具备将一个对象的私有数据和操作这些私有数据的代码衔接在一起的语言成份(面向对象的语言的事务性工作之一就是为程序完成这个工作),这个工作只能由程序员来作。程序员心中必须清楚程序中所说明或分配的变量私有于哪个对象,并采用合适的数据结构来表示它们,以便程序在使用它们时,能根据不同的对象将它们区别开来。
有几种方法可用于区分对象的私有数据:
- 程序员编制额外的代码来判断一个对象应使用哪些数据。
- 使用窗口附加字节。
- 使用属性表。
当使用第一种方法时,程序实际是使用对象句柄作索引来检索与该对象相关的私有数据,Windows 也使用这种方法使用句柄来检索一张表,这个表中存储着该句柄所标识的对象的私有数据。Windows 的许多函数需要一个对象的句柄作为第一参数,其原因就是为区分对象的私有数据,以便使用相同的函数处理不同的对象(的数据)。
后两种方法与第一种方法本质是一样的(我们会将在后面的章节对其进行介绍),只是Windows 提供了一些相关的函数来简化程序的工作。
由于C 没有继承这种语言成分,因为,也就不能形成对象的等级结构。继承是面向对象语言的另一个重要成分。继承使得程序中的对象形成一个分层次的对象结构,低层次的对象可以将它不处理的消息发送到高层对象上进行缺省处理。由于在C 中不能(或说很难)建立对象的这种等级结构,但为了简化应用程序的设计,又必须要求支持消息的缺省处理(否则应用程序要定义一个窗口对象可能接收到的所有消息的处理代码),因此只能使用DefWindowProc 提供消息的缺省处理。这就要求对一个窗口对象所有消息的处理定义在一个函数中,就带来了定义窗口函数的返回值和参数类型时使用了一种较难为人理解的方法。因为不同的消息可以带有不同类型和个数的参数,并且返回数据的类型也不相同,Windows 的设计者采用了一个折中的方法:为消息规定一个十六位的参数和一个32 位的参数,将返回类型指定为LRESULT ,这种类型的长度能容下C 中所有预定义类型的数据。
由于不同类的窗口对象定义有自己的窗口函数,但C 语言不具备根据接受消息的对象自动决定调用该对象的窗口函数的能力(在面向对象的语言中,这种能力被称为多态性)。因此,向不同的窗口对象发送消息时使用函数SendMessage 对窗口函数作间接调用,由Windows 根据该函数调用中所使用的对象标识符来调用该对象的窗口函数。
在程序设计中由于窗口函数的限制,需经常进行各种各样的数据类型转换。例如:
SendMessage(hwnd, WM_USER, (WPARAM)5, MAKELPARAM(89, 3267));
在这个例子中,为了组建一个LPARAM 类型的数据,使用了宏MAKEPARAM 。它将两个十六位的数据组装成一个32 位的数据(低位字为MAKEPARAM 的第一个参数,高位字为第二个参数)。当需要从一个LPARAM 类型的数据中分离出低位字和高位字时,使用宏LOWORD 和HIWORD 。例如,处理上个例子中所发送的WM_USER 消息的窗口函数的代码可能为:
WORD wStart = LOWORD(lParam);
WORD wStart = LOWORD(lParam);
宏MAKELRESULT 与MAKELPARAM 类似,它被用于装配LRESULT 类型的数据。宏MAKELONG 用于装配LONG 类型的数据,当需要从LRESULT 或LONG 类型的数据中分离出高位字和低位字时,使用宏HIWORD 和LOWORD 。
基于上面的介绍,我们在设计Windows 应用程序时,要明确程序中存在哪些对象,对象之间是如何通过消息传递程序控制的,哪些数据是对所有对象公有的, 哪些数据是私有于某一个对象的,公有数据和对象的私有数据必须是存储在静态生存期的变量中(局部生存期的变量在窗口函数返回后就消失了,不能在下次调用该函数时保存上次的值。换句话说,存储对象的数据的变量的生存期不应小于对象的生存期)。
由于Windows 应用程序各个模块之间主要是通过消息传递控制,因此,Windows 应用程序的逻辑结构就不同于MS-DOS 应用程序的逻辑结构,如图1-1 所示。从图1-1 可以看出,Windows 应用程序的各个模块通过消息传递被联系在一起,因此,如果正确地组织程序,程序的模块性和结构较MS-DOS 应用程序要好。
|
图1-1 DOS 应用程序与Windows 应用程序逻辑结构的比较示意说明 |
1.11 、Windows 程序的组织
将1.9 节介绍的程序按照C/C++ 语言的要求组织起来,就得到一个完整的Windows 程序。一个Windows 程序必须有一个名为WinMain 的主函数。
// 1-1.c 代码片段
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int PASCAL WinMain(
HINSTANCE hInstance, // 应用程序的实例句柄
HINSTANCE hPrevInstance, // 该应用程序前一个实例的句柄
LPSTR lpszCmdLine, // 命令行参数串
int nCmdShow) // 程序在初始化时如何显示窗口
{
char szAppName[] = "Window";
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
if (!hPrevInstance) {
// 该实例是程序的第一个实例,注册窗口类
wndclass.style = CS_VREDRAW | CS_HREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(hInstance, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;
if (!RegisterClass(&wndclass))
// 如果注册失败
return FALSE;
}
// 对每个实例,创建一个窗口对象
hwnd = CreateWindow(
szAppName,
"Sample Program",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
while(GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
WinMain 函数是Windows 应用程序开始执行时的入口点,它的返回类型为int 。WinMain 函数的作用十分类似于MS-DOS 中的C 应用程序的main 函数。
WinMain 带有四个参数。参数hInstance 和hPrevInstance 是程序的实例句柄。在Windows 环境下,可以运行同一个程序的多个拷贝,每一个拷贝都是该应用程序的一个句柄,每个实例使用一个实例句柄进行标识。hInstance 是标识当前程序的实例的句柄,它的值不会为NULL 。如果在此之前Windows 中已经运行了该程序的另一个实例,则这个实例的句柄由参数hPrevInstace 给出。如果在运行该程序时,Windows 环境中不存在该程序的另一个实例,则hPrevInstance 为NULL 。
我们曾经说过,对同一个类,不能向Windows 注册一次以上。在这个程序中,通过判别hPrevInstance 的值是否为NULL ,来决定是否应向Windows 注册窗口类。这样的程序逻辑保证了只在该程序的第一个实例中注册窗口类。
参数lpszCmdLine 中包含有运行程序时传递给程序的命令行参数。例如,若以这样的命令运行该程序。Sample.exe Programming Windows 。则lpszCmdLine 将指向字符串“Programming Windows” 。
最后一个参数nCmdShow 是一个int 类型的整数,用以说明在程序被装如内存时,Windows 以何种方式显示这个程序的窗口。根据运行程序的方式不同,该参数被设置为SW_SHOWNORMAL 或SW_SHOWMINNOACTIVE ,SW 的含义是“Show Window” (显示窗口),这两个参数的含义在后面介绍。
<!-- /* Font Definitions */ @font-face {font-family:宋体; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-alt:SimSun; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} @font-face {font-family:"/@宋体"; panose-1:2 1 6 0 3 1 1 1 1 1; mso-font-charset:134; mso-generic-font-family:auto; mso-font-pitch:variable; mso-font-signature:3 135135232 16 0 262145 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-parent:""; margin:0cm; margin-bottom:.0001pt; text-align:justify; text-justify:inter-ideograph; mso-pagination:none; font-size:10.5pt; mso-bidi-font-size:12.0pt; font-family:"Times New Roman"; mso-fareast-font-family:宋体; mso-font-kerning:1.0pt;} /* Page Definitions */ @page {mso-page-border-surround-header:no; mso-page-border-surround-footer:no;} @page Section1 {size:595.3pt 841.9pt; margin:72.0pt 90.0pt 72.0pt 90.0pt; mso-header-margin:42.55pt; mso-footer-margin:49.6pt; mso-paper-source:0; layout-grid:15.6pt;} div.Section1 {page:Section1;} -->
在程序Sample.CPP 中,有几个函数我们未曾介绍。表1-3 给出了这些函数的说明。
表1-3-1 ShowWindow 函数
用 途 | 显示或改变给定的窗口 |
| ||||||||
原 型 |
| |||||||||
返回值 | 返回该窗口更新前的窗口状态。对先前可见的窗口,其值为非零。对先前隐藏的窗口,其值为零。 |
|
显示方式(nCmdShow )可以是下列常量之一:
类型 | 说明 |
SW_HIDE | 隐藏该窗口(并是另一个窗口激活) |
SW_MINIMIZE | 使窗口变成图标(并激活窗口管理表的顶层窗口) |
SW_SHOW | 激活一个窗口,并根据其当前的尺寸和位置显示该窗口 |
SW_SHOWMAXIMIZED | 激活并以全屏方式显示一个窗口 |
SW_SHOWMINIMIZED | 激活并以图标方式显示一个窗口 |
SW_SHOWMINNOACTIVE | 以图标方式显示一个窗口,当前活动的窗口仍保持活动 |
SW_SHOWNA | 以当前状态显示一个窗口,当前活动的窗口仍保持活动 |
SW_SHOWNOACTIVE | 以最近的大小和位置显示一个窗口,当前活动的窗口仍保持活动 |
SW_SHOWNORMAL | 激活并显示一个窗口,若其为图标或全屏方式显示,则恢复为它的原始大小和位置 |
SW_RESTORE | 同SH_SHOWNORMAL |
表1-3-2 UpdateWindow 函数
用 途 | 若应用程序的消息队列中存在WM_PAINT 消息(绘制用户区消息),则该函数使Windows 立即调用窗口函数,向其传递WM_PAINT 。否则该函数不作为任何动作。 |
| ||||||
原 型 |
| |||||||
返回值 | 无 |
|
表1-3-3 GetMessage 函数
用 途 | 从应用程序中的消息队列中检索一条消息。 |
| ||||||||||||
原 型 |
| |||||||||||||
返回值 | 在检索出WM_QUIT 消息时,返回零值,在其它情况下返回非零值。 |
|
表1-3-4 DispatchMessage 函数
用 途 | 将消息发送到指定的窗口对象上(窗口函数被调用)。 |
| ||||||
原 型 |
| |||||||
返回值 | 若有一个WM_CHAR 消息被放到应用程序的消息队列中,返回非零,否则返回零。该函数不改变lpMsg 所指向的变量中存储的消息数据。 |
|
Windows 的主函数都是首先以初始化(注册类、创建对象等)这一步开始,而且紧跟着就是消息循环运行这一步。这些步骤对所有的Windows 应用程序都大同小异。Windows 应用程序主要的不同点在窗口函数的定义上,由于一个应用程序所解决的任务不同,它的窗口函数对消息的处理方式也就不相同,因而每个应用程序需要定义不同的窗口函数。
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY:
PostQuitMessage(0):
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
这个窗口函数仅处理一条WM_DESTROY 消息。这条消息是在用户关闭了屏幕上的窗口时,Windows 发送给窗口对象的。该函数对这条消息的处理只是简单地调用Windows 函数PostQuitMessage 。表1-4 给出了函数PostQuitMessage 的说明。当主函数的消息循环中的GetMessage 函数检索出WM_QUIT 消息时,函数GetMessage 返回零,这样,消息循环终止,程序也随之被终止。存储消息数据的变量msg 的wParam 域的值是在调用函数PostQuitMessage 时所提供的实参的值。如果程序正常结束,调用PostQuitMessage 函数时使用零作为该函数的参数,如果需要表示程序由于出现了异常或错误而必须终止时,使用非零值(一般使用-1 )作为该函数的参数。在调用PostQuitMessage 使用的参数值被主函数用语句:
return msg.wParam;
返回给Windows ,供Windows 或其它应用程序使用。因此,我们也称PostQuitMessage 使用的参数为程序的退出码。
表1-4 PostQuitMessage 函数
用 途 | 通知Windows ,应用程序希望中止。它一般用于响应WM_DESTROY 消息。该函数将消息WM_QUIT 消息放入应用程序的消息队列中。 |
| ||||||
原 型 |
| |||||||
返回值 | 无 |
|
小结
本章首先介绍了图形用户界面的优点和面向对象的程序设计方法。从某种意义上说,Windows 是面向对象的,它主要建立在把窗口作为一个对象的概念上。窗口之间通过消息进行消息传递。
Windows 支持直接操作技术。直接操作是对屏幕对象的操作,数据和函数的封装允许该对象自己响应它们接收到的消息。在用户界面上发生的任何事件被作为消息发送给窗口对象。程序员在设计程序时,只须关心一个对象要接受哪些消息和怎样处理这些消息。消息传递工作由Windows 负责。因而,使用Windows 操作环境可以极大地方便程序开发用户界面的工作,并使程序的结构合理、模块化程序高。更重要的是,支持直接操作技术的Windows 支持用户进行有创造性的界面设计。