Silverlight中使用遞歸構造關系圖
這兩天遇到一個問題,項目中需要在silverlight中使用連接圖的方式來顯示任務之間的關系,總體有父子和平行兩種,昨天在改同事的代碼,一直出問題,索性晚上寫了一下實現方法。
需求
有一個List對象中存了若干個Task,這些Task對象通過ParentID屬性進行關聯,現在要求將這個List中的任務使用圖的方式形成如父子關系和平行關系的圖示例如下圖:
實現方法思考
剛開始接到這個任務我就想著遞歸應該可以搞定了,但是仔細考慮才發現每個任務的子任務需要在一定區域內才行,需要計算子級和子級之間的距離,如果使用遞歸,例如上圖的元素“12”的位置就沒有辦法很好確定了。
我決定將途中的節點抽象為一個類,這個類至少應該含有上邊界top,左邊屆left及節點的名稱等屬性,然后從這個List對象中構造出每個節點的屬性。
實現步驟
1,首先我們為圖模擬一個數據源,注意其中的任務是通過ParentID關聯的。

public MainPage()
{
InitializeComponent();
listTask = new List<Task>();
listTask.Add(new Task() { ID = 1, ParentID = 0, Name = "1" });
listTask.Add(new Task() { ID = 2, ParentID = 1, Name = "11" });
listTask.Add(new Task() { ID = 3, ParentID = 1, Name = "12" });
listTask.Add(new Task() { ID = 4, ParentID = 2, Name = "21" });
listTask.Add(new Task() { ID = 5, ParentID = 2, Name = "22" });
listTask.Add(new Task() { ID = 6, ParentID = 3, Name = "31" });
listTask.Add(new Task() { ID = 7, ParentID = 3, Name = "32" });
listTask.Add(new Task() { ID = 8, ParentID = 3, Name = "33" });
listTask.Add(new Task() { ID = 9, ParentID = 4, Name = "42" });
listTask.Add(new Task() { ID = 10, ParentID =4, Name = "42" });
listTask.Add(new Task() { ID = 11, ParentID =3, Name = "34" });
listTask.Add(new Task() { ID = 12, ParentID = 5, Name = "51" });
listTask.Add(new Task() { ID = 13, ParentID = 8, Name = "81" });
this.Loaded += new RoutedEventHandler(MainPage_Loaded);
}
2,然后我們為要生成的圖中節點構造一個類。

{
public Task task { set; get; }
public double top { set; get; }
public double left { set; get; }
public int index { set; get; }//這是為了找到節點在某層的位置來計算left
}
3,使用遞歸將List中的數據做初步整理,存入一個List<TaskPro>中,此時節點對象將具備top屬性,上邊距搞定。

{
if (task.ParentID == 0)
{
listOfTaskPro.Add(new TaskPro() { task = task, top = 0, index = 0, left = 0 });
}
else
{
var t=listTask.Where(m=>m.ID==task.ParentID).FirstOrDefault();
var tpro=listOfTaskPro.Where(m=>m.task.ID==t.ID).FirstOrDefault();
listOfTaskPro.Add(new TaskPro() { task=task, index=0, top=tpro.top+50, left=0 });
}
foreach (Task t in listTask.Where(m=>m.ParentID==task.ID).ToList())
{
AddMethod(t);
}
}
4,我們需要算出節點對象的左邊距,在第3步中我沒能找到方法,于是想到利用每一級的元素個數來計算每個節點的位置,然后使用每一級的平均節點距離*節點的索引便可得到left。

