Window 平台开发基础(一)SDK

基本概念

什么是 SDK

SDK 是软件开发工具包(Software Development Kit)的缩写。它是一个集成了软件开发所需工具、库文件、示例代码和文档等资源的软件包。

SDK 通常由软件开发公司或平台提供,旨在帮助开发人员构建、测试和部署特定类型的应用程序、库或服务。它提供了一套开发工具,使开发人员能够利用平台或框架的功能和特性来创建软件。

SDK 可能包含以下组件:

内核对象的数据结构仅能够从内核模式访问,所以直接在内存中定位这些数据结构对应用程序来说是不可能的,只能通过 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 键控到调试信息。然而 OutputDebugString 函数不支持格式化字符串,因此我们需要将其进行如下改进:

#ifdef _DEBUG
void DebugPrintf(LPCTSTR format, ...) {TCHAR szBuf[MAXBYTE];va_list args;va_start(args, format);
#ifdef UNICODEvswprintf_s(szBuf, sizeof(szBuf) / sizeof(TCHAR), format, args);
#elsevsprintf_s(szBuf, sizeof(szBuf), format, args);
#endifva_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(实际上是模块的加载基址)。
  • 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 的时候要注意活动子集要选择 “(整个集合)” ,然后再索引栏查找。
在这里插入图片描述

窗口创建的主要步骤

窗口创建主要分为 6 个步骤:

  • 设计注册窗口类

  • 创建窗口实例

  • 显示窗口

  • 更新窗口

  • 消息循环

  • 实现窗口过程函数(窗口回调函数)

    设计注册窗口类

  • 窗口种类是定义窗口属性的模板,这些属性包括窗口式样,鼠标形状,菜单等等。

  • 窗口种类也指定处理该类中所有窗口消息的窗口函数。只有先建立窗口种类,才能根据窗口种类来创建 Windows 应用程序的一个或多个窗口。创建窗口时,还可以指定窗口独有的附加特性。

  • 窗口种类简称窗口类,窗口类不能重名,且窗口类名,是操作系统识别窗口类的唯一标识符,在建立窗口类后,必须向 Windows 登记(注册窗口类)。

    • 注意:不能注册相同名字的窗口类。根据窗口类名字来确定是否已经注册过,如果注册过,则注册失败。

      设计自己的窗口类

      操作系统中预定义了很多窗口类。我们要使用的时候可以直接调用,如果要用自己的窗口,就要设计自己所需要的窗口类,设计完成后,通过 RegisterClass 注册自己设计的窗口类载入到操作系统中。

RegisterClass 用到了 WNDCLASS 结构体(RegisterClassEx 用到了 WNDCLASSEX 结构体)。

    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 。

    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 _MACDWORD       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 windowUINT uMsg,    // message identifierWPARAM wParam,// first message parameterLPARAM 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 windowUINT uMsg,    // message identifierWPARAM wParam,// first message parameterLPARAM 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;
}

注意:

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

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 UNICODEvswprintf_s(szBuf, sizeof(szBuf) / sizeof(TCHAR), format, args);
#elsevsprintf_s(szBuf, sizeof(szBuf), format, args);
#endifva_end(args);OutputDebugString(szBuf);
}
#else
#define DebugPrintf
#endifLRESULT 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 会直接将调用窗口过程函数,而 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 UNICODEvswprintf_s(szBuf, sizeof(szBuf) / sizeof(TCHAR), format, args);
#elsevsprintf_s(szBuf, sizeof(szBuf), format, args);
#endifva_end(args);OutputDebugString(szBuf);
}
#else
#define DebugPrintf
#endifLRESULT 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
#endifstd::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);}// 获取 DCHDC 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);// 释放 DCReleaseDC(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));// 获取 DCHDC 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);// 释放 DCReleaseDC(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));// 获取 DCPAINTSTRUCT 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 类型。

    // 弹出菜单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;}

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

    // 删除快捷键表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 实际上是非常困难的。

#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 UNICODEvswprintf_s(szBuf, sizeof(szBuf) / sizeof(TCHAR), format, args);
#elsevsprintf_s(szBuf, sizeof(szBuf), format, args);
#endifva_end(args);OutputDebugString(szBuf);
}
#else
#define DebugPrintf
#endif#ifdef UNICODE
#define tstring wstring
#else
#define tstring string
#endifstd::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));// 获取 DCPAINTSTRUCT 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_COMMANDif (!TranslateAccelerator(hWnd, hAccel, &msg)) {TranslateMessage(&msg);// 翻译消息DispatchMessage(&msg); // 派发消息}}// 删除快捷键表DestroyAcceleratorTable(hAccel);HeapFree(GetProcessHeap(), 0, pAccelNews);return msg.wParam;
}

