kr 第二阶段(一)SDK

基本概念

什么是 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 ,因此两个符号都需要定义)

    #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定义
    CHARchar
    PSTRLPSTRchar*
    PCSTRLPCSTRconst char*
    WCHARwchar_t
    PWSTRLPWSTRwchar_t*
    PCWSTRLPCWSTRconst wchar_t*
  • 在结构体类型定义上,由于成员类型不同,因此一个结构体需要有 Unicode 与 ANSI 两个版本的定义,例如 WNDCLASSWNDCLASSW 和 和 WNDCLASSA 两个版本的定义。

  • 在 API 定义上,由于参数类型不同,因此一个 API 需要有 Unicode 与 ANSI 两个版本的定义,例如 MessageBoxMessageBoxAMessageBoxW 两种定义。

为了让一个程序既可以以 Unicode 编码也可以以 ANSI 编码编译成功,微软还定义了一系列的宏和数据类型。

  • 在数据类型上,有 TCHAR 类型可以根据当前程序的字符集自动切换为 charwchar_t 。另外还有下面两个定义:

    Typedef定义
    PTSTRLPTSTRTCHAR*
    PCTSTRLPCTSTRconst TCHAR*
  • 在字符串定义上可以在字符串外面加上 _T() 或者 TEXT() 实现不同编码下的字符串定义。例如 _T("sky123") 或者 TEXT("sky123") 。这里要注意 _T() 宏需要额外导入 tchar.h 头文件。

  • 函数和结构体也有对应的宏可以自动切换到正确的函数上,例如 WNDCLASSMessageBox 在不同的字符集下可以切换到正确的结构体和函数名称上。

  • Microsoft C 运行时库的标头定义了一组类似的宏。 例如,如果 _UNICODE 未定义,则 _tcslen 解析为 strlen;否则解析为 wcslen ,这是 strlen 的宽字符版本。类似的还有下面这些定义,总之前面要加一个 _t 前缀,如果 ANSI 版有 str 前缀则先将 str 转为 wcs 再将 w 替换为 _t

    ANSI 编码模式下的函数Unicode 编码模式下的函数
    _tcslenstrlenwcslen
    _tcscpystrcpywcscpy
    _tprintfprintfwprintf
    _tscanfscanfwscanf

总之为了防止给自己挖坑最好是把程序写成两种编码下都能正常编译的形式。

类型

从前面的 Unicode 与 ANSI 相关数据类型可以看出,微软为了兼容性不会使用 C/C++ 原生的数据类型,而是通过 typedef 定义了一些数据类型。

从某种角度说微软定义了这些数据类型就是为了日后修改数据长度时确保兼容性的,因此最好使用微软定义的这些数据类型而不是 C/C++ 原生的数据类型。总之原则就是微软定义的 API 或结构体成员是什么类型那么我们就使用什么样的数据类型赋值和接收输出。

整数类型

数据类型大小签署?
BYTE8 位无符号
DWORD32 位无符号
INT3232 位有符号
INT6464 位有符号
LONG32 位有符号
LONGLONG64 位有符号
UINT3232 位无符号
UINT6464 位无符号
ULONG 32 位无符号
ULONGLONG64 位无符号
WORD16 位无符号

布尔类型

BOOLint 的类型,不同于 C++ 的 bool。(所以千万不要混用

#define FALSE    0 
#define TRUE     1

尽管定义为 TRUE,但大多数返回 BOOL 类型的函数都可以返回任何非零值来指示布尔值。 因此,应始终编写:

// Right way.
if (SomeFunctionThatReturnsBoolean()) { 
    ...
}

// or
if (SomeFunctionThatReturnsBoolean() != FALSE) { 
    ...
}

而不是

// Wrong!
if (result == TRUE) {
    ... 
}

指针类型

Windows 定义的指针类型名称中常有前缀 PLP 。因此下面的变量声明是等效的。

RECT*  rect;  // Pointer to a RECT structure.
LPRECT rect;  // The same
PRECT  rect;  // Also the same.

PLP 的起源:
在 16 位体系结构 (16 位 Windows) 有 2 种类型的指针, P 表示“指针”, LP 代表“长指针”。 长指针(也称为远指针)用于处理当前段以外的内存范围。 LP 前缀已保留,以便更轻松地将 16 位代码移植到 32 位 Windows。 今天没有区别,这些指针类型都是等效的。

另外 PCLPC 前缀表示常量指针。

指针精度类型

以下数据类型始终是指针的大小,即 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 用于检索调用线程最后错误代码值,因此多个线程不会覆盖彼此的最后错误代码。

_Post_equals_last_error_ DWORD GetLastError();

得到错误码后我们有如下几个方法得到对应的错误信息:

  • Visual Studio 的 工具->错误查找 可以通过错误码检索错误信息。

  • 在调试的时候可以在监视窗口添加<存取错误码的变量>,hr就可以再“值”那一栏看到错误信息,而输入 @err,hr 可以随时查看当前错误码和错误信息。

  • VC 6.0 的错误查找方式:监视窗口 *(unsigned long*)(tib + 0x34),hr

  • FormatMessage 函数可以将错误码转换为错误信息,为了方便这里直接把文档中的实例代码封装成一个函数,这样每次调用 API 失败的时候就可以调用这个函数显示错误信息。

    void 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 函数不支持格式化字符串,因此我们需要将其进行如下改进:

#ifdef _DEBUG
#include <tchar.h>
#include <stdio.h>
#include <windows.h>
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程序)wWinMainSUBSYSTEM:console
控制台程序(Consolo程序)mainSUBSYSTEM:WINDOWS

