Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

STNodeEditor - Make All Your Functions in Node

4.84/5 (7 votes)
20 May 2021MIT6 min read 15.3K  
Have you ever imagined that your flowchart is executable?
STNodeEditor is a lightweight and powerful node editor, and is very simple to use. Provides a wealth of properties and events, which can easily complete the data interaction and notification between nodes. A large number of virtual functions are available for developers to use. It is very free.

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 the author didn't know why the whole world had 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. The author didn’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.

Make Your Functions as Flowchart

Many times when we are doing development, we will use flowcharts, and code to complete the functions and execution processes on the flowcharts.

But this will cause a problem, our function execution process will be hard-coded into the program, and the execution process is not visible to the user. And when we need to change the execution process, we have to re-modify the code.

STNodeEditor is to solve such problems.

Image 1

This project home page: https://debugst.github.io/STNodeEditor/index_en.html

You can see that the above picture contains a NodeEditor, TreeView and a PropertyGrid. They are combined into a complete framework.

  • TreeView:
    • You can code your function into the node, and then add the node to the TreeView. The nodes in the TreeView can be directly dragged and added to the NodeEditor.
  • PropertyGrid:
    • Maybe your node may need some properties, and PropertyGrid can provide access operations to modify node properties just like the WinForm designer.
  • NodeEditor:
    • The Node Editor combines the functions of the nodes through wires. Let your function execution process become visualized.

How to Use It?

STNodeEditor is very simple to use, you hardly need any learning costs. First, you need to know how to create a node.

You can create a node very simply like use WinForm:

C#
public class MyNode : STNode
{
    public MyNode() { //same as [override void 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");
    }
}
//Add it to NodeEditor
stNodeEditor.Nodes.Add(new MyNode());

Image 2

You can see that it is almost the same as developing a WinForm program. The only difference is that STNode currently does not provide a WYSIWYG UI designer. Of course, the control type required by STNode is 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.

Note: 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.

The above example is just to make everyone feel familiar, because it is very similar to WinForm.
The most important function for a node is data input and output. For a node, there are two important properties, InputOptions and OutOptions and the data type is STNodeOption.

C#
public class MyNode : STNode
{
    protected override void OnCreate() {//same as [public MyNode(){}]
        base.OnCreate();
        this.Title = "TestNode";
        //Get the index 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);
    }
    //Occurs when the owner changes, submit the color
    //Color is used to distinguish between different data types.
    //The different data types cannot be connected.
    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); 
        //The highest priority, the color information in the container will be ignored 
        //this.SetOptionDotColor(op, Color.Red); //Not need set it on OnOwnerChanged()
    }
}

Image 3

Now you can see that the node has a point that can be used to connect. But currently, the connection point does not have any function.

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.

C#
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);
    }
}

Image 4

  • 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)

How to Interact With Data?

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.

Let's make two nodes to show it:

Create a node whose function is to output the current system time every second.

C#
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 set STNodeOption.Data automatically
                //and automatically post data to all connections on the option
                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.

C#
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());
        }
    }
}

Image 5

You can see that ShowClockNode is refreshing every second.

ImageNode

The above is a relatively complicated example, and the code is not given here.

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.

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.

For more tutorials, please refer to this link.

About the Future

The current framework is not perfect, it only provides some very basic functions, and there are still many functions to be realized. The author will continue to update in subsequent versions. Such as provides some common controls available to nodes and a set of executable frameworks, so that developers only need to provide DLL files containing STNode. Like this picture:

Image 7

This is the author's original idea and the first demo. Developers only need to provide the DLL file containing STNode. The program will be automatically loaded into the TreeView, and the user only needs to drag the Node to the NodeEditor to combine the logic and execution flow.

You can see the Start button on it, yes. In some application scenarios, the developer hopes that the user clicks the Start button after processing the logic to start the execution process.

It can also be done in the current version, but you need to implement it yourself. First, you need to define a rule, for example, each node must contain Start and Stop functions. Or the node that only needs to provide data input must include Start and Stop functions.

Such as:

C#
//define the BaseNode
public abstract class BaseNode : STNode
{
    public abstract void Start();
    public abstract void Stop();
}
//===================================================================
//InputNode is the starting node and provides data input entry.
public abstract class InputNode : BaseNode { }
//OutputNode is the ending node and process the result data
//such as save data to file or database .etc
public abstract class OutputNode : BaseNode { }
//other function node
public abstract class ExecNode : BaseNode { }
//===================================================================
//create a TestInputNode to provides a string to start
public class TestInputNode : InputNode
{
    //Use "STNodeProperty" will be display in "STNodePropertyGrid"
    [STNodeProperty("NameInPropertyGrid", "Description")]
    public string TestText { get; set; }

