玩转Win32开发(2):完整的开发流程

 

上一篇中我给各位说了一般人认为C++中较为难的东西——指针。其实对于C++,难点当然不局限在指针这玩意儿上,还有一些有趣的概念,如模板类、虚基类、纯虚函数等,这些都是概念性的东西,几乎每一本C++书上都会介绍,而平时我们除了会接触到纯虚函数外,其他的不多用。纯虚函数,你可以认为与C#中的抽象方法或接口中的方法类似,即只定义,不实现。好处就是多态,发何处理,由派生类来决定。

在开始吹牛之前,我先推荐一套视频教程,孙鑫老师的C++教程,共20课,我不是帮他老人家打广告,而是因为孙老师讲的课是我听过的最好的课,我都看过4次了,我完全可以用他的视频教程来复习C++的。

 

好了,F话说完了,下面我就扯一下编写一个Win32应用程序的大致流程,不管你的程序有多么复杂,多么变态,其基本思路和流程是不变的。这就好比你写书法的时候,特别是写楷书,我不管你用的是欧体、颜体,还是柳体,你都得遵守“永字八法”基本规则。

那么,我们要编写一个Win32应用程序,要经过哪几个步骤呢?

你不妨想一想,你有一家工厂是生产女性服装的,如果你要生产一批新式服装(例如某种冬装),你会有哪些流程?

首先,如果我们确定要做这么一款服式,我们要请设计师来把服装设计好,然后打版,打版就是生成基本样本,以后工人就按照这个样本做就行了。

其次,注册产品,向上级主管申报,登记后就转入车间或下游加工企业开工。

再次,为了展示你的新产品的特色,你要举办一场服装表演。

接着、持续更新,发现产品存在的问题,不断改进修正。

最后,推向市场。

我们开发Win32应用程序也是遵守这样的规范。不过,我想现在很少人用Win32在实际开发中,毕竟它的开发效率是相当地低下,所以,曾被某些人误认为只适用于开发木马程序。其实,也不一定的,不要太邪恶了。

MFC对Win API函数的封装,后来出现了托管C++,你可以用于写WinForm程序,这样可以提高开发效率。

如果你有足够的时间,如果你还在学习编程,如果你是刚进入大学的年轻有为者,你不用急,因为你会有更多的时间磨炼,你应当考虑多学一点C类语言,C++的学习你会发现你能学到很多其他语言中学不到的知识,特别是接触到不少原理性的东西,能加深你对编程哲学的认知。

 

一、WinMain入口点

我们在学习标准C++的时候,都知道每个应用程序运行时都会先进入入口点函数main,而当从main函数跳出时程序就结束了。在Windows编程里面,也是一样的,只是我们的入口点函数不叫main,叫WinMain,这个函数不同于main,我们不能乱来,它的定义必须与声明保持一致。

我建议各位安装VS的时候,都顺便更新帮助文档到本地硬盘,这样我们可以方便查找。有一点要注意,目前DestTop Develop的文档基本上是英文的,做好心理准备。

WinMain函数怎么写呢,不用记的,到MSDN文档一搜,直接复制就行了。

[cpp] view plaincopyprint?
  1. int CALLBACK WinMain(  
  2.     _In_  HINSTANCE hInstance,  
  3.     _In_  HINSTANCE hPrevInstance,  
  4.     _In_  LPSTR lpCmdLine,  
  5.     _In_  int nCmdShow  
  6.   );  


这个函数带了一个CALLBACK,说明它是一个回调函数,那么这个CALLBACK是啥呢。我们先不管,我们先动写一个Windows,让大家有一个更直观的认识。

1、启动你的开发工具,版本任意。

2、从菜单栏中依次【文件】【新建】【项目】,在新建项目窗口中,选择Win32-Win32应用程序。

 

2、点击确定后,会弹出一个向导,单击【下一步】。项目类型选择Windows应用程序,附加选项选择空项目,我们要自己编写实现代码。

 

3、单击完成,项目创建成功。打开【解决方案资源管理器】,在“源文件”文件夹上右击,从菜单中找到【添加】【新建项】,注意,是源文件,不要搞到头文件去了。

在新建项窗口中选C++代码文件,.cpp后缀的,不要选错了,选成头文件,不然无法编译,因为头文件是不参与编译的。文件名随便。

 

包含Windows.h头文件,这个是最基本的。

[cpp] view plaincopyprint?
  1. #include <Windows.h>  


然后是入口点,这个我们直接把MSDN的声明Ctrl + C,然后Ctrl + V上去就行了。

[cpp] view plaincopyprint?
  1. int CALLBACK WinMain(  
  2.     _In_  HINSTANCE hInstance,  
  3.     _In_  HINSTANCE hPrevInstance,  
  4.     _In_  LPSTR lpCmdLine,  
  5.     _In_  int nCmdShow  
  6.   )  
  7. {  
  8.   
  9.     return 0;  
  10. }  


WinMain返回整型,返回0就行了,其实是进程的退出码,一定要0,不要写其他,因为0表示正常退出,其他值表示非正常退出。

刚才我们提到这个函数带了CALLBACK,那么,它是什么?很简单,你回到IDE,在CALLBACK上右击,选【转到定义】,看看吧。

我们看到它其实是一个宏,原型如下:

[cpp] view plaincopyprint?
  1. #define CALLBACK    __stdcall  

这时候我们发现了,它其实就是__stdcall,那么这个__stdcall是什么呢?它是和__cdecl关键字对应的,这些资料,你网上搜一下就有了,如果你觉得不好理解,你不妨这样认为,__stdcall是专门用来调用Win API 的,反正MSDN上也是这样说的,它其实是遵循Pascal的语法调用标准,相对应地,__cdecl是C语言的调用风格,这个也是编译器选项。
打开项目属性,找到节点C/C++\高级,然后查看一下调用约定,我们看到默认是选择C风格调用的,所以,WIN API 函数才用上关键字__stdcall,如果你实在不懂,也没关系,这个东西一般不影响我们写代码,但属性窗口中的编译器选项不要乱改,改掉了可能会导致一些问题。

 

