Foreword

Introduction

It was a winter. The author who was studying radio security used GNURadio. That was the first time the author used the node editor.

-> What? Excuse me... What's this?.. What the hell is this?...

It was a spring season, and I don't know why the whole world has changed after the Chinese New Year. Everyone was forced to stay at home. The extremely boring author learned Blender. That was the second time the author used the node editor.

-> Wo...It turns out that this one is really convenient to use.

So some ideas gradually emerged in the author's mind, making the author want to make one.

It was a summer, I don’t know why the author started to learn Davinci again. That was the third time the author used the node editor. The use of this time has doubled the author"s favor with the node editor. The author instantly felt that as long as it is a program that can be modularized and streamlined, everything can be nodeized.

So STNodeEditor appeared.


[Basic]STNode

STNode is the core of the whole framework. If STNodeEditor is regarded as Desktop, then an STNode can be regarded as an application on the desktop. It is very necessary to develop a strong STNode.


Create Node


The base class STNode of the node is modified by abstract, so the node must be extended to STNode

STNode contains a large number of virtual functions for developers to overload. For more information, please refer to API Documentation

using ST.Library.UI.NodeEditor;

namespace WinNodeEditorDemo
{
    public class MyNode : STNode
    {
        public MyNode() { //Equivalent to OnCreate()
            this.Title = "TestNode";
        }
        //protected override void OnCreate() {
        //    base.OnCreate();
        //}
    }
}
//Add to STNodeEditor
stNodeEditor1.Nodes.Add(new MyNode());

You will find that you only see a title and nothing. Because no input and output options are added to it, let's add some code.

public class MyNode : STNode
{
    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "TestNode";
        //get the index of STNodeOption that added
        int nIndex = this.InputOptions.Add(new STNodeOption("IN_1", typeof(string), false));
        //get the STNodeOption that added
        STNodeOption op = this.InputOptions.Add("IN_2", typeof(int), true);

        this.OutputOptions.Add("OUT", typeof(string), false);
    }
}

In this way, the added option will be displayed on the node, but this is not enough. You can see that there are two data types string and int. Color should be added to the data type to distinguish different data types.

public class MyNode : STNode
{
    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "TestNode";
        int nIndex = this.InputOptions.Add(new STNodeOption("IN_1", typeof(string), false));
        STNodeOption op = this.InputOptions.Add("IN_2", typeof(int), true);
        //The highest priority, the color information in the container will be ignored
        //this.SetOptionDotColor(op, Color.Red); 
        this.OutputOptions.Add("OUT", typeof(string), false);
    }
    //Occurs when the owner changes, submit the color
    protected override void OnOwnerChanged() {
        base.OnOwnerChanged();
        if (this.Owner == null) return;
        this.Owner.SetTypeColor(typeof(string), Color.Yellow);
        //will replace old color
        this.Owner.SetTypeColor(typeof(int), Color.DodgerBlue, true);
    }
}

Such a node is created, but this node does not have any functions. In the next case, functions will be added.

In any case, developers should try to keep an empty parameter constructor for the extended STNode, otherwise there will be unnecessary troubles in many functions.


STNodeOption


From the above case, we can see that STNodeOption is the connection option of STNode. The connection option can be multi-connection and single-connection mode.

public class MyNode : STNode {
    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "MyNode";
        this.TitleColor = Color.FromArgb(200, Color.Goldenrod);
        //multi-connection
        this.InputOptions.Add("Single", typeof(string), true);
        //single-connection
        this.OutputOptions.Add("Multi", typeof(string), false);
    }
}

In multi-connection mode, an option can be connected by multiple options of the same data type (rectangle)

In single-connection mode, an option can only be connected by one option of the same data type (circle)


STNodeOption.Empty


STNodeOption.Empty is a static property, added to STNode is only used to occupy a place during auto layout.

public class MyNode : STNode {
    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "MyNode";
        this.TitleColor = Color.FromArgb(200, Color.Goldenrod);

        this.InputOptions.Add(STNodeOption.Empty);
        this.InputOptions.Add("IN_1", typeof(string), false);
        this.InputOptions.Add("IN_2", typeof(string), false);

        this.OutputOptions.Add("OUT_1", typeof(string), false);
        this.OutputOptions.Add(STNodeOption.Empty);
        this.OutputOptions.Add(STNodeOption.Empty);
        this.OutputOptions.Add("OUT_2", typeof(string), false);
    }
}

a STNodeOption height can set by STNode.ItemHeight(protected)


STNode.AutoSize


AutoSizedefault value istrue,when AitoSize is set WidthHeightcan not be set

public class MyNode : STNode {
    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "MyNode";
        this.TitleColor = Color.FromArgb(200, Color.Goldenrod);

        this.InputOptions.Add("IN", typeof(string), false);
        this.OutputOptions.Add("OUT", typeof(string), false);
        //you should close AutoSize Model before you set the node size
        this.AutoSize = false;
        this.Size = new Size(100, 100);
    }
}

You can see that the size of MyNode is no longer automatically calculated, but the position of STNodeOption will still be automatically calculated. If you want to modify the position of STNodeOption, you can override OnSetOptionXXX

public class MyNode : STNode
{
    private STNodeOption m_op_in;
    private STNodeOption m_op_out;

    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "MyNode";
        this.TitleColor = Color.FromArgb(200, Color.Goldenrod);

        m_op_in = this.InputOptions.Add("IN", typeof(string), false);
        m_op_out = this.OutputOptions.Add("OUT", typeof(string), false);
        this.AutoSize = false;
        this.Size = new Size(100, 100);
    }
    //you can set the location of STNodeOption when the AutoSize is true
    protected override Point OnSetOptionDotLocation(STNodeOption op, Point pt, int nIndex) {
        if (op == m_op_in) return new Point(pt.X, pt.Y + 20);
        return base.OnSetOptionDotLocation(op, pt, nIndex);
    }
    //you can set the rectangle of STNodeOption when the AutoSize is true
    protected override Rectangle OnSetOptionTextRectangle(STNodeOption op, Rectangle rect, int nIndex) {
        if (op == m_op_out) return new Rectangle(rect.X, rect.Y + 20, rect.Width, rect.Height);
        return base.OnSetOptionTextRectangle(op, rect, nIndex);
    }
}

