Bootstrap

WPF制作流程图的代码及思路

本文将详细介绍wpf流程图的一种实现方式

  1. 流程图结构分析

我们知道流程图的基本功能就是可以将各个模块连接起来,连接时会产生一定的效果。如图(1)

图 1(大致成果)

我们可以将每一个模块称为一个Node,每一个Node上都可能存在多个输入口和一个输出口一样,就像是一个函数可以有多个输入参数,和一个返回值。每一个输出口可以连接多个输入口,一个输入口只能连接一个输出口。(黄色的圆点是输出口(OutputPort),绿色的是输出口(InputPort))。Nodedou会通过紫色的线连接,我们将紫色的线称为Link。

2.代码讲解

 public  class PortBase:ListBoxItem
    {
        public PortBase()
        {
            this.SetValue(StyleProperty, Application.Current.Resources[typeof(PortBase)]);
        }
        static PortBase()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(PortBase), new FrameworkPropertyMetadata(typeof(PortBase)));
        }
        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);
            this.IsSelected = true;
            e.Handled= true;
        }

        internal Node NodeParent => VisualTreeUtility.FindParent<Node>(this);

        internal  GraphControl GraphControlParent => VisualTreeUtility.FindParent<GraphControl>(this);



        public Point Center
        {
            get { return (Point)GetValue(CenterProperty); }
            set { SetValue(CenterProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Center.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CenterProperty =
            DependencyProperty.Register("Center", typeof(Point), typeof(PortBase));





        public Brush SelectedBorderBrush
        {
            get { return (Brush)GetValue(SelectedBorderBrushProperty); }
            set { SetValue(SelectedBorderBrushProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SelectedBorderBrush.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedBorderBrushProperty =
            DependencyProperty.Register("SelectedBorderBrush", typeof(Brush), typeof(PortBase), new PropertyMetadata(Brushes.Yellow));




        public Thickness SelectedBorderThickness
        {
            get { return (Thickness)GetValue(SelectedBorderThicknessProperty); }
            set { SetValue(SelectedBorderThicknessProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SelectedBorderThickness.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedBorderThicknessProperty =
            DependencyProperty.Register("SelectedBorderThickness", typeof(Thickness), typeof(PortBase), new PropertyMetadata(new Thickness(1)));



        public CornerRadius CornerRadius
        {
            get { return (CornerRadius)GetValue(CornerRadiusProperty); }
            set { SetValue(CornerRadiusProperty, value); }
        }

        // Using a DependencyProperty as the backing store for CornerRadius.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CornerRadiusProperty =
            DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(PortBase), new PropertyMetadata(new CornerRadius(5)));



        public void UpdatePosition()
        {
            Center= this.TransformToAncestor(GraphControlParent).Transform(new Point(this.ActualWidth/2, this.ActualHeight/2));
            if (this.GraphControlParent.LinksControl is null)
            {
                throw new NullReferenceException("LinksControl Can not be null");
            }
            foreach (var item in GraphControlParent.LinksControl.Items)
            {
                var link = (Link)GraphControlParent.LinksControl.ItemContainerGenerator.ContainerFromItem(item);
                if(link.Source==this)
                {
                    link.SourcePoint = this.Center;
                }
                if(link.Target==this)
                {
                    link.TargetPoint = this.Center;
                }
            }
        }

        public bool IsNear(Point point)
        {
            double vX=point.X-Center.X;
            double vY=point.Y-Center.Y;
            return Math.Sqrt(vX*vX+ vY*vY) <= 10;
        }

    }

PortBase

Port是可以被选中的所以我选择继承自 ListBoxIte。

UpdatePosition

是更新自身的位置信息。当我们拖动某一个Node的时候该Node上的Port的位置信息都会更新,以及更新与当前port连接的Link的起始点或者终点。

IsNear

是为了判断一个点是不是在改Port附近。我这里的主要功能是为了判断鼠标是不是在Port附近,如果在port附近我会让某一个Link的终点为当前的Port也就是为一个自动吸附的功能做基础。

NodeParent

当前Port所在的Node

Center

主要是为了保存Port中心点的坐标。

其他的属性可以看也可以不看,基本无关紧要。

public class OutputPort : PortBase
    {

        public List<Link> AttachedLinks { get; } = new();

        public OutputPort()
        {
            
        }
        static OutputPort()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(OutputPort), new FrameworkPropertyMetadata(typeof(OutputPort)));

          //  StyleProperty.OverrideMetadata(typeof(OutputPort), new FrameworkPropertyMetadata(Application.Current.Resources[typeof(OutputPort)]));
           
        }
        public object OutputValue
        {
            get { return (object)GetValue(OutputValueProperty); }
            set { SetValue(OutputValueProperty, value); }
        }

        // Using a DependencyProperty as the backing store for OutputValue.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty OutputValueProperty =
            DependencyProperty.Register("OutputValue", typeof(object), typeof(OutputPort), new PropertyMetadata(null));



        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);
           if( this.GraphControlParent.LinksControl is null)
            {
                throw new NullReferenceException("LinksControl Can not be null");
            }
            this.GraphControlParent.LinksControl.OnRemoveLink -= LinksControl_OnRemoveLink;
            this.GraphControlParent.LinksControl.OnRemoveLink += LinksControl_OnRemoveLink;
            Link link = new Link();
            link.IsSelected = true;
            link.IsDraging = true;
            link.Source = this;
            link.TargetPoint = this.Center;
            this.AttachedLinks.Add(link);
           
            this.GraphControlParent.LinksControl.Items.Add(link);
            ReleaseMouseCapture();

        }
       
        private void LinksControl_OnRemoveLink(object? sender, Link e)
        {
            if (AttachedLinks.Contains(e))
            {
                AttachedLinks.Remove(e);
            }
        }
    }

OutputPort

AttachedLinks

该属性主要是为了保存连接到该OutputPort上的一些Link,之前说过一个输出口可以连接多个输入口。

OutputValue

该属性主要是为了保存输出的值,因为输出的值可能是任何类型,所以我这里给了一个object,同时这是一个依赖属性,方便我们去VM中去操作它。

OnMouseLeftButtonDown

这是重写了一个方法,他主要的功能是当我们鼠标在OutputPort上按下的时候就要去创建一个Link了,而当前的这个OutputPort就是Link的起始点。往下看,看到LinksControl的时候或许你就会明白。


 public class InputPort:PortBase
    {
        internal Link? AttachedLink { get; set; }



        public Type ExpectationType
        {
            get { return (Type)GetValue(ExpectationTypeProperty); }
            set { SetValue(ExpectationTypeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ExpectationType.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ExpectationTypeProperty =
            DependencyProperty.Register("ExpectationType", typeof(Type), typeof(InputPort), new PropertyMetadata(null));



        public bool Validation
        {
            get { return (bool)GetValue(ValidationProperty); }
            set { SetValue(ValidationProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Validation.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ValidationProperty =
            DependencyProperty.Register("Validation", typeof(bool), typeof(InputPort), new PropertyMetadata(true));



        public object InputValue
        {
            get { return (object)GetValue(InputValueProperty); }
            set { SetValue(InputValueProperty, value); }
        }

        // Using a DependencyProperty as the backing store for InputValue.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty InputValueProperty =
            DependencyProperty.Register("InputValue", typeof(object), typeof(InputPort), new PropertyMetadata(null,OnInputValueChanged));
   
        static InputPort()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(InputPort), new FrameworkPropertyMetadata(typeof(InputPort)));
           
        }

        private static void OnInputValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var port = (InputPort)d;
            if(port.ExpectationType is null||e.NewValue is null)
            {
                port.Validation = true;
            }
            else
            {
                port.Validation = e.NewValue.GetType() == port.ExpectationType;
            }
        }
    }

InputPort

AttachedLink

这个属性是为了保存连接到当前InputPort上的Link,可以看到我这里只个了一个,说明一个Inputport只能有一个Link,只能连接一个OutputPort。

ExpectationType

这表示的是你希望这个输入口输入的参数是什么类型的,这个属性会结合 InputValue属性和 Validation属性,当输入的属性和期望的属性不一样的时候Validation会为false,我们就可以去UI上提醒,表示连接有误,或者直接不让连接这个口。


 public class InputPortsControl : ListBox
    {
        protected override DependencyObject GetContainerForItemOverride()
        {
            return new InputPort();
        }
        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return item is InputPort;
        }




        public string InputValueField
        {
            get { return (string)GetValue(InputValueFieldProperty); }
            set { SetValue(InputValueFieldProperty, value); }
        }

        // Using a DependencyProperty as the backing store for InputValueField.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty InputValueFieldProperty =
            DependencyProperty.Register("InputValueField", typeof(string), typeof(InputPortsControl), new PropertyMetadata(string.Empty));

        public InputPortsControl()
        {
            this.SetValue(StyleProperty, Application.Current.Resources[typeof(InputPortsControl)]);
            ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
        }

        private void ItemContainerGenerator_StatusChanged(object? sender, EventArgs e)
        {
            if (ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
            {
                Binding itemSourceBind = BindingOperations.GetBinding(this, ItemsSourceProperty);
                if (!string.IsNullOrWhiteSpace(InputValueField))
                {

                    foreach (var item in Items)
                    {
                        Binding binding = new Binding(InputValueField);
                        var port = (InputPort)ItemContainerGenerator.ContainerFromItem(item);
                        binding.Source = item;
                        binding.Mode = BindingMode.TwoWay;
                        port.SetBinding(InputPort.InputValueProperty, binding);
                    }
                }
            }
        }

        static InputPortsControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(InputPortsControl), new FrameworkPropertyMetadata(typeof(InputPortsControl)));
        }



    }

InputPortsControl

该控件主要是为了管理输入口集合的,里面的方法对实现流程图没有影响,大概了解也就行了。

  [TemplatePart(Name = "PART_Polyline", Type = typeof(Polyline))]
    public class Link : ListBoxItem
    {
        public event EventHandler<InputPort>? OnLinked;
        public event EventHandler<OutputPort>? OnBeginLink;
        public bool IsDraging { get; set; }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            Polyline = (Polyline)Template.FindName("PART_Polyline", this);
            Polygon = (Polygon)Template.FindName("PART_Polygon", this);
        }

        public Point SourcePoint
        {
            get { return (Point)GetValue(SourcePointProperty); }
            set { SetValue(SourcePointProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SourcePoint.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SourcePointProperty =
            DependencyProperty.Register("SourcePoint", typeof(Point), typeof(Link), new PropertyMetadata(default(Point), OnSourcePointChanged));

        private static void OnSourcePointChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var link = ((Link)d);
            link.UpdatePolyline();

        }

        public Point TargetPoint
        {
            get { return (Point)GetValue(TargetPointProperty); }
            set { SetValue(TargetPointProperty, value); }
        }

        // Using a DependencyProperty as the backing store for TargetPoint.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TargetPointProperty =
            DependencyProperty.Register("TargetPoint", typeof(Point), typeof(Link), new PropertyMetadata(default(Point), OnTargetPointChanged));

        private static void OnTargetPointChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var link = ((Link)d);
            link.UpdatePolyline();
        }

        public InputPort Target
        {
            get { return (InputPort)GetValue(TargetProperty); }
            set { SetValue(TargetProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Target.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TargetProperty =
            DependencyProperty.Register("Target", typeof(InputPort), typeof(Link), new PropertyMetadata(null, OnTargetChanged));

        private static void OnTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var link = ((Link)d);
            var port = (InputPort)e.NewValue;
            if (port != null)
            {
                link.TargetPoint = port.Center;
                port.AttachedLink = link;
                port.InputValue = link.Source.OutputValue;
                link.OnLinked?.Invoke(link, port);
            }
        }

        public OutputPort Source
        {
            get { return (OutputPort)GetValue(SourceProperty); }
            set { SetValue(SourceProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Source.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SourceProperty =
            DependencyProperty.Register("Source", typeof(OutputPort), typeof(Link), new PropertyMetadata(null, OnSourceChanged));

        public Link()
        {
            this.SetValue(Link.StyleProperty, Application.Current.Resources[typeof(Link)]);
          
        }
        static Link()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Link), new FrameworkPropertyMetadata(typeof(Link)));

        }




        private static void OnSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var link = ((Link)d);
            if (e.NewValue != null)
            {
                link.SourcePoint = link.Source.Center;
                link.OnBeginLink?.Invoke(link, link.Source);
            }
        }

        public Brush Stroke
        {
            get { return (Brush)GetValue(StrokeProperty); }
            set { SetValue(StrokeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Stroke.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty StrokeProperty =
            DependencyProperty.Register("Stroke", typeof(Brush), typeof(Link));



        public Brush SelectedStroke
        {
            get { return (Brush)GetValue(SelectedStrokeProperty); }
            set { SetValue(SelectedStrokeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SelectedStroke.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedStrokeProperty =
            DependencyProperty.Register("SelectedStroke", typeof(Brush), typeof(Link), new PropertyMetadata(Brushes.Red));



        public double StrokeThickness
        {
            get { return (double)GetValue(StrokeThicknessProperty); }
            set { SetValue(StrokeThicknessProperty, value); }
        }

        // Using a DependencyProperty as the backing store for StrokeThickness.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty StrokeThicknessProperty =
            DependencyProperty.Register("StrokeThickness", typeof(double), typeof(Link), new PropertyMetadata(2.0));



        public double SelectedStrokeThickness
        {
            get { return (double)GetValue(SelectedStrokeThicknessProperty); }
            set { SetValue(SelectedStrokeThicknessProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SelectedStrokeThickness.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedStrokeThicknessProperty =
            DependencyProperty.Register("SelectedStrokeThickness", typeof(double), typeof(Link), new PropertyMetadata(2.0));


     internal Polyline? Polyline { get; private set; }

       internal Polygon? Polygon { get; private set; }
        protected virtual void UpdatePolyline()
        {
            if (Polyline is null) return;
            if (Polygon is null) return;
            Polygon.Points = new PointCollection() { TargetPoint, new Point(TargetPoint.X - 15, TargetPoint.Y - 5), new Point(TargetPoint.X - 15, TargetPoint.Y + 5) };
            if (SourcePoint.X < TargetPoint.X)
            {
                var vX = TargetPoint.X - SourcePoint.X;

                Point p1 = new Point(SourcePoint.X + vX / 2, SourcePoint.Y);
                Point p2 = new Point(SourcePoint.X + vX / 2, TargetPoint.Y);
                Polyline.Points = new PointCollection() { SourcePoint, p1, p2,TargetPoint };
              
            }
            else
            {
                double offsetX = 15.0;
                double offsetY = 15.0;
             
                if (Target != null)
                {
                    if (Source.NodeParent.Y + Source.NodeParent.Height + offsetY > Target.NodeParent.Y)
                    {
                        var vY = Target.NodeParent.Y + Target.NodeParent.Height - Source.NodeParent.Y - Source.NodeParent.Height;
                        offsetY += vY;
                        if (offsetY <= 0)
                        {
                            offsetY = 15.0;
                        }
                    }
                    if (Source.NodeParent.X + Source.NodeParent.Width + offsetX > Target.NodeParent.X)
                    {
                        var vX = Target.NodeParent.X + Target.NodeParent.Width - Source.NodeParent.X - Source.NodeParent.Width;
                        offsetX += vX;
                        if (offsetX <= 0)
                        {
                            offsetX = 15.0;
                        }
                    }


                }

                Point p1 = new Point(SourcePoint.X, SourcePoint.Y + offsetY);
                Point p2 = new Point(TargetPoint.X - offsetX, SourcePoint.Y + offsetY);
                Point p3 = new Point(TargetPoint.X - offsetX, TargetPoint.Y);
                Polyline.Points = new PointCollection() { SourcePoint, p1, p2, p3 ,TargetPoint};
               
            }

        }

    }

Link

这是一个非常关键的类了,里面保存很多关键信息。

SourcePoint

顾名思义这个一个起始点,一般是OutputPort的Center属性,因为一个Link也是由OutputPort这个类来创建的。如果你忘记了可以回顾一下上面的OutputPort中的 OnMouseLeftButtonDown方法。

TargetPoint

显然这是目标点,一般情况下一个是一个InputPort的Center属性,当然在我们拖拽Link的时候TargetPoint就是我们鼠标当前的坐标。

IsDraging

很明显这是标记这这个Link是不是正在被鼠标拖拽。

Source

我们可以看到Source属性是一个 OutputPort类型的,因为一个连接的源只能是OutputPort,而且连接源是一定存在的,当我们创建一个Link的时候一定要给Source赋值的。你可以看看我在上面的OutputPort类中的 OnMouseLeftButtonDown方法是怎么做的。

Target

这是一个目标,我们可以看到这是一个InputPort类型,与Source不同的是Source是一定存在的,而Target是不一定存在的如同2。我们可以看出在拖动Link的途中Target的值应该为NULL的。当然当我们的Target有值的时候我们应该立马更新我们的TargetPoint,让TargetPoint为Target的Center属性,这样就实现我们之前提到的吸附功能。当然当鼠标Up的时候如果Target还是没有值我们就可以认为该Link无效我们就可以将当前的Link移除。

图2(Target无值的情况)

OnTargetChanged

在这个方法中我们可以看到,我给TargetPoint赋值了,并且我们人Target的AttachedLink属性设置为当前的Link对象。这里我也解释一下为什么需要这个AttachedLink。当某一个InputPort的AttachedLink有值的时候我们就知道该InputPort已经被连接了,当我们再一次连接到该InputPort的时候我们需要将之前的连接对象从连接图中移除掉。

Polygon

这个就是我们所看到的紫色连接线前的箭头

Polyline

这就是我们看到的紫色的连接线了,这个对象是怎么得到的呢?相信认真看代码且有一定的wpf基础的小伙伴一定关注到了 OnApplyTemplate方法吧(ps:这段代码的第8行),他是从Template中得到的。当然具体是什么样式的你可以任意更改。

UpdatePolyline

这是Link的核心代码之一因为有这一段代码我们就可以得到各种类型的连接线段。下面我给大家展示一下Link的几种样式。

图3

图4

这两种情况(及Source在左边Target在右边)其实只是在SourcePoint和TargetPoint之间插入了两个点,结合上面的代码:

                var vX = TargetPoint.X - SourcePoint.X;
                Point p1 = new Point(SourcePoint.X + vX / 2, SourcePoint.Y);
                Point p2 = new Point(SourcePoint.X + vX / 2, TargetPoint.Y);

图 5

图6

在这两种情况下(Source在Target的右边),结合代码:

                double offsetX = 15.0;
                double offsetY = 15.0;
             
                if (Target != null)
                {
                    if (Source.NodeParent.Y + Source.NodeParent.Height + offsetY > Target.NodeParent.Y)
                    {
                        var vY = Target.NodeParent.Y + Target.NodeParent.Height - Source.NodeParent.Y - Source.NodeParent.Height;
                        offsetY += vY;
                        if (offsetY <= 0)
                        {
                            offsetY = 15.0;
                        }
                    }
                    if (Source.NodeParent.X + Source.NodeParent.Width + offsetX > Target.NodeParent.X)
                    {
                        var vX = Target.NodeParent.X + Target.NodeParent.Width - Source.NodeParent.X - Source.NodeParent.Width;
                        offsetX += vX;
                        if (offsetX <= 0)
                        {
                            offsetX = 15.0;
                        }
                    }


                }

                Point p1 = new Point(SourcePoint.X, SourcePoint.Y + offsetY);
                Point p2 = new Point(TargetPoint.X - offsetX, SourcePoint.Y + offsetY);
                Point p3 = new Point(TargetPoint.X - offsetX, TargetPoint.Y);
                Polyline.Points = new PointCollection() { SourcePoint, p1, p2, p3 ,TargetPoint};
           

很容易也就实现Link的拖拽。


  [TemplatePart(Name = "PART_InputPortsControl", Type = typeof(InputPortsControl))]
    [TemplatePart(Name = "PART_OutputPort", Type = typeof(OutputPort))]
    public class Node : ListBoxItem
    {
      
        private Point startMovePoint;
        private bool draging = false;


        static Node()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(Node), new FrameworkPropertyMetadata(typeof(Node)));

            StyleProperty.OverrideMetadata(typeof(Node), new FrameworkPropertyMetadata(Application.Current.Resources[typeof(Node)]));

        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            this.OutputPort = (OutputPort)Template.FindName("PART_OutputPort", this);
            InputPortsControl = (InputPortsControl)Template.FindName("PART_InputPortsControl", this);
        }
        internal OutputPort? OutputPort { get; set; }
        internal InputPortsControl? InputPortsControl { get; set; }

        internal GraphControl GraphControlParent => VisualTreeUtility.FindParent<GraphControl>(this);









        public Guid Id
        {
            get { return (Guid)GetValue(IdProperty); }
            set { SetValue(IdProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Id.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IdProperty =
            DependencyProperty.Register("Id", typeof(Guid), typeof(Node), new PropertyMetadata(Guid.NewGuid()));

       

        public double X
        {
            get { return (double)GetValue(XProperty); }
            set { SetValue(XProperty, value); }
        }

        // Using a DependencyProperty as the backing store for X.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty XProperty =
            DependencyProperty.Register("X", typeof(double), typeof(Node));



        public double Y
        {
            get { return (double)GetValue(YProperty); }
            set { SetValue(YProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Y.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty YProperty =
            DependencyProperty.Register("Y", typeof(double), typeof(Node), new PropertyMetadata(0.0));





        public int ZIndex
        {
            get { return (int)GetValue(ZIndexProperty); }
            set { SetValue(ZIndexProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ZIndex.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ZIndexProperty =
            DependencyProperty.Register("ZIndex", typeof(int), typeof(Node), new PropertyMetadata(0));



        public Brush SelectedBorderBrush
        {
            get { return (Brush)GetValue(SelectedBorderBrushProperty); }
            set { SetValue(SelectedBorderBrushProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SelectedBorderBrush.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedBorderBrushProperty =
            DependencyProperty.Register("SelectedBorderBrush", typeof(Brush), typeof(Node), new PropertyMetadata(Brushes.Yellow));



        public Thickness SelectedBorderThickness
        {
            get { return (Thickness)GetValue(SelectedBorderThicknessProperty); }
            set { SetValue(SelectedBorderThicknessProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SelectedBorderThickness.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedBorderThicknessProperty =
            DependencyProperty.Register("SelectedBorderThickness", typeof(Thickness), typeof(Node), new PropertyMetadata(new Thickness(2)));









        public Style InputPortsControlStyle
        {
            get { return (Style)GetValue(InputPortsControlStyleProperty); }
            set { SetValue(InputPortsControlStyleProperty, value); }
        }

        // Using a DependencyProperty as the backing store for InputPortsControlStyle.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty InputPortsControlStyleProperty =
            DependencyProperty.Register("InputPortsControlStyle", typeof(Style), typeof(Node), new PropertyMetadata(null));





        public Thickness InputPortsControlMargin
        {
            get { return (Thickness)GetValue(InputPortsControlMarginProperty); }
            set { SetValue(InputPortsControlMarginProperty, value); }
        }

        // Using a DependencyProperty as the backing store for InputPortsControlMargin.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty InputPortsControlMarginProperty =
            DependencyProperty.Register("InputPortsControlMargin", typeof(Thickness), typeof(Node), new PropertyMetadata(new Thickness(-5, 0, -5, 0)));




        public Brush OutputPortSelectedBorderBrush
        {
            get { return (Brush)GetValue(OutputPortSelectedBorderBrushProperty); }
            set { SetValue(OutputPortSelectedBorderBrushProperty, value); }
        }

        // Using a DependencyProperty as the backing store for OutputPortSelectedBorderBrush.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty OutputPortSelectedBorderBrushProperty =
            DependencyProperty.Register("OutputPortSelectedBorderBrush", typeof(Brush), typeof(Node), new PropertyMetadata(Brushes.Red));



        public Thickness OutputPortSelectedBorderThickness
        {
            get { return (Thickness)GetValue(OutputPortSelectedBorderThicknessProperty); }
            set { SetValue(OutputPortSelectedBorderThicknessProperty, value); }
        }

        // Using a DependencyProperty as the backing store for OutputPortSelectedBorderThickness.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty OutputPortSelectedBorderThicknessProperty =
            DependencyProperty.Register("OutputPortSelectedBorderThickness", typeof(Thickness), typeof(Node), new PropertyMetadata(new Thickness(1)));



        public Thickness OutputPortMargin
        {
            get { return (Thickness)GetValue(OutputPortMarginProperty); }
            set { SetValue(OutputPortMarginProperty, value); }
        }

        // Using a DependencyProperty as the backing store for OutputPortMargin.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty OutputPortMarginProperty =
            DependencyProperty.Register("OutputPortMargin", typeof(Thickness), typeof(Node), new PropertyMetadata(new Thickness(-5, 1, -5, 1)));






        public IEnumerable InputPortItemsSource
        {
            get { return (IEnumerable)GetValue(InputPortItemsSourceProperty); }
            set { SetValue(InputPortItemsSourceProperty, value); }
        }

        // Using a DependencyProperty as the backing store for InputPortItemsSource.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty InputPortItemsSourceProperty =
            DependencyProperty.Register("InputPortItemsSource", typeof(IEnumerable), typeof(Node));



        public DataTemplate InputPortsControlItemTemplate
        {
            get { return (DataTemplate)GetValue(InputPortsControlItemTemplateProperty); }
            set { SetValue(InputPortsControlItemTemplateProperty, value); }
        }

        // Using a DependencyProperty as the backing store for InputPortsControlItemTemplate.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty InputPortsControlItemTemplateProperty =
            DependencyProperty.Register("InputPortsControlItemTemplate", typeof(DataTemplate), typeof(Node));



        public CornerRadius CornerRadius
        {
            get { return (CornerRadius)GetValue(CornerRadiusProperty); }
            set { SetValue(CornerRadiusProperty, value); }
        }

        // Using a DependencyProperty as the backing store for CornerRadius.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CornerRadiusProperty =
            DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(Node), new PropertyMetadata(new CornerRadius(2)));




        public CornerRadius OutputPortCornerRadius
        {
            get { return (CornerRadius)GetValue(OutputPortCornerRadiusProperty); }
            set { SetValue(OutputPortCornerRadiusProperty, value); }
        }

        // Using a DependencyProperty as the backing store for OutputPortCornerRadius.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty OutputPortCornerRadiusProperty =
            DependencyProperty.Register("OutputPortCornerRadius", typeof(CornerRadius), typeof(Node), new PropertyMetadata(new CornerRadius(5)));





        public object OutputPortContent
        {
            get { return (object)GetValue(OutputPortContentProperty); }
            set { SetValue(OutputPortContentProperty, value); }
        }

        // Using a DependencyProperty as the backing store for OutputPortContent.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty OutputPortContentProperty =
            DependencyProperty.Register("OutputPortContent", typeof(object), typeof(Node), new PropertyMetadata(null));




        public bool CanNotifyOutputChanged
        {
            get { return (bool)GetValue(CanNotifyOutputChangedProperty); }
            set { SetValue(CanNotifyOutputChangedProperty, value); }
        }

        // Using a DependencyProperty as the backing store for CanNotifyOutputChanged.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CanNotifyOutputChangedProperty =
            DependencyProperty.Register("CanNotifyOutputChanged", typeof(bool), typeof(Node), new PropertyMetadata(true));



        public object OutputValue
        {
            get { return (object)GetValue(OutputValueProperty); }
            set { SetValue(OutputValueProperty, value); }
        }

        // Using a DependencyProperty as the backing store for OutputValue.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty OutputValueProperty =
            DependencyProperty.Register("OutputValue", typeof(object), typeof(Node), new PropertyMetadata(null, OnOutputValueChanged));



        public Visibility OutputPortVisibility
        {
            get { return (Visibility)GetValue(OutputPortVisibilityProperty); }
            set { SetValue(OutputPortVisibilityProperty, value); }
        }

        // Using a DependencyProperty as the backing store for OutputPortVisibility.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty OutputPortVisibilityProperty =
            DependencyProperty.Register("OutputPortVisibility", typeof(Visibility), typeof(Node), new PropertyMetadata(Visibility.Visible));



        private static void OnOutputValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {

            var node = (Node)d;
            if (!node.CanNotifyOutputChanged) return;
            foreach (var item in node.OutputPort.AttachedLinks)
            {
                item.Target.InputValue = e.NewValue;
            }

        }

        public string InputValueField
        {
            get { return (string)GetValue(InputValueFieldProperty); }
            set { SetValue(InputValueFieldProperty, value); }
        }

        // Using a DependencyProperty as the backing store for InputValueField.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty InputValueFieldProperty =
            DependencyProperty.Register("InputValueField", typeof(string), typeof(Node), new PropertyMetadata(null));



      

        public void NotifyOutputChanged()
        {
            foreach (var item in this.OutputPort.AttachedLinks)
            {
                item.Target.InputValue = item.Source.OutputValue;
            }
        }
        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);
            startMovePoint = e.GetPosition(GraphControlParent);
            draging = true;
            this.IsSelected = true;
            e.Handled = true;
        
        }
       
        protected override void OnMouseMove(MouseEventArgs e)
        {
           
            if (draging)
            {
                var newMousePosition = e.GetPosition(GraphControlParent);
                var delta = newMousePosition - startMovePoint;

                X += delta.X;
                Y += delta.Y;

                startMovePoint = newMousePosition;
  
                CaptureMouse();
            }
          
             
            

            e.Handled = true;
            base.OnMouseMove(e);
        }
        protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
        {
            draging = false;
            base.OnMouseLeftButtonUp(e);
          
            ReleaseMouseCapture();
        }

    }

Node

这一部分代码看上去很多,其实你需要关心的代码并不多。

X,Y,ZIndex

这三个属性主要是为了获取或设置当前Node的位置的,ZIndex可以理解为图层在这里的作用我是这样想的当我们选中一个Node的时候我们将其他Node的ZIndex设置为0当前的Node的ZIndex设置为1,这样我们就可以得到如图7的效果,我们选中的Node永远在其他Node的上方(有黄色边框的为选中项)。这个功能可以配合Canvas使用如图8.

图7

图8

OnMouseLeftButtonDown

显而易见当MouseLeftButtonDown的时候我们就应该是开始拖动当前的Node了,具体做了上面如下:

  startMovePoint = e.GetPosition(GraphControlParent);
            draging = true;
            this.IsSelected = true;
            e.Handled = true;

大概的意思就是获取鼠标按下的位置,然后标记draging=true,当然是相对于GraphControlParent的,这个是什么如果有wpf基础的朋友应该看出来了。当然稍后我会解释它。

OnMouseMove

当我们的鼠标按下也就是我之前标记的draging为true的时候也就代表我们在拖动Node,其实我们只需要设置X,Y的值就好了。当然也是简单的数学问题具体操作如下:

                var newMousePosition = e.GetPosition(GraphControlParent);
                var delta = newMousePosition - startMovePoint;
                X += delta.X;
                Y += delta.Y;
                startMovePoint = newMousePosition;
                CaptureMouse();

你最好不要丢掉CaptureMouse方法,丢掉会有问题,您可以自己尝试一下。说到这里有一定wpf开发经验的朋友一定会知道一个东西那就是wpf的行为库内有一个MouseDragElementBehavior行为,这个行为确实可以做到拖动元素。但是经过本人的测试,通过MVVM的方法去使用该流程图的时候会出现拖动不了,主要的原因也在与MouseDragElementBehavior无法准确的找到我们是相对于哪一个控件拖动的,我们自己实现可以很好的控制我们是相对于GraphControlParent来拖动控件的。

OnMouseLeftButtonUp

看完上面这个方法就很好理解了,当MouseLeftButtonUp的时候也就标志着我们拖拽结束了


 public class LinksControl : ListBox
    {
        public event EventHandler<Link>? OnRemoveLink;
        internal GraphControl GraphControlParent => VisualTreeUtility.FindParent<GraphControl>(this);
        internal Link SelectedLink => (Link)ItemContainerGenerator.ContainerFromItem(SelectedItem);

        public Brush ItemsStroke
        {
            get { return (Brush)GetValue(ItemsStrokeProperty); }
            set { SetValue(ItemsStrokeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Stroke.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ItemsStrokeProperty =
            DependencyProperty.Register("ItemsStroke", typeof(Brush), typeof(LinksControl), new PropertyMetadata(Brushes.Black));





        public double ItemsStrokeThickness
        {
            get { return (double)GetValue(ItemsStrokeThicknessProperty); }
            set { SetValue(ItemsStrokeThicknessProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ItemsStrokeThickness.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ItemsStrokeThicknessProperty =
            DependencyProperty.Register("ItemsStrokeThickness", typeof(double), typeof(LinksControl), new PropertyMetadata(2.0));



        public Brush SelectedItemsStroke
        {
            get { return (Brush)GetValue(SelectedItemsStrokeProperty); }
            set { SetValue(SelectedItemsStrokeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Stroke.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedItemsStrokeProperty =
            DependencyProperty.Register("SelectedItemsStroke", typeof(Brush), typeof(LinksControl), new PropertyMetadata(Brushes.Red));





        public double SelectedItemsStrokeThickness
        {
            get { return (double)GetValue(SelectedItemsStrokeThicknessProperty); }
            set { SetValue(SelectedItemsStrokeThicknessProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ItemsStrokeThickness.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedItemsStrokeThicknessProperty =
            DependencyProperty.Register("SelectedItemsStrokeThickness", typeof(double), typeof(LinksControl), new PropertyMetadata(2.0));

      
        public LinksControl()
        {
            SetValue(StyleProperty, Application.Current.Resources[typeof(LinksControl)]);
           
        }
        static LinksControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(LinksControl), new FrameworkPropertyMetadata(typeof(LinksControl)));
          
        }



        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return item is Link;
        }

        protected override DependencyObject GetContainerForItemOverride()
        {
            return new Link();
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            base.OnMouseMove(e);

            if (SelectedLink != null && SelectedLink.IsDraging)
            {
                CaptureMouse();
                var point = e.GetPosition(GraphControlParent);
                SelectedLink.TargetPoint = point;
                foreach (var item in GraphControlParent.InputPorts)
                {
                    if (item.NodeParent == SelectedLink.Source.NodeParent)
                        continue;
                    if (item.IsNear(point))
                    {
                        //先将已经有链接的inputport的链接清空
                        if (item.AttachedLink != null)
                        {
                            ReMoveLink(item.AttachedLink);
                        }
                      //  同一个OutputPort只能连接到同一个Node中的一个口,不能多连
                        foreach (var link in SelectedLink.Source.AttachedLinks)
                        {
                            if (link.Target != null && link.Target.NodeParent == item.NodeParent)
                            {
                                ReMoveLink(link);
                                break;
                            }
                        }
                        SelectedLink.Target = item;
                        SelectedLink.IsDraging = false;
                        break;
                    }
                }
            }

        }

        protected override void OnMouseUp(MouseButtonEventArgs e)
        {
            base.OnMouseUp(e);

            if (SelectedLink != null && SelectedLink.Target is null)
            {
                ReMoveLink(SelectedLink);
            }
          

        }

        public void ReMoveLink(Link link)
        {
            OnRemoveLink?.Invoke(this, link);
            var item = ItemContainerGenerator.ItemFromContainer(link);
            this.Items.Remove(item);
        }
    }

LinksControl

这是一个很好理解的控件,它负责管理我们所有的Link。Link是继承自ListBoxItem的,所以我们的LinksControl继承自ListBox。

SelectedLink

为了获取或设置我们当前选中的Link,由于使用SelectedItem比较麻烦所以我添加了这个属性

OnMouseMove

当这个事件触发的时候并且SelectedLink != null && SelectedLink.IsDraging的时候就说明我们正在拖动一个Link,拖动的时候我们要判断是不是有InputPort在当前Link的targetPoint附近,用我们之前提到的IsNear方法判断,如果在附近就直接连接上去。当然当前Node的OutputPort不能连接当前Node的Inputport的,至少我是这么理解的,当然你也可以修改。

 if (item.NodeParent == SelectedLink.Source.NodeParent)
                        continue;

如上NodeParent是某一个口所在的Node只有Source的NodeParent和Target的NodeParent不相同的时候才会连接。具体内容可以看该方法的具体实现,我也写了一点点注释。


  [TemplatePart(Name = "PART_LinksControl", Type = typeof(LinksControl))]
    public class GraphControl : ListBox
    {
        internal Dictionary<Guid,Node> cache= new Dictionary<Guid,Node>();
        public GraphControl()
        {
            this.LayoutUpdated += GraphControl_LayoutUpdated;
            
        }
        static GraphControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(GraphControl), new FrameworkPropertyMetadata(typeof(GraphControl)));
            StyleProperty.OverrideMetadata(typeof(GraphControl), new FrameworkPropertyMetadata(Application.Current.Resources[typeof(GraphControl)]));
        }
        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return item is Node;
        }

        protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
        {
            base.OnItemsChanged(e);
            foreach (var item in Items)
            {
                var node = (Node)ItemContainerGenerator.ContainerFromItem(item);
                if (node != null)
                {
                    cache.TryAdd(node.Id, node);
                }
            }
        }
        protected override DependencyObject GetContainerForItemOverride()
        {
            return new Node();
        }
        internal Node SelectedNode => (Node)ItemContainerGenerator.ContainerFromItem(SelectedItem);
        private void GraphControl_LayoutUpdated(object? sender, EventArgs e)
        {
            if (ItemContainerGenerator.Status != System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated) return;
           
            foreach (var item in OutputPorts)
            {
                item.UpdatePosition();
            }
            foreach (var item in InputPorts)
            {
                item.UpdatePosition();
            }
           

        }

        internal LinksControl? LinksControl { get; set; }

        internal List<OutputPort> OutputPorts
        {
            get
            {
                List<OutputPort> ports = new List<OutputPort>();
                foreach (var item in Items)
                {
                    var node = (Node)ItemContainerGenerator.ContainerFromItem(item);
                    if (node.OutputPort is null) continue;
                    if (node.OutputPort.Visibility == Visibility.Visible)
                        ports.Add(node.OutputPort);
                }
                return ports;
            }
        }
        internal List<InputPort> InputPorts
        {
            get
            {
                List<InputPort> ports = new List<InputPort>();
                foreach (var item in Items)
                {
                    var node = (Node)ItemContainerGenerator.ContainerFromItem(item);
                    if (node.InputPortsControl is null) continue;
                    foreach (var portItem in node.InputPortsControl.Items)
                    {
                        var port = (InputPort)node.InputPortsControl.ItemContainerGenerator.ContainerFromItem(portItem);
                        ports.Add(port);
                    }
                }
                return ports;
            }
        }



        public Brush LinkStroke
        {
            get { return (Brush)GetValue(LinkStrokeProperty); }
            set { SetValue(LinkStrokeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Stroke.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LinkStrokeProperty =
            DependencyProperty.Register("LinkStroke", typeof(Brush), typeof(GraphControl), new PropertyMetadata(Brushes.BlueViolet));





        public double LinkStrokeThickness
        {
            get { return (double)GetValue(LinkStrokeThicknessProperty); }
            set { SetValue(LinkStrokeThicknessProperty, value); }
        }

        // Using a DependencyProperty as the backing store for LinkStrokeThickness.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LinkStrokeThicknessProperty =
            DependencyProperty.Register("LinkStrokeThickness", typeof(double), typeof(GraphControl), new PropertyMetadata(2.0));


        public Brush SelectedLinkStroke
        {
            get { return (Brush)GetValue(SelectedLinkStrokeProperty); }
            set { SetValue(SelectedLinkStrokeProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Stroke.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedLinkStrokeProperty =
            DependencyProperty.Register("SelectedLinkStroke", typeof(Brush), typeof(GraphControl), new PropertyMetadata(Brushes.BlueViolet));





        public double SelectedLinkStrokeThickness
        {
            get { return (double)GetValue(SelectedLinkStrokeThicknessProperty); }
            set { SetValue(SelectedLinkStrokeThicknessProperty, value); }
        }

        // Using a DependencyProperty as the backing store for LinkStrokeThickness.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedLinkStrokeThicknessProperty =
            DependencyProperty.Register("SelectedLinkStrokeThickness", typeof(double), typeof(GraphControl), new PropertyMetadata(2.0));
        public Style LinksControlStyle
        {
            get { return (Style)GetValue(LinksControlStyleProperty); }
            set { SetValue(LinksControlStyleProperty, value); }
        }

        // Using a DependencyProperty as the backing store for LinksControlStyle.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LinksControlStyleProperty =
            DependencyProperty.Register("LinksControlStyle", typeof(Style), typeof(GraphControl));





        public IEnumerable LinksControlItemSource
        {
            get { return (IEnumerable)GetValue(LinksControlItemSourceProperty); }
            set { SetValue(LinksControlItemSourceProperty, value); }
        }

        // Using a DependencyProperty as the backing store for LinksControlItemSource.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LinksControlItemSourceProperty =
            DependencyProperty.Register("LinksControlItemSource", typeof(IEnumerable), typeof(GraphControl));




        public Style LinksControlItemContainerStyle
        {
            get { return (Style)GetValue(LinksControlItemContainerStyleProperty); }
            set { SetValue(LinksControlItemContainerStyleProperty, value); }
        }

        // Using a DependencyProperty as the backing store for LinksControlItemContainerStyle.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LinksControlItemContainerStyleProperty =
            DependencyProperty.Register("LinksControlItemContainerStyle", typeof(Style), typeof(GraphControl));



     

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            LinksControl = (LinksControl)Template.FindName("PART_LinksControl", this);

            this.SetValue(LinksControlStyleProperty, Application.Current.Resources[typeof(LinksControl)]);
            this.SetValue(LinksControlItemContainerStyleProperty, Application.Current.Resources[typeof(Link)]);
        }



        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            base.OnSelectionChanged(e);
            foreach (var item in Items)
            {
                var node = (Node)ItemContainerGenerator.ContainerFromItem(item);
                node.ZIndex = 0;
            }
            SelectedNode.ZIndex = 1;
        }
    }

GraphControl

该控件管理着以上所有的控件,该控件的Template中有一个LinksControl,以及其本身也就是Node控件的集合。

LayoutUpdated

很核心的一个方法,当Layout更新的时候我们需要更新所有Inputport和OutputPort的位置信息,当然具体的实现都不在其中。

OnSelectionChanged

当选中项变化的时候我们将当前的Node的ZIndex设置为1其他的设置为0,就是这么简单的一个功能。


结束

仅仅提供一个实现的思路,当然还有很多的Style并没有写上,你可以根据自己的需要去定制自己的流程图。拜拜!!!!!!!!!!

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;