那么CALLBACK有什么特别呢?一句话:函数不是我们调用的,但函数只定义了模型没有具体处理,而代码处理权在被调用者手里。怎么说呢,我们完全把它理解为.NET中的委托,我想这样就好理解了,委托只声明了方法的参数和返回值,并没有具体处理代码。

WinMain是由系统调用的,而WinMain中的代码如何写,那操作系统就不管了。就好像我告诉你明天有聚会,一起去爬山,反正我是通知你了,至于去不去那是你决定了。

接下来看看入口点函数的参数。

注意,我们平时看到很多如HANDLE,HINSTANCE,HBRUSH,WPARAM。LPARAM,HICON,HWND等一大串数据类型,也许我们会说,怎么Windows开发有那么多数据类型。其实你错了,人总是被眼睛所看到的东西欺骗,Win API 中根本没有什么新的数据类型,全都是标准C++中的类型,说白了,这些东西全是数字来的。如果你不信,自己可以研究一下。

它定义这些名字,只是方便使用罢了,比如下面这样:

[cpp] view plaincopyprint?
  1. int hWindow;  
  2. int hIcon;  
  3. int theAppInstance;  

第一个变量指的是窗口的句柄,第二个指的是一个图标的句柄,第三个是当前应用程序的实例句柄,你看看,如果我们所有的句柄都是int,我们就无法判断那些类型是专门用来表示光标资源,不知道哪些类型是专用来表示位图的句柄了,但是,如果我们这样:

[cpp] view plaincopyprint?
  1. #defin HBRUSH  int64  

这样就很直观,我一看这名就知道是Brush Handlers,哦,我就明白它是专门用来管理内存中的画刷资源的,看,这就很明了,所以,通常这些新定义的类型或者宏,都是取有意义的名字。比如消息,它也是一个数字,如果我说115代表叫你去滚,但光是一个115谁知道你什么意思,但是,如果我们为它定义一个宏:

[cpp] view plaincopyprint?
  1. #define WM_GET_OUT    115  

这样,只要我SendMessage(hwnd,  WM_GET_OUT, NULL, NULL),你就会收到一条消息,滚到一边去。

 

WinMain的第一个参数是当前应用程序的实例句柄,第二个参数是前一个实例,比如我把kill.exe运行了两个实例,进程列表中会有两个kill.exe,这时候第一次运行的实例号假设为0001,就传递第一个参数hInstance,第二次运行的假设实例号为0002,就传给了hPrevInstance参数。

lpCmdLine参数从名字上就猜到了,就是命令行参数,那LPSTR是啥呢,它其实就是一个字符串,你可以跟入定义就知道了,它其实就是char*,指向char的指针,记得我上一篇文章中说的指针有创建数组的功能吗?对,其实这里传入的命令行参数应该是char[ ],这就是我在第一篇文章中要说指针的原因。

这里告诉大家一个技巧,我们怎么知道哪些参数是指针类型呢,因为不是所有参数都有 * 标识。技巧还是在命名上,以后,只要我们看到P开头的,或者LP开头的,都是指针类型

比如LPWSTR,LPCTSTR,LPRECT等等。

最后一个参数nCmdShow是主窗口的显示方式。它定义了以下宏。

 

ValueMeaning
SW_HIDE0

Hides the window and activates another window.

SW_MAXIMIZE3

Maximizes the specified window.

SW_MINIMIZE6

Minimizes the specified window and activates the next top-level window in the Z order.

SW_RESTORE9

Activates and displays the window. If the window is minimized or maximized, the system restores it to its original size and position. An application should specify this flag when restoring a minimized window.

SW_SHOW5

Activates the window and displays it in its current size and position.

SW_SHOWMAXIMIZED3

Activates the window and displays it as a maximized window.

SW_SHOWMINIMIZED2

Activates the window and displays it as a minimized window.

SW_SHOWMINNOACTIVE7

Displays the window as a minimized window. This value is similar toSW_SHOWMINIMIZED, except the window is not activated.

SW_SHOWNA8

Displays the window in its current size and position. This value is similar toSW_SHOW, except the window is not activated.

SW_SHOWNOACTIVATE4

Displays a window in its most recent size and position. This value is similar toSW_SHOWNORMAL, except the window is not activated.

SW_SHOWNORMAL1

Activates and displays a window. If the window is minimized or maximized, the system restores it to its original size and position. An application should specify this flag when displaying the window for the first time.

 


这个参数是操作系统传入的,我们无法修改它。那么,应用程序在运行时,是如何决定这个参数的呢?看看这个,不用我介绍了吧,你一定很熟悉。

我们写了WinMain,但我们还要在WinMain前面预先定义一个WindowProc函数。C++与C#,Java这些语言不同,你只需记住,C++编译器的解析是从左到右,从上到下的,如果某函数要放到代码后面来实现,但在此之前要使用,那么你必须先声明一下,不然编译时会找不到。这里因为我们通常会把WindowProc实现放在WinMain之后,但是在WinMain中设计窗口类时要用到它的指针,这时候,我们必须在WinMain之前声明WindowProc。

同样地,WindowProc的定义我们不用记,到MSDN直接抄就行了。

[cpp] view plaincopyprint?
  1. #include <Windows.h>  
  2. // 必须要进行前导声明  
  3. LRESULT CALLBACK WindowProc(  
  4.     _In_  HWND hwnd,  
  5.     _In_  UINT uMsg,  
  6.     _In_  WPARAM wParam,  
  7.     _In_  LPARAM lParam  
  8. );  
  9.   
  10. int CALLBACK WinMain(  
  11.     _In_  HINSTANCE hInstance,  
  12.     _In_  HINSTANCE hPrevInstance,  
  13.     _In_  LPSTR lpCmdLine,  
  14.     _In_  int nCmdShow  
  15.   )  
  16. {  
  17.     return 0;  
  18. }  
  19. // 在WinMain后实现  
  20. LRESULT CALLBACK WindowProc(  
  21.     _In_  HWND hwnd,  
  22.     _In_  UINT uMsg,  
  23.     _In_  WPARAM wParam,  
  24.     _In_  LPARAM lParam  
  25. )  
  26. {  
  27.     return DefWindowProc(hwnd, uMsg, wParam, lParam);  
  28. }  


