期中考试结束了,又回想起了期中考试前花了八天时间搞出来的小脚本,决定完善一下,增加点稳定性。
为什么要学习win32编程
脚本当时最大的问题就是浏览器最小化之后,视频的自动切换下一集的功能出现了故障,导致可能花了4,5集的时间结果只播放了一集。花了一天时间研究了一下chrome浏览器的工作原理,发现了setInterval函数的延迟不由JavaScript引擎完成而是由浏览器的一个定时器线程完成的,浏览器最小化之后那个线程停止运行导致setInterval函数延迟出错。具体的工作原理参见我的另一篇文章浅析chrome的工作原理。
于是决定修复它。想了两种思路,第一种是借助Chrome的扩展程序,扩展程序可以获取活动选项卡,而且浏览器最小化后不会停止运行。但是还是没法保证setInterval函数在浏览器最小化之后仍然能够正常运行。第二种思路就是借助WebAssembly在js代码中嵌入底层c++代码,借助Windows的API来实现对浏览器相关线程的控制,这样就可以避免定时器线程暂停的问题。当然这个也是一个初步的思路,可行性只有在具体实践之后才能知道。(可能还需要借助扩展程序完成)
既然要在底层通过c++来操控浏览器,那win32编程是绕不过了。但也只是初步的把一些需要用到的知识学一下,顺便了解一下windows系统的运作原理等等。顺便把C语言也复习复习吧。ok,那就开始。
最基本的操作–创建窗口
0x00 引入
我们就先来一个最简单的windows应用程序。
1 | #include <windows.h> |
这段代码运行后会生成一个命令行界面和对话框,图标为Windows默认图标,标题为”Hello Windows!”,内容为”Hello Windows!”,再加上一个”确定”按钮。
恭喜我们成功生成了一个命令行外的东西–对话框!我们走出了摆脱命令行的巨大一步。
当然这个程序非常简单,而且说到底这个程序依然是属于c++程序,不能算是Windows程序,因为他的入口点函数依然是main函数,main函数是控制台应用程序的入口点。所以程序运行的时候依然会出现一个shell的界面。
那么我们如何不让他生成这个命令行呢,我们需要换一种思路了。
0x01 创建入口点函数WinMain
Windows编程中,我们需要使用WinMain函数来创建程序入口点。于是我们把上述代码修改一下:
1 | #include <windows.h> |
运行,发现刚刚的命令行没有了,只剩下一个对话框了!!!
我们来解释一下这个函数吧。这个WinMain函数是Windows程序的应用程序入口点,程序将从WinMain处开始执行。有一点需要注意,c++中的main函数里面的参数是可以不写的,但这里不行。四个参数都要写。
WinMain函数前面带了一个CALLBACK,这个CALLBACK是什么意思呢,我们再IDE上按住CTRL键左键点击这个函数转到函数定义,发现CALLBACK是一个宏,原型如下:
1 | #define CALLBACK __stdcall |
这时我们发现这个CALLBACK其实就是__stdcall,那么__stdcall是什么?这个其实是跟C语言的__cdecl对应的,都是一种调用约定,这个可以不用具体深究它,可以将这个__stdcall理解为专门来调用Win API函数的就行了。
CALLBACK有什么特别功能吗?字面意思”callback”是回调的意思。我们的WinMain函数由操作系统调用,但WinMain函数具体做什么,操作系统就不管了,它有可能回头调用操作系统的一些API都是没有问题的。
下面我们来分析WinMain的这四个参数。我们经常会看到诸如HANDLE,HINSTANCE,HBRUSH,WPARAM。LPARAM,HICON,HWND等一大串数据类型,感觉好多好复杂啊……其实并不是,他们说到底还是C++的那几种基本数据类型,只是微软为了方便使用而重新定义了他们罢了。例如HINSTANCE类型,CTRL加鼠标左键转到定义,发现HINSTANCE也是一个宏定义:
1 | typedef void *HINSTANCE; |
第一个参数hInstance是当前应用程序的实例句柄,第二个参数hPrevInstance是前一个应用程序的实例句柄(如果你把这个应用程序开了两个的话),比如我把a.exe运行了两个实例,进程列表中会有两个a.exe,这时候第一次运行的实例号假设为01,就传递第一个参数hInstance,第二次运行的假设实例号为02,就传给了hPrevInstance参数。
第三个参数lpCmdLine名字上就可以理解到是命令行参数,但是LPSTR是什么意思呢?它是一个字符串,char*类型(注意,Windows编程中,所有以lp开头的,均为指针类型,比如LPSTR可以理解为long point string)
第四个参数是nCmdShow,它指明了主窗口显示方式,它有以下几个值:
Value | Meaning |
---|---|
SW_HIDE 0 | Hides the window and activates another window. |
SW_MAXIMIZE 3 | Maximizes the specified window. |
SW_MINIMIZE 6 | Minimizes the specified window and activates the next top-level window in the Z order. |
SW_RESTORE 9 | 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_SHOW 5 | Activates the window and displays it in its current size and position. |
SW_SHOWMAXIMIZED 3 | Activates the window and displays it as a maximized window. |
SW_SHOWMINIMIZED 2 | Activates the window and displays it as a minimized window. |
SW_SHOWMINNOACTIVE 7 | Displays the window as a minimized window. This value is similar to SW_SHOWMINIMIZED, except the window is not activated. |
SW_SHOWNA 8 | Displays the window in its current size and position. This value is similar to SW_SHOW, except the window is not activated. |
SW_SHOWNOACTIVE 4 | Displays a window in its most recent size and position. This value is similar to SW_SHOWNORMAL, except the window is not activated. |
SW_SHOWNORMAL 1 | 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,我们还需要一个WindowProc函数,这个函数是用来处理传给我们应用程序的消息的(即消息处理函数)。该函数定义如下:
1 | //这个函数也可以不叫WindowProc,可以命名成任何你想要的名字,但是命名成WindowProc更利于理解,而且也符合一般的消息处理函数的命名法则。 |
WindowProc用来处理源源不断从操作系统发送来的消息,当然如果我们对这些消息不感兴趣或者不想处理的话,就交给DefWindowProc函数来处理,这是Windows系统默认的消息处理函数。
其中:
hWnd是当前窗口的句柄。
uMsg是系统发过来的消息。
wParam是消息参数。
lParam是消息参数。
这个函数也带有CALLBACK,同理,这也是操作系统调用的函数。
0x02 设计和注册窗口类
设计窗口类,其实就是设计我们程序的主窗口,如有没有标题栏,背景什么颜色,有没有边框,可不可以调整大小等。要设计窗口类,我们用到一个结构————
1 | typedef struct tagWNDCLASS { |
通常情况下,我们用tagWNDCLASS就可以了,当然还有一个tagWNDCLASSEX的扩展结构,在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拿来用就行了,这些都比较好理解,看名字就知道了。
1 | COLOR_ACTIVEBORDER |
lpszMenuName指的是菜单的ID,没有菜单就NULL,lpszClassName就是我们要向系统注册的类名,字符,但不能与系统已存在的类名冲突。
随后,我们在WinMain中设计窗口类。
1 | // 类名 |
窗口类设计完成后,不要忘了向系统注册,这样系统才能知道有这个窗口类的存在。向操作系统注册窗口类,使用RegisterClass函数,它的参数是一个指向WNDCLASS结构体的指针,所以我们传递的时候,要加上&符号。
1 | // 注册窗口类 |
0x03 创建和显示窗口
窗口类注册完成后,就应该创建窗口,然后显示窗口,调用CreateWindow创建窗口,如果成功,会返回一个窗口的句柄,我们对这个窗口的操作都要用到这个句柄。什么是句柄呢?其实它就是一串数字,只是一个标识而已,内存中会存在各种资源,如图标、文本等,为了可以有效标识这些资源,每一个资源都有其唯一的标识符,这样,通过查找标识符,就可以知道某个资源存在于内存中哪一块地址中。
创建窗口的函数CreateWindow返回一个HWND类型,它就是窗口类的句柄。函数原型如下:
1 | HWND WINAPI CreateWindow( |
(好复杂QAQ,嗯看着注释就能理解了。)
调用一下这个函数:
1 | // 创建窗口 |
窗口创建后,就要显示它,就像我们的产品做了,要向客户展示。显示窗口调用ShowWindow函数。
1 | ShowWindow(hWnd, SW_SHOW); |
既然要显示窗口了,那么ShowWindow的第一个参数就是刚才创建的窗口的句柄,第二个参数控制窗口如何显示,你可以从SW_XXXX中选一个,也可以用WinMain传进来的参数,还记得WinMain的最后一个参数吗?没错,就是它了。
0x04 (可选)更新窗口
为什么更新窗口这一步可有可无呢?因为只要程序在运行着,只要不是最小化,只要窗口是可见的,那么,我们的应用程序会不断接收到WM_PAINT通知。(关于WM_PAINT后面再说)更新窗口调用的是UpdateWindow函数。
1 | UpdateWindow(hWnd); |
0x05 (important)消息循环
Windows操作系统是基于消息控制机制的,用户与系统之间的交互,程序与系统之间的交互,都是通过发送和接收消息来完成的。
我们知道,代码是不断往前执行的,像我们刚才写的WinMain函数一样,如果你现在运行程序,你会发现什么都没有,是不是程序不能运行呢,不是,其实程序是运行了,只是它马上结束了,只要程序执行跳出了WinMain的右大括号,程序就会结束了。那么,要如何让程序不结束了,可能大家注意到我们在C程序中可以用一个getchar()函数来等到用户输入,这样程序就人停在那里,直到用户输入内容。但我们的窗口应用不能这样做,因为用户有可能进行其他操作,如最小化窗口,移动窗口,改变窗口大小,或者点击窗口上的按钮等。因此,我们不能简地弄一个getchar在那里,这样就无法响应用户的其他操作了。
于是我们需要引入消息循环,只要有与用户交互,系统人不断地向应用程序发送消息通知,因为这些消息是不定时不断发送的,必须有一个绶冲区来存放,就好像你去银行办理手续要排队一样,我们从最前端取出一条一条消息处理,后面新发送的消息会一直在排队,直到把所有消息处理完,这就是消息队列。
要取出一条消息,调用GetMessage函数。函数会传入一个tagMSG结构体的指针,当收到消息,会填充tagMSG结构体中的成员变量,这样我们就知道我们的应用程序收到什么消息了,直到GetMessage函数取不到消息,条件不成立,循环跳出,这时应用程序就退出。
tagMSG结构定义如下:
1 | typedef struct tagMSG { |
hWnd就不用说了,就是窗口句柄,哪个窗口的句柄?还记得WindowProc回调函数吗?你把这个函数交给了谁来处理,hWnd就是谁的句柄,比如我们上面的代码,我们是把WindowProc赋给了新注册的窗口类,并创建了主窗口,返回一个表示主窗口的句柄,所以,这里MSG中的hWnd指的就是我们的主窗口。
message就是我们接收到的消息,它是个无符号整数,所以我们操作的所有消息都是数字来的。wParam和lParam是消息的附加参数,也是数值来的。通常,lParam指示消息的处理结果,不同消息的结果(返回值)不同,具体参阅MSDN。
有了一个整数值来表示消息,我们为什么还需要附加参数呢?你不妨想一下,如果接收一条WM_LBUTTONDOWN消息,即鼠标左键按下时发送的通知消息,那么,我们不仅需要知道左键按下,我们更感兴趣的是,鼠标在屏幕上的哪个坐标处按下左键,按了几下,这时候,你只靠一条WM_LBUTTONDOWN是无法传递这么多消息的。可能我们需要把按下左键时的坐标放入wParam参数中;最典型的就是WM_COMMAND消息,因为只要你使用菜单,点击按钮都会发送这样一条消息,那么我怎么知道用户点了哪个按钮呢?如果窗口中只有一个按钮,那好办,但是,如果窗口上有10个按钮呢?而每一个按钮被单击都会发送WM_COMMAND消息,你能知道用户点击了哪个按钮吗?所以,我们要把用户点击了的那个按钮的句柄存到lParam参数中,这样一来,我们就可以判断出用户到底点击了哪个按钮了。
GetMessage函数声明如下:
1 | BOOL WINAPI GetMessage( |
这个函数在定义时带了一个WINAPI,应该猜到,它也是一个宏,而真实的值是__stdcall,前文中说过了。
第一个参数是以LP开头,还记得吗,我说过的,你应该想到它就是 MSG* ,一个指向MSG结构的指针。第二个参数是句柄,通常我们用NULL,因为我们会捕捉整个应用程序的消息。后面两个参数是用来过滤消息的,指定哪个范围内的消息接收,在此范围之外的消息拒收,如果不过滤就全设为0。
1 | // 消息循环 |
TranslateMessage是用于转换按键信息的,因为键盘按下和弹起会发送WM_KEYDOWN和WM_KEYUP消息,但如果我们只想知道用户输了哪些字符,这个函数可以把这些消息转换为WM_CHAR消息,它表示的就是键盘按下的那个键的字符,如“A”,这样我们处理起来就更方便了。
DispatchMessage函数是必须调用的,它的功能就相当于一根传送带,每收到一条消息,DispatchMessage函数负责把消息传到WindowProc让我们的代码来处理,如果不调用这个函数,我们定义的WindowProc就永远接收不到消息,此时应用程序无响应。(死机啦……)
0x06 消息响应
其实现在我们的应用程序是可以运行了,因为在WindowProc中我们调用了DefWindowProc,函数,消息我们不作任何处理,又把控制权路由回到操作系统来默认处理,所以,整个过程的消息循环是成立的,只不过我们做默认响应罢了。
0x07 完整的创建窗口代码(不是Visual Studio自动生成的)
1 | #include <Windows.h> |
0xFF 什么?你说窗口创建失败?
是啊,你的代码为什么窗口一创建就退出了?仔细想想……初学C语言的时候,老师告诉我们(或者自学的时候书上有提到)定义一个变量如果不初始化就直接运算操作的话,会出现未知错误。现在回到我们的代码里。我们发现WNDCLASS结构体成员没有初始化,好了,发现问题。
1 | WNDCLASS wc; |
但是这样初始化它未免太麻烦,不如我们换个方式吧。
1 | WNDCLASS wc = { }; |
至于为什么可以这样,复习一下C语言基础吧。我们换一个简单的例子来看。
1 |
|
我们定义了一个表示矩形的结构体 RECT ,它有四个成员,分别横坐标,纵坐标,宽度,高度,但是,我们在声明和赋值中,我们只用了一对大括号,把每个成员的值,按照定义的顺序依次写到大括号中,即{ 0, 0, 20, 30 },x的值为0,y的值为0,width为20,height的值为30。
也就是说,我们可以通过这种方法向结构变量赋值。
再回到那句代码WNDCLASS wc = { };
上,是不是好理解一些了?我们通过这种巧妙的办法为结构体的所有变量赋了初值。
0xFFFF 什么?程序不能退出?你在逗我?
通常情况下,当我们的主窗口关闭后,应用程序应该退出(木马程序除外),但是,我们刚才运行后发现,为什么我的窗口关了,但程序不退出呢?我们知道,要退出程序,就要跳出消息循环,而与关闭哪个窗口是无关的。因此,我们要解决两个问题:
1.如果跳出消息循环;
2.什么时候退出程序。
其实两个问题可以合并到一起解决。
当窗口被关闭后,为窗口所分配的内存会被销毁,同时,我们会收到一条WM_DESTROY消息,因而,我们只要在收到这条消息时调用PostQuitMessage函数,这个函数提交一条WM_QUIT消息,而在消息循环中,WM_QUIT消息使GetMessage函数返回0,这样一来,GetMessage返回FALSE,就可以跳出消息循环了,这样应用程序就可以退出了。
所以,我们要做的就是捕捉WM_DESTROY消息,然后PostQuitMessage.
我们修改WindowProc函数如下:
1 |
|
我们会收到很多消息,所以用switch判断一下是不是WM_DESTROY消息,如果是,退出应用程序。
好了,这样,我们一个完整的Windows应用程序就做好了。
下面是完整的代码(这回没问题了):
1 | #include <Windows.h> |