    private STNodeOption m_op_out;

    protected override void OnCreate() {
        base.OnCreate();
        this.Title = "StringInput";
        m_op_out = this.OutputOptions.Add("OutputString", typeof(string), false);
    }

    public override void Start() {
        //When user click the "Start" transfer the data to all the connected nodes
        m_op_out.TransferData(this.TestText);
        this.LockOption = true;//Lock all the options
    }

    public override void Stop() {
        this.LockOption = false;//unlock all the options
    }
}
//===================================================================
//create a TextFileOutputNode to save text to file
public class TextFileOutputNode : OutputNode
{
    [STNodeProperty("FileName", "Description")]
    public string FileName { get; set; }

    private StreamWriter m_writer;

    protected override void OnCreate() {
        base.OnCreate();
        this.InputOptions.Add("Text", typeof(string), false)
            .DataTransfer += new STNodeOptionEventHandler(op_DataTransfer);
    }

    void op_DataTransfer(object sender, STNodeOptionEventArgs e) {
        if (e.Status != ConnectionStatus.Connected) return;
        if (e.TargetOption.Data == null) return;
        if (m_writer == null) return;
        //When get a text write it to file
        lock (m_writer) m_writer.WriteLine(e.TargetOption.Data.ToString());
    }

    public override void Start() {
        m_writer = new StreamWriter(this.FileName, false, Encoding.UTF8);
        this.LockOption = true;
    }

    public override void Stop() {
        this.LockOption = false;
        if (m_writer == null) return;
        m_writer.Close();
        m_writer = null;
    }
}

And when the Start button was clicked:

C#
public void OnClickStart() {
    List<InputNode> lst_input = new List<InputNode>();
    List<OutputNode> lst_output = new List<OutputNode>();
    List<BaseNode> lst_other = new List<BaseNode>();
    foreach (var v in stNodeEditor.Nodes) {
        if ((v is BaseNode)) continue;
        if (v is InputNode) {
            lst_input.Add((InputNode)v);
        } else if (v is OutputNode) {
            lst_output.Add((OutputNode)v);
        } else {
            lst_other.Add((BaseNode)v);
        }
    }
    //before the start you should check something.
    if (lst_output.Count == 0)
        throw new Exception("Can not found [OutputNode] please add it.");
    if (lst_input.Count == 0)
        throw new Exception("Can not found [InputNode] please add it.");
    foreach (var v in lst_other) v.Start();
    foreach (var v in lst_output) v.Start();
    //The InputNode should be Start at the last.
    foreach (var v in lst_input) v.Start();
    stNodePropertyGrid1.ReadOnlyModel = true;//not forget this
}

If you want only one InputNode to be added:

C#
stNodeEditor.NodeAdded += new STNodeEditorEventHandler(stNodeEditor_NodeAdded);
void stNodeEditor_NodeAdded(object sender, STNodeEditorEventArgs e) {
    int nCounter = 0;
    foreach (var v in stNodeEditor.Nodes) {
        if (v is InputNode) nCounter++;
    }
    if (nCounter > 1) {
        System.Windows.Forms.MessageBox.Show("Only one InputNode can be added");
        stNodeEditor.Nodes.Remove(e.Node);
    }
}

But I think no one has such a demand, right?

Of course, the above code does not have any exception handling, just to show you how to implement the logic. In fact, a lot of code needs to be supplemented and improved in order to make a universal framework, so the author intends to complete it in subsequent versions. The above code is just to show you how to do it by yourself if you have similar requirements as above.

Points of Interest

Image 8

When there are many applications (modules), they need to call each other to transfer data to complete a set of processes.

It is easy to develop a single-function application (module), but it is tedious to develop a whole set of applications that call each other with many functions.

Developers using this framework only need to define the data type, and then develop a single function node, as for the execution process, hand it over to the framework and the user connection.

Image 9

For more information, please refer to https://debugst.github.io/STNodeEditor/index_en.html.

If you think STNodeEditor is useful, you can recommend it to your friends, and mark it.

Thanks for reading!

History

  • 19th May, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License