这两天阿瘦找我给他的一个程序写个界面,听说是要参加啥三创比赛(都大四老狗了,汗),然后问要用什么语言——C/C++,Windows平台的。他之前没怎么接触过C++方面的界面开发,然后我就开始了一波Windows教学,顺便自己也回忆回忆(大一大二玩了一年多,之后几乎就没碰过)。
整体流程
1 |
|
看到上面这段代码,倍感亲切啊
Windows程序的入口
我们知道标准C的入口函数是main
,对于Windows程序来说程序的入口函数是WinMain
。
不要被
main
函数限制了你的想象力,main
只是入口函数的标记,程序运行时会根据main
所指向的函数地址找到要从哪条指令开始执行。我们编译链接的时候可以自行指定这个标记。在VC的链接器中有**/SUBSYSTEM:CONSOLE和/SUBSYSTEM:WINDOWS**两个选项
应用程序类型 入口函数(入口点) 嵌入执行体的启动函数 处理ANSI字符和字符串的GUI应用 程序 _tWinMain (WinMain) WinMainCRTStartup 处理Unicode字符和字符串的GUI应 用程序 _tWinMain (wWinMain) wWinMainCRTStartup 处理ANSI字符和字符串的CUI应用 程序 _tmain (Main) mainCRTStartup 处理Unicode字符和字符串的CUI应 用程序 _tmain (Wmain) wmainCRTStartup 如果指定了**/SUBSYSTEM:WINDOWS链接器开关,链接器就会寻找WinMain或wWinMain函数。如果没有找到这两个函数,链接器将返回一个“unresolved external symbol(未解析的外部符号)”错误;否则,它将根据具体情况分别选择WinMainCRTStartup或wWinMainCRTStartup**函数。
这篇文章演示了gcc和vc两种编译器自定义入口函数。
WinMain
函数的定义如下:
1 | // 返回值为int类型,如果程序在进入消息循环之前就结束了,应该返回0 |
消息循环机制
消息循环(message loop)机制是整个Windows应用程序的核心。
Windows界面程序是基于事件驱动的,在启动一个进程后,操作系统会为它维护一个单独的消息队列。操作系统会将窗口上的操作以消息的形式放入消息队列,比如鼠标在窗口上移动,窗口获取焦点后键盘敲击,点击窗口的按钮等。
程序员通过调用GetMessage
函数从消息队列中获取消息,如果队列中没有消息,GetMessage
函数将会阻塞。然后调用DispatchMessage
将消息交给程序员定义的消息回调函数WndProc
处理。
注意:整个过程都是在主线程处理的,
WndProc
也会在主线程中回调,所以WndProc
中不要执行一些耗时的操作,比如网络请求、耗时的计算,否则主线程阻塞在这,处理不了其他的消息,将会导致窗口“假死”。
消息循环机制不仅存在Windows程序中,其实安卓、Web这些带UI界面的程序本质上都是基于消息循环机制。
GetMessage
函数是阻塞式的,如果消息队列中没有消息这个函数会发生阻塞,这时循环是“静止”的,这往往意味着低的CPU占用,对系统、对其它应用程序都是友好的。
另外还有一个PeekMessage
的函数,它可以检测消息队列中是否有消息,如果没有消息它会返回而不是发生阻塞,如果有消息可以通过最后一个参数传递**PM_NOREMOVE
或PM_REMOVE
**决定是否从消息队列中移除消息。
1 | HWND hwnd; |
所以游戏编程里经常会出现这样的代码(DX红龙书上的代码片段):
1 | MSG msg; |
参考:
回调函数-消息处理函数
上面WinMain的代码,90%以上都是“样板代码”:通常创建一个Windows窗口应用就必须按照这个流程,可变化的地方很少。
我们实际要干的活都在WndProc这个回调函数中。
在让我们认识一下这个回调函数:
1 | LRESULT CALLBACK WndProc(HWND hwnd, UINT Message, WPARAM wParam, LPARAM lParam) { |
窗口回调函数有四个参数,事实上这四个参数都来MSG
这个消息结构体,因为窗口回调函数就是用来处理消息的:
1 | typedef struct tagMSG { |
message是消息的ID号,UNIT类型的有四个字节,不过能用的只有两个低字节(0x0000~0xFFFF),高字节被系统保留。
消息主要分为两种:系统消息和用户自定义消息
- 系统消息:0x0000 ~ 0x03FF(
WM_USER
-1) - 用户自定义消息:用户自定义消息也分两种
- 窗口类私有消息:0x0400(
WM_USER
) ~ 0x7FFF(WM_APP
-1) - 应用程序私有消息:0x8000(
WM_APP
) ~ 0xBFFF还有一种消息范围在0xC000 ~ 0xFFFF,这个范围内的消息是通过
RegisterWindowMessage
函数进行注册得到的,这些消息ID能保证在整个操作系统是唯一的。
- 窗口类私有消息:0x0400(
大多数情况下我们都是对系统消息进行处理,这些系统消息的ID都定义在在WinUser.h
头文件中。
1 | /* |
消息很多,每个消息的附加参数意义也不一样,你不可能靠脑子去记的,所以需要经常查文档:https://msdn.microsoft.com/EN-US/library/windows/desktop/ms644927.aspx#system_defined
参考:
句柄与指针
前面频繁出现的一个东西HWND——窗口句柄。当然除了窗口句柄(HWND),还有应用实例句柄(HINSTANCE)、文件句柄(HFILE)等各种句柄。有了这些句柄我们就可以操作对应的Windows对象。
前面为了方便理解,我们把它叫做指针。每一个程序员都有一颗好奇的心,你肯定尝试过使用这个“指针”去看看它指向的那段内存到底存的是不是窗口或者应用实例,但现实让你碰了一鼻子灰——句柄和指针还是不同的。
指针指向系统中物理内存的地址,而句柄是windows在内存中维护的一个对象内存物理地址列表的整数索引,句柄是一种指向指针的指针
使用Windows对象的句柄规范对系统资源的访问,这主要有两个原因:
- 给你一个Windows对象句柄而不是整个对象,是因为微软更新Windows系统时可能会修改Windows对象的内部结构(比如多加两个字段),但是API中全部使用句柄,那么系统维护的时候就可以尽可能少的修改API接口,让Windows开发者也尽可能少的修改代码或者不修改代码。
- 为了系统安全性,Windows内部为每个对象维护了一个访问控制列表(ACL),只有指定进程可以在对象上操作,每次为对象创建句柄的时候,系统都会去检查对象的ACL。
关于Windows ACL的详细内容可以参考:https://msdn.microsoft.com/en-us/library/windows/desktop/aa374860.aspx
对指针和句柄的理解,我个人觉的这篇文章讲的非常好,言简意赅:https://blog.csdn.net/u014041012/article/details/44878375
参考: