STNodeEditor - modularize your program functions

catalogue

brief introduction

Use your functions like a flowchart

How to use it?

How to interact with data?

About the next version

Concluding remarks

brief introduction

It was a winter, and the author who was studying radio safety came into contact with it GNURadio That was the author's first exposure to the node editor

-> What? Excuse me... What "s this

It was a spring. I don't know why the whole world changed after the new year. Everyone was forced to stay at home. The bored author learned Blender That was the author's second exposure to the node editor

-> Wo... So this thing can play like this That's convenient

So some ideas are gradually born in the author's mind, which makes the author have the idea of making such a thing

It was a summer. I don't know why the author played again Davinci It was the third time that the author came into contact with the node editor. This contact doubled the author's favor for the node editor. The author instantly felt that everything can be nodalized as long as it is a function that can be modularized and processed

Use your functions like a flowchart

Have you ever imagined that a flowchart is no longer a flowchart, but can be executed directly?

In some development processes, we may design a flow chart for the whole program, which contains our existing functional modules and execution flow, and then the developers will implement them one by one

However, this will bring some problems. The execution process of the program may be hard coded into the program. If suddenly one day it may be necessary to change the execution order or add or delete an execution module, the developer may not be required to re edit and compile the code, and the calls between various functional modules also need the developer to code and schedule, increasing the development cost and so on topic

And STNodeEditor was born for this

   

STNodeEditor is a WinForm based framework. It is developed with GDI + without any additional dependencies. The whole call library is only 100+kb

Project home page: https://debugst.github.io/STNodeEditor/

Tutorial documentation: https://debugst.github.io/STNodeEditor/doc_cn.html 

NuGet: https://www.nuget.org/packages/ST.Library.UI/

GitHub: https://github.com/DebugST/STNodeEditor 

As can be seen from the above figure, stnodededitor includes three parts treeview PropertyGrid nodededitor, which constitute a complete set of usable framework

  • TreeView
    • Development can encode the execution functions into a node, while TreeView is responsible for displaying and retrieving nodes. Nodes in TreeView can be directly dragged and added to NodeEditor
  • PropertyGrid
    • Similar to the attribute window used in WinForm development, as a node, it can also have attributes. In the process of editor design, the author also regards a node as a Form, so that developers can directly start the development of a node with little learning cost
  • NodeEditor
    • Nodededitor is the place where users combine their own execution processes to visualize the execution process of functional modules

How to use it?

The use of STNodeEditor is very simple. You can use it almost without any learning cost. Of course, the most important thing is that you need to know how to create a node

You can create a Node just like creating a Form

using ST.Library.UI.NodeEditor;

public class MyNode : STNode
{
    public MyNode() { //Equivalent to [void] {
        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 to editor
stNodeEditor.Nodes.Add(new MyNode());

You can see that its usage is really similar to that of Form. In fact, there is no WYSIWYG UI Designer currently, and an STNode also has its control collection and the data type is STNodeControl

STNodeControl, as the base class of STNode control, has and system Windows. Forms. Many properties and events with the same name in control are designed to be close to WinForm

Note: in the current version (2.0), stnodededitor only provides the STNodeControl base class and does not provide any available controls. Of course, some examples are included in the attached Demo project to demonstrate how to customize a control. Because this belongs to the scope of custom controls, there are not many demonstrations. For more information on how to develop custom controls, please refer to the author: Custom control development Of course, in the subsequent versions of the series of articles, the author will provide some common controls, although the author wants to rely on WinForm for its use, just using it as WinForm is not the author's original intention

The above demonstration is just to make you feel cordial. After all, WinForm may be a familiar thing, but if you just use it as WinForm, it makes no sense. Of course, the most important attribute for a node is the input and output of data

public class MyNode : STNode
{
    protected override void OnCreate() {//Equivalent to [public MyNode() {}]
        base.OnCreate();
        this.Title = "TestNode";
        //You can get the added index position
        int nIndex = this.InputOptions.Add(new STNodeOption("IN_1", typeof(string), false));
        //You can get the added STNodeOption
        STNodeOption op = this.InputOptions.Add("IN_2", typeof(int), true);
        this.OutputOptions.Add("OUT", typeof(string), false);
    }
    //When the owner changes (that is, it is added or removed in NodeEditor)
    //The container should be presented with the desired color for the connection point that has its own data type
    //Color is mainly used to distinguish different data types
    protected override void OnOwnerChanged() {
        base.OnOwnerChanged();
        if (this.Owner == null) return;
        this.Owner.SetTypeColor(typeof(string), Color.Yellow);
        //The existing colors in the current container will be replaced
        this.Owner.SetTypeColor(typeof(int), Color.DodgerBlue, true); 
        //The following code will ignore the existing colors in the container
        //this.SetOptionDotColor(op, Color.Red); // No need to set in OnOwnerChanged()
    }
}

Through the above case, you can see that STNode has two important attributes InputOptions and OutputOptions, and its data type is STNodeOption, while STNodeOption has two connection modes: single connection and multi connection

  • single-connection
    • Single connection mode in single connection mode, a connection point can only be connected by one point of the same data type at the same time
  • multi-connection
    • Multi connection mode in multi connection mode, a connection point can be connected by multiple points of the same data type at the same time
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);
    }
}

