基于 GDI+ 的流程图开发:矩形关联与连线逻辑实现
1. 基础连接逻辑:关联两个矩形中心
在流程图应用中,最基础的连线需求是连接两个已有图形。我们首先实现一种简单场景:计算两个矩形的几何中心点,并在它们之间绘制一条直线。
实现原理在于:实时获取两个矩形的坐标与尺寸,计算出中心点 $P(x, y)$,公式为 $x = X + Width/2, y = Y + Height/2$。当矩形位置发生变动时,重新计算这两个点并触发重绘。
以下是核心逻辑的初步实现示例:
public class NodeElement
{
public string NodeId { get; set; }
public Rectangle Bounds { get; set; }
}
// 定义连接点
private Point _startCenter = Point.Empty;
private Point _endCenter = Point.Empty;
// 计算中心位置
private void UpdateConnectionPoints()
{
if (_nodes.Count >= 2)
{
var n1 = _nodes[0].Bounds;
_startCenter = new Point(n1.X + n1.Width / 2, n1.Y + n1.Height / 2);
var n2 = _nodes[1].Bounds;
_endCenter = new Point(n2.X + n2.Width / 2, n2.Y + n2.Height / 2);
}
}
// 绘图方法
private void RenderCanvas(Graphics g)
{
g.SmoothingMode = SmoothingMode.AntiAlias;
foreach (var node in _nodes)
{
g.FillRectangle(Brushes.SkyBlue, node.Bounds);
}
if (!_startCenter.IsEmpty && !_endCenter.IsEmpty)
{
using (Pen p = new Pen(Color.Black, 2))
{
g.DrawLine(p, _startCenter, _endCenter);
}
}
}
2. 进阶功能:实现任意图形间的动态连线
在实际应用中,用户需要手动选择起始节点和终点节点。这需要引入"交互状态机"的概念,通过鼠标点击事件来捕获节点 ID,并建立关联关系。
我们需要定义一个连接对象 EdgeLink,存储起始节点和结束节点的标识符。在鼠标按下事件(MouseDown)中,根据当前是否处于"连线模式"来决定是拖动图形还是选择连线目标。
public class EdgeLink
{
public string LinkId { get; set; }
public string FromNodeId { get; set; }
public string ToNodeId { get; set; }
public Color LinkColor { get; set; } = Color.Black;
}
private List<EdgeLink> _edges = new List<EdgeLink>();
private NodeElement _linkingSource = null;
private bool _isLinkingMode = false;
private void Canvas_MouseDown(object sender, MouseEventArgs e)
{
var targetNode = _nodes.FindLast(n => n.Bounds.Contains(e.Location));
if (_isLinkingMode && targetNode != null)
{
if (_linkingSource == null)
{
_linkingSource = targetNode;
// 提示用户选择第二个节点
}
else if (_linkingSource.NodeId != targetNode.NodeId)
{
// 创建新的连接关系
_edges.Add(new EdgeLink {
LinkId = Guid.NewGuid().ToString(),
FromNodeId = _linkingSource.NodeId,
ToNodeId = targetNode.NodeId
});
ResetLinkingState();
Invalidate(); // 刷新画布
}
}
}
3. 功能优化:颜色区分、去重与操作中止
为了提升用户体验,我们需要完善以下细节:
- 颜色区分:通过索引或属性为不同的连线分配颜色,便于视觉识别。
- 连接去重:在建立连接前检查
_edges集合,避免在相同两个节点间重复绘线。 - 操作中止:允许用户通过右键或特定按钮退出连线模式,清除当前选中的起始节点。
优化后的绘图与校验逻辑如下:
private void CreateConnection(NodeElement source, NodeElement target)
{
// 去重检查:判断是否已存在相同方向或反向的连接
bool exists = _edges.Any(link =>
(link.FromNodeId == source.NodeId && link.ToNodeId == target.NodeId) ||
(link.FromNodeId == target.NodeId && link.ToNodeId == source.NodeId));
if (!exists)
{
var newLink = new EdgeLink {
LinkId = "L" + (_edges.Count + 1),
FromNodeId = source.NodeId,
ToNodeId = target.NodeId,
LinkColor = GetDynamicColor(_edges.Count)
};
_edges.Add(newLink);
}
}
private Color GetDynamicColor(int index)
{
Color[] palette = { Color.Red, Color.ForestGreen, Color.RoyalBlue, Color.Orange };
return palette[index % palette.Length];
}
// 综合绘制方法
private void OnDraw(Graphics g)
{
// 先画线,后画节点,保证节点遮盖线头
foreach (var link in _edges)
{
Point p1 = GetNodeCenter(link.FromNodeId);
Point p2 = GetNodeCenter(link.ToNodeId);
using (Pen customPen = new Pen(link.LinkColor, 2))
{
g.DrawLine(customPen, p1, p2);
}
}
foreach (var node in _nodes)
{
// 绘制矩形及文字标识
g.FillRectangle(Brushes.White, node.Bounds);
g.DrawRectangle(Pens.DimGray, node.Bounds);
g.DrawString(node.NodeId, this.Font, Brushes.Black, node.Bounds);
}
}
通过上述逻辑,我们构建了一个具备基础交互能力的连线系统。用户可以动态创建关联,且在拖动节点时,由于中心点是根据节点坐标实时计算的,连线会自动跟随移动,从而实现了动态布局的初步效果。