注意 SDK 程序不是没有 main 函数,而是 Microsoft C 运行时库提供了调用 WinMainwWinMainmain 实现,而 CRT 在 main 中执行了一些 SDK 相关的初始化工作。

每个 Windows 程序都包含一个名为 WinMainwWinMain 的入口点函数。wWinMain 函数的定义如下:

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

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 结构体)。

    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;// 窗口类名

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 。

    if (RegisterClassEx(&wc) == 0) {
        ShowErrMsg();
        return 0;
    }

创建窗口实例

创建窗口用到了 CreateWindowExW 函数,该函数定义如下:

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:用户定义的参数。它是一个指向用户自定义数据的指针,可以在窗口过程中使用。

在示例程序中我传入的参数如下:

    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 函数定义如下:

BOOL
WINAPI
ShowWindow(
    _In_ HWND hWnd,
    _In_ int nCmdShow);
  • hWnd:窗口句柄,告诉 ShowWindow 显示哪一个窗口。
  • nCmdShow:如何显示这个窗口。
    • SW_MINIMIZE:最小化
    • SW_SHOWNORMAL:普通
    • SW_SHOWMAXIMIZED:最大化

在示例程序中我传入的参数如下:

    ShowWindow(hWnd, SW_SHOWNORMAL);

WinMain 调用完 ShowWindow 后,还需要调用函数 UpdateWindow,最终把窗口显示了出来(在高版本的 SDK 里面这一步已经没有必要了,因为 ShowWindow 做了这件事)。调用函数 UpdateWindow 将产生一个 WM_PAINT 消息,这个消息将使窗口重画,即使窗口得到更新,且不通过消息循环。

另外如果想修改窗口的属性可以使用 SetClassLongPtr 来修改。SetClassLongPtrW 函数定义如下:

WINUSERAPI
ULONG_PTR
WINAPI
SetClassLongPtrW(
    _In_ HWND hWnd,
    _In_ int nIndex,
    _In_ LONG_PTR dwNewLong);
  • hWnd:窗口句柄。
  • nIndex:要替换的属性。
  • dwNewLong:属性的被替换成的值。

示例程序中在调用完 ShowWindow 后再调用 SetClassLongPtr 修改了光标的类型。

SetClassLongPtr(hWnd, GCLP_HCURSOR, (LONG) LoadCursor(NULL, IDC_CROSS));

创建消息循环

Windows 为每个正在运行的应用程序都保持一个消息队列。当你按下鼠标或者键盘时,Windows 并不是把这个输入事件直接送给应用程序,而是将输入的事件先翻译成一个消息,然后把这个消息放入到这个应用程序的消息队列中去。

在消息循环中用到了消息结构体 tagMSG,操作系统将消息封装成 MSG 结构体投递到消息队列。

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 就把这些消息分配给相应的窗口函数以便处理它们,这段代码是一段循环代码,故称为”消息循环”。

在这里插入图片描述
示例代码中的消息循环实现如下:

    MSG msg;
    while (BOOL bRet = GetMessage(&msg, NULL, 0, 0)) {
        if (bRet == -1) {
            ShowErrMsg();
            break;
        }
        TranslateMessage(&msg); // 翻译消息
        DispatchMessage(&msg); // 派发消息
    }
    return msg.wParam;

其中 GetMessageW 定义如下:

BOOL
WINAPI
GetMessageW(
    _Out_ LPMSG lpMsg,
    _In_opt_ HWND hWnd,
    _In_ UINT wMsgFilterMin,
    _In_ UINT wMsgFilterMax);
  • lpMsg:接收消息的 MSG 结构的地址。
  • hWnd:窗口句柄,NULL 则表示要获取该应用程序创建的所有窗口的消息。
  • wMsgFilterMin:最小消息过滤值。它是一个无符号整数,用于指定获取消息的最小消息值。只有消息的值大于等于 wMsgFilterMin 的消息才会被获取。
  • wMsgFilterMax:最大消息过滤值。它是一个无符号整数,用于指定获取消息的最大消息值。只有消息的值小于等于 wMsgFilterMax 的消息才会被获取。
  • 如果 wMsgFilterMinwMsgFilterMax 同时为 0 则过滤无效。
  • 返回值:
    • 在接收到除 WM_QUIT 之外的任何一个消息后,GetMessage() 都返回 TRUE
    • 如果 GetMessage 收到一个 WM_QUIT 消息,则返回 FALSE
    • 如果出现错误则返回 -1 。

