在图形应用程序中构建定向图






3.75/5 (3投票s)
本文介绍了一种使用控件构建有向图的简单方法。
引言
我开发了一个文档流应用程序,其中包含一个流程模块,该模块通过不同的路由(取决于用户答案)提供文档流。例如,动作 1 如果“是”,则为动作 2,如果“否”,则为动作 3,依此类推。
该流程是**有向图**,其中**节点**是阶段,**边**是连接。构建路由的简单方法是使用图形编辑器(用户可以通过鼠标移动和单击来构建)。我一直在 C# 中寻找类似的模块,但只找到了 piccolo.net 框架,但它对我来说“太难”了。因此,我决定自己开发一个简单的图表编辑器模块。
图表编辑器使用修改后的Button
和Panel
控件(GraphNode
、GraphPanel
- 类)构建一个简单的有向图。用户可以添加、删除、拖动节点,删除和添加连接,将节点标记为第一个元素。GraphNode
和GraphPanel
类允许用户使用任何事件(鼠标单击、拖动、移动等)。
背景
主要任务是构建有向图
- 每个节点最多有两个出边(“是”和“否”连接)和无限的入边。
- 边在鼠标悬停时高亮显示,事件处理程序允许拖动带有边的节点。
- 弹出菜单在鼠标右键单击边或节点时调用。
- 第一个节点有不同的颜色。
- 节点可以连接到自身。
首先,我必须选择一个容器(或背景)来绘制我的图。这是一个Panel
控件。它允许添加新控件,并为超出边界的控件提供自动滚动。GraphPanel
是Panel
控件的子类。它包括节点构建和管理的方法。GraphNode
类的鼠标事件在此定义。
其次,选择节点的基类。我选择了Button
控件,但您可以使用任何控件(ListView
、PictureBox
、Label
等)。我称之为GraphNode
类。该类有自己的字段和方法。GraphNode
包括两个点数组(每个出边的起点和终点)以及到连接节点的链接。修改它非常容易。
我使用非常简单的方法来解决我的任务
我使用上一个位置和当前位置之间的偏移量来移动Node
控件
//Set Node offset
//Params: offset
public void SetOffset(Point offset )
{
//Get current location
Point p=this.Location;
///set offset
p.Offset(offset);
//set new location
this.Location = p;
}
对于边的绘制,我使用标准的Graphics
方法DrawLine
和DrawCurve
,它们在我的GraphPanel
上绘制。起初,我使用绝对坐标(在面板中)来保存边,但使用这些绝对值存在一个问题,当您滚动面板时,所有坐标都会移动),因此,最好的方法是使用当前节点和连接节点之间的相对坐标。例如,EdgeYes[0]
是当前节点上的一个点,EdgeYes[1]
是NodeYes
控件上的一个点。每次面板无效时,所有边都会使用相对坐标重新绘制。
另一个问题是检查鼠标事件是否在Edge
上,因为Edge
只是一个在GraphPanel
上绘制的简单线条。我编写了一个函数来求解直线方程(x-x1)/(x2-x1)=(y-y1)/(y2-y1)。如果左边等于右边,则点(x,y)在直线(x1,y1)、(x2,y2)上。
private bool isPointIn(Point p1, Point p2, Point px)
{
//Check line bounds
if (((px.X > p1.X) && (px.X > p2.X)) || ((px.X < p1.X) && (px.X < p2.X)))
return false;
double r1 = (double)(px.X - p1.X) / (p2.X - p1.X);
double r2 = (double)(px.Y - p1.Y) / (p2.Y - p1.Y);
//if r1==r2 or r1=0 or r2=0 then px belongs to the line p1;p2
if ((r1 == 0) || (r2 == 0))
return true;
return Math.Round(r1, 1) == Math.Round(r2, 1);
}
使用代码
如果您想在自己的应用程序中使用这些类,只需包含GraphNode
和GraphPanel
类,然后更改命名空间,您就可以使用它们了。
我在GraphPanel
中定义了一些字段,以帮助用户设置图表
//Edge width
public int LineWidth = 2;
//Edge color
public Color LineColor = Color.Black;
LineWidth
是边的线宽,LineColor
是边的颜色。
您可以根据需要更改默认的GraphNode
属性(颜色、大小、控件等)。如果您想在鼠标右键单击节点时添加弹出菜单,可以定义它
public Form1()
{
InitializeComponent();
pnGraph.NodeMenu = mnuNode;
}
mnuNode.Tag
包含指向当前GraphNode
对象的链接。
手动
添加节点
在面板上鼠标右键单击,然后单击“添加”菜单项。
拖动节点
鼠标左键单击并移动它,直到鼠标松开。
添加连接(边)
- 在节点上鼠标右键单击,然后移动到另一个节点,然后松开鼠标按钮。
- “是”连接必须从左侧开始,“否”连接从右侧开始。
- 如果我们想让节点连接到自身 - 鼠标右键单击然后松开鼠标,然后从弹出菜单中选择“Cycle”。
- 使用此菜单,我们可以删除节点并将其标记为第一个节点(黄色)。
选择边
将鼠标光标放在边上(它会变成红色),然后鼠标右键单击。
保存有向图
我使用序列化来保存和打开图表,因此在GraphNode
类中,我定义了方法
public virtual void GetObjectData(SerializationInfo info, StreamingContext context) {
info.AddValue("Name",this.Name);
info.AddValue("Location", this.Location);
info.AddValue("Width", this.Width);
info.AddValue("Text", this.Text);
info.AddValue("isFirst", this.isFirst,typeof(Boolean));
info.AddValue("NodeYes", this.NodeYes,typeof(GraphNode));
info.AddValue("NodeNo", this.NodeNo, typeof(GraphNode));
info.AddValue("EdgeYes", this.EdgeYes, typeof(Point[]));
info.AddValue("EdgeNo", this.EdgeNo, typeof(Point[]));
}
它只保存此方法中列出的字段。此方法允许将图表保存到文件、数组、数据库,并从源加载。
老实说,这些类的源代码非常容易理解和使用。
附言。
这是我在这里的第一篇文章,请不要对我过于苛责。我希望它能对某人有所帮助,并帮助他们完成他们的项目。就我而言,我已经成功地在我的项目中使用过这些类了。