You can see that in the code, the point and text area of the STNodeOption connection line are modified by overloading the function. Why is it not designed to be STNodeOption.DotLeft=xxx because the author thinks it will be more troublesome.

The pt and rect passed in the overloaded function are all automatically calculated data so that the developer will have a certain reference when modifying the position. If the method is STNodeOption.DotLeft=xxx, the developer cannot Obtaining a reference position requires all calculations by yourself

It also needs to bind events such as STNode.Resize to monitor the changes in the size of STNode to recalculate the position, so the OnSetOptionXXX method is more friendly in comparison.

All instances currently have no functions. In the next cases, functions will be added.


e.g. - ClockNode


STNodeOption can get all the data input of this option by binding to the DataTransfer event

STNodeOption.TransferData(object) function can transfer data to all connections on this option

Next, implement a functional node. The best example for now is to create a clock node.

Because the content introduced so far is not enough to be able to freely provide arbitrary data to nodes, a node that can generate data by itself is needed.

The node outputs the current system time every second

public class ClockNode : STNode
{
    private Thread m_thread;
    private STNodeOption m_op_out_time;

    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "ClockNode";
        m_op_out_time = this.OutputOptions.Add("Time", typeof(DateTime), false);
    }
    //when the owner changed 
    protected override void OnOwnerChanged() {
        base.OnOwnerChanged();
        if (this.Owner == null) {   //when the owner is null abort thread
            if (m_thread != null) m_thread.Abort();
            return;
        }
        this.Owner.SetTypeColor(typeof(DateTime), Color.DarkCyan);
        m_thread = new Thread(() => {
            while (true) {
                Thread.Sleep(1000);
                //STNodeOption.TransferData(object) will automatically post data to all connections on the option
                //STNodeOption.TransferData(object) will set STNodeOption.Data automatically
                m_op_out_time.TransferData(DateTime.Now);
                //if you need to operate across UI threads in a thread, the node provides Begin/Invoke() to complete the operation.
                //this.BeginInvoke(new MethodInvoker(() => m_op_out_time.TransferData(DateTime.Now)));
            }
        }) { IsBackground = true };
        m_thread.Start();
    }
}

Of course, we can directly display the time of the above node, but in order to demonstrate the data transfer, we also need a node that accepts the data

public class ShowClockNode : STNode {
    private STNodeOption m_op_time_in;
    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "ShowTime";
        //use "single-connection" model
        m_op_time_in = this.InputOptions.Add("--", typeof(DateTime), true);
        //This event is triggered when data is transferred to m_op_time_in
        m_op_time_in.DataTransfer += new STNodeOptionEventHandler(op_DataTransfer);
    }

    void op_DataTransfer(object sender, STNodeOptionEventArgs e) {
        //This event is not only triggered when there is data coming in.
        //This event is also triggered when there is a connection or disconnection,
        //so you need to check the connection status.
        if (e.Status != ConnectionStatus.Connected || e.TargetOption.Data == null) {
            //When STNode.AutoSize=true, it is not recommended to use STNode.SetOptionText
            //the STNode will recalculate the layout every time the Text changes.
            //It should be displayed by adding controls.
            //Since STNodeControl has not yet been mentioned, the current design will be used for now.
            this.SetOptionText(m_op_time_in, "--");
        } else {
            this.SetOptionText(m_op_time_in, ((DateTime)e.TargetOption.Data).ToString());
        }
    }
}

add to STNodeEditor

You can see that ShowClockNode is refreshing every second


STNode.SetOptionXXX


In the above and previous examples, we can see that when some properties of STNodeOption need to be modified, they are not modified in the way of STNodeOption.XXX=XXX. This design is for safety.

The author thinks that STNodeOption can only be modified by its owner, and the method of STNodeOption.XXX=XXX cannot know who modified it and STNode.SetOptionXXX() is marked by protected only internally Is called and inside the function will check whether STNodeOption.Owner is the current class


About TransferData


It is not necessary to STNodeOption.TransferData(object) to transfer data. TransferData(object) only actively updates data

When a new connection is successful, the DataTransfer event will also be triggered, the following will modify the code of ClockNode

public class ClockNode : STNode
{
    private Thread m_thread;
    private STNodeOption m_op_out_time;

    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "ClockNode";
        m_op_out_time = this.OutputOptions.Add("Time", typeof(DateTime), false);
        //Assign value to option data
        m_op_out_time.Data = DateTime.Now;
    }
}

You can see that ShowClockNode still shows the time but the data does not change because the DataTransfer event will be triggered when the connection is successful. In the event, ShowClockNode gets the data of the ClockNode option through e.TargetOption.Data

When a connection is successful and disconnected, the event trigger sequence is as follows

Connecting-Connected-DataTransfer | DisConnecting-DataTransfer-DisConnected


STNodeHub


STNodeHub is a built-in node that can disperse one output to multiple inputs or concentrate multiple outputs on one input point to prevent repeated connections. It can also be used for layout when the node connection is complicated.

[basic]STNodeControl

As the base class of STNode control, STNodeControl has many properties and events with the same name as System.Windows.Forms.Control, allowing developers to develop a node like a WinForm program.

In this version (2.0), no available control is provided. Only the STNodeControl base class needs to be extended by the developer. If available later, the author will improve it.


add a control


Same as System.Windows.Forms.Control STNode has the Controls collection and its data type is STNodeControl

public class MyNode : STNode
{
    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "MyNode";
        this.TitleColor = Color.FromArgb(200, Color.Goldenrod);
        this.AutoSize = false;
        this.Size = new Size(100, 100);