TranslateMessage 函数用于将虚拟键消息转换为字符消息。该函数会解析 lpMsg 所指向的消息,并根据其中的虚拟键码和键盘状态信息,生成相应的字符消息。生成的字符消息会被插入到线程的消息队列中,并可以通过后续的调用 GetMessage 函数来获取。

DispatchMessage: 函数用于将消息分派给窗口过程进行处理。当获取到一个消息后,通常需要将其传递给相应的窗口过程函数来进行处理。

实现窗口过程函数

窗口的回调函数在处理完消息后还可以把消息的处理结果放入消息队列。
请添加图片描述
示例代码中窗口回调函数实现如下:

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 消息以确保资源正常释放。

示例程序

#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 针对不同的消息调用对应的消息处理函数。

因此一般一个窗口对应的回调函数为下面这种形式:

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)

在窗口创建的时候会发送该消息,通常我们会将一些该窗口初始化相关的代码写在对应的处理函数中。例如下面这个代码将热键的注册写到了窗口创建的处理函数中,这样一旦该窗口创建则相关的热键就会生效。

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)

点击窗口的关闭按钮的时候会发送该消息。注意此时窗口相关资源还没有释放,因此最好不要在此时结束进程。另外关闭窗口是操作系统的工作,因此这个消息必须交给系统默认的处理函数。

LRESULT CALLBACK OnClose(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    DebugPrintf(_T("[sky123] OnClose %x\n"), WORD(uMsg));
    return FALSE; // 返回 FALSE 表示这个消息没有处理,需要调用系统默认的处理函数。
}

实际上关闭窗口也可以通过 DestroyWindow 这一 API 来完成。

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 消息。

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)

窗口(具体来说是窗口左上角)移动的时候会发送该消息,我们可以通过参数获取窗口移动后的坐标,具体可以查阅文档。

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)

可以获取左键按下时光标的坐标,具体可查阅文档。

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)

可以获取左键抬起时光标的坐标,具体可查阅文档。

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)

可以获取鼠标移动时光标的坐标,具体可查阅文档。

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)

可以获取左键双击时光标的坐标。

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;
}

注意:

  • 只有两次点击鼠标左键的时间间隔在一定范围内才算是左键双击。

  • 左键双击的消息是替换了第二次鼠标左键按下的消息。

  • 需要在 WNDCLASSEstyle 中添加 CS_DBLCLKS 属性鼠标左键双击的消息才能有效。

键盘消息

键盘按下(WM_KEYDOWN)

wParam 为虚拟键码,不过要想转换为具体字符需要借助 ToAscii 函数。该函数定义如下:

WINUSERAPI
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 即可,即标准转换。

因此可以采用如下方式获取按键输入的具体字符:

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 相似,因此可以采用如下方式获取按键输入的具体字符:

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 码。

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_KEYDOWNWM_CHAR 。当然,这个 API 的作用不止转换键盘输入,还会参与其它消息的转换。

热键(WM_HOTKEY)

当窗口注册的热键被按下的时候会发送 WM_HOTKEY 消息到对应窗口。

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,该函数定义如下:

WINUSERAPI
BOOL
WINAPI
RegisterHotKey(
    _In_opt_ HWND hWnd,
    _In_ int id,
    _In_ UINT fsModifiers,
    _In_ UINT vk);
  • hWnd:可选参数,指定接收热键消息的窗口句柄。如果为 NULL,则热键消息将被发送到调用线程的消息队列中。

  • id:标识热键的 ID。每个热键都需要一个唯一的 ID 来标识。

  • fsModifiers:指定热键的修饰键。可以是以下值之一,或者它们的组合:

    • MOD_ALTAlt 键。
    • MOD_CONTROLCtrl 键。
    • MOD_SHIFTShift 键。
    • MOD_WINWindows 键。
  • uVirtKey:指定热键的虚拟键码。这是要注册的热键的键码值。

  • 返回值:函数返回一个 BOOL 类型的值,表示注册热键的成功与否。如果注册成功,返回值为非零;否则,返回值为零。

例如示例代码中注册了一个 Ctrl+F1 的热键:

RegisterHotKey(hwnd, 5566, MOD_CONTROL, VK_F1);

如果我们用不到该热键的时候可以调用 UnregisterHotKey 来销毁该热键,UnregisterHotKey函数定义如下:

WINUSERAPI
BOOL
WINAPI
UnregisterHotKey(
    _In_opt_ HWND hWnd,
    _In_ int id);
  • hWnd:可选参数,指定先前注册热键时所使用的窗口句柄。如果该窗口句柄与注册时不匹配,或者为 NULL,则取消注册所有匹配指定 ID 的热键。

  • id:指定要取消注册的热键的 ID。

  • 返回值:函数返回一个 BOOL 类型的值,表示取消注册热键的成功与否。如果取消注册成功,返回值为非零;否则,返回值为零。

