深入解析:GDI+ 自定义形状绘制与文本居中策略
引言:从矩形到多边形的跃迁在流程图绘制中,矩形只是起点。真实业务场景中,决策节点需要菱形 ,输入/输出常用平行四边形 ,可选步骤用圆角矩形 来柔和视觉。本章将手把手带你实现这三种形状的绘制,并解决一个高频痛点------文本在任意几何形状中的精确居中 。### 1. 形状绘制的数学基础#### 1.1 菱形:旋转 45° 的正方形菱形本质是四个顶点坐标的线性组合。假设中心点为 (cx, cy),半宽为 hw,半高为 hh,则四个顶点分别为:- 上顶点:(cx, cy - hh)- 右顶点:(cx + hw, cy)- 下顶点:(cx, cy + hh)- 左顶点:(cx - hw, cy)关键点 :hw 和 hh 不一定相等,这样可以得到"扁菱形"或"瘦菱形",适配不同文本长度。#### 1.2 平行四边形:斜切变换平行四边形可看作矩形在水平方向上的错切(Shear)。若偏移量为 offset,则四个顶点为:- 左上:(x + offset, y)- 右上:(x + width + offset, y)- 右下:(x + width, y + height)- 左下:(x, y + height)注意 :偏移量应小于宽度的一半,否则形状会"翻转"成自交多边形。#### 1.3 圆角矩形:贝塞尔曲线拟合GDI+ 提供了现成的 GraphicsPath.AddArc 方法,但我们需要手动控制圆角半径。核心思路:1. 在四个角上用 AddArc 添加 90° 圆弧。2. 用直线连接相邻圆弧的端点。---### 2. 核心代码:形状绘制引擎csharpusing System.Drawing;using System.Drawing.Drawing2D;public static class ShapeDrawer{ /// <summary> /// 绘制菱形,返回用于文本居中的中心点 /// </summary> public static PointF DrawDiamond(Graphics g, RectangleF bounds, float penWidth) { PointF[] points = { new PointF(bounds.Left + bounds.Width / 2, bounds.Top), // 上 new PointF(bounds.Right, bounds.Top + bounds.Height / 2), // 右 new PointF(bounds.Left + bounds.Width / 2, bounds.Bottom), // 下 new PointF(bounds.Left, bounds.Top + bounds.Height / 2) // 左 }; using (Pen pen = new Pen(Color.Black, penWidth)) using (GraphicsPath path = new GraphicsPath()) { path.AddPolygon(points); g.DrawPath(pen, path); } // 返回几何中心(即对角线交点) return new PointF(bounds.Left + bounds.Width / 2, bounds.Top + bounds.Height / 2); } /// <summary> /// 绘制平行四边形,支持自定义斜切偏移量 /// </summary> public static PointF DrawParallelogram(Graphics g, RectangleF bounds, float offset, float penWidth) { // 防止偏移量过大导致形状翻转 offset = Math.Min(offset, bounds.Width / 2 - 1); PointF[] points = { new PointF(bounds.Left + offset, bounds.Top), new PointF(bounds.Right + offset, bounds.Top), new PointF(bounds.Right - offset, bounds.Bottom), new PointF(bounds.Left - offset, bounds.Bottom) }; using (Pen pen = new Pen(Color.Blue, penWidth)) using (GraphicsPath path = new GraphicsPath()) { path.AddPolygon(points); g.DrawPath(pen, path); } return new PointF(bounds.Left + bounds.Width / 2, bounds.Top + bounds.Height / 2); } /// <summary> /// 绘制圆角矩形,radius 控制圆角大小 /// </summary> public static PointF DrawRoundedRectangle(Graphics g, RectangleF bounds, float radius, float penWidth) { // 防止半径过大,超过宽高的一半 radius = Math.Min(radius, Math.Min(bounds.Width, bounds.Height) / 2); using (GraphicsPath path = new GraphicsPath()) { // 左上角圆弧 path.AddArc(bounds.Left, bounds.Top, radius * 2, radius * 2, 180, 90); // 右上角圆弧 path.AddArc(bounds.Right - radius * 2, bounds.Top, radius * 2, radius * 2, 270, 90); // 右下角圆弧 path.AddArc(bounds.Right - radius * 2, bounds.Bottom - radius * 2, radius * 2, radius * 2, 0, 90); // 左下角圆弧 path.AddArc(bounds.Left, bounds.Bottom - radius * 2, radius * 2, radius * 2, 90, 90); path.CloseFigure(); using (Pen pen = new Pen(Color.Green, penWidth)) { g.DrawPath(pen, path); } } return new PointF(bounds.Left + bounds.Width / 2, bounds.Top + bounds.Height / 2); }}代码解析 :- 所有方法均返回中心点,供后续文本绘制使用。- 使用 GraphicsPath 统一管理路径,便于绘制和复用。- 增加了防御性校验(如 offset 和 radius 的边界检查),避免异常图形。---### 3. 文本居中的高级技巧#### 3.1 为什么 StringFormat 不够用?GDI+ 的 StringFormat.Alignment 和 LineAlignment 只能基于矩形的边界对齐,但:- 菱形的中心与矩形边界中心一致,但文本若直接绘制在边界内,会"溢出"到菱形外部。- 平行四边形和圆角矩形同理,边界矩形不能代表实际可视区域。#### 3.2 解决方案:基于实际形状的尺寸缩放核心思路 :动态计算文本可用的最大矩形(内接矩形),然后在该矩形内居中。csharp/// <summary>/// 在任意形状内绘制居中文本/// </summary>public static void DrawCenteredText(Graphics g, string text, Font font, Brush brush, RectangleF shapeBounds, ShapeType type, float offset = 10){ // 1. 根据形状类型计算内接矩形比例 float scaleX = 1f, scaleY = 1f; switch (type) { case ShapeType.Diamond: // 菱形内接矩形是原矩形的一半 scaleX = 0.5f; scaleY = 0.5f; break; case ShapeType.Parallelogram: // 平行四边形内接矩形需考虑偏移量 scaleX = 0.8f; // 经验值,可调 scaleY = 0.8f; break; case ShapeType.RoundedRect: // 圆角矩形内接矩形需减去圆角半径 scaleX = 0.9f; scaleY = 0.9f; break; } float innerWidth = shapeBounds.Width * scaleX; float innerHeight = shapeBounds.Height * scaleY; RectangleF innerRect = new RectangleF( shapeBounds.X + (shapeBounds.Width - innerWidth) / 2, shapeBounds.Y + (shapeBounds.Height - innerHeight) / 2, innerWidth, innerHeight); // 2. 使用 StringFormat 居中绘制 StringFormat format = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; g.DrawString(text, font, brush, innerRect, format);}进阶优化 :对于菱形,可以更精确地计算内接矩形。设菱形半宽为 hw,半高为 hh,则内接矩形的宽为 hw * √2,高为 hh * √2。但为了通用性,上述代码采用了比例缩放法,兼顾性能与准确性。---### 4. 完整示例:集成到流程图绘制csharppublic void DrawFlowchartNode(Graphics g, RectangleF bounds, string text, ShapeType type, float radius = 10){ PointF center; switch (type) { case ShapeType.Diamond: center = ShapeDrawer.DrawDiamond(g, bounds, 2); DrawCenteredText(g, text, new Font("微软雅黑", 10), Brushes.Black, bounds, ShapeType.Diamond); break; case ShapeType.Parallelogram: center = ShapeDrawer.DrawParallelogram(g, bounds, 15, 2); DrawCenteredText(g, text, new Font("微软雅黑", 10), Brushes.Black, bounds, ShapeType.Parallelogram); break; case ShapeType.RoundedRect: center = ShapeDrawer.DrawRoundedRectangle(g, bounds, radius, 2); DrawCenteredText(g, text, new Font("微软雅黑", 10), Brushes.Black, bounds, ShapeType.RoundedRect); break; } // 可选:在中心点绘制一个小圆点用于调试 // g.FillEllipse(Brushes.Red, center.X - 2, center.Y - 2, 4, 4);}调用示例 :csharpusing (var g = panel.CreateGraphics()){ g.SmoothingMode = SmoothingMode.AntiAlias; DrawFlowchartNode(g, new RectangleF(50, 50, 120, 80), "判断", ShapeType.Diamond); DrawFlowchartNode(g, new RectangleF(200, 50, 140, 60), "输入数据", ShapeType.Parallelogram); DrawFlowchartNode(g, new RectangleF(400, 50, 150, 70), "处理步骤", ShapeType.RoundedRect, 15);}---### 5. 性能与视觉优化建议1. 抗锯齿 :绘制前设置 g.SmoothingMode = SmoothingMode.AntiAlias,否则边缘会有锯齿。2. 缓存路径 :如果节点不频繁移动,可将 GraphicsPath 缓存为字段,避免重复计算。3. 字体缩放 :当文本过长时,可动态缩小字号: csharp while (g.MeasureString(text, font).Width > innerRect.Width && font.Size > 6) font = new Font(font.FontFamily, font.Size - 0.5f); ---### 总结本章从几何定义出发,实现了三种常见流程图形状的精准绘制,并解决了文本居中这一关键问题。核心收获:- 形状绘制本质是数学建模 :理解顶点坐标、贝塞尔曲线,才能自由扩展更多形状。- 文本居中需适配实际形状 :不能简单依赖边界矩形,需计算内接区域。- 防御性编程 :对参数进行边界校验,避免产生自交或越界图形。在下一章中,我们将探讨连接线的自动路由 与箭头绘制,敬请期待。