        var ctrl = new STNodeControl();
        ctrl.Text = "Button";
        ctrl.Location = new Point(10, 10);
        this.Controls.Add(ctrl);
        ctrl.MouseClick += new MouseEventHandler(ctrl_MouseClick);
    }

    void ctrl_MouseClick(object sender, MouseEventArgs e) {
        MessageBox.Show("MouseClick");
    }
}

You can see that there is almost no difference between developing a WinForm program. The only difference is that STNode does not yet provide a WYSIWYG UI designer.


Customize a Button


Although the above code looks like it adds a button control, in fact it is just the default drawing style of STNodeControl

The following is to customize a Button control with mouse hovering and clicking effects to make it more like a button

public class STNodeButton : STNodeControl {

    private bool m_b_enter;
    private bool m_b_down;

    protected override void OnMouseEnter(EventArgs e) {
        base.OnMouseEnter(e);
        m_b_enter = true;
        this.Invalidate();
    }

    protected override void OnMouseLeave(EventArgs e) {
        base.OnMouseLeave(e);
        m_b_enter = false;
        this.Invalidate();
    }

    protected override void OnMouseDown(MouseEventArgs e) {
        base.OnMouseDown(e);
        m_b_down = true;
        this.Invalidate();
    }

    protected override void OnMouseUp(MouseEventArgs e) {
        base.OnMouseUp(e);
        m_b_down = false;
        this.Invalidate();
    }

    protected override void OnPaint(DrawingTools dt) {
        //base.OnPaint(dt);
        Graphics g = dt.Graphics;
        SolidBrush brush = dt.SolidBrush;
        brush.Color = base.BackColor;
        if (m_b_down) brush.Color = Color.SkyBlue;
        else if (m_b_enter) brush.Color = Color.DodgerBlue;
        g.FillRectangle(brush, 0, 0, this.Width, this.Height);
        g.DrawString(this.Text, this.Font, Brushes.White, this.ClientRectangle, base.m_sf);
    }
}

Of course, in order to make the code as simple as possible, the effect of the button is written in the code. The above code is just to demonstrate how to build a custom control. Of course, you need to have some GDI related knowledge before this.

<GDI+Programming> is a good book


e.g. - Image info


In the above ClockNode case, the data for the data is written in the node through code. Next, in this case, the data is obtained through the STNodeButton for output.

public class ImageShowNode : STNode
{
    private STNodeOption m_op_out;

    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "ImageShowNode";
        this.TitleColor = Color.FromArgb(200, Color.Goldenrod);
        this.AutoSize = false;
        this.Size = new Size(160, 150);
        m_op_out = this.OutputOptions.Add("", typeof(Image), false);

        var ctrl = new STNodeButton();
        ctrl.Text = "Open Image";
        ctrl.Location = new Point(5, 0);
        ctrl.Size = new Size(150, 20);
        this.Controls.Add(ctrl);
        ctrl.MouseClick += new MouseEventHandler(ctrl_MouseClick);
    }

    void ctrl_MouseClick(object sender, MouseEventArgs e) {
        OpenFileDialog ofd = new OpenFileDialog();
        ofd.Filter = "*.png|*.png|*.jpg|*.jpg";
        if (ofd.ShowDialog() != DialogResult.OK) return;
        m_op_out.TransferData(Image.FromFile(ofd.FileName), true);
        this.Invalidate();
    }

    protected override void OnDrawBody(DrawingTools dt) {
        base.OnDrawBody(dt);
        //of course you can extended STNodeControl to build a "STNodePictureBox" for display image
        Graphics g = dt.Graphics;
        Rectangle rect = new Rectangle(this.Left + 5, this.Top + this.TitleHeight + 20, 150, 105);
        g.FillRectangle(Brushes.Gray, rect);
        if (m_op_out.Data != null)
            g.DrawImage((Image)m_op_out.Data, rect);
    }
}

Now we need a node to get image size.

public class ImageSizeNode : STNode {

    private STNodeOption m_op_in;

    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "ImageSize";
        this.TitleColor = Color.FromArgb(200, Color.Goldenrod);
        m_op_in = this.InputOptions.Add("--", typeof(Image), true);
        m_op_in.DataTransfer += new STNodeOptionEventHandler(m_op_in_DataTransfer);
    }

    void m_op_in_DataTransfer(object sender, STNodeOptionEventArgs e) {
        if (e.Status != ConnectionStatus.Connected || e.TargetOption.Data == null) {
            this.SetOptionText(m_op_in, "--");
        } else { 
            Image img = (Image)e.TargetOption.Data;
            this.SetOptionText(m_op_in, "W:" + img.Width + " H:" + img.Height);
        }
    }
}

click the Open Image button, you can select an image and display it in the node. After the ImageSizeNode is connected, the size of the image will be display.

The code of the ImageChannel node is not given here. The code is used in the WinNodeEditorDemo project to extract the RGB channel of an image. For ImageShowNode, it just provides the data source and displays it. For the ImageSizeNode and ImageChannel nodes, they don't know what node will be connected. They just complete their functions and package the results to the output option, waiting to be connected by the next node

The execution logic is completely connected by the user to connect their functions together. During the development, there is no interaction between nodes and nodes. The only thing that ties them together is an Image data type, so that nodes and nodes There is no coupling relationship between them. High class poly low coupling.

[basic]STNodeEditor

STNodeEditor as a container of STNode also provides a large number of properties and events for developers to use. For more detailed API list of STNodeEditor, please refer to API document


Save canvas


The relationship between nodes and connections in STNodeEditor can be saved to a file

The contents of the canvas can be saved to a file through the STNodeEditor.SaveCanvas(string strFileName) function

Note that the SaveCanvas() function will call the internal byte[] STNode.GetSaveData() function to get the binary data of each node

The GetSaveData() function does not serialize the node itself. The GetSaveData() function binarizes the basic data and original attributes of the node itself and then calls virtual OnSaveNode(Dictionary<string, byte[]> dic) to The expansion node asks for the data that the node needs to save

If there is a saving requirement, the node developer may need to override the OnSaveNode() function to ensure that some required data can be saved

More content about saving nodes will be introduced in the following content