前导声明与后面实现的函数的签名必须一致,编译才会认为它们是同一个函数。在WindowProc中返回DefWindowProc是把我们不感兴趣或者没有处理的消息交回给操作系统来处理。也许你会问,函数的名字一定要叫WindowProc吗?当然不是了,你可以改为其他名字,如MyProc,但前提是返回值和参数的类型以及个数必须一致。

[cpp] view plaincopyprint?
  1. LRESULT CALLBACK MyProc(  
  2.     _In_  HWND hwnd,  
  3.     _In_  UINT uMsg,  
  4.     _In_  WPARAM wParam,  
  5.     _In_  LPARAM lParam  
  6. )  

这个函数带了CALLBACK,说明不是我们调用的,也是由操作系统调用的,我们在这个函数里面对需要处理的消息进行响应。至于,为什么可以改函数的名字而系统为什么能找到这个函数呢,后面你就知道了。

 

二、设计与注册窗口类

设计窗口类,其实就是设计我们程序的主窗口,如有没有标题栏,背景什么颜色,有没有边框,可不可以调整大小等。要设计窗口类,我们用到一个结构——

[cpp] view plaincopyprint?
  1. typedef struct tagWNDCLASS {  
  2.   UINT      style;  
  3.   WNDPROC   lpfnWndProc;  
  4.   int       cbClsExtra;  
  5.   int       cbWndExtra;  
  6.   HINSTANCE hInstance;  
  7.   HICON     hIcon;  
  8.   HCURSOR   hCursor;  
  9.   HBRUSH    hbrBackground;  
  10.   LPCTSTR   lpszMenuName;  
  11.   LPCTSTR   lpszClassName;  
  12. } WNDCLASS, *PWNDCLASS;  

通常情况下,我们用WNDCLASS就可以了,当然还有一个WNDCLASSEX的扩展结构,在API里面,凡是看到EX结尾的都是扩展的意思,比如CreateWindowEx就是CreateWindow的扩展函数。

第一个成员是窗口的类样式,注意,不要和窗口样式(WS_xxxxx)混淆了,这里指的是这个窗口类的特征,不是窗口的外观特征,这两个style是不一样的。

它的值可以参考MSDN,通常我们只需要两个就可以了——CS_HREDRAW | CS_VREDRAW,从名字就看出来了,就是同时具备水平重画和垂直重画。因为当我们的窗口显示的时候,被其他窗口挡住后重新显示,或者大小调整后,窗口都要发生绘制,就像我们在纸上涂鸦一样,每次窗口的变化都会“粉刷”一遍,并发送WM_PAINT消息。

lpfnWndProc参数就是用来设置你用哪个WindowProc来处理消息,前面我说过,我们只要不更改回调函数的返回值和参数的类型和顺序,就可以随意设置函数的名字,那为什么系统可以找到我们用的回调函数呢,对的,就是通过lpfnWndProc传进去的,它是一个函数指针,也就是它里面保存的是我们定义的WindowProc的入口地址,使用很简单,我们只需要把函数的名字传给它就可以了。

cbClsExtra和cbWndExtra通常不需要,设为0就OK。hInstance是当前应用程序的实例句柄,从WinMain的hInstance参数中可以得到。hIcon和hCursor就不用我说了,看名字就知道了。

hbrBackground是窗口的背景色,你也可以不设置,但在处理WM_PAINT消息时必须绘制窗口背景。也可以直接用系统定义的颜色,MSDN为我们列出这些值,大家不用记,直接到MSDN拿来用就行了,这些都比较好理解,看名字就知道了。

  • COLOR_ACTIVEBORDER
  • COLOR_ACTIVECAPTION
  • COLOR_APPWORKSPACE
  • COLOR_BACKGROUND
  • COLOR_BTNFACE
  • COLOR_BTNSHADOW
  • COLOR_BTNTEXT
  • COLOR_CAPTIONTEXT
  • COLOR_GRAYTEXT
  • COLOR_HIGHLIGHT
  • COLOR_HIGHLIGHTTEXT
  • COLOR_INACTIVEBORDER
  • COLOR_INACTIVECAPTION
  • COLOR_MENU
  • COLOR_MENUTEXT
  • COLOR_SCROLLBAR
  • COLOR_WINDOW                                         /*  这个就是窗口的默认背景色  */
  • COLOR_WINDOWFRAME
  • COLOR_WINDOWTEXT

lpszMenuName指的是菜单的ID,没有菜单就NULL,lpszClassName就是我们要向系统注册的类名,字符,不能与系统已存在的类名冲突,如“BUTTON”类。

所以,在WinMain中设计窗口类。

[cpp] view plaincopyprint?
  1. // 类名  
  2. WCHAR* cls_Name = L"My Class";  
  3. // 设计窗口类  
  4. WNDCLASS wc;  
  5. wc.hbrBackground = (HBRUSH)COLOR_WINDOW;  
  6. wc.lpfnWndProc = WindowProc;  
  7. wc.lpszClassName = cls_Name;  
  8. wc.hInstance = hInstance;  

窗口类设计完成后,不要忘了向系统注册,这样系统才能知道有这个窗口类的存在。向操作系统注册窗口类,使用RegisterClass函数,它的参数就是一个指向WNDCLASS结构体的指针,所以我们传递的时候,要加上&符号。

[cpp] view plaincopyprint?
  1. // 注册窗口类  
  2. RegisterClass(&wc);  


 

三、创建和显示窗口

