if (!hWnd)
return FALSE;
验证窗口是否被成功创建,配合前文提到的代码可实现 “如果窗口创建失败就自动退出” 的功能。
ShowWindow
显示窗口。
顾名思义,让创建好的窗口显示出来。
这里终于用上了前面提到的 nCmdShow。
UpdateWindow
更新一次窗口,用于让窗口里除了控件以外的东西正常显示。
return
返回真,代表窗口成功创建。
4、 WndProc
到这里几个用于创建窗口的函数就都结束了,接下来是窗口过程函数,也就是用于为窗口添加内容,处理用户操作的函数。
前面应该提过,这个函数不需要我们自己调用,它是由前面的消息循环(while)中的某个 API 函数调用的。每当程序收到一个消息,这个函数就会被运行一次。
看看参数:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
窗口句柄。
message
收到的消息正文。
wParam / lParam
收到的消息的附加内容。
在模板里,这个函数里面就是一个 switch 语句,用于判断收到的消息是什么。
介绍 4 个最基础的消息,剩余的可以去 MSDN 查找。
主要的一些消息:https://docs.microsoft.com/en-us/windows/win32/winmsg/window-notifications
更多的内容(不止关于消息):https://docs.microsoft.com/en-us/windows/win32/winmsg/windowing
WM_COMMAND
当程序中的任意控件被触发(如用户点击按钮)都会收到这个消息,需要依靠附加内容判断具体被触发的控件。
int wmId = LOWORD(wParam);
wmId 的值就是控件的 Id,因此继续用 switch 判断就可以了。
模板里的 Id 因为代表的都是在资源文件里定义的内容(菜单,对话框之类的),所以都是定义在资源文件里的。以后我们自己使用代码创建按钮等控件时,选择 Id 的值以及为了方便阅读代码而定义宏之类的操作都需要我们自己来做。
WM_PAINT
窗口重绘消息。
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 在此处添加使用 hdc 的任何绘图代码...
EndPaint(hWnd, &ps);
这一堆东西都是为了在创建出的窗口中绘图而准备的。
我并不太了解 GDI 的各种函数,因此读者可以自行查阅 MSDN,并在注释:
// TODO:
之后,EndPaint 之前编写自己的绘图代码。
注意,GDI+ 需要额外头文件和静态链接库,具体是:
#include <objidl.h>
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
C++ 中,GDI+ 的命名空间是 Gdiplus。
微软没有对 C 语言提供 GDI+ 支持。
WM_CREATE
窗口被创建时收到的消息。
这个消息被收到时,CreateWindow 函数还没有返回。
WM_DESTROY
窗口将被销毁时收到的消息,最常见的情况是用户点击了窗口右上角的 X。
此时窗口还没被销毁,可以在这里做一些保存之类的操作。
前面讲消息循环的时候我提到过有个函数会发送真正的退出消息 WM_QUIT,就是下面这个:
PostQuitMessage
它的唯一一个参数,我前文有提到会保存在 msg.wParam 里并最终 return 给操作系统。一般没出错的程序传 0 就行。
Windows 有非常多的消息,有些消息我们在程序里用不到,因此需要让操作系统帮我们处理。switch 的 default 分支承担了此工作。
DefWindowProc
这个函数的 4 个参数对应了窗口过程函数的 4 个参数,直接把窗口过程函数的形参作为这个函数的实参传进去就可以。
补充一个函数:
DestroyWindow
窗口将要被关闭时会收到 3 个参数,按先后顺序排列是:
WM_CLOSE WM_DESTROY WM_QUIT
DefWindowProc 如果收到了 WM_CLOSE,就会调用 DestroyWindow 给窗口发送 WM_DESTROY,我们收到 WM_DESTROY,再调用 PostQuitMessage 让程序收到 WM_QUIT 进而退出消息循环。
如果我们想自定义一个控件用于退出程序,但 WM_DESTROY 里又有需要在关闭前运行的代码,那么就可以在 WM_COMMAND 的处理逻辑中调用 DestroyWindow,达到使用自定义方式关闭程序的同时不浪费 WM_DESTROY 中的代码的效果。
非常开心啊,写gu了半年,终于把创建窗口的基本流程写完了。接下来是一些更高级的内容。
四、 通过控件让用户与程序交互
控件,是用户与程序交互的途径,Win32 程序可以使用下列几大类控件:
通过设置不同的样式,可以做出非常多种不同的控件。
正式编程前,先介绍微软官方的一个小工具:Control Spy,这是一个方便开发者尝试控件的软件,功能非常强大,读者可以自行尝试研究一下。
前面提到过,Win32 中,每个控件都是一个窗口,因此,显而易见的,创建控件的函数也是 CreateWindow。
为了方便,这里直接介绍 CreateWindowEx。
CreateWindowEx
lpWindowName,x,y,nWidth,nHeight,hWndParent,hInstance,lpParam 参数的功能均与 CreateWindow 一致,这里不再重复。注意创建控件的时候,hWndParent 填刚才创建好的窗口的句柄,x / y 是相对窗口左上角的坐标。
dwStyle
首先,因为控件是子窗口,因此要加上 WS_CHILD。
其次,Windows 将一些相近的控件放到了一个类里面,因此,在 Control Spy 里能看到这个参数随着控件的改变还可以填一些别的值。大多数值的意义可以通过宏定义的名称理解,因此读者可以通过 Control Spy 自己试一试各个值的含义。记得改变值以后要点击右边的 Apply 按钮。
dwExStyle
窗口的扩展样式,在 MSDN 里有详细介绍可选的值的作用。
如果用不上这个参数的话,请直接使用 CreateWindow。除了这个扩展样式的参数外,这两个函数剩下的参数表示的意义相同。
lpClassName
窗口类名,创建控件时则是微软预定义的控件类名。
很遗憾,我并没有找到微软对这个控件类名的汇总,因此,介绍一个新软件:Spy++。
这是 VS 自带的一个软件,我们主要用到它的搜索功能。
在 VS 上方菜单栏中找到“工具”-“Spy++”,就能打开这个软件。打开以后内容很复杂,我们不用管,继续在新打开的软件的菜单栏里找到“搜索”-“查找窗口”,点击它。
上面的 GIF 图为剩余操作步骤,按步操作即可。
hMenu
控件的唯一 ID。
微软认为控件不需要菜单,因此这个参数就被用来标识控件了。
消息循环中 WM_COMMAND 消息只能告诉我们有控件被触发,而附加消息里的值就是在这个参数里指定的。
当此参数用于标识控件时,传入的值必须是整数,且需要 (HMENU) 强制转换类型。
为了增强程序可读性,可以定义宏定义来代表 ID,如:
#define IDC_MYBTN 1234
由于这个 ID 是我们指定的,因此,上面例子中的 1234 可以是任意的数,只要确保没有重复即可。
如果留心的话,读者应该能发现上面创建出来的控件是“拟态”风格(就像现实中突出来的一个按钮一样),而更多的程序的控件则是蓝边框,鼠标移上去有渐变效果的版本。
这其实可以在链接的时候选择的,如果你的编译环境高于 VS 2015,那么在源文件最上方加入下面这行代码,就可以让控件使用上面我提到的新样式了。
#pragma comment(linker,"\"/manifestdependency:type='win32' \name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
关于控件的创建就是这些内容,读者可以参考 VS 的模板代码,通过在 WM_COMMAND 消息处理代码的 switch 语句中增加分支,来响应自己的控件。
分支的值和前面创建控件时定义的标识 ID 的值需要完全对应。
五、 使用对话框快捷的创建窗口与控件
用 CreateWindow 创建控件其实是一件很麻烦的事,所以可以拖控件创建窗口的对话框就出现了。
观察一下 VS 的模板,应该可以发现“About”这个窗口就是用对话框创建的。
这项技术对于创建简单窗口来说非常方便,唯一的缺点是对高分屏和系统缩放的支持很糟糕(Windows 传统),会出现文字模糊等情况。
下面来具体说一下用对话框作为主窗口的过程。
1、 创建一个对话框
参照上图创建一个对话框(上传此图时imgtu出了点问题,因此换用sm.ms图床,速度可能较慢,见谅)。
可以看到 VS 为我们准备了功能非常丰富的可视化编辑器,读者可自行尝试用此工具设计程序的界面。
MFC 的控件虽然显示在了控件工具箱区域里,但是是不可以使用的(会导致窗口无法被显示)。
2、 将对话框作为主窗口
对话框不涉及窗口类的内容,所以直接在 WinMain 里用 CreateDialog 函数创建就行。
这种情况下,WinMain 里只包括 CreateDialog,ShowWindow 和关于消息循环部分的代码(对话框创建窗口的消息循环部分的代码与传统方式创建窗口的代码完全相同)。
CreateDialog
这也是一个嵌套宏定义,默认指向 CreateDialogW。
看看参数:
hInstance
当前实例句柄,用 WinMain 的形参 hInstance 就行。
lpName
对话框的 ID,使用 MAKEINTRESOURCE 宏创建,如:
MAKEINTRESOURCE(IDD_MYDLG)
IDD_MYDLG 部分请参考上面的动图填写创建出来的对话框的真实 ID。
hWndParent
对话框的父窗口。
我们要把对话框当作主窗口使用,因此请在此参数位置填写 GetDesktopWindow(),直接使用此函数获取桌面的窗口句柄并作为参数使用。
lpDialogFunc
对话框过程函数。
请填写函数名,但末尾不要加括号,并且需要 (DLGPROC) 强制转换类型。
对话框也需要过程函数,要求参数和返回类型是:
INT_PTR Dlgproc(
HWND unnamedParam1,
UINT unnamedParam2,
WPARAM unnamedParam3,
LPARAM unnamedParam4
{...}
Dlgproc 可以自由填写,在上面的 lpDialogFunc 中指定即可。
对话框的过程函数与传统窗口的过程函数处理代码几乎完全相同,处理控件消息的方式也一样。区别大约有三点:创建操作,删除操作,让消息由系统代处理。
对话框窗口过程函数中让系统代处理消息
传统窗口中,我们使用 DefWindowProc 函数忽略消息,但在对话框中,万万不可这样做。
对话框中的操作其实更简单:如果我们处理了消息,就让窗口过程函数返回 TRUE;如果我们打算忽略这条消息,让系统处理它,那么直接返回 FALSE 就行。
注意:TRUE 和 FALSE 都是宏定义,不要用 C++ 自带的 bool 类型的 true / false 或者 C 语言新标准的 _Bool 类型代替。
WM_INITDIALOG
对话框完全创建完成后会收到这条消息,此时可以对对话框进行操作。
注意:此消息的返回值带有特殊含义,当返回 TRUE 时,键盘焦点会自动移动到这个对话框上,当返回 FALSE 时则不会改变键盘焦点。
对话框的关闭消息
此消息在 WM_SYSCOMMAND 中。
当对话框收到 WM_SYSCOMMAND 消息,且 wParam 等于 SC_CLOSE 时,就代表用户执行了关闭对话框的操作。
此时可以用 DestroyWindow 函数 或 PostQuitMessage 函数关闭对话框并结束消息循环。
关于对话框的内容就介绍到这里,读者可参考 VS 模板中 About 窗口的代码与传统窗口的过程函数,来处理可视化制作出的对话框上的各种控件的消息。
(对话框分模态对话框和非模态对话框,本节介绍的是非模态对话框,即 CreateDialog 函数执行完后会立刻返回。模态对话框的创建要比非模态对话框简单,具体代码参看 VS 模板的 About 窗口即可。)
六、 使用 EasyX 在窗口中绘图。
鉴于EasyX作者面对网络的极度保守的思想,不再保留这部分内容。
当然实现这个功能还是很简单的,直接复制dc就行。
七、 高 DPI 支持
这方面的内容比较复杂,下面是一篇来自看雪论坛的文章,非常详细的介绍了这部分内容,可以参考。
《Win32应用程序DPI适配的设计与实现》
https://bbs.pediy.com/thread-267416.htm
(已获得转载链接授权)
关于对话框,在 Win10 1703 版本以上,有一个方便的解决办法:
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
(所以下载 Windows sdk 的时候要下最新版啊,要不然就只能 函数指针 + LoadLibrary + GetProcAddress 从 dll 里动态加载函数了。)
八、 定时器
这东西的效果是定时通知程序(发消息 / 调用回调函数)
主要是两个函数 SetTimer,KillTimer。
SetTimer
设置一个定时器,有 4 个参数。
要通知的窗口的句柄。
这个参数不能为 NULL,需要填写上面 CreateWindow 函数,或 CreateDialog 函数的返回值。
nIDEvent
定时器的标识。
看类型就能知道,这里填写一个非 0 的,不重复的 unsigned int 类型的数就行。
uElapse
每两次通知之间的时间间隔,以毫秒为单位。
注:1000 毫秒 = 1 秒。
lpTimerFunc
回调函数,填回调函数名,不加括号。
当此参数为 0 时,定时器会按照参数定期向窗口过程函数发送消息 WM_TIMER。
但因为这个消息的优先级低,因此时间很可能不准,所以不推荐使用。
回调函数的定义:
VOID CALLBACK TimerProc (
HWND hwnd,
UINT message,
UINT iTimerID,
DWORD dwTime)
//此处的代码会被定期执行。
KillTimer
用于结束一个定时器,第一个参数是想结束的 SetTimer 使用的 hWnd ,第二个参数是这个 SetTimer 使用的 nIDEvent。
这篇文章到这里就结束了,看起来有点长,但我所写的这些内容,其实只是一些关于 Win32 窗口部分的非常非常基础的内容。
读者可以自行在网络上查找一些更高级的内容(比如使用 GDI 绘图),来让自己的程序的图形界面更高级,更漂亮。