编辑
2025-09-20
C#
00

目录

🎯 核心架构设计分析
📊 数据模型设计
🔗 连接对象的精妙设计
🎨 图形绘制的核心算法
🔥 智能边界点计算
💡 多形状边界计算策略
⚡ 交互体验优化技巧
🎯 双缓冲消除闪烁
🖱️ 智能鼠标事件处理
完整代码
📚 总结

在现代软件开发中,流程图是展示业务逻辑和系统架构的重要工具。你是否想过用C#从零开始构建一个功能完整的流程图编辑器?今天我们就来深入解析一个完整的WinForms流程图编辑器实现,让你掌握图形绘制、交互设计和面向对象编程的核心技巧。

本文将通过实际代码案例,带你了解如何实现节点创建、连接绘制、拖拽操作等核心功能,让你的C#技能更上一层楼!

🎯 核心架构设计分析

📊 数据模型设计

首先,我们来看看这个流程图编辑器的核心数据结构:

C#
// 节点类型枚举 public enum NodeType { Rectangle, // 矩形节点 Ellipse, // 椭圆节点 Diamond // 菱形节点 } // 连接方向枚举 public enum ConnectionDirection { Forward, // 正向箭头 (起始->结束) Backward, // 反向箭头 (结束->起始) Both, // 双向箭头 None // 无箭头 }

设计亮点:通过枚举类型明确定义节点和连接的类型,提高代码可读性和维护性。这种设计模式在复杂系统中尤其重要。

🔗 连接对象的精妙设计

C#
public class Connection { public FlowChartNode StartNode { get; set; } public FlowChartNode EndNode { get; set; } public ConnectionDirection Direction { get; set; } public Connection(FlowChartNode startNode, FlowChartNode endNode) : this(startNode, endNode, ConnectionDirection.Forward) { } public Connection(FlowChartNode startNode, FlowChartNode endNode, ConnectionDirection direction) { StartNode = startNode; EndNode = endNode; Direction = direction; } }

核心技巧:构造函数重载设计,默认使用正向连接,同时支持自定义方向。这种设计让API既简单易用,又具备完整功能。

🎨 图形绘制的核心算法

🔥 智能边界点计算

这是整个项目中最精彩的部分——如何让连接线精确地连接到节点边缘而不是中心:

C#
// 计算节点边缘的连接点 private Point GetNodeEdgePoint(FlowChartNode fromNode, FlowChartNode toNode) { Rectangle fromBounds = fromNode.Bounds; Rectangle toBounds = toNode.Bounds; // 计算两个节点中心点 Point fromCenter = new Point( fromBounds.X + fromBounds.Width / 2, fromBounds.Y + fromBounds.Height / 2); Point toCenter = new Point( toBounds.X + toBounds.Width / 2, toBounds.Y + toBounds.Height / 2); // 计算方向向量 double dx = toCenter.X - fromCenter.X; double dy = toCenter.Y - fromCenter.Y; double distance = Math.Sqrt(dx * dx + dy * dy); if (distance == 0) return fromCenter; // 单位方向向量 double unitX = dx / distance; double unitY = dy / distance; return GetNodeBoundaryPoint(fromNode, unitX, unitY); }

算法精髓:通过向量数学计算出精确的边界连接点,让连接线看起来更专业。这种几何计算在图形软件开发中应用极广。

💡 多形状边界计算策略

不同形状的边界计算需要不同的数学方法:

C#
// 矩形边界点计算 private Point GetRectangleBoundaryPoint(Rectangle bounds, Point center, double dirX, double dirY) { double halfWidth = bounds.Width / 2.0; double halfHeight = bounds.Height / 2.0; double t = Math.Min(halfWidth / Math.Abs(dirX), halfHeight / Math.Abs(dirY)); return new Point( (int)(center.X + dirX * t), (int)(center.Y + dirY * t) ); } // 椭圆边界点计算 private Point GetEllipseBoundaryPoint(Rectangle bounds, Point center, double dirX, double dirY) { double theta = Math.Atan2(dirY, dirX); double a = bounds.Width / 2.0; double b = bounds.Height / 2.0; double x = center.X + a * Math.Cos(theta); double y = center.Y + b * Math.Sin(theta); return new Point((int)x, (int)y); }