Load canvas


The saved data can be loaded from the file through the STNodeEditor.LoadCanvas(string strFileName) function

If STNodeEditor has nodes in other assemblies, you need to load the assembly by calling STNodeEditor.LoadAssembly(string strFile) to ensure that the nodes in the file can be restored correctly

Because the restored node is not serialized, a node is dynamically created by (STNode)Activator.CreateInstance(stNodeType) and then called virtual OnSaveNode(Dictionary<string, byte[]> dic) to restore the data, while dic Is the data saved by OnSaveNode()

Because the restore node is dynamically created through reflection, an empty parameter constructor must be provided in the extended STNode

More content about saving nodes will be introduced in the following content


useful event


ActiveChanged,SelectedChanged can monitor the selected changes of the node in the control

stNodeEditor1.ActiveChanged += (s, e) => Console.WriteLine(stNodeEditor1.ActiveNode.Title);

stNodeEditor1.SelectedChanged += (s, e) => {
    foreach(var n in stNodeEditor1.GetSelectedNode()){
        Console.WriteLine(n.Title);
    }
};

If you want to display the scale on the editor after each zoom of the canvas, you can get it through the CanvasScaled event

stNodeEditor1.CanvasScaled += (s, e) => {
    stNodeEditor1.ShowAlert(stNodeEditor1.CanvasScale.ToString("F2"),
        Color.White, Color.FromArgb(127, 255, 255, 0));
};

If you want to display the connection status when there are nodes connected in the canvas, you can get the status through the OptionConnected event

stNodeEditor1.OptionConnected += (s, e) => {
    stNodeEditor1.ShowAlert(e.Status.ToString(), Color.White,
        Color.FromArgb(125, e.Status ConnectionStatus.Connected ? Color.Lime : Color.Red));
};

useful function


/// <summary>
/// Move the origin position of the canvas to the specified control position 
/// (cannot be moved when there is no Node)
/// </summary>
/// <param name="x">X</param>
/// <param name="y">Y</param>
/// <param name="bAnimation">use animation</param>
/// <param name="ma">Specify the position that needs to be modified</param>
public void MoveCanvas(float x, float y, bool bAnimation, CanvasMoveArgs ma);

/// <summary>
/// Scale Canvas(cannot be moved when there is no Node)
/// </summary>
/// <param name="f">scale</param>
/// <param name="x">The position of the zoom center X on the control</param>
/// <param name="y">The position of the zoom center Y on the control</param>
public void ScaleCanvas(float f, float x, float y);

/// <summary>
/// Add the default data type color to the editor
/// </summary>
/// <param name="t">data type</param>
/// <param name="clr">color</param>
/// <param name="bReplace">replace</param>
/// <returns>new color</returns>
public Color SetTypeColor(Type t, Color clr, bool bReplace);

/// <summary>
/// Display information in the canvas
/// </summary>
/// <param name="strText">message text</param>
/// <param name="foreColor">fore color</param>
/// <param name="backColor">back color</param>
/// <param name="nTime">time</param>
/// <param name="al">message location</param>
/// <param name="bRedraw">redraw</param>
void ShowAlert(string strText, Color foreColor, Color backColor, int nTime, AlertLocation al, bool bRedraw);
//e.g.
stNodeEditor1.ShowAlert("this is test info", Color.White, Color.FromArgb(200, Color.Yellow));

For more propertyfunctioneventsofSTNodeEditor, please refer to API document This document pays more attention to the example of STNode

STNodePropertyGrid

STNodePropertyGrid is another control released with the class library and can be used with STNodeEditor

There are two panels in STNodePropertyGrid, which can be switched by the button on the top Property Panel and Node Information Panel.

Only include attribute or node information will display their panel


how to use


The core method of STNodePropertyGrid is SetNode(STNode) usually used with STNodeEditor

stNodeEditor1.ActiveChanged += (s, e) => stNodePropertyGrid1.SetNode(stNodeEditor1.ActiveNode);

STNode is a class which of course can have properties, and STNodePropertyGrid is to display and modify them, just like what you see in the UI designer during the development of WinForm

Let"s try it

public class PropertyTestNode : STNode {

    private int _Number;

    public int Number {
        get { return _Number; }
        set { _Number = value; }
    }

    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "PropertyTest";
    }
}

You will find that the Number property is not displayed as you expected

The difference with System.Windows.Forms.PropertyGrid is that PropertyGrid will display all the properties in class while STNodePropertyGrid does not.

S T N O D E PropertyGrid This is STNodePropertyGrid and does not display a property casually, because the author thinks that the developer may not want all the properties in STNode to be displayed, even if it needs to be displayed, the developer may not want to display a property. What you see in the window is Number but a different name. After all, Number is used when writing code

Only property with the STNodePropertyAttribute feature added will be displayed in the STNodePropertyGrid


STNodePropertyAttribute


STNodePropertyAttribute has three properties Name, Description and DescriptorType

Name-the name of this property that you want to display on STNodePropertyGrid

Description-The description you want to display when the left mouse button is long press the property name on STNodePropertyGrid

DescriptorType-Data interaction interface with properoty editor, this property will be mentioned later

The constructor of STNodePropertyAttribute is STNodePropertyAttribute(string strName,string strDescription)

public class PropertyTestNode : STNode
{
    private int _Number;
    [STNodeProperty("Name", "Description for this property")]
    public int Number {
        get { return _Number; }
        set { _Number = value; }
    }

    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "PropertyTest";
    }
}

Now you can see that the property is displayed correctly and can be set, and long press the property name will display the description information


STNodeAttribute


If you want to display node information, STNode needs to be marked with the STNodeAttribute feature

[STNode("AA/BB", "Author", "Mail", "Link", "Description")]
public class PropertyTestNode : STNode
{
    private int _Number;
    [STNodeProperty("Name", "Description for this property")]
    public int Number {
        get { return _Number; }
        set { _Number = value; }
    }

    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "PropertyTest";
    }
}

You can switch between panels through the button in the top

