Windows程序设计是一种面向对象的编程。Windows操作系统以数据结构的形式定义了大量预定义的对象作为操作系统的数据类型。Windows动态链接库提供了各种各样的API接口函数供Windows应用程序调用。一个Windows应用程序是运行在Windows操作系统之上的。这些API接口函数的调用所实现的功能仅仅是冰山的一角,更多的功能实现都是Windows操作系统内核自动实现的。我们对此一定要有清醒的认识。
本节必须掌握的知识点:
Windows数据类型
匈牙利命名法
窗口
消息
2.1.1 Winodws数据类型
Windows操作系统有自己的数据类型,且往往以对象名的方式定义数据类型的名称。
Windows 支持的数据类型用于定义函数返回值、函数和消息参数以及结构成员。 它们定义这些元素的大小和含义。 有关Windows数据类型的信息可以查阅MSDN,搜索“Windows数据类型”。由于Windows操作系统可以运行各种编程语言的应用程序,因此,也支持汇编语言、C\C++语言的数据类型。当我们调用Windows API函数时自然使用的是Windows定义的数据类型。
下表包含以下数据类型:字符、整型、布尔、指针和句柄。 字符、整数和布尔类型是大多数 C 编译器通用的。 大多数指针类型名称以P或LP前缀开头。句柄引用已加载到内存中的资源。除此之外,我们还会陆续接触大量Windows预定义的对象结构类型。
Windows数据类型参见《附录A》。
提示
请读者不要死记硬背任何内容,否则就失去了学习的兴趣,Windows程序设计将变得枯燥无味。只要可以查阅到的知识都等同于已经记住了。学习的过程关键在于理解、动手实验和勤于思考。为了加深印象,可以认真做好笔记,方便查阅,提高学习效率,仅此而已。
2.1.2 匈牙利命名法
在程序中需要给一些常量值、自定义数据类型、变量、函数命名,随着代码量的增大,对于初学者而言这似乎是一件比较麻烦的事情。其实我们只需要遵循既定的一些命名规范就可以了。
■四种基本的编程命名规范
匈牙利命名法、驼峰式命名法、帕斯卡命名法和下划线命名法。
●匈牙利命名法
匈牙利命名法是由微软的一个匈牙利人发明的,Windows操作系统遵循的就是匈牙利命名法。该命名规范,要求前缀字母用变量类型的缩写,其余部分用变量的英文或英文的缩写,单词第一个字母大写。匈牙利标记法可以避免因数据类型不匹配造成的错误。
1.变量类型:
Ex
int iMyAge; # "i": int
char cMyName[10]; # "c": char
float fManHeight; # "f": float
其他前缀类型还有:
a 数组(Array)
b 布尔值(Boolean)
by 字节(Byte)
c 有符号字符(Char)
cb 无符号字符(Char Byte)
cr 颜色参考值(Color Ref)
cx,cy 坐标差(长度 Short Int)
dw 双字(Double Word)
fn 函数(Function)
h Handle(句柄)
i 整形(Int)
l 长整型(Long Int)
lp 长指针(Long Pointer)
m_ 类成员(Class Member)
n 短整型(Short Int)
np 近程指针(Near Pointer)
p 指针(Pointer)
s 字符串(String)
sz 以 Null 做结尾的字符串型(String with Zero End)
w 字(Word)
举例
szCmdLine的前缀sz表示“以零结尾的字符串”。
hInstance和hPrevInstance中的前缀h表示“句柄”;
iCmdShow中的前缀i表示“整型”。
2.结构类型
当命名结构变量时,可以使用结构名(或结构名称的缩写)的小写形式作为变量名称的前缀或整个变量名。
举例
HELLOWIN.C的WinMain函数中,msg变量就是一个MSG类型的结构,wnclass是一个WNDCLASS类型的结构。
在WndProc函数中,ps是一个PAINTSTRUCT结构,而rect是一个RECT结构。
3.常量类型
Windows中的常量使用大写标识符。这些标识符都在windows头文件中定义的。这些标识符有很多都是以两个或三个字母作为前缀,且其后缀紧跟一个下划线。
举例
CS_HREARAW 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 | 类风格选项 | CLASS |
CW | 创建窗口选项 | CreateWindow |
DT | 文本绘制选项 | DrawText |
IDI | 图标的ID号 | Icon ID |
IDC | 光标的ID号 | Cursor ID |
MB | 消息框选项 | MessageBox |
SND | 声音选项 | Sound |
WM | 窗口消息 | Windows Message |
WS | 窗口风格 | Windows style |
●驼峰式命名法
驼峰式命名法,又叫小驼峰式命名法。该命名规范,要求第一个单词首字母小写,后面其他单词首字母大写,简单粗暴易学易用。
举例
int myAge;
char myName[10];
float manHeight;
●帕斯卡命名法
帕斯卡命名法,又叫大驼峰式命名法。与小驼峰式命名法的最大区别在于,每个单词的第一个字母都要大写。
举例
int MyAge;
char MyName[10];
float ManHeight;
●下划线命名法
下划线命名法要求单词与单词之间通过下划线连接。尤其在宏定义和常量中使用比较多,通过下划线来分割全部都是大写的单词。
举例
int my_age;
char my_name[10];
float man_height;
随着技术的发展,命名规范也在不断的细化。不同的语言不同 IDE 推崇的规范也有所不同,无法评判哪一种最好,可以根据个人习惯或者指定的规范选择合适的命名规则。
例如,谷歌 C++ 编程规范,从项目的命名到文件的命名,再到类和变量以及宏定义的命名都做到了细致入微,充分的结合了下划线命名法与驼峰式命名法。
当然,命名规范并不代表着编程规范,仅仅是编程规范的一部分而已,除去命名规范,还有很多编程上的细节是必须关注的,例如,等号两边留空格还是等号对齐?空行什么时候什么地方留更加符合代码结构?空格什么时候什么地方留更加美观?花括号是否对齐?诸如此类,还有很多,无法一下子全部掌握并应用,但是在编程经验增加的过程中,一定也要不断的留意,自己所在的公司部门使用的是什么样的规范,并不提倡大家练就自己的规范,一定要去融入工作环境的需求。每次去新的工作环境,第一个要看的文档一定是编程规范。
2.1.3 窗口
Windows窗口是用来和用户进行交互信息的,可以在窗口中输入信息,也可以在窗口中输出信息。这些信息包括文本、图形、动画等等。对比控制台黑窗口,Windows窗口可以输入和输出的信息要丰富的多。
■自定义窗口
窗口就是对象,作为程序员来说,就是一块内存。这块内存中通常包含一个标题栏、一个菜单栏、一个工具栏和一个滚动条等其他小的对象(更小的一块内存)。通常我们会使用窗口对象的句柄对窗口进行操作。
如果我们要创建一个窗口,需要告诉Windows系统一些明确的信息:
typedef struct tagWNDCLASSA {
UINT style; //创建一个什么样风格的窗口
WNDPROC lpfnWndProc; //由谁来负责处理窗口的消息—窗口过程
int cbClsExtra; //要根据窗口类结构分配的额外字节数
int cbWndExtra; //在窗口实例之后分配的额外字节数
HINSTANCE hInstance; //窗口所属的进程
HICON hIcon; //窗口的图标
HCURSOR hCursor; //窗口的鼠标
HBRUSH hbrBackground; //窗口的背景颜色
LPCSTR lpszMenuName; //窗口所包含的菜单
LPCSTR lpszClassName; //窗口类名(等同于进程名)
} WNDCLASSA, *PWNDCLASSA, *NPWNDCLASSA, *LPWNDCLASSA;
显然,将创建窗口的所有信息组合到一起就是一个结构体。细心的读者可能会发现,创建窗口的信息还应该包含窗口的尺寸。这是一定需要的,我们将在下一节中使用示例代码来说明。
■预定义对话框窗口
Windows还预定义另一种窗口,可以不带标题栏,而是包含各种各样的控件,例如按钮、列表框、滚动条和文本框等。
对话框窗口可以分为:
●模态对话框:当程序显示一个模态对话框时,用户不能在对话框和该程序的其他窗口之间进行切换。用户必须先明确地终止该对话框。这通常由单击OK或Cancel按钮来实现。但是当对话框正在显示时,用户可以切换到其他的程序。有些对话框(所谓“系统模态”)则连这种切换都不允许。在Windows 中,用户必须先结束系统模态对话框才可以进行其他操作。典型的默认对话框就是MessageBox框了。
●非模态对话框:非模态对话框允许用户在对话框和窗口之间,以及在对话框和其他程序之间进行切换。非模态对话框因此更接近于程序自定义的正常弹出窗口。如果在一段时间内,一直要显示某个对话框,用户更倾向于使用非模态对话框,因为它更方便。典型的非模态对话框如查找和替换对话框。
●公用对话框:为了方便用户快速熟悉和使用Windows应用程序,从Windows 3.1开始,提供了统一标准的“公用对话框库”(common dialogue box library)。这个库包括了一些函数,可以用来激活打开和存储文件、查找和替换、选择颜色、选择字体(所有这些将在本书第十一章中演示)以及打印(将在第十三章中演示)的对话框。
■预定义子窗口控件
子窗口控件也是一种窗口,也是Windows预定义的窗口类型。虽然我们也可以自定义子窗口控件,但是直接使用Windows提供的标准子窗口控件更为方便快捷。Windows预定义了子窗口控件的窗口类和窗口过程,并且提供多种不同风格的子窗口样试供我们选择。常用的子窗口控件包括按钮类的BUTTON控件、静态文本控件、滚动条控件、文本编辑控件和列表框控件。我们将在第八章详细讲解子窗口控件。
2.1.4 消息
Windows操作系统是一种消息驱动的系统,这句话应该被每一位从事Windows程序设计的开发人员所熟知。那么什么是消息呢?也许有很多已经从事多年Windows程序开发的程序员都无法说清楚。这是因为他们并不清楚消息传递的实现过程。
■消息的本质
消息其实非常简单,就是函数调用时传递的参数,仅此而已。只不过我们并不能从代码中真实的感受到消息传递的整个过程,所以会觉得不可琢磨。Windows操作系统中有很多很多消息,这些消息可以是键盘消息、鼠标消息、滚动条消息、字符消息、绘图消息、控件消息、菜单消息或者是系统消息等等。键盘消息和鼠标消息肯定是由用户操作键盘和鼠标产生的,这是人机交互的需要。其他消息又是如何产生的呢?如果我们把消息看作是参数,那么这些消息肯定是在调用函数调用传递参数时产生的,消息本身就是参数。例如绘图时,我们将绘图消息WM_PAINT作为实参传递给GDI绘图函数。不论什么样的消息都是由操作系统捕捉并分发的。API函数的调用和执行也是由Windows操作系统实现的,这些都是潜藏在水面以下的冰山。正是因为我们看不见,所以才会感到困惑。
既然消息就是一个参数,我们来了解一下消息本身:
typedef struct tagMSG {
HWND hwnd; //接收消息的窗口的句柄
UINT message; //指定消息类型-消息ID,例如 WM_PAINT、WM_CLOSE等。
WPARAM wParam; //针对特定消息的附加信息
LPARAM lParam; //详细的附加信息,通常与wParam一起配合使用
DWORD time; //消息发布的时间
POINT pt; //鼠标在发布消息的时候在屏幕上的位置(鼠标消息特性)
DWORD lPrivate; //仅在某些消息类型中使用,其他情况下是保留项
} MSG, *PMSG, *NPMSG, *LPMSG;
我们用一个结构体来描述消息,显然消息是一个对象(Windows系统处处是对象)。消息结构体中包含7个成员,我们只需要关系前面四个成员,后面三个成员仅提供给操作系统使用。
hwnd:接收消息的窗口的句柄,所有的消息都是发送给指定窗口的。
message:指定消息类型-消息ID,Windows系统使用一个32位无符号整数标识消息。
wParam 和lParam:针对特定消息的附加信息。仅有一个消息ID远远不够的,还需要借助于32位的wParam 和32位的lParam传递具体的信息。
我们在Windows程序中通常会将上述四个成员作为参数传递。
举例
例如发送一个消息:
LRESULT SendMessage(
[in] HWND hWnd, //接收消息的窗口句柄
[in] UINT Msg, //消息ID
[in] WPARAM wParam, //附加参数,其他的消息特定信息
[in] LPARAM lParam //附加参数,其他的消息特定信息
);
很显然,这里的消息就是参数。在Windows系统内部产生和传递消息也是如此。
Windows系统中的消息虽然非常多,但是绝大多数消息都是由Windows系统按照默认方式处理的,不需要我们劳心费力,目的当然是不想为难我们这些开发者了。
■窗口过程
在Winodws程序中,我们需要写一个函数来处理各种消息,这个函数称为窗口过程。它是一个回调函数。正常情况下,在Windows程序中是由我们来调用操作系统的API函数,回调的意思是由操作系统反过来调用我们写的窗口过程来处理消息。窗口过程只需要处理我们关心的一小部分消息,绝大多数消息都是交给默认的Windows系统的窗口过程来处理。本书的学习过程也就是学习各种各样消息处理的过程。
■队列消息
Windows操作系统的消息可以分为队列消息和非队列消息。Windows操作系统中有一个总消息队列,所有进程的队列消息都会被送入总消息队列中。然后再根据消息所属的窗口,将消息分发到各个窗口队列。还记得消息结构的第一个成员就是接收消息的窗口。以此判断该消息属于哪个窗口。
什么样的消息属于队列消息呢?鼠标消息、键盘消息肯定是属于队列消息,一定会被送入消息队列。这是由于用户通过鼠标、键盘与窗口进行交互产生的消息,而用户的操作是有先后顺序的,因此需要使用队列进行同步操作。因此可以认为,凡是与窗口进行交互或需要保持同步操作的消息都是队列消息,一定会被送上消息队列。队列消息都是调用PostMessage函数将消息送入指定窗口的消息队列。
BOOL PostMessage( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
消息队列存在于线程中,理论上每个线程都可以创建一个消息队列。
假如一个Windows进程只有一个主线程,那么非常简单,所有的窗口消息都是送入主线程的消息队列。
假如一个Windows进程同时拥有多个线程,其中只有一个主线程负责处理窗口消息,其它线程执行其他任务。意思是在多线程的Windows程序中,同样只有一个消息队列,所有消息都交给主线程处理,不存在多个线程间的冲突问题。因此,虽然窗口是不包含消息队列的,但是我们将线程消息队列称为窗口消息队列也没什么错。
在Windows程序设计中,需要遵循“十分之一秒原则”。“十分之一秒原则”是指用户在做出操作后,至少应在0.1秒之内得到某种形式的反馈,这样才能让用户感觉到系统在对他的操作给予响应。如果一个任务执行时间超过0.1秒,我们为了良好的用户体验,就需要考虑新建一个线程来单独处理这个任务。我们将在本书的第二十章详细讲解进程与线程。
windows应用程序中一般都包含一小段称为“消息循环”的代码,该段代码用于从消息队列中检索消息,并将其分发给相应的窗口过程。其他非队列消息则不经过消息队列直接发送给窗口过程。
■非队列消息
除了队列消息之外,剩下的消息就都属于非队列消息了。不需要被送入消息队列的消息可以通过SendMessage函数将消息直接发送给负责处理消息的窗口过程。
在Windows程序设计中,SendMessage函数被用于将一个消息发送给指定窗口或线程,它会等待消息处理完毕才会返回,也就是说它是一个同步或者说阻塞的函数。这是一个非常重要的用来在窗口间或线程间通信的函数。SendMessage函数可以在同一个线程内、同一个进程内的线程间或者不同进程的线程间通过消息进行通信。
下面是SendMessage函数的定义:
LRESULT SendMessage( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
SendMessage函数应该是使用最频繁的一个函数了,在后面的章节中我们将深有体会。