How to interact with data?

In the above case, only an option point that can be connected is made and does not contain any functions

  • STNodeOption can get all the data passed into this option by binding DataTransfer event
  • STNodeOption can deliver data to all connected options on this option through transferdata (object object)

The following is a case to demonstrate the creation of two nodes. One node is used to output the current system event once per second, and the other node is used to receive and display an event

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 added or removed
    protected override void OnOwnerChanged() {
        base.OnOwnerChanged();
        if (this.Owner == null) {   //If removed, stop the 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 set stnodeoption Data
                //Then automatically transfer data to all connected options
                m_op_out_time.TransferData(DateTime.Now);
                //If you need some time-consuming operations, STNode also provides Begin/Invoke() operations
                //this.BeginInvoke(new MethodInvoker(() => {
                //    m_op_out_time.TransferData(DateTime.Now);
                //}));
            }
        }) { IsBackground = true };
        m_thread.Start();
    }
}

Of course, the time can be displayed in a straight line, but here, in order to demonstrate the transmission of data, a receiving node is also required

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

    void op_DataTransfer(object sender, STNodeOptionEventArgs e) {
        //This event will be triggered when the connection is established or disconnected, so you need to judge the connection status
        if (e.Status != ConnectionStatus.Connected || e.TargetOption.Data == null) {
            //When stnode Autosize = true. Stnode is not recommended SetOptionText
            //Because the layout will be recalculated when the text changes. The correct way is to customize a control such as label
            //As the display of time, of course, this scheme is adopted here for demonstration
            this.SetOptionText(m_op_time_in, "--");
        } else {
            this.SetOptionText(m_op_time_in, ((DateTime)e.TargetOption.Data).ToString());
        }
    }
}

You can see that the ShowTime node refreshes every second when the connection is established. The following is a more complex case, but the code is not given here. Please refer to the Demo of the attached project

 

Click the Open Image button to open and display a picture in the ImageShowNode and take the picture as output data. ImageChanel is responsible for receiving an image and processing the RGB image and original image of the output image. ImageSize is responsible for receiving and displaying the size information of an image

During the development of the above nodes, they do not know what kind of nodes they will be connected to, nor do they know what nodes they will be connected to. Developers only complete their own functions, process the received data and package the results to STNodeOption. There is no need to know who will take away and process the results, which greatly reduces the coupling relationship between nodes One thing that connects them is an Image data type. The final execution logic is handed over to the users themselves. Drag and drop nodes to combine the processes they want to make the function execution process visual, which is also the author's original intention

For more tutorials and documentation, please refer to: https://debugst.github.io/STNodeEditor/doc_cn.html The downloaded call library package also contains the offline version of the document

About the next version

In fact, there are still many codes that need to be improved in this version. For example, the above-mentioned basic controls are provided, and the currently provided things are still very primitive. At present, some application scenarios need developers to write their own code

