欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Windows编程 第三回 Windows程序的生与死(中)  

程序员文章站 2022-05-05 23:18:33
...

-----路过的朋友,若发现错误或有好的建议,欢迎在下面留言,谢谢!-----

自家之言

我认为,我们对任何事物的认知过程都是循序渐进的,正如我们对人生的看法,当你在20岁,30岁,40岁乃至50岁时对人生看法肯定不一样,并且是逐步加深的。我们都听说过这句话“不听老人言,吃亏在眼前”,有时长辈给你唠叨一些人生宝贵经验与心得时,当时你不理解,或许对它不屑于故,或许认为它太老套,直到以后的某天吃亏时才发现当初长辈的话是对的。(请珍视长辈的教导,你不认同多半源于你的无知与轻薄)

学习也是这个道理,对一种知识的认识与掌握也随着我们的成长而不断加深。同时我们对知识的接受能力与理解在不同的阶段也是不同的(吃亏前就没理解老人“言”,吃亏后就理解了)。如同乍一接触windows编程,这对我们来说是一个全新的世界,我们的接受能力与理解能力处在最弱的一个阶段,因此我认为我们应该学习最简单的东西来熟悉它,就是所谓的“入门”,“入门”之后再层层深入。如同我们在初中、高中和大学都学物理,讲的知识点还是那些,但是对知识点的讲解深度是一步一步的加深的。因此我在上一回中的讲解很浅显,很菜鸟(本人本身就是菜鸟),很不专业,难免有出错的地方,只是希望把初学者领进这全新的世界快点熟悉它,好入门。最权威最正确的最详细的知识介绍未必对初学者是最好的教材;很浅显易懂,难免有点错误,能带大家入门的知识介绍未必就是最差的。(大家学深入后,如果发现其中真有错误,别忘通知我。这也符合人类对科学的认知过程:学习,再深入的学习,推翻原来的错误,树立新的当下认为正确理论,可能新的当下认为正确的理论再会被后人推翻——没有绝对“最正确”,只是我们又向“最正确”逼近了一步)

因此对知识介绍不在于最权威,而在于介绍最合适读者接受,所谓的“因材施教”吧。我有时感叹,当下的很多教材,适用对象层次很不分明(有时更本就没有区分),讲
同一方面的书总有一种感觉是一个模子里刻出来的,对菜鸟太深(看不太懂,反而增加了学习的畏惧心理,希望通过我的努力,尽量降低对新读者的畏惧心理),对老鸟太浅(看了没啥价值)……

真诚的希望当下作家,对不同读者有区分度的出书,也别把书出的千篇一律,都是一个样子,对一个知识的讲解就一定要遵循前人吗?

吐槽

我本以为上一回很难写,这一回简单点,哪知这一回写得却比上一会还要更让我吐血,心累。主要是这一回信息量太大(原本windows程序的生与死要写两回,看来要改成三回了)。我总是处在一种犹豫当中,即想让读者掌握现阶段该掌握的(以后会用函数就行了,就像让刚会开车的新手能掌握各项操作就行了,此时没必要过深的掌握各项操作的原理,一是我们掌握不了,二是我们以后未必都要干设计汽车的或修车的这一行)。但是我又怕读者理解困难,总是想放更多的资料帮助理解;放的资料太多,我又怕吓着读者继续学习,或者让读者迷失在资料当中而忘了我们的目的(就是掌握函数基本用法,会用就行)。思来想去,于是我打算把讲解程序框架的内容(我认为最重要的)放在本回,把一些补充内容、注释、一些次重要的概念、函数用法和我搜集的一些资料放在下回,但愿我的决策是对的。

上一回我对windows程序进行了“写意”式的介绍,这一回我可就要来“工笔”式的描述了。这一回我讲解的会更多更详细,相信有前面的基础,大家会顺利过关的。你准备好了吗?我们开始了!

毛举缕析讲代码

