在软件开发过程中,经常需要处理复杂的流程图或决策树。这些流程图通常可以表示为有向图,其中节点代表流程中的不同阶段,而边则代表从一个阶段到另一个阶段的转换。本文将介绍如何使用C#开发一个简单的图形编辑器,允许用户通过图形界面创建和编辑有向图。
有向图是一种基本的数据结构,广泛应用于各种领域,如网络路由、工作流管理等。在有向图中,每个节点可以有多个出边(表示“是”和“否”的分支),但只能有一个入边。本文的目标是构建一个简单的图形编辑器,允许用户通过鼠标操作来添加、移动节点以及创建连接。
在设计图形编辑器时,首先需要确定容器(或背景)用于绘制图形。在本文中,选择了Panel控件作为容器,因为它允许添加新的控件并且具有自动滚动功能。
接下来,需要选择节点的基类。在本文中,选择了Button控件,但也可以使用其他控件,如ListView、PictureBox、Label等。将其称为GraphNode类。这个类包含自己的字段和方法,包括两个点的数组(每个出边的起点和终点)以及与连接节点的链接。
在实现过程中,首先定义了GraphNode和GraphPanel类。GraphPanel类是Panel控件的子类,它包含了节点构建和管理的方法。GraphNode类的鼠标事件在GraphPanel类中定义。
为了移动节点,定义了一个SetOffset方法,它使用当前位置和新位置之间的偏移量来更新节点的位置。
public void SetOffset(Point offset) {
Point p = this.Location;
p.Offset(offset);
this.Location = p;
}
在绘制边时,使用了标准的Graphics方法DrawLine和DrawCurve,它们在GraphPanel上绘制。最初,使用绝对坐标来保存边,但当滚动面板时,所有坐标都会发生偏移,因此最好使用当前节点和连接节点的相对坐标。
另一个问题是检查鼠标事件是否在边上。边是简单地在GraphPanel上绘制的线。编写了一个函数来解决线的方程,如果方程的两边相等,则点(x,y)在线上。
private bool isPointIn(Point p1, Point p2, Point px) {
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 == 0) || (r2 == 0)) return true;
return Math.Round(r1, 1) == Math.Round(r2, 1);
}
要在自己的应用程序中使用这些类,只需包含GraphNode和GraphPanel类,并更改命名空间即可。
在GraphPanel中定义了一些字段,帮助用户设置图形:
public int LineWidth = 2;
public Color LineColor = Color.Black;
LineWidth是边的线宽,LineColor是边的颜色。可以根据自己的喜好更改GraphNode的默认属性(颜色、大小、控件等)。如果想在鼠标右键点击节点时添加弹出菜单,可以这样定义:
public Form1() {
InitializeComponent();
pnGraph.NodeMenu = mnuNode;
}
mnuNode.Tag包含当前GraphNode对象的链接。
添加节点:在面板上右键点击,然后点击“添加”菜单项。
移动节点:鼠标左键点击并移动,直到鼠标松开。
添加连接(边):在节点上右键点击,然后移动到另一个节点并松开鼠标按钮。“是”连接必须从左侧开始,“否”连接从右侧开始。如果需要创建节点到自身的循环,可以先右键点击,然后松开鼠标并从弹出菜单中选择“循环”。
使用此菜单,可以删除节点并将其标记为第一个节点(黄色)。
选择边:将鼠标光标放在边上(它会变成红色),然后右键点击。
使用序列化来保存和打开图形,因此在GraphNode类中定义了GetObjectData方法:
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[]));
}
它只保存在该方法中枚举的字段。此方法允许将图形保存到文件、数组、数据库中,并从源加载。