在示例代码中我们在销毁窗口的时候调用该函数取消注册该窗口在创建时注册的热键。

UnregisterHotKey(hwnd, 5566);

示例程序

#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;
}

消息的发送

消息的发送有 SendMessagePostMessage 两个 API ,它们的主要区别在于消息的同步性和返回值的处理。

  • SendMessage
    • SendMessage 是一个同步的函数,即在消息发送之后,它会等待消息处理完毕,并且返回值是消息处理函数的返回值。
    • 调用 SendMessage 会阻塞当前线程,调用窗口过程函数,直到消息被处理完毕,然后才会继续执行后续代码。
    • 返回值:SendMessage 的返回值通常由消息处理函数返回,可以根据具体的消息类型和上下文来确定返回值的含义。
  • PostMessage
    • PostMessage 是一个异步的函数,即在消息发送之后,它会立即返回,并不等待消息处理完毕。
    • 调用 PostMessage 不会阻塞当前线程,而是将消息放入消息队列中,然后立即返回,允许当前线程继续执行后续代码。
    • 返回值:PostMessage 函数没有返回值。

由于窗口句柄是全局的,因此操纵自己的窗口和操纵其他进程的窗口是没有区别的。因此我们只要获取到其他进程的窗口句柄就可以向该窗口发消息,从而操作该进程的窗口。

获取窗口句柄的函数为 FindWindow,该函数定义如下:

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 函数可以完成该功能。

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 因为消息队列接收消息后还有额外的处理) 向窗口发送键盘输入消息即可。

完整代码如下:

#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 来创建。该函数定义如下:

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 函数创建定时器。

SetTimer(hwnd, 1, 10, NULL);

OnDestroy 函数中需要调用 KillTimer 来销毁定时器。

KillTimer(hwnd, 1);

定时器会以一定的时间间隔向队列里面发送 WM_TIMER 消息,因此我们可以在 OnTimer 中写需要定时执行的代码。例如定时在桌面上打印字符串:

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;
}

完整代码如下:

#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 类型。

#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):

WINUSERAPI
HDC
WINAPI
GetDC(
    _In_opt_ HWND hWnd);
  • hWnd:要检索其 DC 的窗口的句柄。 如果此值为 NULL ,则 GetDC 将检索整个屏幕的 DC。

  • 返回值:如果函数成功,则返回值是指定窗口工作区的 DC 的句柄。如果函数失败,则返回值为 NULL

注意,当 GetDC 获取一个 DC 的同时系统会为 DC 申请相关的资源,因此在使用完 DC 后需要调用 ReleaseDC 函数将 DC 释放。该函数定义如下:

WINUSERAPI
int
WINAPI
ReleaseDC(
    _In_opt_ HWND hWnd,
    _In_ HDC hDC);
  • hWnd:要释放其 DC 的窗口的句柄。

  • hDC:要释放的 DC 的句柄。

  • 返回值:返回值指示是否释放了 DC。 如果释放 DC,则返回值为 1。如果未释放 DC,则返回值为 0。

绘制文本

Notepad 需要显示输入内容,也就是绘制文本。绘制文本相关的 API 有 DrawTextTextOut

TextOut 定义如下:

 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 实现如下:

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 定义如下:

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 实现如下:

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;
}

其中 \bBackspace 键,这里对应为模拟字符删除操作。

虽然上述实现虽然解决了 TextOut 存在的问题,但是在删除字符后发现已经删除的字符还会显示出来。这是因为 DrawText 只会将字符串打印在给定的区域,而字符串覆盖不到的区域会保持原样。因此我们在 DrawText 显示字符串之前还要先将窗口刷成背景色。因此有如下改进代码。这里要注意刷子在使用完之后需要还原并释放刷子。

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 函数,该函数定义如下:

WINUSERAPI
BOOL
WINAPI
CreateCaret(
    _In_ HWND hWnd,
    _In_opt_ HBITMAP hBitmap,
    _In_ int nWidth,
    _In_ int nHeight);
  • hWnd:指定插入符要创建的窗口句柄。插入符将与该窗口关联。
  • hBitmap:可选参数,指定插入符的位图句柄。如果为 NULL,则插入符将以系统默认样式显示。
  • nWidth:指定插入符的宽度(以像素为单位)。
  • nHeight:指定插入符的高度(以像素为单位)。
  • 返回值:函数返回一个 BOOL 类型的值,表示创建插入符的成功与否。如果创建成功,返回值为非零;否则,返回值为零。

由于要计算插入符的高度以及位置,因此需要在 OnCreate 函数中获取字体相关信息。

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 创建插入符,并且设置插入符的位置并显示:

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 函数销毁插入符。

LRESULT CALLBACK OnKillFocus(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    DebugPrintf(_T("[sky123] OnKillFocus %x\n"), WORD(uMsg));
    
    DestroyCaret();

    return TRUE;
}