为了提高开发效率,实际上 Windows 已经预先定义了一些常用的窗口类型。

#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 UNICODEvswprintf_s(szBuf, sizeof(szBuf) / sizeof(TCHAR), format, args);
#elsevsprintf_s(szBuf, sizeof(szBuf), format, args);
#endifva_end(args);OutputDebugString(szBuf);
}
#else
#define DebugPrintf
#endifLRESULT CALLBACK OnCreate(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {DebugPrintf(_T("[sky123] OnCreate %x\n"), WORD(uMsg));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 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);// 默认窗口处理函数
}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;}RECT rc;GetClientRect(hWnd, &rc);HWND hEdit = CreateWindowEx(0,_T("EDIT"),NULL,WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | ES_MULTILINE,0,0,rc.right - rc.left,rc.bottom - rc.top,hWnd,NULL,hInstance,NULL);if (hEdit == NULL) {ShowErrMsg();return 0;}// 显示和更新窗口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;
}

控件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/78868.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IAM、EIAM、CIAM、RAM、IDaaS 都是什么?

后端程序员在做 ToB 产品或者后台系统时&#xff0c;都不可避免的会遇到账号系统、登录系统、权限系统、日志系统等这些核心功能。这些功能一般都是以 SSO 系统、RBAC 权限管理系统等方式命名&#xff0c;但这些系统合起来有一个专有名词&#xff1a;IAM。 IAM IAM 是 Identi…

一线大厂Redis高并发缓存架构实战与性能优化

多级缓存架构 缓存设计 缓存穿透 缓存穿透是指查询一个根本不存在的数据&#xff0c; 缓存层和存储层都不会命中&#xff0c; 通常出于容错的考虑&#xff0c; 如果从存储层查不到数据则不写入缓存层。 缓存穿透将导致不存在的数据每次请求都要到存储层去查询&#xff0c; 失…

9.14 C++作业

仿照vector手动实现自己的myVector&#xff0c;最主要实现二倍扩容功能 #include <iostream>using namespace std;template <typename T> class Myvector {T *data; //存储数据的数组int len; //当前数组的长度int mycapa; //容纳数据的总容量public://…

Ubuntu下Nginx配置ModSecurity详细思路及过程

下面是一个简介&#xff1a; Ubuntu是一个linux操作系统&#xff0c;Nginx是一个web服务器软件&#xff0c;ModSecurity是一款开源的web应用防火墙&#xff08;江湖人称“WAF”&#xff09;。 如果上面的概念没有一定的了解&#xff0c;下面的内容其实也能看。就是不好操作。…

浅谈C++|模板篇

一.模板模板概念 模板就是建立通用的模具&#xff0c;大大提高复用性 模板的特点&#xff1a; 1.模板不可以直接使用&#xff0c;它只是一个框架 2.模板的通用并不是万能的 C另一种编程思想称为泛型编程&#xff0c;主要利用的技术就是模板。 C提供两种模板机制:函数模板和类模…

宋浩概率论笔记(八)假设检验

宋浩系列全系列的最后一更&#xff01; 本章考察频率很低&#xff0c;核心在于记忆检验不同参数时用到的分布~

【项目实战】【已开源】USB2.0 HUB 集线器的制作教程(详细步骤以及电路图解释)

写在前面 本文是一篇关于 USB2.0 HUB 集线器的制作教程&#xff0c;包括详细的步骤以及电路图解释。 本文记录了笔者制作 USB2.0 HUB 集线器的心路历程&#xff0c;希望对你有帮助。 本文以笔记形式呈现&#xff0c;通过搜集互联网多方资料写成&#xff0c;非盈利性质&#xf…

许战海战略文库|品类缩量时代:制造型企业如何跨品类打造份额产品?

所有商业战略的本质是围绕着竞争优势与竞争效率展开的。早期&#xff0c;所有品牌立足于从局部竞争优势出发。因此,品牌创建初期大多立足于单个品类。后期增长受限,就要跨品类持续扩大竞争优势&#xff0c;将局部竞争优势转化为长期竞争优势&#xff0c;如果固化不前很难获得增…

腾讯mini项目-【指标监控服务重构】2023-07-19

今日已办 OpenTelemetry Logs 通过日志记录 API 支持日志收集 集成现有的日志记录库和日志收集工具 Overview 日志记录 API - Logging API&#xff0c;允许您检测应用程序并生成结构化日志旨在与其他 telemerty data&#xff08;例如metric和trace&#xff09;配合使用&am…

Java代码审计16之fastjson反序列化漏洞(1)