为方便大家阅读,我再把代码贴一遍。




我将按我们一般写代码的顺序带大家研究代码。

孙新老师如是说(下文中引号内部分):

“接触过Windows 编程方法的读者都知道,在应用程序中有一个重要的函数WinMain,这个函数是应用程序的基础。当Windows 操作系统启动一个程序时,它调用的就是该程序的WinMain 函数。WinMain 是Windows程序的入口点函数,与DOS 程序的入口点函数main 的作用相同,当WinMain 函数结束或返回时,Windows 应用程序结束。”

那我们就先来讲解WinMain函数。

“WinMain 函数的原型声明如下:

int WINAPI WinMain(

HINSTANCE hInstance, // handle tocurrent instance(应用程序的实例句柄)

HINSTANCE hPrevInstance,// handle to previous instance(该应用程序前一个实例的句柄)

LPSTR lpCmdLine, // command line(命令行参数串)

int nCmdShow // show state(程序在初始化时如何显示窗口)

);

WinMain 函数接收4 个参数,这些参数都是在系统调用WinMain 函数时传递给应用

程序的。

第一个参数 hInstance 表示该程序当前运行的实例的句柄(解释见下),这是一个数值。当程序在Windows 下运行时,它唯一标识运行中的实例(注意,只有运行中的程序实例,才有实例句柄)。一个应用程序可以运行多个实例,每运行一个实例,系统都会给该实例分配一个句柄值,并通过hInstance 参数传递给WinMain 函数。

第二个参数 hPrevInstance 表示当前实例的前一个实例的句柄。通过查看MSDN 我们可以知道,在Win32 环境下,这个参数总是NULL,即在Win32 环境下,这个参数不再起作用。(可以忽略掌握)

第三个参数 lpCmdLine 是一个以空终止的字符串,指定传递给应用程序的命令行参数。例如:在D 盘下有一个sunxin.txt 文件,当我们用鼠标双击这个文件时将启动记事本程序(notepad.exe),此时系统会将D:\sunxin.txt作为命令行参数传递给记事本程序的WinMain函数,记事本程序在得到这个文件的全路径名后,就在窗口中显示该文件的内容。要在VC++开发环境中向应用程序传递参数,可以单击菜单【Project】→【Settings】,选择“Debug”选项卡,在“Programarguments”编辑框中输入你想传递给应用程序的参数。

第四个参数 nCmdShow 指定程序的窗口应该如何显示,例如最大化、最小化、隐藏等。”

概念讲解:

句柄(HANDLE)——是Windows 程序中一个重要的概念,使用也非常频繁。在Windows 程序中,有各种各样的资源(窗口、图标、光标等),系统在创建这些资源时会为它们分配内存,并返回标识这些资源的标识号,即句柄。在后面的内容中我们还会看到窗口句柄(HWND)图标句柄(HICON)、光标句柄(HCURSOR)和画刷句柄(HBRUSH)。在Windows中,对象使用句柄进行标识,这样,通过使用一个句柄,应用程序可以访问一个对象。句柄是一个数,但实际情况并不这样简单,它的长度将会随着不同的计算机平台和Windows的发展而有所变化,例如,在32位Windows中,句柄将是一个32位的数据,并且不是整数类型。(这是一个越说越复杂的概念,还是从应用中慢慢体会吧,目前我们的目标是会用它,有兴趣的可以看一下注释②,不要过分研究注释而忽略我们只要会用它的目的,不要走火入魔,我提供注释仅是为了力求全面)

其次来讲解窗口类(行15-25)

请回忆上一篇文章中,我引用孙鑫老师关于做填空题的比喻,在Windows中要达到做填空题的效果,只能通过结构体来完成。在Windows中,窗口类是在类型为WNDCLASS的结构变量中定义的结构类型WNDCLASS的定义如下:

