如何使用 Win32 和 COM API 在 C++ 中编写桌面程序?2025 年 3 月 25 日 | 99 分钟阅读 在本系列结束时,你将掌握从头开始创建桌面程序的技能,所以让我们开始这个有趣的 C++ 桌面程序创建之旅。 ![]() Win32 编程简介C++ 中的 Win32 编程指的是使用 Win32 API 创建 Windows 应用程序,Win32 API 是 Microsoft Windows 操作系统提供的一组核心函数和结构。以下是一个基本的入门介绍: 设置开发环境如果你有一个支持 C++ 的 IDE(即 Visual Studio)和 Windows SDK,那将会有所帮助。Visual Studio 为 Win32 开发提供了一个全面的环境,包括项目模板、调试工具和许多其他功能。
了解 Win32 APIWin32 API 提供了创建窗口、处理消息、绘制图像和访问系统对象的功能。熟悉文档很重要,可以在 Microsoft Developer Network (MSDN) 网站上找到。 Windows 编码约定大多数 Windows API 由函数或组件对象 模型 (COM) 接口组成。很少有 Windows API 以 C++ 类的形式提供。(一个值得注意的例外是 GDI+,它是 2D 图形 API 之一。) Typedefs整数类型
什么是 Signed 和 Unsigned 关键字?有符号变量可以保存正整数、负整数,包括零。默认情况下,C++ 中的整数是有符号的。因此,你可以直接使用 int,而不是显式使用 signed int。例如:
布尔类型BOOL 是 int 的类型别名,与 C++ 的 bool 以及表示布尔值的其他类型不同。头文件 WinDef.h 也为 BOOL 定义了两个值。
尽管 TRUE 有此定义,但大多数返回 BOOL 类型的函数都可以返回任何非零值来表示布尔真。因此,你应始终这样编写: 示例 指针类型Windows 定义了许多 X 类型指针的数据类型。这些数据类型通常以 P- 或 LP- 开头。SQ 是描述一个正方形的结构,而 LPSQ,例如,指的是一个 SQ。以下变量声明是可互换的。 在 16 位体系结构上,有两种不同的指针:P 代表“指针”,LP 代表“长指针”。长指针,通常称为远指针,需要访问当前段之外的内存区域。保留 LP 前缀有助于将 16 位代码移植到 32 位 Windows。截至目前,这些指针类型之间没有区别。如果必须使用这些前缀之一,请使用 P 而不是其他。 示例 说明 在上面的示例中,我们声明了三个不同的指针,它们都指向相同的类型 'SQ'。类型 'SQ' 未在此处明确定义,但假设它在代码中的其他位置是结构或 typedef 类型。这些声明显示了相同类型的不同指针语法:'SQ*'、'LPSQ' 和 'PSQ'。通常,在 Windows 编程中,'LPSQ' 和 'PSQ' 用作指针的 typedef,其中 'LP' 代表“长指针”,'P' 代表“指针”。 指针精度类型以下数据类型的大小始终与指针相同:在 32 位程序中,它们是 32 位宽,在 64 位应用程序中,它们是 64 位宽。大小在编译时决定。这些数据类型在 64 位 Windows 上运行 32 位应用程序时仍为 4 字节宽。
上述数据类型用于整数可能被强制转换为指针的情况。它们还用于定义用于指针算术的变量,以及定义遍历内存缓冲区中完整字节范围的循环计数器。更一般地说,它们出现在 32 位值在 64 位 Windows 上扩展到 64 位的地方。 什么是匈牙利命名法?匈牙利命名法是 Charles Simonyi 在 20 世纪 70 年代引入的一种编程命名约定。发明者是匈牙利人,因此得名。这个概念随着时间的推移而发展,并在编程社区中受到了赞扬和批评。 在匈牙利命名法中,变量名包含一个指示变量数据类型的前缀。此前缀通常是一组小写字母,后跟一个大写字母。以下是一些常见前缀的示例:
示例
匈牙利命名法在变量名中提供了关于变量数据类型的明确信息,这使得理解代码更容易并防止与不正确数据类型相关的错误。然而,批评者认为,现代 IDE、语法高亮和编程语言中的类型系统使得匈牙利命名法不再那么必要,有时甚至不利于代码可读性。 我们再举一个例子:符号 i、cb、rw 和 col 分别代表索引、行号和列号,并表示以字节为单位的大小(“字节计数”)。这些前缀的目的是防止在不正确的上下文中使用变量。例如,如果你看到方程 rwPosition + cbTable,你就会知道行号正在添加到大小中。这几乎肯定是代码中的一个问题。 字符串Windows 轻松支持用于 UI 元素、文件名等的 Unicode 字符串。由于 Unicode 包含所有字符集和语言,因此它是推荐的字符编码。Windows 使用 UTF-16 编码来表示 Unicode 字符;每个字符编码为一个或两个 16 位整数。为了将它们与 8 位 ANSI 字符区分开来,UTF-16 字符被称为宽字符。Visual C++ 编译器通过内置的 wchar_t 支持宽字符数据类型。头文件 WinNT.h 也定义了以下 typedef。 编码 在字面量前加上 L 来定义宽字符或宽字符字符串字面量。 说明 上述代码定义了一个宽字符数据类型别名 'WCHAR',声明了一个宽字符变量 'a',其值为 'a',以及一个指向宽字符字符串 "hello" 的指针 'str'。 Unicode 和 ANSI 函数为了方便过渡,Microsoft 在将 Unicode 支持添加到 Windows 时提供了两套并发的 API:一套用于 ANSI 字符串,另一套用于 Unicode typedef wchar_t WCHAR; wchar_t a = L'a'; wchar_t *str = L"hello"; Unicode 和 ANSI 函数字符串。例如,有两种方法可以自定义窗口标题栏中显示的文本:
ANSI 版本在内部将字符串转换为 Unicode。此外,Windows 头文件中指定的宏在没有 Unicode 版本时解析为 ANSI 版本,或者在声明预处理器符号 UNICODE 时解析为 Unicode 版本。ANSI 版本在内部将字符串转换为 Unicode。此外,Windows 头文件中声明的宏在定义预处理器符号 UNICODE 时解析为 Unicode 版本,在其他情况下解析为 ANSI 版本。 示例 说明 上述代码根据是否定义了 'UNICODE' 符号提供条件编译。如果定义了 'UNICODE',它将 'SetWindowText' 别名为 'SetWindowTextW',后者是用于宽字符字符串的函数的 Unicode 版本。如果未定义 'UNICODE',它将 'SetWindowText' 别名为 'SetWindowTextA',后者是用于窄字符字符串的函数的 ANSI 版本。这允许代码适应不同的字符编码要求。 尽管是宏的名称而不是方法本身,但 SetWindowText 是 MSDN 中记录的函数。新程序应始终调用 Unicode 版本。Unicode 对于许多全球语言都是必需的。如果你使用 ANSI 字符串,则无法对程序进行本地化。ANSI 版本效率也较低,因为操作系统必须在运行时将 ANSI 字符串转换为 Unicode。你可以根据自己的喜好使用宏或手动使用 SetWindowTextW 等 Unicode 函数。尽管这两个版本相同,但 MSDN 示例代码通常会调用宏。大多数 Windows 较新的 API 只有 Unicode 版本;没有 ANSI 版本。 TCHARS当程序除了 Windows 95、Windows 98 和 Windows Me 之外还必须支持 Windows NT 时,根据应用程序的目标平台,为 Unicode 或 ANSI 字符串编写相同的代码会很有帮助。为此,Windows SDK 提供了根据平台将字符串转换为 ANSI 或 Unicode 的宏。
编码 说明 上述代码行将窗口文本设置为“My Application”。“TEXT”宏允许代码适应不同的字符编码,具体取决于是否定义了“UNICODE”,方法是使用宽字符或窄字符字符串。 解析为单个选项编码 说明 上面两行代码都将窗口文本设置为“My Application”。第一行使用“SetWindowTextW”函数,该函数需要一个宽字符字符串 (Unicode),而第二行使用“SetWindowTextA”函数,该函数需要一个窄字符字符串 (ANSI)。 如今,所有程序都应该使用 Unicode,这使得 TEXT 和 TCHAR 宏的作用减小。尽管如此,一些较旧的程序和 MSDN 代码示例可能仍包含它们。微软 C 运行时库的头文件中定义了一组相关的宏。例如,如果 _UNICODE 未知,_tcslen 解析为 strlen;否则,它解析为 wcslen,这是 strlen 的宽字符等效项。 编码 说明 上述代码将 '_tcslen' 定义为 'wcslen' (用于 Unicode) 或 'strlen' (用于非 Unicode) 的别名,具体取决于编译期间是否定义了 '_UNICODE'。这使得编写能够无缝处理 Unicode 和非 Unicode 字符编码的代码成为可能。 注意:虽然某些头文件使用带下划线前缀的 _UNICODE,但其他头文件使用预处理器符号 UNICODE。请务必始终声明这两个符号。在 Visual C++ 中启动新项目时,默认情况下会配置这两个符号。什么是窗口?通常,窗口包含按钮、文本框、图形和其他交互元素等视觉元素,允许用户与应用程序进行交互。窗口彼此独立,这意味着它们可以逐一移动、调整大小、缩小、最大化和关闭。这种灵活性允许用户同时使用多个应用程序。每个窗口顶部通常有一个标题栏,显示标题,并且可能有用于最小化、最大化和关闭窗口的按钮。 窗口通常具有定义其边界的边界和其他控件,例如滚动条,可以容纳不适合窗口视图区域的元素。窗口可以在许多 GUI 设置中以父子关系编程。例如,对话框可以被认为是主应用程序窗口的子窗口。窗口可以接收并响应各种类型的信息,例如鼠标点击、键盘输入和系统消息。这些问题通常使用应用程序事件或消息循环来处理。 什么是父窗口和所有者窗口?父窗口包含其他窗口,称为子窗口。子窗口显示在其父窗口的客户区中。对话框是父窗口的一个示例。客户端通常创建和维护其子窗口,控制其外观、行为和情绪。 主窗口基本上是对话框的概念。当显示对话框时,它必须有一个主窗口,通常是主应用程序窗口或另一个顶级窗口。主窗口充当演示框,即使对话框打开,也可以与应用程序交互。 窗口句柄Windows 不是 C++ 类,而是具有代码和数据的对象。相反,句柄是计算机用来引用窗口的值。窗口句柄使用不透明的数据类型。它实际上只是操作系统分配给每个项的一个数字。想象一下 Windows 有一个大表,其中包含所有创建的窗口。它使用此表通过句柄查找窗口。窗口句柄使用数据类型 HWND,通常发音为“aitch-wind”。CreateWindow 和 CreateWindowEx 方法在构造窗口时返回窗口句柄。 为了在窗口上执行任务,你通常需要调用一个接受 HWND 值作为输入参数的函数。例如,使用 MoveWindow 方法在屏幕上移动窗口: 示例 说明 上述代码将通过其句柄 (hwnd) 标识的窗口移动并调整大小到指定位置 (X, Y) 和大小 (nWidth, nHeight),并可以选择重新绘制窗口 (bRepaint)。 屏幕和窗口坐标屏幕坐标指的是元素(如窗口、图标或文本)在屏幕上相对于整个显示区域的位置和大小。这些坐标通常由一对值 (X, Y) 表示,其中 (0, 0) 通常表示屏幕的左上角。 另一方面,窗口坐标是相对于窗口客户区的。它们指定特定窗口内元素的位置和大小,通常由一对值 (X, Y) 表示,其中 (0, 0) 通常表示窗口客户区的左上角。 WinMain 应用程序入口点WinMain 函数是 C 或 C++ 编写的 Windows 应用程序的入口点。它相当于基于控制台应用程序中的 main 函数。当 Windows 应用程序启动时,操作系统调用 WinMain 函数,传递句柄和命令行参数等参数。 通常,WinMain 函数的签名如下所示: 示例 说明 在上述代码中,hInstance 告诉如何处理应用程序的当前实例。同样,hPrevInstance 用于处理应用程序的上一个实例。此参数在 Win32 编程中始终为 NULL。lpCmdLine 是一个指向以 null 结尾的字符串的指针,该字符串包含传递给程序的命令行参数,不包括程序名称。nCmdShow 参数指定窗口的显示方式,例如最大化、最小化或正常显示。 上述函数返回一个 int 值。返回值可以向另一个应用程序发送状态码,但操作系统不使用它。 调用约定,例如 WINAPI,指定如何将参数从调用者发送到函数。例如,调用约定确定参数在堆栈上出现的顺序。请记住,精确地按照上述示例所示指定 wWinMain 函数。 WinMain 函数与 wWinMain 不同,它以 ANSI 字符串接收命令行参数。尽管推荐使用 Unicode 字符串,但即使你的程序编译为 Unicode,你仍然可以使用 ANSI WinMain 函数。要获取 Unicode 命令行参数,请使用 GetCommandLine 函数,该函数将所有参数作为单个字符串返回。对于 argv 样式数组,将此字符串传递给 CommandLineToArgvW。 编译器如何知道使用 wWinMain 而不是默认的 main 函数是合适的?实际上,WinMain 或 wWinMain 是由 Microsoft C 运行时库 (CRT) 提供的 main 的实现调用的。在 main 内部,CRT 执行其他任务。例如,它在任何静态初始化器之前调用 wWinMain。如果你链接到 CRT,即使你可以指示链接器使用不同的入口点函数,你也应该使用默认的入口点函数。如果省略 CRT 初始化代码,可能会发生意外结果,例如全局对象初始化不正确。 这是空的 WinMain 函数: 编码 说明 上述代码定义了 WinMain 函数,它是 Windows 应用程序的入口点。它接收实例句柄和命令行参数,并返回 0,不执行任何操作。 创建窗口创建窗口包含几个步骤,下面逐步描述。考虑所有步骤以显示窗口。 1. 注册窗口在此部分中,你需要定义一个窗口类 WNDCLASS 结构,包括窗口类和窗口过程的所有信息。 2. 创建窗口要创建窗口,请使用 CreateWindowEx 方法。接下来,提供所有信息,例如窗口的大小、位置、父窗口、菜单句柄、实例句柄、类名、标题、样式和其他属性(如果可用)。 3. 处理消息
4. 运行消息循环使用 GetMessage 方法从应用程序的消息队列中获取消息。然后,使用 TranslateMessage 和 DispatchMessage 翻译和分派消息。最后,继续循环直到收到退出消息 (WM_QUIT)。 编码 说明 在上面的代码中,我们展示了使用 Win32 API 在 C++ 中创建的 Windows 应用程序。接下来,我们定义了一个窗口程序 (WindowProc),它处理滚动窗口和响应其关闭等消息。'WinMain' 函数作为入口点,在操作系统中注册一个窗口类,根据此窗口类创建一个窗口,显示它,进入消息循环以处理窗口事件,消息循环从请求队列接收消息。接下来,它翻译并移动合适的窗口到事件。 已发布消息和已发送消息
编写窗口过程要使用 Win32 API 在 C++ 中编写窗口过程,你通常需要定义一个具有 LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 签名的函数。以下是如何编写基本窗口过程的示例: 编码 说明
LRESULT 表示你的程序返回给 Windows 的整数值,它传达了对特定消息的响应。此值的解释因消息代码而异。CALLBACK 指定函数的调用约定。一个常见的窗口过程是一个根据消息代码切换的大型 switch 语句。此语句中的每个 case 都适用于你希望处理的消息。 一个传统的窗口过程包含一个根据消息代码评估的实质性 switch 语句。在此语句中,包含用于管理每种消息类型的相应 case。 编码 默认消息处理默认消息处理强调为窗口过程中未明确处理的消息提供响应机制。这通常通过调用 `DefWindowProc` 函数来完成,该函数对未处理的消息执行默认处理。这确保窗口为应用程序未专门处理的消息保持标准行为。 编码 绘制窗口我们已经创建了窗口;现在,我们想在其中显示一些东西。在窗口术语中,窗口内显示的东西称为绘制。有时,你的软件会开始绘制以改变窗口的外观。有时,操作系统会警告你需要重新绘制窗口的一部分。当这种情况发生时,操作系统会向窗口发送 WM_PAINT 消息。必须绘制的窗口部分称为更新区域。 当窗口显示时,窗口客户区必须被绘制。因此,当你显示消息时,你将始终收到一个 WM_PAINT 消息。绘制客户区后,删除更新区域。这会通知操作系统除非发生变化,否则不要发送另一个 WM_PAINT 消息。 ![]() 你只负责绘制客户端部分。操作系统会自动绘制其余的框架和标题栏部分。绘制客户区后,删除更新区域。这会通知操作系统除非发生变化,否则不要发送另一个 WM_PAINT 消息。 现在,假设用户移动到另一个窗口,遮挡了你的窗口的一部分。当非透明部分再次可见时,该部分会添加到更新区域,并且窗口会收到另一个 WM_PAINT 消息。 ![]() 如果用户拉伸窗口,更新区域也会受到影响。例如,在下图中,我们通过将窗口拉伸到右侧来显示这一点。因此,你可以看到更新区域也随之改变。 ![]() 编码 说明 上述代码处理 Windows 消息 'WM_PAINT'。它通过使用 'BeginPaint' 获取设备上下文 ('HDC') 开始绘制,执行绘制操作(例如,用窗口背景色填充矩形),并使用 'EndPaint' 结束绘制。最后,它返回 '0' 表示消息已处理。 现在,通过调用 BeginPaint 函数开始绘制任务。此函数填写 PAINTSTRUCT 结构以及重新绘制请求的信息。PAINTSTRUCT 的 rcPaint 组件指示当前更新区域,该区域是相对于客户端区域定义的。 ![]() 你的绘画代码有两种实现选择。
下面提供的代码用单一颜色填充更新区域:系统定义的窗口背景色。COLOR_WINDOW 指定的颜色基于用户当前的配色方案。 编码 说明 上述代码行将 'PAINTSTRUCT' 结构 ('ps') 的 'rcPaint' 成员指定的矩形填充为窗口背景色。'(HBRUSH) (COLOR_WINDOW+1)' 部分表示用于填充矩形的画笔是系统定义的窗口背景色加一。 关闭窗口每当用户关闭窗口时,该操作会按顺序触发窗口消息。用户通过关闭按钮或 ALT+F4 等快捷选项关闭窗口。WM_CLOSE 消息允许你在关闭窗口前向用户请求确认。如果你确实想关闭窗口,请使用 DestroyWindow 函数。否则,如果你希望保持窗口打开,则从 WM_CLOSE 消息返回零,操作系统将忽略该消息,使窗口保持原样。 ![]() 以下是程序如何处理 WM_CLOSE 函数的示例。 编码 说明 上述代码处理 WM_CLOSE 消息。它显示一个消息框,询问用户是否真的要退出应用程序。如果用户单击确定,窗口将被销毁。如果用户单击取消,则什么也不会发生。最后,它返回 0 表示消息已处理。 通常,在主应用程序窗口中处理 WM_DESTROY 消息时,你会调用 PostQuitMessage 函数。 编码 说明 上述代码处理 WM_DESTROY 消息。它向消息队列发布一条退出消息,参数为 0,表示应用程序正常终止。然后,它返回 0 表示消息已处理。这通常会导致应用程序的消息循环终止,进而导致应用程序本身终止。 管理应用程序状态窗口过程作为每个消息调用的函数,本身缺乏状态。因此,需要一种方法来监视函数调用之间应用程序的状态。最简单的方法是使用全局变量,这适用于小型程序,如许多 SDK 示例所示。然而,在大型程序中,这会导致过多的全局变量使用,使管理复杂化,特别是当有多个窗口及其各自的过程时。相反,CreateWindowEx 函数提供了一个解决方案,允许将任何数据结构传递给窗口,这通过在调用窗口过程时发送到窗口过程的两条消息来简化。
WM_NCCREATE 和 WM_CREATE 消息在窗口可见之前分派,这使得它们非常适合 UI 初始化任务,例如定义初始窗口布局。 CreateWindowEx 的最后一个参数是一个 void 指针,允许传递任何指针值。在处理 WM_NCCREATE 或 WM_CREATE 消息时,窗口过程可以从消息数据中提取此值。 编码 当你调用 CreateWindowEx 时,将此结构的指针传递给最终的 void* 参数。 编码 说明 上述代码为 StateInfo 结构分配内存。如果分配失败,则返回 0。然后,使用指定的参数创建窗口,包括窗口类、文本、样式、大小、位置、父窗口、菜单、实例句柄和附加应用程序数据。 收到 WM_NCCREATE 和 WM_CREATE 消息时,每条消息的 lParam 参数都包含一个指向 CREATESTRUCT 结构的指针。此结构反过来又包含传递给 CreateWindowEx 的指针。 ![]() 要提取数据结构的指针,首先将 lParam 参数强制转换为 CREATESTRUCT 结构。 编码 说明 给定的代码使用 reinterpret_cast 将 lParam 参数(一个指针)转换为 CREATESTRUCT 类型的指针。此任务检索存储在 CREATESTRUCT 结构中的信息。 CREATESTRUCT 结构中的 lpCreateParams 包含 CreateWindowEx 中指定的原始 void 指针。要获取自定义数据结构的指针,只需强制转换 lpCreateParams。 编码 随后,调用 SetWindowLongPtr 函数,并将数据结构的指针作为参数提供。 编码 说明 上述代码行将自定义指针 ('pState') 设置为附加到 Windows 应用程序中窗口 ('hwnd') 的数据,允许应用程序稍后存储和检索该窗口的自身信息。 完成此操作后,你可以使用 GetWindowLongPtr 从窗口中检索指针 编码 说明 上述代码使用 'GetWindowLongPtr' 获取存储在窗口中的自定义指针,然后将其解释为 'StateInfo*' 指针以供进一步使用。 你可以为每个窗口创建特定的实例数据,使你能够拥有多个窗口,每个窗口都有其自己的数据结构实例。当处理窗口类(例如自定义控件类)时,此方法变得有利,你可能希望创建相同类型的多个窗口。 编码 说明 在上述代码中,'GetAppState' 使用 'GetWindowLongPtr' 检索与窗口 ('hwnd') 关联的自定义指针。然后,它使用 'reinterpret_cast' 将其转换为 'StateInfo*' 指针。最后,它返回获取的指针。 在下一部分中,我们将看到第一个 Windows 程序,其中我们将创建一个简单的表单来学习使用 Win32 API 的 C++ Windows 程序的基础知识。 模块 1:第一个 Windows 程序(一个简单的表单)在这里,我们将指导你使用强大的 Win32 API 创建一个简单的 Windows 表单程序。
代码 (MyForm.cpp) 说明 上述代码在 C++ 中启动一个 Windows Forms 应用程序,设置所需的属性和命名空间。这包括一个名为 "MyForm.h" 的文件,你将其导入到表单类中。然后,它将 [STAThread] 属性设置为主要任务,为 COM 兼容性创建单线程构建模式。Application::EnableVisualStyles() 调用为应用程序启用视觉样式,提供现代的外观和感觉。 Application::SetCompatibleTextRenderingDefault(false) 将兼容文本呈现设置为 false;检查良好的字体呈现。它创建名为 MyForm 的表单实例,并最终运行应用程序,向用户显示该表单。 代码 (MyForm.h) 输出 (MyForm.h) ![]() 说明 上面给出的代码描述了使用 Visual Studio 在 C++/CLI 中实现的 Windows Forms 应用程序。我们创建了一个名为 MyForm 的表单,其中包含用于名字、中间名和姓氏的文本框,以及用于确定和删除操作的按钮。该表单使用表格布局面板配置控件。当表单加载时,一个事件处理程序从输入文本框中检索信息,将其连接起来,并在表单上的标签上显示欢迎消息。此外,代码还包括为表单控件设计的初始化和事件处理代码,例如设置它们的属性和定义它们的行为。 在下一个模块中,我们将讨论在 Windows 程序中使用 COM(组件对象模型)来创建可重用的软件组件。 模块 2:在 Windows 程序中使用 COMCOM 代表组件对象模型。它是一种用于创建可重用软件组件的规范。许多基于 Windows 的现代程序功能都依赖于 COM,包括:
以下是与用户界面设计和开发相关的术语:
注意:必须认识到 COM 是一个二进制标准,不限于任何特定的编程语言。COM 定义了应用程序和软件组件之间的二进制接口。COM 的主要目标包括:以下是 COM 的一些主要关键方面:
注意:为了促进对象链接与嵌入 (OLE) 2.0,COM 最初创建于 1993 年。OLE 2.0 是基于 COM 构建的,但你不需要了解 OLE 即可理解 COM。例如,假设你想在程序中创建一个“打开”对话框。COM 允许你无缝地实现这一点。 了解 COM 接口接口概述了一系列对象可以提供的方法,抽象了这些方法如何实现的细节。它充当调用方法的代码与负责实现方法的代码之间的明确分离。在计算机科学中,这个概念被称为解耦,其中方法的调用者与其实现细节分离。 ![]() 在 C++ 中,纯虚类最接近接口。这样的类只包含纯虚方法,没有任何其他成员。下面是 C++ 中假设接口的说明性示例: 编码 说明 上述代码是伪 C++ 语法中名为 'IDrawable' 接口的概念表示。它定义了一个方法 'Draw()',但没有指定其实现。此接口允许对象如果共享可绘制的共同能力则被统一处理,但重要的是要注意此代码不是实际的 COM。 由于所有接口都是抽象的,程序无法直接实例化 IDrawable 类型的对象。因此,以下代码片段将无法编译: 编码 相反,图形库提供符合 IDrawable 接口的对象。例如,该库可能提供一个用于绘制几何形状的形状对象和一个专门用于渲染图像的位图对象。在 C++ 中,这通过从共享抽象基类继承来实现: 编码 说明 在上述代码中,'IDrawable' 类型的指针 'pDrawable' 被分配了 'CreateTriangleShape()' 函数创建的三角形形状对象的地址。如果 'pDrawable' 不为空,则调用三角形形状对象的 'Draw()' 函数。 以下示例说明了遍历 'IDrawable' 指针数组。该数组可以包含各种形状、位图或其他图形实体,前提是数组中的每个对象都继承自 'IDrawable'。 编码 说明 函数 'DrawSomeShapes' 遍历 'IDrawable' 指针数组,并对每个对象调用 'Draw()' 函数。 注意:本文档中提供的代码不是真实的示例。它说明了通用概念。创建新的 COM(组件对象模型)接口超出了本系列的范围。通常,COM 接口不是直接在头文件中定义的。相反,它是使用接口定义语言 (IDL) 定义的。随后,IDL 文件由 IDL 编译器处理,该编译器生成 C++ 头文件。示例 使用 COM 时,必须认识到接口本身不是对象,而是一组对象必须遵循的方法。多个对象可以实现相同的接口,如 Shape 和 Bitmap 示例所示。此外,单个对象可以实现多个接口。例如,图形库可能会引入一个名为 ISerializable 的接口,以方便保存和加载图形对象。请参阅下面的类声明: 编码 说明 上述代码定义了一个名为 'ISerializable' 的接口,其中包含从文件加载和保存对象的方法。此外,它还声明了两种可绘制对象类型:'Shape',它继承自 'IDrawable',以及 'Bitmap',它继承自 'IDrawable' 和 'ISerializable'。 ![]() 在本节中,我们详细说明了接口。让我们进入下一部分,我们将讨论 COM 库。 初始化 COM 库初始化 COM (组件对象模型) 库通常通过调用 CoInitializeEx 函数来完成。此函数在当前单元和线程上初始化 COM 库。以下是如何初始化 COM 库的简单示例: 编码 说明 在上述代码示例中,CoInitializeEx 的第一个参数为 NULL,这意味着 COM 库已针对当前单元和线程进行初始化。第二个参数指定了此线程将使用的并发模型。COINIT_APARTMENTTHREADED 通常用于桌面应用程序。 COM 允许两种不同的线程模型,单元线程和多线程。如果你使用单元线程,你正在做出以下保证:
如果以上条件都不成立,请选择多线程模型。要定义线程模型,请使用 dwColnit 参数中提供的标志之一。
请确保正确放置其中一个标志。通常,控制窗口的线程应使用 'COINIT_APARTMENTTHREADED' 标志;其他线程应使用 'COINIT_MULTITHREADED'。但是,某些 COM 组件可能需要特定的线程模型。请查看 MSDN 文档以了解此类要求。 注意:事实上,即使指定了内部布线,仍然可以使用一种称为封送处理的技术来共享布线接口。但是,封送处理的复杂性不在此模块的讨论范围之内。重要的是要了解,在使用单元线程时,切勿简单地将接口指针复制到另一个线程。除了上述标志外,建议将 COINIT_DISABLE_OLE1DDE 标志添加到 dwCoInit 参数中。此标志有助于避免与旧技术对象链接和嵌入 (OLE) 1.0 相关的一些开销。下面是使用单元线程的 COM 初始配置: 编码 取消初始化 COM 库要在 C++ 中取消初始化 COM 库,可以使用 CoUninitialize() 函数。此函数会覆盖 COM 库提供的对象,并且对于每次成功调用 CoInitialize 或 CoInitializeEx,都必须调用一次。以下是如何操作: 编码 COM 中的错误代码在 COM 中,错误代码由 HRESULT 值表示,它是一个 32 位值,包含错误代码和有关错误的其他信息。COM 函数通常返回 HRESULT 值以指示内部操作的成功或失败。 HRESULT 的关键部分决定了操作是否成功。高位为零 (0) 表示成功,而值为一 (1) 表示失败。 它导致以下数字范围:
大多数 COM 方法返回 HRESULT 值以指示成功或失败。但是,少数方法(例如 AddRef 和 Release)返回未初始化的长值。要确定 COM 方法是否成功,可以检查返回的 HRESULT 的高位。方便的是,Windows SDK 头文件提供了两个宏:SUCCEEDED 和 FAILED。SUCCEEDED 宏在 HRESULT 表示成功结果时返回 TRUE,在表示错误条件时返回 FALSE。下面是使用 SUCCEEDED 宏验证 CoInitializeEx 操作是否成功的示例。 编码 说明 上述代码使用单元线程初始化 COM 库并启用 OLE 1.0 DDE。然后,它使用 SUCCEEDED 宏检查初始化是否成功。如果成功,它将继续该过程;如果失败,它将修复错误。 有时,测试相反的条件会更简单。FAILED 宏与 SUCCEEDED 作用相反:对于错误代码返回 TRUE,对于成功代码返回 FALSE。 编码 说明 上述代码使用指定设置初始化 COM 库。然后,它使用 FAILED 宏检查初始化是否失败。如果失败,它将处理错误;否则,它将继续执行函数。 在 COM 中创建对象在 COM(组件对象模型)中,你通常通过调用 CoCreateInstance 函数来创建对象。此函数创建一个指定 COM 类的实例并返回其接口指针。以下是如何在 COM 中创建对象的基本示例: 通常,有两种方法可以创建 COM 对象;让我们看看它们:
让我们以假设的 Shape 对象为例。在给定的示例中,Shape 对象实现了名为 IDrawable 的接口。实现 Shape 对象的图形库可能会导出具有以下签名的函数。 编码 顺便说一句,你可以按如下方式创建一个新的 Shape 对象: 说明 上述代码使用名为 CreateShape 的函数创建一个 Shape 对象,该函数返回 HRESULT。如果创建成功,它将继续使用 Shape 对象;否则,它将处理错误。 参数 ppShape 是一个指向 IDrawable 类型的指针的指针。该函数需要向调用者返回一个 IDrawable 指针。但是,函数的返回值已指定用于错误/成功代码。因此,指针必须通过函数参数传递回来。调用者将提供一个 IDrawable* 类型的变量给函数,函数将使用新的 IDrawable 指针更新该变量。在 C++ 中,函数可以通过传引用或传地址来覆盖参数值。COM 使用后者,即传地址。由于需要指针的地址,因此参数类型必须是 IDrawable**。 这是一个了解这里发生了什么的图表: ![]() CreateShape 函数使用地址运算符 (&) 和 pShape (&pShape) 来更新 pShape 的值以获取新指针。 使用 CoCreateInstance 创建对象CoCreateInstance 函数提供了一种通用的对象创建方法。理解 CoCreateInstance 需要认识到两个 COM 对象可以实现相同的接口,并且一个对象可以实现多个接口。因此,对象创建的通用函数需要两条信息。
在 COM 中,对象和接口都通过分配给它们一个 128 位数字(称为全局唯一标识符 (GUID))来区分。GUID 的生成是为了确保它们的实际唯一性,而无需中央注册机构。它们也称为通用唯一标识符 (UUID),最初用于 DCE/RPC (分布式计算环境/远程过程调用),后来被 COM 采用。有各种算法可用于生成新的 GUID。 虽然并非所有算法都确保完全唯一性,但两次给出相同 GUID 值的概率非常低,实际上可以忽略不计。GUID 可以识别除了对象和接口之外的各种实体,但在本模块中,我们只关注它们在此上下文中的使用。例如,Shapes 库可以声明两个 GUID 常量。 示例 CLSID_Shape 代表常量 Shape 对象,而常量 IID_IDrawable 代表 IDrawable 接口。前缀“CLSID”和“IID”分别代表类标识符和接口标识符,与 COM 中的命名标准相关联。要实例化新的 Shape 对象,你需要按如下方式进行: 编码 说明 在上面的代码中,我们创建了一个 Shape 对象的新实例并获取了其 IDrawable 接口指针。如果可能,请使用 Shape 对象;否则,修复错误。 CoCreateInstance 函数包含五个参数。第一个和第四个参数分别表示类标识和接口标识。这些参数指示用户“创建 Shape 对象的实例并提供指向 IDrawable 接口的指针”。 第二个参数指定 NULL。第三个参数基本上是一组用于描述对象执行上下文的标志。此规范指示对象是在与应用程序相同的进程中运行,在同一计算机上的特定进程中运行,还是在远程计算机上运行,下表列出了该参数最常见的值。
第五个 CoCreateInstance 参数接受一个接口指针。由于 CoCreateInstance 的通用性,它没有强类型参数。因此,它的数据类型是 void**,调用者必须将指针的地址设置为 void** 类型。这解释了 reinterpret_cast 在前面示例中的使用方式。 检查 CoCreateInstance 的返回值很重要。如果函数返回错误代码,则 COM 接口指针无效,尝试绕过其引用可能会导致程序崩溃。 CoCreateInstance 使用各种内部方法来创建对象。最简单的形式是,它在注册表中查找类标识符,并识别使用该对象的 DLL 或 EXE。此外,CoCreateInstance 可以使用 COM+ 目录或并行 (SxS) 显示中的信息。但是,此使用信息对调用者来说是无形的。有关 CoCreateInstance 内部工作原理的更多信息,请参阅 COM 客户端和服务器文档。 上面的例子不是一个真实的例子。让我们进入下一节,看看现实世界中的问题,在那里我们将详细讨论 COM 以及现实世界中的问题。 打开对话框:一个真实世界的示例要显示“打开”对话框,程序可以使用一个名为“常用项对话框”的 COM 对象。该对象识别 Shobjidl.h 头文件中声明的指定接口 IFileOpenDialog。 ![]() 下面是一个程序,演示如何向用户呈现“打开”对话框。选择文件后,程序会弹出一个对话框,显示文件名称。 编码 说明 上述代码初始化组件对象模型 (COM) 并实例化 `IFileOpenDialog` 接口,以在 Windows 应用程序中显示“打开”对话框。它从对话框中检索选定的文件路径,向用户显示消息框,并释放共享资源。`wWinMain` 函数充当应用程序的入口点,确保 COM 对象的正确初始化和解析。此代码演示了如何将 Windows API 函数与 COM 接口结合使用,以促进 GUI 应用程序中的文件选择和用户交互。 处理对象的生命周期每个 COM 接口都必须直接或间接继承自 IUnknown 接口。此接口建立了所有 COM 对象必须具备的基本功能。特别是,IUnknown 接口包含三个方法:
QueryInterface 方法使程序能够在运行时查询对象的功能。更多细节将在即将到来的主题“请求对象的接口”中提供。目前,AddRef 和 Release 方法用于控制对象的生命周期,这是本次讨论的重点。 什么是引用计数?每个 COM 对象都维护一个内部计数器,称为引用计数,它控制对对象的活动引用数量。当引用数量达到零时,表示没有活动引用,对象将自身分发。重要的是要强调对象处理其删除,程序不明确删除对象。 以下是引用计数的规则:
下面展示了一个简单但复杂的案例: ![]() 程序创建一个对象并持有其指针 (p)。最初,引用计数设置为 1。一旦使用指针,程序调用 Release 方法。因此,引用计数设置为零,导致对象自动删除。之后,p 指针无效。尝试使用 p 调用另一个方法将导致错误。 下图显示了一个更复杂的示例: ![]() 在这里,程序创建一个对象并存储指针 p,如前所述。然后程序将 p 复制到新变量 q。在这种情况下,程序必须调用 AddRef 来增加引用计数。因此,解释的数量达到 2,表示两个相关的决定因素。然后,当程序通过执行 p 终止时,调用 Release,将引用计数减少到 1 并使 p 失效。但是,q 仍然有效。稍后,在使用 q 之后,程序再次调用 Release。此操作将引用计数减少到零,并导致对象自行删除。 你可以在这里提问,为什么程序会复制 p。主要有两个原因:
这样,你就可以将其复制到具有更广泛作用域的另一个变量中。 引用计数的一个优点是,指针可以在代码的不同部分共享,而无需协调不同的代码路径来删除对象,相反,每个代码路径在使用对象后只需调用 Release。因此,对象及时处理自身的删除。 示例 说明 上述代码启动 COM,打开文件对话框,检索选定文件的路径,显示它,然后进行清理。 在此代码中,引用计数使用了两次。最初,如果函数成功创建了 Common Item Dialog 对象,你需要对 pFileOpen 指针调用 Release。 编码 说明 上面的代码是文件打开对话框 COM 对象的一个示例。如果构建成功,它将执行一个任务,然后使用 Release 方法释放对象以更有效地管理其内存。 此外,如果 GetResult 方法提供了指向 IShellItem 接口的指针,程序必须对 pItem 指针调用 Release。 编码 说明 上述代码检索文件对话框中选定的项目,如果成功,则对其执行操作,然后释放其资源。 注意:在两种情况下,调用 Release 都是指针不再在作用域内的最后一步。此外,重要的是要注意,只有在 HRESULT 确认其成功后才调用 Release。例如,如果 CoCreateInstance 调用失败,pFileOpen 指针仍然处于活动状态。因此,尝试对指针调用 Release 将导致错误。请求对象的接口一个对象可以使用多个接口,例如“通用项对话框”对象。对于标准功能,该产品使用 IFileOpenDialog 接口,其中包含显示对话框和接收有关所选文件信息的方法。此外,该产品支持 IFileDialogCustomize 接口以实现高级自定义选项。此接口允许用户通过添加额外的 UI 控件来更改对话框的外观和感觉。请记住,任何 COM 接口都可以直接或间接继承自 IUnknown 接口。下图显示了“通用项对话框”对象的属性结构。 ![]() 如图所示,IFileOpenDialog 的直接前身是 IFileDialog 接口,它本身扩展了 IModalWindow。随着层次结构从 IFileOpenDialog 到 IModalWindow 的演进,接口逐渐定义了所有生成的窗口函数。毕竟,IModalWindow 接口继承自 IUnknown。此外,“通用项对话框”对象支持 IFileDialogCustomize 接口,该接口是特定属性集的一部分。 现在,假设你有一个 IFileOpenDialog 接口的指针。你如何获取 IFileDialogCustomize 接口的指针? ![]() 直接将 IFileOpenDialog 指针强制转换为 IFileDialogCustomize 指针是不可能的。如果运行时类型信息 (RTTI) 不跨越序列层次结构,则特定语言是不可能的。 相反,COM 方法会要求对象提供一个 IFileDialogCustomize 指针,该指针使用原始接口作为通道。这需要从第一个接口指针调用 IUnknown::QueryInterface 方法。从概念上讲,QueryInterface 相当于 C++ 中的 dynamic_cast 关键字,但不依赖于语言。 QueryInterface 方法的定义如下所示: 语法 QueryInterface 如何工作?
以下是调用 QueryInterface 以获取 IFileDialogCustomize 指针的方法: 编码 说明 上述代码片段使用 QueryInterface 从 pFileOpen 请求 IFileDialogCustomize 接口指针。如果成功,则使用该接口,然后释放其资源。如果失败,则执行错误处理。 COM 中的内存分配组件对象模型中的内存分配很重要,因为 COM 组件通常在进程边界使用,因此内存管理需要明确定义以确保稳定性和效率。 以下是 COM 中内存分配的简要概述: 有时,方法可能会通过在堆上分配内存缓冲区来向调用者提供缓冲区的地址。COM 提供了许多专门设计用于在堆上分配和释放内存的函数。 在涉及“打开”对话框的示例中观察到了这种模式。 编码 说明 上述代码初始化类型为 'PWSTR' 的指针 'pszFilePath',并调用 'pItem' 对象上的 'GetDisplayName' 方法,传递 'SIGDN_FI'LESYSPATH' 作为参数和 'pszFilePath' 的地址。它检查操作是否成功,如果成功,可能会使用存储在 'pszFilePath' 中的数据,执行更多代码。最后,它使用 'CoTaskMemFree' 任务释放分配给 'pszFilePath' 的内存。 COM 为什么定义自己的内存分配函数?COM 定义自己的内存分配函数,以确保跨编程语言、平台和组件进行一致可靠的内存管理。这些任务,例如 'CoTaskMemAlloc'、'CoTaskMemRealloc' 和 'CoTaskMemFree',专门设计用于以符合 COM 规则和要求的方式处理内存分配和解除分配。 这有助于避免在使用 COM 环境中的标准内存分配函数时可能发生的内存泄漏、碎片和其他与内存相关的问题。 COM 编码实践在本主题中,我们将确保在 COM 框架内创建健壮、可维护和高效的代码。遵循一些基本原则很重要。以下是一些推荐的最佳实践。 运行程序时可能会遇到以下链接器错误: 错误代码 上述错误消息表明创建了一个具有外部链接的 GUID 常量,但链接器无法找到此常量的定义。通常,GUID 常量的值是从静态库文件中导出的。通过在 Microsoft Visual C++ 中使用 `__uuidof` 函数(这是一个 Microsoft 语言扩展),你可以避免链接到静态库。此函数从表达式中检索 GUID 值,该表达式可以是接口类型名称、类名称或接口指针。使用 `__uuidof`,你可以实例化 Common Item Dialog 对象,如下所示: 编码 说明 上述代码初始化指向文件打开对话框接口 (`IFileOpenDialog`) 的指针,并创建 `FileOpenDialog` 类的实例,从而实现与 Windows 文件打开对话框功能的交互。 注意:你不需要导出库。编译器从头文件中检索 GUID 值。类型名称与头文件中的 __declspec(uuid(...)) 相关联。IID_PPV_ARGS 宏我们注意到 CoCreateInstance 和 QueryInterface 都要求最后一个参数强制转换为 void** 类型,这存在类型不一致的风险。请考虑以下代码片段: 编码 说明 上面的代码请求 IFileDialogCustomize 接口,但提供了 IFileOpenDialog 指针。使用 reinterpret_cast 表达式绕过了 C++ 类型系统,允许编译器忽略此错误。在最好的情况下,如果对象不支持请求的接口,调用会简单地失败。然而,在最坏的情况下,项目可能会成功,导致符号不一致。特别是,指针类型与内存中实际的 vtable 不匹配。正如你可能预期的那样,这种情况会带来意想不到的后果。 注:vtable(虚拟表)是资源管理中用于实现多态性的工具。它基本上是一个与类或对象关联的函数指针数组,可实现虚拟任务的动态调度。如果一个类包含虚函数,编译器通常会为该类创建一个 vtable。然后,每个类对象都会传递一个指向其相应 vtable 的指针,vtable 可以根据对象的实际类型而不是其静态类型在运行时调用正确的虚函数。IID_PPV_ARGS 宏有助于缓解此错误。要使用此宏,请替换以下代码 编码 替换为以下代码 编码 此宏实际上将 __uuidof(IFileOpenDialog) 添加到接口标识符中,确保它与指针类型匹配。正确的代码如下所示: 编码 此外,您可以通过 QueryInterface 使用相同的宏 编码 什么是 SafeRelease 模式?SafeRelease 策略是 COM 设计(组件对象模型)中用于安全释放接口和管理内存的常见方法。这包括在释放前确保接口指针不为 NULL,然后在释放后将指针设置为 NULL,以防止可能的重复或不正确的内存访问。 示例 说明 上述代码中的 `SafeRelease` 模板函数是 C++ 中的一个实用工具,旨在安全地释放分配给 COM 接口的对象。它将其参数作为指针的引用,并检查该指针是否不为 `NULL`。如果指针不为 `NULL`,它将使用 `Release()` 方法释放接口资源,并将指针设置为 `NULL`,从而通过确保对象得到正确清理来防止内存泄漏。 此函数接受一个 COM 接口指针作为参数,并执行以下操作:
请参阅以下示例,了解如何使用 SafeRelease 示例 说明 如果 CoCreateInstance 成功,则通过调用 SafeRelease 释放指针。如果发生故障,如果 pFileOpen 仍为 NULL,则 SafeRelease 函数会检测到此情况并避免调用 Release。 在同一指针上多次调用 SafeRelease 是安全的,如下所示: 编码 说明 上述代码片段表示,释放 `pFileOpen` 的第二行是不必要的,因为它已经释放过一次。 COM 智能指针SafeRelease 函数很有用,但您只需记住两点:
C++ 也有构造函数和析构函数;这就是为什么最好有一个包装主接口指针的类,它会自动初始化和释放指针。类似于提供的代码: 示例 说明 显示的类定义缺少基本组件,使其无法使用。为了使其正常工作,您需要定义一个复制构造函数、一个赋值运算符以及一个访问底层 COM 指针的方法。您无需承担此任务,因为 Microsoft Visual Studio 在活动模板库 (ATL) 中提供了一个智能指针类。 ATL 智能指针类名为 CComPtr。以下是使用 CComQIPtr 的示例。 编码 说明 以上代码演示了在 COM 环境中使用 Active Template Library 中的 'CComQIPtr'。它定义了一个带有 'SomeMethod' 方法的 COM 接口 'IFoo',并实现了一个支持该接口的 COM 对象 'CFoo'。'SomeClass' 类使用 'CComQIPtr CComPtr 是一个类模板,它保存指向指定 COM 接口类型的指针。它在内部管理此指针,并重写 'operator->()' 和 'operator&()' 运算符以模拟底层指针的行为。这使得与 COM 接口的无缝交互成为可能。例如,使用 CComPtr,可以像直接调用一样调用 'IFileOpenDialog::Show' 方法。 编码 CComPtr 还提供了一个方法,该方法与一些默认参数值一起调用 CoCreateInstance 函数。该方法仅需要类标识符作为参数,如下例所示: 编码 'CComPtr::CoCreateInstance' 方法的存在纯粹是为了方便;但是,您可以选择直接调用 COM 'CoCreateInstance' 函数。 COM 中的错误处理在 COM 中,HRESULT 用于显示方法或函数调用的成功或失败消息。有几个 SDK 头文件定义了 HRESULT 常量。WinError.h 定义了一个通用详细代码列表。下面,您可以找到一个显示所有这些系统返回代码的表格。
在上面的表格中,所有以“E_”为前缀的常量都表示错误代码,而 S_OK 和 S_FALSE 表示成功代码。尽管 COM 方法的成功率约为 99%,但重要的是不要被这些统计数据所误导。某些方法可能会返回任意的成功代码,需要使用 SUCCEEDED 或 FAILED 宏进行错误检查。下面的示例代码显示了检查函数调用成功与否的错误和正确方法。 编码 说明 S_FALSE 成功规则值得一提。某些方法使用 S_FALSE 来表示不良的非失败条件,或表示“无操作”——方法成功但没有效果。例如,如果 CoInitializeEx 函数在同一线程中第二次调用,则返回 S_FALSE。为了在您的代码中区分 S_OK 和 S_FALSE,您应该直接测试该值,但仍使用 FAILED 或 SUCCEEDED 来处理其余语句。这是一个示例代码片段: 编码 说明 上述代码片段检查了由 'hr' 变量表示的函数调用的结果。如果 'hr' 等于 'S_FALSE',它将处理此特定成功情况。如果函数调用通常成功(由 'SUCCEEDED' 宏指示),它将满足正常成功条件。如果发生错误,则打印错误消息。 检查结果时,请使用 'SUCCEEDED' 和 'FAILED' 宏。在测试特定错误代码的情况下,请务必也包含一个默认情况。 编码 说明 上述代码检查是否遇到了与像素格式相关的特定错误。如果是,则处理该场景。否则,处理其他错误。 错误处理模式在本节中,我们将探讨一些组织 COM 错误处理的模式。每种方法都有其优点和缺点。如果您使用现有项目,它可能已经有一些限制特定风格的准则。但是,无论您使用哪种模式,鲁棒代码都将考虑以下规则:
根据这些规则,有四种错误处理模式。
嵌套 if在每次调用 HRESULT 后,使用 if 语句验证成功。随后,将后续方法调用封装在 if 语句的范围内。这种 if 语句的嵌套可以达到任何必要的深度。尽管此模式已在本模块之前的代码示例中演示过,但在此处再次重申以求清晰。 编码 说明 上述代码定义了一个名为 ShowDialog 的函数,该函数尝试显示一个文件打开对话框。它首先初始化一个指向 IFileOpenDialog 的指针,然后尝试使用 CoCreateInstance 创建它的实例。如果成功,它将继续显示对话框并检索选定的项目。每个步骤之后都使用 if 语句检查 HRESULT 是否成功。如果所有操作都成功,它将释放资源并返回最终的 HRESULT。 优点
缺点
级联 if在每次方法调用之后,使用 if 语句验证其成功。如果成功,将后续方法调用包含在 if 块中。但是,请不要进一步嵌套 if 语句。相反,将每个后续的 SUCCEEDED 测试放在前一个 if 块之后。如果任何方法失败,所有后续的 SUCCEEDED 测试也将失败,直到函数结束。 编码 说明 ShowDialog 函数初始化 IFileOpenDialog 和 IShellItem 接口的指针,将它们设置为 NULL。然后,它尝试使用 CoCreateInstance 创建 FileOpenDialog 的实例。如果成功,它将继续显示对话框并检索选定的项目。如果两个操作都成功,pItem 已准备好使用。最后,它使用 SafeRelease 释放资源并返回 HRESULT。 这样,资源只在工作结束时释放。但是,如果发生 bug,当您退出项目时,信号可能无效。尝试显示无效指针可能会导致程序崩溃或崩溃。为了缓解这种情况,初始化所有指针为 NULL 并在废除它们之前验证它们的非 NULL 状态非常重要。此处说明了 SafeRelease 函数的实现,而智能指示器提供了一个可靠的替代方案。使用此方法时请注意循环结构;如果调用失败,请立即退出循环。 优点
缺点
失败时跳转跟踪每个方法调用,查看它是否失败。如果发生失败,立即转到操作结束附近的指定行。收到此标签后,但在离开项目之前,如果有,请释放项目。 示例 说明 上面给出的代码描述了一个名为 'ShowDialog()' 的函数,其目的是在文件中显示一个打开对话框。首先,它尝试使用 COM 函数 'CoCreateInstance()' 创建文件打开对话框的实例。如果成功,它将继续使用 'Show()' 显示对话框。然后,它使用 'GetResult()' 检索选定的对象。如果其中任何一个步骤失败(如 'FAILED(hr)' 检查所示),代码将跳转到 'done' 标签,在那里它使用 'SafeRelease()' 函数释放分配的对象('pItem' 和 'pFileOpen')。最后,它返回 HRESULT,指示操作的成功或失败。 优点
缺点
失败时抛出异常您可以使用抛出异常而不是使用标签跳转。这种方法可以导致更具比喻性的 C++ 编程方法,特别是如果您习惯于编写异常安全代码。 编码 说明 上述代码片段使用 Windows COM API 创建文件打开对话框。'throw_if_fail' 函数检查 COM 函数的 HRESULT 返回值,如果指示失败,则抛出 '_com_error' 异常。在 'ShowDialog' 函数中,它首先使用 'CoCreateInstance' 创建 'IFileOpenDialog' 接口的实例,然后使用 'Show' 方法显示对话框,最后使用 'GetResult' 检索选定的文件对象路径。在此过程中遇到的任何错误都由 try-catch 块捕获,其中处理 '_com_error' 异常。 此示例演示了如何使用 'CComPtr' 类来处理 C++ 中的接口指针。它强调了遵守 RAII(资源获取即初始化)过程的重要性,尤其是在排他性规则的情况下。RAII 确保使用析构函数承诺释放的材料正确处理商品。使用 RAII 可以防止资源泄漏,因为即使在资源分配或使用过程中发生异常,析构函数也会被调用。 优点
缺点
在对 COM 进行详细讨论之后,在后面一节中,我们将看到 Windows 图形,其中我们将创建一个空窗口并使用视觉效果。 模块 3:Windows 图形在第一个模块中,我们学习了如何创建一个空窗口。在模块 2 中,我们简要讨论了组件对象模型,它是许多现代 Windows API 的基础。现在,我们准备将图像添加到我们之前从模块 1 创建的空白窗口中。为此,我们将主要关注 Direct2D。我们还将学习如何解释图像和处理视觉效果。 Windows 图形架构Windows 平台提供了各种 C++/COM 图形 API,如下图所示。 ![]() GDI 它是 Windows 的基本图形接口。它最初是为 16 位 Windows 开发的,后来更新为 32 位和 64 位版本。 GDI+ GDI+ 在 Windows XP 中引入,是 GDI 的继任者。它通过许多 C++ 类访问,并在 .NET Framework 中可用。 Direct3D Direct3D 是一个 3D 建模 API。它使开发人员能够创建沉浸式 3D 环境、游戏和模拟。Direct3D 使用 GPU 的硬件速度。 Direct2D Direct2D 是一种超越 GDI 和 GDI+ 的现代 2D 图形 API。它提供硬件加速渲染、清晰度和抗锯齿。 DirectWrite DirectWrite 是一个文本处理和光栅化引擎,处理高质量文本、字体设置和布局。您可以使用 GDI 或 Direct2D 绘制光栅化文本。 DXGI 它代表 DirectX 图形基础结构。它充当 Direct3D 和图形驱动程序之间的中间层。大多数应用程序通过 Direct3D 间接与 DXGI 通信。 注:Direct2D 和 DirectWrite 随 Windows 7 引入,但也通过平台更新提供给 Windows Vista 和 Windows Server 2008。Direct2D 的优点这里列出了使用 Direct2D API 的一些主要优点: 硬件加速硬件加速是指使用图形处理单元 (GPU) 而不是 CPU 进行图形计算。现代 GPU 针对图形显示进行了优化,因此它们在执行此类任务时效率更高。将更多的工作负载从 CPU 转移到 GPU 通常可以提高性能。 尽管 GDI 为特定任务加速硬件,但其大部分功能都依赖于 CPU。相比之下,Direct2D 基于 Direct3D 构建,并充分利用了 GPU 的硬件速度。如果 GPU 没有 Direct2D 所需的资源,则默认为软件渲染。通常,Direct2D 在大多数情况下在性能方面优于 GDI 和 GDI+。 透明度和抗锯齿Direct2D 完全支持硬件加速的 Alpha 混合,并提供灵活的抗锯齿边缘。GDI 对 Alpha 混合的支持有限。大多数 GDI 应用程序不支持 Alpha 混合,尽管 GDI 在 bitblt 应用程序中确实支持 Alpha 混合。GDI+ 支持透明度,但 CPU 执行 Alpha 混合,因此它不受益于硬件加速。硬件加速的 Alpha 混合也支持抗锯齿。 当连续任务实例化时,别名会作为伪影出现。例如,当将卷曲线转换为像素时,别名可能导致蛀牙。任何减少姓氏这些派生词的方法都被认为与姓氏不兼容。在绘画中,通过将边缘混合到背景中来完成抗锯齿。例如,这里是一个由 GDI 绘制的圆圈和由 Direct2D 绘制的相同圆圈。 ![]() 下图提供了每个圆圈的特写视图。 ![]() 在上面的图像中,左侧的 GDI 绘制的圆圈曲线旁边有黑色像素,而右侧的 Direct2D 渲染的圆圈使用混合来获得平滑的曲线。 GDI 在绘制线条和曲线等几何形状时缺少抗锯齿支持,导致边缘锯齿状。尽管 GDI 可以使用 ClearType 进行文本抗锯齿,但其他 GDI 文本显示为锯齿状,由于字体设置不连贯而影响可读性。尽管 GDI+ 支持抗锯齿,但它依赖于 CPU,导致性能低于 Direct2D。 互操作性对于新程序,建议使用 Direct2D;但是,在必要时它可以与 GDI 配合使用。 矢量图形Direct2D 使用矢量图,它使用数学公式来描述线条和曲线。与栅格图形不同,矢量图形保持维度中性,允许轻松调整大小而不会损失质量。这种可伸缩性使得矢量图形对于适应各种显示器尺寸或屏幕分辨率非常宝贵。 桌面窗口管理器在 Windows Vista 之前,Windows 应用程序通过直接写入视频卡维护的内存缓冲区,将其信息直接显示在屏幕上,跳过每个中间级别,但如果窗口未正确重置,那么这可能会导致视觉错误。例如,当将一个窗口拖到另一个窗口时,如果底部窗口重绘速度不够快,则可能会由于最顶层窗口而导致积压。 ![]() 拖影效应发生是因为在这两种情况下,窗口都引用了内存的同一部分。当上层窗口被拉下时,下层窗口应该被重绘。如果这种重新成像非常慢,就会出现前一张图像中看到的视觉效果。Windows Vista 中引入的桌面窗口管理器 (DWM) 从根本上改变了 Windows 的解释。当 DWM 启用时,Windows 不会直接绘制到显示缓冲区。相反,每个窗口都在一个名为屏幕外表面的屏幕外内存缓冲区中定义。然后 DWM 将这些页面组合起来以在屏幕上创建最终显示。 ![]() 桌面窗口管理器的优点与旧的图形架构相比,桌面窗口管理器 (DWM) 提供了许多优点。
注:但是,需要注意的是,DWM 并非始终启用。某些显卡不符合 DWM 支持的系统要求,用户可以通过“系统属性”控制面板激活它们。因此,您的项目不应完全依赖 DWM 的重绘行为。最好在 DWM 启用时测试您的程序,以确保重绘正常工作。保留模式与立即模式图形 API 分为两种主要类型:保留模式 API 和立即模式 API。Direct2D 属于立即模式类别,而 Windows Presentation Foundation (WPF) 是保留模式 API 的一个示例。 在保留模式 API 中,该方法是一个声明。应用程序使用图形原语(如形状和线条)创建视图。图像库将此视觉图像存储在内存中。绘制帧时,图形库将此视图转换为一系列绘制命令。在后续帧中,库将其情况保持在脑海中。要更改所定义的内容,应用程序会发出命令以更新视图,例如添加或删除形状。然后,库负责相应地重绘更新的模板。 ![]() 相比之下,立即模式 API 以过程方式运行。在每个帧绘制时,应用程序直接发出绘制命令,而图形库不存储帧之间的场景模型。相反,应用程序负责跟踪场景的状态。 ![]() 保留模式 API 通常因其易用性而受到青睐,因为它们处理初始化、状态管理和清理等任务。但是,由于其预定义的场景模型,它们往往不够灵活,并且由于需要通用的场景模型,可能需要更多的内存。相比之下,立即模式 API 提供更大的灵活性,并允许实现有针对性的优化。 我们的第一个 Direct2D 程序现在,让我们创建我们的第一个 Direct2D 程序。这个程序并不奢华;它的主要功能是绘制一个填充窗口客户区的圆圈。尽管如此,这个简单的任务使我们熟悉了许多基本的 Direct2D 概念。 ![]() 以下是 Circle 程序的代码清单。此程序利用了在“管理应用程序状态”主题中介绍的 BaseWindow 类。后续主题将更详细地探讨代码的细节。 编码 说明 上述代码是一个用 C++ 编写的 Windows 应用程序,它使用 Direct2D(一个图形 API)在窗口内绘制一个绿色圆圈。它在 'CreateGraphicsResources' 函数中初始化 Direct2D 资源,在 'WinMain' 函数中创建窗口,并在 'WndProc' 函数中处理窗口消息。圆圈绘制和资源管理在 'OnPaint' 函数中处理,该函数在窗口需要绘制时调用。应用程序进入消息循环以处理用户输入和窗口事件,直到窗口关闭。 D2D1 命名空间D2D1 命名空间是 Direct2D API 中使用的命名空间,它是 Microsoft Windows SDK 的一个组件。它包含用于渲染 2D 图形的类、接口、枚举和结构。Direct2D API 提供了一个高性能、硬件加速的接口,用于在 Windows 平台上渲染 2D 图形,例如形状、文本和图像。 在 D2D1 命名空间中,您会找到表示基本图形元素、渲染上下文和转换矩阵等功能的各种类和结构。使用这些组件,开发人员可以为 Windows 创建视觉丰富和交互式的 2D 图形应用程序。 渲染目标、设备和资源在 Direct2D 中,渲染目标是图形渲染到的对象。它们充当绘图操作的目标。Direct2D 提供不同类型的渲染目标,例如用于直接绘制到窗口的窗口渲染目标、用于离屏绘制的位图渲染目标和用于打印的打印机渲染目标。 Direct2D 中的设备表示负责渲染图形的硬件或软件组件。这些设备管理渲染过程并与底层图形硬件或软件交互。它们提供了一个抽象层,允许应用程序以平台无关的方式渲染图形。 ![]() Direct2D 中的资源是指渲染过程中使用的对象,例如位图、画笔、几何体和效果。这些资源包含渲染所需的数据,通常由 Direct2D 设备创建和管理。适当的资源管理对于 Direct2D 应用程序中高效可靠的图形渲染至关重要。 Direct2D 中的某些资源受益于硬件加速,这意味着它们经过优化以利用底层设备(无论是硬件 GPU 还是软件 CPU)的功能。这些资源被称为设备相关资源,包括画笔和网格。如果与这些资源关联的设备变得不可用,则必须为新设备重新创建它们。 另一方面,有些资源存储在 CPU 内存中,并且独立于所使用的设备。示例包括笔划样式和几何体。与设备相关资源不同,当设备更改时,无需重新创建设备无关资源。 MSDN 文档指定某个功能是设备相关还是设备无关。每种资源类型都由从 ID2D1Resource 创建的接口表示;例如,画笔由 ID2D1Brush 接口表示。 Direct2D 工厂对象Direct2D 工厂对象充当创建 Direct2D 对象和对象的入口点。它负责初始化 Direct2D 环境并管理渲染目标、画笔、几何体和效果等事物。Direct2D 工作区由两个不同的对象组成
渲染目标对象负责创建设备相关资源,例如画笔和位图。 ![]() 工厂对象通常通过 'D2D1CreateFactory' 任务创建,它提供了一个中心位置来配置 Direct2D,包括调试选项和资源创建策略。它还确保 Direct2D 为当前状态正确初始化,使用配置来处理任何必要的服务。 使用 D2D1CreateFactory 函数实例化 Direct2D 工厂对象。 编码 说明 上面代码中的第一个参数是指示需要创建的标志。当使用 D2D1_FACTORY_TYPE_SINGLE_THREADED 标志时,这意味着 Direct2D 调用不会来自多个线程。如果有多个线程运行到 Direct2D,请选择 D2D1_FACTORY_TYPE_MULTI_THREADED。如果您的应用程序只处理来自单个线程的 Direct2D 调用,则选择单线程以提高效率。D2D1CreateFactory 函数的第二个参数接受指向 ID2D1Factory 接口的指针。 建议您在处理第一个 WM_PAINT 消息之前实例化 Direct2D 工作区对象。创建工作区的正确位置是在 WM_CREATE 消息处理程序中。 编码 说明 在上述代码中,在 WM_CREATE 消息处理程序中,代码尝试使用 D2D1CreateFactory 函数和 D2D1_FACTORY_TYPE_SINGLE_THREADED 标志创建 Direct2D 工厂对象,如果创建失败,则返回 -1 表示失败。否则,返回 0。 创建 Direct2D 资源Circle 程序依赖于特定的设备相关资源:
这两种资源都通过 COM 接口表示。
在 Circle 程序中,MainWindow 类将指向这些接口的指针存储为其成员变量。 编码 在以下代码中,我们创建了这两个资源: 编码 说明 'CreateGraphicsResources' 函数初始化渲染所需的图形资源。首先,它检查渲染目标是否尚未设置。否则,它会恢复应用程序窗口的大小、与窗口关联的 Direct2D 渲染目标以及用于绘图的纯色画笔。如果成功,它会计算系统并返回结果。 要为窗口生成渲染目标,请使用 Direct2D 工厂的 ID2D1Factory::CreateHwndRenderTarget 方法。
此外,如果渲染目标已经存在,CreateGraphicsResources 方法将简单地返回 S_OK,而无需任何进一步操作。 使用 Direct2D 绘图现在,创建图形资源后,您就可以开始绘图了。 绘制椭圆Circle 程序执行基本绘图逻辑:
由于渲染目标是一个窗口,因此绘画是响应 WM_PAINT 消息而发生的。这是 Circle 程序的窗口路径。 编码 说明 上述代码中的 'HandleMessage' 函数处理窗口消息。当它收到 WM_PAINT 消息时,它会调用 OnPaint 函数来处理绘画操作。它还会向默认窗口结构发送其他消息以进行进一步处理。 这里是绘制圆圈的代码: 编码 说明 在上述代码中,'OnPaint' 函数处理绘画操作。它首先执行一个绘图对象,如果成功,则开始绘图。它用天蓝色清除渲染目标,然后用所示的画笔填充椭圆。绘制完成后,它检查错误或渲染目标是否需要娱乐。如果是,它将放弃图形处理。最后,图片结束。 ID2D1RenderTarget 接口使用程序的 OnPaint 方法处理所有绘图任务,如下所示:
BeginDraw、Clear 和 FillEllipse 方法不返回任何值。如果创建它们时发生错误,EndDraw 方法的返回值将显示。创建 Direct2D 资源中显示的创建 Direct2D 资源的过程包括 CreateGraphicsResources 方法,该方法负责创建渲染目标和纯色画笔。 绘图命令可以在机器中缓冲,并通过调用 EndDraw 稍后执行。为了确保立即执行任何挂起的渲染操作,您可以使用 ID2D1RenderTarget::Flush 方法。但是,重要的是要注意刷新可能会影响性能。 您正在使用的图形设备可能在程序运行时无法工作。这可能由于多种原因而发生,例如显示分辨率更改或用户移除显示适配器。当设备丢失时,渲染目标和任何关联的设备相关资源将失效。Direct2D EndDraw 方法返回错误代码 D2DERR_RECREATE_TARGET,指示设备丢失。如果遇到此错误代码,重新构建渲染目标和任何设备相关资源非常重要。 要丢弃资源,只需释放与该资源关联的接口。 编码 说明 在上述实现中,'MainWindow' 类的 'DiscardGraphicsResources()' 函数使用安全释放方法释放分配给渲染目标和画笔的 Direct2D 资源的内存,以阻止内存泄漏。 注意:资源创建可能代价高昂,因此最好不要为每个 WM_PAINT 消息重新创建资源。相反,一次创建资源并存储其指针,直到它因设备丢失或不再需要而失效。Direct2D 渲染循环无论您下载什么,您的程序都应该创建一个类似于以下内容的循环。
当渲染目标是窗口时,会发生步骤 2,每个窗口都会收到 WM_PAINT 消息。此循环通过丢弃设备相关对象,然后从循环开始处(步骤 2a)重复来处理设备丢失。 DPI 和设备无关像素要成功开发 Windows 图形应用程序,您需要了解两个相关概念。
让我们探讨 DPI,其中包含对艺术参考的快捷方式。在书法中,字体大小以点为单位测量,一个点是 1/72 英寸。因此,1 磅 = 1/72 英寸。 例如,让我们看一个 24 磅的字体,旨在适合 1/3 英寸(24/72)的字体。然而,字体中的每个字体在现实中都不会正好测量 1/3 英寸高;某些字符(例如 Å)比这个标称高度包含更多的字体。为了正确披露,信息需要额外的空间,称为首行缩进。 假设下图所示的 72 磅线。实线覆盖文本周围的 1 英寸边界框,而虚线表示原点,字母表中的大多数线条在此处相交。总高度的文本包括原点上方(上升)和下方(下降)的区域。 ![]() 在计算机显示器上,由于像素大小可变,测量文本大小很困难。像素大小取决于显示分辨率和显示器的物理尺寸。因此,使用物理英寸作为测量单位是不切实际的,因为英寸和像素之间没有特定的关系。相反,线条是逻辑测量的,72 磅线定义为一逻辑英寸长。然后将这些逻辑英寸转换为像素。历史上,Windows 使用了一个变体,其中一逻辑英寸相当于 96 像素,导致 72 磅字体代表 96 像素,12 磅字体高 16 像素。 此转换因子称为每英寸 96 点 (DPI),尽管更准确地描述为每逻辑英寸 96 像素。由于像素大小不同,在一个显示器上可读的信息在另一个显示器上可能太小,导致用户偏爱更大的信息。Windows 通过允许用户调整 DPI 设置来解决此问题。例如,DPI 为 144 时,72 磅字符的高度为 144 像素。默认 DPI 设置包括 100% (96 DPI)、125% (120 DPI) 和 150% (144 DPI),用户可以访问默认设置。从 Windows 7 开始,DPI 可为每个用户配置。 DWM 缩放当程序忽略 DPI 考量时,使用高 DPI 设置时可能会出现以下几个问题:
桌面窗口管理器 (DWM) 使用一个有用的回退设置,以确保在高 DPI 下与旧设置兼容。如果进程没有 DPI 知识,DWM 会请求整个 UI 匹配 DPI 设置。例如,在 144 DPI 下,UI 会按 150% 更改,包括文本、图像、控件和窗口尺寸。例如,如果程序创建 500 × 500 窗口,它将显示为 750 × 750 像素,并相应地缩放内容。 虽然这种方法确保旧系统在高 DPI 设置下运行良好,但由于窗口绘制后应用的缩放,它引入了一些模糊。 DPI 感知应用程序为了防止 DWM 缩放,程序可以将其自身指定为 DPI 感知,指示 DWM 避免自动 DPI 更改。所有新应用程序都必须包含 DPI 知识,因为它可以提高高 DPI 设置下 UI 的清晰度。 DPI 知识通过应用程序清单进行识别,清单是一个 DLL 或 XML 文件,详细说明了应用程序的特征。通常包含在可执行文件中,清单也可以作为单独的文件存在。它包含重要信息,例如 DLL 依赖项、请求的权限级别和 Windows 版本兼容性。 如果您想为您的系统声明 DPI 知识,请确保清单包含以下信息。 语法 显示的列表仅代表部分显示;Visual Studio 链接器会自动生成其余部分。在 Visual Studio 中按照以下步骤将清单组件添加到您的项目:
当您将应用程序指定为 DPI 感知时,您指示桌面窗口管理器 (DWM) 根据 DPI 设置调整您的应用程序窗口大小。因此,无论用户的 DPI 设置如何,创建 500 × 500 像素的窗口都将保持该分辨率。 GDI 和 DPIGDI 图像是基于像素的。这意味着如果您的应用程序指定为 DPI 感知,并且您指示 GDI 绘制一个 200 × 100 像素的矩形,则生成的矩形将与屏幕上显示的完全相同,但 GDI 会根据当前 DPI 设置缩放更大的字符。例如,如果您创建 72 磅字体,它在 96 DPI 下将是 96 像素,但在 144 DPI 下将显示 144 像素。下面是使用 GDI 以 144 DPI 渲染的 72 磅字体示例。 ![]() 当您的应用程序是 DPI 感知并使用 GDI 进行绘图时,请确保所有绘图坐标都按 DPI 设置进行缩放。 Direct2D 和 DPI在 Direct2D 中,相应的缩放会自动根据 DPI 设置进行。测量单位称为设备无关像素 (DIPs),其中一个 DIP 等于 1/96 逻辑英寸。Direct2D 中的每个绘图函数都以 DIP 为单位定义,然后适应标准 DPI 设置。
让我们通过一个例子来理解。例如,如果用户的 DPI 设置保持在 144 DPI,并且您强制 Direct2D 创建一个 200 × 100 的矩形,则生成的矩形将占据 300 × 150 的物理像素。此外,DirectWrite 以 DIP 而不是点来测量字体大小。要创建 12 磅字体,您必须指定 16 的 DIP(因为 12 磅是 1/6 逻辑英寸,相当于 96/6 的 DIP)。Direct2D 会在屏幕上渲染文本时自动将 DIP 转换为物理像素。采用此方法可确保测量数据和成像实践的准确性,无论当前的 DPI 设置如何。 然而,重要的是要注意鼠标和窗口坐标仍然以物理像素给出,而不是 DIP。例如,在处理 WM_LBUTTONDOWN 消息时,鼠标按下事件的状态以物理像素显示。要在该区域绘制一个点,您必须首先将像素坐标转换为 DIP。 如何将物理像素转换为 DIP?将物理像素转换为 DIP 时,请调整像素缩放以匹配显示器的 DPI 设置。这种灵活性可确保在 DPI 配置之间实现一致的转换。 将物理像素转换为 DIP 的公式是 其中
例如,如果您有一个 DPI 设置为 144 的显示器,并且您想将 288 物理像素的测量值转换为 DIP 因此,在 DPI 设置为 144 的显示器上,288 物理像素相当于 2 DIP。 默认 DPI 值 USER_DEFAULT_SCREEN_DPI 为 96。要计算缩放因子,请将当前 DPI 值除以 USER_DEFAULT_SCREEN_DPI。 调用 GetDpiForWindow 函数以获取 DPI 设置。DPI 值作为浮点数返回。相应地计算两个轴的缩放因子。 编码 说明 上述代码根据从窗口获取的 DPI 值初始化全局缩放因子 'g_DPIScale'。它提供了两个二维函数 'PixelsToDipsX' 和 'PixelsToDipsY',它们使用计算出的缩放因子 'g_DPIScale' 将像素坐标转换为设备无关像素 (DIPs)。 注意:建议桌面应用程序使用 GetDpiForWindow,通用 Windows 平台 (UWP) 应用程序使用 DisplayInformation::LogicalDpi。尽管可以使用 SetProcessDpiAwarenessContext 配置默认 DPI 感知,但不鼓励这样做。一旦在您的项目中创建了窗口 (HWND),更改 DPI 感知模式将无济于事。如果您需要以编程方式配置进程默认 DPI 感知模式,请务必在创建任何 HWND 之前调用相应的 API。调整渲染目标大小当窗口大小改变时,您需要相应地改变渲染值。通常,您还需要更新布局并重绘窗口。以下规则说明了这些操作。 编码 说明 上述代码片段描述了 'MainWindow' 类中的一个名为 'Resize()' 的方法。它检查渲染目标 'pRenderTarget' 是否处于活动状态,检索窗口客户端区域的尺寸,相应地调整渲染目标的大小,重新计算布局,并使窗口客户端区域失效以触发重绘。此方法确保当窗口定义随着大小改变而适当改变时,保持相同的输出图像。 GetClientRect 函数以物理像素而不是 DIP 检索客户端区域的更新大小。ID2D1HwndRenderTarget::Resize 方法然后调整渲染目标的大小,也以像素指定。为了触发重绘,InvalidateRect 函数标记整个客户端区域以进行更新。随着窗口形状的改变,重新计算绘制对象的位置是很常见的。例如,在圆形设计中,需要调整圆的半径和焦点。 编码 说明 上面的代码计算了在 Direct2D 渲染目标上绘制的椭圆的顺序。此渲染采用目标形状,计算中心坐标和半径,并使用这些参数创建椭圆。 ID2D1RenderTarget::GetSize 方法检索渲染目标以 DIP(设备无关像素)为单位的大小,这是配置计算的适当单位。 相比之下,ID2D1RenderTarget::GetPixelSize 以物理像素提供大小。尽管后者与从 HWND 渲染值的 GetClientRect 派生的推理相匹配,但应注意绘图操作发生在 DIP 中,而不是物理像素中。 在 Direct2D 中使用颜色Direct2D 使用 RGB 颜色模型,其中通过混合不同强度的红色、绿色和蓝色来生成不同的颜色。 此外,第四个参数 Alpha 决定了像素的清晰度。在 Direct2D 中,每个这些对象都有 0.0 到 1.0 的浮点值。对于颜色分量,此范围表示颜色强度,而对于 Alpha,0.0 表示绝对透明,1.0 表示完全透明。下表显示了在 100% 强度下各种组合获得的颜色。
从 0 到 1 的值会在这些原色中产生各种深浅。Direct2D 使用 D2D1_COLOR_F 结构来表示颜色。例如,以下代码定义了洋红色。 编码 说明 上述代码片段创建了一个 D2D1_COLOR_F 结构,表示洋红色,其中红色和蓝色的强度值最大,绿色的强度值最小。 您也可以使用 D2D1::ColorF 类定义颜色,该类继承自 D2D1_COLOR_F 结构。 编码 Alpha 混合Alpha 混合通过将前景颜色与背景颜色混合来组合半透明区域,采用特定公式: 在上述公式中,Cb 代表背景色,Cf 是前景色,af 是前景色的 Alpha 值。此计算单独应用于每个颜色分量。例如,如果前景色为 (R = 1.0, G = 0.4, B = 0.0),Alpha 值为 0.6,背景色为 (R = 0.0, G = 0.5, B = 1.0),则生成的 Alpha 混合颜色将是: 像素格式D2D1_COLOR_F 结构没有指定像素在内存中的存储方式,这通常不是问题,因为 Direct2D 在内部管理颜色信息到像素的转换。但是,当直接操作内存位图或将 Direct2D 与 Direct3D 或 GDI 集成时,了解像素格式变得很重要。DXGI_FORMAT 枚举列出了各种像素格式,尽管只有一部分与 Direct2D 相关,因为其他主要由 Direct3D 使用。
下图描绘了 BGRA 像素排列。 ![]() 使用 ID2D1RenderTarget::GetPixelFormat 函数获取渲染目标的像素格式。需要注意的是,渲染目标的像素格式可能与显示分辨率不同。例如,显示器可以配置为 16 位颜色,而渲染目标使用 32 位颜色。 Alpha 模式渲染目标还包含 Alpha 模式,用于确定 Alpha 值的处理方式。
使用以下示例来考虑直接 Alpha 和预乘 Alpha 之间的区别:假设我们的目标是 50% 全强度(100%)的纯红色,它在 Direct2D 中显示为 (1, 0, 0, 0.5)。当使用直接 Alpha 并捕获 8 位颜色分量时,像素的红色部分保持为 0xFF。但是,如果 Alpha 首先相乘,则血液分量会减少 50% 到 0x80。 值得注意的是,D2D1_COLOR_F 数据类型使用恒定的直接 Alpha 来表示颜色,而 Direct2D 会根据需要将像素转换为预乘 Alpha 格式。如果您的程序不使用 Alpha 混合,请使用它;考虑创建具有 D2D1_ALPHA_MODE_IGNORE 模式的渲染目标,这可以提高性能,因为 Direct2D 可以跳过 Alpha 计算。 如何在 Direct2D 中应用变换?在上一节中,我们探讨了使用 Direct2D 绘图。我们了解到 ID2D1RenderTarget::FillEllipse 方法用于绘制与 x 轴和 y 轴平行的椭圆。但是,请考虑您必须以一定角度绘制椭圆的情况。 ![]() 利用变换,您可以通过各种操作修改形状,包括:
变换是一种数学运算,它将一组点重新排列到新的顺序。例如,考虑下图,它显示了点 P3 周围的扭曲矩形。反转后,点 P1 保持不变,点 P1',点 P2 保持不变,点 P2',点 P3。 ![]() 变换通过 Direct2D 中的矩阵完成,尽管它们的使用不需要对矩阵计算有深入的理解。如果您有兴趣深入了解数学方面,请参阅“附录:形状转换”部分。要在 Direct2D 中应用变换,请传递 ID2D1RenderTarget::SetTransform 方法,该方法接受定义变换的 D2D1_MATRIX_3X2_F 结构。D2D1::Matrix3x2F 类中的方法可以初始化为此框架,该框架提供用于为变量构造矩阵的静态方法。
作为示例,以下代码在坐标 (100, 100) 周围执行 20 度旋转。 编码 变换对所有后续绘图操作生效,直到另一次 SetTransform 调用。要重新排序当前变换,请使用 SetTransform 和单位矩阵,可以使用 Matrix3x2F::Identity 函数创建单位矩阵。 编码 绘制钟表指针让我们通过将我们的 Circle 程序变成一个模拟时钟来实现变换。它包括用于表示时针的字母。 ![]() 我们不是直接计算线条的坐标,而是可以计算一个角度,然后应用旋转变换。下面的程序演示了如何绘制一个钟表指针。参数 fAngle 表示指针的角度,以度为单位指定。 编码 说明 上述代码表示从钟面到末端的一条垂直线。通过对表示钟表中心的椭圆中心应用旋转变换来获得循环。 ![]() 以下代码演示了整个钟面的绘制。 编码 说明 上述程序显示了钟面。它首先用天蓝色清除渲染目标。然后,它填充并吸收了手表的圆柱形表面。然后,它根据当前时间显示时针和分针。最后,它使用 DrawClockHand 函数绘制时针和分针并返回标识变量。 组合变换创建两个或更多矩阵可以组合四个基本变量。例如,以下代码将旋转与平移相结合。 编码 说明 在上述代码中,Matrix3x2F 类提供了用于矩阵乘法的 operator*(),其中顺序很重要。应用变换 (M × N) 意味着“先应用 M,然后应用 N”。例如,以下演示了旋转后平移: ![]() 请参阅下面的程序以了解此转换。 编码 现在,让我们将其与反向变换进行对比:先平移后旋转。 ![]() 旋转以原始矩形的中心为中心。以下是此转换的代码。 编码 您可以看到矩阵保持相同,但操作顺序已更改。这是因为矩阵乘法是非交换的:M × N ≠ N × M。 模块 4:用户输入本节将解释如何处理鼠标和键盘输入。在您的应用程序中实现用户交互功能。此处指出了我们将讨论的术语: 鼠标输入鼠标可以与 Windows 配合使用。它配备了五个按钮:三个标准按钮(左、中和右)和两个额外的按钮,标记为 XBUTTON1 和 XBUTTON2。 在 Windows 中,大多数鼠标通常都有左键和右键。左键有多种用途,包括拖动、选择和指向。右键通常用于显示上下文菜单。某些鼠标的左键和右键之间有一个滚轮。中间键是滚轮,它也可能可点击,具体取决于鼠标。 响应鼠标点击如果鼠标指针位于客户区上方时单击鼠标按钮,则会在窗口中显示以下消息之一。
坐标每条消息都包含鼠标指针在 lParam 参数中的 x 和 y 坐标。x 坐标包含在 lParam 的最低 16 位中,而 y 坐标包含在接下来的 16 位中。要从 lParam 中提取坐标,请使用 GET_X_LPARAM 和 GET_Y_LPARAM 宏。 编码 说明 在上述代码中,GET_X_LPARAM 和 GET_Y_LPARAM 宏通常在 Windows 编程中使用,用于从 LPARAM 值中提取 x 和 y 坐标,LPARAM 值是一个 32 位值,包含消息发生时鼠标光标的坐标。 双击默认情况下,双击警报不会发送到窗口。注册窗口类时,在 WNDCLASS 结构中设置 CS_DBLCLKS 标志以接受双击。 编码 说明 上述代码初始化了一个名为 'wc' 的 'WNDCLASS' 结构,并将其 'style' 成员设置为 'CS_DBLCLKS',表示该窗口类应生成双击消息。在根据需要设置其他结构成员后,使用 'RegisterClass' 函数注册该类。 如果设置了 CS_DBLCLKS 标志,双击通知将显示在窗口中。双击时,将出现一个名为“DBLCLK”的窗口消息。例如,当左键双击时,会出现以下消息:
本质上,WM_LBUTTONDBLCLK 消息取代了通常会创建的第二个 WM_LBUTTONDOWN 消息。为中键、右键和 XBUTTON 按钮定义了等效消息。 非客户区鼠标消息当鼠标事件发生在窗口的非客户区时,会定义一组不同的消息。这些通信的名称中会出现“NC”首字母。例如,WM_LBUTTONDOWN 的非客户区对应项是 WM_NCLBUTTONDOWN。 鼠标移动当鼠标移动时,Windows 会发送 WM_MOUSEMOVE 通知。默认情况下,WM_MOUSEMOVE 导航到包含光标的窗口。下一节将解释捕获鼠标可以改变此行为。 WM_MOUSEMOVE 消息的参数与鼠标点击消息的参数相同。x 坐标包含在 lParam 的最低 16 位中,而 y 坐标包含在接下来的 16 位中。要从 lParam 中提取坐标,请使用 GET_X_LPARAM 和 GET_Y_LPARAM 宏。wParam 参数包含表示 SHIFT、CTRL 键和其他鼠标按钮状态的标志的位或。下面的代码使用 lParam 获取鼠标坐标。 编码 鼠标在窗口外移动如果鼠标超出客户端区域的边缘,WM_MOUSEMOVE 消息将自动停止发送到窗口。但是,您可能需要跟踪鼠标位置以执行一些后续任务。如下图所示,绘图应用程序可以,例如,允许用户将选择矩形拖动到窗口边缘之外。 左键按下按照以下步骤处理左键按下消息
编码 说明 上面的代码是当窗口中按下鼠标左键时的事件处理程序。它捕获鼠标,将椭圆的位置设置为鼠标光标位置,将椭圆半径设置为 1,并使窗口失效以触发重绘。 鼠标移动验证鼠标左键是否按下以获取鼠标移动消息。如果是,则重绘窗口并重新计算椭圆。Direct2D 中的椭圆由其 x 和 y 半径以及中心点定义。我们想要绘制一个适合边界框的椭圆。 椭圆的宽度、高度和位置由鼠标按下点 (ptMouse) 和当前光标位置 (x, y) 确定。因此,需要一些算术部分来计算椭圆的高度、宽度和位置。下面的代码在重新计算椭圆后调用 InvalidateRect 来重新绘制窗口。 编码 说明 上面的代码是鼠标移动的事件处理程序。它检查鼠标左键是否按下。如果是,它会根据当前鼠标位置(“pixelX”、“pixelY”)与先前位置(“ptMouse”)之间的差异计算椭圆的宽度和高度。然后,它使用计算出的参数创建一个椭圆,并使窗口失效以进行重绘。 左键抬起要获取左键抬起消息,请调用 ReleaseCapture 释放鼠标捕获。 编码 说明 上面的代码定义了 'MainWindow' 类中的成员函数 'OnLButtonUp()'。调用时,它会释放鼠标捕获,允许其他窗口或控件捕获鼠标输入事件。 鼠标操作在本节中,我们将看到可以使用鼠标执行的其他重要鼠标操作。 拖动 UI 元素如果您的用户界面允许用户拖动 UI 组件,则应在鼠标按下消息处理程序中使用 DragDetect 作为附加方法。如果用户做出旨在被视为拖动的鼠标手势,则 DragDetect 方法返回 TRUE。下面的代码演示了此功能的使用。 编码 说明 上面的代码处理鼠标左键按下事件。它根据按钮按下后鼠标的移动情况检测是否应该开始拖动操作。如果检测到拖动操作,它将启动拖动。 限制光标有时您可能希望将光标限制在客户端区域或仅仅是一个部分。ClipCursor 函数将光标的移动限制在指定的矩形内。由于此矩形以屏幕坐标而不是客户端坐标提供,因此屏幕的左上角由点 (0, 0) 表示。调用 ClientToScreen 方法将客户端坐标转换为屏幕坐标。使用以下代码,光标被限制在窗口的客户端区域。 编码 说明 上面的代码获取窗口客户端区域的尺寸,将其转换为屏幕坐标,然后将光标移动限制在该区域。 鼠标跟踪功能:悬停和离开尽管这些功能默认未启用,但还有两个鼠标消息可能在特定情况下有用
使用 TrackMouseEvent 方法启用这些消息。 编码 说明 上面的代码为指定窗口('hwnd')设置了悬停和离开事件的跟踪。它使用必要的参数(例如窗口句柄、悬停和离开事件的标志以及默认悬停时间)初始化 'TRACKMOUSEEVENT' 结构。最后,它调用 'TrackMouseEvent' 并开始跟踪鼠标事件。 您可以使用这个小型实用程序类来处理鼠标跟踪事件。 编码 说明 上面给出的代码启用了一个窗口的鼠标悬停和离开事件跟踪。当调用 'OnMouseMove' 方法时,如果尚未启用鼠标跟踪,它会设置鼠标跟踪。'Reset' 方法用于重置跟踪状态。'TRACKMOUSEEVENT' 结构用于指定跟踪参数。 以下示例演示了如何在窗口过程中使用此课程。 编码 说明 上面的代码是窗口类的消息处理程序的示例。它处理与鼠标移动('WM_MOUSEMOVE')、鼠标离开窗口('WM_MOUSELEAVE')和鼠标悬停在窗口上('WM_MOUSEHOVER')相关的消息。它相应地启动或重置鼠标跟踪,并返回 0 表示消息已处理。如果消息不是已处理的鼠标事件之一,它会调用默认窗口过程 'DefWindowProc' 来处理该消息。 如果您不需要鼠标跟踪事件,请将其保持禁用状态,因为它们需要系统进行额外的处理。为了彻底起见,这里有一个方法可以询问系统默认的悬停超时时间。 编码 说明 上面的代码检索鼠标指针在 UI 元素上方保持静止多长时间(以毫秒为单位)才会触发悬停事件。它使用带有 'SPI_GETMOUSEHOVERTIME' 参数的 'SystemParametersInfo' 函数从系统设置中获取此值。如果成功,它将返回悬停时间;否则,它将返回 0。 鼠标滚轮下面给出的函数检查是否存在鼠标滚轮。 编码 说明 上面的代码检查系统是否有鼠标滚轮,如果有则返回 true;如果没有则返回 false。当用户旋转鼠标滚轮时,WM_MOUSEWHEEL 消息将发送到具有焦点的窗口。此消息的 wParam 参数包含 delta,一个整数,指示滚轮旋转的距离。delta 以任意单位测量,其中 120 单位是完成一个“动作”所需的旋转次数。 delta 的两个符号表示旋转方向。
delta 值与附加标志一起放置在 wParam 中。使用 GET_WHEEL_DELTA_WPARAM 宏访问 delta 值。 编码 说明 这行代码使用 'GET_WHEEL_DELTA_WPARAM' 宏从 'wParam' 参数中提取鼠标滚轮移动量。值 'delta' 表示鼠标滚轮滚动的距离和方向。 键盘输入键盘用于多种类型的命令,包括
注意:值得注意的是,从键盘按下字母 A 不仅仅算作 A;例如,它可以是 a、a 或 a。按住 ALT 键会使击键变为 ALT+A,系统将其解释为命令而不是字符。键码键码是指分配给键盘上每个键的数字。计算机系统使用这些代码来识别按下了哪些键或释放了哪些键。在软件应用程序、游戏、操作系统和设备驱动程序中处理键盘输入需要键码。键码可能因平台、操作系统和所使用的编程语言而异。例如 虚拟键码在 Windows 编程中,虚拟键码表示键盘上的键。此代码在 Windows API 中定义,通常用于消息处理函数,例如 WM_KEYDOWN 和 WM_KEYUP。例如,左箭头键的虚拟键地址是 VK_LEFT (0x25)。其他示例是 Enter 键的 VK_RETURN,空格键的 VK_SPACE 等。 ASCII 码它使用 7 或 8 位表示字符和符号。键盘上的许多可打印字符都有相应的 ASCII 码。例如,字母“A”的 ASCII 码是 65,“B”是 66,“1”是 49,“2”是 50 等。 扫描码扫描码是键盘控制器在按下或释放键时生成的硬件特定代码。它们被发送到计算机处理器,然后由操作系统翻译成键码。扫描码通常在比虚拟键码更低的级别实现,并直接从键盘硬件读取。它们可能因键盘和设置而异。 UnicodeUnicode 是一种字符编码标准,它为全球书写系统中使用的字符和符号分配唯一的数字值。虽然与键盘输入没有直接关系,但 Unicode 通常用于软件开发中,以表示和处理来自键盘的文本输入。 按键按下和按键抬起消息当您按下某个键时,以下消息之一将出现在具有键盘焦点的窗口中。
系统键是启动系统命令的击键,如 WM_SYSKEYDOWN 消息所示。系统键有两种类别
F10 键激活打开窗口的菜单栏。其他几个 ALT 键调用系统命令。例如,ALT + TAB 键使您能够切换到新窗口。但是,某些 ALT 键没有任何作用。 字符消息TranslateMessage 函数将击键转换为字符。此功能查找按下键消息并将其转换为字符。TranslateMessage 函数为每个生成的字符向窗口的消息队列添加 WM_CHAR 或 WM_SYSCHAR 消息。UTF-16 字符包含在消息的 wParam 字段中。 下面的代码显示了调试器的主要键盘消息。尝试使用各种键盘组合来查看出现的消息。 编码 说明 上面的代码在 Windows 应用程序中定义了一个窗口过程('WindowProc')。该过程接收发送到由 'hwnd' 标识的窗口的消息('uMsg')。根据接收到的消息类型,会执行不同的操作。例如,当按下某个键('WM_KEYDOWN')时,代码会向调试输出记录一条消息,指示按下了哪个键。同样,其他消息,如 'WM_SYSKEYDOWN'、'WM_CHAR' 等,也会相应地处理。最后,如果消息不是专门处理的消息之一,则会调用默认窗口过程('DefWindowProc')来处理它。 键盘消息事件触发键盘提示。换句话说,当发生任何值得注意的事情,例如按下某个键时,您会收到一条消息,通知您发生了什么。 但是,您也可以随时使用 GetKeyState 方法检查某个键的状态。虚拟键码发送到 GetKeyState 消息,该消息返回一组位标志。确定该键是否在该时刻被按下的位标志位于 0x8000。 编码 说明 上面的代码检查 ALT 键当前是否按下。如果是,则条件 'GetKeyState(VK_MENU) & 0x8000' 评估为 true。 大多数键盘都有左 ALT 键和右 ALT 键。前面的示例检查它们是否被按下。此外,您可以使用 GetKeyState 识别 CTRL、SHIFT 和 ALT 键的左侧和右侧实例。例如,下面的代码检查正确的 ALT 键是否被按下。 编码 说明 上面给出的代码行检查右 Alt 键当前是否被按下。如果是,则条件 'GetKeyState(VK_RMENU) & 0x8000' 评估为 true,并且执行花括号内的代码,表示右 Alt 键被按下。 GetKeyState 在每个消息排队时提供键盘的快照。它提供用户按下鼠标按钮时的精确瞬间的键盘状态,例如,如果队列中的前一个消息是 WM_LBUTTONDOWN。GetKeyState 还会忽略发送到另一个应用程序的键盘输入,因为它依赖于您的消息队列。如果用户切换到另一个应用程序,它还会忽略发送到该应用程序的任何按键。 加速器表键盘快捷键的集合称为加速器表。每个快捷键都有一个定义,由以下内容提供
加速器表在应用程序资源列表中通过表本身的数字标识唯一标识。现在,让我们为基本的绘图应用程序构建一个加速器表。此程序将具有两种模式:绘图模式和选择模式。用户可以在绘图模式下绘制形状。用户可以在选择模式下选择形状。我们希望为此软件建立以下键盘快捷键。 首先,我们将为表和应用程序命令定义数字标识符。这些值是任意的。然后,我们将通过在头文件中声明它们来为标识符分配符号常量。例如 编码 说明 上面编写的代码定义了 Windows 应用程序中加速器表和菜单项的常量值。具体来说
编码 说明 上面的代码定义了一个名为 IDR_ACCEL1 的加速器表,它将键盘快捷键与特定的命令或操作关联起来
花括号指定加速器快捷键。每个快捷键都有以下条目。
注意:如果您使用 ASCII 字符作为快捷键,则小写字符的快捷键将与大写字符的快捷键不同。例如,输入“a”可能导致与输入“A”不同的命令。通常最好使用虚拟键码作为快捷键而不是 ASCII 字符,因为这样做可能会误导用户。加载加速器表Windows 编程中的 LoadAccelerators 方法允许您加载加速器表。加速器表资源的名称或 ID 和应用程序实例句柄(通常通过 GetModuleHandle(NULL) 获取)作为输入发送到此过程。以下是可能的用法示例 编码 说明 上面的代码使用 'LoadAccelerators' 函数加载一个加速器表资源。如果成功,它将返回一个加速器表的句柄('hAccelTable')。如果加载失败('hAccelTable' 为 NULL),则应实现错误处理机制。 将击键转换为命令WM_COMMAND 消息是使用加速器表从击键生成的。 命令的数字标识包含在 WM_COMMAND 的 wParam 参数中。例如,当使用前面显示的表时,击键 CTRL+M 会导致 WM_COMMAND 消息,其值为 ID_TOGGLE_MODE。为此,请将消息循环修改为如下所示 编码 说明 上面的代码监听消息,翻译键盘加速器,并将消息分派到其相应的窗口过程,直到没有更多消息。 绘图应用程序可以以下列方式响应 WM_COMMAND 消息 编码 说明 上面的代码处理来自窗口菜单的命令。它检查命令的 ID 并相应地设置绘图模式。如果触发了切换模式命令,它会在绘图模式和选择模式之间切换。 设置光标图像光标是一个小图像,显示鼠标或其他指向设备的位置。几个应用程序用于更改光标图像以向用户提供反馈。然而,它不是强制性的;它为应用程序提供了超级功能。 系统光标是 Windows 附带的默认光标图片集合。这些包括沙漏(现在是旋转的圆圈)、手形、箭头、I 形光标和其他对象。本节介绍系统光标的使用。 通过调整 WNDCLASS 或 WNDCLASSEX 结构的 hCursor 成员,您可以将光标与窗口类关联起来。如果未关联,则箭头将作为默认光标。 当鼠标经过窗口时(除非另一个窗口捕获了鼠标),窗口会收到 WM_SETCURSOR 消息。此时,会发生以下情况之一
程序执行以下操作来设置光标
如果应用程序将 WM_SETCURSOR 发送给 DefWindowProc 方法,则该方法使用以下过程设置光标图像
可以使用 LoadCursor 函数加载系统光标之一或来自资源的自定义光标。下面的示例演示如何将光标设置为预定义的系统链接选择光标。 编码 说明 上面的代码加载名为“IDC_HAND”的光标资源,将其分配给句柄变量“hCursor”,并将其设置为当前光标。 除非您拦截 WM_SETCURSOR 消息并再次重置光标,否则光标图像会在每次鼠标移动后重置。以下是如何在代码中处理 WM_SETCURSOR 的示例。 编码 说明 上面的代码检查消息是否与为客户端区域设置光标有关,然后使用句柄“hCursor”设置光标并返回“TRUE”。 用户输入:扩展示例用户除了可以绘制椭圆外,还可以选择、移动和删除各种颜色的椭圆。但是,为了保持简单的用户界面,应用程序不允许用户选择椭圆颜色。相反,计算机自动循环显示预设的一组颜色。除了椭圆之外,应用程序不支持任何其他形状。不过,它仍然是一个有用的模型。本节将只讨论主要功能。 在程序中,椭圆由一个结构表示,该结构包含颜色 (D2D1_COLOR_F) 和椭圆数据 (D2D1_ELLIPSE)。该结构还定义了两种方法:一种用于绘制椭圆,另一种用于命中检测。 编码 说明 上面的代码定义了一个结构 'MyEllipse',表示一个带有位置、大小和颜色等属性的椭圆。它有一个 'Draw' 方法,用于在 Direct2D 渲染目标上绘制椭圆,用指定的颜色填充它并绘制其轮廓。'HitTest' 方法检查给定点 '(x, y)' 是否在椭圆内部或边界上,如果是则返回 'TRUE',否则返回 'FALSE'。 结论使用 Win32 和 COM API 在 C++ 中开发桌面程序为 Windows 应用程序提供了强大的功能。通过利用这些 API,开发人员可以对系统资源进行低级控制,并访问强大的 COM 对象。虽然这种方法需要理解复杂的细节,但它能够根据特定要求实现高效且可定制的软件解决方案,从而增强用户体验和功能。 |
我们请求您订阅我们的新闻通讯以获取最新更新。