The AA/BB is used to construct the path in STNodeTreeView

stNodePropertyGrid1.SetInfoKey("Author", "Mail", "Link", "Show Help");

The key of the content of the Information Panel can be used to set the language through the SetInfoKey() function, which is displayed in simplified Chinese by default.


Show help


In the example of Information Panel, you can see that the Show Help button is not available. If you want it to be available, you need to provide a magic method

[STNode("AA/BB", "Author", "Mail", "Link", "Description")]
public class PropertyTestNode : STNode
{
    private int _Number;
    [STNodeProperty("Name", "Description for this property")]
    public int Number {
        get { return _Number; }
        set { _Number = value; }
    }

    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "PropertyTest";
    }
    /// <summary>
    /// This method is a magic method
    /// If there is "static void ShowHelpInfo(string)" and this class is marked by "STNodeAttribute"
    /// Then this method will be used as the "Show Help" function on the property editor
    /// </summary>
    /// <param name="strFileName">The file path of the module where this class is located.</param>
    public static void ShowHelpInfo(string strFileName) {
        MessageBox.Show("this is -> ShowHelpInfo(string);\r\n" + strFileName);
    }
}

Now find that the Show Help button has become enabled. STNodeAttribute also provides two static functions.

/// <summary>
/// Get help method for stNodeType
/// </summary>
/// <param name="stNodeType">stNodeType</param>
/// <returns>MethodInfo</returns>
public static MethodInfo GetHelpMethod(Type stNodeType);
/// <summary>
/// Excute the "ShowHelpInfo" for stNodeType
/// </summary>
/// <param name="stNodeType">节点类型</param>
public static void ShowHelp(Type stNodeType);

e.g. - Add number


STNodePropertyGrid can display and modify properties. Then this case will provide a data input through the STNodePropertyGrid.

public class NumberInputNode : STNode
{
    private int _Number;
    [STNodeProperty("Input", "Input number")]
    public int Number {
        get { return _Number; }
        set {
            _Number = value;
            this.SetOptionText(m_op_out, value.ToString());
            m_op_out.TransferData(value);
        }
    }

    private STNodeOption m_op_out;

    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "NumberInput";
        m_op_out = this.OutputOptions.Add("0", typeof(int), false);
    }
}
public class NumberAddNode : STNode {

    private STNodeOption m_op_in_1;
    private STNodeOption m_op_in_2;
    private STNodeOption m_op_out;

    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "NumberAdd";
        m_op_in_1 = this.InputOptions.Add("0", typeof(int), true);
        m_op_in_2 = this.InputOptions.Add("0", typeof(int), true);
        m_op_out = this.OutputOptions.Add("0", typeof(int), false);

        m_op_in_1.DataTransfer += new STNodeOptionEventHandler(m_op_in_DataTransfer);
        m_op_in_2.DataTransfer += new STNodeOptionEventHandler(m_op_in_DataTransfer);
    }

    void m_op_in_DataTransfer(object sender, STNodeOptionEventArgs e) {
        if (e.Status != ConnectionStatus.Connected || e.TargetOption == null) {
            if (sender == m_op_in_1) m_op_in_1.Data = 0;
            if (sender == m_op_in_2) m_op_in_2.Data = 0;
        } else {
            if (sender == m_op_in_1) m_op_in_1.Data = e.TargetOption.Data;
            if (sender == m_op_in_2) m_op_in_2.Data = e.TargetOption.Data;
        }
        if (m_op_in_1.Data == null) m_op_in_1.Data = 0;
        if (m_op_in_2.Data == null) m_op_in_2.Data = 0;
        int nResult = (int)m_op_in_1.Data + (int)m_op_in_2.Data;
        this.SetOptionText(m_op_in_1, m_op_in_1.Data.ToString());
        this.SetOptionText(m_op_in_2, m_op_in_2.Data.ToString());
        this.SetOptionText(m_op_out, nResult.ToString());
        m_op_out.TransferData(nResult);
    }
}

Pass the entered number through the set accessor of the Number property.


ReadOnlyModel


In some cases, you don’t want STNodePropertyGrid to set the properties, you just want to display the properties, you can enable ReadOnlyModel

stNodePropertyGrid1.ReadOnlyModel = true;

You can not set the properoty value from STNodePropertyGrid when the ReadOnlyModel is true

STNodePropertyDescriptor

The Name and Description properties of STNodePropertyAttribute are introduced above. There is also a properoty DescriptorType whose data type is Type and the default value is typeof(STNodePropertyDescriptor)

Although from the current case, there is no problem with the above operation, but not all data type properties can be correctly supported by STNodePropertyGrid. The default STNodePropertyDescriptor only supports the following data types.

intfloatdoubleboolstringEnum and their Array


e.g. - ColorTestNode


Create a node below and add an properoty of type Color

public class ColorTestNode : STNode
{
    [STNodeProperty("TitleColor", "Get or set the node TitleColor")]
    public Color ColorTest {
        get { return this.TitleColor; }
        set { this.TitleColor = value; }
    }
    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "ColorNode";
    }
}

If you run the above code, you will find that there will be errors when setting properties through STNodePropertyGrid, and the display of property values in STNodePropertyGrid is also very strange.

Even though System.Windows.Forms.PropertyGrid can support many data types, it does not support all types. For example, when the property type is a user-defined type, the property editor cannot know how to interact with the properties on the property grid.

The solution for System.Windows.Forms.PropertyGrid is to provide TypeConverter, mark the target type with TypeConverter and implement overloading so that PropertyGrid can know how to interact with the PropertyGrid through TypeConverter .

The solution provided by STNodePropertyGrid is STNodePropertyDescriptor.


about properoty discriptor


STNodePropertyGrid can correctly obtain and modify the value of the STNode property, because STNodePropertyDescriptor is performing data conversion and responding to some event operations on the property window.

The property marked by STNodePropertyAttribute will be packaged as STNodePropertyDescriptor and passed to STNodePropertyGrid. A STNodePropertyDescriptor contains the Name and Description in STNodePropertyAttribute and the position information of the attribute will be displayed on STNodePropertyGrid, and How to respond to mouse events or keyboard events.