typedef struct _WNDCLASS {

UINT style;

WNDPROC lpfnWndProc;

int cbClsExtra;

int cbWndExtra;

HANDLE hInstance;

HICON hIcon;

HCURSOR hCursor;

HBRUSH hbrBackground;

LPCTSTR lpszMenuName;

LPCTSTR lpszClassName;

} WNDCLASS;

“下面对该结构体的成员变量做一个说明。(可以不必太深究每个成员具体的含义,现在有看不懂的正常,以后慢慢体会吧,只要我们会“填空”得到我们想要的窗口就行)

第一个成员变量 style 指定这一类型窗口的样式,常用的样式如下:

n CS_HREDRAW

当窗口水平方向上的宽度发生变化时,将重新绘制整个窗口。当窗口发生重绘(不太懂就跳过,我以后的文章会再讲)时,窗口中的文字和图形将被擦除。如果没有指定这一样式,那么在水平方向上调整窗口宽度时,将不会重绘窗口。

n CS_VREDRAW

当窗口垂直方向上的高度发生变化时,将重新绘制整个窗口。如果没有指定这一样式,那么在垂直方向上调整窗口高度时,将不会重绘窗口。

n CS_NOCLOSE

禁用系统菜单的 Close 命令,这将导致窗口没有关闭按钮。

n CS_DBLCLKS

当用户在窗口中双击鼠标时,向窗口过程发送鼠标双击消息。

style 成员的其他取值请参阅MSDN。(这些具体效果可以运行程序试一下)③

第二个成员变量lpfnWndProc 是一个函数指针,指向窗口过程函数本例即WinSunProc,窗口过程函数是一个回调函数④。

第三个成员变量cbClsExtra:Windows 为系统中的每一个窗口类管理一个WNDCLASS 结构。在应用程序注册一个窗口类时,它可以让Windows 系统为WNDCLASS 结构分配和追加一定字节数的附加内存空间,这部分内存空间称为类附加内存,由属于这种窗口类的所有窗口所共享,类附加内存空间用于存储类的附加信息。Windows 系统把这部分内存初始化为0。一般我们将这个参数设置为0。(仅知道黑体字就好了)

第四个成员变量 cbWndExtra:Windows 系统为每一个窗口管理一个内部数据结构,在

注册一个窗口类时,应用程序能够指定一定字节数的附加内存空间,称为窗口附加内存。

在创建这类窗口时,Windows 系统就为窗口的结构分配和追加指定数目的窗口附加内存空

间,应用程序可用这部分内存存储窗口特有的数据。Windows 系统把这部分内存初始化为

0。如果应用程序用WNDCLASS 结构注册对话框(用资源文件中的CLASS 伪指令创建),

必须给DLGWINDOWEXTRA设置这个成员。一般我们将这个参数设置为0。(仅知道黑体字就好了)

第五个成员变量hInstance 指定包含窗口过程的程序的实例句柄WinMain参数中的hInstance,系统为我们分配好了。

第六个成员变量 hIcon 指定窗口类的图标句柄。这个成员变量必须是一个图标资源的句柄,如果这个成员为NULL,那么系统将提供一个默认的图标。(大家可以自己试一下)

第七个成员变量hCursor 指定窗口类的光标句柄。这个成员变量必须是一个光标资源的句柄。如果这个成员为NULL,那么鼠标进入到应用程序窗口中时它的形状可就不太确定了(不信你可以试一下)。所以一般应用程序都必须明确地设置光标的形状。

第八个成员变量hbrBackground 指定窗口类的背景画刷句柄。当窗口发生重绘时,系统使用这里指定的画刷来擦除窗口的背景。我们既可以为hbrBackground 成员指定一个画刷的句柄,也可以为其指定一个标准的系统颜色值。

(LoadIcon,LoadCursor,GetStockObject函数下回讲)