The above figure shows the author's original idea and the first Demo version. In the above figure, you can see that there is a start button. In some application scenarios, the user may need to click the execute button before starting to execute the logic deployed by the user. Before, the case data interaction above is more user-friendly and real-time. Of course, in the current version, it is also possible to realize it, but it is necessary Developers write part of the code themselves. Because the author of this part of the code has not yet conceived many details, many functions will appear in the next version

The idea in the figure above is that developers do not need relational architecture to execute logic, but developers only need relational function points. In this province, they only need to develop the # DLL # file containing STNode. When the program starts TreeView, it will automatically load the # DLL # file in the directory and load STNode into TreeView, and then let the user drag and execute. For the above mentioned needs to be executed through the start button, as shown in the previous paragraph What is the implementation of the current version? The author gives some ideas here

//First, define a base class containing Start and Stop methods
public abstract class BaseNode : STNode
{
    public abstract void Start();
    public abstract void Stop();
}
//===================================================================
//Then three types are defined based on the base class
//InputNode will act as the start node and the entry node for data execution, similar to the Main function
public abstract class InputNode : BaseNode { }
//OutputNode will be used as the processing node of final data, such as file saving, database saving, etc
public abstract class OutputNode : BaseNode { }
//You need to define nodes that perform other functions
public abstract class ExecNode : BaseNode { }
//===================================================================
//Create a TestInputNode, provide a string input and serve as the start node
public class TestInputNode : InputNode
{
    //If the "STNodeProperty" property is used, this property will be displayed in the "STNodePropertyGrid"
    [STNodeProperty("The name of the property you want to display", "Attribute second speed")]
    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 the execution starts, the data is transferred to the connected options
        m_op_out.TransferData(this.TestText);
        this.LockOption = true;//Lock options after start
    }

    public override void Stop() {
        this.LockOption = false;//After unlocking option
    }
}
//===================================================================
//Create a TextFileOutputNode to save the received string in a text file
public class TextFileOutputNode : OutputNode
{
    [STNodeProperty("Display attribute name", "Attribute 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;
        //Write text when a data is received
        lock (m_writer) m_writer.WriteLine(e.TargetOption.Data.ToString());
    }

    public override void Start() {
        //Initialize the file at the beginning
        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;
    }
}

The above code demonstrates an input and output type node. As for other requirements, draw inferences from one instance when the user clicks the start button

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);
        }
    }
    //There are some things you should deal with before you really start
    if (lst_output.Count == 0)
        throw new Exception("Can't find [OutputNode] Please add a node of type.");
    if (lst_input.Count == 0)
        throw new Exception("Can't find [InputNode] Please add a node of type.");
    foreach (var v in lst_other) v.Start();
    foreach (var v in lst_output) v.Start();
    //At least there must be at least another node of InputNode type, otherwise how to start
    //Moreover, the node of InputNode type should be the last to start
    foreach (var v in lst_input) v.Start();
    stNodePropertyGrid1.ReadOnlyModel = true;//Don't forget to set the properties window to read-only
}

If you want only one InputNode type node to be added

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("There can only be one InputNode Added");
        stNodeEditor.Nodes.Remove(e.Node);
    }
}

Of course, this demand is estimated to be rare

Of course, the execution effect of the above code fragments is not given here, because the above only provides ideas for readers to draw inferences from one instance, and the above code does not have any exception handling. In fact, there are still many details to be handled and many codes to be written. Therefore, it is temporarily determined that the current version does not provide such a function

Concluding remarks

When there are many applications (modules), they need to call each other and transfer data to complete the work of a whole set of processes, and develop single function applications (modules) It is relatively easy to implement a whole set of applications with many functions calling each other, which is relatively cumbersome. The developers of this set of framework only need to define the data types to be transmitted, and then realize the functions of a single node respectively. As for the execution process, it can be handed over to the framework and user wiring

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

If you think this project is good, you can pay attention to it:

GitHub: https://github.com/DebugST/STNodeEditor

Gitee: https://gitee.com/DebugST/DotNet_WinForm_NodeEditor (sync GitHub only)

thank you!!!

Keywords: winform Open Source

Added by spectacell on Wed, 09 Feb 2022 18:35:29 +0200