在现代软件开发中,流程图是展示业务逻辑和系统架构的重要工具。你是否想过用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();
}
}
}
}
通过这个完整的流程图编辑器实现,我们不仅学会了C#图形编程的核心技术,更重要的是掌握了从需求分析到代码实现的完整开发思路。这些技能在GUI开发、游戏编程、数据可视化等领域都有广泛应用。
你在开发类似图形工具时还遇到过哪些挑战?在图形计算方面有什么独特的解决方案?欢迎在评论区分享你的经验和想法!
觉得这篇技术干货对你有帮助,请转发给更多需要提升C#图形编程技能的同行朋友! 🚀
相关信息
通过网盘分享的文件:AppFlowChart.zip 链接: https://pan.baidu.com/s/1_Gzdy_l3mi50KgqV9OcLYQ?pwd=3qy9 提取码: 3qy9 --来自百度网盘超级会员v9的分享
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!