第九个成员变量lpszMenuName 是一个以空终止的字符串,指定菜单资源的名字。如果你使用菜单资源的ID 号,那么需要用MAKEINTRESOURCE 宏⑤来进行转换。如果将lpszMenuName 成员设置为NULL,那么基于这个窗口类创建的窗口将没有默认的菜单。要注意,菜单并不是一个窗口,很多初学者都误以为菜单是一个窗口。

第十个成员变量lpszClassName 是一个以空终止的字符串,指定窗口类的名字。这和汽车的设计类似,设计一款新型号的汽车,需要给该型号的汽车取一个名字。同样的,设计了一种新类型的窗口,也要为该类型的窗口取个名字,这里我们将这种类型窗口的命名为“HelloWorld”,后面将看到如何使用这个名称。”

再次来讲注册窗口类(行26)

“在设计完汽车后,需要报经国家有关部门审批,批准后才能生产这种类型的汽车。同样地,设计完窗口类(WNDCLASS)后,需要调用RegisterClass 函数对其进行注册,注册成功后,才可以创建该类型的窗口。注册函数的原型声明如下:

ATOM RegisterClass(CONST WNDCLASS*lpWndClass);

该函数只有一个参数,即上一步骤中所设计的窗口类对象的指针。

如果函数成功,返回值是唯一标识已注册的类的一个原子;如果函数失败,返回值为0。”

复次来讲创建窗口(行27-39)

“设计好窗口类并且将其成功注册之后,就可以用CreateWindow 函数产生这种类型的窗口了。CreateWindow 函数的原型声明如下:

HWNDCreateWindow(

LPCTSTRlpClassName, // pointer to registered class name

LPCTSTRlpWindowName, // pointer to window name

DWORD dwStyle,// window style

int x, //horizontal position of window

int y, //vertical position of window

int nWidth, //window width

int nHeight, //window height

HWND hWndParent,// handle to parent or owner window

HMENU hMenu, //handle to menu or child-window identifier

HANDLEhInstance, // handle to application instance

LPVOID lpParam// pointer to window-creation data

);

参数lpClassName 指定窗口类的名称,即我们在步骤1 设计一个窗口类中为WNDCLASS的 lpszClassName 成员指定的名称即“Hello World”, 在这里应该设置为“Hello World”,表示要产生“Hello World”这一类型的窗口。产生窗口的过程是由操作系统完成的,如果在调用CreateWindow 函数之前,没有用RegisterClass 函数注册过名称为“Hello World”的窗口类型,操作系统将无法得知这一类型窗口的相关信息,从而导致创建窗口失败。

参数 lpWindowName 指定窗口的名字。如果窗口样式指定了标题栏,那么这里指定的窗口名字将显示在标题栏上,本例中标题栏会显示“HelloWorld Program”。

参数 dwStyle 指定创建的窗口的样式。⑥就好像同一型号的汽车可以有不同的颜色一样,同一型号的窗口也可以有不同的外观样式。要注意区分WNDCLASS 中的style 成员与CreateWindow 函数的dwStyle 参数,前者是指定窗口类的样式,基于该窗口类创建的窗口都具有这些样式(即共性),后者是指定某个具体的窗口的样式(即个性)。

参数xy,nWidth,nHeight 分别指定窗口左上角的xy坐标,窗口的宽度,高度。如果参数x被设为CW_USEDEFAULT,那么系统为窗口选择默认的左上角坐标并忽略y参数。如果参数nWidth 被设为CW_USEDEFAULT,那么系统为窗口选择默认的宽度和高度,参数nHeight 被忽略。本例中指定窗口左上角坐标(相对于屏幕)为(0,0),窗口宽600像素,高400像素。

参数hWndParent 指定被创建窗口的父窗口句柄,一般为NULL

参数hMenu 指定窗口菜单的句柄,本例中窗口没有菜单,故设为NULL。

参数hInstance 指定窗口所属的应用程序实例的句柄即WinMain参数中的hInstance,系统为我们分配好了。