另外在 OnChar 函数中每写入一个字符的时候都要重新计算插入符的位置。这里需要注意显示插入符必须在 FillRect 重绘背景之前。因为插入符一直在闪烁也就是一直在重绘,因此改变插入符位置会在插入符上一个所在位置上留下“残影”。

    ShowCaret(hwnd);
    SetCaretPos(g_tm.tmAveCharWidth * g_Text.size(), 0);

无效区域

当窗口最小化最大化之后,窗口中的文字会消失。这是因为 Windows 重绘窗口把原本的文字覆盖了。

Windows 重绘窗口涉及到两个消息:

  • WM_ERASEBKGND:消息通常在窗口需要重绘背景时发送给窗口。它用于擦除窗口的背景,并为重绘做准备。
  • WM_PAINT:消息在窗口需要绘制或重新绘制时发送给窗口。

当页面发生改变的时候系统会依次向消息队列中发送 WM_ERASEBKGNDWM_PAINT 两个消息。因此我们可以在接收到 WM_PAINT 消息的时候调用将文字重新显示在窗口中。

然而这样做的话会有一个问题,系统会不停的发送 WM_ERASEBKGNDWM_PAINT 消息导致窗口很卡,为了解决这一问题,这里引入了一个“无效区域”的概念。

“无效区域”(Invalid Region)是指在窗口或设备上需要重新绘制的区域。当窗口或设备的内容发生变化时,无效区域表示需要更新的部分,而不是整个窗口或设备。而与之相对应的有效区域是指窗口中没有变化的部分,这一部分不需要重新绘制。

而我们在接收到 WM_PAINT 消息的时候调用将文字重新显示在窗口中时,没有把显示文字的部分设为有效区域,系统发现这一部分还是无效区域,就继续向消息队列中发送 WM_ERASEBKGNDWM_PAINT 两个消息。

因此在重新显示文字后需要使用 ValidateRect 函数将窗口设为有效区域。

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 会自动将无效区域设为有效区域。

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 函数中显示文字。

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 + 快捷键 打开菜单的对应项,不过需要逐级展开。

    // 弹出菜单
    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 有如下实现:

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

    // 申请堆地址空间
    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;
    }

并且在消息循环中要将键盘消息转换为快捷键消息。

TranslateAccelerator(hWnd, hAccel, &msg)

在程序结束的时候要销毁快捷键。

    // 删除快捷键表
    DestroyAcceleratorTable(hAccel);
    HeapFree(GetProcessHeap(), 0, pAccelNews);

OnCommand 函数根据消息参数确定消息的来源并分别处理。

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)

#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 消息,而具体的消息由 wParamlParam 决定。由于快捷键和菜单也使用 WM_COMMAND 消息,因此有如下规则区分和处理 WM_COMMAND 消息:

  • 如果 wParam 的高 4 字节为 1 则消息来自快捷键,wParam 低 4 字节表示快捷键消息。
  • 如果 wParam 的高 4 字节为 0 则消息来自菜单,wParam 低 4 字节表示快菜单消息。
  • 如果 lParam 不为 NULL 则消息来自控件且 lParam 表示控件句柄。另外 wParam 低 4 字节表示控件 ID ,高 4 字节表示控件消息。

因此处理 WM_COMMAND 消息的 OnCommand 函数有如下实现:

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 类型的控件。

    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:窗口或控件的样式。
  • xy:窗口或控件的初始位置。
  • nWidthnHeight:窗口或控件的初始大小。
  • hWndParent:父窗口的句柄,控件的很多消息会发送至父窗口。
  • hMenu:窗口或控件的菜单句柄,在父窗口的过程函数中处理控件消息会根据这个来确定是哪个控件。
  • hInstance:应用程序实例句柄。
  • lpParam:用户指定的参数。
  • 返回值:如果函数调用成功,将返回新创建窗口的句柄。如果函数调用失败,将返回 NULL。

由于 IDB_BUTTON1 是选择按钮,因此需要根据先 SendMessage 发送 BM_GETCHECK 消息到控件来获取按钮是否已被选择,然后根据按钮状态通过 SendMessage 更新按钮状态。

注意由于接收的消息为 BN 前缀因此这段代码应当写到控件的父窗口的过程函数调用的 OnCommand 函数中。

        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 对应的控件。

    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_SETTEXTWM_PASTE 消息来清空编辑框和粘贴内容。

            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 属性并且在处理消息的时候两种消息都要处理。

    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

            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);
                }
            }

示例程序