It can be considered that STNodePropertyDescriptor is a graphical interface for each properoty marked by STNodePropertyAttribute, and the main function is to transfer data between the graphical interface and the real properoty.


GetValueFromString()


/// <summary>
/// Convert the value of the string to the value of the property's type.
/// Only int float double string bool and the above types of Array are supported by default
/// If the target type is not in the above, please override the function to convert it by yourself.
/// </summary>
/// <param name="strText">text</param>
/// <returns>the properoty value</returns>
protected internal virtual object GetValueFromString(string strText);

The GetValueFromString() function converts the string entered by the user in the STNodePropertyGrid into the real value required by the property.

For example: If the property is int type value and the user can only enter the string 123 in STNodePropertyGrid, then the default GetValueFromString() function will be int.Parse(strText) such as string Type 123 becomes 123 of type int


GetStringFromValue()


/// <summary>
/// Convert the value of the properoty to a string.
/// Calling Tostring() of the property value is the default operation.
/// If you need special processing, please override this function to convert by yourself
/// </summary>
/// <returns>string</returns>
protected internal virtual string GetStringFromValue();

The GetStringFromValue() function converts the property value into a string and displays it in the STNodePropertyGrid. The default GetStringFromValue() internally just calls the property value ToString().


e.g. - ColorTestNode


Extend the STNodePropertyDescriptor of ColorTestNode.ColorTest in the above failed example.

public class ColorTestNode : STNode
{
    //Specify to use the extended STNodePropertyDescriptor in order to complete the support for the Color type.
    [STNodeProperty("TitleColor", "Get or set the node TitleColor", DescriptorType = typeof(ColorDescriptor))]
    public Color ColorTest {
        get { return this.TitleColor; }
        set { this.TitleColor = value; }
    }
    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "ColorNode";
    }
}

public class ColorDescriptor : STNodePropertyDescriptor
{
    protected override object GetValueFromString(string strText) {
        string[] strs = strText.Split(',');
        return Color.FromArgb(int.Parse(strs[0]), int.Parse(strs[1]), int.Parse(strs[2]), int.Parse(strs[3]));
        //return base.GetValueFromString(strText);
    }

    protected override string GetStringFromValue() {
        var v = (Color)this.GetValue(null);//get the value
        return v.A + "," + v.R + "," + v.G + "," + v.B;
        //return base.GetStringFromValue();
    }
}

At this time, the property value can be displayed and set correctly, because GetValueFromString() and GetStringFromValue() correctly complete the conversion between the property value and the string.


e.g. - ColorTestNode


Although the above example can be used to set the Color property through ARGB,but this method is not friendly enough.

Can it be the same as System.Windows.Forms.PropertyGrid to allow users to make visual settings. STNodePropertyDescriptor can of course do it.

STNodePropertyDescriptor can be regarded as a custom control that displays properties. Since it is a custom control, it can respond to mouse events.

now add some code to ColorDescriptor

public class ColorDescriptor : STNodePropertyDescriptor
{
    //This rect is used as a color preview for drawing on the STNodePropertyGrid.
    private Rectangle m_rect;

    protected override object GetValueFromString(string strText) {
        string[] strs = strText.Split(',');
        return Color.FromArgb(int.Parse(strs[0]), int.Parse(strs[1]), int.Parse(strs[2]), int.Parse(strs[3]));
        //return base.GetValueFromString(strText);
    }

    protected override string GetStringFromValue() {
        var v = (Color)this.GetValue(null);
        return v.A + "," + v.R + "," + v.G + "," + v.B;
        //return base.GetStringFromValue();
    }

    //Called when the position of this property is confirmed in the STNodePropertyGrid.
    protected override void OnSetItemLocation() {
        base.OnSetItemLocation();
        Rectangle rect = base.RectangleR;
        m_rect = new Rectangle(rect.Right - 25, rect.Top + 5, 19, 12);
    }
    //Called when drawing this properoty value area in the STNodePropertyGrid
    protected override void OnDrawValueRectangle(DrawingTools dt) {
        base.OnDrawValueRectangle(dt);//call the base and then draw color preview
        dt.SolidBrush.Color = (Color)this.GetValue(null);
        dt.Graphics.FillRectangle(dt.SolidBrush, m_rect);//fill color
        dt.Graphics.DrawRectangle(Pens.Black, m_rect);   //draw rectangle
    }

    protected override void OnMouseClick(MouseEventArgs e) {
        //If the user clicks in the color preview area, show system color dialog.
        if (m_rect.Contains(e.Location)) {
            ColorDialog cd = new ColorDialog();
            if (cd.ShowDialog() != DialogResult.OK) return;
            this.SetValue(cd.Color, null);
            this.Invalidate();
            return;
        }
        //Otherwise, show the text input box by default.
        base.OnMouseClick(e);
    }
}

At this point, you can see that compared with the previous example, there is one more color preview area, and clicking the preview area will pop up the system color dialog box to set the property value. If you click in the non-preview area, the default operation method will be show text input box to input ARGB.

About STNodePropertyDescriptor there are two important overloaded functions GetBytesFromValue() and SetValue(byte[]) which will be introduced in Save Canvas later.

STNodeTreeView

STNodeTreeView, like STNodePropertyGrid, is another control released with the class library and can be used with STNodeEditor.

The nodes in STNodeTreeView can be dragged and dropped into STNodeEditor, and preview and search functions are provided.

STNodeTreeView is easy to use, and there is no need to create a tree directory by yourself like System.Windows.Forms.TreeView.

Mark the STNode subclass by STNodeAttribute to set the path you want to display in STNodeTreeView and the information you want to display in STNodePropertyGrid.

Note: If you want nodes to be displayed in STNodeTreeView, you must use STNodeAttribute to mark the STNode subclass.

how to use