参数lpParam:作为WM_CREATE消息的附加参数lParam 传入的数据指针。在创建多文档界面的客户窗口时,lpParam 必须指向CLIENTCREATESTRUCT 结构体。多数窗口将这个参数设置为NULL。(知道这个就行了)

如果窗口创建成功,CreateWindow 函数将返回系统为该窗口分配的句柄,否则,返回NULL。注意,在创建窗口之前应先定义一个窗口句柄变量来接收创建窗口之后返回的句柄值。

接着来讲显示及更新窗口

“(1)显示窗口

窗口创建之后,我们要让它显示出来,这就跟汽车生产出来后要推向市场一样。调用函数ShowWindow 来显示窗口,该函数的原型声明如下所示:

BOOLShowWindow(

HWNDhWnd, // handle to window

intnCmdShow // show state

);

ShowWindow 函数有两个参数,第一个参数hWnd 就是在上一步骤中成功创建窗口后返回的那个窗口句柄;第二个参数nCmdShow 指定了窗口显示的状态,常用的有以下几种。

n SW_HIDE:隐藏窗口并激活其他窗口。

n SW_SHOW:在窗口原来的位置以原来的尺寸激活和显示窗口。

n SW_SHOWMAXIMIZED:激活窗口并将其最大化显示。

n SW_SHOWMINIMIZED:激活窗口并将其最小化显示。

n SW_SHOWNORMAL:激活并显示窗口。如果窗口是最小化或最大化的状态,系统将其恢复到原来的尺寸和大小。应用程序在第一次显示窗口的时候应该指定此标志。关于nCmdShow 参数的详细内容请参见MSDN。

(2)更新窗口

在调用 ShowWindow 函数之后,我们紧接着调用UpdateWindow 来刷新窗口,就好像我们买了新房子,需要装修一下。UpdateWindow 函数的原型声明如下:

BOOLUpdateWindow(

HWNDhWnd // handle to window

);

其参数 hWnd 指的是创建成功后的窗口的句柄。”

到此,一个窗口就算创建完成了。

然后讲消息循环

上一回只提到消息,到底消息是什么样子的呢?在 Windows 程序中,消息是由MSG 结构体⑦来表示的,结构体重包含消息的很多信息,由系统自动填充,我们不用操心。

在创建窗口、显示窗口、更新窗口后,我们需要编写一个消息循环,不断地从消息队列中取出消息,并进行响应。要从消息队列中取出消息,我们需要调用GetMessage()函数,该函数的原型声明如下:

BOOL GetMessage(

LPMSG lpMsg, //address of structure with message

HWND hWnd, //handle of window

UINTwMsgFilterMin, // first message

UINTwMsgFilterMax // last message

);

参数lpMsg 指向一个消息(MSG)结构体,GetMessage 从线程(现在不管这个概念,在以后的文章中会讲)的消息队列中取出的消息信息将保存在该结构体对象中。

参数hWnd 指定接收属于哪一个窗口的消息。通常我们将其设置为NULL,用于接收属于调用线程的所有窗口的窗口消息。

参数wMsgFilterMin 指定要获取的消息的最小值,通常设置为0。

参数wMsgFilterMax 指定要获取的消息的最大值。如果wMsgFilterMin 和wMsgFilterMax 都设置为0,则接收所有消息。

GetMessage 函数接收到除WM_QUIT (看着眼熟了吧,在前面是否看过类似的东东,可以参考一下注释⑦)外的消息均返回非零值。对于WM_QUIT 消息,该函数返回零。如果出现了错误,该函数返回-1,例如,当参数hWnd 是无效的窗口句柄或lpMsg 是无效的指针时。

通常我们编写的消息循环代码如下:

MSGmsg;

while(GetMessage(&msg,NULL,0,0))

{

TranslateMessage(&msg);

DispatchMessage(&msg);

}