数学之美:每种形状都有其特定的数学模型,掌握这些算法让你的图形程序更加精准和专业。

⚡ 交互体验优化技巧

🎯 双缓冲消除闪烁

C#
public class CustomPanel : Panel { public CustomPanel() { // 启用双缓冲和自定义绘制 this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer | ControlStyles.ResizeRedraw, true); this.UpdateStyles(); } }

性能优化要点:双缓冲技术是WinForms图形程序必备的优化手段,能显著提升绘制性能和用户体验。

🖱️ 智能鼠标事件处理

C#
private void pnlMain_MouseDown(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { FlowChartNode clickedNode = GetNodeAt(e.Location); if (isConnecting) { // 连接模式下的处理逻辑 if (clickedNode != null && clickedNode != connectStartNode) { connections.Add(new Connection(connectStartNode, clickedNode, currentConnectionDirection)); isConnecting = false; connectStartNode = null; pnlMain.Invalidate(); } } else { // 选择和拖拽模式 selectedNode = clickedNode; if (selectedNode != null) { isDragging = true; dragNode = selectedNode; dragStartPoint = e.Location; } } } }

交互设计精髓:通过状态机模式管理不同的交互状态,让复杂的用户操作变得清晰有序。

完整代码

C#
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AppFlowChart { public class FlowChartEditor { public enum NodeType { Rectangle, Ellipse, Diamond } public class FlowChartNode { public NodeType NodeType { get; set; } public Rectangle Bounds { get; set; } public string Text { get; set; } public Color BackColor { get; set; } public FlowChartNode() { NodeType = NodeType.Rectangle; Bounds = new Rectangle(0, 0, 100, 60); Text = "新节点"; BackColor = Color.LightBlue; } } } }
C#
using static AppFlowChart.FlowChartEditor; namespace AppFlowChart { public partial class Form1 : Form { private List<FlowChartNode> nodes; private List<Connection> connections; private FlowChartNode selectedNode; private FlowChartNode dragNode; private bool isDragging; private Point dragStartPoint; private bool isConnecting; private FlowChartNode connectStartNode; private Point mousePosition; private ConnectionDirection currentConnectionDirection; public Form1() { InitializeComponent(); InitializeFlowChart(); } private void InitializeFlowChart() { nodes = new List<FlowChartNode>(); connections = new List<Connection>(); selectedNode = null; isDragging = false; isConnecting = false; currentConnectionDirection = ConnectionDirection.Forward; UpdateDirectionButtons(); } private void UpdateDirectionButtons() { // 重置所有按钮颜色 btnForward.BackColor = Color.LightGray; btnBackward.BackColor = Color.LightGray; btnBoth.BackColor = Color.LightGray; btnNone.BackColor = Color.LightGray; // 高亮当前选择的按钮 switch (currentConnectionDirection) { case ConnectionDirection.Forward: btnForward.BackColor = Color.Orange; break; case ConnectionDirection.Backward: btnBackward.BackColor = Color.Orange; break; case ConnectionDirection.Both: btnBoth.BackColor = Color.Orange; break; case ConnectionDirection.None: btnNone.BackColor = Color.Orange; break; } } private void DrawNode(Graphics g, FlowChartNode node) { Rectangle bounds = node.Bounds; // 绘制节点背景 using (Brush brush = new SolidBrush(node.BackColor)) { if (node.NodeType == NodeType.Rectangle) g.FillRectangle(brush, bounds); else if (node.NodeType == NodeType.Ellipse) g.FillEllipse(brush, bounds); else if (node.NodeType == NodeType.Diamond) g.FillPolygon(brush, GetDiamondPoints(bounds)); } // 绘制边框 Color borderColor = node == selectedNode ? Color.Red : Color.Black; int borderWidth = node == selectedNode ? 3 : 2; using (Pen pen = new Pen(borderColor, borderWidth)) { if (node.NodeType == NodeType.Rectangle) g.DrawRectangle(pen, bounds); else if (node.NodeType == NodeType.Ellipse) g.DrawEllipse(pen, bounds); else if (node.NodeType == NodeType.Diamond) g.DrawPolygon(pen, GetDiamondPoints(bounds)); } // 绘制文本 using (StringFormat sf = new StringFormat()) { sf.Alignment = StringAlignment.Center; sf.LineAlignment = StringAlignment.Center; using (Brush textBrush = new SolidBrush(Color.Black)) { g.DrawString(node.Text, this.Font, textBrush, bounds, sf); } } } private Point[] GetDiamondPoints(Rectangle bounds) { int centerX = bounds.X + bounds.Width / 2; int centerY = bounds.Y + bounds.Height / 2; return new Point[] { new Point(centerX, bounds.Y), new Point(bounds.Right, centerY), new Point(centerX, bounds.Bottom), new Point(bounds.X, centerY) }; } private void DrawConnection(Graphics g, Connection connection) { // 计算节点边缘的连接点,而不是中心点 Point startPoint = GetNodeEdgePoint(connection.StartNode, connection.EndNode); Point endPoint = GetNodeEdgePoint(connection.EndNode, connection.StartNode); using (Pen pen = new Pen(Color.Black, 2)) { g.DrawLine(pen, startPoint, endPoint); // 根据连接方向绘制箭头 switch (connection.Direction) { case ConnectionDirection.Forward: DrawArrow(g, pen, startPoint, endPoint); break; case ConnectionDirection.Backward: DrawArrow(g, pen, endPoint, startPoint); break; case ConnectionDirection.Both: DrawArrow(g, pen, startPoint, endPoint); DrawArrow(g, pen, endPoint, startPoint); break; case ConnectionDirection.None: // 不绘制箭头 break; } } } // 计算节点边缘的连接点 private Point GetNodeEdgePoint(FlowChartNode fromNode, FlowChartNode toNode) { Rectangle fromBounds = fromNode.Bounds; Rectangle toBounds = toNode.Bounds; // 计算两个节点中心点 Point fromCenter = new Point( fromBounds.X + fromBounds.Width / 2, fromBounds.Y + fromBounds.Height / 2); Point toCenter = new Point( toBounds.X + toBounds.Width / 2, toBounds.Y + toBounds.Height / 2); // 计算方向向量 double dx = toCenter.X - fromCenter.X; double dy = toCenter.Y - fromCenter.Y; double distance = Math.Sqrt(dx * dx + dy * dy); if (distance == 0) return fromCenter; // 单位方向向量 double unitX = dx / distance; double unitY = dy / distance; // 根据节点类型计算边缘点 return GetNodeBoundaryPoint(fromNode, unitX, unitY); } // 根据节点类型计算边界点 private Point GetNodeBoundaryPoint(FlowChartNode node, double directionX, double directionY) { Rectangle bounds = node.Bounds; Point center = new Point(bounds.X + bounds.Width / 2, bounds.Y + bounds.Height / 2); switch (node.NodeType) { case NodeType.Rectangle: return GetRectangleBoundaryPoint(bounds, center, directionX, directionY); case NodeType.Ellipse: return GetEllipseBoundaryPoint(bounds, center, directionX, directionY); case NodeType.Diamond: return GetDiamondBoundaryPoint(bounds, center, directionX, directionY); default: return center; } } // 计算矩形边界点 private Point GetRectangleBoundaryPoint(Rectangle bounds, Point center, double dirX, double dirY) { double halfWidth = bounds.Width / 2.0; double halfHeight = bounds.Height / 2.0; // 计算与矩形边界的交点 double t = Math.Min(halfWidth / Math.Abs(dirX), halfHeight / Math.Abs(dirY)); return new Point( (int)(center.X + dirX * t), (int)(center.Y + dirY * t) ); } // 计算椭圆边界点 private Point GetEllipseBoundaryPoint(Rectangle bounds, Point center, double dirX, double dirY) { // 获取目标点方向的极角 double theta = Math.Atan2(dirY, dirX); double a = bounds.Width / 2.0; double b = bounds.Height / 2.0; double x = center.X + a * Math.Cos(theta); double y = center.Y + b * Math.Sin(theta); return new Point((int)x, (int)y); } // 计算菱形边界点 private Point GetDiamondBoundaryPoint(Rectangle bounds, Point center, double dirX, double dirY) { double halfWidth = bounds.Width / 2.0; double halfHeight = bounds.Height / 2.0; // 根据方向角度计算交点 double absX = Math.Abs(dirX); double absY = Math.Abs(dirY); // 菱形边界条件:|x/a| + |y/b| = 1 double scale = 1.0 / (absX / halfWidth + absY / halfHeight); return new Point( (int)(center.X + dirX * scale), (int)(center.Y + dirY * scale) ); } // 箭头绘制方法 private void DrawArrow(Graphics g, Pen pen, Point start, Point end) { // 计算箭头方向 double dx = end.X - start.X; double dy = end.Y - start.Y; double angle = Math.Atan2(dy, dx); int arrowLength = 15; // 增加箭头长度 double arrowAngle = Math.PI / 6; // 30度角 // 计算箭头的两个端点 Point arrowPoint1 = new Point( (int)(end.X - arrowLength * Math.Cos(angle - arrowAngle)), (int)(end.Y - arrowLength * Math.Sin(angle - arrowAngle))); Point arrowPoint2 = new Point( (int)(end.X - arrowLength * Math.Cos(angle + arrowAngle)), (int)(end.Y - arrowLength * Math.Sin(angle + arrowAngle))); // 绘制箭头 using (Pen arrowPen = new Pen(pen.Color, pen.Width)) { g.DrawLine(arrowPen, end, arrowPoint1); g.DrawLine(arrowPen, end, arrowPoint2); // 可选:填充箭头(实心箭头) Point[] arrowPoints = { end, arrowPoint1, arrowPoint2 }; using (Brush arrowBrush = new SolidBrush(pen.Color)) { g.FillPolygon(arrowBrush, arrowPoints); } } } private void pnlMain_Paint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // 绘制连接线 foreach (var connection in connections) { DrawConnection(g, connection); } // 绘制临时连接线 if (isConnecting && connectStartNode != null) { using (Pen pen = new Pen(Color.Blue, 2)) { pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash; // 从节点边缘开始绘制临时连接线 Point startPoint = new Point( connectStartNode.Bounds.X + connectStartNode.Bounds.Width / 2, connectStartNode.Bounds.Y + connectStartNode.Bounds.Height / 2); g.DrawLine(pen, startPoint, mousePosition); // 在鼠标位置绘制临时箭头预览 if (currentConnectionDirection == ConnectionDirection.Forward || currentConnectionDirection == ConnectionDirection.Both) { using (Pen arrowPen = new Pen(Color.Blue, 2)) { DrawArrow(g, arrowPen, startPoint, mousePosition); } } } } // 绘制节点 foreach (var node in nodes) { DrawNode(g, node); } } private FlowChartNode GetNodeAt(Point location) { for (int i = nodes.Count - 1; i >= 0; i--) { if (nodes[i].Bounds.Contains(location)) return nodes[i]; } return null; } private void btnAddRectangle_Click(object sender, EventArgs e) { AddNode(NodeType.Rectangle); } private void btnAddEllipse_Click(object sender, EventArgs e) { AddNode(NodeType.Ellipse); } private void btnAddDiamond_Click(object sender, EventArgs e) { AddNode(NodeType.Diamond); } private void AddNode(NodeType nodeType) { FlowChartNode newNode = new FlowChartNode { NodeType = nodeType, Bounds = new Rectangle(50 + nodes.Count * 20, 50 + nodes.Count * 20, 100, 60), Text = $"节点{nodes.Count + 1}", BackColor = GetNodeColor(nodeType) }; nodes.Add(newNode); pnlMain.Invalidate(); } private Color GetNodeColor(NodeType nodeType) { switch (nodeType) { case NodeType.Rectangle: return Color.LightBlue; case NodeType.Ellipse: return Color.LightGreen; case NodeType.Diamond: return Color.LightYellow; default: return Color.White; } } private void btnConnect_Click(object sender, EventArgs e) { if (selectedNode != null) { isConnecting = true; connectStartNode = selectedNode; MessageBox.Show($"请点击目标节点以创建连接\n当前箭头方向: {GetDirectionDescription(currentConnectionDirection)}"); } else { MessageBox.Show("请先选择一个节点"); } } private string GetDirectionDescription(ConnectionDirection direction) { switch (direction) { case ConnectionDirection.Forward: return "正向 (→)"; case ConnectionDirection.Backward: return "反向 (←)"; case ConnectionDirection.Both: return "双向 (↔)"; case ConnectionDirection.None: return "无箭头 (─)"; default: return "未知"; } } private void btnDelete_Click(object sender, EventArgs e) { if (selectedNode != null) { connections.RemoveAll(c => c.StartNode == selectedNode || c.EndNode == selectedNode); nodes.Remove(selectedNode); selectedNode = null; pnlMain.Invalidate(); } else { MessageBox.Show("请先选择一个节点"); } } private void btnClear_Click(object sender, EventArgs e) { if (MessageBox.Show("确定要清空所有内容吗?", "确认", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { nodes.Clear(); connections.Clear(); selectedNode = null; pnlMain.Invalidate(); } } // 方向按钮事件处理 private void btnForward_Click(object sender, EventArgs e) { currentConnectionDirection = ConnectionDirection.Forward; UpdateDirectionButtons(); } private void btnBackward_Click(object sender, EventArgs e) { currentConnectionDirection = ConnectionDirection.Backward; UpdateDirectionButtons(); } private void btnBoth_Click(object sender, EventArgs e) { currentConnectionDirection = ConnectionDirection.Both; UpdateDirectionButtons(); } private void btnNone_Click(object sender, EventArgs e) { currentConnectionDirection = ConnectionDirection.None; UpdateDirectionButtons(); } private void pnlMain_MouseDown(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { FlowChartNode clickedNode = GetNodeAt(e.Location); if (isConnecting) { if (clickedNode != null && clickedNode != connectStartNode) { // 创建带方向的连接 connections.Add(new Connection(connectStartNode, clickedNode, currentConnectionDirection)); isConnecting = false; connectStartNode = null; pnlMain.Invalidate(); } else if (clickedNode == null) { isConnecting = false; connectStartNode = null; pnlMain.Invalidate(); } } else { selectedNode = clickedNode; if (selectedNode != null) { isDragging = true; dragNode = selectedNode; dragStartPoint = e.Location; } pnlMain.Invalidate(); } } } private void pnlMain_MouseUp(object sender, MouseEventArgs e) { isDragging = false; dragNode = null; } private void pnlMain_MouseMove(object sender, MouseEventArgs e) { mousePosition = e.Location; if (isDragging && dragNode != null) { int deltaX = e.X - dragStartPoint.X; int deltaY = e.Y - dragStartPoint.Y; dragNode.Bounds = new Rectangle( dragNode.Bounds.X + deltaX, dragNode.Bounds.Y + deltaY, dragNode.Bounds.Width, dragNode.Bounds.Height); dragStartPoint = e.Location; pnlMain.Invalidate(); } else if (isConnecting) { pnlMain.Invalidate(); } } } }

image.png

image.png

📚 总结

通过这个完整的流程图编辑器实现,我们不仅学会了C#图形编程的核心技术,更重要的是掌握了从需求分析到代码实现的完整开发思路。这些技能在GUI开发、游戏编程、数据可视化等领域都有广泛应用。

你在开发类似图形工具时还遇到过哪些挑战?在图形计算方面有什么独特的解决方案?欢迎在评论区分享你的经验和想法!

觉得这篇技术干货对你有帮助,请转发给更多需要提升C#图形编程技能的同行朋友! 🚀

相关信息

通过网盘分享的文件:AppFlowChart.zip 链接: https://pan.baidu.com/s/1_Gzdy_l3mi50KgqV9OcLYQ?pwd=3qy9 提取码: 3qy9 --来自百度网盘超级会员v9的分享

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!