通用GUI编程技术——图形渲染实战(三十)——Direct2D几何体系统:从路径到命中测试

张开发
2026/4/17 20:10:48 15 分钟阅读

分享文章

通用GUI编程技术——图形渲染实战(三十)——Direct2D几何体系统:从路径到命中测试
通用GUI编程技术——图形渲染实战三十——Direct2D几何体系统从路径到命中测试仓库已经开源喜欢的话点个⭐包含Win32的目前已完成教程力争做一个完备的GUI教程欢迎各位大佬前来参观https://github.com/Charliechen114514/anatomy_gui在上一篇文章中我们搭建了 Direct2D 的基础框架——创建工厂、创建渲染目标、处理设备丢失并且成功在窗口中绘制了一个旋转的三角形。如果你跟着做了应该已经感受到了 Direct2D 的渲染质量线条平滑、没有锯齿、GPU 加速让 60fps 动画毫无压力。今天我们要深入的是 Direct2D 的几何体系统。GDI 时代我们只有Rectangle、Ellipse、Polygon这几个简单的图形函数想做复杂一点的自定义形状就得手动逐像素填充。Direct2D 的几何体系统要强大得多——它支持贝塞尔曲线、路径组合的布尔运算、描边扩展以及最重要的精确的命中测试。这些能力是构建交互式图形应用的基础不管是矢量绘图工具还是数据可视化图表都离不开几何体系统。环境说明操作系统: Windows 10/11编译器: MSVC (Visual Studio 2022)图形库: Direct2D链接d2d1.lib前置知识: 文章 29Direct2D 架构与初始化内置几何体矩形、圆角矩形和椭圆Direct2D 的ID2D1RenderTarget提供了直接绘制基本形状的方法不需要显式创建几何体对象// 绘制矩形描边pRenderTarget-DrawRectangle(D2D1::RectF(50.0f,50.0f,300.0f,200.0f),// 矩形区域pBrush,// 画刷2.0f// 线宽);// 填充圆角矩形D2D1_ROUNDED_RECT rrD2D1::RoundedRect(D2D1::RectF(50.0f,50.0f,300.0f,200.0f),15.0f,// X 圆角半径15.0f// Y 圆角半径);pRenderTarget-FillRoundedRectangle(rr,pBrush);// 绘制椭圆D2D1_ELLIPSE ellipseD2D1::Ellipse(D2D1::Point2F(400.0f,150.0f),// 中心点100.0f,// X 半径80.0f// Y 半径);pRenderTarget-DrawEllipse(ellipse,pBrush,2.0f);这些方法直接在BeginDraw/EndDraw之间调用即可。但它们只是即时绘制没有办法对形状进行后续操作比如命中测试、布尔运算。如果你需要这些高级功能就必须使用独立的几何体对象。路径几何体PathGeometry自由形状的核心ID2D1PathGeometry是 Direct2D 中最灵活的几何体类型。它由一系列线段和曲线段组成可以表达任意复杂的 2D 形状。绘制五角星的完整示例我们来用PathGeometry绘制一个五角星这是理解路径几何体工作方式的最佳示例ID2D1PathGeometry*CreateStarGeometry(ID2D1Factory*pFactory,floatcx,floatcy,floatouterR,floatinnerR){ID2D1PathGeometry*pPathNULL;pFactory-CreatePathGeometry(pPath);ID2D1GeometrySink*pSinkNULL;pPath-Open(pSink);// 五角星有 10 个顶点5 个外顶点 5 个内顶点pSink-BeginFigure(D2D1::Point2F(cx,cy-outerR),// 从顶部外顶点开始D2D1_FIGURE_BEGIN_FILLED);for(inti1;i9;i){floatangle-3.14159f/2.0fi*(3.14159f/5.0f);floatr(i%20)?outerR:innerR;pSink-AddLine(D2D1::Point2F(cxr*cosf(angle),cyr*sinf(angle)));}pSink-EndFigure(D2D1_FIGURE_END_CLOSED);pSink-Close();// ⚠️ 必须调用 CloseSafeRelease(pSink);returnpPath;}这段代码有几个关键点需要注意。Open方法返回一个ID2D1GeometrySink几何体接收器你通过这个接收器来定义路径的形状。路径由一个或多个图形Figure组成每个图形由BeginFigure开始、EndFigure结束。BeginFigure的第一个参数是起始点坐标第二个参数指定填充模式D2D1_FIGURE_BEGIN_FILLED表示这个图形参与填充计算D2D1_FIGURE_BEGIN_HOLLOW表示只做描边不填充。⚠️ 注意一个非常重要的坑GeometrySink使用完毕后必须调用Close()。未 Close 的几何体在绘制时会静默忽略——不报错不崩溃但什么都不画。这个坑花了我大半天时间才排查出来因为调试器不会给你任何提示。在 WM_PAINT 中使用voidOnPaint(HWND hwnd){CreateDeviceResources(hwnd);if(!g_pRT)return;// 创建五角星几何体只需创建一次建议缓存ID2D1PathGeometry*pStarCreateStarGeometry(g_pFactory,300,250,100,40);g_pRT-BeginDraw();g_pRT-Clear(D2D1::ColorF(0.1f,0.1f,0.12f,1.0f));// 填充五角星g_pBrush-SetColor(D2D1::ColorF(1.0f,0.85f,0.0f,1.0f));// 金色g_pRT-FillGeometry(pStar,g_pBrush);// 描边g_pBrush-SetColor(D2D1::ColorF(0.8f,0.6f,0.0f,1.0f));// 深金色g_pRT-DrawGeometry(pStar,g_pBrush,2.0f);HRESULT hrg_pRT-EndDraw();if(hrD2DERR_RECREATE_TARGET){DiscardDeviceResources();InvalidateRect(hwnd,NULL,FALSE);}SafeRelease(pStar);}⚠️ 注意上面的示例每次OnPaint都创建几何体这只是为了演示简洁。实际项目中几何体是设备无关资源应该在初始化时创建并缓存。如果你每帧都创建和销毁几何体对象性能会大幅下降。贝塞尔曲线段路径几何体不仅支持直线段AddLine还支持贝塞尔曲线// 三次贝塞尔曲线Cubic BezierpSink-AddBezier(D2D1::BezierSegment(D2D1::Point2F(100,50),// 控制点1D2D1::Point2F(200,50),// 控制点2D2D1::Point2F(250,150)// 终点));// 二次贝塞尔曲线Quadratic BezierpSink-AddQuadraticBezier(D2D1::QuadraticBezierSegment(D2D1::Point2F(150,50),// 控制点D2D1::Point2F(250,150)// 终点));// 弧线段pSink-AddArc(D2D1::ArcSegment(D2D1::Point2F(300,150),// 弧线终点D2D1::SizeF(80,80),// 椭圆半径0.0f,// 旋转角度D2D1_SWEEP_DIRECTION_CLOCKWISE,D2D1_ARC_SIZE_SMALL));贝塞尔曲线是矢量图形的基石。三次贝塞尔有两个控制点可以表达 S 形曲线二次贝塞尔有一个控制点适合表达简单的弧线。所有主流矢量图形格式SVG、PDF、AI的曲线都基于贝塞尔曲线。几何体的布尔运算Direct2D 支持对几何体进行布尔运算——并集Union、交集Intersect、差集Xor/Exclude。这些操作通过CombineWithGeometry方法实现// 创建两个圆形ID2D1EllipseGeometry*pCircle1NULL;ID2D1EllipseGeometry*pCircle2NULL;g_pFactory-CreateEllipseGeometry(D2D1::Ellipse(D2D1::Point2F(200,200),100,100),pCircle1);g_pFactory-CreateEllipseGeometry(D2D1::Ellipse(D2D1::Point2F(280,200),100,100),pCircle2);// 求并集ID2D1PathGeometry*pUnionNULL;g_pFactory-CreatePathGeometry(pUnion);ID2D1GeometrySink*pUnionSinkNULL;pUnion-Open(pUnionSink);pCircle1-CombineWithGeometry(pCircle2,D2D1_COMBINE_MODE_UNION,// 并集NULL,// 变换矩阵可选pUnionSink);pUnionSink-Close();SafeRelease(pUnionSink);D2D1_COMBINE_MODE枚举提供了四种布尔运算UNION并集两个形状合并、INTERSECT交集只保留重叠部分、XOR异或重叠部分被去掉、EXCLUDE差集从第一个形状中去掉第二个形状。这些操作在构建复杂的自定义形状时非常有用。命中测试让图形可交互几何体系统最强大的功能之一是精确的命中测试。GDI 时代你只能用PtInRect做矩形范围判断对于不规则形状无能为力。Direct2D 的FillContainsPoint和StrokeContainsPoint方法可以精确判断一个点是否在形状内部或描边上。实现可点击的五角星// 全局变量ID2D1PathGeometry*g_pStarGeometryNULL;D2D1_POINT_2F g_starCenterD2D1::Point2F(300,250);floatg_starOuterR100.0f;floatg_starInnerR40.0f;BOOL g_isStarHoveredFALSE;// 在 WM_MOUSEMOVE 中进行命中测试caseWM_MOUSEMOVE:{floatx(float)GET_X_LPARAM(lParam);floaty(float)GET_Y_LPARAM(lParam);if(g_pStarGeometry){BOOL containsFALSE;g_pStarGeometry-FillContainsPoint(D2D1::Point2F(x,y),NULL,// 变换矩阵contains// 输出结果);if(contains!g_isStarHovered){g_isStarHoveredcontains;InvalidateRect(hwnd,NULL,FALSE);// 状态变化时触发重绘}}return0;}FillContainsPoint判断点是否在几何体的填充区域内StrokeContainsPoint判断点是否在描边线段上可以指定线宽和容差。命中测试的精确度非常高——即使鼠标指针在五角星的两个角之间凹陷处也能正确判断为不在形状内部。描边命中测试// 判断点是否在描边线段上指定线宽容差BOOL isOnStrokeFALSE;g_pStarGeometry-StrokeContainsPoint(D2D1::Point2F(x,y),5.0f,// 描边线宽命中容差NULL,// 描边样式NULL,// 变换矩阵isOnStroke);描边命中测试的线宽参数实际上充当了命中容差——鼠标指针离描边线段多远以内算命中。设为 5.0f 意味着鼠标离描边 5 像素以内就会被检测到。这在做交互式编辑器时特别有用用户不需要精确地点击在线段上。几何体的其他操作Widen将描边转换为填充区域Widen方法可以将一个几何体的描边膨胀为一个新的填充几何体。这在需要给描边做渐变填充或裁切时很有用ID2D1PathGeometry*pWidenedNULL;g_pFactory-CreatePathGeometry(pWidened);ID2D1GeometrySink*pWidenSinkNULL;pWidened-Open(pWidenSink);g_pStarGeometry-Widen(8.0f,// 扩展宽度NULL,// 描边样式NULL,// 变换矩阵pWidenSink);pWidenSink-Close();SafeRelease(pWidenSink);GetBounds 和 GetWidenedBounds获取边界矩形D2D1_RECT_F bounds;g_pStarGeometry-GetBounds(NULL,bounds);// 获取描边后的边界考虑线宽D2D1_RECT_F widenedBounds;g_pStarGeometry-GetWidenedBounds(3.0f,NULL,NULL,widenedBounds);边界矩形在布局计算和脏区刷新时非常有用。比如你可以只在鼠标移动到几何体的边界矩形内时才做精确的命中测试避免不必要的计算开销。常见问题与调试问题1几何体绘制不出来没有报错首先检查你是否调用了GeometrySink::Close()。未 Close 的几何体在绘制时静默忽略不会给你任何错误提示。这是 Direct2D 中最隐蔽的坑之一。问题2CombineWithGeometry 结果为空如果两个几何体没有重叠INTERSECT运算会返回一个空几何体。同样如果第一个几何体完全被第二个包含EXCLUDE也会返回空。确保你的输入几何体确实有预期的重叠关系。问题3命中测试精度不够FillContainsPoint和StrokeContainsPoint有一个可选的flatteningTolerance参数默认值为D2D1_DEFAULT_FLATTENING_TOLERANCE约 0.25 像素。如果你发现命中测试不够精确可以尝试减小这个值但通常默认值已经足够好了。总结Direct2D 的几何体系统是一个完整的 2D 矢量图形引擎。从简单的矩形和椭圆到复杂的贝塞尔曲线路径从布尔运算组合到精确的命中测试它提供了构建交互式图形应用所需的全部工具。和 GDI 的Polygon/PolyBezier相比Direct2D 的几何体是独立于渲染的对象——你可以创建一次、多次绘制、随时做命中测试不需要每次重绘时重新定义形状。下一步我们要进入 Direct2D 的效果Effects系统。这是 Direct2D 1.1Windows 8引入的图像处理管线——通过连接多个效果节点你可以实现高斯模糊、阴影、色彩矩阵等高级视觉效果。配合图层Layer系统这些效果可以精确地应用到画面的局部区域而不是整个窗口。练习用PathGeometry和贝塞尔曲线绘制一个心形填充为红色并在鼠标悬停时变为亮红色。实现两个圆形的交集、并集、差集和异或可视化四个按钮切换不同的布尔运算模式。实现矢量图形编辑器原型支持点击添加矩形和椭圆拖拽移动已有图形使用命中测试Delete 键删除选中图形。用Widen方法将一个复杂路径的描边扩展为填充区域然后用渐变画刷填充扩展后的形状。参考资料:ID2D1PathGeometry interface - Microsoft LearnID2D1GeometrySink interface - Microsoft LearnID2D1Geometry::FillContainsPoint - Microsoft LearnID2D1Geometry::CombineWithGeometry - Microsoft LearnGeometries Overview - Microsoft LearnID2D1Geometry::Widen - Microsoft Learn相关阅读现代Qt开发教程新手篇1.1——QObject 与元对象系统 - 相似度 100%现代Qt开发教程新手篇1.2——信号与槽 - 相似度 100%通用GUI编程技术——图形渲染实战二十八——图像格式与编解码PNG/JPEG全掌握 - 相似度 100%

更多文章