由于公司需要给用户展示逻辑图,于是要定义一套正交布局算法实现布局。笔者在网上找了一些例子都没有具体写清楚,于是翻遍了论文,并且整理了一套适用的算法。
这是我实现的结果,正交布局其实就是画一颗树。知道这一点对于设计有很大的帮助。首先介绍下实现树布局的几个原则:
P1.树的边不应该相互交叉。
P2.相同深度的所有节点赢绘制在同一水平线上。
P3.同一层级的树节点应该有一定的空隙。
P4.父母应该集中在子节点身上。
根据这几个原则,画树的时候分成两步:
1.初始化树数据(此步会记录树的上联下联节点数据,以及所有叶子节点的数据)。
2.画树(此步会根据树的后序遍历方法计算子节点位置,并调整父节点的位置,保持父节点相对子节点居中)。
笔者使用的是简单的winform的控件实现了一下思路,使用Button代表树节点,并Paint响应事件重绘窗口画红色的线。
下面是自己定义的树节点类,代码如下:
public class TreeNode
{
/// <summary>
/// 树节点编号
/// </summary>
public uint ID { get; set; }
/// <summary>
/// 树节点深度(从零开始)
/// </summary>
public int Deep { get; set; }
/// <summary>
/// 树节点子节点
/// </summary>
public List<TreeNode> Children { get; set; }
/// <summary>
/// 树节点父节点
/// </summary>
public TreeNode Parent { get; set; }
/// <summary>
/// 树节点横坐标
/// </summary>
public int X { get; set; }
/// <summary>
/// 树节点纵坐标
/// </summary>
public int Y { get; set; }
public TreeNode()
{
Children = new List<TreeNode>();
}
}
根据图一初始化的树,会有个全局变量记录根节点,既是节点2,3,4,7,9,10,11这几个节点。后序遍历树的顺序为:2-3-4-1-7-6-9-10-8-5-11-0,这里不懂的需要去复习下数据结构树的后序遍历。这里明显可以看出只要树的子节点之间做好步长空隙计算就可以保证计算完成之后满足所有设计原则。下面是实现这个思路的代码展示:
/// <summary>
/// 控件横向步长
/// </summary>
private const int STEP_X = 100;
/// <summary>
/// 控件纵向步长
/// </summary>
private const int STEP_Y = 30;
/// <summary>
/// 叶子节点集合
/// </summary>
private List<TreeNode> _leafNodesList;
/// <summary>
/// 树对象
/// </summary>
private TreeNode _root;
/// <summary>
/// 连线端点元组集合
/// </summary>
private List<Tuple<Point, Point>> _lineList = new List<Tuple<Point,Point>>();
private int _halfNodeSum;
private int _startY;
private int _startX;
public Form1()
{
InitializeComponent();
_root = InitTree();
_halfNodeSum = _leafNodesList.Count / 2;
_startY = Height / 2;
_startX = Width / 2 + 30;
DrawTree(_root);
this.Paint += Form1_Paint;
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
Pen pen = new Pen(Color.Red);
Graphics g = e.Graphics;
foreach (var kvItem in _lineList)
{
g.DrawLine(pen, kvItem.Item1, kvItem.Item2);
}
}
/// <summary>
/// 初始化树
/// </summary>
private TreeNode InitTree()
{
TreeNode root = CreateTreeNode(0, 0, null);
TreeNode root1 = CreateTreeNode(1, 1, root);
root.Children.Add(root1);
TreeNode root2 = CreateTreeNode(2, 2, root1);
root1.Children.Add(root2);
TreeNode root3 = CreateTreeNode(3, 2, root1);
root1.Children.Add(root3);
TreeNode root4 = CreateTreeNode(4, 2, root1);
root1.Children.Add(root4);
TreeNode root5 = CreateTreeNode(5, 1, root);
root.Children.Add(root5);
TreeNode root6 = CreateTreeNode(6, 2, root5);
root5.Children.Add(root6);
TreeNode root7 = CreateTreeNode(7, 3, root6);
root6.Children.Add(root7);
TreeNode root8 = CreateTreeNode(8, 2, root5);
root5.Children.Add(root8);
TreeNode root9 = CreateTreeNode(9, 3, root8);
root8.Children.Add(root9);
TreeNode root10 = CreateTreeNode(10, 3, root8);
root8.Children.Add(root10);
TreeNode root11 = CreateTreeNode(11, 1, root);
root.Children.Add(root11);
_leafNodesList = new List<TreeNode>() { root2, root3, root4, root7, root9, root10, root11 };
return root;
}
/// <summary>
/// 创建树节点
/// </summary>
private TreeNode CreateTreeNode(uint id, int deep, TreeNode pTreeNode)
{
TreeNode treeNode = new TreeNode() { ID = id, Deep = deep, X = deep * STEP_X, Parent = pTreeNode };
return treeNode;
}
private void DrawTree(TreeNode treeNode)
{
for (int i = 0; i < treeNode.Children.Count; i++)
{
DrawTree(treeNode.Children[i]);
}
int y = GetCoordinateY(treeNode);
treeNode.Y = y;
DrawButton(treeNode.ID.ToString(), treeNode.X, y);
for (int i = 0; i < treeNode.Children.Count; i++)
{
_lineList.Add(new Tuple<Point, Point>(new Point(treeNode.X + 30, treeNode.Y + 20), new Point(treeNode.Children[i].X + 30, treeNode.Children[i].Y + 20)));
}
}
private int GetCoordinateY(TreeNode treeNode)
{
int coordinateX = 0;
if (treeNode.Children.Count > 1)
{
coordinateX = (treeNode.Children[0].Y + treeNode.Children[treeNode.Children.Count - 1].Y) / 2;
}
else if (treeNode.Children.Count == 1)
{
coordinateX = treeNode.Children[0].Y;
}
else
{
coordinateX = GetLeafCoordinate(treeNode.ID);
}
return coordinateX;
}
/// <summary>
/// 获取叶子节点的Y轴坐标
/// </summary>
private int GetLeafCoordinate(uint id)
{
int coordinate = 0;
for (int i = 0; i < _leafNodesList.Count; i++)
{
if (_leafNodesList[i].ID != id) continue;
if (i <= _halfNodeSum)
{
coordinate = _startY + STEP_Y * (_halfNodeSum - i);
}
else
{
coordinate = _startY - STEP_Y * (i - _halfNodeSum);
}
break;
}
return coordinate;
}
private void DrawButton(string name, int x, int y)
{
Button button = new Button();
button.Size = new System.Drawing.Size(60, 20);
button.Location = new Point(x + 30, y + 10);
button.Text = name;
button.UseVisualStyleBackColor = true;
this.Controls.Add(button);
}
}
好了,这就是所有实现过程,笔者想要再优化一下,考虑怎么实现树的纵向展示与横向展示切换。
遇到的问题:笔者在画线的时候犯了个愚蠢的错误,使用控件的CreateGraphics方法并没有真正的重绘窗口,必须在Paint事件中重绘才会有效果。