C#和WPF实现图形化编程

图形化编程在很多领域都应用很多,比如儿童编程、硬件控制,目前最火的两个引擎肯定应该是scratch和google blocky,很多图形化编程软件都是基于这两个引擎,比如国内编程猫使用了scratch,makecode使用了google blocky。

使用其他引擎必然会收到很大的限制,不如要实现比如局部变量等基本功能时候,使用scratch就没法实现了,而且scratch和blocky都是基于javascript,如果要控制某些硬件,就要做很多javascript和其他硬件的通信模块,不是很方便,所以我们也许要自己写一个图形化编程工具。我工作一直用图形化工具控制硬件,但是老外的软件做的很一般,有天突然想是否自己也能写一个图形化编程工具,就花了点时间,用C#和WPF写了一个图形化工具,代码已经放在github下开源

代码地址 https://github/weihuajiang/WPF-Blocky

也在代码中实现了一个代码编辑器,和scratch一样的编辑器

程序语法树AST的实现

图形化程序也是程序,要显示和运行,我们必须要知道程序语法树(abstract syntax tree)的概念。语法树可以分为表达式Expression、语句Statement和声明Declaration。比如简单的一个语句a+b,就是一个BinaryExpression,而a+b;虽然多了分号,不过就编程了一个Statement语句,这个是个ExpressionStatement,其中的表达式就是BinaryExpression。更多AST的信
息大家可以看esprima解释的很详细。在我们程序中,我们定义了AST基类Node和三个基本的语法树结构Expression、Statement和Decaration(主要就是函数定义),与之对应基本控件也就只有三个控件,Expression控件、Statement控件和Function控件。WPF的数据绑定,让控件编写简化了很多。

程序的运行

程序的运行,简单来说就是各个语法树节点的运行,比如a+5;可以转换成以下结构:

            "type": "ExpressionStatement",
            "expression": {
                "type": "BinaryExpression",
                "operator": "+",
                "left": {
                    "type": "Identifier",
                    "name": "a"
                },
                "right": {
                    "type": "Literal",
                    "value": 5,
                    "raw": "5"
                }
            }

执行顺序是ExpressionStatement->BinaryExpression->left, right,返回left+right,left是Identifier是变量,需要从运行环境获得变量的值,right是Literal是整数5,这样就能获得运行的结果。

程序跳转的实现

break、continue和return,还有异常都会导致程序运行顺序发生跳转,我们通过程序返回值类型来实现程序的跳转。返回值总共有五种类型,分别对应五种可能情况,如果执行过程中出现异常,则应返回Exception,如果正确执行返回Value类型,循环会处理continue和break,函数处理return,如果执行过程中返回非Value,则应该把改返回类型返回,这样就解决了程序跳转的问题。

    public enum CompletionType
    {
        Value,
        Continue,
        Break,
        Return,
        Exception
    }
    public class Completion
    {
        public static Completion Void = new Completion();
        public Node Location { get; internal set; } = null;
        public CompletionType Type { get; internal set; } = CompletionType.Value;
        public object ReturnValue { get; internal set; } = null;

        public bool IsValue
        {
            get
            {
                return Type == CompletionType.Value;
            }
        }
        public bool IsException
        {
            get
            {
                return Type == CompletionType.Exception;
            }
        }
    }

运行环境的实现

我们使用Dictionary来保存运行运行环境的变量值,当前变量就保存在当前的Dictionary中,运行到一个位置,比如需要一个新的运行环境时,就生成一个新的环境,这样在这个环境中定义的变量在parent环境就无法访问,也就实现了局部变量的操作,同时当前环境可以访问前边所有parent环境的数据,也就是访问其他作用域的变量。

    public class ExecutionEnvironment
    {
        ExecutionEnvironment _parent=null;
        Dictionary<string, object> currentVariables = new Dictionary<string, object>();
        public ExecutionEnvironment()
        {
            _parent = null;
        }
        public ExecutionEnvironment(ExecutionEnvironment parent)
        {
            _parent = parent;
        }
        public bool HasValue(string variable)
        {
            if (currentVariables.ContainsKey(variable))
                return true;
            if (_parent != null)
                return _parent.HasValue(variable);
            return false;
        }
        public void RegisterValue(string variable, object value)
        {
            if (currentVariables.ContainsKey(variable))
                throw new Exception(Variable was defined already");
            currentVariables.Add(variable, value);
        }
        public object GetValue(string variable)
        {
            if (currentVariables.ContainsKey(variable))
            {
                return currentVariables[variable];
            }
            if (_parent != null)
                return _parent.GetValue(variable);
            throw new KeyNotFoundException();
        }
        public void SetValue(string variable, object value)
        {
            if (currentVariables.ContainsKey(variable))
            {
                currentVariables[variable] = value;
                return;
            }
            else if (_parent != null)
            {
                _parent.SetValue(variable, value);
                return;
            }
            else
                throw new KeyNotFoundException();
        }
    }

具体实现的例子

我们下边以if语句为例,看看具体如何实现语句的运行,if语句包含三部分,条件判断表达式Test,true时候运行的Consequent语句,false时候运行的Alternate语句,运行时候,我们先执行Test.ExecuteImpl语句,如果为true,则运行Consequent,false时候运行Alternate,不过在Consequent和Alternate运行时候,都产生了一个新的变量作用域,我们需要新建一个运行环境。


    public class IfStatement : Statement
    {
        public IfStatement()
        {
            Consequent = new BlockStatement();
        }
        public Expression Test { get; set; }
        public BlockStatement Consequent {get; set;}
        public BlockStatement Alternate {get; set;}
        protected override Completion ExecuteImpl(ExecutionEnvironment enviroment)
        {
            if (Test == null)
                return Completion.Void;
            var t = Test.Execute(enviroment);
            if (!t.Type.IsValue)
                return t;
            if (t.ReturnValue is bool)
            {
                if ((bool)t.ReturnValue)
                {
                    if (Consequent == null)
                        return Completion.Void;
                    ExecutionEnvironment current = new ExecutionEnvironment(enviroment);
                    return Consequent.Execute(current);
                }
                else
                {
                    if (Alternate == null)
                        return Completion.Void;
                    ExecutionEnvironment current = new ExecutionEnvironment(enviroment);
                    return Alternate.Execute(current);
                }
            }
            else
                return Completion.Exception("Not a boolean value", Test);
        }
    }

结语

在githup上开源了编辑器的代码,大家可以下载看看,同时这个编辑器放在了微软商店里,链接是Visual Code Editor,如果想用微软商店直接安装,可以点击下边连接,
ms-windows-store://pdp/?productid=9NG2QVSXT34H
里边除了基本的语法功能,增加了更多类库和多语言等功能,比如python turtle一样的画板、翻译、语音、数学、数据结构等

更多推荐

C#和WPF实现图形化编程