foreach (TaskPro t in listOfTaskPro)
{
bool IsExist = false;
foreach (TaskCount tc in listTopAndTasks)
{
IsExist = tc.Top==t.top?true:false;
}
if (!IsExist)
{
listTopAndTasks.Add(new TaskCount() { Top = t.top, Tasks = new List<Task>() });
}
var topAndTasks = listTopAndTasks.Where(m => m.Top == t.top).FirstOrDefault();
topAndTasks.Tasks.Add(t.task);
}
//構造index
foreach (TaskPro t in listOfTaskPro)
{
for (int i = 0; i < listTopAndTasks.Count; i++)
{
for (int j = 0; j < listTopAndTasks[i].Tasks.Count; j++)
{
if (listTopAndTasks[i].Tasks[j].ID == t.task.ID)
{
t.index = j + 1;
}
}
}
}
//構造left
for (int i = 0; i < listOfTaskPro.Count; i++)
{
if (listOfTaskPro[i].task.ParentID == 0)
{
listOfTaskPro[i].left = this.canvas1.Width / 2;
}
else
{
var childCount = listOfTaskPro.Where(m => m.task.ParentID == listOfTaskPro[i].task.ParentID).Count();
var parentLeft = listOfTaskPro.Where(m => m.task.ID == listOfTaskPro[i].task.ParentID).FirstOrDefault().left;
var perLength = parentLeft * 1.5 / (childCount + 1);
listOfTaskPro[i].left = listOfTaskPro[i].index * perLength;
}
}
5,至此,節點對象已經具備了left,top屬性,我們只需要找到每個節點的父節點即可將兩個幾點的坐標確定,進而進行劃線的操作了。

{
AddBtn(t.task.Name, t.left, t.top);
if (t.task.ParentID != 0)
{
TaskPro tp = listOfTaskPro.Where(m => m.task.ID == t.task.ParentID).FirstOrDefault();
AddLine(tp.left + buttonWidth / 2, tp.top + buttonHeight, t.left + buttonWidth / 2, t.top);
}
}
6,添加按鈕及劃線的方法。

double buttonHeight = 20;
double buttonWidth = 50;
void AddBtn(string content, double left, double top)
{
Button btn = new Button();
btn.Content = content;
btn.Width = buttonWidth;
btn.Height = buttonHeight;
this.canvas1.Children.Add(btn);
Canvas.SetLeft(btn, left);
Canvas.SetTop(btn, top);
}
//畫線方法,只需要有起始亮點的坐標即可
void AddLine(double startLeft, double startTop, double endLeft, double endTop)
{
Path p = new Path();
LineGeometry geometry = new LineGeometry();
SolidColorBrush brush = new SolidColorBrush();
brush.Color = Colors.Black;
geometry.StartPoint = new Point(startLeft, startTop);
geometry.EndPoint = new Point(endLeft, endTop);
p.Data = geometry;
p.Stroke = brush;
p.StrokeThickness = 1;
canvas1.Children.Add(p);
}
#endregion
運行一下,如上圖。之前沒有使用遞歸的方法是只有這樣的。