The nodes in the above example are all added through the STNodeEditor.Nodes.Add(STNode) method, and then added through the STNodeTreeView drag-and-drop method. But before that, you need to add the nodes to the STNodeTreeView first.

[STNode("AA/BB", "Author", "Mail", "Link", "Description")]
public class MyNode : STNode
{
    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "TreeViewTest";
    }
}
//add to STNodeTreeView
stNodeTreeView1.AddNode(typeof(MyNode));

You can see that the added nodes are displayed in STNodeTreeView and the path is automatically created. The nodes can be previewed and dragged to STNodeEditor.

There cannot be a node with the same name in the same path, otherwise it will be overwritten.


Load assembly


In addition to STNodeTreeView.AddNode(Type) to add nodes to the tree, you can also load nodes from an assembly through the LoadAssembly(string) method. LoadAssembly(string) will automatically check the nodes marked by STNodeAttribute in the assembly and add them, and will create a root node display with the assembly name.

stNodeTreeView1.AddNode(typeof(MyNode));
stNodeTreeView1.LoadAssembly(Application.ExecutablePath);
//If the node is in another assembly, STNodeEditor should do the same to confirm that the node is recognized by STNodeEditor.
stNodeEditor1.LoadAssembly(Application.ExecutablePath);

You can see that there are two root nodes AA and WinNodeEditorDemo. In fact, MyNode belongs to the WinNodeEditorDemo assembly, but it cannot be found in the child nodes of WinNodeEditorDemo. Because MyNode is added before loading the assembly, when the loading assembly recognizes that MyNode has been added repeatedly, it is skipped.

STNodeTreeView cannot add a node of the same type and the same name with the same path.

Save nodes

In the introduction of STNodeEditor above, we mentioned the saving and loading of canvas. Here we will introduce it in detail.


Save


When STNodeEditor.SaveCanvas(), STNodeEditor will call internal byte[] STNode.GetSaveData() of each Nodes to get the binary data of each node.

GetSaveData() is an internal method modified by internal. The method body is as follows.

internal byte[] GetSaveData() {
    List<byte> lst = new List<byte>();
    Type t = this.GetType();
    byte[] byData = Encoding.UTF8.GetBytes(t.Module.Name + "|" + t.FullName);
    lst.Add((byte)byData.Length);
    lst.AddRange(byData);
    byData = Encoding.UTF8.GetBytes(t.GUID.ToString());
    lst.Add((byte)byData.Length);
    lst.AddRange(byData);

    var dic = this.OnSaveNode();
    if (dic != null) {
        foreach (var v in dic) {
            byData = Encoding.UTF8.GetBytes(v.Key);
            lst.AddRange(BitConverter.GetBytes(byData.Length));
            lst.AddRange(byData);
            lst.AddRange(BitConverter.GetBytes(v.Value.Length));
            lst.AddRange(v.Value);
        }
    }
    return lst.ToArray();
}

You can see that GetSaveData() gets the binary of its own basic data, and then calls OnSaveNode() to get a dictionary.

internal Dictionary<string, byte[]> OnSaveNode() {
    Dictionary<string, byte[]> dic = new Dictionary<string, byte[]>();
    dic.Add("Guid", this._Guid.ToByteArray());
    dic.Add("Left", BitConverter.GetBytes(this._Left));
    dic.Add("Top", BitConverter.GetBytes(this._Top));
    dic.Add("Width", BitConverter.GetBytes(this._Width));
    dic.Add("Height", BitConverter.GetBytes(this._Height));
    dic.Add("AutoSize", new byte[] { (byte)(this._AutoSize ? 1 : 0) });
    if (this._Mark != null) dic.Add("Mark", Encoding.UTF8.GetBytes(this._Mark));
    dic.Add("LockOption", new byte[] { (byte)(this._LockLocation ? 1 : 0) });
    dic.Add("LockLocation", new byte[] { (byte)(this._LockLocation ? 1 : 0) });
    Type t = this.GetType();
    foreach (var p in t.GetProperties()) {
        var attrs = p.GetCustomAttributes(true);
        foreach (var a in attrs) {
            if (!(a is STNodePropertyAttribute)) continue;
            var attr = a as STNodePropertyAttribute;//Get the property marked by STNodePropertyAttribute and save them automatically.
            object obj = Activator.CreateInstance(attr.DescriptorType);
            if (!(obj is STNodePropertyDescriptor))
                throw new InvalidOperationException("[STNodePropertyAttribute.Type]参数值必须为[STNodePropertyDescriptor]或者其子类的类型");
            var desc = (STNodePropertyDescriptor)Activator.CreateInstance(attr.DescriptorType);
            desc.Node = this;
            desc.PropertyInfo = p;
            byte[] byData = desc.GetBytesFromValue();//Get the binary data of the property through STNodePropertyDescriptor.
            if (byData == null) continue;
            dic.Add(p.Name, byData);
        }
    }
    this.OnSaveNode(dic);
    return dic;
}
/// <summary>
/// When saving the node, get the data that needs to be saved from the expansion node.
/// Note: Saving is not serialization. When restoring, only re-create this Node through the empty parameter constructor,
/// and then call OnLoadNode() to restore the saved data.
/// </summary>
/// <param name="dic">the data need to save</param>
protected virtual void OnSaveNode(Dictionary<string, byte[]> dic) { }

From the above code, you can see that STNode will save binary data of its own basic property, and will recognize the property marked by STNodePropertyAttribute and get the property binary data through GetBytesFromValue(), and then call OnSaveNode(dic) Ask the expansion node for the data that needs to be saved.

If there is an properoty that needs to be saved, it should be marked with STNodePropertyAttribute and make sure that GetBytesFromValue() can correctly get the binary data of the property, or save it through OnSaveNode(dic)

If there are private fields that need to be saved, they should be saved through OnSaveNode(dic).


OnSaveNode(dic)


OnSaveNode(dic) and OnLoadNode(dic) correspond to each other.

protected override void OnSaveNode(Dictionary<string, byte[]> dic) {
    dic.Add("count", BitConverter.GetBytes(this.InputOptionsCount));
}