#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)中常见的两种对话框类型,它们在交互方式和应用程序控制方面有所不同。

  • 模态对话框:
    • 模态对话框是指打开后,用户必须完成对话框上的操作,或关闭对话框才能继续与应用程序的其他部分进行交互。
    • 模态对话框会阻塞应用程序的其他窗口,用户无法与应用程序的其他部分进行交互,直到对话框被关闭。
    • 模态对话框通常用于需要用户输入或确认的关键操作,例如文件保存、选项设置等。
    • 通常,模态对话框使用函数如 DialogBoxDoModal 来创建和显示。
      int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, TCHAR *lpCmdLine, int nCmdShow) {
      
          // 创建模态对话框
          INT_PTR nExitCode = DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DialogProc);
      
          return nExitCode;
      }
      
  • 非模态对话框
    • 非模态对话框是指打开后,用户可以同时与对话框和应用程序的其他部分进行交互。
    • 非模态对话框不会阻塞应用程序的其他窗口,用户可以在对话框打开的同时执行其他操作。
    • 非模态对话框通常用于提供辅助功能或快捷操作,例如工具选项、即时预览等。
    • 通常,非模态对话框使用函数如 CreateDialogCreateDialogIndirect 来创建和显示,并通过消息循环处理用户的输入。
      int 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 函数。
            case WM_CLOSE:
                DebugPrintf(_T("[sky123] WM_CLOSE"));
                EndDialog(hwndDlg, 0);
                break;
    
  • 对于非模态对话框,需要再收到 WM_CLOSE 消息时调用 DestroyWindow 函数,并且在收到 WM_DESTROY 消息时调用 PostQuitMessage 函数。
            case WM_CLOSE:
                DebugPrintf(_T("[sky123] WM_CLOSE"));
                DestroyWindow(hwndDlg);
                break;
            case WM_DESTROY:
                DebugPrintf(_T("[sky123] WM_DESTROY"));
                PostQuitMessage(0);
                break;
    
控件使用举例(树控件)
                    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 加载快捷键资源。

    HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1));

之后就可以在消息循环中调用 TranslateAccelerator 函数转换快捷键消息,并且在窗口过程函数中处理快捷键消息。

菜单

菜单资源的创建

在这里插入图片描述
菜单可以多级展开,&[快捷键] 可以设置快捷键。另外为了美观菜单栏遵循如下格式 [菜单名](&[菜单快捷键])\t快捷键 。在属性栏可以设置菜单的 ID 号(为了方便可以设置的和快捷键一样)。

菜单资源的使用

通过 LoadMenu 函数加载菜单资源,然后 SetMenu 函数将菜单资源应用到对话框中。

    HMENU hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_MENU1));
    SetMenu(hDlg, hMenu);

字符串表

字符串表可以用来统一管理字符串,这样就可以很方便的进行字符串的修改。

字符串表资源的创建

设置好 ID 和标题即可。
在这里插入图片描述

字符串表资源的使用

LoadString 根据字符串的 ID 加载对应字符串即可。

                    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);
                    }

光标

光标资源的创建

在这里插入图片描述

光标资源的使用
    HCURSOR hCursor = LoadCursor(hInstance, MAKEINTRESOURCE(IDC_CURSOR1));
    SetCursor(hCursor);
    SetClassLongPtr(hDlg, GCLP_HCURSOR, (LONG) hCursor);

版本

版本资源用来描述程序的相关信息。
在这里插入图片描述
版本资源不需要显示加载,编译成可执行文件后版本资源会在可执行文件的详细信息中体现。
在这里插入图片描述

位图

位图资源的创建

在添加资源窗口选择导入,然后导入图片即可。注意图片应当是 BMP 格式。
在这里插入图片描述

位图资源的使用

在对话框中添加 Picture Control 控件,然后设置类型为 Bitmap 并且选择加载的 Bitmap 资源。
在这里插入图片描述

SDK 项目封装

示例项目

这里通过实现一个俄罗斯方块游戏来讲解 SDK 项目封装。这里将整个游戏逻辑拆分出下面这几个类,实现界面和游戏逻辑分离。

  • CApplication:负责程序初始化,消息循环,退出。
  • CWindow:负责消息处理,API 的调用。
  • CMainWindow:继承于 CWindow ,与主窗口消息处理有关,根据消息调用 CGameView 中的函数完成游戏视图的更新。
  • CGameView:游戏视图,根据 CMainWindow 调用相应的 CGame 方法并且根据 CGame 的反馈更新游戏视图。
  • CGame:实现游戏逻辑,将 CGameView 的函数调用作为输入,模拟游戏过程并反馈结果。

各个类之间的关系如下图所示:
在这里插入图片描述

CApplication

CApplication 类的构成如下,主要是创建窗口和消息循环。

class CApplication {
public:
    CApplication(HINSTANCE hInstance) : m_hInstance(hInstance) {}
    
    CApplication() : m_hInstance(GetModuleHandle(NULL)){};
    
    ~CApplication();
    
    virtual BOOL InitInstance(int CmdShow);
    
    INT MessageLoop();

private:
    HINSTANCE m_hInstance{};
    WCHAR m_szTitle[MAX_LOADSTRING]{};      // 标题栏文本
    WCHAR m_szWindowClass[MAX_LOADSTRING]{};// 主窗口类名
    HACCEL m_hAccelTable{};
    static CWindow *m_pMainWindow;
   
    static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
};

这里要注意 m_pMainWindow 初始化的时机,m_pMainWindow 的初始化必须在窗口创建和窗口过程函数接收第一个消息之间。

