基本概念
什么是 SDK
SDK 是软件开发工具包(Software Development Kit)的缩写。它是一个集成了软件开发所需工具、库文件、示例代码和文档等资源的软件包。
SDK 通常由软件开发公司或平台提供,旨在帮助开发人员构建、测试和部署特定类型的应用程序、库或服务。它提供了一套开发工具,使开发人员能够利用平台或框架的功能和特性来创建软件。
SDK 可能包含以下组件:
-
开发工具:例如编译器、调试器、集成开发环境(IDE)和命令行工具,用于编写、构建和调试代码。
-
库文件:包含可重用的代码和功能的库文件,开发人员可以在自己的应用程序中使用这些库来加速开发过程。
-
示例代码:提供了使用 SDK 的示例代码和示例项目,帮助开发人员理解和使用 SDK 的功能。
-
文档:详细的文档和参考资料,解释了 SDK 的各个组件和功能,提供了开发指南和API文档等。
-
测试工具:用于测试开发的应用程序、库或服务的工具,包括单元测试框架、模拟器、调试器等。
API
API 是操作提供的一组功能性函数。使应用程序能够间接的使用操作系统接管的输入输出设备,所提供的接口。
内核对象与句柄
内核对象是受操作系统保护的对象,只允许通过该对象提供的 API 接口来修改或则访问的对象。
-
为了管理应用程序使系统稳定的运行,内核代码的数据是不允许访问的。
-
内核对象是用户模式下代码与内核模式下代码进行交互的基本接口。
内核对象的数据结构仅能够从内核模式访问,所以直接在内存中定位这些数据结构对应用程序来说是不可能的,只能通过 API 来访问它,在用户界别下用来表示内存对象的数据成为对象句柄。可以认为是另一种形式下的"指针"。既:访问内核对象需要 API + 相应的句柄(哪个内核对象)
操作系统内部有非常多的内核对象,如果想要访问特定的内核对象,那么必须告诉操作系统,我们需要访问的对象是哪一个对象,也就是给对象一个标识符---------句柄。方便与用户代码进行交互。
用户和内核模式
CPU 权限分 0 环 ~ 3 环 4 个等级。
- 操作系统-高权限------0环
- 普通应用-低权限------3环
WINDOWS 操作系统为什么只有 0 环和 3 环?
目的:为了提升兼容。不想与 CPU 绑定,防止 CPU 更改权限使其不兼容。
在 DOS 时代不区分权限,只要程序有执行的能力,DOS 能够修改操作系统的内容,所以那时的病毒非常的泛滥。
目前的病毒,要做的第一件事是提权,跟随电脑的启动而启动(服务),由于创建服务需要管理员权限,所以这种病毒只是 3 环的病毒,不会影响内核的东西。
消息机制(Msg)
Windows 是消息驱动的操作系统。没消息的时候,什么也不干,当有消息的时候才开始干活。
比如当按下记事本关于的时候,弹出一个框,显示其中的信息,其实,这个框是操作系统从鼠标获取响应,再通过封装 APP 的代码,跳转到 APP 开发者的代码部分。
- 实质:不停的输入,不停的封装消息,应用程序不停的处理。
- 本质:回调函数,开发者自己实现该动作的响应方法,然后把这个函数的地址传给操作系统,操作系统接收到响应动作的时候,通过函数指针回调开发者自定义的函数。
在调试的时候监视窗口添加 <消息变量>,wm
可以查看消息具体类型。
窗口
什么是窗口
在 Windows 系统中一切图形界面都是由窗口组成。
例如下图所示窗口为应用程序窗口 或 main 窗口 。它通常具有带有标题栏、最小化 和最大化 按钮以及其他标准 UI 元素的框架 。框架有操作系统管理因此称之为窗口的非客户区域 。框架中的区域是客户区域 ,这是程序管理的窗口的一部分。
下面是另一种类型的窗口:
UI 控件和应用程序窗口之间的主要区别在于控件本身不能独立存在。 相反,控件相对于应用程序窗口进行定位。 拖动应用程序窗口时,控件会随预期一起移动。 此外,控件和应用程序窗口可以相互通信。 (例如,应用程序窗口接收来自 button 的单击通知。)
当编写程序定义一个窗口时需要考虑以下几个方面:
- 占据屏幕的特定部分。
- 在给定时刻可能可见,也可能不可见。
- 知道如何绘制自身。
- 响应来自用户或操作系统的事件。
Visual Studio 的 工具 -> Spy++
可以查看窗口信息,其中工具栏的查找窗口可以识别一个界面中的各种窗口。
父窗口和所有者窗口
对于 UI 控件,控件窗口称为应用程序窗口的 子 窗口。 应用程序窗口是控件窗口的 父 窗口。 父窗口提供用于定位子窗口的坐标系。 具有父窗口会影响窗口外观的各个方面;例如,剪裁子窗口,以便子窗口的任何部分都不能显示在其父窗口的边框之外。
另一种关系是应用程序窗口与模式对话框窗口之间的关系。 当应用程序显示模式对话框时,应用程序窗口是 所有者 窗口,而对话框是 拥有 的窗口。 拥有的窗口始终显示在其所有者窗口的前面。 当所有者最小化时,它将隐藏,并且与所有者同时销毁。
下图显示了一个应用程序,该应用程序显示一个带有两个按钮的对话框:
应用程序窗口拥有对话框窗口,对话框窗口是两个按钮窗口的父窗口。 下图显示了这些关系:
屏幕和窗口坐标
坐标以与设备无关的像素度量。如果一个窗口由父窗口则该窗口的坐标原点为父窗口的左上角,否则为桌面的左上角,因为所有没有父窗口的窗口默认桌面为父窗口。
Unicode 与 ANSI
Windows 有两种编码体系:Unicode 和 ANSI 。
- Unicode 是一种字符编码标准,用于表示世界上几乎所有的字符。Windows 使用的 Unicode 是 UTF-16LE 编码标准,在 Windows 编程中,通常使用 Unicode 字符串类型(如
wchar_t
)来处理文本数据。 - ANSI:ANSI(American National Standards Institute)是一个字符编码标准的组织,但在 Windows 上的 ANSI 编码实际上指的是默认的系统代码页(Code Page)编码。因此会出现比如中文程序在英文操作系统上乱码的现象。
设置一个 Windows 程序编码为 Unicode 或 ANSI 的方法有以下几种:
-
Visual Studio 的
项目属性 -> 配置属性 -> 高级 -> 字符集
可以选择程序的编码。 -
在程序开头添加下面两个宏。(由于有些头文件使用预处理器符号
UNICODE
,另一些头文件使用_UNICODE
,因此两个符号都需要定义)cpp#define UNICODE #define _UNICODE
-
编辑 Visual Studio 的
项目属性 -> C/C++ -> 预处理器 -> 预处理器
添加UNICODE
和_UNICODE
,本质和添加宏一样。
由于 Unicode 与 ANSI 两者的差异,windows 在字符串定义,数据类型定义,结构体类型定义,API 定义上面都有两套规则:
-
ANSI 编码的字符串定义只需要加上
"
即可,例如"sky123"
,但是 Unicode 编码的字符串需要额外加上L
,例如L"sky123"
。 -
数据类型上 ANSI 编码的字符串单个字符都是
char
类型的,但是 Unicode 编码的字符串单个字符都是wchar_t
类型。因此微软有如下几种数据类型的定义:Typedef 定义 CHAR
char
PSTR
或LPSTR
char*
PCSTR
或LPCSTR
const char*
WCHAR
wchar_t
PWSTR
或LPWSTR
wchar_t*
PCWSTR
或LPCWSTR
const wchar_t*
-
在结构体类型定义上,由于成员类型不同,因此一个结构体需要有 Unicode 与 ANSI 两个版本的定义,例如
WNDCLASS
有WNDCLASSW
和 和WNDCLASSA
两个版本的定义。 -
在 API 定义上,由于参数类型不同,因此一个 API 需要有 Unicode 与 ANSI 两个版本的定义,例如
MessageBox
有MessageBoxA
和MessageBoxW
两种定义。
为了让一个程序既可以以 Unicode 编码也可以以 ANSI 编码编译成功,微软还定义了一系列的宏和数据类型。
-
在数据类型上,有
TCHAR
类型可以根据当前程序的字符集自动切换为char
和wchar_t
。另外还有下面两个定义:Typedef 定义 PTSTR
或LPTSTR
TCHAR*
PCTSTR
或LPCTSTR
const TCHAR*
-
在字符串定义上可以在字符串外面加上
_T()
或者TEXT()
实现不同编码下的字符串定义。例如_T("sky123")
或者TEXT("sky123")
。这里要注意_T()
宏需要额外导入tchar.h
头文件。 -
函数和结构体也有对应的宏可以自动切换到正确的函数上,例如
WNDCLASS
和MessageBox
在不同的字符集下可以切换到正确的结构体和函数名称上。 -
Microsoft C 运行时库的标头定义了一组类似的宏。 例如,如果
_UNICODE
未定义,则_tcslen
解析为strlen
;否则解析为wcslen
,这是strlen
的宽字符版本。类似的还有下面这些定义,总之前面要加一个_t
前缀,如果 ANSI 版有str
前缀则先将str
转为wcs
在将w
替换为_t
。宏 ANSI 编码模式下的函数 Unicode 编码模式下的函数 _tcslen
strlen
wcslen
_tcscpy
strcpy
wcscpy
_tprintf
printf
wprintf
_tscanf
scanf
wscanf
总之为了防止给自己挖坑最好是把程序写成两种编码下都能正常编译的形式。
类型
从前面的 Unicode 与 ANSI 相关数据类型可以看出,微软为了兼容性不会使用 C/C++ 原生的数据类型,而是通过 typedef
定义了一些数据类型。
从某种角度说微软定义了这些数据类型就是为了日后修改数据长度时确保兼容性的,因此最好使用微软定义的这些数据类型而不是 C/C++ 原生的数据类型。总之原则就是微软定义的 API 或结构体成员是什么类型那么我们就使用什么样的数据类型赋值和接收输出。
整数类型
数据类型 | 大小 | 签署? |
---|---|---|
BYTE |
8 位 | 无符号 |
DWORD |
32 位 | 无符号 |
INT32 |
32 位 | 有符号 |
INT64 |
64 位 | 有符号 |
LONG |
32 位 | 有符号 |
LONGLONG |
64 位 | 有符号 |
UINT32 |
32 位 | 无符号 |
UINT64 |
64 位 | 无符号 |
ULONG |
32 位 | 无符号 |
ULONGLONG |
64 位 | 无符号 |
WORD |
16 位 | 无符号 |
布尔类型
BOOL
是 int
的类型,不同于 C++ 的 bool
。(所以千万不要混用)
cpp
#define FALSE 0
#define TRUE 1
尽管定义为 TRUE
,但大多数返回 BOOL
类型的函数都可以返回任何非零值来指示布尔值。 因此,应始终编写:
cpp
// Right way.
if (SomeFunctionThatReturnsBoolean()) {
...
}
// or
if (SomeFunctionThatReturnsBoolean() != FALSE) {
...
}
而不是
cpp
// Wrong!
if (result == TRUE) {
...
}
指针类型
Windows 定义的指针类型名称中常有前缀 P
或 LP
。因此下面的变量声明是等效的。
cpp
RECT* rect; // Pointer to a RECT structure.
LPRECT rect; // The same
PRECT rect; // Also the same.
P
和LP
的起源:在 16 位体系结构 (16 位 Windows) 有 2 种类型的指针,
P
表示"指针",LP
代表"长指针"。 长指针(也称为远指针)用于处理当前段以外的内存范围。LP
前缀已保留,以便更轻松地将 16 位代码移植到 32 位 Windows。 今天没有区别,这些指针类型都是等效的。
另外 PC
和 LPC
前缀表示常量指针。
指针精度类型
以下数据类型始终是指针的大小,即 32 位应用程序中为 32 位宽,在 64 位应用程序中为 64 位宽。 大小在编译时确定。 当 32 位应用程序在 64 位 Windows 上运行时,这些数据类型仍为 4 个字节宽。 (64 位应用程序无法在 32 位 Windows 上运行,因此不会发生相反的情况。)
DWORD_PTR
INT_PTR
LONG_PTR
ULONG_PTR
UINT_PTR
这些类型用于整数可能强制转换为指针的情况。 它们还用于定义指针算术的变量,并定义循环计数器,循环访问内存缓冲区中所有字节的范围。 更一般地,它们出现在 64 位 Windows 上现有 32 位值扩展为 64 位的位置。
错误码与调试信息
错误码
当一个 API 调用失败的时候会产生错误码(类似 Linux 下的 errno),win32 程序的错误码可以通过 GetLastError
函数获取。
GetLastError
用于检索调用线程 的最后错误代码值,因此多个线程不会覆盖彼此的最后错误代码。
cpp
_Post_equals_last_error_ DWORD GetLastError();
得到错误码后我们有如下几个方法得到对应的错误信息:
-
Visual Studio 的
工具->错误查找
可以通过错误码检索错误信息。 -
在调试的时候可以在监视窗口添加
<存取错误码的变量>,hr
就可以再"值"那一栏看到错误信息,而输入@err,hr
可以随时查看当前错误码和错误信息。 -
VC 6.0 的错误查找方式:监视窗口
*(unsigned long*)(tib + 0x34),hr
。 -
FormatMessage
函数可以将错误码转换为错误信息,为了方便这里直接把文档中的实例代码封装成一个函数,这样每次调用 API 失败的时候就可以调用这个函数显示错误信息。cppvoid ShowErrorMsg() { LPVOID lpMsgBuf; FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),// Default language (LPTSTR) &lpMsgBuf, 0, NULL); MessageBox(NULL, (LPCTSTR) lpMsgBuf, _T("Error"), MB_OK | MB_ICONINFORMATION); LocalFree(lpMsgBuf); }
调试信息
在 Windows 程序中原本 Console 程序的输入输出函数无效,因此需要找到一个新的输出调试信息的方式。
首先会想到使用 MessageBox
弹窗来实现调试信息输出,然而弹出窗口的时候焦点转移到新弹出的窗口上,而原版窗口的操作中断导致后续消息丢失。例如按下鼠标左键后在光标位置弹出一个窗口,此时由于光标位于弹出的窗口上,因此抬起鼠标左键的操作不再作用于原本的窗口,导致起鼠标左键这个信息"丢失"。另外有些操作比如移动窗口发送的消息过于频繁,如果采用弹窗实现调试信息输出会影响正常操作。因此弹窗不适合作为 Windows 程序调试信息输出的方式。
Windows API 中的 OutputDebugString
函数可以输出字符串,我们可以通过 DebugView 键控到调试信息。
注意 DebugView 捕获本机消息需要设置 Computer -> Connect Local
。
然而 OutputDebugString
函数不支持格式化字符串,因此我们需要将其进行如下改进:
cpp
#ifdef _DEBUG
void DebugPrintf(LPCTSTR format, ...) {
TCHAR szBuf[MAXBYTE];
va_list args;
va_start(args, format);
#ifdef UNICODE
vswprintf_s(szBuf, sizeof(szBuf) / sizeof(TCHAR), format, args);
#else
vsprintf_s(szBuf, sizeof(szBuf), format, args);
#endif
va_end(args);
OutputDebugString(szBuf);
}
#else
#define DebugPrintf
#endif
改进后的 DebugPrintf
函数有如下特性:
- 支持格式化字符串。
- 可以在 Debug 版本使用, Release 版本自动去除。
- 另外支持 Unicode 和 ANSI 两种字符集下编译运行。
另外 DebugView 键控的是所有进程的消息,因此在输出调试信息的时候最好带一个标记,这样利用 DebugView 的过滤功能就可以只监控特定的消息。或者调试状态在 Visual Studio 的输出窗口查看输出信息。
SDK 程序
控制台编程与 Windows 程序在流程上的区别
控制台机制:主要使用顺序的,过程驱动的程序设计方法 。过程驱动的程序有一个明显的开始,明显的过程及一个明显的结束,因此程序能直接控制程序事件或过程的顺序。虽然在顺序的过程驱动的程序中也有很多处理异常的方法,但这样的异常处理也仍然是顺序的,过程驱动的结构。
Windows 程序:消息驱动,不由事件的顺序来控制,而是由事件的发生来控制,所有的事件都是无序的。因为编写程序时,我们并不知道用户先按哪个按纽,也不知道程序先触发哪个消息。我们的任务就是对正在开发的应用程序要发出或要接收的消息进行排序和管理。事件驱动程序设计是密切围绕消息的产生与处理而展开的,一条消息是关于发生的事件的消息。
Windows 程序与 Console 程序入口的区别
入口 | 链接选项 | |
---|---|---|
Windows桌面应用程序(SDK程序) | wWinMain |
SUBSYSTEM:console |
控制台程序(Consolo程序) | main |
SUBSYSTEM:WINDOWS |
注意 SDK 程序不是没有 main
函数,而是 Microsoft C 运行时库提供了调用 WinMain
或 wWinMain
的 main
实现,而 CRT 在 main 中执行了一些 SDK 相关的初始化工作。
每个 Windows 程序都包含一个名为 WinMain
或 wWinMain
的入口点函数。wWinMain
函数的定义如下:
cpp
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow);
hInstance
:实例的句柄或模块的句柄。 当可执行文件加载到内存中时,操作系统使用此值来标识可执行文件或 EXE(实际上是模块的加载基址)。这个值也可以通过GetModuleHandle(NULL)
获取。hPrevInstance
:保留参数,它在 16 位 Windows 中使用,但现在始终为零。pCmdLine
:命令行参数。nCmdShow
:是一个标志,指示主应用程序窗口是最小化、最大化还是正常显示。- 函数返回一个
int
值,作为程序的退出码。
WinMain
函数与 wWinMain
相同,只是命令行参数作为 ANSI 字符串传递。如果想要两种字符集都通用可以使用 _tWinMain
。
cpp
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, TCHAR *lpCmdLine, int nCmdShow)
SDK 开发基础
创建 SDK 项目
Visual Studio 选择"桌面应用程序"(DesktopApplication)(Win32程序)
如果想要创建一个空白的 SDK 项目可以 Windows 桌面向导(WindowsDesktopWizard)->创建->选择桌面应用程序,空项目
。
创建文件的方法:选择项目源文件右键->添加->新建项
添加库:项目属性->链接器->输入->编辑附加依赖项
(一般不需要设置)
SDK 开发文档
官方文档:https://learn.microsoft.com/zh-cn/windows/win32/api/ ,如果想搜索某一定义的解释可以直接在搜索栏搜索。
本地开发文档可以 Visual Studio Installer->Visual Studio 点击修改->单个组件->安装Help Viewer
安装,安装完成之后帮助栏多了一项添加和移除帮助内容,不过最新版只到 VS2015 。
另外可以选择下载 msdn ,虽然版本很老,但是完全够用。
在搜索 API 的时候要注意活动子集要选择 "(整个集合)" ,然后再索引栏查找。
在 目录 -> Platform SDK Documentation -> Windows API -> Reference -> Functions by Category
中可以按照分类检索相关 API 即控件的文档。
窗口创建的主要步骤
窗口创建主要分为 6 个步骤:
-
设计注册窗口类
-
创建窗口实例
-
显示窗口
-
更新窗口
-
消息循环
-
实现窗口过程函数(窗口回调函数)
设计注册窗口类
-
窗口种类是定义窗口属性的模板,这些属性包括窗口式样,鼠标形状,菜单等等。
-
窗口种类也指定处理该类中所有窗口消息的窗口函数。只有先建立窗口种类,才能根据窗口种类来创建 Windows 应用程序的一个或多个窗口。创建窗口时,还可以指定窗口独有的附加特性。
-
窗口种类简称窗口类,窗口类不能重名,且窗口类名,是操作系统识别窗口类的唯一标识符,在建立窗口类后,必须向 Windows 登记(注册窗口类)。
-
注意:不能注册相同名字的窗口类。根据窗口类名字来确定是否已经注册过,如果注册过,则注册失败。
设计自己的窗口类
操作系统中预定义了很多窗口类。我们要使用的时候可以直接调用,如果要用自己的窗口,就要设计自己所需要的窗口类,设计完成后,通过
RegisterClass
注册自己设计的窗口类载入到操作系统中。
-
RegisterClass
用到了 WNDCLASS
结构体(RegisterClassEx
用到了 WNDCLASSEX
结构体)。
cpp
TCHAR szWndClassName[] = TEXT("CR41WndClassName");
TCHAR szWndName[] = _T("sky123");
WNDCLASSEX wc{};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_VREDRAW | CS_HREDRAW;// 窗口类型
wc.lpfnWndProc = WindowProc; // 窗口过程函数(窗口回调函数->处理信息)
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL,IDI_ERROR); // 图标
wc.hCursor = LoadCursor(NULL, IDC_HAND); // 光标
wc.hbrBackground = CreateSolidBrush(RGB(255,255,255));// 窗口背景颜色刷子
wc.lpszMenuName = NULL; // 菜单名称
wc.lpszClassName = szWndClassName;// 窗口类名
WNDCLASS
的属性解释如下,其中加粗的是必须提供的。
-
wc.style
:成员style
控制窗口的某些重要特性,在WINDOWS.H
中定义了一些前缀为 CS 的常量,在程序中可组合使用这些常量.也可把sytle
设为0。wc.style = CS_HREDRAW | CS_VREDRAW
它表示当窗口的纵横坐标发生变化时要重画整个窗口。- eg:无论怎样拉动窗口的大小,那行字都会停留在窗口的正中部,而假如把这个参数设为 0 的话,当改动窗口的大小时,那行字则不一定处于中部了。
-
wc.lfnWndProc
:窗口过程函数,它将接收 Windows 发送给窗口的消息,并执行相应的任务。并且必须在模快定义中回调它。WndProc
是一个回调函数(详见消息循环)。 -
wc.cbClsExtra
:指定用本窗口类建立的所有窗口结构分配的额外字节数。当有两个以上的窗口属于同一窗口类时,如果想将不同的数据和每个窗口分别相对应。则使用该域很有用。一般来讲,只要把它们设为 0 就行了,不必过多考虑。 -
wc.hInstance
:标识应用程序的实例hInstance
,当然,实例名是可以改变的。wc.hInstance = MyhInstance;
这一成员可使 Windows 连接到正确的程序(自己的程序)。
-
wc.hIcon
:成员hIcon
被设置成应用程序所使用图标的句柄,图标是将应用程序最小化时出现在任务栏里的的图标,用以表示程序仍驻留在内存中。Windows 提供了一些默认图标,我们也可定义自己的图标,VC 里面专有一个制作图标的工具。 -
wc.hCursor
:定义该窗口产生的光标形状。LoadCursor
可返回固有光标句柄或者应用程序定义的光标句柄。例如IDC_ARROW
表示箭头光标. -
wc.hbrBackground
:决定 Windows 用于着色窗口背景的刷子颜色,函数GetStockObject
返回窗口的颜色,本程序中返回的是白色,你也可以把它改变为红色等其他颜色.试试看 -
wc.lpszMenuName
:用来指定菜单名,本程序中没有定义菜单,所以为 NULL 。 -
wc.lpszClassName
:指定了本窗口的类名。类名是操作系统识别类的唯一 ID 。注册窗口类
当对
WNDCLASS
结构域一一赋值后,就可注册窗口类了,在创建窗口之前,是必须要注册窗口类的,注册窗口类用的 API 函数是RegisterClass
,注册失败的话,函数RegisterClass
返回 0 。
cpp
if (RegisterClassEx(&wc) == 0) {
ShowErrMsg();
return 0;
}
创建窗口实例
创建窗口用到了 CreateWindowExW
函数,该函数定义如下:
cpp
HWND
WINAPI
CreateWindowExW(
_In_ DWORD dwExStyle,
_In_opt_ LPCWSTR lpClassName,
_In_opt_ LPCWSTR lpWindowName,
_In_ DWORD dwStyle,
_In_ int X,
_In_ int Y,
_In_ int nWidth,
_In_ int nHeight,
_In_opt_ HWND hWndParent,
_In_opt_ HMENU hMenu,
_In_opt_ HINSTANCE hInstance,
_In_opt_ LPVOID lpParam);
lpClassName
:注册的类名,和窗口类的名称对应起来。lpWindowName
:窗口名,就是窗口右上角的标题。dwStyle
:窗口样式,主要有下面几种类型:WS_OVERLAPPED
:标准的窗口样式,包括标题栏、边框和系统菜单。WS_POPUP
:创建一个无边框、无标题栏的弹出窗口。WS_CHILD
:创建一个子窗口,必须依附于其他父窗口。WS_VISIBLE
:创建一个可见的窗口。WS_DISABLED
:创建一个禁用的窗口,用户无法与之交互。WS_MINIMIZE
:创建一个带有最小化的窗口。WS_MAXIMIZE
:创建一个带有最大化的窗口。WS_CAPTION
:创建一个带有标题栏的窗口。WS_SYSMENU
:创建一个带有系统菜单的窗口。WS_SIZEBOX
:创建一个可调整大小的窗口。WS_BORDER
:创建一个带有边框的窗口。WS_CLIPCHILDREN
:在绘制窗口时,防止子窗口重叠。WS_CLIPSIBLINGS
:在绘制窗口时,防止兄弟窗口重叠。
X
:窗口左上角的 x 坐标。它是一个整数,用于指定窗口相对于其父窗口或屏幕的水平位置。Y
:窗口左上角的 y 坐标。它是一个整数,用于指定窗口相对于其父窗口或屏幕的垂直位置。nWidth
:窗口的宽度。它是一个整数,用于指定窗口的宽度。nHeight
:窗口的高度。它是一个整数,用于指定窗口的高度。hWndParent
:父窗口句柄。它是一个窗口句柄,用于指定新窗口的父窗口。如果新窗口没有父窗口,则可以设置为 NULL。hMenu
:菜单句柄。它是一个菜单句柄,用于指定新窗口的菜单。如果新窗口没有菜单,则可以设置为 NULL。hInstance
:应用程序实例句柄。它是一个应用程序实例的句柄,用于指定新窗口所属的应用程序实例。lpParam
:用户定义的参数。它是一个指向用户自定义数据的指针,可以在窗口过程中使用。
在示例程序中我传入的参数如下:
cpp
HWND hWnd = CreateWindowEx(
0,
szWndClassName,
szWndName,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);
if (hWnd == NULL) {
ShowErrMsg();
return 0;
}
显示和更新窗口
API 函数 CreateWindow
创建完窗口后,要想把它显示出现,还必须调用另一个 API 函数 ShowWindows
。
ShowWindows
函数定义如下:
cpp
BOOL
WINAPI
ShowWindow(
_In_ HWND hWnd,
_In_ int nCmdShow);
hWnd
:窗口句柄,告诉ShowWindow
显示哪一个窗口。nCmdShow
:如何显示这个窗口。SW_MINIMIZE
:最小化SW_SHOWNORMAL
:普通SW_SHOWMAXIMIZED
:最大化
在示例程序中我传入的参数如下:
cpp
ShowWindow(hWnd, SW_SHOWNORMAL);
WinMain
调用完 ShowWindow
后,还需要调用函数 UpdateWindow
,最终把窗口显示了出来(在高版本的 SDK 里面这一步已经没有必要了,因为 ShowWindow
做了这件事)。调用函数 UpdateWindow
将产生一个 WM_PAINT
消息,这个消息将使窗口重画,即使窗口得到更新,且不通过消息循环。
另外如果想修改窗口的属性可以使用 SetClassLongPtr
来修改。SetClassLongPtrW
函数定义如下:
cpp
WINUSERAPI
ULONG_PTR
WINAPI
SetClassLongPtrW(
_In_ HWND hWnd,
_In_ int nIndex,
_In_ LONG_PTR dwNewLong);
hWnd
:窗口句柄。nIndex
:要替换的属性。dwNewLong
:属性的被替换成的值。
实例程序中在调用完 ShowWindow
后再调用 SetClassLongPtr
修改了光标的类型。
cpp
SetClassLongPtr(hWnd, GCLP_HCURSOR, (LONG) LoadCursor(NULL, IDC_CROSS));
创建消息循环
Windows 为每个正在运行的应用程序都保持一个消息队列。当你按下鼠标或者键盘时,Windows 并不是把这个输入事件直接送给应用程序,而是将输入的事件先翻译成一个消息,然后把这个消息放入到这个应用程序的消息队列中去。
在消息循环中用到了消息结构体 tagMSG
,操作系统将消息封装成 MSG
结构体投递到消息队列。
cpp
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
#ifdef _MAC
DWORD lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;
hwnd
:要发送的窗口句柄。如果是在一个有多个窗口的应用程序中,用这个参数就可决定让哪个窗口接收消息。message
:消息编号。wParam
:一个 32 位的消息参数,这个值的确切意义取决于消息本身。lParam
:一个 32 位的消息参数,这个值的确切意义取决于消息本身。time
:消息放入消息队列中的时间(消息发生时间),在这个域中写入的并不是日期,而是从 Windows 启动后所测量的时间值。Windows 用这个域来使用消息保持正确的顺序。pt
:消息放入消息队列时的鼠标坐标。
应用程序的 WinMain
函数通过执行一段代码从她的队列中来检索 Windows
送往它的消息。然后 WinMain
就把这些消息分配给相应的窗口函数以便处理它们,这段代码是一段循环代码,故称为"消息循环"。
示例代码中的消息循环实现如下:
cpp
MSG msg;
while (BOOL bRet = GetMessage(&msg, NULL, 0, 0)) {
if (bRet == -1) {
ShowErrMsg();
break;
}
TranslateMessage(&msg); // 翻译消息
DispatchMessage(&msg); // 派发消息
}
return msg.wParam;
其中 GetMessageW
定义如下:
cpp
BOOL
WINAPI
GetMessageW(
_Out_ LPMSG lpMsg,
_In_opt_ HWND hWnd,
_In_ UINT wMsgFilterMin,
_In_ UINT wMsgFilterMax);
lpMsg
:接收消息的 MSG 结构的地址。hWnd
:窗口句柄,NULL 则表示要获取该应用程序创建的所有窗口的消息。wMsgFilterMin
:最小消息过滤值。它是一个无符号整数,用于指定获取消息的最小消息值。只有消息的值大于等于wMsgFilterMin
的消息才会被获取。wMsgFilterMax
:最大消息过滤值。它是一个无符号整数,用于指定获取消息的最大消息值。只有消息的值小于等于wMsgFilterMax
的消息才会被获取。- 如果
wMsgFilterMin
和wMsgFilterMax
同时为 0 则过滤无效。 - 返回值:
- 在接收到除
WM_QUIT
之外的任何一个消息后,GetMessage()
都返回TRUE
。 - 如果
GetMessage
收到一个WM_QUIT
消息,则返回FALSE
。 - 如果出现错误则返回 -1 。
- 在接收到除
TranslateMessage
函数用于将虚拟键消息转换为字符消息。该函数会解析 lpMsg
所指向的消息,并根据其中的虚拟键码和键盘状态信息,生成相应的字符消息。生成的字符消息会被插入到线程的消息队列中,并可以通过后续的调用 GetMessage
函数来获取。
DispatchMessage
: 函数用于将消息分派给窗口过程进行处理。当获取到一个消息后,通常需要将其传递给相应的窗口过程函数来进行处理。
实现窗口过程函数
窗口的回调函数在处理完消息后还可以把消息的处理结果放入消息队列。
示例代码中窗口回调函数实现如下:
cpp
LRESULT CALLBACK WindowProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam,// first message parameter
LPARAM lParam // second message parameter
) {
if (uMsg == WM_CLOSE) {
// 向消息队列投递 WM_QUIT 消息
PostQuitMessage(0);
}
return DefWindowProc(hwnd, uMsg, wParam, lParam); // 默认窗口处理函数
}
由于示例窗口没有任何功能,所以可以将收到的消息交给默认窗口处理函数由系统处理(比如最大化,最小化和关闭窗口)。
不过需要注意的是关闭窗口并不意味着进程终止,因此需要调用 PostQuitMessage
函数向消息队列 中 投递 WM_QUIT
消息通知进程结束。其中 PostQuitMessage
的参数是 MSG
中的 wParam
,是传递的参数,这里我们将其作为进程的退出码。
另外 WM_DESTROY
消息在 WM_CLOSE
之后,因此最好在接收到 WM_DESTROY
消息时向消息队列投递 WM_QUIT
消息以确保资源正常释放。
示例程序
cpp
#include<Windows.h>
#include<tchar.h>
void ShowErrMsg() {
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
GetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),// Default language
(LPTSTR) &lpMsgBuf,
0,
NULL);
MessageBox(NULL, (LPCTSTR) lpMsgBuf, _T("Error"), MB_OK | MB_ICONINFORMATION);
LocalFree(lpMsgBuf);
}
// 实现窗口过程函数
LRESULT CALLBACK WindowProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam,// first message parameter
LPARAM lParam // second message parameter
) {
if (uMsg == WM_DESTROY) {
// 向消息队列投递 WM_QUIT 消息
PostQuitMessage(0);
}
return DefWindowProc(hwnd, uMsg, wParam, lParam); // 默认窗口处理函数
}
int WINAPI _tWinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
TCHAR *lpCmdLine,
int nCmdShow) {
// 设计注册窗口类
TCHAR szWndClassName[] = TEXT("sky123ClassName");
TCHAR szWndName[] = _T("sky123");
WNDCLASSEX wc{};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_VREDRAW | CS_HREDRAW;// 窗口类型
wc.lpfnWndProc = WindowProc; // 窗口过程函数(窗口回调函数->处理信息)
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_ERROR); // 图标
wc.hCursor = LoadCursor(NULL, IDC_HAND); // 光标
wc.hbrBackground = CreateSolidBrush(RGB(255, 255, 255));// 窗口背景颜色刷子
wc.lpszMenuName = NULL; // 菜单名称
wc.lpszClassName = szWndClassName; // 窗口类名
if (RegisterClassEx(&wc) == 0) {
ShowErrMsg();
return 0;
}
// 创建窗口实例
HWND hWnd = CreateWindowEx(
0,
szWndClassName,
szWndName,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);
if (hWnd == NULL) {
ShowErrMsg();
return 0;
}
// 显示和更新窗口
ShowWindow(hWnd, SW_SHOWNORMAL);
SetClassLongPtr(hWnd, GCLP_HCURSOR, (LONG) LoadCursor(NULL, IDC_CROSS));
UpdateWindow(hWnd);
// 创建消息循环
MSG msg;
while (BOOL bRet = GetMessage(&msg, NULL, 0, 0)) {
if (bRet == -1) {
ShowErrMsg();
break;
}
TranslateMessage(&msg);// 翻译消息
DispatchMessage(&msg); // 派发消息
}
return msg.wParam;
}
消息
消息处理
在 SDK 程序中每个窗口都会接收并处理消息,因此需要再窗口对应的回调函数中写一个 switch 针对不同的消息调用对应的消息处理函数。
因此一般一个窗口对应的回调函数为下面这种形式:
cpp
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
LRESULT lReturn = FALSE;
switch (uMsg) {
case WM_CREATE:
lReturn = OnCreate(hwnd, uMsg, wParam, lParam);
break;
case WM_CLOSE:
lReturn = OnClose(hwnd, uMsg, wParam, lParam);
break;
case WM_DESTROY:
lReturn = OnDestroy(hwnd, uMsg, wParam, lParam);
break;
...
if (lReturn) {
return lReturn;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);// 默认窗口处理函数
}
下面会列举一些常见的消息以及注意事项。
窗口消息
窗口创建(WM_CREATE)
在窗口创建的时候会发送该消息,通常我们会将一些该窗口初始化相关的代码写在对应的处理函数中。例如下面这个代码将热键的注册写到了窗口创建的处理函数中,这样一旦该窗口创建则相关的热键就会生效。
cpp
LRESULT CALLBACK OnCreate(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnCreate %x\n"), WORD(uMsg));
RegisterHotKey(hwnd, 5566, MOD_CONTROL, VK_F1);
return TRUE;
}
窗口关闭(WM_CLOSE)
点击窗口的关闭按钮的时候会发送该消息。注意此时窗口相关资源还没有释放,因此最好不要在此时结束进程。另外关闭窗口是操作系统的工作,因此这个消息必须交给系统默认的处理函数。
cpp
LRESULT CALLBACK OnClose(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnClose %x\n"), WORD(uMsg));
return FALSE; // 返回 FALSE 表示这个消息没有处理,需要调用系统默认的处理函数。
}
实际上关闭窗口也可以通过 DestroyWindow
这一 API 来完成。
cpp
LRESULT CALLBACK OnClose(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnClose %x\n"), WORD(uMsg));
DestroyWindow(hwnd);
return TRUE;
}
窗口销毁(WM_DESTROY)
与窗口创建相对应,一般会将该窗口相关资源释放的代码写到该函数中,另外如果想要在关闭窗口的同时结束进程还可以调用 PostQuitMessage
向消息循环发送 WM_QUIT
消息。
cpp
LRESULT CALLBACK OnDestroy(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnDestroy %x\n"), WORD(uMsg));
UnregisterHotKey(hwnd, 5566);
PostQuitMessage(0);
return TRUE;
}
窗口移动(WM_MOVE)
窗口(具体来说是窗口左上角)移动的时候会发送该消息,我们可以通过参数获取窗口移动后的坐标,具体可以查阅文档。
cpp
LRESULT CALLBACK OnMove(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);
DebugPrintf(_T("[sky123] OnMove (%d, %d)\n"), xPos, yPos);
return TRUE;
}
鼠标消息
左键按下(WM_LBUTTONDOWN)
可以获取左键按下时光标的坐标,具体可查阅文档。
cpp
LRESULT CALLBACK OnLButtonDown(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);
DebugPrintf(_T("[sky123] OnLButtonDown (%d, %d)\n"), xPos, yPos);
return TRUE;
}
左键抬起(WM_LBUTTONUP)
可以获取左键抬起时光标的坐标,具体可查阅文档。
cpp
LRESULT CALLBACK OnLButtonUp(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);
DebugPrintf(_T("[sky123] OnLButtonUp (%d, %d)\n"), xPos, yPos);
return TRUE;
}
鼠标移动(WM_MOUSEMOVE)
可以获取鼠标移动时光标的坐标,具体可查阅文档。
cpp
LRESULT CALLBACK OnMouseMove(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);
DebugPrintf(_T("[sky123] OnMouseMove (%d, %d)\n"), xPos, yPos);
return TRUE;
}
左键双击(WM_LBUTTONDBLCLK)
可以获取左键双击时光标的坐标。
cpp
LRESULT CALLBACK OnLButtonDoubleClick(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);
DebugPrintf(_T("[sky123] OnLButtonDoubleClick (%d, %d)\n"), xPos, yPos);
return TRUE;
}
注意:
-
只有两次点击鼠标左键的时间间隔在一定范围内才算是左键双击。
-
左键双击的消息是替换了第二次鼠标左键按下的消息。
-
需要在
WNDCLASSE
的style
中添加CS_DBLCLKS
属性鼠标左键双击的消息才能有效。键盘消息
键盘按下(WM_KEYDOWN)
wParam
为虚拟键码,不过要想转换为具体字符需要借助ToAscii
函数。该函数定义如下:cppWINUSERAPI int WINAPI ToAscii( _In_ UINT uVirtKey, _In_ UINT uScanCode, _In_reads_opt_(256) CONST BYTE *lpKeyState, _Out_ LPWORD lpChar, _In_ UINT uFlags);
-
uVirtKey
:指定虚拟键码。这是要转换的键码,也就是wParam
。 -
uScanCode
:指定扫描码,这是与键码关联的硬件扫描码,用于区分不同的键。根据文档可知lParam
的 16 到 23 位为扫描码。 -
lpKeyState
:指向一个长度为 256 字节的键状态数组的指针。这个数组用于指示键盘上每个键的状态,包括按下、释放等。可以通过GetKeyboardState
函数获取。 -
lpChar
:转换后的字符。 -
uFlags
:指定转换标志,这里设为 0 即可,即标准转换。
因此可以采用如下方式获取按键输入的具体字符:
cpp
LRESULT CALLBACK OnKeyDown(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
BYTE KeyState[256];
if (GetKeyboardState(KeyState) == FALSE) {
return TRUE;
}
BYTE ScanCode = lParam >> 16 & 0xFF;
WORD ch;
if (ToAscii(wParam, ScanCode, KeyState, &ch, 0)) {
DebugPrintf(_T("[sky123] OnKeyDown %c\n"), ch);
} else {
DebugPrintf(_T("[sky123] OnKeyDown VK:%x\n"), wParam);
}
return TRUE;
}
键盘抬起(WM_KEYUP)
参数与 WM_KEYDOWN
相似,因此可以采用如下方式获取按键输入的具体字符:
cpp
LRESULT CALLBACK OnKeyUp(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
BYTE KeyState[256];
if (GetKeyboardState(KeyState) == FALSE) {
return TRUE;
}
BYTE ScanCode = lParam >> 16 & 0xFF;
WORD ch;
if (ToAscii(wParam, ScanCode, KeyState, &ch, 0)) {
DebugPrintf(_T("[sky123] OnKeyUp %c\n"), ch);
} else {
DebugPrintf(_T("[sky123] OnKeyUp VK:%x\n"), wParam);
}
return TRUE;
}
键盘输入字符(WM_CHAR)
如果想要获取输入的字符有一种更简单的方法就是通过 WM_CHAR
获取。wParam
就是输入字符的 ASCII 码。
cpp
LRESULT CALLBACK OnChar(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnChar %c\n"), wParam);
return TRUE;
}
不过使用这种方法的前提是在消息循环中 DispatchMessage
派发消息前需要调用 TranslateMessage
进行消息转换,这个 API 会将 WM_KEYDOWN
转换为 WM_KEYDOWN
和 WM_CHAR
。当然,这个 API 的作用不止转换键盘输入,还会参与其它消息的转换。
热键(WM_HOTKEY)
当窗口注册的热键被按下的时候会发送 WM_HOTKEY
消息到对应窗口。
cpp
LRESULT CALLBACK OnHotKey(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
MessageBox(NULL, _T("OnHotKey"), _T("sky123"), MB_OK);
return TRUE;
}
热键是窗口注册,在任何地方按下都会被注册该热键的窗口捕获。例如 win+R
键。
注册热键的 API 是 RegisterHotKey
,该函数定义如下:
cpp
WINUSERAPI
BOOL
WINAPI
RegisterHotKey(
_In_opt_ HWND hWnd,
_In_ int id,
_In_ UINT fsModifiers,
_In_ UINT vk);
-
hWnd
:可选参数,指定接收热键消息的窗口句柄。如果为NULL
,则热键消息将被发送到调用线程的消息队列中。 -
id
:标识热键的 ID。每个热键都需要一个唯一的 ID 来标识。 -
fsModifiers
:指定热键的修饰键。可以是以下值之一,或者它们的组合:MOD_ALT
:Alt
键。MOD_CONTROL
:Ctrl
键。MOD_SHIFT
:Shift
键。MOD_WIN
:Windows
键。
-
uVirtKey
:指定热键的虚拟键码。这是要注册的热键的键码值。 -
返回值:函数返回一个
BOOL
类型的值,表示注册热键的成功与否。如果注册成功,返回值为非零;否则,返回值为零。
例如示例代码中注册了一个 Ctrl+F1
的热键:
cpp
RegisterHotKey(hwnd, 5566, MOD_CONTROL, VK_F1);
如果我们用不到该热键的时候可以调用 UnregisterHotKey
来销毁该热键,UnregisterHotKey
函数定义如下:
cpp
WINUSERAPI
BOOL
WINAPI
UnregisterHotKey(
_In_opt_ HWND hWnd,
_In_ int id);
-
hWnd
:可选参数,指定先前注册热键时所使用的窗口句柄。如果该窗口句柄与注册时不匹配,或者为NULL
,则取消注册所有匹配指定 ID 的热键。 -
id
:指定要取消注册的热键的 ID。 -
返回值:函数返回一个
BOOL
类型的值,表示取消注册热键的成功与否。如果取消注册成功,返回值为非零;否则,返回值为零。
在示例代码中我们在销毁窗口的时候调用该函数取消注册该窗口在创建时注册的热键。
cpp
UnregisterHotKey(hwnd, 5566);
示例程序
cpp
#include <Windows.h>
#include <tchar.h>
#include <stdio.h>
#include <Windowsx.h>
void ShowErrMsg() {
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
GetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),// Default language
(LPTSTR) &lpMsgBuf,
0,
NULL);
MessageBox(NULL, (LPCTSTR) lpMsgBuf, _T("Error"), MB_OK | MB_ICONINFORMATION);
LocalFree(lpMsgBuf);
}
#ifdef _DEBUG
void DebugPrintf(LPCTSTR format, ...) {
TCHAR szBuf[MAXBYTE];
va_list args;
va_start(args, format);
#ifdef UNICODE
vswprintf_s(szBuf, sizeof(szBuf) / sizeof(TCHAR), format, args);
#else
vsprintf_s(szBuf, sizeof(szBuf), format, args);
#endif
va_end(args);
OutputDebugString(szBuf);
}
#else
#define DebugPrintf
#endif
LRESULT CALLBACK OnCreate(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnCreate %x\n"), WORD(uMsg));
RegisterHotKey(hwnd, 5566, MOD_CONTROL, VK_F1);
return TRUE;
}
LRESULT CALLBACK OnClose(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnClose %x\n"), WORD(uMsg));
// DestroyWindow(hwnd);
return FALSE;
}
LRESULT CALLBACK OnDestroy(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnDestroy %x\n"), WORD(uMsg));
UnregisterHotKey(hwnd, 5566);
PostQuitMessage(0);
return TRUE;
}
LRESULT CALLBACK OnMove(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);
DebugPrintf(_T("[sky123] OnMove (%d, %d)\n"), xPos, yPos);
return TRUE;
}
LRESULT CALLBACK OnLButtonDown(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);
DebugPrintf(_T("[sky123] OnLButtonDown (%d, %d)\n"), xPos, yPos);
return TRUE;
}
LRESULT CALLBACK OnLButtonUp(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);
DebugPrintf(_T("[sky123] OnLButtonUp (%d, %d)\n"), xPos, yPos);
return TRUE;
}
LRESULT CALLBACK OnLButtonDoubleClick(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);
DebugPrintf(_T("[sky123] OnLButtonDoubleClick (%d, %d)\n"), xPos, yPos);
return TRUE;
}
LRESULT CALLBACK OnMouseMove(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);
DebugPrintf(_T("[sky123] OnMouseMove (%d, %d)\n"), xPos, yPos);
return TRUE;
}
LRESULT CALLBACK OnKeyDown(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
BYTE KeyState[256];
if (GetKeyboardState(KeyState) == FALSE) {
return TRUE;
}
BYTE ScanCode = lParam >> 16 & 0xFF;
WORD ch;
if (ToAscii(wParam, ScanCode, KeyState, &ch, 0)) {
DebugPrintf(_T("[sky123] OnKeyDown %c\n"), ch);
} else {
DebugPrintf(_T("[sky123] OnKeyDown VK:%x\n"), wParam);
}
return TRUE;
}
LRESULT CALLBACK OnKeyUp(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
BYTE KeyState[256];
if (GetKeyboardState(KeyState) == FALSE) {
return TRUE;
}
BYTE ScanCode = lParam >> 16 & 0xFF;
WORD ch;
if (ToAscii(wParam, ScanCode, KeyState, &ch, 0)) {
DebugPrintf(_T("[sky123] OnKeyUp %c\n"), ch);
} else {
DebugPrintf(_T("[sky123] OnKeyUp VK:%x\n"), wParam);
}
return TRUE;
}
LRESULT CALLBACK OnChar(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnChar %c\n"), wParam);
return TRUE;
}
LRESULT CALLBACK OnHotKey(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
MessageBox(NULL, _T("OnHotKey"), _T("sky123"), MB_OK);
return TRUE;
}
// 实现窗口过程函数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
LRESULT lReturn = FALSE;
switch (uMsg) {
case WM_CREATE:
lReturn = OnCreate(hwnd, uMsg, wParam, lParam);
break;
case WM_CLOSE:
lReturn = OnClose(hwnd, uMsg, wParam, lParam);
break;
case WM_DESTROY:
lReturn = OnDestroy(hwnd, uMsg, wParam, lParam);
break;
//case WM_MOVE:
// lReturn = OnMove(hwnd, uMsg, wParam, lParam);
// break;
case WM_LBUTTONDOWN:
lReturn = OnLButtonDown(hwnd, uMsg, wParam, lParam);
break;
case WM_LBUTTONUP:
lReturn = OnLButtonUp(hwnd, uMsg, wParam, lParam);
break;
//case WM_MOUSEMOVE:
// lReturn = OnMouseMove(hwnd, uMsg, wParam, lParam);
// break;
case WM_LBUTTONDBLCLK:
lReturn = OnLButtonDoubleClick(hwnd, uMsg, wParam, lParam);
break;
case WM_KEYDOWN:
lReturn = OnKeyDown(hwnd, uMsg, wParam, lParam);
break;
case WM_KEYUP:
lReturn = OnKeyUp(hwnd, uMsg, wParam, lParam);
break;
case WM_CHAR:
lReturn = OnChar(hwnd, uMsg, wParam, lParam);
break;
case WM_HOTKEY:
lReturn = OnHotKey(hwnd, uMsg, wParam, lParam);
break;
}
if (lReturn) {
return lReturn;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);// 默认窗口处理函数
}
int WINAPI _tWinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
TCHAR *lpCmdLine,
int nCmdShow) {
// 设计注册窗口类
TCHAR szWndClassName[] = TEXT("sky123ClassName");
TCHAR szWndName[] = _T("sky123");
WNDCLASSEX wc{};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS;// 窗口类型
wc.lpfnWndProc = WindowProc; // 窗口过程函数(窗口回调函数->处理信息)
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_ERROR); // 图标
wc.hCursor = LoadCursor(NULL, IDC_HAND); // 光标
wc.hbrBackground = CreateSolidBrush(RGB(255, 255, 255));// 窗口背景颜色刷子
wc.lpszMenuName = NULL; // 菜单名称
wc.lpszClassName = szWndClassName; // 窗口类名
if (RegisterClassEx(&wc) == 0) {
ShowErrMsg();
return 0;
}
// 创建窗口实例
HWND hWnd = CreateWindowEx(
0,
szWndClassName,
szWndName,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);
if (hWnd == NULL) {
ShowErrMsg();
return 0;
}
// 显示和更新窗口
ShowWindow(hWnd, SW_SHOWNORMAL);
SetClassLongPtr(hWnd, GCLP_HCURSOR, (LONG) LoadCursor(NULL, IDC_CROSS));
UpdateWindow(hWnd);
// 创建消息循环
MSG msg;
while (BOOL bRet = GetMessage(&msg, NULL, 0, 0)) {
if (bRet == -1) {
ShowErrMsg();
break;
}
TranslateMessage(&msg);// 翻译消息
DispatchMessage(&msg); // 派发消息
}
return msg.wParam;
}
消息的发送
消息的发送有 SendMessage
和 PostMessage
两个 API ,它们的主要区别在于消息的同步性和返回值的处理。
SendMessage
:SendMessage
是一个同步的函数,即在消息发送之后,它会等待消息处理完毕,并且返回值是消息处理函数的返回值。- 调用
SendMessage
会阻塞当前线程,调用窗口过程函数,直到消息被处理完毕,然后才会继续执行后续代码。 - 返回值:
SendMessage
的返回值通常由消息处理函数返回,可以根据具体的消息类型和上下文来确定返回值的含义。
PostMessage
:PostMessage
是一个异步的函数,即在消息发送之后,它会立即返回,并不等待消息处理完毕。- 调用
PostMessage
不会阻塞当前线程,而是将消息放入消息队列中,然后立即返回,允许当前线程继续执行后续代码。 - 返回值:
PostMessage
函数没有返回值。
由于窗口句柄是全局的,因此操纵自己的窗口和操纵其他进程的窗口是没有区别的。因此我们只要获取到其他进程的窗口句柄就可以向该窗口发消息,从而操作该进程的窗口。
获取窗口句柄的函数为 FindWindow
,该函数定义如下:
cpp
WINUSERAPI
HWND
WINAPI
FindWindowW(
_In_opt_ LPCWSTR lpClassName,
_In_opt_ LPCWSTR lpWindowName);
lpClassName
:可选参数,指定要查找的窗口类名。如果为NULL,则表示不限制类名搜索条件。lpWindowName
:可选参数,指定要查找的窗口名。如果为NULL,则表示不限制窗口名搜索条件。
由于这里模拟键盘输入项 Notepad 写入内容,而 Notepad 的窗口名不确定,因此这里只指定类名为 Notepad
。
使用 Spy++ 查看 Windows 11 的 Notepad 发现编辑框的窗口类名为 RichEditD2DPT
,需要按照类名查找一个窗口子窗口的句柄。
这里我实现的 FindChildByName
函数可以完成该功能。
cpp
HWND FindChildByName(HWND hWnd, LPTCH name) {
if (hWnd == NULL) {
return NULL;
}
HWND hChild = GetWindow(hWnd, GW_CHILD);
while (hChild != NULL) {
TCHAR className[256];
GetClassName(hChild, className, 256);
if (_tcscmp(className, name) == 0) {
return hChild;
}
hChild = GetWindow(hChild, GW_HWNDNEXT);
}
return NULL;
}
最后调用 PostMessage
(注意不是 SendMessage
因为消息队列接收消息后还有额外的处理) 向窗口发送键盘输入消息即可。
完整代码如下:
cpp
#include <Windows.h>
#include <tchar.h>
HWND FindChildByName(HWND hWnd, LPTCH name) {
if (hWnd == NULL) {
return NULL;
}
HWND hChild = GetWindow(hWnd, GW_CHILD);
while (hChild != NULL) {
TCHAR className[256];
GetClassName(hChild, className, 256);
if (_tcscmp(className, name) == 0) {
return hChild;
}
hChild = GetWindow(hChild, GW_HWNDNEXT);
}
return NULL;
}
int WINAPI _tWinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
TCHAR *lpCmdLine,
int nCmdShow) {
HWND hNotepad = FindWindow(_T("Notepad"), NULL);
if (hNotepad == NULL) {
return FALSE;
}
HWND hNotepadTextBox = FindChildByName(hNotepad, (LPTCH) _T("NotepadTextBox"));
if (hNotepadTextBox == NULL) {
return FALSE;
}
HWND hRichEditD2DPT = FindChildByName(hNotepadTextBox, (LPTCH) _T("RichEditD2DPT"));
if (hRichEditD2DPT == NULL) {
return FALSE;
}
PostMessage(hRichEditD2DPT, WM_KEYDOWN, _T('S'), 0);
PostMessage(hRichEditD2DPT, WM_KEYDOWN, _T('K'), 0);
PostMessage(hRichEditD2DPT, WM_KEYDOWN, _T('Y'), 0);
PostMessage(hRichEditD2DPT, WM_KEYDOWN, _T('1'), 0);
PostMessage(hRichEditD2DPT, WM_KEYDOWN, _T('2'), 0);
PostMessage(hRichEditD2DPT, WM_KEYDOWN, _T('3'), 0);
return 0;
}
定时器
在消息发送的时候我们遇到一个问题,如果是在其他进程的窗口绘制图形会因为该窗口刷新而被覆盖,因此需要一直不停的绘制才能保证图形始终可见。如果写一个死循环来完整这个功能会导致本进程的消息队列无法使用,因此需要借助定时器来定时发送消息调用对应的处理函数来完成相应的功能。
定时器可以通过 SetTimer
来创建。该函数定义如下:
cpp
WINUSERAPI
UINT_PTR
WINAPI
SetTimer(
_In_opt_ HWND hWnd,
_In_ UINT_PTR nIDEvent,
_In_ UINT uElapse,
_In_opt_ TIMERPROC lpTimerFunc);
hWnd
:可选参数,指定要接收定时器消息的窗口句柄。如果为NULL
,则定时器消息将被发送到调用SetTimer
函数的线程的消息队列。nIDEvent
:指定定时器的标识符。可以使用一个整数值来唯一标识定时器。(也就是MSG
中的wParam
)uElapse
:指定定时器触发的时间间隔,以毫秒为单位。lpTimerFunc
:可选参数,指定一个定时器回调函数的指针。当定时器触发时,系统将调用此回调函数。- 返回值:如果函数调用成功,将返回定时器的标识符。可以使用此标识符来识别和操作定时器。如果函数调用失败,将返回 0。
我们在 OnCreate
函数中可以调用 SetTimer
函数创建定时器。
cpp
SetTimer(hwnd, 1, 10, NULL);
在 OnDestroy
函数中需要调用 KillTimer
来销毁定时器。
cpp
KillTimer(hwnd, 1);
定时器会以一定的时间间隔想队列里面发送 WM_TIMER
消息,因此我们可以在 OnTimer
中写需要定时执行的代码。例如定时在桌面上打印字符串:
cpp
LRESULT CALLBACK OnTimer(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnTimer %x\n"), WORD(uMsg));
HWND hDesktop = GetDesktopWindow();
HDC hdc = GetDC(hDesktop);
TextOut(hdc, 0, 0, _T("sky123"), 6);
ReleaseDC(hwnd, hdc);
CloseHandle(hDesktop);
return TRUE;
}
完整代码如下:
cpp
#include <Windows.h>
#include <Windowsx.h>
#include <stdio.h>
#include <tchar.h>
void ShowErrMsg() {
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
GetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),// Default language
(LPTSTR) &lpMsgBuf,
0,
NULL);
MessageBox(NULL, (LPCTSTR) lpMsgBuf, _T("Error"), MB_OK | MB_ICONINFORMATION);
LocalFree(lpMsgBuf);
}
#ifdef _DEBUG
void DebugPrintf(LPCTSTR format, ...) {
TCHAR szBuf[MAXBYTE];
va_list args;
va_start(args, format);
#ifdef UNICODE
vswprintf_s(szBuf, sizeof(szBuf) / sizeof(TCHAR), format, args);
#else
vsprintf_s(szBuf, sizeof(szBuf), format, args);
#endif
va_end(args);
OutputDebugString(szBuf);
}
#else
#define DebugPrintf
#endif
LRESULT CALLBACK OnCreate(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnCreate %x\n"), WORD(uMsg));
SetTimer(hwnd, 1, 10, NULL);
return TRUE;
}
LRESULT CALLBACK OnDestroy(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnDestroy %x\n"), WORD(uMsg));
KillTimer(hwnd, 1);
PostQuitMessage(0);
return TRUE;
}
LRESULT CALLBACK OnTimer(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnTimer %x\n"), WORD(uMsg));
HWND hDesktop = GetDesktopWindow();
HDC hdc = GetDC(hDesktop);
TextOut(hdc, 0, 0, _T("sky123"), 6);
ReleaseDC(hwnd, hdc);
CloseHandle(hDesktop);
return TRUE;
}
// 实现窗口过程函数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
LRESULT lReturn = FALSE;
switch (uMsg) {
case WM_CREATE:
lReturn = OnCreate(hwnd, uMsg, wParam, lParam);
break;
case WM_DESTROY:
lReturn = OnDestroy(hwnd, uMsg, wParam, lParam);
break;
case WM_TIMER:
lReturn = OnTimer(hwnd, uMsg, wParam, lParam);
break;
}
if (lReturn) {
return lReturn;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);// 默认窗口处理函数
}
int WINAPI _tWinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
TCHAR *lpCmdLine,
int nCmdShow) {
// 设计注册窗口类
TCHAR szWndClassName[] = TEXT("sky123ClassName");
TCHAR szWndName[] = _T("sky123");
WNDCLASSEX wc{};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS;// 窗口类型
wc.lpfnWndProc = WindowProc; // 窗口过程函数(窗口回调函数->处理信息)
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_ERROR); // 图标
wc.hCursor = LoadCursor(NULL, IDC_HAND); // 光标
wc.hbrBackground = CreateSolidBrush(RGB(255, 255, 255));// 窗口背景颜色刷子
wc.lpszMenuName = NULL; // 菜单名称
wc.lpszClassName = szWndClassName; // 窗口类名
if (RegisterClassEx(&wc) == 0) {
ShowErrMsg();
return 0;
}
// 创建窗口实例
HWND hWnd = CreateWindowEx(
0,
szWndClassName,
szWndName,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);
if (hWnd == NULL) {
ShowErrMsg();
return 0;
}
// 显示和更新窗口
ShowWindow(hWnd, SW_SHOWNORMAL);
SetClassLongPtr(hWnd, GCLP_HCURSOR, (LONG) LoadCursor(NULL, IDC_CROSS));
UpdateWindow(hWnd);
// 创建消息循环
MSG msg;
while (BOOL bRet = GetMessage(&msg, NULL, 0, 0)) {
if (bRet == -1) {
ShowErrMsg();
break;
}
TranslateMessage(&msg);// 翻译消息
DispatchMessage(&msg); // 派发消息
}
return msg.wParam;
}
可以看到在桌面左上角有打印的字符串:
图形设备接口(GDI)
图形设备接口(GDI,Graph Device Interface)是微软提供的图形绘制 API 。
这里我们通过编写一个简易的 Notepad 来讲解 GDI 的使用。
首先需要一个全局变量 g_Text
保存输入的内容,为了同时兼容 Unicode 和 ANSI 两种字符集,这里定义了 tstring
类型。
cpp
#ifdef UNICODE
#define tstring wstring
#else
#define tstring string
#endif
std::tstring g_Text;
设备上下文(DC)
设备上下文(DC,Device Context)保存了图像绘制的相关信息,其中包含有关设备(如显示器或打印机)绘图属性的信息等。在 GDI 中绘制任何图形都需要提供 DC ,也就是 DC 的句柄 HDC 。
可以通过 GetDC
函数获取窗口的 DC ,该函数定义如下(如果想获取非客户区域的 DC 需要使用 GetWindowDC
):
cpp
WINUSERAPI
HDC
WINAPI
GetDC(
_In_opt_ HWND hWnd);
-
hWnd
:要检索其 DC 的窗口的句柄。 如果此值为NULL
,则GetDC
将检索整个屏幕的 DC。 -
返回值:如果函数成功,则返回值是指定窗口工作区的 DC 的句柄。如果函数失败,则返回值为
NULL
。
注意,当 GetDC
获取一个 DC 的同时系统会为 DC 申请相关的资源,因此在使用完 DC 后需要调用 ReleaseDC
函数将 DC 释放。该函数定义如下:
cpp
WINUSERAPI
int
WINAPI
ReleaseDC(
_In_opt_ HWND hWnd,
_In_ HDC hDC);
-
hWnd
:要释放其 DC 的窗口的句柄。 -
hDC
:要释放的 DC 的句柄。 -
返回值:返回值指示是否释放了 DC。 如果释放 DC,则返回值为 1。如果未释放 DC,则返回值为 0。
绘制文本
Notepad 需要显示输入内容,也就是绘制文本。绘制文本相关的 API 有 DrawText
和 TextOut
。
TextOut
定义如下:
cpp
WINGDIAPI BOOL WINAPI TextOutW( _In_ HDC hdc, _In_ int x, _In_ int y, _In_reads_(c) LPCWSTR lpString, _In_ int c);
hdc
:定要进行绘制的设备上下文句柄。该句柄表示用于绘制的设备,可以是显示器、打印机或内存设备上下文等。x
:指定字符串绘制的起始点的 x 坐标。y
:指定字符串绘制的起始点的 y 坐标。lpString
:指向要绘制的字符串的指针。字符串以 null 终止。c
:指定要绘制的字符数。如果为 -1,则函数将绘制整个以 null 结尾的字符串。- 返回值:函数返回一个
BOOL
类型的值,表示绘制是否成功。如果绘制成功,返回值为非零;否则,返回值为零。
如果是使用 TextOut
实现绘制文本则 OnChar
实现如下:
cpp
LRESULT CALLBACK OnChar(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
g_Text.push_back(wParam == _T('\r') ? _T('\n') : wParam);
HDC hdc = GetDC(hwnd);
TextOut(hdc, 0, 0, g_Text.c_str(), g_Text.size());
ReleaseDC(hwnd, hdc);
return TRUE;
}
调试发现回车键对应的 wParam
为 \r
因此需要手动转成 \n
。然而 TextOut
本身无法正确显示回车,因此该 API 不适合此场景。
DrawText
定义如下:
cpp
WINUSERAPI
_Success_(return)
int
WINAPI
DrawTextW(
_In_ HDC hdc,
_When_((format & DT_MODIFYSTRING), _At_((LPWSTR)lpchText, _Inout_grows_updates_bypassable_or_z_(cchText, 4)))
_When_((!(format & DT_MODIFYSTRING)), _In_bypassable_reads_or_z_(cchText))
LPCWSTR lpchText,
_In_ int cchText,
_Inout_ LPRECT lprc,
_In_ UINT format);
hdc
:指定要进行绘制的设备上下文句柄。lpchText
:指向要绘制的文本的指针。可以是以 null 结尾的字符串,或者是包含 null 字符的缓冲区。cchText
:指定要绘制的字符数。如果为 -1,则函数将绘制整个以 null 结尾的字符串。lprc
:指向一个RECT
结构的指针,表示文本绘制的矩形区域。绘制的文本将根据指定的矩形区域进行换行和截断。这里通过GetClientRect
获取窗口范围即可。format
:指定文本绘制的格式和选项。
如果是使用 DrawText
实现绘制文本则 OnChar
实现如下:
cpp
LRESULT CALLBACK OnChar(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
if ((char) wParam == '\x08' && !g_Text.empty()) {
g_Text.pop_back();
} else {
g_Text.push_back(wParam == _T('\r') ? _T('\n') : wParam);
}
HDC hdc = GetDC(hwnd);
RECT rc;
GetClientRect(hwnd, &rc);
DrawText(hdc, g_Text.c_str(), g_Text.length(), &rc, DT_LEFT);
ReleaseDC(hwnd, hdc);
return TRUE;
}
其中 \b
为 Backspace
键,这里对应为模拟字符删除操作。
虽然上述实现虽然解决了 TextOut
存在的问题,但是在删除字符后发现已经删除的字符还会显示出来。这是因为 DrawText
只会将字符串打印在给定的区域,而字符串覆盖不到的区域会保持原样。因此我们在 DrawText
显示字符串之前还要先将窗口刷成背景色。因此有如下改进代码。这里要注意刷子在使用完之后需要还原并释放刷子。
cpp
LRESULT CALLBACK OnChar(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
if ((TCHAR) wParam == _T('\b') && !g_Text.empty()) {
g_Text.pop_back();
} else {
g_Text.push_back(wParam == _T('\r') ? _T('\n') : wParam);
}
// 获取 DC
HDC hdc = GetDC(hwnd);
// 获取窗口客户区域大小
RECT rc;
GetClientRect(hwnd, &rc);
// 创建一个白色刷子
HBRUSH hBrush = CreateSolidBrush(RGB(255, 255, 255));
// DC 选择刷子
HBRUSH hBrushOld = SelectBrush(hdc, hBrush);
// 绘制背景
FillRect(hdc, &rc, hBrush);
// 绘制文本
DrawText(hdc, g_Text.c_str(), g_Text.length(), &rc, DT_LEFT);
// 还原刷子
SelectBrush(hdc, hBrushOld);
// 释放刷子
DeleteBrush(hBrush);
// 释放 DC
ReleaseDC(hwnd, hdc);
return TRUE;
}
添加插入符
添加插入符可以使用 CreateCaret
函数,该函数定义如下:
cpp
WINUSERAPI
BOOL
WINAPI
CreateCaret(
_In_ HWND hWnd,
_In_opt_ HBITMAP hBitmap,
_In_ int nWidth,
_In_ int nHeight);
hWnd
:指定插入符要创建的窗口句柄。插入符将与该窗口关联。hBitmap
:可选参数,指定插入符的位图句柄。如果为 NULL,则插入符将以系统默认样式显示。nWidth
:指定插入符的宽度(以像素为单位)。nHeight
:指定插入符的高度(以像素为单位)。- 返回值:函数返回一个
BOOL
类型的值,表示创建插入符的成功与否。如果创建成功,返回值为非零;否则,返回值为零。
由于要计算插入符的高度以及位置,因此需要在 OnCreate
函数中获取字体相关信息。
cpp
TEXTMETRIC g_tm;
LRESULT CALLBACK OnCreate(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnCreate %x\n"), WORD(uMsg));
HDC hdc = GetDC(hwnd);
SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));
GetTextMetrics(hdc, &g_tm);
ReleaseDC(hwnd, hdc);
return TRUE;
}
在 WM_SETFOCUS
消息处理函数 OnSetFocus
函数中调用 CreateCaret
创建插入符,并且设置插入符的位置并显示:
cpp
LRESULT CALLBACK OnSetFocus(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnSetFocus %x\n"), WORD(uMsg));
// 创建插入符
CreateCaret(hwnd, (HBITMAP) NULL, 2, g_tm.tmHeight);
// 显示插入符
ShowCaret(hwnd);
// 设置插入符位置
SetCaretPos(g_tm.tmAveCharWidth * g_Text.size(), 0);
return TRUE;
}
在处理 WM_KILLFOCUS
消息的函数 OnKillFocus
函数中调用 DestroyCaret
函数销毁插入符。
cpp
LRESULT CALLBACK OnKillFocus(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnKillFocus %x\n"), WORD(uMsg));
DestroyCaret();
return TRUE;
}
另外再 OnChar
函数中每写入入一个字符的时候都要重新计算插入符的位置。这里需要注意显示插入符必须在 FillRect
重绘背景之前。因为插入符一直在闪烁也就是一直在重绘,因此改变插入符位置会在插入符上一个所在位置上留下"残影"。
cpp
ShowCaret(hwnd);
SetCaretPos(g_tm.tmAveCharWidth * g_Text.size(), 0);
无效区域
当窗口最小化最大化之后,窗口中的文字会消失。这是因为 Windows 重绘窗口把原本的文字覆盖了。
Windows 重绘窗口涉及到两个消息:
WM_ERASEBKGND
:消息通常在窗口需要重绘背景时发送给窗口。它用于擦除窗口的背景,并为重绘做准备。WM_PAINT
:消息在窗口需要绘制或重新绘制时发送给窗口。
当页面发生改变的时候系统会依次向消息队列中发送 WM_ERASEBKGND
和 WM_PAINT
两个消息。因此我们可以在接收到 WM_PAINT
消息的时候调用将文字重新显示在窗口中。
然而这样做的话会有一个问题,系统会不停的发送 WM_ERASEBKGND
和 WM_PAINT
消息导致窗口很卡,为了解决这一问题,这里引入了一个"无效区域"的概念。
"无效区域"(Invalid Region)是指在窗口或设备上需要重新绘制的区域。当窗口或设备的内容发生变化时,无效区域表示需要更新的部分,而不是整个窗口或设备。而与之相对应的有效区域是指窗口中没有变化的部分,这一部分不需要重新绘制。
而我们在接收到 WM_PAINT
消息的时候调用将文字重新显示在窗口中时,没有把显示文字的部分设为有效区域,系统发现这一部分还是无效区域,就继续向消息队列中发送 WM_ERASEBKGND
和 WM_PAINT
两个消息。
因此在重新显示文字后需要使用 ValidateRect
函数将窗口设为有效区域。
cpp
LRESULT CALLBACK OnPaint(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnPaint %x\n"), WORD(uMsg));
// 获取 DC
HDC hdc = GetDC(hwnd);
// 设置光标
ShowCaret(hwnd);
SetCaretPos(g_tm.tmAveCharWidth * g_Text.size(), 0);
// 获取窗口客户区域大小
RECT rc;
GetClientRect(hwnd, &rc);
// 绘制文本
DrawText(hdc, g_Text.c_str(), g_Text.length(), &rc, DT_LEFT);
// 释放 DC
ReleaseDC(hwnd, hdc);
// 将窗口设为有效区域
ValidateRect(hwnd, &rc);
return TRUE;
}
事实上我们通常的做法是使用 BeginPaint
函数来获取 DC,因此这样获取的 DC 之和无效区域有关,这样重绘的也只是无效区域,并且 EndPaint
会自动将无效区域设为有效区域。
cpp
LRESULT CALLBACK OnPaint(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnPaint %x\n"), WORD(uMsg));
// 获取 DC
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
// 设置光标
ShowCaret(hwnd);
SetCaretPos(g_tm.tmAveCharWidth * g_Text.size(), 0);
// 获取窗口客户区域大小
RECT rc;
GetClientRect(hwnd, &rc);
// 绘制文本
DrawText(hdc, g_Text.c_str(), g_Text.length(), &rc, DT_LEFT);
// 自动将无效区域设为有效区域
EndPaint(hwnd, &ps);
return TRUE;
}
另外我们发现 OnChar
函数中的代码和 OnPaint
函数中的代码有重复,并且 WM_PAINT
之前的 WM_ERASEBKGND
会重绘背景,因此我们只需要再 OnChar
函数中将窗口设为无效区域就可以自动在 OnPaint
函数中显示文字。
cpp
LRESULT CALLBACK OnChar(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
if ((TCHAR) wParam == _T('\b') && !g_Text.empty()) {
g_Text.pop_back();
} else {
g_Text.push_back(wParam == _T('\r') ? _T('\n') : wParam);
}
// 获取窗口客户区域大小
RECT rc;
GetClientRect(hwnd, &rc);
// 设置为无效区域
InvalidateRect(hwnd, &rc, TRUE);
return TRUE;
}
添加菜单
添加菜单可以使用 Menu
类型。注意在字符串中加入 &[快捷键]
就可以使用 Alt + 快捷键
打开菜单的对应项,不过需要逐级展开。
cpp
// 弹出菜单
HMENU hMenu = CreateMenu();
AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT_PTR) hMenu, _T("文件(&F)"));
AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT_PTR) hMenu, _T("编辑(&E)"));
SetMenu(hWnd, hMenu);
// 添加子菜单
HMENU hSubMenu = GetSubMenu(hMenu, 0);
AppendMenu(hSubMenu, MF_STRING, IDM_OPEN, _T("打开(&O)"));
AppendMenu(hSubMenu, MF_STRING, IDM_SAVE, _T("报错(&S)"));
AppendMenu(hSubMenu, MF_STRING, IDM_EXIT, _T("退出(&E)"));
在菜单被点击的时候会发送 WM_COMMAND
消息,并且 wParam
的低 2 字节存放菜单编号。因此 OnCommand
有如下实现:
cpp
LRESULT CALLBACK OnCommand(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnCommand %x\n"), WORD(uMsg));
WORD wID = LOWORD(wParam);
switch (wID) {
case IDM_OPEN:
MessageBox(NULL, _T("打开"), _T("sky123"), MB_OK);
break;
case IDM_SAVE:
MessageBox(NULL, _T("保存"), _T("sky123"), MB_OK);
break;
case IDM_EXIT:
PostQuitMessage(0);
break;
}
return TRUE;
}
快捷键
与菜单相似我们可以利用 CreateAcceleratorTable
创建快捷键,快捷键的消息类型也是 WM_COMMAND
。
cpp
// 申请堆地址空间
ACCEL *pAccelNews = (ACCEL *) HeapAlloc(GetProcessHeap(), 0, sizeof(ACCEL) * 3);
if (pAccelNews == NULL) {
ShowErrMsg();
return 0;
}
pAccelNews[0].fVirt = FCONTROL | FVIRTKEY;
pAccelNews[0].key = _T('O');
pAccelNews[0].cmd = IDM_OPEN;
pAccelNews[1].fVirt = FCONTROL | FVIRTKEY;
pAccelNews[1].key = _T('S');
pAccelNews[1].cmd = IDM_SAVE;
pAccelNews[2].fVirt = FCONTROL | FALT | FVIRTKEY;
pAccelNews[2].key = _T('E');
pAccelNews[2].cmd = IDM_EXIT;
// 创建快捷键表
HACCEL hAccel = CreateAcceleratorTable(pAccelNews, 3);
if (hAccel == NULL) {
ShowErrMsg();
return 0;
}
并且在消息循环中要将键盘消息转换为快捷键消息。
cpp
TranslateAccelerator(hWnd, hAccel, &msg)
在程序结束的时候要销毁快捷键。
cpp
// 删除快捷键表
DestroyAcceleratorTable(hAccel);
HeapFree(GetProcessHeap(), 0, pAccelNews);
OnCommand
函数根据消息参数确定消息的来源并分别处理。
cpp
LRESULT CALLBACK OnCommand(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnCommand %x\n"), WORD(uMsg));
WORD wID = LOWORD(wParam);
WORD wNotifyCode = HIWORD(wParam);
if (wNotifyCode == 1) { // 快捷键
switch (wID) {
case IDM_OPEN:
MessageBox(NULL, _T("快捷键打开"), _T("sky123"), MB_OK);
break;
case IDM_SAVE:
MessageBox(NULL, _T("快捷键保存"), _T("sky123"), MB_OK);
break;
case IDM_EXIT:
PostQuitMessage(0);
break;
}
} else if (wNotifyCode == 0) { // 菜单
switch (wID) {
case IDM_OPEN:
MessageBox(NULL, _T("菜单打开"), _T("sky123"), MB_OK);
break;
case IDM_SAVE:
MessageBox(NULL, _T("菜单保存"), _T("sky123"), MB_OK);
break;
case IDM_EXIT:
PostQuitMessage(0);
break;
}
} else if (lParam != NULL) { // 控件
}
return TRUE;
}
示例程序
事实上这一实现还存在很多严重问题,并且功能上还有很多缺失。因此实现一个完整功能的 Notepad 实际上是非常困难的。事实上为了提高开发效率 Windows 已经提前实现好一些特定功能的窗口,我们称这类窗口为控件(Controls)。
cpp
#include <Windows.h>
#include <Windowsx.h>
#include <stdio.h>
#include <string>
#include <tchar.h>
void ShowErrMsg() {
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
GetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),// Default language
(LPTSTR) &lpMsgBuf,
0,
NULL);
MessageBox(NULL, (LPCTSTR) lpMsgBuf, _T("Error"), MB_OK | MB_ICONINFORMATION);
LocalFree(lpMsgBuf);
}
#ifdef _DEBUG
void DebugPrintf(LPCTSTR format, ...) {
TCHAR szBuf[MAXBYTE];
va_list args;
va_start(args, format);
#ifdef UNICODE
vswprintf_s(szBuf, sizeof(szBuf) / sizeof(TCHAR), format, args);
#else
vsprintf_s(szBuf, sizeof(szBuf), format, args);
#endif
va_end(args);
OutputDebugString(szBuf);
}
#else
#define DebugPrintf
#endif
#ifdef UNICODE
#define tstring wstring
#else
#define tstring string
#endif
std::tstring g_Text;
TEXTMETRIC g_tm;
enum {
IDM_OPEN = 100,
IDM_SAVE,
IDM_EXIT
};
LRESULT CALLBACK OnCreate(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnCreate %x\n"), WORD(uMsg));
HDC hdc = GetDC(hwnd);
SelectObject(hdc, GetStockObject(SYSTEM_FIXED_FONT));
GetTextMetrics(hdc, &g_tm);
ReleaseDC(hwnd, hdc);
return TRUE;
}
LRESULT CALLBACK OnClose(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnClose %x\n"), WORD(uMsg));
return FALSE;
}
LRESULT CALLBACK OnDestroy(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnDestroy %x\n"), WORD(uMsg));
PostQuitMessage(0);
return TRUE;
}
LRESULT CALLBACK OnChar(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
if ((TCHAR) wParam == _T('\b') && !g_Text.empty()) {
g_Text.pop_back();
} else {
g_Text.push_back(wParam == _T('\r') ? _T('\n') : wParam);
}
// 获取窗口客户区域大小
RECT rc;
GetClientRect(hwnd, &rc);
// 设置为无效区域
InvalidateRect(hwnd, &rc, TRUE);
return TRUE;
}
LRESULT CALLBACK OnSetFocus(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnSetFocus %x\n"), WORD(uMsg));
// 创建插入符
CreateCaret(hwnd, (HBITMAP) NULL, 2, g_tm.tmHeight);
// 显示插入符
ShowCaret(hwnd);
// 设置插入符位置
SetCaretPos(g_tm.tmAveCharWidth * g_Text.size(), 0);
return TRUE;
}
LRESULT CALLBACK OnKillFocus(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnKillFocus %x\n"), WORD(uMsg));
DestroyCaret();
return TRUE;
}
LRESULT CALLBACK OnCommand(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnCommand %x\n"), WORD(uMsg));
WORD wID = LOWORD(wParam);
WORD wNotifyCode = HIWORD(wParam);
if (wNotifyCode == 1) { // 快捷键
switch (wID) {
case IDM_OPEN:
MessageBox(NULL, _T("快捷键打开"), _T("sky123"), MB_OK);
break;
case IDM_SAVE:
MessageBox(NULL, _T("快捷键保存"), _T("sky123"), MB_OK);
break;
case IDM_EXIT:
PostQuitMessage(0);
break;
}
} else if (wNotifyCode == 0) { // 菜单
switch (wID) {
case IDM_OPEN:
MessageBox(NULL, _T("菜单打开"), _T("sky123"), MB_OK);
break;
case IDM_SAVE:
MessageBox(NULL, _T("菜单保存"), _T("sky123"), MB_OK);
break;
case IDM_EXIT:
PostQuitMessage(0);
break;
}
} else if (lParam != NULL) { // 控件
}
return TRUE;
}
LRESULT CALLBACK OnPaint(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnPaint %x\n"), WORD(uMsg));
// 获取 DC
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
// 设置光标
ShowCaret(hwnd);
SetCaretPos(g_tm.tmAveCharWidth * g_Text.size(), 0);
// 获取窗口客户区域大小
RECT rc;
GetClientRect(hwnd, &rc);
// 绘制文本
DrawText(hdc, g_Text.c_str(), g_Text.length(), &rc, DT_LEFT);
// 自动将无效区域设为有效区域
EndPaint(hwnd, &ps);
return TRUE;
}
// 实现窗口过程函数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
LRESULT lReturn = FALSE;
switch (uMsg) {
case WM_CREATE:
lReturn = OnCreate(hwnd, uMsg, wParam, lParam);
break;
case WM_CLOSE:
lReturn = OnClose(hwnd, uMsg, wParam, lParam);
break;
case WM_DESTROY:
lReturn = OnDestroy(hwnd, uMsg, wParam, lParam);
break;
case WM_CHAR:
lReturn = OnChar(hwnd, uMsg, wParam, lParam);
break;
case WM_SETFOCUS:
lReturn = OnSetFocus(hwnd, uMsg, wParam, lParam);
break;
case WM_KILLFOCUS:
lReturn = OnKillFocus(hwnd, uMsg, wParam, lParam);
break;
case WM_PAINT:
lReturn = OnPaint(hwnd, uMsg, wParam, lParam);
break;
case WM_COMMAND:
lReturn = OnCommand(hwnd, uMsg, wParam, lParam);
break;
}
if (lReturn) {
return lReturn;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);// 默认窗口处理函数
}
int WINAPI _tWinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
TCHAR *lpCmdLine,
int nCmdShow) {
// 设计注册窗口类
TCHAR szWndClassName[] = TEXT("sky123ClassName");
TCHAR szWndName[] = _T("sky123");
WNDCLASSEX wc{};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS;// 窗口类型
wc.lpfnWndProc = WindowProc; // 窗口过程函数(窗口回调函数->处理信息)
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_ERROR); // 图标
wc.hCursor = LoadCursor(NULL, IDC_HAND); // 光标
wc.hbrBackground = CreateSolidBrush(RGB(255, 255, 255));// 窗口背景颜色刷子
wc.lpszMenuName = NULL; // 菜单名称
wc.lpszClassName = szWndClassName; // 窗口类名
if (RegisterClassEx(&wc) == 0) {
ShowErrMsg();
return 0;
}
// 创建窗口实例
HWND hWnd = CreateWindowEx(
0,
szWndClassName,
szWndName,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);
if (hWnd == NULL) {
ShowErrMsg();
return 0;
}
// 弹出菜单
HMENU hMenu = CreateMenu();
AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT_PTR) hMenu, _T("文件(&F)"));
AppendMenu(hMenu, MF_STRING | MF_POPUP, (UINT_PTR) hMenu, _T("编辑(&E)"));
SetMenu(hWnd, hMenu);
// 添加子菜单
HMENU hSubMenu = GetSubMenu(hMenu, 0);
AppendMenu(hSubMenu, MF_STRING, IDM_OPEN, _T("打开(&O)"));
AppendMenu(hSubMenu, MF_STRING, IDM_SAVE, _T("保存(&S)"));
AppendMenu(hSubMenu, MF_STRING, IDM_EXIT, _T("退出(&E)"));
// 申请堆地址空间
ACCEL *pAccelNews = (ACCEL *) HeapAlloc(GetProcessHeap(), 0, sizeof(ACCEL) * 3);
if (pAccelNews == NULL) {
ShowErrMsg();
return 0;
}
pAccelNews[0].fVirt = FCONTROL | FVIRTKEY;
pAccelNews[0].key = _T('O');
pAccelNews[0].cmd = IDM_OPEN;
pAccelNews[1].fVirt = FCONTROL | FVIRTKEY;
pAccelNews[1].key = _T('S');
pAccelNews[1].cmd = IDM_SAVE;
pAccelNews[2].fVirt = FCONTROL | FALT | FVIRTKEY;
pAccelNews[2].key = _T('E');
pAccelNews[2].cmd = IDM_EXIT;
// 创建快捷键表
HACCEL hAccel = CreateAcceleratorTable(pAccelNews, 3);
if (hAccel == NULL) {
ShowErrMsg();
return 0;
}
// 显示和更新窗口
ShowWindow(hWnd, SW_SHOWNORMAL);
// 创建消息循环
MSG msg;
while (BOOL bRet = GetMessage(&msg, NULL, 0, 0)) {
if (bRet == -1) {
ShowErrMsg();
break;
}
// 转换快捷键消息 WM_COMMAND
if (!TranslateAccelerator(hWnd, hAccel, &msg)) {
TranslateMessage(&msg);// 翻译消息
DispatchMessage(&msg); // 派发消息
}
}
// 删除快捷键表
DestroyAcceleratorTable(hAccel);
HeapFree(GetProcessHeap(), 0, pAccelNews);
return msg.wParam;
}
控件
为了提高开发效率,Windows 预先定义了一些常用的窗口类型,这些窗口类型称为控件。常用控件的类名和作用如下:
Button
(按钮):用于触发操作或执行特定功能。ComboBox
(组合框):结合了文本框和下拉列表,允许用户从预定义的选项中选择或输入文本。Edit
(编辑框):用于接收和显示用户输入的文本。ListBox
(列表框):显示一个选项列表,允许用户选择一个或多个选项。MDIClient
(MDI 客户端窗口):用于承载多文档界面(Multiple Document Interface)应用程序中的子窗口。ScrollBar
(滚动条):用于在具有滚动内容的窗口中控制可见区域的位置。Static
(静态控件):用于显示文本或图像等静态内容,通常用作标签或说明文字。
控件消息统一为 WM_COMMAND
消息,而具体的消息由 wParam
和 lParam
决定。由于快捷键和菜单也使用 WM_COMMAND
消息,因此有如下规则区分和处理 WM_COMMAND
消息:
- 如果
wParam
的高 4 字节为 1 则消息来自快捷键,wParam
低 4 字节表示快捷键消息。 - 如果
wParam
的高 4 字节为 0 则消息来自菜单,wParam
低 4 字节表示快菜单消息。 - 如果
lParam
不为 NULL 则消息来自控件且lParam
表示控件句柄。另外wParam
低 4 字节表示控件 ID ,高 4 字节表示控件消息。
因此处理 WM_COMMAND
消息的 OnCommand
函数有如下实现:
cpp
LRESULT CALLBACK OnCommand(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnCommand %x\n"), WORD(uMsg));
WORD wID = LOWORD(wParam);
WORD wNotifyCode = HIWORD(wParam);
if (lParam != NULL) { // 控件
HWND hControl = (HWND) lParam; // 控件句柄
switch(wID) { // 控件ID
switch(wNotifyCode) { // 控件消息
...
}
...
}
} else if (wNotifyCode == 1) { // 快捷键
switch (wID) { // 快捷键消息
...
}
} else if (wNotifyCode == 0) { // 菜单
switch (wID) { // 菜单消息
...
}
}
return TRUE;
}
控件消息可以通过其宏定义的前缀来分类。以 Button
为例,其前缀主要有以下两种:
B(C)M
:即 Button (Control) Message ,表示按钮控件的消息。通常用于发送指令给按钮控件,或获取按钮控件的状态信息。(SendMessage
发往控件)B(C)N
:即 Button (Control) Notification ,表示按钮控件的通知。通常与按钮控件的事件或状态变化相关。(用户操作控件,控件发送消息到父窗口)
Button
Button
即按钮,是一种很常见的控件。我们可以通过 CreateWindow
函数来创建一个 Button
类型的控件。
cpp
HWND hButton1 = CreateWindowEx(
0,
_T("BUTTON"),
_T("确定"),
WS_CHILD | WS_VISIBLE | BS_CHECKBOX,
0,
0,
100,
50,
hWnd,
(HMENU) IDB_BUTTON1,
g_hInstance,
NULL);
lpClassName
:需要指定控件的类名 ,大小写不敏感。lpWindowName
:窗口标题或控件的文本。这里为按钮上的文字。dwStyle
:窗口或控件的样式。x
、y
:窗口或控件的初始位置。nWidth
、nHeight
:窗口或控件的初始大小。hWndParent
:父窗口的句柄,控件的很多消息会发送至父窗口。hMenu
:窗口或控件的菜单句柄,在父窗口的过程函数中处理控件消息会根据这个来确定是哪个控件。hInstance
:应用程序实例句柄。lpParam
:用户指定的参数。- 返回值:如果函数调用成功,将返回新创建窗口的句柄。如果函数调用失败,将返回 NULL。
由于 IDB_BUTTON1
是选择按钮,因此需要根据先 SendMessage
发送 BM_GETCHECK
消息到控件来获取按钮是否已被选择,然后根据按钮状态通过 SendMessage
更新按钮状态。
注意由于接收的消息为 BN
前缀因此这段代码应当写到控件的父窗口的过程函数调用的 OnCommand
函数中。
cpp
if (wID == IDB_BUTTON1) {
if (wNotifyCode == BN_CLICKED) {
LRESULT lResult = SendMessage(hControl, BM_GETCHECK, NULL, NULL);
if (lResult == BST_CHECKED) {
SendMessage(hControl, BM_SETCHECK, BST_UNCHECKED, NULL);
MessageBox(NULL, _T("BST_UNCHECKED"), _T("sky123"), MB_OK);
} else if (lResult == BST_UNCHECKED) {
SendMessage(hControl, BM_SETCHECK, BST_CHECKED, NULL);
MessageBox(NULL, _T("BST_CHECKED"), _T("sky123"), MB_OK);
}
}
Edit
Edit
是文本编辑器控件,即前面实现的 Notepad 对应的控件。
cpp
HWND hEdit = CreateWindowEx(
0,
_T("Edit"),
_T("粘贴区域"),
WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_LEFT | ES_MULTILINE | ES_AUTOVSCROLL,
0,
60,
200,
100,
hWnd,
(HMENU) IDE_EDIT1,
g_hInstance,
NULL);
上述示例代码创建了一个 Edit
控件,其中 dwStyle
中设置了如下属性:
WS_CHILD
:创建一个子窗口,作为父窗口的一个子元素。WS_VISIBLE
:使窗口可见。WS_VSCROLL
:显示垂直滚动条。ES_LEFT
:文本左对齐。ES_MULTILINE
:允许多行文本输入。ES_AUTOVSCROLL
:自动垂直滚动文本。
这里我们实现一个点击按钮就将剪贴板中的内容粘贴到编辑框中的功能。我们通过两个 SendMessage
分别发送 WM_SETTEXT
和 WM_PASTE
消息来清空编辑框和粘贴内容。
cpp
if (wNotifyCode == BN_CLICKED) {
HWND hEdit = GetDlgItem(hwnd, IDE_EDIT1);
SendMessage(hEdit, WM_SETTEXT, 0, 0);
SendMessage(hEdit, WM_PASTE, 0, 0);
}
ListBox
ListBox
即列表框,在创建后我们可以通过 SendMessage
发送 LB_ADDSTRING
消息来向其中添加内容。
注意 WIndows 中较新的控件会除了使用 WM_COMMAND
消息外还会使用 WM_NOTIFY
消息。因为 WM_COMMAND
消息只能传递 8 字节的参数,无法存储一些复杂的消息。所以在创建这些控件的时候要添加 LBS_NOTIFY
属性并且在处理消息的时候两种消息都要处理。
cpp
HWND hListBox = CreateWindowEx(
0,
_T("ListBox"),
_T("编程语言"),
WS_CHILD | WS_VISIBLE | LBS_NOTIFY | WS_VSCROLL,
0,
160,
200,
100,
hWnd,
(HMENU) IDL_LISTBOX1,
g_hInstance,
NULL);
SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM) _T("C/C++"));
SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM) _T("Java"));
SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM) _T("Python"));
对应列表框,我们实现一个点击列表框时获取点击的列表项的内容。这里首先发送 LB_GETCURSEL
消息获取点击的列表中的项的下标,之后发送 LB_GETTEXTLEN
消息获得该项的内容的长度,之后发送 LB_GETTEXT
获取列表项的内容,注意接收列表项的内容的缓存区的长度应当考虑字符串末尾的 \0
。
cpp
if (wNotifyCode == LBN_SELCHANGE) {
int nIndex = SendMessage(hControl, LB_GETCURSEL, 0, 0);
if (nIndex != -1) {
int nLen = SendMessage(hControl, LB_GETTEXTLEN, nIndex, 0);
LPVOID lpBuff = malloc((nLen + 1) * sizeof(TCHAR));
int nLength = SendMessage(hControl, LB_GETTEXT, nIndex, (LPARAM) lpBuff);
MessageBox(NULL, (LPTCH) lpBuff, _T("sky123"), MB_OK);
free(lpBuff);
}
}
示例程序
cpp
#include <Windows.h>
#include <Windowsx.h>
#include <stdio.h>
#include <string>
#include <tchar.h>
void ShowErrMsg() {
LPVOID lpMsgBuf;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
GetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),// Default language
(LPTSTR) &lpMsgBuf,
0,
NULL);
MessageBox(NULL, (LPCTSTR) lpMsgBuf, _T("Error"), MB_OK | MB_ICONINFORMATION);
LocalFree(lpMsgBuf);
}
#ifdef _DEBUG
void DebugPrintf(LPCTSTR format, ...) {
TCHAR szBuf[MAXBYTE];
va_list args;
va_start(args, format);
#ifdef UNICODE
vswprintf_s(szBuf, sizeof(szBuf) / sizeof(TCHAR), format, args);
#else
vsprintf_s(szBuf, sizeof(szBuf), format, args);
#endif
va_end(args);
OutputDebugString(szBuf);
}
#else
#define DebugPrintf
#endif
enum {
IDB_BUTTON1 = 105,
IDB_BUTTON2,
IDE_EDIT1,
IDL_LISTBOX1
};
HINSTANCE g_hInstance;
LRESULT CALLBACK OnDestroy(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnDestroy %x\n"), WORD(uMsg));
PostQuitMessage(0);
return TRUE;
}
LRESULT CALLBACK OnNotify(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnNotify %x\n"), WORD(uMsg));
NMHDR *pnmh = (LPNMHDR) lParam;
if (wParam == IDL_LISTBOX1) {
if (pnmh->code == LBN_SELCHANGE) {
int nIndex = SendMessage(pnmh->hwndFrom, LB_GETCURSEL, 0, 0);
if (nIndex != -1) {
int nLen = SendMessage(pnmh->hwndFrom, LB_GETTEXTLEN, nIndex, 0);
LPVOID lpBuff = malloc((nLen + 1) * sizeof(TCHAR));
int nLength = SendMessage(pnmh->hwndFrom, LB_GETTEXT, nIndex, (LPARAM) lpBuff);
MessageBox(NULL, (LPTCH) lpBuff, _T("sky123"), MB_OK);
free(lpBuff);
}
}
}
return TRUE;
}
LRESULT CALLBACK OnCommand(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
DebugPrintf(_T("[sky123] OnCommand %x\n"), WORD(uMsg));
WORD wID = LOWORD(wParam);
WORD wNotifyCode = HIWORD(wParam);
HWND hControl = (HWND) lParam;
if (hControl != NULL) {
if (wID == IDB_BUTTON1) {
if (wNotifyCode == BN_CLICKED) {
LRESULT lResult = SendMessage(hControl, BM_GETCHECK, NULL, NULL);
if (lResult == BST_CHECKED) {
SendMessage(hControl, BM_SETCHECK, BST_UNCHECKED, NULL);
MessageBox(NULL, _T("BST_UNCHECKED"), _T("sky123"), MB_OK);
} else if (lResult == BST_UNCHECKED) {
SendMessage(hControl, BM_SETCHECK, BST_CHECKED, NULL);
MessageBox(NULL, _T("BST_CHECKED"), _T("sky123"), MB_OK);
}
}
} else if (wID == IDB_BUTTON2) {
if (wNotifyCode == BN_CLICKED) {
HWND hEdit = GetDlgItem(hwnd, IDE_EDIT1);
SendMessage(hEdit, WM_SETTEXT, 0, 0);
SendMessage(hEdit, WM_PASTE, 0, 0);
}
} else if (wID == IDL_LISTBOX1) {
if (wNotifyCode == LBN_SELCHANGE) {
int nIndex = SendMessage(hControl, LB_GETCURSEL, 0, 0);
if (nIndex != -1) {
int nLen = SendMessage(hControl, LB_GETTEXTLEN, nIndex, 0);
LPVOID lpBuff = malloc((nLen + 1) * sizeof(TCHAR));
int nLength = SendMessage(hControl, LB_GETTEXT, nIndex, (LPARAM) lpBuff);
MessageBox(NULL, (LPTCH) lpBuff, _T("sky123"), MB_OK);
free(lpBuff);
}
}
}
}
return TRUE;
}
// 实现窗口过程函数
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
LRESULT lReturn = FALSE;
switch (uMsg) {
case WM_DESTROY:
lReturn = OnDestroy(hwnd, uMsg, wParam, lParam);
break;
case WM_COMMAND:
lReturn = OnCommand(hwnd, uMsg, wParam, lParam);
break;
case WM_NOTIFY:
lReturn = OnNotify(hwnd, uMsg, wParam, lParam);
break;
}
if (lReturn) {
return lReturn;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);// 默认窗口处理函数
}
void InitControl(HWND hWnd) {
HWND hButton1 = CreateWindowEx(
0,
_T("BUTTON"),
_T("确定"),
WS_CHILD | WS_VISIBLE | BS_CHECKBOX,
0,
0,
100,
50,
hWnd,
(HMENU) IDB_BUTTON1,
g_hInstance,
NULL);
HWND hButton2 = CreateWindowEx(
0,
_T("BUTTON"),
_T("粘贴"),
WS_CHILD | WS_VISIBLE,
110,
0,
100,
50,
hWnd,
(HMENU) IDB_BUTTON2,
g_hInstance,
NULL);
HWND hEdit = CreateWindowEx(
0,
_T("Edit"),
_T("粘贴区域"),
WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_LEFT | ES_MULTILINE | ES_AUTOVSCROLL,
0,
60,
200,
100,
hWnd,
(HMENU) IDE_EDIT1,
g_hInstance,
NULL);
HWND hListBox = CreateWindowEx(
0,
_T("ListBox"),
_T("编程语言"),
WS_CHILD | WS_VISIBLE | LBS_NOTIFY | WS_VSCROLL,
0,
160,
200,
100,
hWnd,
(HMENU) IDL_LISTBOX1,
g_hInstance,
NULL);
SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM) _T("C/C++"));
SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM) _T("Java"));
SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM) _T("Python"));
}
int WINAPI _tWinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
TCHAR *lpCmdLine,
int nCmdShow) {
// 设计注册窗口类
g_hInstance = hInstance;
TCHAR szWndClassName[] = TEXT("sky123ClassName");
TCHAR szWndName[] = _T("sky123");
WNDCLASSEX wc{};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS;// 窗口类型
wc.lpfnWndProc = WindowProc; // 窗口过程函数(窗口回调函数->处理信息)
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_ERROR); // 图标
wc.hCursor = LoadCursor(NULL, IDC_HAND); // 光标
wc.hbrBackground = CreateSolidBrush(RGB(255, 255, 255));// 窗口背景颜色刷子
wc.lpszMenuName = NULL; // 菜单名称
wc.lpszClassName = szWndClassName; // 窗口类名
if (RegisterClassEx(&wc) == 0) {
ShowErrMsg();
return 0;
}
// 创建窗口实例
HWND hWnd = CreateWindowEx(
0,
szWndClassName,
szWndName,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL);
if (hWnd == NULL) {
ShowErrMsg();
return 0;
}
InitControl(hWnd);
// 显示和更新窗口
ShowWindow(hWnd, SW_SHOWNORMAL);
// 创建消息循环
MSG msg;
while (BOOL bRet = GetMessage(&msg, NULL, 0, 0)) {
if (bRet == -1) {
ShowErrMsg();
break;
}
TranslateMessage(&msg);// 翻译消息
DispatchMessage(&msg); // 派发消息
}
return msg.wParam;
}
资源
什么是资源
资源(Resources)是指应用程序使用的非代码数据,如图像、字符串、图标、对话框模板等。资源可以在编译时嵌入到可执行文件中,然后在运行时由应用程序进行访问和使用。
带资源的程序的编译链接过程如下:
对话框
对话框资源创建
项目右键 -> 添加 -> 资源 -> 选择 Dialog 资源
- 从工具箱拖放控件
- 从属性栏设置对话框属性
- 选择控件可以设置对齐属性
每个控件可以设置 ID 号,这样在相应 WM_COMMAND
消息的时候可以确定消息来自哪个控件。
模态对话框与非模态对话框
模态对话框(Modal Dialog)和非模态对话框(Modeless Dialog)是在图形用户界面(GUI)中常见的两种对话框类型,它们在交互方式和应用程序控制方面有所不同。
-
模态对话框:
-
模态对话框是指打开后,用户必须完成对话框上的操作,或关闭对话框才能继续与应用程序的其他部分进行交互。
-
模态对话框会阻塞应用程序的其他窗口,用户无法与应用程序的其他部分进行交互,直到对话框被关闭。
-
模态对话框通常用于需要用户输入或确认的关键操作,例如文件保存、选项设置等。
-
通常,模态对话框使用函数如
DialogBox
或DoModal
来创建和显示。cppint WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, TCHAR *lpCmdLine, int nCmdShow) { // 创建模态对话框 INT_PTR nExitCode = DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DialogProc); return nExitCode; }
-
-
非模态对话框
-
非模态对话框是指打开后,用户可以同时与对话框和应用程序的其他部分进行交互。
-
非模态对话框不会阻塞应用程序的其他窗口,用户可以在对话框打开的同时执行其他操作。
-
非模态对话框通常用于提供辅助功能或快捷操作,例如工具选项、即时预览等。
-
通常,非模态对话框使用函数如
CreateDialog
或CreateDialogIndirect
来创建和显示,并通过消息循环处理用户的输入。cppint WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, TCHAR *lpCmdLine, int nCmdShow) { // 创建非模态对话框 HWND hDlg = CreateDialog(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DialogProc); ShowWindow(hDlg, SW_SHOWNORMAL); MSG msg; while (BOOL bRet = GetMessage(&msg, NULL, 0, 0)) { if (bRet == -1) { break; } TranslateMessage(&msg);// 翻译消息 DispatchMessage(&msg); // 派发消息 } return msg.wParam; }
-
对话框的消息
-
对话框初始消息是
WM_INITDIALOG
而不是WM_CREATE
。 -
对于模态对话框,需要在收到
WM_CLOSE
消息时调用EndDialog
函数。cppcase WM_CLOSE: DebugPrintf(_T("[sky123] WM_CLOSE")); EndDialog(hwndDlg, 0); break;
-
对于非模态对话框,需要再收到
WM_CLOSE
消息时调用DestroyWindow
函数,并且在收到WM_DESTROY
消息时调用PostQuitMessage
函数。cppcase WM_CLOSE: DebugPrintf(_T("[sky123] WM_CLOSE")); DestroyWindow(hwndDlg); break; case WM_DESTROY: DebugPrintf(_T("[sky123] WM_DESTROY")); PostQuitMessage(0); break;
控件使用举例(树控件)
cpp
HWND hTree = GetDlgItem(hwndDlg, IDC_TREE1);
TVINSERTSTRUCT ts{};
ts.item.mask = TVIF_TEXT;
ts.item.pszText =(LPTSTR)_T("Resource");
ts.item.cchTextMax = _tcsclen(ts.item.pszText);
HTREEITEM hRoot = (HTREEITEM) SendMessage(hTree, TVM_INSERTITEM, 0, (LPARAM) &ts);
ts = {};
ts.hParent = hRoot;
ts.item.mask = TVIF_TEXT;
ts.item.pszText = (LPTSTR) _T("头文件");
ts.item.cchTextMax = _tcsclen(ts.item.pszText);
HTREEITEM hChild1 = (HTREEITEM) SendMessage(hTree, TVM_INSERTITEM, 0, (LPARAM) &ts);
快捷键
快捷键资源创建
参考对话框资源创建,资源类型选择 Accelerator
。
快捷键资源的使用
直接调用 LoadAccelerators
加载快捷键资源。
cpp
HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1));
之后就可以在消息循环中调用 TranslateAccelerator
函数转换快捷键消息,并且在窗口过程函数中处理快捷键消息。
菜单
菜单资源的创建
菜单可以多级展开,&[快捷键]
可以设置快捷键。另外为了美观菜单栏遵循如下格式 [菜单名](&[菜单快捷键])\t快捷键
。在属性栏可以设置菜单的 ID 号(为了方便可以设置的和快捷键一样)。
菜单资源的使用
通过 LoadMenu
函数加载菜单资源,然后 SetMenu
函数将菜单资源应用到对话框中。
cpp
HMENU hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_MENU1));
SetMenu(hDlg, hMenu);
字符串表
字符串表可以用来统一管理字符串,这样就可以很方便的进行字符串的修改。
字符串表资源的创建
设置好 ID 和标题即可。
字符串表资源的使用
LoadString
根据字符串的 ID 加载对应字符串即可。
cpp
TCHAR szSaveBuf[MAXBYTE]{}, szOpenBuf[MAXBYTE]{}, szTitleBuf[MAXBYTE]{};
LoadString(g_hInstance, IDS_SAVE, szSaveBuf, sizeof(szSaveBuf) / sizeof(TCHAR) - 1);
LoadString(g_hInstance, IDS_OPEN, szOpenBuf, sizeof(szOpenBuf) / sizeof(TCHAR) - 1);
LoadString(g_hInstance, IDS_TITLE, szTitleBuf, sizeof(szTitleBuf) / sizeof(TCHAR) - 1);
if (LOWORD(wParam) == IDM_OPEN) {
MessageBox(hwndDlg, szOpenBuf, szTitleBuf, MB_OK);
} else if (LOWORD(wParam) == IDM_SAVE) {
MessageBox(hwndDlg, szSaveBuf, szTitleBuf, MB_OK);
}
光标
光标资源的创建
光标资源的使用
cpp
HCURSOR hCursor = LoadCursor(hInstance, MAKEINTRESOURCE(IDC_CURSOR1));
SetCursor(hCursor);
SetClassLongPtr(hDlg, GCLP_HCURSOR, (LONG) hCursor);
版本
版本资源用来描述程序的相关信息。
版本资源不需要显示加载,编译成可执行文件后版本资源会在可执行文件的详细信息中体现。
位图
位图资源的创建
在添加资源窗口选择导入,然后导入图片即可。注意图片应当是 BMP 格式。
位图资源的使用
在对话框中添加 Picture Control 控件,然后设置类型为 Bitmap 并且选择加载的 Bitmap 资源。
SDK 项目封装(俄罗斯方块)
CApplication
:负责程序初始化,消息循环,退出。CWindow
:负责消息处理,API 的调用。CMainWindow
:继承于CWindow
,与主窗口消息处理有关。CGame
:游戏逻辑。CGameView
:游戏视图。