一、Win32编程中的宏
学习windows编程是一个比较难的过程,主要的障碍是因为windows程序中很多新的东西,我们一下子不能适应,下面我们就来扫清这些障碍:
? 我们在上一课中看到了一个简单的windows程序,大家会发现windows程序中有很多全部大写定义的东西,这就是我们的障碍之一,因为,太多的东西我们以前没有见过。
Windows程序中全大写的东西可以分为三种:
l 第一种:windows定义的变量
比如这句:
LRESULT CALLBACK WinProc(
HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam )
这是一个方法的定义,这和我们以前的程序就有太多的不同。下面的列表是从相应windows的头文件中摘下来的,看完这个后,你是否可以理解上面这句话的意思。
#define CONST const
#define CALLBACK __stdcall
#define WINAPI __stdcall
typedef CONST CHAR * LPCSTR
typedef unsigned long DWORD;
typedef int BOOL;
typedef unsigned char BYTE;
typedef unsigned short WORD;
typedef float FLOAT;
typedef DWORD far * LPDWORD;
typedef void far * LPVOID;
typedef CONST void far * LPCVOID;
typedef int INT;
typedef unsigned int UINT;
typedef UINT WPARAM;
typedef LONG LPARAM;
typedef LONG LRESULT;
其实,windows程序把很多变量进行了重新定义,还记得typedef这个类型定义关键字吧!上面的UINT、WPARAM、LPARAM,其实都是unsigned int(无符号整数),HWND也是无符号整数,后面我们详细介绍HWND,今后,如果大家见到了一个类型是全大写的,那么它一定是在windows的头文件中重新定义了。
还有就是:LRESULT CALLBACK,从上面的表中,大家可以看到他们是返回类型和宏定义,其中,LRESULT是long,而CALLBACK是函数的调用方式__stdcall,是一种调用约定,详见文章《函数调用约定.txt》
l 第二种:windows的宏定义:
在前面,大家看到了__stdcall是一个宏定义,在windows程序中有大量的宏定义,宏定义用于这样几个地方:
n 常量
所有的消息都是常量,比如前面看到的 WM_KEYDOWN、WM_PAINT等,所有的键盘标识,如VK_ESCAPE,还有就是我们在函数调用中用到的 一些参数:如WS_OVERLAPPEDWINDOW, WHITE_BRUSH、IDI_APPLICATION等都是常量
n 简单函数
如:
#define MAKEWORD(a, b) ((WORD)(((BYTE)(a)) | ((WORD)((BYTE)(b))) << 8))
#define MAKELONG(a,b) ((LONG)(((WORD)(a)) | ((DWORD)((WORD)(b))) << 16))
#define LOWORD(l) ((WORD)(l))
#define HIWORD(l) ((WORD)(((DWORD)(l) >> 16) & 0xFFFF))
#define LOBYTE(w) ((BYTE)(w))
#define HIBYTE(w) ((BYTE)(((WORD)(w) >> 8) & 0xFF))
以上的都是简单的函数,如LOWORD是表示取一个DWORD(32位整数)的低16位,而HIWORD取高16位
n 程序结构中用到的宏
在后面的MFC学习中,我们要学习的消息映射中的BEGIN_MESSAGE_MAP、DECLARE_MESSAGE_MAP等就是在程序结构中用到的宏。
l 第三种:是windows中的结构:
windows程序中大量使用结构,对这些结构的掌握程度,可以作为衡量一个程序员对windows程序的熟悉程度,windows程序的结构难,而且结构中的成员非常多,是我们学习的难点。在上一章的程序中,我们看到的结构有:WNDCLASS、MSG等,学习中,大家应该掌握这些常用结构的常用成员的用法。
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;
二、理解句柄
上一章的程序中,我们看到了这样的类型定义:
HINSTANCE hInstanc; //执行实体(程序自身)的句柄
HWND hWnd; //窗口句柄
在windows程序中,有很多以H开头的变量类型,这个H,我们叫HANDLE(句柄)。handle的本意是把柄,把手的意思。是你与操作系统打交道的东西。举个例子:比如你做了亏心事(我说的是比如,呵呵),不幸让我抓住了把柄,那么我让你做什么你就得做什么,因为你的把柄在我这。我们编程的时候也是这样,比如我们要想操纵一个窗口,那我们就必须"抓住它的把柄",只有这样,我们才能改变它的属性,改变它的式样,甚至销毁它。
句柄在Windows中使用非常频繁。句柄是WINDOWS用来标识应用程序所建立或使用对象的唯一整数(通常为32位的整数)。这非常类似我们每个人都有一个身份证号,我们用这个编号区分不同的人,同样windows用不同的句柄来区分和使用各种各样的应用程序实例,如窗口、控件、位图、GDI对象等,如:
HWND 窗口
HINSTANCE 执行程序的实例
HMENU 菜单
HICON 图标
HCURSOR 鼠标光标
HDC 图形设备
HBITMAP 位图
HBRUSH 画刷
HPEN 画笔
我们也可以把Windows中的句柄看成传统C或者MS-DOS程序设计中使用的文件句柄。应用程序几乎总是通过调用一个WINDOWS函数来获得一个句柄,之后其他的WINDOWS函数就可以使用该句柄,以引用相应的对象。句柄的实际值对程序来说是无关紧要的。只是,我们要知道在程序中间怎样去使用这个句柄,以后,在很多的函数中,第一个参数就会是该对象句柄,如:ShowWindow,UpdateWindow等函数的第一个参数都是窗口句柄。
如果想更透彻一点地认识句柄,我可以告诉大家,句柄是一种指向指针的指针。我们知道,所谓指针是一种内存地址。应用程序启动后,组成这个程序的对象是驻留在内存中的。如果简单地理解,似乎我们只要获知这个内存的首地址,那么就可以随时用这个地址访问对象。但是,如果您真的这样认为,那么您就大错特错了。我们知道,Windows是一个以虚拟内存为基础的操作系统,在这种系统环境下,Windows内存管理器经常在内存中来回移动对象,依此来满足各种应用程序的内存需要。对象被移动意味着它的地址变化了。如果地址总是如此变化,我们该到哪里去找该对象呢?
为了解决这个问题,Windows操作系统为各应用程序腾出一些内存地址,用来专门登记各应用对象在内存中的地址变化,而这个内存地址(存储单元的位置)本身是不变的。Windows内存管理器在内存中移动对象的位置后,把对象新的地址告知这个句柄地址来保存。这样我们只需记住这个句柄地址就可以间接地知道对象具体在内存中的哪个位置。这个地址是在对象装载(Load)时由系统分配的,当系统卸载时(Unload)又释放给系统。
句柄地址(稳定)→记载对象在内存中的地址(不稳定)→实际对象
三、消息处理机制
Windows程序中有两个消息队列,分别叫系统消息队列和窗口消息队列,操作系统的执行过程,就是不断产生消息和处理消息的过程,这里的消息又叫事件,前面介绍过windows的事件驱动的概念,核心的思想就是windows程序是通过不断处理消息来完成任务的,那么到底消息是怎么产生的,消息有是什么样子的呢?消息的产生有几个来源:
l 用户操作产生的消息
比如,我们点击了窗口上的一个按钮,按下了键盘上的键等等,都会产生消息。
l 应用程序产生的消息
比如,一个窗口被最大化时,它就会产生重画(WM_PAINT)的消息,后面的课程中会详细说明。也可以使用SendMessage()或PostMessage()主动发送一条消息。PostQuitMessage
PostMessage(hWnd, WM_KEYDOWN, VK_END, 0);
l 操作系统产生的消息
比如,我们在程序中有一个定时器,系统隔相应的时间就会产生WM_TIMER(计时器事件)。
在Win32编程中,消息循环是相当重要的一个概念,看似很难,但是使用起来却是非常简单。在WinMain()函数中,创建了应用程序主窗口之后,就要启动消息循环,其代码如下:
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
Windows应用程序可以接收以各种形式输入的信息,这包括键盘、鼠标动作、记时器产生的消息,也可以是其它应用程序发来的消息等等。Windows系统自动监控所有的输入设备,并将其消息放入该应用程序的消息队列中。
GetMessage()函数则是用来从应用程序的消息队列中按照先进先出的原则将这些消息一个个的取出来,放进一个MSG结构中去。GetMessage()函数原型如下:
BOOL GetMessage(
LPMSG lpMsg, //指向一个MSG结构的指针,用来保存消息
HWND hWnd, //指定哪个窗口的消息将被获取
UINT wMsgFilterMin, //指定获取的主消息值的最小值
UINT wMsgFilterMax //指定获取的主消息值的最大值
);
GetMessage()将获取的消息复制到一个MSG结构中。如果队列中没有任何消息,GetMessage()函数将一直空闲直到队列中又有消息时再返回。如果队列中已有消息,它将取出一个后返回。MSG结构包含了一条Windows消息的完整信息,其定义如下:
typedef struct tagMSG {
HWND hwnd; //接收消息的窗口句柄
UINT message; //主消息值
WPARAM wParam; //副消息值,其具体含义依赖于主消息值
LPARAM lParam; //副消息值,其具体含义依赖于主消息值
DWORD time; //消息被投递的时间
POINT pt; //鼠标的位置
} MSG;
GetMessage()函数获取到一条消息后,会将该消息从消息队列中删除掉。如果不想删除,可以使用PeekMessage()函数,PeekMessage()函数只是从消息队列中复制一条消息,但不会从消息队列中删除该消息。
消息的处理过程如下图所示:
假如,我们在一个窗口中点击了一下鼠标左键,处理过程是这样的:
首先,系统会填充一个MSG消息结构,把当前的窗口句柄放到MSG结构的hwnd成员中去,把消息类型WM_LBUTTONDOWN(鼠标左键按下)放到messge成员中,把鼠标的位置信息放到lParam中,然后把产生的消息放到系统消息队列的尾部,GetMessage函数从消息队列的头部一直不停的取消息,当取到刚才的消息后,会根据消息结构成员的第一个参数即窗口句柄,把消息分发给不同的窗口消息队列,窗口消息队列得到消息后,根据消息的类型,进行不同的处理。
窗口对消息的响应过程,前文有说明,在这里就不多说了。
四、Win32程序对键盘的控制
在一个程序中,我们和程序的交互一般都是通过键盘和鼠标来完成的,下面,我们先来看看对键盘的处理。
l 按键消息
当您按下一个键时,Windows把WM_KEYDOWN或WM_SYSKEYDOWN消息放入有输入焦点的窗口的消息队列;当您释放一个键时,Windows把WM_KEYUP或者WM_SYSKEYUP消息放入消息队列中。
| 键按下 | 键释放 |
非系统键 | WM_KEYDOWN | WM_KEYUP |
系统键 | WM_SYSKEYDOWN | WM_SYSKEYUP |
通常“down(按下)”和“up(放开)”消息总是成对出现的。不过,如果您按住一个键使得自动重复功能生效了,那么当该键最后被释放时,Windows会给窗口消息处理程序发送一系列WM_KEYDOWN(或者WM_SYSKEYDOWN)消息和一个WM_KEYUP(或者WM_SYSKEYUP)消息。像所有放入对列的消息一样,按键消息也有时间信息。通过调用GetMessageTime,您可以获得按下或者释放键的相对时间。
l 系统按键与非系统按键
WM_SYSKEYDOWN和WM_SYSKEYUP中的“SYS”代表“系统”,它表示该按键对Windows系统比对应用程序更加重要。WM_SYSKEYDOWN和WM_SYSKEYUP消息经常由与Alt相组合的按键产生,这些按键启动程序菜单或者系统菜单上的选项,或者用于切换活动窗口等系统功能(Alt-Tab或者Alt-Esc),也可以用作系统菜单加速键(Alt键与一个功能键相结合,例如Alt-F4用于关闭应用程序)。程序通常忽略WM_SYSKEYUP和WM_SYSKEYDOWN消息,并将它们传送到DefWindowProc。由于Windows要处理所有Alt键的功能,所以您无需拦截这些消息。您的窗口消息处理程序将最后收到关于这些按键结果(如菜单选择)的其它消息。如果您想在自己的窗口消息处理程序中加上拦截系统按键的程序码,那么在处理这些消息之后再传送到DefWindowProc,Windows就仍然可以将它们用于通常的目的。
l 虚拟键码
虚拟键码保存在WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN和WM_SYSKEYUP消息的wParam参数中。此代码标识按下或释放的键。
哈,又是「虚拟」,您喜欢这个词吗?虚拟指的是假定存在于思想中而不是现实世界中的一些事物,也只有熟练使用DOS汇编语言编写应用程序的程序写作者才有可能指出,为什么对Windows键盘处理如此基本的键码是虚拟的而不是真实的。
对于早期的程序写作者来说,真实的键码由实际键盘硬件产生。在Windows文件中将这些键码称为「扫描码(scan codes)」。在IBM兼容机种上,扫描码16是Q键,17是W键,18是E、19是R,20是T,21是Y等等。这时您会发现,扫描码是依据键盘的实际布局的。Windows开发者认为这些代码过于与设备相关了,于是他们试图通过定义所谓的虚拟键码,以便经由与设备无关的方式处理键盘。其中一些虚拟键码不能在IBM兼容机种上产生,但可能会在其它制造商生产的键盘中找到,或者在未来的键盘上找到。
您使用的大多数虚拟键码的名称在WINUSER.H表头文件中都定义为以VK_开头。下表列出了与虚拟键相对应的IBM兼容机种键盘上的键。
#define VK_SPACE 0x20
#define VK_PRIOR 0x21
#define VK_NEXT 0x22
#define VK_END 0x23
#define VK_HOME 0x24
#define VK_LEFT 0x25
#define VK_UP 0x26
#define VK_RIGHT 0x27
#define VK_DOWN 0x28
#define VK_F1 0x70
#define VK_F2 0x71
#define VK_F3 0x72
#define VK_F4 0x73
#define VK_F5 0x74
#define VK_F6 0x75
#define VK_F7 0x76
#define VK_F8 0x77
#define VK_F9 0x78
#define VK_F10 0x79
如上表,我们表示向上箭头键,可以用VK_UP,F10可以用VK_F10。
l 组合键
单个键可以用虚拟键码表示,那么,我们如何表示组合键,如Alt+F1呢?如何知道是否按下了位移键(Shift、Ctrl和Alt)或开关键(Caps Lock、Num Lock和Scroll Lock)呢?答案是:调用GetKeyState函数。其原型为:
SHORT GetKeyState( int nVirtKey);
其返回值指明了nVirtKey这个虚拟键码对应的键的状态,如果返回值的高位为1,该键就是按下的,否则就是松开的;如果返回值的低位为1,表明该键是锁定的,否则就不是锁定状态,如大写键,数字键,滚动键。
例如:
SHORT nState = GetKeyState (VK_SHIFT); //sizeof(short) = 2个字节=16位
BOOL bIsPressed = nState & 0x8000; // nState >>15
可以用bIsPressed来判断SHIFT键是否被按下了。
SHORT nState = GetKeyState (VK_ CAPITAL);
BOOL bIsToggled = nState & 0x0001; // nState<<15
可以用bIsToggled来判断大写键是否锁定状态。
通常,您在使用GetKeyState时,会带有虚拟键码VK_SHIFT、VK_CONTROL和VK_MENU(在说明Alt键时调用)。使用GetKeyState时,您也可以用下面的识别字来确定按下的Shift、Ctrl或Alt键是左边的还是右边的:VK_LSHIFT、VK_RSHIFT、VK_LCONTROL、VK_RCONTROL、VK_LMENU、VK_RMENU。这些识别字只用于GetKeyState和GetAsyncKeyState。
如:下面的例子判断Alt+F1
case WM_KEYDOWN:
switch (wParam)
{
case VK_F1:
if (GetKeyState(VK_ MENU)<0)
//Alt和F1被同时按下了
break;
}
l TranslateMessage 函数与WM_KEYDOWN、WM_CHAR消息
TranslateMessage()函数的作用是把能转换成字符消息的虚拟键码消息转换到字符消息,并发送到消息队列。那么,如果在消息循环中包含了TranslateMessage函数,则当用户在键盘上按下A键时,将会产生WM_KEYDOWN和WM_CHAR两个消息;而当按下Shift键时,将只产生WM_KEYDOWN一个消息。
五、Windows对鼠标处理
只要鼠标跨越窗口或者在某窗口中按下鼠标按键,那么窗口消息处理程序就会收到鼠标消息,而不管该窗口是否活动或者是否拥有输入焦点。
Windows为鼠标定义了21种消息,不过,其中有11个消息和显示区域无关(下面称之为「非显示区域」消息,如WM_NCMOUSEMOVE, WM_NCLBUTTONDOWN等10个和WM_NCHITTEST共11个),Windows程序经常忽略这些消息。当鼠标移过窗口的显示区域时,窗口消息处理程序收到WM_MOUSEMOVE消息。当在窗口的显示区域中按下或者释放一个鼠标按键时,窗口消息处理程序会接收到下面这些消息:
键 | 按下 | 释放 | 双击 |
左 | WM_LBUTTONDOWN | WM_ LBUTTONUP | WM_LBUTTONDBLCLK |
中 | WM_MBUTTONDOWN | WM_MBUTTONUP | WM_MBUTTONDBLCLK |
右 | WM_RBUTTONDOWN | WM_RBUTTONUP | WM_RBUTTONDBLCLK |
只有对三键鼠标,窗口消息处理程序才会收到MBUTTON消息;只有对双键或者三键鼠标,才会接收到RBUTTON消息。只有当定义的窗口类别能接收DBLCLK(双击)消息,窗口消息处理程序才能接收到这些消息。
对于所有这些消息来说,其lParam值均含有鼠标的位置:低字节为x座标,高字节为y座标,这两个座标是相对于窗口显示区域左上角的位置。您可以用LOWORD和HIWORD宏来提取这些值:
x = LOWORD (lParam) ;
y = HIWORD (lParam) ;
wParam的值指示鼠标按键以及Shift和Ctrl键的状态。您可以使用表头文件WINUSER.H中定义的位遮罩来测试wParam。MK字首代表「鼠标按键」。
MK_LBUTTON 左键按下了
MK_MBUTTON 中键按下了
MK_RBUTTON 右键按下了
MK_SHIFT Shift键按下了
MK_CONTROL Ctrl键按下了
例如,如果收到了WM_LBUTTONDOWN消息,而且值wparam & MK_SHIFT是TRUE,您就知道左键按下时也同时按下了Shift键。
下面是在按下鼠标左键后,打印鼠标位置信息的代码:
case WM_LBUTTONDOWN:
x = LOWORD(lParam);
y = HIWORD(lParam);
sprintf(szChar, "mouse 位置:x=%d,y=%d",x,y);
break;
解读例子“3WinMain”工程,进一步理解消息和消息的发生时机。
六、ASCII-->MBCS-->Unicode编码
如今,Windows操作系统的使用已经遍及世界,为使Windows操作系统及运行在操作系统上的应用软件更容易被世界所有国籍的用户所使用,需要使Windows及运行在其上的应用程序本地化,即使用用户本民族语言的字符集。字符集的不统一使得本地化变得很困难,这需要对操作系统的源代码根据不同的字符集进行全方位的定制,还要提供API的不同字符集的版本,此外,编写应用软件也要针对不同的字符集开发不同的版本。
在欧美地区,多数使用ASCII编码,字符串被当作一系列以0结尾的单字节字符,这非常自然。使用strlen函数时,会返回一个以0结尾的单字节字符数组中的字符数。单字节字符集包含256个符号。
但是有些语言,如汉字,字符集的符号很多,而单字节字符集最多只能提供256个符号,这是远远不够的。因此,创立了双字节字符集DBCS(double byte character set)来支持这些语言。在双字节字符集中,字符串中的每个字符由1或2字节组成,因此也叫多字节字符集MBCS(multiple byte character set)。由于有些字符是1字节宽,而有些是2字节宽,这使得操作多字节字符串变得非常麻烦,使用strlen操作多字节字符串不能得到字符串的真正长度,而只能得到字符的字节数。
为了更方便地支持软件的国际化,便创立了一个宽字节的字符集标准:Unicode。Unicode这个名称来自三个主要特征:通用(universal)—它解决了世界语言的需要;统一(uniform)—它为了保证效率而使用固定长度的代码;唯一(unique)—字符代码的重复将到了最低点。
Unicode字符串中所有字符都是16位的(2个字节),没有象MBCS字符那样的特殊字节来指示下个字节是同一字符的一部分还是一个新的字符,这意味着可以简单地增减一个指针来访问字符串中的所有字符。
由于Unicode用16位值来表示每个字符,因此可以表示65536个字符,这使得可对世界上所有的书面语言字符进行编码。目前,Unicode已经为Abrabic,Chinese bopomofo,Cyrillie(Russian),Greek,Hebrew,Japanese kane,Korean hangul和English alphabets以及其他文子定义了码点(一个符号在字符集中的位置)。大量的标点符号、数学符号、技术符号、肩头、货币符号、发音符号以及其他符号也包括在这个字符集中。所有的这些符号总共约有34,000个码点,剩余的码点用于将来的扩展。这65536个字符分成若干个区间,如下表所示。
Unicode字符集空间划分
16位代码 | 字符 |
0000-007F | ASCII |
0080-00FF | 拉丁字符 |
0100-017F | 欧洲拉丁 |
0180-01FF | 扩展拉丁 |
0250-02AF | 标号准音标 |
02B0-02FF | 修改字母 |
0300-036F | 通用发音符号 |
0370-03FF | 希腊字母 |
0400-04FF | 西里尔字母 |
0530-058F | 亚美尼亚字母 |
0590-05FF | 希伯莱字母 |
0600-06FF | 阿拉伯字母 |
0900-097F | 天城文字 |
3000-9FFF | 中文、朝鲜文、日文 |
以上是Unicode的概念,Unicode在使用中非常简单,在VC6.0中设置一个项目支持Unicode编码的的一般步骤如下:
1、在“菜单-->Project-->Settings-->C/C++-->Preprocessor”中,将_MBCS预处理定修改为UNICODE;
2、在“菜单-->Tools-->Options-->Debug”中,将“Display unicode strings”选项打勾;
3、设置完成之后,使用时,只需在代码中所有字符串前加上前缀大写L即可:
wchar_t *pszInfo = L"这是Unicode编码"; //L改成TEXT()也可以
CString str = TEXT("这是Unicode编码");
CString str = _T("这是Unicode编码");
在后面的MFC编程中,还会经常看到MessageBoxA和MessageBoxW这样的函数形式,W后缀就表示是Unicode版本的函数,A后缀的则不是。
七、VS2008简介:注释,属性,智能帮助。
八、WinCE简介:SDK for WinCE
九、命名规范和编码规范简介:
匈牙利命名法,nStatus, m_nStatus, iAge, 用Tab代替空格, 操作符前后加空格,单词后加空格,函数间加空行。语义要明确,能不连写的就不要连写,如if (i++ > 0)尽量写成i++; if (i > 0),{}要对称。加注释。
十、软件调试方法:对半排错法,返回值检测法,GetLastError()法,原文对照法,换电脑。
if (uMsg == WM_CHAR) 错写成 if (uMsg = WM_CHAR)后编译不会出错,但很难查错;改成if (WM_CHAR = uMsg)这种写法就会编译出错,很容易发现错误。
作业:
1、移动鼠标,在窗口的标题栏显示当前鼠标位置(API提示:SetWindowText)
2、思考作业:做一个圆形时钟,有走动的时针、分针和秒针。(头文件math.h,API:SetTimer, GetClientRect,InvalidateRect,GetLocalTime,Ellipse,MoveToEx, LineTo, TextOut。消息:WM_CREATE, WM_TIMER, WM_PAINT,结构:POINT,RECT)