窗口类注册完成后,就应该创建窗口,然后显示窗口,调用CreateWindow创建窗口,如果成功,会返回一个窗口的句柄,我们对这个窗口的操作都要用到这个句柄。什么是句柄呢?其实它就是一串数字,只是一个标识而已,内存中会存在各种资源,如图标、文本等,为了可以有效标识这些资源,每一个资源都有其唯一的标识符,这样,通过查找标识符,就可以知道某个资源存在于内存中哪一块地址中,就好比你出身的时候,长辈都要为你取个名字,你说名字用来干吗?名字就是用来标识你的,不然,你见到A叫小明,遇到B又叫小明,那谁知道哪个才是小明啊?就好像你上大学去报到号,会为你分配一个可以在本校学生中唯一标识你的学号,所有学生的学号都是不同的,这样,只要通过索引学号,就可以找到你的资料。

CreateWindow函数返回一个HWND类型,它就是窗口类的句柄。

[cpp] view plaincopyprint?
  1. // 创建窗口  
  2. HWND hwnd = CreateWindow(  
  3.     cls_Name,           //类名,要和刚才注册的一致  
  4.     L"我的应用程序",          //窗口标题文字  
  5.     WS_OVERLAPPEDWINDOW,        //窗口外观样式  
  6.     38,             //窗口相对于父级的X坐标  
  7.     20,             //窗口相对于父级的Y坐标  
  8.     480,                //窗口的宽度  
  9.     250,                //窗口的高度  
  10.     NULL,               //没有父窗口,为NULL  
  11.     NULL,               //没有菜单,为NULL  
  12.     hInstance,          //当前应用程序的实例句柄  
  13.     NULL);              //没有附加数据,为NULL  
  14. if(hwnd == NULL)                //检查窗口是否创建成功  
  15.     return 0;  


窗外观的样式都是WS_打头的,是Window Style的缩写,这个我就不说了,MSDN上全有了。

窗口创建后,就要显示它,就像我们的产品做了,要向客户展示。显示窗口调用ShowWindow函数。

[cpp] view plaincopyprint?
  1. // 显示窗口  
  2. ShowWindow(hwnd, SW_SHOW);  

既然要显示窗口了,那么ShowWindow的第一个参数就是刚才创建的窗口的句柄,第二个参数控制窗口如何显示,你可以从SW_XXXX中选一个,也可以用WinMain传进来的参数,还记得WinMain的最后一个参数吗?

 

四、更新窗口(可选)

为什么更新窗口这一步可有可无呢?因为只要程序在运行着,只要不是最小化,只要窗口是可见的,那么,我们的应用程序会不断接收到WM_PAINT通知。这里先不说,后面你会明白的。好了,更新窗口,当然是调用UpdateWindow函数。

[cpp] view plaincopyprint?
  1. // 更新窗口  
  2. UpdateWindow(hwnd);  


 

五、消息循环

Windows操作系统是基于消息控制机制的,用户与系统之间的交互,程序与系统之间的交互,都是通过发送和接收消息来完成的。就好像军队一样,命令一旦传达,就要执行,当然,我们的应用程序和军队不一样,我们收到指令不一要执行,我们是可以选择性地执行。

我们知道,代码是不断往前执行的,像我们刚才写的WinMain函数一样,如果你现在运行程序,你会发现什么都没有,是不是程序不能运行呢,不是,其实程序是运行了,只是它马上结束了,只要程序执行跳出了WinMain的右大括号,程序就会结束了。那么,要如何让程序不结束了,可能大家注意到我们在C程序中可以用一个getchar()函数来等到用户输入,这样程序就人停在那里,直到用户输入内容。但我们的窗口应用不能这样做,因为用户有可能进行其他操作,如最小化窗口,移动窗口,改变窗口大小,或者点击窗口上的按钮等。因此,我们不能简地弄一个getchar在那里,这样就无法响应用户的其他操作了。

可以让程序留在某处不结束的另一个方法就是使用循环,而且是死循环,这样程序才会永久地停在某个地方,但这个死循环必须具有跳出的条件,不然你的程序会永久执行,直达停电或者把电脑砸了。

这样消息循环就出现了,只要有与用户交互,系统人不断地向应用程序发送消息通知,因为这些消息是不定时不断发送的,必须有一个绶冲区来存放,就好像你去银行办理手续要排队一样,我们从最前端取出一条一条消息处理,后面新发送的消息会一直在排队,直到把所有消息处理完,这就是消息队列

要取出一条消息,调用GetMessage函数。函数会传入一个MSG结构体的指针,当收到消息,会填充MSG结构体中的成员变量,这样我们就知道我们的应用程序收到什么消息了,直到GetMessage函数取不到消息,条件不成立,循环跳出,这时应用程序就退出。MSG的定义如下:

[cpp] view plaincopyprint?
  1. typedef struct tagMSG {  
  2.   HWND   hwnd;  
  3.   UINT   message;  
  4.   WPARAM wParam;  
  5.   LPARAM lParam;  
  6.   DWORD  time;  
  7.   POINT  pt;  
  8. } MSG, *PMSG, *LPMSG;  

hwnd不用说了,就是窗口句柄,哪个窗口的句柄?还记得WindowProc回调函数吗?你把这个函数交给了谁来处理,hwnd就是谁的句柄,比如我们上面的代码,我们是把WindowProc赋给了新注册的窗口类,并创建了主窗口,返回一个表示主窗口的句柄,所以,这里MSG中的hwnd指的就是我们的主窗口。

message就是我们接收到的消息,看到,它是一个数字,无符号整型,所以我们操作的所有消息都是数字来的。wParam和lParam是消息的附加参数,其实也是数值来的。通常,lParam指示消息的处理结果,不同消息的结果(返回值)不同,具体可参阅MSDN。