protected internal override void OnLoadNode(Dictionary<string, byte[]> dic) {
    base.OnLoadNode(dic);
    int nCount = BitConverter.ToInt32(dic["count"], 0);
    while (this.InputOptionsCount < nCount && this.InputOptionsCount != nCount) this.Addhub();
}

The above code snippet is the overload of OnSaveNode(dic) and OnLoadNode(dic) in STNodeHub. You can see that an additional count data is saved because the option of STNodeHub is dynamically created, and a newly created STNodeHub has only one line of connection. Therefore, you need to record the status when saving to ensure that the connection status can be restored correctly, and then restore the status in OnLoadNode(dic).

In addition to saving node data, STNodeEditor also saves the connection relationship of node options. It will number each STNodeOption of all nodes in the current canvas and save the numbering information. Therefore, when saving and restoring STNodeHub, make sure that the number of STNodeOption is unchanged.


GetBytesFromValue()


For properties marked by STNodePropertyAttribute, STNodePropertyDescriptor.GetBytesFromValue() will be automatically called when saving to get the binary data of the attribute.

GetBytesFromValue() and GetValueFromBytes() correspond to each other

/// <summary>
/// Convert properoty value to binary It is called when saving node properoty.
/// By default, GetStringFromValue() is called and then the string is converted to binary data.
/// If you need special processing, please override this method to convert by yourself and override GetValueFromBytes()
/// </summary>
/// <returns>binary data</returns>
protected internal virtual byte[] GetBytesFromValue() {
    string strText = this.GetStringFromValue();
    if (strText == null) return null;
    return Encoding.UTF8.GetBytes(strText);
}
/// <summary>
/// When restoring the properoty value, convert the binary to the properoty value.
/// Convert it to a string by default and then call GetValueFromString(string).
/// If override this method,you should override GetBytesFromValue() together.
/// </summary>
/// <param name="byData">binary data</param>
/// <returns>properoty value</returns>
protected internal virtual object GetValueFromBytes(byte[] byData) {
    if (byData == null) return null;
    string strText = Encoding.UTF8.GetString(byData);
    return this.GetValueFromString(strText);
}

STNodePropertyDescriptor.GetBytesFromValue() calls STNodePropertyDescriptor.GetStringFromValue() by default to get the string value of the property and then converts the string to byte[].

If GetStringFromValue() and GetValueFromString(strText) can run correctly, then the property value can be saved correctly using the default processing method.


OnLoadNode(dic)


When restoring a node, STNodeEditor creates a node through (STNode)Activator.CreateInstance(stNodeType) and then calls OnLoadNode(dic).

protected internal virtual void OnLoadNode(Dictionary<string, byte[]> dic) {
    if (dic.ContainsKey("AutoSize")) this._AutoSize = dic["AutoSize"][0] == 1;
    if (dic.ContainsKey("LockOption")) this._LockOption = dic["LockOption"][0] == 1;
    if (dic.ContainsKey("LockLocation")) this._LockLocation = dic["LockLocation"][0] == 1;
    if (dic.ContainsKey("Guid")) this._Guid = new Guid(dic["Guid"]);
    if (dic.ContainsKey("Left")) this._Left = BitConverter.ToInt32(dic["Left"], 0);
    if (dic.ContainsKey("Top")) this._Top = BitConverter.ToInt32(dic["Top"], 0);
    if (dic.ContainsKey("Width") && !this._AutoSize) this._Width = BitConverter.ToInt32(dic["Width"], 0);
    if (dic.ContainsKey("Height") && !this._AutoSize) this._Height = BitConverter.ToInt32(dic["Height"], 0);
    if (dic.ContainsKey("Mark")) this.Mark = Encoding.UTF8.GetString(dic["Mark"]);
    Type t = this.GetType();
    foreach (var p in t.GetProperties()) {
        var attrs = p.GetCustomAttributes(true);
        foreach (var a in attrs) {
            if (!(a is STNodePropertyAttribute)) continue;
            var attr = a as STNodePropertyAttribute;
            object obj = Activator.CreateInstance(attr.DescriptorType);
            if (!(obj is STNodePropertyDescriptor))
                throw new InvalidOperationException("[STNodePropertyAttribute.Type]参数值必须为[STNodePropertyDescriptor]或者其子类的类型");
            var desc = (STNodePropertyDescriptor)Activator.CreateInstance(attr.DescriptorType);
            desc.Node = this;
            desc.PropertyInfo = p;
            try {
                if (dic.ContainsKey(p.Name)) desc.SetValue(dic[p.Name]);
            } catch (Exception ex) {
                string strErr = "属性[" + this.Title + "." + p.Name + "]的值无法被还原 可通过重写[STNodePropertyAttribute.GetBytesFromValue(),STNodePropertyAttribute.GetValueFromBytes(byte[])]确保保存和加载时候的二进制数据正确";
                Exception e = ex;
                while (e != null) {
                    strErr += "\r\n----\r\n[" + e.GetType().Name + "] -> " + e.Message;
                    e = e.InnerException;
                }
                throw new InvalidOperationException(strErr, ex);
            }
        }
    }
}

The default OnLoadNode(dic) only restores its own basic properties and properties marked by STNodePropertyAttribute.

For OnLoadNode(dic) of STNodeHub

protected internal override void OnLoadNode(Dictionary<string, byte[]> dic) {
    base.OnLoadNode(dic);//First restore its own properties.
    int nCount = BitConverter.ToInt32(dic["count"], 0);
    while (this.InputOptionsCount < nCount && this.InputOptionsCount != nCount) this.Addhub();
}

OnEditorLoadCompleted


If some nodes need some initialization operations after the canvas is restored, you can override OnEditorLoadCompleted to complete the operation.

/// <summary>
/// Occurs when the editor has finished loading all nodes.
/// </summary>
protected internal virtual void OnEditorLoadCompleted();

THE END

Thank you for reading. Thank you for choosing STNodeEditor. If you think STNodeEditor is not bad, you can recommend it to your friends.