《白手起家Win32SDK应用程序》
目 录
《白手起家Win32SDK应用程序》
第一篇、预备知识
第二篇、创建Win32工程和主函数
第三篇、增加一个回调函数
第四篇、注册一个窗口类
第五篇、利用已注册的窗口类来创建一个窗口
第六篇、显示你创建的窗口
第七篇、获取消息及对消息缺省处理
第八篇、关闭窗口的同时退出程序
第九篇、窗口标题栏上显示自定义图标(手动编辑代码)
第一篇、预备知识
白云小飞
1 说在前面
由于VC6及MFC的特点,我们许多人从标准C++学习到VC6MFC应用程序的编程学习的过度会有一个很大的夸跃,从而感到非常的吃力。
究其原因之一:MFC类库设计虽然精巧,但我们在使用MFC设计程序时,会发现MFC到处是API函数的影子。MFC并没有象Delphi的VCL类库,VB的控件库一样封装得让人几乎完全不用知道还有Win32API函数及其操作原理,所以要想利用VC6的MFC编程,我认为就一定要先学习如何直接用Win32API函数来编程。对API编程中的一些关键的概念和原理要有一定认识,这样才会有一个比较平滑的过渡。以上就是我写这个系列的初衷。
2 我假设你已有的知识:
这里我假设你已经掌握了如下的知识,如果你在如下方面知识有点不太清楚,那要去补一补罗,否则你看到相关的内容时会有麻烦的。
下面说是我对你知识的假设:
2.1 Windows系统的文件、文件夹、路径的概念
2.2 C语言的基本知识(基本以等级考试二级C语言为准,还要有所扩充)
2.2.1 指针的概念。
2.2.2 函数指针概念。
2.2.3 各种自定义类型(最重要的是struct类型)的概念。
2.2.4 要知道函数的各种参数传递形式(值、地址、引用传递)。
2.2.5 typedef及其应用。
2.2.6 #include及其应用。
2.2.7 十进制、二进制、十六进制。
2.2.8 按位与、或、非运算的实质。
2.2.9 宏定义概念、使用及意义。
(每个人总是学完了C或C++语法后才会开始用VC6进行Windows编程学习的。但是你的基础又是如何呢?这是一个关键。因此我对你的C知识做了具体的假设。)
2.3 会安装VC6.0并安装到一台机上
2.4 VC6编译界面的各组成部分及基本操作(至少会用VC6写控制台程序)。
2.5 VC6调试中至少要会设置断点哦。
(呵呵!我的要求不过份吧!)
3 还必须预备的知识:
以上知识是你看本系列的前提,不过我还要给你预备一下我们再这个阶段学习中会遇到的新东西。
3.1 你将会接触到的Win32API函数库:
以前的DOS下或Windows的控制台程序下,你要在显示器上输出文字,要用printf(),或cout的函数对象来完。但如果你要显示一个图形或图象或为你的程序设计一个图形化的操作界面等等的,那可就惨了,一切都要你自已完成。复杂得很了!(唉!谁叫DOS是字符界面的操作系统呢!)
现在好了,在Windows下编程你可就轻松得多了。因为Windows操作系统都为我们准备好了,它提供给我们多达数千个函数(啊!我要昏倒了。这么多的函数要学。),我们通过这些函数来操作Windows系统提供给我们的各种功能。比如我要在桌面上创建并显示一个窗口。就只要调用几个相关的被称为API的函数,让Windows来帮助我们完成这些事。我们是通过这些函数与Windows系统交互的,所以这些函数被称作Win32应用程序接口函数,简称Win32API函数。
请不用害怕哟!其实,这么多的函数我们不必都马上一一学过,只要掌握了不多的具有代表性的函数的使用方法,并知道大体API函数都提供了哪些功能就可以了。以后要用时再去查。
Window拥有现成的各种各样的系统功能,供我们的程序调用。那么又是通过什么方式来调用这些系统功能呢?原来,Window还现成提供一个接口,好让我们的程序来使用这些系统功能,这个结口就是Win32API函数了(注:API是应用程序接口的英文缩写)。Win32API函数是我们的应用程序与Windows系统交互的唯一途径。
我并不打算这时就介绍任何一个具体的API函数。你现在只要知道你又要接触一个函数库了——被称为Win32API的函数库,如同你以前所学的C/C++函数库。
哈哈,这真是太好了,我们不用再象DOS一样,自已来完成程序界面的绘制了。我们现在又增加一个全新的函数库,只要调用几个相关API函数,剩下的一切由Windows来完成就可以啦!(当然还有很多其它功能。)
3.2 “新”的数据类型:
学完C、C++之后,我们就可以开始进入VC6的Windows编程学习了。但是在接下来的学习中我们会发现在Windows编程中有许多“新”的数据类型。看下面:
BOOL、BYTE、INT、UINT、WORD、DWORD、FLOAT、CHAR、LPSTR、HINSTANCE、HWND、HMENU、HICON等等。
你看这些大写的数据类型,你以前有见过吗?还有很多哦!我们以后的学习过程中还会见到的。(呵呵!你可要有思想准备了!)
这真是让我们初学者迷惑呀!难道VC6中对C/C++的基本数据类型又有重大的扩充了吗?
其实不用害怕,只是用新瓶装旧酒而已了。在VC6的windef.h头文件中已有这些定义:
typedef int BOOL;
typedef unsigned char BYTE;
typedef int INT;
typedef unsigned int UINT;
typedef unsigned short WORD;
typedef unsigned long DWORD;
typedef float FLOAT;
在winnt.h中有
typedef long LONG;
typedef char CHAR;
typedef CHAR *LPSTR, *PSTR;
你看其中(粗体字)CHAR只不过是char 的别名而已,也就是说它们是等价的。只要你包含了相关的头文件,然后你就可以这样申明一个变量:
INT i; //等同于int i;
CHAR a; //等同于char a;
LPSTR pa; //等同于char *pa;
明白了吗?
我想你一定会问:为什么要这样转义呢?我们直接用int 、unsighed int、char等等不就行了吗?我一句两句也说不清,你只要知道,微软这样做一定是要道理的。
哦!还有这些HINSTANCE、HWND、HMENU、HICON我没说呢!今后你还会见到许多这样以H为开头的数据类型,下面就让我在下一节的“句柄”概念中说给你听。
3.3 “句柄”概念
由windows系统创建出来的或加载的对象(如应用程序进程、线程、窗口、菜单、图标、光标等等的对象),windows系统都会分配给它们一个唯一的标识值,作为这些对象的标志,称之为句柄。我们程序中对这些对象的操作其实就是对其句柄的操作。请记住,句柄就是这些对象的“代号”了。
在编程序中,我们需要用相应的句柄变量来保存这些句柄值,那么用什么类型的句柄变量呢?
就是我们前面提到过的HINSTANCE、HWND。
像其它变量一样(如:int a;)申明句柄变量,如下:
HINSTANCE hst; //hst变量可以保存某个应用程序实例(即一个进程)的句柄。
HWND hwFirst; //hwFrist变量可以保存某个窗体句柄。
HMENU hMenu; //hMenu变量可以保存某个菜单句柄。
HICON hIcon; //hIcon变量可以保存某个图标句柄。
具体的使用让我以后再慢慢与你道来啦。
那么这些类型的实质又是什么?
目前,它们都只是一个int类型(小语:我听说微软也许以后会改变它的类型)。不过不管怎样,你现在只要把HINSTANCE、HWND、HMENU、HICON当做是一个独立的数据类型就可以了。
3.4 消息标识
Windows系统是一个基于消息的系统。这样的机制导致我们的程序与以往DOS下的程序流程会有很大的不同。(这可是很考我们的智慧喽!)
从软件使用者角度看一个Win32窗口程序运行的过程:
1) 我们运行一个应用程序,程序创建并显示一个我们想要的程序窗口。
2) 当我们对窗口进行操作时(如单击、双击、右击、按下键盘、最大化、最小化、关闭窗口等等),程序会完成特定的操作,如:单击最大化、最小化按钮时,窗口会最大化、最小化操作;对窗口中菜单项的选取时,会完成该菜单的相应功能。
从程序员的角度看一个Win32窗口程序运行的过程:
1) 我们运行一个应用程序,程序中我们通过Win32API函数创建并显示一个我们想要的程序窗口。(由我们的程序来调用函数实现)
2) 当我们对窗口进行操作时(如单击、双击、右击、按下键盘、最大化、最小化、关闭窗口等等),窗口会自动产生一系列相应的消息(这是由操作系统实现的)。
3) 具体地讲:当我们改变窗口大小时,会产生WM_SIZE消息;单击关闭按钮关闭窗口时,会产生WM_CLOSE消息;选取某一菜单项时,会产生WM_COMMAND消息;按下键盘时,会产生WM_CHAR、WM_KEYDOWN、WM_KEYUP消息;单击鼠标左键时,会产生WM_LBUTTONUP、WM_LBUTTONDOWN消息等等。啊,很多很多,我也不必全部罗列出来了。(我说过了,这些都是由操作系统实现的)
4) windows系统会将这些消息排入我们窗口所在线程的消息队列中(你会明白线程是什么吗?)(也由Window操作系统实现),这样我们的程序才有机会获取并处理这些产生的消息。
5) 我们的程序可以通过Window操作系统提供的API函数来获取这些消息及相关的信息。然后通过我们学过的条件判断语句来判断是什么消息及其相关的操作信息并可编写相应的程序代码,从而实现对窗口操作的不同反应。(由我们的程序来实现)
看上述的过程描述,你可能要有点的抽象思维能力了。你现在只要有对程序流程有如上的大体认知就可以了。慢慢地我会将上述流程变成确实的程序代码噢!
(等等,还是有个问题:这些WM_CLOSE、WM_COMMAND、WM_CHAR、WM_KEYDOWN、WM_KEYUP、WM_LBUTTONUP、WM_LBUTTONDOWN等等的以WM_开头的消息到底又是什么东西呢?)
看VC6的头文件winuser.h中的片段:
……
#define WM_CLOSE 0x0010
……
#define WM_LBUTTONDOWN 0x0201
#define WM_LBUTTONUP 0x0202
#define WM_LBUTTONDBLCLK 0x0203
#define WM_RBUTTONDOWN 0x0204
……
#define WM_KEYDOWN 0x0100
#define WM_KEYUP 0x0101
#define WM_CHAR 0x0102
……
#define WM_INITDIALOG 0x0110
#define WM_COMMAND 0x0111
#define WM_SYSCOMMAND 0x0112
……
哦!这些WM_开头的所谓的消息只不过是一系列16进制整型数值的符号常量而已。每一个不同的整型数值代表着一个窗口某一操作的标识,因此我们将这些数值或者说以WM_开头的符号常量称之为消息了。
也就说,我们在窗口中作各种不同的操作,Windows系统会产生各种相应的数值。我们就是通过条件语句比较这些数值来判断我们在窗口中所做的操作的。
3.5 资源标识
(你看我没完没了地介绍一个个概念,觉得烦不烦?不用你说,我自已也有点烦了。唉!不过这些似乎是必要的,所以我不得不坚持下去。不过,还好,剩下的不多了。)
那么VC6中资源是什么一种概念呢?
我们的程序中可能要用到各种图标(*.ico文件)、各种形状的鼠标(*.cur文件)、各种图像(*.bmp/*.gif等等)、各种声音(*.wav等)、各种菜单……,这些就是我们这里所说的资源了。
每一个要用到资源,我们都要给它分配一个编号或名称,作为这个资源的标识。之后我们的程序只是通过这个编号或名称来访问这些资源了。所以这些编号或名称我们称之为资源标识。好了,现在你也只要有了一个大体的映象就可以了,具体的形式和应用让我慢慢再与你说了。
(各位可以提出你的疑问,白云小飞一定会尽力回复的。)
啊!终于结束冗长的概念解说了,看到这里,请先回顾一下我们前面讲的东西。然后嘛——我们可以开工啦!。
第二篇、创建Win32工程和主函数
白云小飞
1 在D:\创建一个空的工程(工程名为MyApp)
要编写一个程序,我们就要首先用VC6应用程序向导创建一个工程,下面我将给你创建一个空工程(也就是没有任何源文件及代码的工程)
1.1 操作:
=>文件->新建…->”工程”标签->位置:”D:\”(你可以设置你想要创建的位置)->工程名:MyApp(你可以自己指定其它名)->选择”创建新的工作空间”->确定->一个空工程->完成
1.2 请查看指定位置下生成的文件:打开D:\MyApp
我们发现,它在D:\下生成了一个MyApp文件夹。
并在MyApp文件夹下生成了几个文件,如下:
MyApp.dsp
MyApp.dsw
MyApp.ncb
MyApp.opt
其中,MyApp.dsp是项目文件和MyApp.dsw是工作区文件,它们是不能删除。
项目文件的作用:生成一个EXE或DLL程序的所有相关源文件、有头文件的位置信息都记录在MyApp.dsp项目文件中。
工作区文件的作用:如果一个复杂的软件工程可能是由多个EXE和多个DLL程序组成,这样每个项目文件的位置信息又记录在MyApp.dsw工作区文件中。
当然,我们的这个工程只有一个EXE程序,所以只有一个项目,这个项目文件MyApp.dsp也同样要记录在MyApp.dsw中了。
MyApp.ncb和MyApp.opt虽删除后会自动生成,但是还是请不要这样做哦!以后我还会说它们的作用。
我们下次要编辑源程序时,只要打开工作区文件MyApp.dsw就可。
2 在D:\MyApp下创建一个C++源文件,文件名为MyAppMain(当然你也可以自己定义一个文件名),并同时加入到MyApp工程中
2.1 操作:
=>文件->新建…->”文件”标签->选”添加到工程”->选”MyApp”->文件名:MyAppMain->”位置”默认->确定
2.2 查看指定生成的文件:
可以看到,在D:\MyApp文件夹下生成了MyAppMain.cpp源文件。
3 在MyAppMain.cpp文件中输入一个主函数
3.1 代码如下:
#include <windows.h>
#include<windowsx.h>
int WINAPI WinMain(HINSTANCE hinstance,
HINSTANCE hprevinstance,
LPSTR lpcmdline,
int ncmdshow)
{
// 将会在这里输入主函数的代码
return 0;
}
3.2 包含必要的头文件:
首先你只要包含下面两个头文件就可,因为它们已经包含了绝大多数的MyApp应用程序必要的头文件。
#include <windows.h>
#include<windowsx.h>
3.3 主函数名:
函数头定义的书写格式很有趣:
int WINAPI WinMain(HINSTANCE hinstance,
HINSTANCE hprevinstance,
LPSTR lpcmdline,
int ncmdshow)
写成四行,其实没什么,只不过写在一行里太长了,如下:
int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hprevinstance, LPSTR lpcmdline, int ncmdshow)
并且写成上面的四行反而可读性更强,所以以后你会经常看到这样书写的。
WinMain函数相当于Dos下的main函数,Windows应用程序的主函数不用main而是用WinMain。一个程序必有并只能有一个WinMain函数。这个主函数可以写在本工程中的任何一个.cpp文件中。
3.4 参数:
hinstance:类型是HINSTANCE,这种类型就是前面我们可是提到过的应用程序句柄啦。hinstance的值就是你的当前应用程序进程的句柄。
你的程序每次运行,它的hinstance值是不会一样的。不过我们并不关心hinstance的值是多少。我们只要知道hinstance里的值就是代表本应用程序进程的句柄值就可以了。
我们等一会儿就会用到它了,请看好哦!
hprevinstance:现在已经不用了。我们可以完全不理它。
lpcmdline:这是一个命令行参数。与main(int argc,char **argv)中的命令行参数相似。由于与本题无关,我们也不可完全不理它,对此不再进一步讨论下去。
至于它的数据类型LPSTR,我在前面已经说明了,它其实就是char *类型。
ncmdshow:一个整型值。启动过程中被传递给应用程序,带有如何打开应用程序主窗口的信息。这样,程序的使用者有了一点点的控制应用程序如何启动的能力了。作为一个程序员,如果想忽略它也可以,想使用它也行。哦,那我们这里也先忽略它了。
3.5 返回值:
是一个int值,当我们的程序正常结束退出时,一定要返回一个0值。所以我们的WinMain函数体内最后有return 0;了
3.6 函数名前的WINAPI是什么?
最后还有一个要说明的——就是WINAPI。这是什么呢?
在VC6的Windef.h头文件中是如下定义的:
#define WINAPI __stdcall
也就是WINAPI等于__stdcall了。
你知道吗,凡是提供给Windows操作系统调用的函数都得是__stdcall调用的。WinMain主函数当然是由Windows系统调用的,因此定义WinMain前要用__stdcall(即WINAPI)修饰。(你会明白函数调用方式的具体含义吗?不知道也没关系,现在只要记得WINAPI要放在WinMain前就行了。)
另外说明一点,int 与WINAPI哪个在前哪个在后都是可以的。
好了,我们现在对主函数定义处的各个部分有了必要的了解。你可以休息一会儿,然后回顾一下我们前面操作的过程:
想想我们在本篇中做了哪些事?
生成了哪些文件?
这些文件是什么作用的?
主函数定义的各个部分是什么作用?
如果你已经对上面所做的感到比较清晰,那太好了,Come on!我们继续吧!
第三篇、增加一个回调函数
白云小飞 1 请再创建一个函数。 我猜也许你还会继续下一个疑问:那我又如何具体地使用这个函数呢?慢慢来,现在你只要输入到你的.cpp文件中就可以了。 LRESULT CALLBACK WinProc(HWND hwnd, int WINAPI WinMain(HINSTANCE hinstance, |
第四篇、注册一个窗口类
白云小飞
一 创建并显示一个窗口的“遐想”。
? 首先,要显示的窗口在哪里呢?
要想显示你自己的窗口,显然你得事先创建一个自己的窗口。当你想要一个窗口时,Window系统才会为你创建窗口。不要时,Window再销毁这个窗口。噢,这是多么相当然的一种机制啊,你说是吧!也就是说我们得先创建一个窗口才能显示。(否则哪里来的窗口给你显示呢?)
? 其次,你想创建什么样的窗口呢?
那么,创建什么样的窗口呢?创建前,Window系统可不知道你要的是什么类型的窗口啊(比如标题栏上显示什么图标,鼠标形状是什么,窗口背景颜色等等)。这些类型信息应在你创建前事先告诉Window系统。可以采用这种方法:就是我们事先写一份要创建窗口的类型申请表,提交(注册)给Window系统。然后在创建时,可以让Windows按这个申请表来创建你所要的窗口了。也就是说我们还应该先提交一个申请表,申请成功后再根据这个表创建一个窗口。
依据上述的理由,我假想了以下几个步骤要做:
第一. 你得先填写一份你想创建的窗口类的“申请表”。
第二. 然后将这“申请表”通过一个API函数提交给Windows系统(即注册到Windows系统中)。
第三. 如果提交(注册)成功,就说明Window系统通过了你的“申请表”,Windows系统中就有了一份你所申请的窗口类(注:这个注册成功的已经注册在系统中的“申请表”我们称之为窗口类)。这样你就可以利用这个申请成功的窗口类,通过一个专门的API函数让Windows系统创建一个或多个的同一窗口类的窗口。
第四. 创建成功后,我们有了窗口。但是,虽然窗口已存在在内存中,并不一定就马上显示在屏幕上(这根据你的意愿了),所以之后的某时你可以用一个API函数来让Windows系统显示刚才创建的窗口。
Window系统就是这样设计的噢!(呵呵,这样设计不算坏,我可以接受。)
以上就是创建一个窗口的大致过程。请记住,在Window系统下你的程序要显示一个你想自定的窗口总是得经历如此步骤的。还要记住一点,我们的代码只是通过调用Window系统所提供的API函数来完成对窗口间接的管理。实际上窗口的管理操作都由Window系统直接完成的。
好,让我们在本篇中先来完成第一、二步骤吧!
二 第一步 填写一份“申请表”
1 用什么来作为这种“申请表”呢?
我想,C语言中的struct结构体类型的变量来充当这个“申请表”是再合适不过的了。呵呵,真是这样,VC6下早已为我们准备好了这样的“申请表”了。那就是WNDCLASSEX(我们称之为窗口类结构体)。
看看这个WNDCLASSEX结构体的底细吧!
在windef.h中已经有定义:(下面所列的与真实文件中会有点不同,但目前你只要理解我这份就可以了。)
typedef struct tagWNDCLASSEX {
UINT cbSize; //用来保存本结构体的所占字节数
UINT style; //窗口类型风格。
//比如,可设置“若移动窗口宽度时,则刷新整个窗口。
WNDPROC lpfnWndProc; //回调函数指针,用以指向前面那个回调函数。
int cbClsExtra; //略,我们可不必使用它,只要赋值为0就可
int cbWndExtra; //略,同上
HINSTANCE hInstance; //窗口所属的应用程序实例句柄。
HICON hIcon; //大图标,(这个图标会显示在窗口的哪里呢?)
HICON hIconSm; //小图标,(这个又会显示在窗口的哪里呢?)
HCURSOR hCursor; //鼠标句柄,用以指定鼠标移入窗口时的样式
HBRUSH hbrBackground; //用来刷背景颜色的画刷句柄,
//窗口的颜色就会按这个显示。
LPCSTR lpszMenuName; //用来指向菜单资源名称字符串的指针,
//可让你的窗口有一个菜单。
LPCSTR lpszClassName; //用来指向窗口类名字符串的指针
//作为这个窗口类的标识
} WNDCLASSEX
哦!是一个有12个成员变量的struct结构体的类型。(看来我们又要一个一个耐心地搞定它。)
2 创建一个“申请表”
现在就先让我用这个WNDCLASSEX结构体申明一个变量吧!(看粗体字部分)
HWND hWnd;
MSG msg;
WNDCLASSEX winclass; //变量名可由你自己定。
顺便解释一下:
这里我顺便地同时申明了hWnd的窗口句柄变量,用以保存将要创建出来的窗口的句柄。
还申明了一个msg:它的类型是MSG,也是一个已预定义好的结构体,用来保存窗口操作的各种消息信息。
好了好了,以后再说这两个东东啦。
3 填写“申请表”
接下来就到了填写这个“申请表”的时候啦!也就是要对winclass这个结构体变量的各成员赋值。
winclass.cbSize = sizeof (WNDCLASSEX);
这是将本结构体的大小(占字节数)赋值给其成员变量cbSize,一定要这样做哦!好处是:以后Windows系统只要访问cbSize就可知道wndclass的大小了,就不必每次都要用sizeof(WNDCLASSEX)来获取大小。(哦,不错,这真是一个很值得学习的做法)
winclass.stype= CS_VREDRAW | CS_HREDRAW | CS_OWNDC | CS_DBLCLICKS;
我们这里将这四个值同时赋值给成员变量stype。
新问题:CS_VREDRAW、CS_HREDRAW、CS_OWNDC、CS_DBLCLICKS是什么啊!
stype是一个UINT即unsigned int的类型(共32位二进制位)。stype的可能值为如下的组合:
表二.3 窗口类的类型标志
标识 描述
CS_HREDRAW 若移动或改变了窗口宽度,则刷新整个窗口
CS_VREDRAW 若移动或改变了窗口高度,则刷新整个窗口
CS_OWNDC 为该类中的每个窗口分配一个单值的设备描述表
CS_DBLCLKS 当用户双击鼠标时向窗口程序发送一个双击的信息,同时,光标位于属 于该类的窗口中
CS_PARENTDC 略
CS_NOCLOSE 禁止系统菜单上的关闭命令
CS_SAVEBITS 略
关于更详细的类型标志描述,请自行参考相关书籍。
你可能看完后仍还不能完全明白这些窗口类的类型标志的意思。没有关系,它不妨碍我们对整个程序框架的理解(我们可是要善于把握轻重缓急噢!),现在你只要按我上面赋值就可以了。
还有一个问题:符号“|”是按位或的运算符,即表示CS_VREDRAW、CS_HREDRAW、CS_OWNDC、CS_DBLCLICKS的值同时都赋值给stype
(只能解释到这里了,如果还不太明白“|”的运算,请自己去看关于C语言中“按位或”的相关知识了。)
winclass.lpfnWndProc=WinProc;
lpfnWndProc是一个函数指针(第一篇我已经说过你要对函数指针有一定认识。),它是WNDPROC函数类型,这个函数类型在winuser.h文件中已有定义。如下:
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);
发现没有,WNDPROC类型的格式与我在第三篇中写的那个自定义窗口过程函数的格式是一样的。现在我们将我们的这个函数WinPro的地址赋值给lpfnWndProc函数指针变量。
将前面的那个回调函数WinProc的首地址赋值给成员变量lpfnWndProc。这样这个窗口类就与这个回调函数相关联起来了。
这里,你可能有个疑问:为窗口类指定一个窗口过程函数有什么用处呢?这是Window系统的消息机制的关键。但我现在不想说太多,因为真的现在无法说清楚的。只有等到我将整个程序框架建立起来后,我们再来理解它吧。(唉!我知道这种说了却又没能完全说清楚的情况不应该有,但真的是不得不为之啊!)
wndclass.hInstance = hinstance;
第二篇中可是说过主函数参数中hinstance的值就是本应用程序实例句柄值,我们现在将这个hinstance的句柄值赋值给wndclass.hInstance。这样,由这个窗口类结构创建的窗口就与本程序实例相关联了。
wndclass.cbClsExtra=0;
wndclass.cbWndExtra =0;
我们现在只是对cbClsExtra和cbWndExtra两个成员简单赋值为0。你现在也不用管它们是做什么的。可以保证绝大多数的程序只要这样就行。
wndclass.hCursor =LoadCursor(NULL, IDC_ARROW);
hCursor是一个鼠标光标句柄变量,用来为这个窗口类指定一个鼠标句柄值(也就是想让这个窗口显示一个什么样的鼠标形状了)。LoadIcon(NULL, IDC_ARROW);这个函数是加载一个光标给hCursor的。你现在只要造就输入就可,其它的以后我再说了。
wndclass.hIcon = LoadIcon(NULL,IDI_APPLICATION);
wndclass.hIconSm = LoadIcon(NULL,IDI_APPLICATION);
hIcon和hIconSm都是图标句柄变量,前一个是用以指定大图标,后一个是用以指定小图标。
hIconSm中设置的图标会显示在你的窗口的标题栏左边。
hIcon中设置的图标是会显示在哪儿啊?不好意思,我真的不知道。如果有人知道,请告之,本人非常感谢!
关于LoadIcon函数,它是加载一个图标给hIcon及hIconSm的。现在你只要造就输入就可以了。
wndclass.lpszMenuName =NULL;
lpszMenuName是一个字符串指针,这是用来指向一个菜单资源名字符串。我们这个窗口暂时不要菜单,所以这里先指定NULL。
wndclass.hbrBackground =(HBRUSH)GetStockObject(WHITE_BRUSH);
hbrBackground是一个画刷句柄变量(类型HBRUSH)(呵呵,又一个新句柄。)。窗口客户区的背景颜色是用这个变量指定的画刷来刷的。看不明白(HBRUSH)GetStockObject(BLACK_BRUSH)这个函数没有关系,这里它提供了一个白色画刷给我们。你现在只要造就输入就可以。
wndclass.lpszClassName ="WINCLASS1";
lpszClassName是一个字符串指针,它是为这个窗口类(申请表)指定个字符串,这个字符串作为这个窗口类的名称。这里我们指定”WINCLASS1”字符串作为这个窗口类的名称。(要注意一点的是,一个程序中如果申请(注册)了多个窗口类,那么每个窗口类的这个字段值是不能相同的。)
到此为止,我们终于(有点稀里糊涂地)结束了这个结构体各成员变量的赋值了。以上成员变量赋值代码汇总如下:
wndclass.cbSize = sizeof(WNDCLASSEX);
wndclass.cbClsExtra=0;
wndclass.cbWndExtra =0;
wndclass.lpfnWndProc = WinProc;
wndclass.style = CS_VREDRAW | CS_HREDRAW | CS_OWNDC | CS_DBLCLKS;
wndclass.hInstance = hinstance;
wndclass.hCursor =LoadCursor(NULL, IDC_ARROW);
wndclass.hIcon = LoadIcon(NULL,IDI_APPLICATION);
wndclass.hIconSm = LoadIcon(NULL,IDI_APPLICATION);
wndclass.lpszMenuName = NULL;
wndclass.hbrBackground =(HBRUSH) GetStockObject(BLACK_BRUSH);
wndclass.lpszClassName ="WINCLASS1";
好了,我们已经填好了我们想要创建的窗口的“申请表”了,现在可以透一口气了!
然后……再继续,因为我们的任务还没用完成呢!
三 第二步 提交“申请表”给Window系统
到了提交“申请表”的时候了。我们现在要将wndclass中的各项数据信息提交(注册)到Windows系统中。有一个API函数是专门用来注册窗口类信息的,原型如下:
ATOM WINAPI RegisterClassEx(CONST WNDCLASSEX *);
参数是WNDCLASSEX *类型的指针,只要将要注册的WNDCLASSEX结构体变量地址代入就行。
返回值是一个ATOM类型,其实它也是一个unsigned short类型的别名而已。如果注册不成功则程序返回0值,否则表示注册成功。(注:我们暂时不关心其它返回值的意义。)
所以我们注册窗口类的代码可以如下:
if (!RegisterClassEx(&winclass))
return 0; //不成功程序就直接结束了
四 最后总括代码
本篇中所增加的全部代码现总括如下(粗体字部分):
WINAPI int WinMain(HINSTANCE hinstance,
HINSTANCE hprevinstance,
LPSTR lpcmdline,
int ncmdshow)
{
HWND hWnd; //窗口句柄变量
MSG msg; //消息结构体变量
WNDCLASSEX wndclass; //窗口类结构体变量
//以下是对wndclass各成员赋值
wndclass.cbSize = sizeof(WNDCLASSEX);
wndclass.cbClsExtra=0;
wndclass.cbWndExtra =0;
wndclass.lpfnWndProc = WinProc;
wndclass.style = CS_VREDRAW | CS_HREDRAW | CS_OWNDC | CS_DBLCLKS;
wndclass.hInstance = hinstance;
wndclass.hCursor =LoadCursor(NULL, IDC_ARROW);
wndclass.hIcon =LoadIcon(NULL,IDI_APPLICATION);
wndclass.hIconSm = LoadIcon(NULL,IDI_APPLICATION) ;
wndclass.lpszMenuName = NULL;
wndclass.hbrBackground =(HBRUSH) GetStockObject(BLACK_BRUSH);
wndclass.lpszClassName =WND_CLS_NAME;
//下面是利用wndcalss结构体的信息注册一个窗口类
if (!RegisterClassEx(&wndclass))
return 0;
//……
return 0;
}
第五篇、利用已注册的窗口类来创建一个窗口
白云小飞
一 用CreateWindowEx函数来创建窗口
1 参数及返回值说明:
上篇中我们完成了向Windows系统进行窗口的“申请”工作(即注册一个窗口类)。本篇就是要用这个窗口类来创建一个窗口。
下面这个API函数就是专门用来创建一个窗口的:
HWND WINAPI CreateWindowEx(
DWORD dwExStyle,
LPCSTR lpClassName,
LPCSTR lpWindowName,
DWORD dwStyle,
int X, int Y,
int nWidth, int nHeight,
HWND hWndParent ,
HMENU hMenu,
HINSTANCE hInstance,
LPVOID lpParam);
这是一个拥有一大串参数的函数。(唉!我又要一个个地介绍了。)
DWORD dwExStyle:这是用来指定扩展样式标志,绝大多数情况,我们只要指定为NULL,所以我不想多说。
LPCSTR lpClassName:我们要用前篇中注册的窗口类来完成创建窗口,所以lpCassName所指字符串值要与前篇中注册时所用的窗口类名值相同(即wndclass.lpszClassName的值)。本例中就是"WINCLASS1"字符串值。
LPCSTR lpWindowName:此指针所指的字符串会显示在标题栏上(即标题栏文字)。
DWORD dwStyle:这是用来指定窗口外观类型的。以下是它可能的值(部分):
表 dwStyle可以设置的值
WS_POPUP 弹出式窗口
WS_OVERLAPPED 带有标题栏和边界的重叠式窗口。
WS_OVERLAPPEDWINDOW 具有WS_OVERLAPPED、WS_CAPTION、WS_SYSMENU、WS_THICK、FRAME、WS_MINIMIZEBOX、WS_MAXIMIZEOBX样式的重叠式窗口
WS_VISIBLE 创建之后就立即显示窗口
…… (还有很多其它值呢!请自行参看其它参考书)
还记得上篇中注册窗口类时窗口类结构体成员中有一项:
wndclass.style = CS_VREDRAW | CS_HREDRAW | CS_OWNDC | CS_DBLCLKS;
你可不要搞糊涂啦!wndclass.style所指的是针对窗口内在特性的类型,而CreateWindowEx参数中的dwStyle窗口类型是窗口外观的类型。
int X, int Y,:这是指定相对于桌面的窗口左上角位置(坐标)。
int nWidth, int nHeight,:指定窗口的宽度与高度。
(上面这四个参数应该好理解吧!)
HWND hWndParent :父窗口句柄,不明白它的作用吧?现在你只要赋值为NULL就可。
HMENU hMenu:你还记得吗?我们在前篇的窗口类结构体变量赋值中有这么一句: wndclass.lpszMenuName =NULL; 它是可以为窗口指定一个菜单的。
CreateWindowEx函数的这个hMenu菜单句柄(这又是一个句柄噢!)也可以为窗口指定一个菜单,它将代替前面的设置。不过我们现在暂时不要菜单,所以也赋值为NULL。
HINSTANCE hInstance:要与wndclass.hInstance值相同,本应用程序的句柄代入这里。
LPVOID lpParam:高级特征,现在我们只要设置为NULL就行。
(噢,好不容易介绍完这些参数了。)
返回值:如果窗口成功创建出来,则返回这个窗口的句柄,如果创建不成功,则返回0值。
2 具体实现代码:
下面我就给出具体的创建窗口代码:
hWnd=CreateWindowEx(NULL,”WINCLASS1”,
"这是我的第一个窗口",
WS_OVERLAPPEDWINDOW ,
0, 0,
400,400,
NULL,
NULL,
hinstance,
NULL );
if (!hWnd) //这里应该判断是否创建成功
return 0;
3 调试及结果:
程序写到这里似乎窗口就可以创建出来了。那就让我们调试调试?OK,Let’s go!请在
if (!hWnd)
return 0; //设断点
中的return 0处设置一个断点。看看你的程序会不会执行到这个return 0;当创建不成功,则hWnd==0,程序会执行到这个return 0。反之,程序成功地创建了这个窗口。
(哦!我并不知道你是否会用VC6来设置断点并进行调试。
只要光标放在return 0 处按F9就可以设置此处的断点。如果你再按F9一次,则会取消这个断点。
设置完断点后,按F5编译运行这个程序试试。)
矣?不太对劲啊,调试中我们发现hWnd值为0,即说明窗口并没有创建成功。
(在这里请再按F5,可以从断点处继续运行,程序结束了。)
原因是?……
二 CreateWindowEx的一个条件:调用缺省窗口过程DefWindowProc函数
1 在WinProc中增加调用DefWindowProc函数
原因是:我们还要在这个窗口的自定义窗口过程WinProc中增加如下代码(黑体字部分):
LRESULT CALLBACK WinProc(HWND hwnd,
UINT msg,
WPARAM wparam,
LPARAM lparam)
{
//……
return DefWindowProc(hwnd, msg, wparam, lparam);
}
真让我疑惑啊!
CreateWindowEx函数创建一个窗口与这个DefWindowProc函数什么关系。
又为什么要把这个函数写在WinProc回调函数里呢?
首先,看DefWindowProc在Winuser.h中的原型定义:
LRESULT CALLBACK DefWindowProc(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam);
你有没有看出,它的函数格式与我们自定义WinProc回调函数的格式一完全一样的?只不过DefWindowProc是Window系统提供给我们的一个API函数。
那么在整个调用CreateWindowEx函数中到底发生了什么事啊?让我来告诉你吧!
2 原因何在?
从我们的自定义窗口过程WinProc函数说起:
当应用程序运行时,用户对该程序窗口的操作,都会自动产生一系列的消息。比如创建一个窗口会产生WM_CREATE消息;移动窗口会产生WM_MOVE消息;按下键盘时会产生WM_KEYDOWN消息;关闭窗口时会产生WM_CLOSE消息等等。也就是说,在执行CreateWindowEx函数期间会产生若干个消息(先别管都是些什么消息)。
Window系统会将这些由一系列的消息添加到该进程的消息队例中(噢,你可要有一点想象力哟!)。在CreateWindowEx函数中同时会调用我们写的WinProc函数若干次,并把消息的各项信息通过WinPro函数参数传递进来。(我再说一遍:你得有点想象力啊)
说白了,就是在执行CreateWindowEx创建窗口过程中会引发对WinProc函数的多次调用。
创建窗口过程中要用到缺省窗口过程函数DefWindowProc:
前面说过,在执行CreateWindowEx创建窗口过程中会引发对WinProc函数的多次调用。嘿嘿,这可不是可有可无的调用啊!在这里,我们要让缺省窗口过程DefWindowProc来完成一些默认的消息处理操作。你不必知道它做了什么事,只要把这一切消息都“扔”给它就行啦!只有让 DefWindowProc函数完成必要的消息处理,CreateWindowEx函数才能全程地完成窗口的创建(否则,嘿嘿!窗口的创建必将失败。)。所以我们添加了调用DefWindowProc的代码。(DefWindowProc的返回值返回的是对消息处理的结果,我们再将它作为WinProc的返回值。)
3 本篇增加的全部代码
最后,本篇中所增加的全部代码总括如下(黑体部分):
LRESULT CALLBACK WinProc(HWND hwnd,
UINT msg,
WPARAM wparam,
LPARAM lparam)
{
return DefWindowProc(hwnd, msg, wparam, lparam);
}
int WINAPI WinMain(HINSTANCE hinstance,
HINSTANCE hprevinstance,
LPSTR lpcmdline,
int ncmdshow)
{
HWND hWnd;
MSG msg;
WNDCLASSEX wndclass;
//…… 这里省略了前篇所述的注册窗口类的过程
hWnd=CreateWindowEx(NULL,WND_CLS_NAME,
"这是我的第一个窗口",
WS_OVERLAPPEDWINDOW|WS_VISIBLE ,
CW_USEDEFAULT, 0,
400,400,
NULL,
NULL,
hinstance,
NULL );
if (!hWnd)
return 0;
//……
return 0;
}
4 具体分析
现在现具体地分析一下调用CreateWindowEx函数的过程:
1. 首先从调用CreateWindowEx开始,程序进入了CreateWindowEx函数体内的代码(就是Window系统的代码)。
2. 在此期间,CreateWindowEx会产生若干个消息(我们不用管是什么消息)。
3. 每当一个消息产生后,CreateWindowEx会自动调用我们自己写的WinProc函数,将消息信息传递进来。
4. 这些消息我们自己不作处理而是直接在WinProc调用缺省窗口过程DefWindowProc来作缺省处理(你不用去管DefWindowProc做了什么),并返回处理结果。
5. 每次WinProc结束后回到CreateWindowEx代码处,CreateWindowEx会根据返回值果进行判断,完成最终的窗口创建。
怪不得嘛!没有调用DefWindowProc这个函数,CreateWindowEx无法完成全部的窗口创建过程,所以最终创建窗口失败了。
5 再调试
很好!现在再按前面讲的调试步骤试试看。
if (!hWnd)
return 0; //这里设断点
程序不再执行这个return 0;了。太好啦!这说明窗口创建成功了。当然并没把窗口显示出来,所以你只看到程序停都不停一下就马上就结束了。
关于窗口的显示我就留到下一篇了。
第六篇、显示你创建的窗口
白云小飞
哈!到了显示窗口的时候啦!
看,下面这个函数就是用来显示窗口的:
BOOL ShowWindow( HWND hWnd, int nCmdShow);
一 ShowWindow函数的参数及返回值
hWnd就是你要显示的窗口的句柄:
nCmdShow是窗口的显示方式,其可能的值如下:
SW_HIDE 隐藏应用程序窗口
SW_SHOWNORMAL 激活并显示窗口,如果窗口被最大化或最不化,系统恢复窗口到原始大小和位置(与SW_RESTORE)
SW_RESTORE 同SW_SHOWNORMAL
SW_NORMAL
SW_SHOWMINIMIZED 激活并最小化窗口
SW_SHOWMAXIMIZED 激活并最大化窗口
SW_SHOW 激活窗口,并按其当前大小和位置显示
SW_MAXINIZE 最大化应用程序窗口
SW_MINIMIZE 最不化应用程序窗口
SW_SHOWNOACTIVATE 按最近大小和位置显示窗口,但不改变激活特性
SW_SHOWMINNOACTIVE 最小化窗口,但不改变其激活特性
SW_SHOWNA 按当前大小各位置显示窗口,但不改变其激活特性
返回值:成功则返回TRUE,不成功则返回FALSE。
二 先来两个例子
例一:现假设已经创建了一个窗口,并且该窗口句柄已保存在hWnd变量中。我希望将窗口最大化并使该窗口为当前窗口(即激活该窗口)。请写出ShowWindow函数的具体实现代码。(注:可不必处理它的返回值)
解:ShowWindow(hWnd, SW_SHOWMAXIMIZED);
例二:我希望隐藏一个原来是显示着的窗口。该窗口的句柄在hWnd变量中。应如何写ShowWindow函数呢?
解:ShowWindow(hWnd, SW_HIDE);
三 本系列程序中的实现
(快一点喽,我想马上动手啦!)
好,我们现在继续完善我们的这个Win32SDK程序吧!
具体代码如下(注意粗体字部分):
int WINAPI WinMain(HINSTANCE hinstance,
HINSTANCE hprevinstance,
LPSTR lpcmdline,
int ncmdshow)
{
HWND hWnd;
MSG msg;
WNDCLASSEX wndclass;
//…… 这里省略了前面所述的注册窗口类的过程
//
hWnd=CreateWindowEx(NULL,WND_CLS_NAME,
"这是我的第一个窗口",
WS_OVERLAPPEDWINDOW|WS_VISIBLE ,
CW_USEDEFAULT, 0,
400,400,
NULL,
NULL,
hinstance,
NULL );
if (!hWnd)
return 0;
ShowWindow(hWnd, ncmdshow); //本篇只添加这一句
return 0; //这里设置一个断点,调试看看
}
一点说明:这里的ncmdshow就是WinMain主函数中的传入参数ncmdshow。当我们通过双击*.exe文件来执行程序时,ncmdshow里的值就会是SW_SHOWNORMAL。
四 调试看看
太棒啦!我终于可以亲眼所见我的窗口了!
好,让我们来调试一下吧!看看程序运行后会发生什么。(如果不这样调试而只是运行它,那么还来不及等你看清窗口,程序就会马上结束的。这不用我说明原因吧!)
请在最后一个return 0处设置一个断点(光标放在return 0处,按F9)。然后F5运行程序。
程序暂停在最后一个return 0处。
然后最小化桌面上所有其它无关窗口(包括VC6窗口)(这样才能看到这个程序的窗口噢!)。
认真研究,我发现目前的代码有以下几个问题:
1. 窗口虽然显示,但窗口不能自动被激活(即成为当前窗口)。只有最小化桌面上其它应用程序的窗口后,才能看到我们的这个窗口。(注意:在ShowWindow(hWnd, ncmdshow);函数中ncmdshow值我说过是SW_SHOWNORMAL值,应该会将窗口激活才对啊!这可是个大问题。)
2. 窗口无法进行调整大小,移动位置等的操作。
不过我并不想在这里解决这个问题(为什么?),那是因为这是一个大问题,它将引出Window程序的一个重要机制——消息处理机制。嘿嘿,到了关键一击的时候了!我请你务必带着这两个问题看下篇吧!
第七篇、获取消息及对消息缺省处理
白云小飞
一 重提上篇的问题:
还记得在上一篇的最后,经过调试后我们发现的问题吗?
1. 窗口不能自动被激活(即成为当前窗口)。只有最小化其它应用程序的窗口后,才能看到我们的这个窗口。(注意:在ShowWindow(hWnd, ncmdshow);函数中ncmdshow值我说过是SW_SHOWNORMAL值,应该会将窗口激活才对啊!)
2. 无法进行调整窗口大小,移动窗口位置等等的所有对窗口的操作。
你认为是什么原因呢?
二 且听我说:
1. ShowWindow(hWnd, SW_SHOWNORMAL)函数作用虽然是显示窗口并激活窗口。它确实完成了窗口的显示,但是,它并没有去激活窗口,只是发了一系列与激活窗口有关的消息,这才是ShowWindow(…)函数所做的事。(那又由谁来激活呢?)
2. 真正激活窗口的任务是由DefWindowProc( )完成的。哦,也就是说我们得让DefWindowProc( )收到ShowWindow( )产生的消息并根据消息来执行相应任务。
3. 但ShowWindow( )产生的消息只是加入到该线程的消息队列中,并没有自动地去调用WinProc( )的回调函数对消息处理。因此,DefWindowProc( )也就没被调用,自然地就没法激活窗口了。(请边看代码边分析我的这段话吧!呵呵,你得有点想像力了!)
4. 哦,这不由地让我想起前面的创建窗口函数CreateWindowEx( ) 。这两个函数在执行中都会产生若干的消息。但CreateWindowEx会自动地调用了WinProc( ) 函数,而ShowWindow( )没有这样。
5. 同理,我们在窗口中的各种操作(调整大小、移动窗口等等),虽然会产生各种消息,但操作系统也只是把它们排入我们线程的消息队列中,并不自动去调用 Proc,所以同样的理由,DefWindowProc也没有去处理我们的消息了,窗口也就没法完成如调整大小,移动等等的操作了(你要知道:这些操作的实现都是由DefWindowProc( )完成的)。(还有,由于消息因为没有得到处理,就会一直留在消息队列中,这样,你的线程消息队列中的消息将会越积越多噢!)
看来,上述的两个问题都源于同一个原因:那就是对窗口操作产生的消息只是排入消息队列中,操作系统并没有自动地调用WinProc回调函数来处理我们的窗口消息。
哦!那我们只有自力更生了。
我猜你一定迫切想知道如何解决吧!
三 介绍三个函数给你认识:
我们到了一个Window窗口程序框架最关键的部分了——消息处理机制。
消息队列中的本窗口大量消息并不会被自动取出,也没有自动地调用WinProc函数对消息加以处理,但是,Window系统提供了三个API函数给我们,让我们自己去完成这件事。看吧!
GetMessage( …);
TranslateMessage(…);
DispatchMessage(…);
下面就让我分别对这三个函数解释解释。
1 GetMessage( …)
原型如下:
BOOL GetMessage(
LPMSG lpMsg,
HWND hWnd ,
UINT wMsgFilterMin,
UINT wMsgFilterMax);
功能:这个API函数用来从消息队列中“摘取”一个消息信息放到lpMsg所指的变量里。(注:如果所取窗口的消息队列中没有消息,则程序会暂停在GetMessage(…) 函数里,不会返回。)
参数及返回值:
LPMSG lpMsg:是传出参数。消息结构MSG的指针。如果该函数执行成功,则从消息队列中“摘”取的一个消息信息会放入lpMsg所指的MSG结构变量中。
在Winuser.h中有定义如下:
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;
其中的成员变量message里才是我所说的WM_SIZE、WM_COMMAND、WM_QUIT等等消息标识。
hwnd中是这个消息所在的窗口句柄。
好了,其它成员变量我就暂时省略不说。
HWND hWnd:传入参数。你要获取你程序中哪个窗口的消息,那就把相应的窗口句柄代入其中。
UINT wMsgFilterMin,UINT wMsgFilterMax:这两个参数我就不介绍。你只要用0值代入就可了。
返回值:如果取的是WM_QUIT消息,则返回值为0,如果取的是其它消息,返回值为非0值。WM_QUIT消息是退出程序的消息。当我们想让程序退出时,我们就可以发一个WM_QUIT消息,让GetMessage返回0值。
2 TranslateMessage(…)
原型:BOOL TranslateMessage( CONST MSG *lpMsg);
功能:对GetMessage取得的MSG消息结构中的信息进行必要的预处理(你可不用管它做了什么。)。
参数:
CONST MSG *lpMsg:它是用来对你取的消息结构MSG变量进行必要的预处理。GetMessage函数取得的消息,要经过TranslateMessage处理一下,然后才可以传给DispatchMessage函数,因此,TranslateMessage必放在GetMessage与DispatchMessage之间。
返回值:它虽然有一个返回值,我们总是忽略它,所以我也就不说了。
3 DispatchMessage(…)
原型: LONG DispatchMessage( CONST MSG *lpMsg);
功能:用来完成调用WinPro回调函数并把由GetMessage取得的消息结构MSG变量中的信息传递给WinPro回调函数:(原型如下)
参数及返回值:
MSG *lpMsg :传入参数,MSG消息结构体类型指针,指向你已经取出的消息结构体变量。
返回值:同上,我们总是忽略它。
四 Come on,行动起来吧!
看我添加的代码(粗体部分):
LRESULT CALLBACK WinProc(HWND hwnd,
UINT msg,
WPARAM wparam,
LPARAM lparam)
{
//这里可以添加你的消息处理代码
return DefWindowProc(hwnd, msg, wparam, lparam);
}
int WINAPI WinMain(HINSTANCE hinstance,
HINSTANCE hprevinstance,
LPSTR lpcmdline,
int ncmdshow)
{
HWND hWnd;
MSG msg;
WNDCLASSEX wndclass;
//…… 这里省略了前面所述的注册窗口类的过程
//
hWnd=CreateWindowEx(NULL,WND_CLS_NAME,
"这是我的第一个窗口",
WS_OVERLAPPEDWINDOW|WS_VISIBLE ,
CW_USEDEFAULT, 0,
400,400,
NULL,
NULL,
hinstance,
NULL );
if (!hWnd)
return 0;
ShowWindow(hWnd, ncmdshow);
while(GetMessage(&msg, NULL, 0, 0)) //获取一个消息,成功后会放在msg中。
{
TranslateMessage(&msg); //消息进行必要的处理转换。
DispatchMessage(&msg); //调用WinProc,将msg的各项信息传递给WinProc
}
return 0;
}
看清楚了:这是一个消息循环。只有当GetMessage(…)收到一个WM_QUIT时返回0值,程序才会退出。
五 程序流程分析(粗体部分)
1. 当用户进行如调整窗口大小,移动窗口位置等等的操作时。会产生不同的消息。(当然包括了ShowWindow( )函数产生的若干个消息。
2. Window系统会将这些消息排入本线程的消息队列中。(在操作系统中完成)
3. 只要该窗口消息队列中有消息,我们的GetMessage(&msg,NULL,0,0)就会从消息队列中摘取一个消息的信息,填入msg结构体变量中,如果不是WM_QUIT则返回非零值,就执行循环体。(注意:如果消息队列中没有任何消息可取时,则程序会停在GetMessage函数里,直到消息队列中有了消息,GetMessage函数才会取一个消息信息并返回。)
4. 用TranslateMessage(&msg)对msg中的数据进行预处理(你先不必知道它具体做了什么,但不要忘记这个函数。)。
5. 然后是调用DespatchMessage(&msg),这个函数里会调用WinProc,并将msg中的数据通过WinProc的参数传递给WinProc;
6. 程序转入执行WinProc回调函数体内的代码。
7. 看代码处,WinProc此时只有一句 return DefWindowProc(hwnd, msg, wparam, lparam);这里,我们只是将WinProc传入的参数原样地传给了API函数DefWindowProc。所有的消息都让DefWindowProc进行缺省默认的处理。(你不用理会DefWindowProc都做了些什么。)
8. DefWindowProc完成一个消息处理后,返回消息处理的结果。
9. 我们的WinProc也原样地将DefWindowProc返回值返回。
10. WinProc执行完成后,程序又返回到DispatchMessage(&msg)函数体内。(因为是在DispathMessage( )中调用WinProc的。)
11. 退出DispatchMessage(&msg);函数后程序又执行下一个循环。即
while(GetMessage(&msg, NULL, 0, 0))
又取下一个消息,进行下一个消息的处理。
12. 直到GetMessage “摘取”到了退出程序的消息WM_QUIT,返回零值,退出循环,结束程序。
(哦,整个流程是通过我们的程序与Window系统相互协作来完成的。你可要多加理解罗!)
六 调试这个程序
不设任何断点,按F5运行程序,
看来一切正常。可以移动窗口;可以调整窗口大小;可以最大化最小化;总之可以进行窗口的基本的操作了。(这些动作都是由DefWindowProc来完成的噢!)
哈!我们可是渐入佳境啦!这就是Window系统消息处理机制带给我们的成果。
不过……只是……有一个问题。
你有没有注意到,点击窗口右上角的关闭按钮时,窗口是关闭了,但程序并没有退出(看来点击关闭按钮时并没有产生WM_QUIT的消息。)。
你只能点击VC菜单的:Debug->Stop Debugging来退出程序了。
(欲知此为如何,请听下回分解!)
第八篇、关闭窗口的同时退出程序
白云小飞
还记得上篇中最后说到一个问题吗?当我们关闭程序窗口时,窗口确实是关闭了,可是程序并没有退出啊。为什么呢???
一. 理解程序的退出条件:
首先,我们要先明白程序退出的条件,看上篇中的这段代码:
while(GetMessage(&msg, NULL, 0, 0)) //获取一个消息,成功后会放在msg中。
{
TranslateMessage(&msg); //消息进行必要的预处理转换。
DispatchMessage(&msg); //调用WinProc回调函数,将msg传递给WinProc函数
}
如果程序一直在这个消息循环中,程序就没能退出。只有当GetMessage收到一个WM_QUIT的消息,则返回值才会为零,退出循环,程序得以结束。(这个道理应该好理解吧?)
二. 点关闭按钮时,发生了什么
当我们点窗口右上角的关闭按钮时,到底发生了什么事呢?(请边看源代码,边体会下面的分析噢!)
第一. 它并没有(或最终没有导致)发出WM_QUIT的消息。因此GetMessage函数不会收到WM_QUIT消息,就没法跳出循环了。(那么又产生了什么消息呢?)
第二. 点关闭按钮时,产生WM_CLOSE的消息。GetMessage会收到WM_CLOSE消息的MSG结构信息。
第三. 按前篇所述的消息处理流程可知:DespatchMessage会调用WinProc回调函数,并把WM_CLOSE消息的相关信息传递给WinProc函数参数中。
第四. 现在我们的WinProc里只有一句:
LRESULT CALLBACK WinProc(HWND hwnd,
UINT msg,
WPARAM wparam,
LPARAM lparam)
{
//这里可以添加你的消息处理代码
return DefWindowProc(hwnd, msg, wparam, lparam);
}
它将WM_CLOSE继续传递给缺省窗口过程函数DefWindowProc。
第五. 在DefWindowProc函数里,判断是WM_CLOSE消息后,就会对参数hwnd所代表的窗口进行销毁。(看吧,销毁窗口的事也是由DefWindowProc来完成了。)
第六. 成功销毁窗口后,DefWindowProc里接着还会发一个WM_DESTROY的消息到消息队列中(表示说窗口已经被销毁了)。然后DefWindowProc函数才结束。
第七. 回到我们的消息循环的GetMessage函数。这个函数又会获得WM_DESTROY消息的信息,开始了下一个消息处理过程。
第八. 这个WM_DESTROY可在WinProc函数中由我们处理。但在WinProc函数体的代码中我们没有自己去处理它。仍然是让DefWindowProc去处理。
第九. 然而,DefWindowProc只是简单地把它给“扔掉”了。
第十. 整个点窗口右上角的关闭按钮作所的所有动作就这样完成了。
你看,上述中,上述程序始终没有产生WM_QUIT的消息,所以窗口确实是销毁了,但程序并没有退出这个消息处理循环。
哦,怪不得我们的程序没法结束了。(那该怎么办呢?)
三. 如何使程序结束。
退出程序的三点说明:
1. 我们希望是通过单击这个窗口右上角的关闭按钮时来退出程序。
2. 应该在窗口成功销毁后,才让程序退出。
3. 只要让程序产生一个WM_QUIT消息,就可以退出循环而结束程序。
终上所述,程序应在收到WM_DESTROY消息后才能退出程序。因为WM_DESTROY消息表示窗口已经销毁。
那么我们又如何才能产生一个WM_QUIT的消息呢?用下面这个--API函数:
PostQuitMessage(0);
参数代入0值就可。它将产生一个WM_QUIT消息。WM_QUIT消息最终会被GetMessage函数“摘取”到并返回0值。从而退出循环,结束程序。看我实现代码:
LRESULT CALLBACK WinProc(HWND hwnd,
UINT msg,
WPARAM wparam,
LPARAM lparam)
{
switch (msg) //msg中保存的就是正要处理的消息
{
case WM_DESTROY: //这是我们自行处理的第一个消息
{
PostQuitMessage(0); //发出一个WM_QUIT消息
return 0; //然后直接返回。
}break;
default:break;
}
return DefWindowProc(hwnd, msg, wparam, lparam);
}
int WINAPI WinMain(HINSTANCE hinstance,
HINSTANCE hprevinstance,
LPSTR lpcmdline,
int ncmdshow)
{
HWND hWnd;
MSG msg;
WNDCLASSEX wndclass;
//…… 这里省略了前面所述的注册窗口类的过程
//
hWnd=CreateWindowEx(NULL,WND_CLS_NAME,
"这是我的第一个窗口",
WS_OVERLAPPEDWINDOW|WS_VISIBLE ,
CW_USEDEFAULT, 0,
400,400,
NULL,
NULL,
hinstance,
NULL );
if (!hWnd)
return 0;
ShowWindow(hWnd, ncmdshow);
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
WinProc函数参数中的UINT msg就是程序传递进来的消息标识。我们只要判断msg是否为WM_DESTROY消息,如果是就发一个WM_QUIT消息。
补充说明一点:
WinProc函数参数中有一个msg变量,而WinMain函数中也定义了一个msg。不要把它们给混了啊!它们可是不同的变量啊!WinMain中定义的msg类型是MSG,在Winuser.h中已定义如下:
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;
你看,它是一个结构体。而WinProc参数中的msg是一个UINT类型,它其实只是WinProc函数里的msg结构成员message的值。
你看看DispatchMessage(&msg);是如何传递这个msg给WinProc的:
它在调用WinProc时,会——
将msg.hwnd值传递给WinProc参数中的HWND hwnd;
将msg.message值传递给WinProc参数中的UINT msg;
将msg.wParam传递给WinProc参数中的WPARAM wParam;
将msg.lParam值传递给WinProc参数中的LPARAM lParam。
整个程序实现退出的流程还是让你自行去分析啦!应该不会太难吧!?
至此,终于完成了这个SDK程序的基本框架了!(哈,真值得我们去举杯庆祝啦!不过,我的任务还没完啊!还有许多要解决的问题等着我噢!)。
第九篇、窗口标题栏上显示自定义图标(手动编辑代码)
白云小飞
本篇想通过手动直接编辑代码的方式(而不是可视化的方式)来操作使用自定义的图标。(以在窗口标题栏上显示自定义图标为例)
通过本篇,你将知道如何使用图标资源及实质,并有助于你理解在可视化方式编辑使用图标资源过程中的代码实质。
另外,可以触类旁通,明白VC中的Window应用程序对各种类似资源(如光标资源、位图资源、声音资源等)操作的一般机制。
一 开始我们的思考:程序所要用到的各类图像、声音等资源应放在什么地方为好?
一个程序可能需要使用各种图像、声音。那么,你认为应把这些东西放在什么地方呢?
一种很显然的做法:就是让这些图形、图像、声音仍以文件形式(如.bmp .jpg .ico .cur .wav等等扩展名的文件,)保存在磁盘中,不包含在应用程序(.exe)中。当我的.exe程序执行时需要这些文件时再把它们从磁盘中加载到内存里并对它们操作。
如果你是想写一个看图软件或播放音乐之类的软件,这当然是一种最佳的做法啦。但是,有时候你的程序可能要显示一个代表你程序的图像等等的情况。由于前述的方法中,图形、图像、声音是以独立的文件形式,所以软件的使用者是很容易就改动及删除它们的,唉!我可不希望这样啊!
那么,还有什么办法呢?
还有一种可以想到的做法:就是让这些图形、图像、声音等数据在编译时就把它们编译进.exe文件中。
显然,由于程序要用到的各种图像、声音等的数据已经包含在.exe里面了,这样,软件使用者只要拥有这个.exe文件就可以。同时使用者要想改动程序所用的这些图形、图像、声音也就不是那么容易。
啊!exe程序文件里居然也能存储各种类型图像、声音!嗯,这可是Windows的一个设计目标:可执行程序里不仅只含有程序代码,还可以存储各种图像、声音等的数据呢!也可能第一次让你有了这个念头!但不管怎么说,程序是可以这样设计的!
好了,那么又如何创建一个包含有图标资源并使用这个图标的.exe文件呢?
二 图像、声音资源操作的一般步骤。
1 显然,你得先创建一个自己想要的图标(提示:这里我以图标为例)
但具体应如何做呢?
这个问题好解决也好理解:我们只要用一个能编辑图标的图形图像软件就可以啦。制做完图标后,把它们以.ico文件的形式(.ico 是图标文件的扩展名)保存在磁盘的某个文件夹中。
当然啦,VC本身也提供了一个图标编辑器。
2 然后你要给这个图标定义一个字符串ID或者整数ID
为什么要给它定义一个字符串ID或整数ID呢?
那是因为这个图标是要被编译链接进可执行程序中(即.exe文件,当然也可能是.dll文件),这样程序就不能通过文件名来访问该图标了。
所以,我们就得给这个图标定义一个字符串ID或整数ID了,用以代表这个图标。(你可以选择使用字符串ID,也可以使用整数ID。)
这个定义是写在一个叫资源脚本文件(扩展名为 .rc)里的。
当然,还要记住把.rc文件加入工程。
3 使用图标第一步:通过ID号加载图标
请先想想前面两部做了什么?前面两步产生了一个图标并定义了一个图标ID。这样我们的程序就可以使用这个图标了:程序首先通过以下这个API函数:
HICON LoadIcon ( HINSTANCE hInstance, LPCSTR lpIconName);
来加载图标。
暂略第一个参数,先来说一下第二个参数LPCSTR lpIconName。该参数是代表要加载图标的字符串ID。
加载成功后,返回系统分配给该图标的一个句柄值(类型是HICON)。
(该函数具体的使用,后面还会有介绍。)
4 加载后,程序都是通过图标句柄来操作该图标
有了图标句柄值,就可以通过这个值来操作相应的图标了。(还记得“句柄”这个概念及作用吗?Window系统总是通过句柄来操作已加载到内存的某个对象。)
5 最后,我们的程序都编好后,只要把资源与程序代码一起编译到.exe文件中
VC会自动用一个专门的资源编译器会把.rc文件及相关的资源文件(*.ico、*.bmp、*.wav等)编译生成一个扩展名为.res的二进制中间文件。然后再用连接程序与程序代码的二进制中间文件一同连接成可执行程序了。
这看上去比较复杂。不用害怕啊!其实你只要按原来的方式编译连接就行了。
到此,最好重新浏览一下上述1~5的步骤,并多加体会这个操作流程。
现在头脑有了概念没有啊?有,那就开始动手吧!(注意:本篇中我是用手动编辑代码的方式来完成的。)
三 具体实现范例:本例为图标定义一个字符串ID(注意:不是整数ID)
任务:在窗口标题栏上显示一个自定义的图标。
(提醒:务必请先对前一篇所完成工程做一个备份。因为以后我还要从上一篇的工程状态开始新的内容呢!)
1 首先我们要有一个自定义图标(文件名以myicon.ico为例)
在我们的工程文件夹(即D:\MyApp)下创建一个myResLib文件夹(用以集中存放各种资源文件)。
然后,你可用一个.ico编辑器创建一个图标文件myicon.ico,把它放在D:\MyApp\myResLib文件夹下。
不过,也可能你并不懂得使用任何一款.ico编辑器,那也没关系,随便找一个.ico(16*16或32*32的)文件(这不应成为问题吧?)。把它复制到D:\MyApp\myResLib文件夹下,并改名为myicon.ico。
好了,现在我们有一个图标文件,请你记住它的路径和名称。
2 用记事本程序创建一个资源脚本文件(文件名为myRes.rc),并在这个文件中为myicon.ico定义一个字符串ID(本例为:IDI_MYICON)作为这个资源的名称。
之所以用记事本来创建而不用VC本身来创建,是因为我不希望让VC生成一些无关码,以便于解说和理解。
另外一点要提醒:如果你的工程里已经包含有一个.rc的资源脚本文件,那么在下面的操作会出现一些不同的情况。但如果从第一篇就按我所述的来操作,本工程是没有.rc文件的。
操作:
=>开始->程序->附件->记事本
=>在记事本中输入如下一行:
IDI_MYICON ICON DISCARDABLE " myResLib\ myicon.ico"
看,在ICON DISCARDABLE的左边写上ID名,右边写上图标所在的相对路径字符串。这样也就将myResLib\myicon.ico图标定义ID号为IDI_MYICON,并且这样定义的ID就是字符串ID。(等一下你就会知道如何使用字符串ID了!)
=>点击“记事本”程序菜单“文件”->另存为->在“保存在”框中选D: ->双击打开MyApp文件夹->在“保存类型”框中选“所有文件(*.*)->在“文件名”框中输入:myRes.rc->点“保存”(操作完成)
现在我们已经为myicon.ico定义了一个字符串ID:IDI_MYICON 。接下来,要干什么呢?哦,你要知道,这样方式创建脚本文件是不会自动加到我们的工程中,所以你要记得自己把myRes.rc加到你的工程中。
3 将myRec.rc加入到工程中。
=>在工作区(Workspace)视图中选FileView选项卡->在其中右击Source Files -> 单击“添加文件到目录… ->双击“myRes.rc”
本操作的目的:将资源脚本文件myRes.rc加入到该工程中。下面我们就可以通过代码来访问这个图标了。
4 要使用图标资源,得先用LoadIcon函数加载图标资源。
LoadIcon原型:
HICON LoadIcon ( HINSTANCE hInstance, LPCSTR lpIconName);
参数:
HINSTANCE hInstance:要加载图标是存在那个应用程序里,就代入这个应用程序实例的句柄。
LPCSTAR lpIconName:是你要加载的图标的字符串ID,就是我们在第3步中定义的。
返回值:加载成功后会返回一个图标句柄值,其类型是HICON。加载后,我们就可以通过这个句柄值来操作对应图标了。
下面就是原来在窗口类结构体中设置窗口标题栏图标的代码(应该还记得下面一行代码在哪里吧!):
wndclass.hIconSm = LoadIcon(NULL, MAKEINTRESOURCE( IDI_APPLICATION ) ) ;
你先别理上面LoadIcon中的参数使用,现在我们把它改成如下:
wndclass.hIconSm = LoadIcon(hinstance,” IDI_MYICON”) ;
其中,hinstance就是本应用程序实例的句柄。”IDI_MYICON”就是我们要加载的图标。
现在,我们把WinMain主函数里的代码修改如下:
int APIENTRY WinMain(HINSTANCE hinstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
//……
wndclass.hInstance = hinstance;
wndclass.hCursor =LoadCursor(NULL, IDC_ARROW);
wndclass.hIcon =NULL;
//wndclass.hIconSm = LoadIcon(NULL, MAKEINTRESOURCE( IDI_APPLICATION ) ) ;
//注释了上句
wndclass.hIconSm = LoadIcon(hinstance,"IDI_ICON1") ; //添加了此句
wndclass.lpszMenuName =NULL;
wndclass.hbrBackground =(HBRUSH) GetStockObject(WHITE_BRUSH);
wndclass.lpszClassName =WND_CLS_NAME;
if (!RegisterClassEx(&wndclass))
return 0;
//……
}
5 最后Build我们的程序
直接F5就行了。
看到了没,程序的窗口已经变成我们想要的图标了。
四 继续上例:将图标资源由使用字符串ID改为使用整数ID
前面我反复地说到字符串ID和整数ID。你首先要明白,所有资源的ID号可以定义为字符串的,也可以定义为整数型的。我在前例中使用的是一个字符串ID的例子,现在我又要改为使用整数ID。
1 创建一个名为Resource.h头文件,内容如下:
#define IDI_MYICON 100
将IDI_MYICON 定义为一个整数型符号常量。这个数值应是以1以上一个数值。
(注意:头文件最后要有一空行,也是是说 #define IDI_MYICON 100 后要按一个回车键。)
2 修改myRes.rc文件。
//myRes.rc
#include “Resource.h” //包含头文件resource.h
IDI_MYICON ICON DISCARDABLE " myResLib\ myicon.ico"
经过上述两处的添加后,IDI_MYICON就不在是字符串ID了,而是整数ID。因为Resource.h已经将IDI_MYICON定义为一个整数的符号常量。
3 在WinMain( )函数所在的源文件中添加包含Resources.h头文件,并修改LoadIcon()函数。
// MyAppMain.cpp :主函数及回调函数
#include
#include
#include "resource.h" //包含resource.h
int WINAPI WinMain(HINSTANCE hinstance,
HINSTANCE hprevinstance,
LPSTR lpcmdline,
int ncmdshow)
{
//……
wndclass.hInstance = hinstance;
wndclass.hCursor =LoadCursor(NULL, IDC_ARROW);
wndclass.hIcon =NULL;
//wndclass.hIconSm = LoadIcon(NULL, MAKEINTRESOURCE( IDI_APPLICATION ) ) ;
//上句注释了
wndclass.hIconSm = LoadIcon(hinstance, (LPCTSTR)IDI_ICON1) ;
//第二个参数由” IDI_ICON1”字符串改成(LPCTSTR) IDI_ICON1
wndclass.lpszMenuName =NULL;
wndclass.hbrBackground =(HBRUSH) GetStockObject(WHITE_BRUSH);
wndclass.lpszClassName =WND_CLS_NAME;
}
分析:
凡是要使用整数ID资源的文件都要包含Resource.h 头文件,这是因为整数ID是由Resource.h定义的。(这个好理解!)
修改LoadIcon()调用。因为LoadIcon的第二个参数接受的是字符串ID(就是LPCTSTR指针),所以,我们得把整数ID转化成LPCTSTR。(LPCTSTR) IDI_MYICON目的就是将IDI_MYICON强制转化成LPCTSTR。
我们可是辛辛苦苦地把字符串ID改成整数ID,现在调用LoadIcon()时又要将IDI_MYICON 强制类型转化成LPCTSTR类型。嘻嘻,真有意思,好似我们在瞎折腾似的,到头来又要回到了原来的状态。但不管怎么说,这也是一种方式噢!
好了,现在你可以编译运行试试了。哈哈,也是相同的作用哟!