有了一个整型的值来表示消息,我们为什么还需要附加参数呢?你不妨想一下,如果接收一条WM_LBUTTONDOWN消息,即鼠标左键按下时发送的通知消息,那么,我们不仅知道左键按下这件事,我们更感趣的是,鼠标在屏幕上的哪个坐标处按下左键,按了几下,这时候,你公凭一条WM_LBUTTONDOWN消息是无法传递这么多消息的。可能我们需要把按下左键时的坐标放入wParam参数中;最典型的就是WM_COMMAND消息,因为只要你使用菜单,点击按钮都会发送这样一条消息,那么我怎么知道用户点了哪个按钮呢?如果窗口中只有一个按钮,那好办,用户肯定单击了它,但是,如果窗口上有10个按钮呢?而每一个按钮被单击都会发送WM_COMMAND消息,你能知道用户点击了哪个按钮吗?所以,我们要把用户点击了的那个按钮的句柄存到lParam参数中,这样一来,我们就可以判断出用户到底点击了哪个按钮了。

GetMessage函数声明如下:

[cpp] view plaincopyprint?
  1. BOOL WINAPI GetMessage(  
  2.   _Out_     LPMSG lpMsg,  
  3.   _In_opt_  HWND hWnd,  
  4.   _In_      UINT wMsgFilterMin,  
  5.   _In_      UINT wMsgFilterMax  
  6. );  

这个函数在定义时带了一个WINAPI,现在,按照前面我说的方法,你应该猜到,它就是一个宏,而真实的值是__stdcall,前文中说过了。

第一个参数是以LP开头,还记得吗,我说过的,你应该想到它就是 MSG* ,一个指向MSG结构的指针。第二个参数是句柄,通常我们用NULL,因为我们会捕捉整个应用程序的消息。后面两个参数是用来过滤消息的,指定哪个范围内的消息我接收,在此范围之外的消息我拒收,如果不过滤就全设为0.。返回值就不说了,自己看。

[cpp] view plaincopyprint?
  1. // 消息循环  
  2. MSG msg;  
  3. while(GetMessage(&msg, NULL, 0, 0))  
  4. {  
  5.     TranslateMessage(&msg);  
  6.     DispatchMessage(&msg);  
  7. }  

TranslateMessage是用于转换按键信息的,因为键盘按下和弹起会发送WM_KEYDOWN和WM_KEYUP消息,但如果我们只想知道用户输了哪些字符,这个函数可以把这些消息转换为WM_CHAR消息,它表示的就是键盘按下的那个键的字符,如“A”,这样我们处理起来就更方便了。

DispatchMessage函数是必须调用的,它的功能就相当于一根传送带,每收到一条消息,DispatchMessage函数负责把消息传到WindowProc让我们的代码来处理,如果不调用这个函数,我们定义的WindowProc就永远接收不到消息,你就不能做消息响应了,你的程序就只能从运行就开始死掉了,没有响应。


 

六、消息响应

其实现在我们的应用程序是可以运行了,因为在WindowProc中我们调用了DefWindowProc,函数,消息我们不作任何处理,又把控制权路由回到操作系统来默认处理,所以,整个过程中,我们现在的消息循环是成立的,只不过我们不做任何响应罢了。

好的,现在我把完整的代码贴一下,方便你把前面我们说的内容串联起来。

[cpp] view plaincopyprint?
  1. #include <Windows.h>  
  2. // 必须要进行前导声明  
  3. LRESULT CALLBACK WindowProc(  
  4.     _In_  HWND hwnd,  
  5.     _In_  UINT uMsg,  
  6.     _In_  WPARAM wParam,  
  7.     _In_  LPARAM lParam  
  8. );  
  9.   
  10. // 程序入口点  
  11. int CALLBACK WinMain(  
  12.     _In_  HINSTANCE hInstance,  
  13.     _In_  HINSTANCE hPrevInstance,  
  14.     _In_  LPSTR lpCmdLine,  
  15.     _In_  int nCmdShow  
  16.   )  
  17. {  
  18.     // 类名  
  19.     WCHAR* cls_Name = L"My Class";  
  20.     // 设计窗口类  
  21.     WNDCLASS wc;  
  22.     wc.hbrBackground = (HBRUSH)COLOR_WINDOW;  
  23.     wc.lpfnWndProc = WindowProc;  
  24.     wc.lpszClassName = cls_Name;  
  25.     wc.hInstance = hInstance;  
  26.     // 注册窗口类  
  27.     RegisterClass(&wc);  
  28.   
  29.     // 创建窗口  
  30.     HWND hwnd = CreateWindow(  
  31.         cls_Name,           //类名,要和刚才注册的一致  
  32.         L"我的应用程序",  //窗口标题文字  
  33.         WS_OVERLAPPEDWINDOW, //窗口外观样式  
  34.         38,             //窗口相对于父级的X坐标  
  35.         20,             //窗口相对于父级的Y坐标  
  36.         480,                //窗口的宽度  
  37.         250,                //窗口的高度  
  38.         NULL,               //没有父窗口,为NULL  
  39.         NULL,               //没有菜单,为NULL  
  40.         hInstance,          //当前应用程序的实例句柄  
  41.         NULL);              //没有附加数据,为NULL  
  42.     if(hwnd == NULL) //检查窗口是否创建成功  
  43.         return 0;  
  44.   
  45.     // 显示窗口  
  46.     ShowWindow(hwnd, SW_SHOW);  
  47.   
  48.     // 更新窗口  
  49.     UpdateWindow(hwnd);  
  50.   
  51.     // 消息循环  
  52.     MSG msg;  
  53.     while(GetMessage(&msg, NULL, 0, 0))  
  54.     {  
  55.         TranslateMessage(&msg);  
  56.         DispatchMessage(&msg);  
  57.     }  
  58.     return 0;  
  59. }  
  60. // 在WinMain后实现  
  61. LRESULT CALLBACK WindowProc(  
  62.     _In_  HWND hwnd,  
  63.     _In_  UINT uMsg,  
  64.     _In_  WPARAM wParam,  
  65.     _In_  LPARAM lParam  
  66. )  
  67. {  
  68.     return DefWindowProc(hwnd, uMsg, wParam, lParam);  
  69. }  


