65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.75/5 (3投票s)

2013年3月17日

CPOL

4分钟阅读

viewsIcon

28273

downloadIcon

1593

本文介绍了一种使用控件构建有向图的简单方法。

引言

我开发了一个文档流应用程序,其中包含一个流程模块,该模块通过不同的路由(取决于用户答案)提供文档流。例如,动作 1 如果“是”,则为动作 2,如果“否”,则为动作 3,依此类推。

该流程是**有向图**,其中**节点**是阶段,**边**是连接。构建路由的简单方法是使用图形编辑器(用户可以通过鼠标移动和单击来构建)。我一直在 C# 中寻找类似的模块,但只找到了 piccolo.net 框架,但它对我来说“太难”了。因此,我决定自己开发一个简单的图表编辑器模块。

图表编辑器使用修改后的ButtonPanel控件(GraphNodeGraphPanel - 类)构建一个简单的有向图。用户可以添加、删除、拖动节点,删除和添加连接,将节点标记为第一个元素。GraphNodeGraphPanel类允许用户使用任何事件(鼠标单击、拖动、移动等)。

背景 

主要任务是构建有向图

  1. 每个节点最多有两个出边(“是”和“否”连接)和无限的入边。
  2. 边在鼠标悬停时高亮显示,事件处理程序允许拖动带有边的节点。
  3. 弹出菜单在鼠标右键单击边或节点时调用。
  4. 第一个节点有不同的颜色。
  5. 节点可以连接到自身。

首先,我必须选择一个容器(或背景)来绘制我的图。这是一个Panel控件。它允许添加新控件,并为超出边界的控件提供自动滚动。GraphPanelPanel控件的子类。它包括节点构建和管理的方法。GraphNode类的鼠标事件在此定义。

其次,选择节点的基类。我选择了Button控件,但您可以使用任何控件(ListViewPictureBoxLabel等)。我称之为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方法DrawLineDrawCurve,它们在我的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);
} 

使用代码 

如果您想在自己的应用程序中使用这些类,只需包含GraphNodeGraphPanel类,然后更改命名空间,您就可以使用它们了。

我在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[]));		
}

它只保存此方法中列出的字段。此方法允许将图表保存到文件、数组、数据库,并从源加载。

老实说,这些类的源代码非常容易理解和使用。

附言。

这是我在这里的第一篇文章,请不要对我过于苛责。我希望它能对某人有所帮助,并帮助他们完成他们的项目。就我而言,我已经成功地在我的项目中使用过这些类了。

© . All rights reserved.