- 示例工程
【免费下载链接】Windows-classic-samples
This repo contains samples that demonstrate the API used in Windows classic desktop applications.
本文以 Windows-classic-samples 仓库中的 Direct2DHelloWorld 示例 README 为骨架,结合其完整源码逐行剖析,说明如何用 C++ 通过 Direct2D 与 DirectWrite 在 Win32 窗口上渲染出 "Hello, World!" 文本。读完本文,你将掌握 Direct2D 应用的标准骨架:设备无关资源与设备相关资源的创建与销毁、渲染目标的建立、文本格式的定义、窗口消息驱动的绘制流程,以及 DPI 感知清单的声明方式,可直接迁移到自己的桌面图形应用中。
示例概览:一个最小可用的 Direct2D 应用
该示例的目标非常朴素:创建一个 Win32 窗口,在白色背景上居中以黑色 Verdana 字体绘制一行文字 "Hello, World!"。它是 multimedia/Direct2D 目录下众多 Direct2D 示例(如 SimpleDirect2DApplication、TextAnimationSample 等)中最基础的一个,恰好完整覆盖了 Direct2D 应用的"最小闭环":
- 创建Direct2D 工厂(
ID2D1Factory)——设备无关资源; - 创建DirectWrite 工厂(
IDWriteFactory)与文本格式(IDWriteTextFormat)——设备无关资源; - 基于 HWND 创建渲染目标(
ID2D1HwndRenderTarget)与纯色画刷(ID2D1SolidColorBrush)——设备相关资源; - 通过
DrawText完成一次绘制,并在WM_PAINT、WM_SIZE、WM_DISPLAYCHANGE等消息驱动下持续更新画面; - 通过内嵌清单声明 DPI 感知,保证窗口尺寸与文本在系统 DPI 下不失真。
示例为纯 C++ 实现,不依赖任何第三方库,链接库仅为系统自带的d2d1.lib、dwrite.lib等,是学习 Direct2D 编程模型的最佳起点。
文件清单与各自职责
示例目录(Samples/Win7Samples/multimedia/Direct2D/Direct2DHelloWorld/)包含 6 个文件,README 已逐一说明其用途:
| 文件 | 职责 |
|---|---|
| DeclareDPIAware.manifest | 声明示例应用程序为 DPI-aware(感知系统 DPI)的清单文件 |
| Direct2DHelloWorld.cpp | 包含应用程序入口点(WinMain)与DemoApp类的全部实现 |
| Direct2DHelloWorld.h | DemoApp类的头文件,含类声明、SafeRelease等辅助宏 |
| Direct2DHelloWorld.sln | 解决方案文件(Visual Studio 2008 格式,Format Version 10.00) |
| Direct2DHelloWorld.vcproj | 示例的项目文件,定义配置、链接库与清单嵌入方式 |
工程文件本身是 Visual C++ 项目(ProjectType="Visual C++",Version="9.00"),同时支持Win32与x64两个平台,并预置了Debug / Release四种组合配置(Direct2DHelloWorld.vcproj),解决方案文件也对应映射了这四种配置(Direct2DHelloWorld.sln)。
前置条件
根据 README 的 Prerequisites 一节,构建并运行该示例需要:
- Microsoft Windows 7(Direct2D 原生支持的系统起点,此后各版本 Windows 亦兼容);
- Windows SDK for Windows 7 与 .NET Framework 3.5 Service Pack 1,用于提供
d2d1.h、dwrite.h等头文件与d2d1.lib、dwrite.lib等导入库。
仓库根 README.md 也补充说明:这些示例大多面向 Windows 7、Windows 8.1 与 Windows Server 2012 R2 编写,多数情况下在更高版本系统上同样可以运行;如果示例无法按预期工作,很可能是平台、版本或特定 SKU 的限制。对于 Direct2D 而言,从 Windows 7 到 Windows 11 的桌面系统均可使用。
两种构建方式
README 给出了命令行与 Visual Studio 两种官方构建路径,以下如实保留并补充细节。
方式一:命令行构建
- 打开“命令提示符”窗口,切换到示例目录(即本仓库的
Samples/Win7Samples/multimedia/Direct2D/Direct2DHelloWorld/); - 执行
msbuild Direct2DHelloWorld.sln。
需要注意的是,msbuild需来自已安装的 .NET Framework 3.5 或 Visual Studio 工具链;在后续版本的 Visual Studio 中,也可使用“开发者命令提示符”(Developer Command Prompt)来调用。
方式二:Visual Studio 2008(推荐方式)
- 打开 Windows 资源管理器,定位到示例目录;
- 双击
.sln解决方案文件的图标,在 Visual Studio 中打开; - 在“生成”(Build)菜单中点击“生成解决方案”(Build Solution)。
生成的应用程序默认位于工程的\Debug或\Release目录下(Direct2DHelloWorld.vcproj 中OutputDirectory指定为Debug/Release,x64 配置则输出到$(PlatformName)\$(ConfigurationName))。双击运行生成的Direct2DHelloWorld.exe,即可看到白底居中的 "Hello, World!" 文本。
程序架构:DemoApp 类与资源生命周期
示例的 UI 逻辑全部封装在DemoApp类中。从 Direct2DHelloWorld.h 的类声明可以看到,类成员按 Direct2D 经典的"设备无关 / 设备相关"两类资源组织:
class DemoApp { public: DemoApp(); ~DemoApp(); HRESULT Initialize(); void RunMessageLoop(); private: HRESULT CreateDeviceIndependentResources(); HRESULT CreateDeviceResources(); void DiscardDeviceResources(); HRESULT OnRender(); void OnResize(UINT width, UINT height); static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); private: HWND m_hwnd; ID2D1Factory *m_pD2DFactory; // Direct2D 工厂(设备无关) IWICImagingFactory *m_pWICFactory; // WIC 图像工厂(声明于头文件,本示例未实际创建) IDWriteFactory *m_pDWriteFactory; // DirectWrite 工厂(设备无关) ID2D1HwndRenderTarget *m_pRenderTarget; // 渲染目标(设备相关) IDWriteTextFormat *m_pTextFormat; // 文本格式(设备无关) ID2D1SolidColorBrush *m_pBlackBrush; // 黑色纯色画刷(设备相关) };DemoApp构造函数将全部指针初始化为NULL(Direct2DHelloWorld.cpp),析构函数则通过SafeRelease依次释放所有接口(Direct2DHelloWorld.cpp)。
SafeRelease是定义在头文件中的模板辅助函数(Direct2DHelloWorld.h):先判空,再调用Release()并将指针置NULL,避免重复释放。这是所有 COM 风格接口的规范释放模式。
入口点:WinMain
程序从WinMain开始(Direct2DHelloWorld.cpp):
int WINAPI WinMain(...) { // 即使 HeapSetInformation 失败也继续运行 HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0); if (SUCCEEDED(CoInitialize(NULL))) { { DemoApp app; if (SUCCEEDED(app.Initialize())) { app.RunMessageLoop(); } } CoUninitialize(); } return 0; }入口流程有三件事值得注意:
HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0):启用堆损坏自动终止,增强健壮性;代码注释明确说明即使失败也忽略返回值继续运行;CoInitialize(NULL):Direct2D/DirectWrite 接口基于 COM,必须在单线程上初始化 COM 后才可使用,结束前用CoUninitialize配对释放;DemoApp被定义在局部作用域中,确保其析构(即资源释放)发生在CoUninitialize之前,顺序正确。
设备无关资源的创建(CreateDeviceIndependentResources)
Direct2D 的编程模型把资源严格区分为两类:
- 设备无关资源(device-independent):生命周期覆盖应用全程,不依赖具体显卡/渲染设备,典型代表是 Direct2D 工厂、DirectWrite 工厂与文本格式;
- 设备相关资源(device-dependent):绑定到具体 Direct3D 设备,典型代表是渲染目标与画刷,设备丢失(如显示器切换、远程桌面、显卡拔出)时需要重建。
DemoApp::Initialize首先调用CreateDeviceIndependentResources(Direct2DHelloWorld.cpp),其实现位于 Direct2DHelloWorld.cpp,按顺序完成三步:
第一步:创建 Direct2D 工厂
hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pD2DFactory);D2D1CreateFactory是 Direct2D 的全局入口函数,参数D2D1_FACTORY_TYPE_SINGLE_THREADED表示工厂及其创建的对象仅在创建它的线程上使用(示例的应用逻辑全在 UI 线程,因此选单线程类型最合理)。工厂对象随后承担创建渲染目标、获取系统 DPI 等职责。
第二步:创建 DirectWrite 工厂
hr = DWriteCreateFactory( DWRITE_FACTORY_TYPE_SHARED, __uuidof(m_pDWriteFactory), reinterpret_cast<IUnknown **>(&m_pDWriteFactory));DWriteCreateFactory是 DirectWrite 的全局入口,DWRITE_FACTORY_TYPE_SHARED表示共享工厂(内部可复用字体缓存等资源)。注意这里以__uuidof取得IDWriteFactory的 IID,并通过IUnknown**转型完成 QueryInterface 式的创建。
第三步:创建文本格式并设置对齐
static const WCHAR msc_fontName[] = L"Verdana"; static const FLOAT msc_fontSize = 50; hr = m_pDWriteFactory->CreateTextFormat( msc_fontName, // 字体族:Verdana NULL, // 字体集合:NULL 表示使用系统默认字体集合 DWRITE_FONT_WEIGHT_NORMAL, // 字重:常规 DWRITE_FONT_STYLE_NORMAL, // 样式:常规 DWRITE_FONT_STRETCH_NORMAL, // 拉伸:常规 msc_fontSize, // 字号:50(DIP) L"", // 区域设置:空字符串表示使用系统默认 locale &m_pTextFormat); // 水平居中 m_pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER); // 垂直居中 m_pTextFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);IDWriteTextFormat封装了"字体族、字重、样式、拉伸、字号、locale、对齐方式"等排版特征,是 DirectWrite 文本绘制的核心参数对象。示例用SetTextAlignment与SetParagraphAlignment实现文字在窗口中水平和垂直双居中。这里的字号 50 以 DIP(Device Independent Pixel,设备无关像素)为单位,与系统 DPI 配合后可保证不同缩放比例下的清晰度。
窗口创建与 DPI 缩放
设备无关资源就绪后,Initialize注册窗口类并创建窗口(Direct2DHelloWorld.cpp):
WNDCLASSEX wcex = { sizeof(WNDCLASSEX) }; wcex.style = CS_HREDRAW | CS_VREDRAW; // 窗口尺寸变化时重绘 wcex.lpfnWndProc = DemoApp::WndProc; wcex.cbWndExtra = sizeof(LONG_PTR); // 预留空间存放 this 指针 wcex.hInstance = HINST_THISCOMPONENT; wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.lpszClassName = L"D2DDemoApp"; RegisterClassEx(&wcex);其中HINST_THISCOMPONENT是头文件里通过__ImageBase取得的当前模块实例句柄(Direct2DHelloWorld.h)。
创建窗口时有一个典型的 DPI 处理细节——CreateWindow的尺寸参数以像素为单位,因此要先从工厂读取系统 DPI 并按比例换算:
FLOAT dpiX, dpiY; m_pD2DFactory->GetDesktopDpi(&dpiX, &dpiY); m_hwnd = CreateWindow( L"D2DDemoApp", L"Direct2D Demo Application", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, static_cast<UINT>(ceil(640.f * dpiX / 96.f)), // 设计尺寸 640×480 static_cast<UINT>(ceil(480.f * dpiY / 96.f)), // 按 DPI 放大 NULL, NULL, HINST_THISCOMPONENT, this); // lpParam 传入 thisGetDesktopDpi返回系统每英寸像素数(96 为 100% 缩放的基准),示例据此把逻辑上的 640×480 窗口放大到实际像素尺寸,使窗口在不同 DPI 设置下物理大小一致。窗口创建成功后会立即ShowWindow与UpdateWindow显示窗口。
CreateWindow的最后一个参数this会被传入WM_CREATE消息的CREATESTRUCT.lpCreateParams,WndProc在WM_CREATE中通过SetWindowLongPtrW(GWLP_USERDATA, ...)把DemoApp*存进窗口的 user data(Direct2DHelloWorld.cpp),此后所有消息都能用GetWindowLongPtrW取回实例指针。这是 Win32 中"面向对象化"窗口过程的标准技巧。
设备相关资源的创建与销毁
创建渲染目标与画刷
CreateDeviceResources(Direct2DHelloWorld.cpp)在首次渲染时才按需创建设备相关资源:
RECT rc; GetClientRect(m_hwnd, &rc); D2D1_SIZE_U size = D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top); hr = m_pD2DFactory->CreateHwndRenderTarget( D2D1::RenderTargetProperties(), // 默认渲染目标属性 D2D1::HwndRenderTargetProperties(m_hwnd, size), // 绑定窗口与客户区尺寸 &m_pRenderTarget); if (SUCCEEDED(hr)) { hr = m_pRenderTarget->CreateSolidColorBrush( D2D1::ColorF(D2D1::ColorF::Black), // 黑色画刷 &m_pBlackBrush); }要点如下:
CreateHwndRenderTarget把渲染目标直接绑定到 HWND,Direct2D 会在内部创建 Direct3D 交换链,把绘制内容呈现到窗口客户区;- 尺寸使用客户区(client area)大小,
D2D1::SizeU与D2D1::HwndRenderTargetProperties都是d2d1helper.h提供的便捷构造辅助; D2D1::ColorF::Black是预定义颜色枚举,也可用D2D1::ColorF(0, 0, 0)指定 RGB。
代码注释还点明了设计意图:设备相关资源全部集中在这里创建,以便 Direct3D 设备丢失(显示器切换、远程会话、显卡移除等)时统一重建。
设备丢失处理:DiscardDeviceResources
void DemoApp::DiscardDeviceResources() { SafeRelease(&m_pRenderTarget); SafeRelease(&m_pBlackBrush); }当检测到设备丢失时,直接释放渲染目标与画刷;下一次OnRender调用CreateDeviceResources时会重新创建。渲染目标内部的CreateDeviceResources判断!m_pRenderTarget才执行创建逻辑(Direct2DHelloWorld.cpp),因此释放后自动走重建路径。
渲染流程:OnRender 详解
OnRender是整段示例的核心(Direct2DHelloWorld.cpp):
HRESULT DemoApp::OnRender() { HRESULT hr; hr = CreateDeviceResources(); if (SUCCEEDED(hr) && !(m_pRenderTarget->CheckWindowState() & D2D1_WINDOW_STATE_OCCLUDED)) { static const WCHAR sc_helloWorld[] = L"Hello, World!"; D2D1_SIZE_F renderTargetSize = m_pRenderTarget->GetSize(); m_pRenderTarget->BeginDraw(); m_pRenderTarget->SetTransform(D2D1::Matrix3x2F::Identity()); m_pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White)); m_pRenderTarget->DrawText( sc_helloWorld, ARRAYSIZE(sc_helloWorld) - 1, // 字符数(不含结尾 \0) m_pTextFormat, D2D1::RectF(0, 0, renderTargetSize.width, renderTargetSize.height), m_pBlackBrush); hr = m_pRenderTarget->EndDraw(); if (hr == D2DERR_RECREATE_TARGET) // 设备丢失信号 { hr = S_OK; DiscardDeviceResources(); } } return hr; }逐段理解其语义:
- 遮挡检测:
CheckWindowState()返回窗口状态标志,若含D2D1_WINDOW_STATE_OCCLUDED(例如锁屏遮挡)则跳过绘制,代码注释明确说明此时不渲染任何内容; - BeginDraw / EndDraw 配对:所有绘制命令必须放在
BeginDraw()与EndDraw()之间;EndDraw把命令批量提交给 GPU 并呈现; - 变换重置:
SetTransform(D2D1::Matrix3x2F::Identity())把当前变换矩阵复位为单位矩阵,确保绘制坐标基于窗口原点; - 清屏:
Clear(White)用白色填充整个渲染目标,形成"白底"; - 绘制文本:
DrawText接收宽字符串L"Hello, World!"、字符长度(ARRAYSIZE - 1去掉结尾\0)、文本格式、目标矩形(此处为整个渲染目标尺寸)与画刷,配合前面设置的双居中对齐,文字正好显示在窗口中央; - 设备丢失恢复:
EndDraw返回D2DERR_RECREATE_TARGET即表示设备丢失,此时把 HRESULT 恢复为S_OK并调用DiscardDeviceResources(),下次绘制自动重建资源——这正是 Direct2D 推荐的标准恢复模式。
消息循环与窗口消息处理
RunMessageLoop(Direct2DHelloWorld.cpp)是标准的 Win32 消息泵:GetMessage取消息 →TranslateMessage转换键盘消息 →DispatchMessage分发给窗口过程,直到收到WM_QUIT退出。
WndProc(Direct2DHelloWorld.cpp)是驱动渲染的中枢,主要处理三类消息:
| 消息 | 处理逻辑 |
|---|---|
WM_SIZE | 取出LOWORD(lParam)/HIWORD(lParam)得到新客户区宽高,调用OnResize(width, height)调整渲染目标尺寸 |
WM_PAINT/WM_DISPLAYCHANGE | 标准BeginPaint/EndPaint包裹下调用OnRender()重绘;WM_DISPLAYCHANGE覆盖了显示器分辨率/色彩深度变化需要重绘的场景 |
WM_DESTROY | 调用PostQuitMessage(0)结束消息循环,退出程序 |
OnResize的实现(Direct2DHelloWorld.cpp)直接调用m_pRenderTarget->Resize(size)同步渲染目标与窗口客户区大小。注释特别说明:Resize 可能失败,但此处可以忽略错误——失败会在下一次EndDraw时重现,这正是 Direct2D 延迟错误报告机制的体现。
DPI 感知清单:DeclareDPIAware.manifest
示例通过内嵌清单声明 DPI 感知(DeclareDPIAware.manifest):
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> <asmv3:application> <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"> <dpiAware>true</dpiAware> </asmv3:windowsSettings> </asmv3:application> </assembly><dpiAware>true</dpiAware>使系统认为该进程是 DPI-aware,从而不会对窗口进行位图拉伸式的虚拟化缩放。这与此前GetDesktopDpi换算窗口尺寸的逻辑互为呼应:窗口按实际 DPI 构造,文本以 DIP 为单位渲染,Direct2D 负责最终的清晰呈现。
该清单通过工程文件的清单工具嵌入程序集,四种配置(Debug/Release × Win32/x64)的VCManifestTool都配置了AdditionalManifestFiles="DeclareDPIAware.manifest"(例如 Direct2DHelloWorld.vcproj)。README 的文件列表中正是把它列为 "Specifies that the sample application is DPI-aware"。
工程配置中的链接库
Direct2DHelloWorld.vcproj 的链接器配置(VCLinkerTool的AdditionalDependencies)在四种配置中完全一致,揭示了本示例用到的系统库:
d2d1.lib dwrite.lib windowscodecs.lib dxgi.lib d3d10_1.lib各库职责如下:
- d2d1.lib:Direct2D 核心 API(工厂、渲染目标、画刷等);
- dwrite.lib:DirectWrite 文本 API(工厂与文本格式);
- windowscodecs.lib:WIC 图像编解码 API(对应头文件中声明的
m_pWICFactory成员,本示例未实际使用,属于预留); - dxgi.lib:DXGI 基础接口(Direct2D 底层交换链依赖);
- d3d10_1.lib:Direct3D 10.1 API(Direct2D 在其之上构建的渲染管线)。
编译器配置方面,WarningLevel="4"且WarnAsError="true"(如 Direct2DHelloWorld.vcproj),即四级警告且将警告视为错误,保证代码质量;UsePrecompiledHeader="0"说明该示例未使用预编译头。
另外,头文件顶部为WINVER与_WIN32_WINNT定义了0x0700(Direct2DHelloWorld.h),即显式声明目标平台为 Windows 7 及以上,同时定义了UNICODE与WIN32_LEAN_AND_MEAN(精简 Windows 头文件),并包含d2d1.h、d2d1helper.h、dwrite.h、wincodec.h四个关键头文件。
常见问题与延伸学习
围绕该示例,有几点值得在实际开发中注意:
- 线程模型:Direct2D 工厂选择
SINGLE_THREADED,渲染必须始终发生在创建工厂的同一线程(通常是 UI 线程),不要在后台线程直接操作渲染目标; - 设备丢失是常态而非异常:远程桌面断开、显卡驱动重置、显示器切换都会触发
D2DERR_RECREATE_TARGET,务必按示例的DiscardDeviceResources+ 延迟重建模式处理; - DPI 两套机制配合:内嵌清单声明感知 + 代码里用
GetDesktopDpi换算像素尺寸,二者缺一不可,否则会出现窗口模糊或文字渲染不清的问题; - 资源释放顺序:COM 接口按依赖逆序释放(画刷/渲染目标 → 文本格式 → 两个工厂),示例在
DemoApp析构函数中集中处理,避免泄漏。
如果想继续深入,仓库的 multimedia/Direct2D 目录还提供了同主题的进阶示例:GdiInteropSample(Direct2D 与 GDI 互操作)、SaveAsImageFileSample(渲染结果存为图片文件)、SimplePathAnimationSample 与 TextAnimationSample(路径与文本动画)等,可在掌握本示例的基础上按需阅读。
小结
Direct2D Hello World 虽然只绘制一行文字,却完整示范了 Direct2D 桌面应用的全部关键模式:双类资源(设备无关/设备相关)的生命周期管理、HWND 渲染目标的创建与窗口尺寸联动、BeginDraw/EndDraw 渲染协议、WM_PAINT 驱动的重绘、DPI 感知声明,以及设备丢失的优雅恢复。以 Direct2DHelloWorld.cpp 为模板,替换DrawText为DrawRectangle、FillGeometry、DrawBitmap等调用,即可快速扩展出任意 2D 图形应用——这正是它作为 Direct2D 入门第一课的价值所在。
- 示例工程
【免费下载链接】Windows-classic-samples
This repo contains samples that demonstrate the API used in Windows classic desktop applications.
相关推荐
3 条路径搞定 Gopeed macOS 部署:从双击 .app 到源码编译
3 条路径搞定 Gopeed macOS 部署:从双击 .app 到源码编译 当你双击 Gopeed 的 .app 图标,弹窗跳出「无法验证开发者,可能已损坏」
示例工程Windows-classic-samples 中的 Simple Direct2D Application:用 C++ 与 Direct2D 绘制图形、文本和位图
Windows classic samples 中的 Simple Direct2D Application:用 C++ 与 Direct2D 绘制图形、文本和
示例工程Windows-classic-samples 之 Direct2D 简单路径动画示例(SimplePathAnimationSample)深度剖析
Windows classic samples 之 Direct2D 简单路径动画示例(SimplePathAnimationSample)深度剖析 导读 本指
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考