所有代码看上去貌似很正常,也遵守了流程,设计窗口类,注册窗口类,创建窗口,显示窗口,更新窗口,消息循环。是吧,这段代码看上去毫无破绽,运行应该没问题吧。好,如果你如此自信,那就试试吧。

按下F5试试运行。
哈哈,结果会让很多人失望,很多初学者就是这样,一切看起来好像正常,于是有人开始骂VC是垃圾,是编译器有bug,也有人开始想放弃了,妈的,这么难,不学了。人啊,总是这样,老指责别人的问题,从不在自己身上找问题,是真的VC的bug吗?

我前面说了,这段代码貌似很正常,呵呵,你看到问题在哪吗?给你两分钟来找错。我提示一下,这个程序没有运行是因为主窗口根本就没有创建,因为我在代码里面做了判断,如果窗口顺柄hwnd为NULL,就退出,现在程序一运行就退出了,明显是窗口创建失败。

…………

好了,不用找了,很多人找不出来,尤其是许多初学者,不少人找了一遍又一遍,都说没有错误,至少代码提示没说有错,编译运行也没报错,所以不少人自信地说,代码没错。

其实你是对的,代码确实没有错,而问题就出在WNDCLASS结构上,认真看一下MSDN上有关RegisterClass函数说明中的一句话,这句话很多人没注意到,但它很关键。

You must fill the structure with the appropriate class attributes before passing it to the function.

现在你明白了吧,还不清楚?没关系,看看我把代码这样改一下你就知道了。

[cpp] view plaincopyprint?
  1. // 设计窗口类  
  2. WNDCLASS wc;  
  3. wc.cbClsExtra = 0;  
  4. wc.cbWndExtra = 0;  
  5. wc.hCursor = LoadCursor(hInstance, IDC_ARROW);;  
  6. wc.hIcon = LoadIcon(hInstance, IDI_APPLICATION);;  
  7. wc.lpszMenuName = NULL;  
  8. wc.style = CS_HREDRAW | CS_VREDRAW;  
  9. wc.hbrBackground = (HBRUSH)COLOR_WINDOW;  
  10. wc.lpfnWndProc = WindowProc;  
  11. wc.lpszClassName = cls_Name;  
  12. wc.hInstance = hInstance;  

现在,你运行一下,你一定能看到窗口。

但现在你对窗口无法进行操作,因为后续的代码还没完成。

为什么现在又可以了呢?MSDN那句话的意思就是说我们在注册窗口类之前必须填充WNDCLASS结构体,何为填充,就是要为结构的所有成员赋值,就算不需要你也要为它赋一个NULL或0,因为结构在创建时没有对成员进行初始化,这就导致变量无法正确的分配内存,最后注册失败。

那么,如果一个结构体成员很多,而我只需要用到其中三个,其他的也要初始化,是不是很麻烦,是的,除了为每个成员赋值,还有一种较简单的方法,就是在声明变量时给它赋一对大括号,里面放置结构体的应该分配内存的大小,如:

[cpp] view plaincopyprint?
  1. // 设计窗口类  
  2. WNDCLASS wc = { sizeof(WNDCLASS) };  
  3. wc.hbrBackground = (HBRUSH)COLOR_WINDOW;  
  4. wc.lpfnWndProc = WindowProc;  
  5. wc.lpszClassName = cls_Name;  
  6. wc.hInstance = hInstance;  

这样一来,我们也发现,窗口也可以成功创建。

我们还可以更简单,直接把sizeof也去掉,在声明变量时,直接赋一对空的大括号就行了,就如这样。

[cpp] view plaincopyprint?
  1. WNDCLASS wc = { };  

这样写更简单,窗口类同样可以正常注册。大括号代表的是代码块,这样,结构体有了一个初值,因此它会按照结构体的大小分配了相应的内存。

 

为什么会这样呢?这里涉及到一个关于结构体的一个很有趣的赋值方式。我们先放下我们这个例子,下面我写一个简单的例子,你就明白了。

[cpp] view plaincopyprint?
  1. #include <stdio.h>  
  2.  typedef struct rectStruct  
  3.  {  
  4.      int x;  
  5.      int y;  
  6.      int width;  
  7.      int height;  
  8.  } RECT, *PRECT;  
  9.   
  10.  void main()  
  11.  {  
  12.      RECT rect = { 0, 0, 20, 30 };  
  13.      printf("矩形的坐标是:%d, %d\n矩形的大小:%d , %d", rect.x, rect.y, rect.width, rect.height);  
  14.      getchar();  
  15.  }  

在本例中,我们定义了一个表示矩形的结构体 RECT ,它有四个成员,分别横坐标,纵坐标,宽度,高度,但是,我们在声明和赋值中,我们只用了一对大括号,把每个成员的值,按照定义的顺序依次写到大括号中,即{ 0, 0, 20, 30 },x的值为0,y的值为0,width为20,height的值为30。

也就是说,我们可以通过这种简单的方法向结构变量赋值,注意值的顺序要和成员变量定义的顺序相同。

现在,回到我们的Windows程序来,我们明白了这种赋值方式,对于 WNDCLASS wc = {  } 就不难理解了,这样虽然大括号里面是空的,其实它已经把变量初始化了,都赋了默认值,这样一来,就可以正确分配内存了。

 

七、为什么不能退出

通常情况下,当我们的主窗口关闭后,应用程序应该退出(木马程序除外),但是,我们刚才运行后发现,为什么我的窗口关了,但程序不退出呢?前面我说了,要退出程序,就要先跳出消息循环,和关闭哪个窗口无关。因此,我们要解决两个问题:

1、如果跳出消息循环;

2、什么时候退出程序。

其实两个问题是可以合并到一起解决。

首先要知道,当窗口被关闭,为窗口所分配的内存会被销毁,同时,我们会收到一条WM_DESTROY消息,因而,我们只要在收到这条消息时调用PostQuitMessage函数,这个函数提交一条WM_QUIT消息,而在消息循环中,WM_QUIT消息使GetMessage函数返回0,这样一来,GetMessage返回FALSE,就可以跳出消息循环了,这样应用程序就可以退出了。