前面已经介绍了,GetMessage 函数只有在接收到WM_QUIT 消息时,才返回0。此时while 语句判断的条件为假,循环退出,程序才有可能结束运行。在没有接收到WM_QUIT消息时,Windows 应用程序就通过这个while 循环来保证程序始终处于运行状态。

TranslateMessage 函数用于将虚拟键(这个我暂时不解释,n回后我讲)消息转换为字符消息。字符消息被投递到调用线程的消息队列中,当下一次调用GetMessage 函数时被取出。当我们敲击键盘上的某个字符键时,系统将产生WM_KEYDOWN 和WM_KEYUP 消息。这两个消息的附加参数(wParam 和lParam)包含的是虚拟键代码和扫描码等信息,而我们在程序中往往需要得到某个字符的ASCII 码,TranslateMessage 这个函数就可以将WM_KEYDOWN 和WM_KEYUP 消息的组合转换为一条WM_CHAR 消息(该消息的wParam 附加参数包含了字符的ASCII 码),并将转换后的新消息投递到调用线程的消息队列中。注意,TranslateMessage函数并不会修改原有的消息,它只是产生新的消息并投递到消息队列中。

DispatchMessage 函数分派一个消息到窗口过程,由窗口过程函数对消息进行处理。DispachMessage 实际上是将消息回传给操作系统,由操作系统调用窗口过程函数对消息进

行处理(响应)。”

(以上几个函数看不太懂可先跳过,一般就这个格式,照抄就行了。)

温故知新

“既然讲到这了,那我们再来回顾一下上一回我们曾提到的Windows 应用程序的消息处理机制(也就是程序运行机制)

Windows编程  第三回   Windows程序的生与死(中)
            
    
    
         

(1)操作系统接收到应用程序的窗口消息,将消息投递到该应用程序的消息队列中。

(2)应用程序在消息循环中调用GetMessage 函数从消息队列中取出一条一条的消息。取出消息后,应用程序可以对消息进行一些预处理,例如,放弃对某些消息的响应,或者调用TranslateMessage 产生新的消息。

(3)应用程序调用DispatchMessage,将消息回传给操作系统。消息是由MSG 结构体对象来表示的,其中就包含了接收消息的窗口的句柄。因此,DispatchMessage 函数总能进行正确的传递。

(4)系统利用WNDCLASS 结构体的lpfnWndProc 成员保存的窗口过程函数的指针调用窗口过程,对消息进行处理(即“系统给应用程序发送了消息”)。”

最后编写窗口过程函数

“在完成上述步骤后,剩下的工作就是编写一个窗口过程函数,用于处理发送给窗口的消息。一个Windows 应用程序的主要代码部分就集中在窗口过程函数中。在MSDN 中可以查到窗口过程函数的声明形式,如下所示:

LRESULT CALLBACKWindowProc(

HWND hwnd, //handle to window

UINT uMsg, //message identifier

WPARAM wParam,// first message parameter

LPARAM lParam //second message parameter

);

窗口过程函数的名字可以随便取,如WinSunProc,但函数定义(行50-55)的形式必须和上述声明(行2-7,由于WinMain函数中有对此函数的参考,即行22,所以要在WinMain函数之前声明)的形式相同。

提示:系统通过窗口过程函数的地址(指针)来调用窗口过程函数,而不是名字。

WindowProc 函数的4 个参数分别对应消息的窗口句柄、消息代码、消息代码的两个附加参数。(后三个参数知道名字就行了)由于一个程序可以有多个窗口,窗口过程函数的第1 个参数hwnd 就标识了接收消息的特定窗口,就是CreateWindow返回的那个句柄。

在窗口过程函数内部使用 switch/case 语句来确定窗口过程接收的是什么消息,以及如何对这个消息进行处理。”

声明:本文大部分讲解援引自孙鑫老师的《VC++深入详解》,吃水不忘挖井人,在此向孙鑫老师拜谢。