LRESULT CALLBACK CApplication::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    if (m_pMainWindow == nullptr) {
        m_pMainWindow = new CMainWindow(hWnd);
    }
    LRESULT lResult = FALSE;
    switch (message) {
        case WM_CREATE:
            lResult = m_pMainWindow->OnCreate(wParam, lParam);
            break;
        ...
    }

    if (!lResult) {
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return lResult;
}

CWindow

CMainWindow 的基类,封装了一些消息处理函数。

class CWindow {
public:
    CWindow(HWND hWnd) : m_hWnd(hWnd) {}

    virtual BOOL OnCreate(WPARAM wParam, LPARAM lParam) { return FALSE; }

    virtual BOOL OnDestroy(WPARAM wParam, LPARAM lParam) {
        PostQuitMessage(0);
        return FALSE;
    }

    virtual BOOL OnClose(WPARAM wParam, LPARAM lParam) { return FALSE; }

    virtual BOOL OnKeydown(WPARAM wParam, LPARAM lParam) { return FALSE; }

    virtual BOOL OnKeyup(WPARAM wParam, LPARAM lParam) { return FALSE; }

    virtual BOOL OnTimer(WPARAM wParam, LPARAM lParam) { return FALSE; }

    virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) { return FALSE; }

    virtual BOOL OnPaint(WPARAM wParam, LPARAM lParam) { return FALSE; }

protected:
    HWND m_hWnd;
};

CMainWindow

重写了 CWindow 中的消息处理函数。

class CMainWindow : public CWindow {
public:
    CMainWindow(HWND hWnd);

    ~CMainWindow();

    BOOL OnCommand(WPARAM wParam, LPARAM lParam) override;

    BOOL OnPaint(WPARAM wParam, LPARAM lParam) override;

    BOOL OnCreate(WPARAM wParam, LPARAM lParam) override;

    BOOL OnDestroy(WPARAM wParam, LPARAM lParam) override;

    BOOL OnTimer(WPARAM wParam, LPARAM lParam) override;

    BOOL OnKeydown(WPARAM wParam, LPARAM lParam) override;

private:
    static INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam);
    CGameView *m_pGameView;
};

重写的过程处理函数调用 CGameView 中的相关函数来更新游戏视图。整个游戏过程由用户按键操作和定时器决定。如果视图发生变化会调用 InvalidateRect 函数将界面设为无效区域。

BOOL CMainWindow::OnPaint(WPARAM wParam, LPARAM lParam) {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(m_hWnd, &ps);
        m_pGameView->OnDraw(hdc);
        EndPaint(m_hWnd, &ps);
        return TRUE;
}

BOOL CMainWindow::OnCreate(WPARAM wParam, LPARAM lParam) {
        SetTimer(m_hWnd, IDT_DROP, DROP_TIME, NULL);
        return TRUE;
}

BOOL CMainWindow::OnDestroy(WPARAM wParam, LPARAM lParam) {
        KillTimer(m_hWnd, IDT_DROP);
        PostQuitMessage(0);
        return TRUE;
}


BOOL CMainWindow::OnTimer(WPARAM wParam, LPARAM lParam) {
        if (m_pGameView->OnDown()) {
            InvalidateRect(m_hWnd, NULL, TRUE);
        }
        return TRUE;
}

BOOL CMainWindow::OnKeydown(WPARAM wParam, LPARAM lParam) {
        BOOL Ret = FALSE;
        switch (wParam) {
            case VK_UP:
                Ret = m_pGameView->OnUp();
                break;
            case VK_DOWN:
                Ret = m_pGameView->OnDownFix();
                break;
            case VK_LEFT:
                Ret = m_pGameView->OnLeft();
                break;
            case VK_RIGHT:
                Ret = m_pGameView->OnRight();
                break;
        }
        if (Ret) {
            InvalidateRect(m_hWnd, NULL, TRUE);
        }
        return TRUE;
}

CGameView

核心逻辑是 OnDraw 根据 CGame 中的游戏数据更新游戏视图。