所以,我们要做的就是捕捉WM_DESTROY消息,然后PostQuitMessage.

[cpp] view plaincopyprint?
  1. // 在WinMain后实现  
  2. LRESULT CALLBACK WindowProc(  
  3.     _In_  HWND hwnd,  
  4.     _In_  UINT uMsg,  
  5.     _In_  WPARAM wParam,  
  6.     _In_  LPARAM lParam  
  7. )  
  8. {  
  9.     switch(uMsg)  
  10.     {  
  11.     case WM_DESTROY:  
  12.         {  
  13.             PostQuitMessage(0);  
  14.             return 0;  
  15.         }  
  16.     }  
  17.     return DefWindowProc(hwnd, uMsg, wParam, lParam);  
  18. }  


我们会收到很多消息,所以用switch判断一下是不是WM_DESTROY消息,如果是,退出应用程序。

好了,这样,我们一个完整的Windows应用程序就做好了。

下面是完整的代码清单。

[cpp] view plaincopyprint?
  1. #include <Windows.h>  
  2. // 必须要进行前导声明  
  3. LRESULT CALLBACK WindowProc(  
  4.     _In_  HWND hwnd,  
  5.     _In_  UINT uMsg,  
  6.     _In_  WPARAM wParam,  
  7.     _In_  LPARAM lParam  
  8. );  
  9.   
  10. // 程序入口点  
  11. int CALLBACK WinMain(  
  12.     _In_  HINSTANCE hInstance,  
  13.     _In_  HINSTANCE hPrevInstance,  
  14.     _In_  LPSTR lpCmdLine,  
  15.     _In_  int nCmdShow  
  16.   )  
  17. {  
  18.     // 类名  
  19.     WCHAR* cls_Name = L"My Class";  
  20.     // 设计窗口类  
  21.     WNDCLASS wc = { };  
  22.     wc.hbrBackground = (HBRUSH)COLOR_WINDOW;  
  23.     wc.lpfnWndProc = WindowProc;  
  24.     wc.lpszClassName = cls_Name;  
  25.     wc.hInstance = hInstance;  
  26.     // 注册窗口类  
  27.     RegisterClass(&wc);  
  28.   
  29.     // 创建窗口  
  30.     HWND hwnd = CreateWindow(  
  31.         cls_Name,           //类名,要和刚才注册的一致  
  32.         L"我的应用程序",  //窗口标题文字  
  33.         WS_OVERLAPPEDWINDOW, //窗口外观样式  
  34.         38,                 //窗口相对于父级的X坐标  
  35.         20,                 //窗口相对于父级的Y坐标  
  36.         480,                //窗口的宽度  
  37.         250,                //窗口的高度  
  38.         NULL,               //没有父窗口,为NULL  
  39.         NULL,               //没有菜单,为NULL  
  40.         hInstance,          //当前应用程序的实例句柄  
  41.         NULL);              //没有附加数据,为NULL  
  42.     if(hwnd == NULL) //检查窗口是否创建成功  
  43.         return 0;  
  44.   
  45.     // 显示窗口  
  46.     ShowWindow(hwnd, SW_SHOW);  
  47.   
  48.     // 更新窗口  
  49.     UpdateWindow(hwnd);  
  50.   
  51.     // 消息循环  
  52.     MSG msg;  
  53.     while(GetMessage(&msg, NULL, 0, 0))  
  54.     {  
  55.         TranslateMessage(&msg);  
  56.         DispatchMessage(&msg);  
  57.     }  
  58.     return 0;  
  59. }  
  60. // 在WinMain后实现  
  61. LRESULT CALLBACK WindowProc(  
  62.     _In_  HWND hwnd,  
  63.     _In_  UINT uMsg,  
  64.     _In_  WPARAM wParam,  
  65.     _In_  LPARAM lParam  
  66. )  
  67. {  
  68.     switch(uMsg)  
  69.     {  
  70.     case WM_DESTROY:  
  71.         {  
  72.             PostQuitMessage(0);  
  73.             return 0;  
  74.         }  
  75.     }  
  76.     return DefWindowProc(hwnd, uMsg, wParam, lParam);  
  77. }  

转载于:https://www.cnblogs.com/lvdongjie/p/4502532.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/375975.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

赞!带进度条的 jQuery 文件拖放上传插件

jQuery File Uploader 是一个 jQuery 文件拖放上传插件&#xff0c;包括 Ajax 上传和进度条效果。作者编写这个插件的想法是要保持它非常简单&#xff0c;不像其他的插件&#xff0c;很多的标记&#xff0c;并提供一些 Hack 的方式使之兼容那些古老的浏览器。jQuery File Uploa…

::范围解析运算符

学习笔记&#xff1a;参考链接 ::是范围解析运算符&#xff0c;或者称为域区分符&#xff0c;用来指明一个函数或一个数据属于哪一个类。 ::也可以不跟类名&#xff0c;表示全局函数或者全局数据 eg: #include<iostream> using namespace std;int month;//全局变量 i…

Linux下C语言串口应用编程,Linux下串口C语言编程

Linux下串口C语言编程 (5页)本资源提供全文预览&#xff0c;点击全文预览即可全文预览,如果喜欢文档就下载吧&#xff0c;查找使用更方便哦&#xff01;9.9 积分串口操作代码#include #include #include #include #include #include #include #include #include #define BUFFER…

开启linux ssh

kali linux在开机的时候ssh服务是默认关闭的&#xff0c;可通过以下方法开启服务

IOS 基于APNS消息推送原理与实现(JAVA后台)

Push的原理&#xff1a;Push 的工作机制可以简单的概括为下图图中&#xff0c;Provider是指某个iPhone软件的Push服务器&#xff0c;这篇文章我将使用.net作为Provider。APNS 是Apple Push Notification Service&#xff08;Apple Push服务器&#xff09;的缩写&#xff0c;是苹…

