Dev-C++ 配 EasyX 接入 Direct2D:实时绘图与抗锯齿渲染 Dev-C 挂 EasyX 画图是很多人入门图形编程的第一站装完就能用initgraph一行起窗口line、circle、outtextxy随手就来。但只要你稍微想做点“像样”的东西——平滑的曲线、带透明度的渐变、几万个图元还能跑满帧率——就会立刻撞到 GDI 的天花板锯齿明显、抗锯齿只能靠“多画几遍”硬凑、复杂路径填充慢得离谱。这时候把 Direct2D 拉进来做渲染后端用 EasyX 管窗口、管输入、管图片解码是一条实际项目里真能跑通的路子。这篇内容讲的就是用 Dev-C 这个 IDE、基于 EasyX 提供的窗口句柄和离屏设备上下文把 Direct2D 的绘制管线接进 EasyX 的帧循环里做成一个可交互的实时绘图程序。适合已经能写出initgraph 消息循环、但卡在“画面不够好看、性能上不去”这一步的人也适合完全没碰过 COM 接口、想找个最小可用样例照着抄的同学。下面全部是我自己反复折腾出来的配置、代码和坑Dev-C 工程怎么设、库怎么链、两种架构怎么选、D2D 画完被 EasyX 刷掉怎么救一条条说清楚。1. 先想明白这套组合到底图什么1.1 EasyX 的能力边界在哪里EasyX 本质是对 GDI 的一层 C 封装它把HDC、HPEN、HBRUSH这些 Win32 GDI 对象包装成了setlinecolor、fillcircle这种好用的函数再配上一套双缓冲机制BeginBatchDraw/FlushBatchDraw解决闪烁问题。它的优势在于上手成本和生态几十个函数覆盖了绝大多数 2D 绘图需求loadimage一行就能读 PNG/JPG中文字体直接用系统字体名教材和习题里全是它。但 GDI 有两个绕不过去的硬伤。第一是几何绘制质量GDI 的线条没有真正的抗锯齿line画斜线出来是阶梯状的锯齿只能靠先画粗再缩、或者叠加半透明线来“伪抗锯齿”又慢又难看。第二是复杂图元性能GDI 的路径填充和渐变支持非常弱你想画一个几千个控制点的贝塞尔曲线、或者一个径向渐变的光晕用 GDI 基本等于自己写软件光栅化器。这两个问题在“实时交互绘图”场景里会被无限放大——鼠标一拖几百个线段要重绘画面立刻掉帧。1.2 Direct2D 补上的是哪块板Direct2D 是系统自带的硬件加速 2D 渲染接口抗锯齿、几何路径、渐变、透明度混合、图层裁剪这些都是原生支持的而且走 GPU几万个图元也能稳住帧率。它的坐标系统叫 DIP设备无关像素默认和一个逻辑像素对齐配合 DPI 缩放能保证高分屏下不出糊。把两者拼起来的核心思路很朴素EasyX 保留它擅长的部分窗口创建、消息循环、图片解码、文字输出把渲染这一层换成 Direct2D。EasyX 提供了两个关键入口让我们把 D2D 接进去入口函数返回什么典型用途GetHWnd()EasyX 绘图窗口的窗口句柄HWND创建ID2D1HwndRenderTarget直接往窗口上画GetImageHDC(IMAGE*)IMAGE对象内部的设备上下文HDC创建ID2D1DCRenderTarget把 D2D 画进离屏位图这两个函数是整个方案的地基。GetHWnd让 D2D 拿到同一块画布GetImageHDC让 D2D 的输出可以塞进 EasyX 的图片对象从而和putimage、批量绘制体系无缝对接。1.3 为什么不干脆全用 D2D有人会问既然 Direct2D 更强为什么不扔掉 EasyX 直接写 Win32 D2D答案是成本。纯 Win32 起一个窗口要注册窗口类、写WndProc、处理WM_PAINT、处理WM_SIZE、处理 DPI 变化光样板代码就两百行起步中间任何一个消息漏了就是白屏或者卡死。EasyX 把这些全包了initgraph(960, 640)一行搞定鼠标键盘用peekmessage一次拿全。对于工具类、教学类、Demo 类项目这个取舍非常划算——你只想画点东西不想先写一个窗口框架。我的判断标准是如果你的项目重心在“画什么”而不是“窗口怎么管”就用 EasyX 打底 D2D 渲染如果你要做的是需要多窗口、多线程渲染、复杂 UI 布局的产品级软件那还是老老实实上原生框架。2. Dev-C 环境怎么配才不出错2.1 编译器位数必须和 EasyX 库对齐Dev-C 是一个 IDE真正干活的是它捆绑的 GCC 工具链。这带来第一个必踩的坑EasyX 的库文件分 32 位和 64 位你的编译器也分 32 位和 64 位两边必须严格一致。拿 32 位的libeasyx.a去喂 64 位编译器链接阶段会甩给你一堆file format not recognized或者undefined reference to initgraph看起来像是函数没实现其实是格式不匹配。排查方法很直接在 Dev-C 的“工具 → 编译选项”里看当前配置用的编译器路径或者在命令行敲gcc -dumpmachine输出里带x86_64就是 64 位带i686就是 32 位。然后去看 EasyX 库目录下lib文件夹的结构里面通常按编译器类型和位数分了子目录选对应的那一份。这一步做完再谈其他配置否则后面全是白费功夫。注意Dev-C 老版本默认带的编译器版本比较旧可能缺失d2d1.h这类较新的头文件。如果编译时报“找不到 d2d1.h”别去网上乱下单个头文件凑正确做法是换一个较新的 MinGW-w64 工具链或者在 IDE 里新增一套编译器配置指向新工具链的bin目录。头文件和库必须来自同一套工具链混搭迟早出事。2.2 头文件路径和链接参数逐个说清楚在 Dev-C 的“项目属性 → 文件/目录 → 包含目录”里加上 EasyX 的include目录其实把graphics.h直接拷进工程目录也行工程小的话更省事。然后是最容易漏的一步链接器参数。在“项目属性 → 参数 → 链接器”里加下面这一串-leasyx -ld2d1 -ldwrite -lgdi32 -limm32 -lole32 -loleaut32 -luuid -lmsimg32 -lwinmm逐个解释为什么要这些-leasyxEasyX 本体initgraph、BeginBatchDraw都在里面。-ld2d1Direct2D 的导入库D2D1CreateFactory、各种Create*方法靠它。-ldwriteDirectWrite如果你打算用 D2D 画文字就得加本文示例里用 EasyX 的outtextxy出文字加上它不亏后面想换 D2D 文字时不用再改配置。-lgdi32GDI 基础库EasyX 和 D2D 的 DC 渲染目标都依赖。-limm32输入法相关EasyX 的输入框功能会用到。-lole32、-loleaut32、-luuidCOM 基础。Direct2D 的工厂、渲染目标全是 COM 对象CoInitialize、IID_*这些符号从这里来。这三个漏一个就是满屏undefined reference to IID_ID2D1Factory。-lmsimg32AlphaBlend之类EasyX 的透明贴图功能需要。-lwinmm多媒体计时相关EasyX 内部有依赖。我的习惯是先把这一长串全加上跑通了再一个个删着试确认哪些确实用不到。反正静态链接库没被引用的部分不会进最终 exe多加几个不影响体积。2.3 语言标准和字符集这两个开关别乱动语言标准Direct2D 的辅助头文件d2d1helper.h里用了模板和命名空间必须是 C 而非 C。在 Dev-C 的“编译选项 → 代码生成 → 语言标准”里选 ISO C11 或更高我一般直接给-stdc17。用 C98 编译会在一堆D2D1::ColorF的构造上报错。字符集这是 MinGW EasyX 组合里最阴的一个坑。EasyX 的函数签名里有TCHAR而TCHAR在没有定义UNICODE宏的时候等于char定义了就等于wchar_t。很多人从别处抄配置时顺手加了-DUNICODE结果outtextxy(0, 0, hello)直接编译失败因为字符串常量是const char*函数要const wchar_t*。MinGW 环境下我的建议是不要加-DUNICODE让TCHAR保持为char字符串常量直接写双引号outtextxy、loadimage的路径都不用加L前缀代码干净省事。真要输出中文把源文件保存为 UTF-8EasyX 的outtextxy配合系统字体能正常显示个别情况下需要设置控制台代码页但绘图窗口里一般没问题。提示如果你在 Dev-C 里打开一个从别处拿来的工程编译报几十个“cannot convert const char* to const wchar_t*”八成就是它的工程选项里带UNICODE宏。删掉这个宏比给每个字符串加L前缀省事得多。3. 两种渲染架构先选对再动手3.1 方案一HwndRenderTarget 直接叠加到 EasyX 窗口这是最直观的接法。GetHWnd()拿到窗口句柄创建一个ID2D1HwndRenderTarget绑定到这个窗口之后所有 D2D 绘制命令直接输出到窗口表面。EasyX 那边照常用cleardevice、circle、outtextxy画画完FlushBatchDraw把后备缓冲贴到窗口然后 D2D 再在窗口上叠加绘制。关键约束是顺序必须严格遵守EasyX 侧完成本帧全部绘制背景、贴图、静态元素。调用FlushBatchDraw()把 EasyX 的后备缓冲刷到窗口。这一步会把整个窗口内容覆盖掉。立刻执行 D2D 的BeginDraw→ 绘制命令 →EndDraw把动态层画上去。为什么顺序不能反因为FlushBatchDraw是一次整窗BitBlt它会无条件覆盖窗口上的所有像素。如果你先画 D2D 再刷新 EasyXD2D 的内容会瞬间消失表现就是“画了但看不见”。反过来先刷 EasyX 再画 D2D 就没问题因为 D2D 直接操作窗口表面不会再被覆盖。这个方案的优点是代码量小、理解成本低、D2D 的绘制直接可见。缺点是每帧都要重画一次因为下一帧 EasyX 的清屏和刷新会把上一帧的 D2D 内容抹掉。对于“每帧内容都在变”的动画场景这完全不是问题但如果你想让 D2D 画的东西“留”在屏幕上不动就得自己维护一个图元列表每帧重放。3.2 方案二DCRenderTarget 绑定到 EasyX 的 IMAGE这个方案更“正统”一些也更适合做完整的分层渲染。思路是先IMAGE canvas(W, H)创建一块离屏位图用GetImageHDC(canvas)拿到它的设备上下文创建ID2D1DCRenderTarget并用BindDC绑定上去。之后所有 D2D 绘制都输出到这张位图里画完用putimage(0, 0, canvas)贴到窗口。这么做的价值在于D2D 的输出变成了一个普通的 EasyX 图像对象可以自由参与putimage、rotateimage、SetWorkingImage这些操作也可以塞进 EasyX 的批量绘制体系里配合BeginBatchDraw实现真正的整帧双缓冲彻底消除撕裂。代价是需要自己管理位图的清空和重绘。RenderTarget的Clear会把整张位图清成指定颜色这意味着 EasyX 在窗口层的绘制会被这张位图整块盖住。所以实际的分层策略是D2D 层负责整帧画面背景、几何图形、动态内容EasyX 层负责在其之上的 UI 元素分数、提示文字、按钮顺序是先putimage贴 D2D 层再用 EasyX 画 UI。如果你想让 EasyX 画的东西在 D2D 层下面那就反过来先 EasyX 画完刷新再贴 D2D 层——但那样 D2D 层的透明通道就必须处理干净否则会看到一块黑边。3.3 两种方案的对比和选择建议对比项方案一HwndRenderTarget方案二DCRenderTarget IMAGE代码复杂度低十行以内建好中等要管位图和绑定双缓冲D2D 内部有但与 EasyX 刷新交替可能有轻微撕裂完整可完全纳入 EasyX 批量绘制每帧重绘要求必须否则被 EasyX 刷新覆盖由你控制Clear时才重置与 EasyX 绘图的叠放关系固定为 D2D 在上任意用putimage顺序控制适合的场景实时动画、鼠标涂鸦、图元全变的演示分层 UI、需要截图/后处理、要复用 EasyX 图像函数性能少一次内存拷贝略快多一次整帧 BitBlt大分辨率下有开销我的实际做法是先写方案一跑通确认 D2D 能正常出图、交互能响应再根据需求决定要不要迁到方案二。因为方案一的代码几乎就是方案二的子集迁移的时候只需要把创建渲染目标的那几行换掉绘制逻辑一行不用改这是很划算的演进路径。4. 初始化这块每个参数都得说得出理由4.1 COM 初始化和工厂创建Direct2D 的对象都是 COM 对象理论上要调CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)。但 EasyX 的initgraph内部其实已经做了OleInitialize所以大部分情况下不调也能跑。我的习惯是显式调一次并且判断返回值因为如果主线程已经被别的东西初始化成了多线程模式单线程工厂会出问题。if (FAILED(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED))) { // 已经初始化过也算失败返回这里不中断继续往下走 }工厂用D2D1CreateFactory创建第一个参数是线程模型。这里选D2D1_FACTORY_TYPE_SINGLE_THREADED因为我们所有的绘制都在主线程的消息循环里完成单线程模式省掉了内部加锁的开销性能更好。只有当你打算开渲染线程的时候才用多线程模式那时候每个渲染目标都要额外的同步处理。4.2 DPI 参数为什么一定要写 96这是最容易出隐蔽 bug 的地方。D2D1::RenderTargetProperties的第三、第四个参数是dpiX和dpiY很多人直接传 0 表示“用系统默认值”。在你的显示器缩放是 100% 的时候没问题一旦用户把系统缩放调到 150%系统 DPI 变成 144D2D 就会认为 1 个 DIP 等于 1.5 个像素于是你用DrawLine(D2D1::Point2F(0, 0), D2D1::Point2F(100, 100))画的线实际在窗口上占了 150 个像素宽。而 EasyX 的坐标系是货真价实的像素putimage(100, 100, img)就是贴在第 100 个物理像素位置。两边坐标系一错位叠加绘制就会出现“越往右下偏得越多”的诡异现象非常难查。解决办法有两个我一般两个都做// 1) 强制 D2D 使用 96 DPI保证 1 DIP 1 像素 D2D1_RENDER_TARGET_PROPERTIES props D2D1::RenderTargetProperties( D2D1_RENDER_TARGET_TYPE_DEFAULT, D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_UNKNOWN), 96.0f, 96.0f, D2D1_RENDER_TARGET_USAGE_NONE, D2D1_FEATURE_LEVEL_DEFAULT); // 2) 在 initgraph 之前声明进程 DPI 感知避免窗口被系统拉伸 SetProcessDPIAware(); initgraph(960, 640);第二行单独说一句。如果程序不声明 DPI 感知系统在 150% 缩放的屏幕上会把你的 960×640 窗口整个拉伸到 1440×960 物理像素画面会糊成一片这时候 D2D 的抗锯齿做得再好也白搭因为输出端已经在做最近邻放大了。加上SetProcessDPIAware()之后initgraph(960, 640)就是实打实的 960 个物理像素D2D 的 96 DPI 设置也才真正对得上。这行代码需要user32库MinGW 默认就链接了不用额外加参数。4.3 EndDraw 的返回值必须检查EndDraw()不是永远成功。当设备丢失比如用户切换了显示器、显卡驱动重置、全屏独占切换时它会返回D2DERR_RECREATE_TARGET意思是“渲染目标已经废了你得重建”。很多人图省事不检查返回值结果就是程序跑一会儿突然黑屏怎么点都没反应其实只是渲染目标挂了。标准处理姿势是EndDraw返回这个错误码时释放渲染目标及相关资源重新创建一遍然后继续下一帧。HRESULT hr g_rt-EndDraw(); if (hr D2DERR_RECREATE_TARGET) { RecreateRenderTarget(); // 内部 Release 后重新 Create }RecreateRenderTarget里要按依赖顺序释放先释放画刷再释放渲染目标工厂可以留着工厂不依赖设备。重建的时候画刷也要跟着重建因为画刷是绑定在特定渲染目标上的设备一换就失效。注意这段逻辑一定要放在帧循环里不要只在初始化时判断一次。设备丢失是运行期事件只在初始化检查等于没检查。5. 帧循环和交互这才是“交互”两个字的落点5.1 消息怎么取别用 while(1) 死循环EasyX 提供了两套消息接口。老式的peekmessage(msg, EM_MOUSE | EM_KEY)一次拿一条新式的ExMessage配合peekmessage(msg)用位掩码分类。做实时交互我推荐非阻塞的 peekmessage 固定帧率循环因为getmessage是阻塞的没有消息时程序会停在那里动画就不动了。结构大概是这样ExMessage msg; while (running) { while (peekmessage(msg, EX_MOUSE | EX_KEY)) { HandleMessage(msg); } Update(deltaTime); Render(); }内层while一次把消息队列掏空保证输入不丢外层按帧推进。这样鼠标移动产生的密集消息会被一次性处理完不会堆积导致输入延迟。5.2 时间步长用 QueryPerformanceCounter 而不是 GetTickCount帧率控制的核心是拿到两帧之间的真实耗时。GetTickCount()的精度是 15.6 毫秒左右对于 60 帧16.7 毫秒的循环来说误差太大了会导致动画速度肉眼可见地不均。正确做法是用高精度计数器LARGE_INTEGER freq, now, last; QueryPerformanceFrequency(freq); QueryPerformanceCounter(last); while (running) { QueryPerformanceCounter(now); float dt (float)((double)(now.QuadPart - last.QuadPart) / (double)freq.QuadPart); last now; // 用 dt 推进动画而不是用固定值 ... }计算过程说白了就是dt 时间差 / 计数器频率单位是秒。比如计数器频率是 10000000两次采样差 167000那dt 0.0167秒正好是 60Hz。所有跟时间相关的量都乘上dt动画速度就和帧率解耦了——机器快就多画几帧机器慢就少画几帧但物体移动的物理速度是一致的。如果要限制最高帧率省电、避免空转占满 CPU在帧末判断dt是否小于目标间隔不足就Sleep补上。60 帧的目标间隔是1.0 / 60 ≈ 0.01667秒换算成毫秒是 16.6Sleep只接受整数毫秒所以补 15 毫秒左右比较稳妥剩下的靠下一帧自然对齐。5.3 一个能直接抄的完整骨架下面这段是基于方案一的完整可跑代码做的是一个鼠标拖拽画平滑曲线的交互 Demo。EasyX 负责窗口、输入、文字提示Direct2D 负责全部线条绘制。#include graphics.h #include d2d1.h #include d2d1helper.h #include vector #include windows.h template typename T class ComPtr { T* p; public: ComPtr() : p(nullptr) {} ~ComPtr() { if (p) { p-Release(); p nullptr; } } T** addr() { return p; } T* operator-() const { return p; } T* get() const { return p; } void reset() { if (p) { p-Release(); p nullptr; } } private: ComPtr(const ComPtr); ComPtr operator(const ComPtr); }; static ComPtrID2D1Factory g_factory; static ComPtrID2D1HwndRenderTarget g_rt; static ComPtrID2D1SolidColorBrush g_brush; static bool InitD2D(HWND hwnd, UINT w, UINT h) { if (FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, g_factory.addr()))) return false; D2D1_RENDER_TARGET_PROPERTIES props D2D1::RenderTargetProperties( D2D1_RENDER_TARGET_TYPE_DEFAULT, D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_UNKNOWN), 96.0f, 96.0f, D2D1_RENDER_TARGET_USAGE_NONE, D2D1_FEATURE_LEVEL_DEFAULT); D2D1_HWND_RENDER_TARGET_PROPERTIES hprops D2D1::HwndRenderTargetProperties(hwnd, D2D1::SizeU(w, h), D2D1_PRESENT_OPTIONS_NONE); if (FAILED(g_factory-CreateHwndRenderTarget(props, hprops, g_rt.addr()))) return false; g_rt-CreateSolidColorBrush( D2D1::ColorF(D2D1::ColorF::CornflowerBlue), g_brush.addr()); g_rt-SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); return true; } int main() { SetProcessDPIAware(); const int W 960, H 640; initgraph(W, H); setbkcolor(WHITE); cleardevice(); if (!InitD2D(GetHWnd(), W, H)) { outtextxy(10, 10, D2D init failed); getchar(); return -1; } struct Stroke { std::vectorD2D1_POINT_2F pts; D2D1_COLOR_F color; }; std::vectorStroke strokes; bool dragging false; BeginBatchDraw(); LARGE_INTEGER freq, now, last; QueryPerformanceFrequency(freq); QueryPerformanceCounter(last); bool running true; ExMessage msg; while (running) { while (peekmessage(msg, EX_MOUSE | EX_KEY)) { if (msg.message WM_LBUTTONDOWN) { Stroke s; s.color D2D1::ColorF(rand() % 100 / 100.0f, rand() % 100 / 100.0f, rand() % 100 / 100.0f, 1.0f); s.pts.push_back(D2D1::Point2F((float)msg.x, (float)msg.y)); strokes.push_back(s); dragging true; } else if (msg.message WM_MOUSEMOVE dragging) { if (!strokes.empty()) { D2D1_POINT_2F p D2D1::Point2F((float)msg.x, (float)msg.y); D2D1_POINT_2F q strokes.back().pts.back(); float dx p.x - q.x, dy p.y - q.y; if (dx * dx dy * dy 4.0f) // 像素距离超过 2 才采样 strokes.back().pts.push_back(p); } } else if (msg.message WM_LBUTTONUP) { dragging false; } else if (msg.message WM_KEYDOWN msg.vkcode VK_ESCAPE) { running false; } } QueryPerformanceCounter(now); float dt (float)((double)(now.QuadPart - last.QuadPart) / (double)freq.QuadPart); last now; // ---- 第一层EasyX 画背景和 UI ---- cleardevice(); settextcolor(RGB(90, 90, 90)); outtextxy(16, 16, (LPCTSTR)Drag to draw, ESC to quit); char buf[128]; sprintf(buf, frames dt %.4f s, dt); outtextxy(16, 40, buf); FlushBatchDraw(); // ---- 第二层D2D 画描边 ---- if (g_rt.get()) { g_rt-BeginDraw(); for (size_t i 0; i strokes.size(); i) { const Stroke s strokes[i]; if (s.pts.size() 2) continue; g_brush-SetColor(s.color); for (size_t k 1; k s.pts.size(); k) { g_rt-DrawLine(s.pts[k - 1], s.pts[k], g_brush.get(), 3.0f); } // 端点补圆让拐角圆润 for (size_t k 0; k s.pts.size(); k) { g_rt-FillEllipse( D2D1::Ellipse(s.pts[k], 1.5f, 1.5f), g_brush.get()); } } HRESULT hr g_rt-EndDraw(); if (hr D2DERR_RECREATE_TARGET) { g_brush.reset(); g_rt.reset(); InitD2D(GetHWnd(), W, H); } } // 补 15ms粗略锁在 60 帧附近 if (dt 0.0156f) Sleep(15); } EndBatchDraw(); closegraph(); return 0; }这段代码里有几个刻意的设计值得单独说采样间隔判断。鼠标移动消息的密度跟鼠标硬件有关高刷新率鼠标一秒钟能发几百条如果每一帧都往点数组里塞一个笔画很快就上千个点D2D 每帧要画上千条线段帧率就掉下来了。用dx*dx dy*dy 4.0做个距离阈值平方比较省一次开方像素距离小于 2 就丢掉点数量能降到原来的几分之一视觉上完全看不出区别。端点补圆。只画线段的话折线的拐角处会露出缝隙看起来断断续续。在每个采样点上补一个半径等于线宽一半的小圆拐角立刻变得顺滑这是做手绘效果的标准手法。画刷颜色每笔一换。SetColor是廉价的因为它只改一个字段不会触发设备状态重建。真正贵的是CreateSolidColorBrush所以整个程序只创建了一个画刷靠改颜色复用这是 D2D 里非常常见的优化手段。6. 那些我踩过并且记下来的坑6.1 现象、原因、处理方式速查现象最可能的原因处理方式链接报undefined reference to D2D1CreateFactory没加-ld2d1链接器参数补上链接报undefined reference to IID_ID2D1Factory没加-luuid或-lole32补齐 COM 相关三个库编译报找不到d2d1.h工具链版本太旧换较新的 MinGW-w64不要单独下头文件D2D 画的东西一闪就没了被 EasyX 的FlushBatchDraw覆盖调整顺序EasyX 刷完再 D2D 画图形越往右下偏得越多D2D 的 DPI 用了系统默认值RenderTargetProperties的 dpi 显式传 96整个窗口文字发虚、线条发糊程序没声明 DPI 感知被系统缩放initgraph之前调SetProcessDPIAware()运行一段时间后黑屏设备丢失EndDraw返回D2DERR_RECREATE_TARGET没处理帧循环里检查返回值并重建outtextxy报类型转换错误工程里定义了UNICODE宏删掉宏或所有字符串加L前缀窗口缩放后 D2D 内容错位渲染目标尺寸没跟着窗口变大处理WM_SIZE时调Resize()鼠标画线有明显延迟用了阻塞的getmessage改成peekmessage 固定帧循环6.2 窗口缩放导致 D2D 内容错位方案一里如果用户拖拽窗口边框改变大小EasyX 的绘图区域会自动适配它内部处理了WM_SIZE但 D2D 的渲染目标尺寸是创建时固定死的不会自己变。结果是窗口变大了D2D 还在按老尺寸画右边和下边一片空白或者内容被拉伸。处理办法是在消息循环里盯住WM_SIZEif (msg.message WM_SIZE) { UINT w LOWORD(msg.lparam); UINT h HIWORD(msg.lparam); if (g_rt.get()) { g_rt-Resize(D2D1::SizeU(w, h)); } }注意Resize之后所有绑定在这个目标上的位图、图层都要重新设置画刷倒是不受影响。另外 EasyX 的窗口默认是可以拖拽调整大小的如果你不希望用户改尺寸可以考虑在初始化时锁定具体做法取决于你的需求我一般倾向于支持缩放毕竟工具类程序用户经常会想拉大一点看细节。6.3 中文字体在两边表现不一致EasyX 的settextstyle用系统字体名比如“微软雅黑”直接传字符串就行配上outtextxy能正常显示中文。Direct2D 那边如果要用中文需要走 DirectWrite 的CreateTextFormat传入字体族名和 locale还要注意字符串必须是宽字符。两边字体渲染的度量行高、字宽也不完全一样所以在做对齐的时候不要混用。我的做法是中文一律交给 EasyX 输出D2D 只画图形。这样省掉了 DirectWrite 的整套配置也不用操心宽窄字符串转换对中小项目来说是最省心的分工。6.4 画刷忘了 Release 导致内存缓慢增长COM 对象是引用计数管理CreateSolidColorBrush出来之后必须Release。用裸指针写的时候很容易漏尤其是错误分支提前返回的地方。这也是我在示例里自己包了一个极简ComPtr的原因——析构自动释放异常和提前返回都不会漏。标准库里有类似的智能指针封装但老版本 MinGW 不一定带自己写一个二十行的模板最保险。CloseHandle之类的资源同理initgraph之后一定要配closegraphBeginBatchDraw之后一定要配EndBatchDraw这些不对称的调用在调试时不会报错但会留下莫名其妙的资源泄漏和后遗症。7. 性能和维护上的一些个人经验实时绘制这条路上最容易犯的错是“每帧重建一切”。我见过不少写法是在帧循环里创建画刷、创建几何路径、创建渐变画完就扔。D2D 的这些对象创建成本都不低每帧创建几十个的话帧率直接被拖到个位数。正确的做法是把不随帧变化的对象全部提到循环外画刷一次创建反复改色几何路径在数据变化时才重建用ID2D1PathGeometry配合Sink渐变在初始化时算好。另一个经验是关于BeginDraw到EndDraw之间的时间。这段时间是在“录制命令”D2D 会攒一批命令再一次性提交。所以这段时间里不要做耗时操作不要读文件不要等锁更不要在里面Sleep。我曾经把一段文件读取塞进BeginDraw和EndDraw中间结果画面撕裂得非常严重因为提交被拖延了。把数据处理全部放在BeginDraw之前完成中间只留纯绘制指令。关于帧率目标不必盲目追求 60 帧。交互式绘图鼠标拖拽画线这种40 帧就已经很跟手了多出来的帧只是空转耗电。反过来说如果你的应用里有惯性、有弹簧动画这类物理模拟帧率低于 50 会明显感觉到卡顿这时候就得把每帧的绘制量压下来比如对密集的折线做抽稀或者把静态的笔画烘焙到一张离屏位图上只重绘正在画的那一笔。最后提一个我觉得挺实用的扩展方向这套骨架里D2D 那一层完全可以换成更复杂的组合比如上ID2D1Layer做局部裁剪、上ID2D1BitmapBrush给笔画贴纹理、上ID2D1Effect加模糊和阴影。因为渲染目标和绘制逻辑已经解耦了加这些东西只需要在Render函数里插命令EasyX 那侧的窗口和输入代码一行都不用动。我自己后来就是在这个骨架上加了辉光和模糊全是 D2D 的活改起来很清爽。