VOID CGameView::OnDraw(HDC hdc) {
    HDC hBitmapDC = CreateCompatibleDC(hdc);
    SelectObject(hBitmapDC, g_hBitmap);

    Block &block = m_pGame->g_Blocks[m_pGame->m_Type];

    for (int i = 0; i <= BOARD_WIDTH + 1; i++) {
        for (int j = 0; j <= BOARD_HEIGHT; j++) {
            if (m_pGame->m_Board[i][j] != -1) {
                BitBlt(hdc,
                       i * m_CubeSize,
                       j * m_CubeSize,
                       m_CubeSize, m_CubeSize,
                       hBitmapDC,
                       0, m_CubeSize * m_pGame->m_Board[i][j],
                       SRCCOPY);
            }
        }
    }

    for (int i = 0; i < BLOCK_SIZE; i++) {
        for (int j = 0; j < BLOCK_SIZE; j++) {
            if (block.m_Data[i][j]) {
                BitBlt(hdc,
                       (m_pGame->m_x + i) * m_CubeSize,
                       (m_pGame->m_y + j) * m_CubeSize,
                       m_CubeSize, m_CubeSize,
                       hBitmapDC,
                       0, m_CubeSize * m_pGame->m_Color,
                       SRCCOPY);
            }
        }
    }

    TCHAR szBuf[MAXBYTE];
#ifdef UNICODE
    swprintf_s(szBuf, sizeof(szBuf) / sizeof(TCHAR),_T("当前得分:%d"), m_pGame->TotalScore);
#else
    sprintf_s(szBuf, sizeof(szBuf), _T("当前得分:%d"), m_pGame->TotalScore);
#endif
    TextOut(hdc, m_CubeSize * (BOARD_WIDTH + 4), m_CubeSize * BOARD_HEIGHT / 2, szBuf, _tcslen(szBuf));

    memset(szBuf, 0, sizeof(szBuf));
#ifdef UNICODE
    swprintf_s(szBuf, sizeof(szBuf) / sizeof(TCHAR), _T("最大得分:%d"), m_pGame->MaxScore);
#else
    sprintf_s(szBuf, sizeof(szBuf), _T("最大得分:%d"), m_pGame->TotalScore);
#endif
    TextOut(hdc, m_CubeSize * (BOARD_WIDTH + 4), m_CubeSize * (BOARD_HEIGHT / 2 + 2), szBuf, _tcslen(szBuf));
    

    DeleteDC(hBitmapDC);
}

其余操作都是调用 OnDraw 相关函数更新游戏数据。

BOOL CGameView::OnDown() {
    return m_pGame->MoveDown();
}

BOOL CGameView::OnUp() {
    return m_pGame->MoveRotate();
}

BOOL CGameView::OnLeft() {
    return m_pGame->MoveLeft();
}

BOOL CGameView::OnRight() {
    return m_pGame->MoveRight();
}

BOOL CGameView::OnDownFix() {
    return m_pGame->MoveDownFix();
}

CGame

游戏的核心逻辑和关键数据。

  • m_Board:已经固定的方块
  • g_Blocks:可选的方块形状
  • m_x, m_y:当前下落的方块的坐标
  • m_Color:当前下落的方块的颜色
  • m_Type:当前下落的是哪种形状的方块
#define BOARD_HEIGHT 25
#define BOARD_WIDTH  15
#define BLOCK_SIZE    4

struct Block {
    INT8 m_Data[BLOCK_SIZE][BLOCK_SIZE];
};

class CGame {
public:
    INT TotalScore,MaxScore;
    UINT8 m_Type, m_Color;
    INT8 m_Board[BOARD_WIDTH + 2][BOARD_HEIGHT + 1];
    static Block g_Blocks[];
    INT m_x, m_y;

    CGame();
    
    BOOL MoveDown();
    
    BOOL MoveLeft();
    
    BOOL MoveRight();
    
    BOOL MoveRotate();
    
    BOOL MoveDownFix();

private:
    std::mt19937_64 rnd;
   
    void Init();
    
    VOID CreateNewBlock();
    
    VOID FixBlock();
    
    BOOL IsCanMove();
    
    INT ReleaseLine();
    
    BOOL IsFullLine(INT nRow);
    
    void RotateBlock(Block &OrigBlock, UINT8 Times = 1);
    
    void ReverseBlock(Block &OrigBlock);
};

效果图

在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
使用C++代码封装的win32操作类, 与MFC相似,对于学习SDKC++是巨好的参考 Tutorials Menu of tutorials Tutorial 1: The Simplest Window Tutorial 2: Using Classes and Inheritance Tutorial 3: Using Messages to Create a Scribble Window Tutorial 4: Repainting the Window Tutorial 5: Wrapping a Frame around our Scribble Window Tutorial 6: Customising Window Creation Tutorial 7: Customising the Toolbar Tutorial 8: Loading and Saving Files Tutorial 9: Printing Tutorial 10: Finishing Touches Tutorial 1: The Simplest Window The following code uses Win32++ to create a window. This is all the code you need (in combination with Win32++) to create and display a simple window. Note that in order to add the Win32++ code to our program, we use an #include statement as shown below. #include "../Win32++/Wincore.h" INT WINAPI WinMain(HINSTANCE, HINSTANCE, LPTSTR, int) { //Start Win32++ CWinApp MyApp; //Create a CWnd object CWnd MyWindow; //Create (and display) the window MyWindow.Create(); //Run the application return MyApp.Run(); } This program has four key steps: Start Win32++. We do this here by creating a CWinApp object called MyApp. Create a CWnd object called MyWindow. Create a default window by calling the Create function. Start the message loop, by calling the Run function. If you compile and run this program, you'll find that the application doesn't end when the window is closed. This is behaviour is normal. An illustration of how to use messages to control the windows behaviour (including closing the application) will be left until tutorial 3.
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_sky123_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值