我如何绘制代表项目的时间导向模型的网络模型?
How can i draw a Network Model that represents a time-oriented model of the project?
我想可视化活动及其与这样的网络模型的关系
我有table,想画模型。您建议使用哪种方法来解决此问题?
编辑:
当我向该程序添加节点数据(DataTable 包含超过 100 行的活动和前置任务列)并用作节点资源时,得到了
"Index was out of range. Must be non-negative and less than the size of the collection"
(根据@TaW 的回答),
在layoutNodeY()部分
行:
nodes.Values.ElementAt(i)[j].VY = 1f * j - c / 2
NodeChart NC = new NodeChart();
private void Form1_Load(object sender, EventArgs e)
{
for (int i = 0; i < sortedtable.Rows.Count - 1; i++)
{ List<string> pred = sortedtable.Rows[i][2].ToString().Split(',').ToList();
for (int j = 0; j < sortedtable.Rows.Count - 1; j++)
{
foreach (var item in pred)
{
if (item == sortedtable.Rows[j][0].ToString() + "." + sortedtable.Rows[j][1].ToString())
{
NC.theNodes.Add(new NetNode(sortedtable.Rows[i][0].ToString() + "." + sortedtable.Rows[i][1].ToString(), item));
}
}
}
}
}
部分数据table的截图:
我建议将尽可能多的复杂性放入数据结构中。
我经常使用 List<T>
并且曾经使用过 Dictionary<float, List<NetNode>>
请注意,这个 post 比 SO 答案通常要长很多;希望对大家有所启发..
让我们从一个节点开始class
- 应该知道自己要打印的名称和其他文本数据
- 它还需要知道 previous 节点 and 以允许在 both 方向上遍历..
- ..下一个个节点的列表
- 它也会知道它在布局中的虚拟位置;绘制时必须缩放以适合给定区域..
- 并且它应该知道如何绘制自身以及与其邻居的联系。
然后可以在一秒钟内收集和管理这些节点 class,可以分析它们以填充列表和位置。
这是结果,使用您的数据加上一个额外的节点:
现在让我们仔细看看代码。
第class个节点:
class NetNode
{
public string Text { get; set; }
public List<NetNode> prevNodes { get; set; }
public List<NetNode> nextNodes { get; set; }
public float VX { get; set; }
public float VY { get; set; }
public string prevNodeNames;
public NetNode(string text, string prevNodeNames)
{
this.prevNodeNames = prevNodeNames;
prevNodes = new List<NetNode>();
nextNodes = new List<NetNode>();
Text = text;
VX = -1;
VY = -1;
}
...
}
如您所见,它利用 List<T>
来保存自己的列表。它的构造函数需要一个 string
,它应该包含一个节点名称列表; 稍后 将由 NodeChart
对象解析,因为为此我们需要完整的节点集。
绘图代码很简单,仅作为概念验证。为了获得更好的曲线,您可以使用 DrawCurves
加上一些额外的点或构建所需的贝塞尔曲线控制点来轻松改进它。
箭头也是便宜的;不幸的是,内置端盖不是很好。为了改进,您可以创建一个自定义的,可能带有图形路径..
我们开始:
public void draw(Graphics g, float scale, float size)
{
RectangleF r = new RectangleF(VX * scale, VY * scale, size, size);
g.FillEllipse(Brushes.Beige, r);
g.DrawEllipse(Pens.Black, r);
using (StringFormat fmt = new StringFormat()
{ Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center})
using (Font f = new Font("Consolas", 20f))
g.DrawString(Text, f, Brushes.Blue, r, fmt);
foreach(var nn in nextNodes)
{
using (Pen pen = new Pen(Color.Green, 1f)
{ EndCap = System.Drawing.Drawing2D.LineCap.ArrowAnchor })
g.DrawLine(pen, getConnector(this, scale, false, size),
getConnector(nn, scale, true, size));
}
}
PointF getConnector(NetNode n, float scale, bool left, float size)
{
RectangleF r = new RectangleF(n.VX * scale, n.VY * scale, size, size);
float x = left ? r.Left : r.Right;
float y = r.Top + r.Height / 2;
return new PointF(x, y);
}
您需要扩展节点 class 以包含更多文本、颜色、字体等。
上面的 draw 方法是最长的代码之一。现在让我们看看 NodeChart
class。
它持有..:[=34=]
- 节点列表和..
- 起始节点列表。不过,实际上应该只有 一个 ;所以你可能想抛出一个 'start node not unique' exception..
- 分析节点数据的方法列表。
我遗漏了与将图形适合给定区域以及任何错误检查相关的任何内容..
class NodeChart
{
public List<NetNode> theNodes { get; set; }
public List<NetNode> startNodes { get; set; }
public NodeChart()
{
theNodes = new List<NetNode>();
startNodes = new List<NetNode>();
}
..
}
第一种方法使用前个节点的名称解析字符串:
public void fillPrevNodes()
{
foreach (var n in theNodes)
{
var pn = n.prevNodeNames.Split(',');
foreach (var p in pn)
{
var hit = theNodes.Where(x => x.Text == p);
if (hit.Count() == 1) n.prevNodes.Add(hit.First());
else if (hit.Count() == 0) startNodes.Add(n);
else Console.WriteLine(n.Text + ": prevNodeName '" + p +
"' not found or not unique!" );
}
}
}
下一个方法填写nextNodes
列表:
public void fillNextNodes()
{
foreach (var n in theNodes)
{
foreach (var pn in n.prevNodes) pn.nextNodes.Add(n);
}
}
现在我们有了数据,需要布置节点。 水平 布局很简单,但与分支数据一样,需要递归:
public void layoutNodeX()
{
foreach (NetNode n in startNodes) layoutNodeX(n, n.VX + 1);
}
public void layoutNodeX(NetNode n, float vx)
{
n.VX = vx;
foreach (NetNode nn in n.nextNodes) layoutNodeX(nn, vx + 1);
}
垂直布局有点复杂。它计算 每个 x 位置的节点并将它们平均分布。 Dictionary
承担了大部分工作:首先我们填写它,然后我们遍历它来设置值。 最后 我们将节点向上推到需要的程度 居中 它们..:[=34=]
public void layoutNodeY()
{
NetNode n1 = startNodes.First();
n1.VY = 0;
Dictionary<float, List<NetNode>> nodes =
new Dictionary<float, List<NetNode>>();
foreach (var n in theNodes)
{
if (nodes.Keys.Contains(n.VX)) nodes[n.VX].Add(n);
else nodes.Add(n.VX, new List<NetNode>() { n });
}
for (int i = 0; i < nodes.Count; i++)
{
int c = nodes[i].Count;
for (int j = 0; j < c; j++)
{
nodes.Values.ElementAt(i)[j].VY = 1f * j - c / 2;
}
}
float min = theNodes.Select(x => x.VY).Min();
foreach (var n in theNodes) n.VY -= min;
}
这里总结一下我如何从 Form
和 PictureBox
:
调用它
NodeChart NC = new NodeChart();
private void Form1_Load(object sender, EventArgs e)
{
NC.theNodes.Add(new NetNode("A",""));
NC.theNodes.Add(new NetNode("B","A"));
NC.theNodes.Add(new NetNode("C","B"));
NC.theNodes.Add(new NetNode("D","B"));
NC.theNodes.Add(new NetNode("T","B"));
NC.theNodes.Add(new NetNode("E","C"));
NC.theNodes.Add(new NetNode("F","D,T"));
NC.theNodes.Add(new NetNode("G","E,F"));
NC.fillPrevNodes();
NC.fillNextNodes();
NC.layoutNodeX();
NC.layoutNodeY();
pictureBox1.Invalidate();
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (NC.theNodes.Count <= 0) return;
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
foreach (var n in NC.theNodes) n.draw(e.Graphics, 100, 33);
}
除了已经提到的内容之外,您可能还想添加一个 y 缩放或 'leading' 参数来垂直分布节点,从而为额外的文本腾出更多空间..
更新:
这是您提供的数据的结果:
我做了一些修改:
- 我把第二个“35.2”改成了“35.3”。您的意思可能是“25.2”,但这会带来更多数据错误;你应该照顾好他们!请检查输出窗格!!
- 我在
Paint
事件中将比例更改为 n.draw(e.Graphics, 50, 30);
- 最后我在 NetNode.draw.
中将字体大小更改为 Font("Consolas", 10f)
您还必须确保 pbox 足够大 and/or docked/anchored 以允许使用表单调整大小。
我想可视化活动及其与这样的网络模型的关系
我有table,想画模型。您建议使用哪种方法来解决此问题?
编辑:
当我向该程序添加节点数据(DataTable 包含超过 100 行的活动和前置任务列)并用作节点资源时,得到了
"Index was out of range. Must be non-negative and less than the size of the collection"
(根据@TaW 的回答),
在layoutNodeY()部分
行:
nodes.Values.ElementAt(i)[j].VY = 1f * j - c / 2
NodeChart NC = new NodeChart();
private void Form1_Load(object sender, EventArgs e)
{
for (int i = 0; i < sortedtable.Rows.Count - 1; i++)
{ List<string> pred = sortedtable.Rows[i][2].ToString().Split(',').ToList();
for (int j = 0; j < sortedtable.Rows.Count - 1; j++)
{
foreach (var item in pred)
{
if (item == sortedtable.Rows[j][0].ToString() + "." + sortedtable.Rows[j][1].ToString())
{
NC.theNodes.Add(new NetNode(sortedtable.Rows[i][0].ToString() + "." + sortedtable.Rows[i][1].ToString(), item));
}
}
}
}
}
部分数据table的截图:
我建议将尽可能多的复杂性放入数据结构中。
我经常使用 List<T>
并且曾经使用过 Dictionary<float, List<NetNode>>
请注意,这个 post 比 SO 答案通常要长很多;希望对大家有所启发..
让我们从一个节点开始class
- 应该知道自己要打印的名称和其他文本数据
- 它还需要知道 previous 节点 and 以允许在 both 方向上遍历..
- ..下一个个节点的列表
- 它也会知道它在布局中的虚拟位置;绘制时必须缩放以适合给定区域..
- 并且它应该知道如何绘制自身以及与其邻居的联系。
然后可以在一秒钟内收集和管理这些节点 class,可以分析它们以填充列表和位置。
这是结果,使用您的数据加上一个额外的节点:
现在让我们仔细看看代码。
第class个节点:
class NetNode
{
public string Text { get; set; }
public List<NetNode> prevNodes { get; set; }
public List<NetNode> nextNodes { get; set; }
public float VX { get; set; }
public float VY { get; set; }
public string prevNodeNames;
public NetNode(string text, string prevNodeNames)
{
this.prevNodeNames = prevNodeNames;
prevNodes = new List<NetNode>();
nextNodes = new List<NetNode>();
Text = text;
VX = -1;
VY = -1;
}
...
}
如您所见,它利用 List<T>
来保存自己的列表。它的构造函数需要一个 string
,它应该包含一个节点名称列表; 稍后 将由 NodeChart
对象解析,因为为此我们需要完整的节点集。
绘图代码很简单,仅作为概念验证。为了获得更好的曲线,您可以使用 DrawCurves
加上一些额外的点或构建所需的贝塞尔曲线控制点来轻松改进它。
箭头也是便宜的;不幸的是,内置端盖不是很好。为了改进,您可以创建一个自定义的,可能带有图形路径..
我们开始:
public void draw(Graphics g, float scale, float size)
{
RectangleF r = new RectangleF(VX * scale, VY * scale, size, size);
g.FillEllipse(Brushes.Beige, r);
g.DrawEllipse(Pens.Black, r);
using (StringFormat fmt = new StringFormat()
{ Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center})
using (Font f = new Font("Consolas", 20f))
g.DrawString(Text, f, Brushes.Blue, r, fmt);
foreach(var nn in nextNodes)
{
using (Pen pen = new Pen(Color.Green, 1f)
{ EndCap = System.Drawing.Drawing2D.LineCap.ArrowAnchor })
g.DrawLine(pen, getConnector(this, scale, false, size),
getConnector(nn, scale, true, size));
}
}
PointF getConnector(NetNode n, float scale, bool left, float size)
{
RectangleF r = new RectangleF(n.VX * scale, n.VY * scale, size, size);
float x = left ? r.Left : r.Right;
float y = r.Top + r.Height / 2;
return new PointF(x, y);
}
您需要扩展节点 class 以包含更多文本、颜色、字体等。
上面的 draw 方法是最长的代码之一。现在让我们看看 NodeChart
class。
它持有..:[=34=]
- 节点列表和..
- 起始节点列表。不过,实际上应该只有 一个 ;所以你可能想抛出一个 'start node not unique' exception..
- 分析节点数据的方法列表。
我遗漏了与将图形适合给定区域以及任何错误检查相关的任何内容..
class NodeChart
{
public List<NetNode> theNodes { get; set; }
public List<NetNode> startNodes { get; set; }
public NodeChart()
{
theNodes = new List<NetNode>();
startNodes = new List<NetNode>();
}
..
}
第一种方法使用前个节点的名称解析字符串:
public void fillPrevNodes()
{
foreach (var n in theNodes)
{
var pn = n.prevNodeNames.Split(',');
foreach (var p in pn)
{
var hit = theNodes.Where(x => x.Text == p);
if (hit.Count() == 1) n.prevNodes.Add(hit.First());
else if (hit.Count() == 0) startNodes.Add(n);
else Console.WriteLine(n.Text + ": prevNodeName '" + p +
"' not found or not unique!" );
}
}
}
下一个方法填写nextNodes
列表:
public void fillNextNodes()
{
foreach (var n in theNodes)
{
foreach (var pn in n.prevNodes) pn.nextNodes.Add(n);
}
}
现在我们有了数据,需要布置节点。 水平 布局很简单,但与分支数据一样,需要递归:
public void layoutNodeX()
{
foreach (NetNode n in startNodes) layoutNodeX(n, n.VX + 1);
}
public void layoutNodeX(NetNode n, float vx)
{
n.VX = vx;
foreach (NetNode nn in n.nextNodes) layoutNodeX(nn, vx + 1);
}
垂直布局有点复杂。它计算 每个 x 位置的节点并将它们平均分布。 Dictionary
承担了大部分工作:首先我们填写它,然后我们遍历它来设置值。 最后 我们将节点向上推到需要的程度 居中 它们..:[=34=]
public void layoutNodeY()
{
NetNode n1 = startNodes.First();
n1.VY = 0;
Dictionary<float, List<NetNode>> nodes =
new Dictionary<float, List<NetNode>>();
foreach (var n in theNodes)
{
if (nodes.Keys.Contains(n.VX)) nodes[n.VX].Add(n);
else nodes.Add(n.VX, new List<NetNode>() { n });
}
for (int i = 0; i < nodes.Count; i++)
{
int c = nodes[i].Count;
for (int j = 0; j < c; j++)
{
nodes.Values.ElementAt(i)[j].VY = 1f * j - c / 2;
}
}
float min = theNodes.Select(x => x.VY).Min();
foreach (var n in theNodes) n.VY -= min;
}
这里总结一下我如何从 Form
和 PictureBox
:
NodeChart NC = new NodeChart();
private void Form1_Load(object sender, EventArgs e)
{
NC.theNodes.Add(new NetNode("A",""));
NC.theNodes.Add(new NetNode("B","A"));
NC.theNodes.Add(new NetNode("C","B"));
NC.theNodes.Add(new NetNode("D","B"));
NC.theNodes.Add(new NetNode("T","B"));
NC.theNodes.Add(new NetNode("E","C"));
NC.theNodes.Add(new NetNode("F","D,T"));
NC.theNodes.Add(new NetNode("G","E,F"));
NC.fillPrevNodes();
NC.fillNextNodes();
NC.layoutNodeX();
NC.layoutNodeY();
pictureBox1.Invalidate();
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (NC.theNodes.Count <= 0) return;
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
foreach (var n in NC.theNodes) n.draw(e.Graphics, 100, 33);
}
除了已经提到的内容之外,您可能还想添加一个 y 缩放或 'leading' 参数来垂直分布节点,从而为额外的文本腾出更多空间..
更新:
这是您提供的数据的结果:
我做了一些修改:
- 我把第二个“35.2”改成了“35.3”。您的意思可能是“25.2”,但这会带来更多数据错误;你应该照顾好他们!请检查输出窗格!!
- 我在
Paint
事件中将比例更改为n.draw(e.Graphics, 50, 30);
- 最后我在 NetNode.draw. 中将字体大小更改为
Font("Consolas", 10f)
您还必须确保 pbox 足够大 and/or docked/anchored 以允许使用表单调整大小。