linux系统调用open、write、close、read以及stat函数详解

学习笔记 参考链接1 、参考链接2以及百度百科 在进行C语言学习的时候我们了解到了C语言相关的一些IO操作&#xff0c;如fopen&#xff0c;fwrite&#xff0c;fread&#xff0c;fprintf&#xff0c;fclose等相关函数&#xff0c;他们都是由C库函数提供的一些函数&#xff0c;是…

【转载】Ubuntu环境下配置Android Studio

之前学习Android开发的时候&#xff0c;一直跟各种教程一样&#xff0c;使用的是EclipseADT&#xff0c;主要是比较方便&#xff0c;容易上手&#xff0c;特别是对于习惯用Eclipse开发java的朋友来说&#xff0c;上手更是好无压力。但毕竟EclipseADT最多只能算Google的干儿子&a…

安装Discuz

1.下载Discuz 版本文件http://download.comsenz.com/DiscuzX/3.2/Discuz_X3.2_SC_GBK.zip2.下载PHPhttp://windows.php.net/downloads/releases/php-5.6.9-Win32-VC11-x64.zip把里面文件的php.ini-development 更名为php.ini Uncomment下列语句 extension_dir "ext"…

linux跑caffe模型的步骤,Caffe初步实践——使用训练好的模型完成语义分割任务

Caffe刚刚安装配置结束&#xff0c;乘热打铁&#xff01;(一)环境准备前面我有两篇文章写到caffe的搭建&#xff0c;第一篇cpu only &#xff0c;第二篇是在服务器上搭建的&#xff0c;其中第二篇因为硬件环境更佳我们的步骤稍显复杂。其实&#xff0c;第二篇也仅仅是caffe的初…

灵悟礼品网上专卖店——新建数据库

一、小组成员&#xff1a; 洪雪意&#xff08;产品负责人&#xff09; 陈淑筠&#xff08;Master&#xff09; 二、组内人员任务情况 计划完成的任务的第三个模块&#xff1a;分析并建立数据库 已完成的任务&#xff1a; 任务的第三个模块&#xff1a; 陈淑筠&#xff08;负责…

c语言大乐透编译,Excel大乐透摇号vba代码分享,说不定就中百万了呢

大家好我是Excel从零到一&#xff0c;今天闲来无聊做了一套大乐透摇号程序的vba代码分享给大家来看下效果Excel大乐透摇号vba代码分享&#xff0c;说不定就中百万了呢Sub 摇号()Dim i, a, test, s(1 To 35) As IntegerFor i 1 To 5line1: test Application.WorksheetFunction…

纸上谈兵: 堆 (heap)

纸上谈兵: 堆 (heap) 作者&#xff1a;Vamei 出处&#xff1a;http://www.cnblogs.com/vamei 欢迎转载&#xff0c;也请保留这段声明。谢谢&#xff01; 堆(heap)又被为优先队列(priority queue)。尽管名为优先队列&#xff0c;但堆并不是队列。回忆一下&#xff0c;在队列中&a…

最近用到这个强大的工具 PhysicsEditor (转)

今天收到PhysicsEditor作者发过来的license key&#xff0c;所以顺便把PhysicsEditor也尝试了一下。主要是尝试将PhysicsEditor与cocos2dx&#xff0c;box2d结合开发的一些步骤。之前大概网络检索了一下&#xff0c;知道PhysicsEditor的功能其实很简单。一句话就是给图片的边缘…

如何更新Chrome

在浏览器的地址栏中输入chrome://help即可进行自动更新&#xff0c;如下图&#xff1a;

我也要谈谈大型网站架构之系列(4)——分布式中的异步通信

我们知道在面向对象编程中&#xff0c;总会想着各种办法来实现代码的解耦&#xff0c;从而让项目中的各种人员面对自己熟悉的业务进行开发&#xff0c; 做到术业有专攻&#xff0c;比如大家非常熟悉的三层架构&#xff0c;MVC&#xff0c;MVP以及MVVM模式&#xff0c;让前端设计…

node模块函数图解

已截图方式记录模块信息&#xff1a; HTTP模块&#xff1a; 对于网络返回处理状态封装了很多种&#xff0c;我已截图展现 以上状态也是在http协议中包含的状态。 http函数&#xff1a; path模块&#xff1a; 转载于:https://www.cnblogs.com/kuailingmin/p/4547538.html

android 心跳效果动画,Android实现心跳的效果

最近再做一个教育类的项目。在做一些学习工具的时候&#xff0c;美工提出了一些要求&#xff0c;大致如下&#xff1a;其实实现过程也不难&#xff0c;大致就是对一个视图控件添加一个圆形的背景&#xff0c;然后该视图进行动画处理&#xff0c;膨胀的同时&#xff0c;透明度增…

操作系统上机作业--使用系统调用实现mycat

mycat.c的功能与系统cat程序相同mycat将指定的文件内容输出到屏幕&#xff0c;例子如下&#xff1a;要求使用系统调用open/read/write/close实现 $ cat /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/u…

微信android版字体,微信炫彩字下载-微信七彩字体 安卓版v1.6.2-PC6安卓网

微信七彩字体一款方便的手机字体更换软件&#xff0c;微信炫彩字软件集合了上百款优质中文美化字体&#xff0c;微信七彩发光字里有可爱的喵呜体、卡通体&#xff0c;清秀的静蕾体等多种字体。软件介绍微信、qq上最好用、最个性的聊天字体应用&#xff0c;让你的聊天与众不同&a…

Android SQLite 数据库 增删改查操作

Android SQLite 数据库 增删改查操作 转载▼一、使用嵌入式关系型SQLite数据库存储数据在Android平台上&#xff0c;集成了一个嵌入式关系型数据库——SQLite&#xff0c;SQLite3支持NULL、INTEGER、REAL&#xff08;浮点数字&#xff09;、TEXT(字符串文本)和BLOB(二进制对象…