文章目录 1、简介fastjson2、fastjson的使用2.1、将类序列化为字符串2.2、将字符串还原为对象2.3、小结以上2.4、稍微扩展思路 3、fastjson漏洞利⽤原理与dnslog4、JdbcRowSetImpl利用链4.1、JdbcRowSetImpl的基本知识4.2、利用代码复现4.3、生成poc4.4、模拟真实场景4.5、利用…

MongoDB差异数据对比的快速指南

MongoDB是一种非关系型数据库&#xff0c;它以灵活的 JSON-like 文档的形式存储数据&#xff0c;这种特性使其在处理大量数据和实现快速开发时更具有优势。而由于其灵活的数据模型和强大的性能&#xff0c;MongoDB 被广泛应用在各种业务场景中。随着业务的发展和数据的增长&…

Excel VLOOKUP 初学者教程:通过示例学习

目录 前言 一、VLOOKUP的用法 二、应用VLOOKUP的步骤 三、VLOOKUP用于近似匹配 四、在同一个表里放置不同的VLOOKUP函数 结论 前言 Vlookup&#xff08;V 代表“垂直”&#xff09;是 excel 中的内置函数&#xff0c;允许在 excel 的不同列之间建立关系。 换句话说&#x…

iPhone苹果15手机怎么看是国行还是美版或港版的苹果iPhone15手机?

iPhone苹果手机15机型区域版本识别代码 CH代码为国行 LL代码为美版 ZP代码为港版 iPhone苹果15手机怎么看是国行还是美版或港版的苹果iPhone15手机&#xff1f; 1、打开苹果iPhone15手机桌面上的「设置」&#xff1b; 2、在iPhone苹果15手机设置内找到「通用」并点击打开&…

【OJ比赛日历】快周末了,不来一场比赛吗? #09.16-09.22 #12场

CompHub[1] 实时聚合多平台的数据类(Kaggle、天池…)和OJ类(Leetcode、牛客…&#xff09;比赛。本账号会推送最新的比赛消息&#xff0c;欢迎关注&#xff01; 以下信息仅供参考&#xff0c;以比赛官网为准 目录 2023-09-16&#xff08;周六&#xff09; #3场比赛2023-09-17…

【FPGA项目】进阶版沙盘演练——报文收发(报文处理、CDC、CRC)

前言 书接上文【FPGA项目】沙盘演练——基础版报文收发_子墨祭的博客-CSDN博客&#xff0c;前面我们做了基础版的报文收发&#xff0c;相信对逻辑设计有了一定的认知&#xff0c;在此基础上&#xff0c;继续完善一个实际报文收发可能会遇到的一些处理&#xff1a; 报文处理握手…

公交查询系统

目录 需求分析 1 概述 2 课题分析 3 实现功能步骤 4 项目背景 概要设计 1 系统流程图. 2 功能模块. 3 各功能模块 4 数据存储 5 类设计 三、详细设计 1公交线路查询系统用户界面 2公交信息存储模快 3公交信息查询模块 4用户信息输入和输出模块 四、调试分析 五、使用说明 六、…

STM32外部复位IC与看门狗冲突,无法复位问题解决方案

使用STM32H743制作了一款飞控&#xff0c;外部复位IC采用MAX809STR,打板完后&#xff0c;烧录飞控固件后大量板子无法正常启动&#xff0c;怀疑是晶振没有起振或MCU未焊接好&#xff0c;检查后均焊接正常&#xff0c;编写裸机LED定时闪烁验证程序可正常运行。经网上查询资料锁定…

Python 环境搭建,集成开发环境IDE: PyCharm

Python 环境搭建,集成开发环境IDE: PyCharm 一、Python 环境搭建二、Python下载三、Python安装四、环境变量配置五、Python 环境变量六、运行Python1、交互式解释器&#xff1a;2、命令行脚本3、集成开发环境&#xff08;IDE&#xff1a;Integrated Development Environment&am…

Hadoop-Hive

1. hive安装部署 2. hive基础 3. hive高级查询 4. Hive函数及性能优化 1.hive安装部署 解压tar -xvf ./apache-hive-3.1.2-bin.tar.gz -C /opt/soft/ 改名mv apache-hive-3.1.2-bin/ hive312 配置环境变量&#xff1a;vim /etc/profile #hive export HIVE_HOME/opt/soft/hive…

软件测试的基本流程是什么?软件测试流程详细介绍

软件测试和软件开发一样&#xff0c;是一个比较复杂的工作过程&#xff0c;如果无章法可循&#xff0c;随意进行测试势必会造成测试工作的混乱。为了使测试工作标准化、规范化&#xff0c;并且快速、高效、高质量地完成测试工作&#xff0c;需要制订完整且具体的测试流程。 01…