using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace SilverlightApplication2
{
public class Task
{
public int ID { set; get; }
public int ParentID { set; get; }
public string Name { set; get; }
}
public partial class MainPage : UserControl
{
private static List<Task> listTask;
public MainPage()
{
InitializeComponent();
listTask = new List<Task>();
listTask.Add(new Task() { ID = 1, ParentID = 0, Name = "1" });
listTask.Add(new Task() { ID = 2, ParentID = 1, Name = "11" });
listTask.Add(new Task() { ID = 3, ParentID = 1, Name = "12" });
listTask.Add(new Task() { ID = 4, ParentID = 2, Name = "21" });
listTask.Add(new Task() { ID = 5, ParentID = 2, Name = "22" });
listTask.Add(new Task() { ID = 6, ParentID = 3, Name = "31" });
listTask.Add(new Task() { ID = 7, ParentID = 3, Name = "32" });
listTask.Add(new Task() { ID = 8, ParentID = 3, Name = "33" });
listTask.Add(new Task() { ID = 9, ParentID = 4, Name = "42" });
listTask.Add(new Task() { ID = 10, ParentID =4, Name = "42" });
listTask.Add(new Task() { ID = 11, ParentID =3, Name = "34" });
listTask.Add(new Task() { ID = 12, ParentID = 5, Name = "51" });
listTask.Add(new Task() { ID = 13, ParentID = 8, Name = "81" });
this.Loaded += new RoutedEventHandler(MainPage_Loaded);
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
AddAll();
}
class TaskPro
{
public Task task { set; get; }
public double top { set; get; }
public double left { set; get; }
public int index { set; get; }
}
class TaskCount
{
public double Top { set; get; }
public List<Task> Tasks { set; get; }
}
static List<TaskPro> listTaskPro = new List<TaskPro>();
static List<TaskCount> listTopAndTasks = new List<TaskCount>();
void AddAll()
{
foreach(Task t in listTask)
{
if (t.ParentID == 0)
{
listTaskPro.Add(new TaskPro() { task = t, index = 1, left = this.canvas1.Width / 2, top = 0 });
}
else
{
for(int i=0;i<listTaskPro.Count;i++)
{
if (t.ParentID == listTaskPro[i].task.ID)
{
listTaskPro.Add(new TaskPro() { task = t, top = listTaskPro[i].top + 80, index = 0, left = 0 });
}
}
}
}
#region 匯總層及層內的元素個數
foreach (TaskPro t in listTaskPro)
{
bool IsExist = false;
foreach(TaskCount tc in listTopAndTasks)
{
if(tc.Top==t.top)
{
IsExist = true;
}
}
if(!IsExist)
{
listTopAndTasks.Add(new TaskCount() { Top=t.top, Tasks=new List<Task>() });
}
var topAndTasks = listTopAndTasks.Where(m=>m.Top==t.top).FirstOrDefault();
topAndTasks.Tasks.Add(t.task);
}
#endregion
foreach (TaskPro t in listTaskPro)
{
for (int i = 0; i < listTopAndTasks.Count;i++ )
{
for (int j = 0; j < listTopAndTasks[i].Tasks.Count;j++ )
{
if (listTopAndTasks[i].Tasks[j].ID == t.task.ID)
{
t.index = j + 1;
}
}
}
}
for (int i = 0; i < listTaskPro.Count; i++)
{
if (listTaskPro[i].task.ParentID == 0)
{
listTaskPro[i].left = this.canvas1.Width / 2;
}
else
{
var childCount = listTaskPro.Where(m => m.task.ParentID == listTaskPro[i].task.ParentID).Count();
var parentLeft = listTaskPro.Where(m => m.task.ID == listTaskPro[i].task.ParentID).FirstOrDefault().left;
var perLength = parentLeft*1.5 / (childCount + 1);
listTaskPro[i].left=listTaskPro[i].index*perLength;
}
}
foreach (TaskPro t in listTaskPro)
{
AddBtn(t.task.Name, t.left, t.top);
if(t.task.ParentID!=0)
{
TaskPro tp = listTaskPro.Where(m=>m.task.ID==t.task.ParentID).FirstOrDefault();
AddLine(tp.left+buttonWidth/2, tp.top+buttonHeight, t.left+buttonWidth/2, t.top);
}
}
}
double buttonHeight = 20;
double buttonWidth = 50;
void AddBtn(string content,double left,double top)
{
Button btn = new Button();
btn.Content = content;
btn.Width = buttonWidth;
btn.Height = buttonHeight;
this.canvas1.Children.Add(btn);
Canvas.SetLeft(btn, left);
Canvas.SetTop(btn, top);
}
void AddLine(double startLeft,double startTop,double endLeft,double endTop)
{
Path p = new Path();
LineGeometry geometry = new LineGeometry();
SolidColorBrush brush = new SolidColorBrush();
brush.Color = Colors.Black;
geometry.StartPoint = new Point(startLeft, startTop);
geometry.EndPoint = new Point(endLeft, endTop);
p.Data = geometry;
p.Stroke = brush;
p.StrokeThickness = 1;
canvas1.Children.Add(p);
}
}
}