基于WPF制作一个可编程画板

基于wpf制作一个可编程画板

 

先上一张效果动图

同样老规矩,先上源码地址:https://gitee.com/akwkevin/aistudio.-wpf.-diagram

简单使用,自定义一个text模块的代码如下

code = @"using system;
namespace aistudio.wpf.csharpscript
{
    public class writer
    {   
        public string stringvalue{ get; set;} = ""welcome to aistudio.wpf.diagram"";
        public string execute()
        {
            return stringvalue;
        }
    }
}";

是不是很简单。

 

本次扩展的主要内容

1.可编程模块,使用c#语言。

2.控制台打印控件,可以打印程序中的console.writeline数据

3.为了便于大家使用,写了一个box工厂分配box的数据流向效果图。

可编程模块的实现原理

使用microsoft.codeanalysis.csharp.scripting对代码进行编译,生成assembly,然后对assembly反射获得对象,对象内部固定有一个execute方法,每次扫描的时候执行即可。

1.编译使用的using,必须添加引用集,为了省事,把整个程序的reference都放入进行编译,获得引用的核心代码如下:

var references = appdomain.currentdomain.getassemblies().where(p => !p.isdynamic && !string.isnullorempty(p.location)).select(x => metadatareference.createfromfile(x.location)).tolist();
//costura.fody压缩后,无location,读取资源文件中的reference
foreach (var assemblyembedded in appdomain.currentdomain.getassemblies().where(p => !p.isdynamic && string.isnullorempty(p.location)))
{
    using (var stream = assembly.getentryassembly().getmanifestresourcestream($"costura.{assemblyembedded.getname().name.tolowerinvariant()}.dll.compressed"))
    {
        if (stream != null)
        {
            using (var compressstream = new deflatestream(stream, compressionmode.decompress))
            {
                var memstream = new memorystream();
                copyto(compressstream, memstream);
                memstream.position = 0;
                references.add(metadatareference.createfromstream(memstream));
            }
        }
    }
}

2.动态编译的代码的核心代码如下:

public static assembly generateassemblyfromcode(string code, out string message)
{
    assembly assembly = null;
    message = "";
    // 丛代码中转换表达式树
    syntaxtree syntaxtree = csharpsyntaxtree.parsetext(code);
    // 随机程序集名称
    string assemblyname = path.getrandomfilename();
    // 引用
    // 创建编译对象
    csharpcompilation compilation = csharpcompilation.create(assemblyname, new[] { syntaxtree }, references, new csharpcompilationoptions(outputkind.dynamicallylinkedlibrary));
    using (var ms = new memorystream())
    {
        // 将编译好的il代码放入内存流
        emitresult result = compilation.emit(ms);
        // 编译失败,提示
        if (!result.success)
        {
            ienumerable<diagnostic> failures = result.diagnostics.where(diagnostic =>
                        diagnostic.iswarningaserror ||
                        diagnostic.severity == diagnosticseverity.error).tolist();
            foreach (diagnostic diagnostic in failures)
            {
                message += $"{diagnostic.id}: {diagnostic.getmessage()}";
                console.writeline(message);
            }
        }
        else
        {
            // 编译成功,从内存中加载编译好的程序集
            ms.seek(0, seekorigin.begin);
            assembly = assembly.load(ms.toarray());
        }
    }
    return assembly;
}

3.获得编译后的程序集,以及执行。

// 反射获取程序集中 的类
type type = assembly.gettypes().firstordefault(p => p.fullname.startswith("aistudio.wpf"));   //assembly.gettype("aistudio.wpf.csharpscript.write");
// 创建该类的实例
object obj = activator.createinstance(type);
// 通过反射方式调用类中的方法。
var result = type.invokemember("execute",
    bindingflags.default | bindingflags.invokemethod,
    null,
    obj,
    new object[] { });

代码编辑模块的实现

选择avalonedit控件,另外为了使用vs2019_dark的黑色皮肤,引用官方demo中的hl和texteditlib实现自定义换肤。

官方demo的换肤写的超级复杂,看不懂,但是我们只要理解换肤的核心部分就是动态资源字典,因此我简化下,改进后的核心换肤代码如下:

public class texteditorthemehelper
{
    static dictionary<string, resourcedictionary> themedictionary = new dictionary<string, resourcedictionary>();
    public static list<string> themes = new list<string>() { "dark", "light", "trueblue", "vs2019_dark" };
    public static string currenttheme { get; set; }
    static texteditorthemehelper()
    {
        var resource = new resourcedictionary { source = new uri("/texteditlib;component/themes/lightbrushs.xaml", urikind.relativeorabsolute) };
        themedictionary.add("light", resource);
        resource = new resourcedictionary { source = new uri("/texteditlib;component/themes/darkbrushs.xaml", urikind.relativeorabsolute) };
        themedictionary.add("dark", resource);
        application.current.resources.mergeddictionaries.add(resource);
    }
    /// <summary>
    /// 设置主题
    /// </summary>
    /// <param name="theme"></param>
    public static void setcurrenttheme(string theme)
    {
        onappthemechanged(theme);//切换到vs2019_dark
        currenttheme = theme;
    }
    /// <summary>
    /// invoke this method to apply a change of theme to the content of the document
    /// (eg: adjust the highlighting colors when changing from "dark" to "light"
    ///      with current text document loaded.)
    /// </summary>
    internal static void onappthemechanged(string theme)
    {
        themedhighlightingmanager.instance.setcurrenttheme(theme);
        if (themedictionary.containskey(theme))
        {
            foreach (var key in themedictionary[theme].keys)
            {
                applytodynamicresource(key, themedictionary[theme][key]);
            }
        }
        // does this highlighting definition have an associated highlighting theme?
        else if (themedhighlightingmanager.instance.currenttheme.hltheme != null)
        {
            // a highlighting theme with globalstyles?
            // apply these styles to the resource keys of the editor
            foreach (var item in themedhighlightingmanager.instance.currenttheme.hltheme.globalstyles)
            {
                switch (item.typename)
                {
                    case "defaultstyle":
                        applytodynamicresource(texteditlib.themes.resourcekeys.editorbackground, item.backgroundcolor);
                        applytodynamicresource(texteditlib.themes.resourcekeys.editorforeground, item.foregroundcolor);
                        break;
                    case "currentlinebackground":
                        applytodynamicresource(texteditlib.themes.resourcekeys.editorcurrentlinebackgroundbrushkey, item.backgroundcolor);
                        applytodynamicresource(texteditlib.themes.resourcekeys.editorcurrentlineborderbrushkey, item.bordercolor);
                        break;
                    case "linenumbersforeground":
                        applytodynamicresource(texteditlib.themes.resourcekeys.editorlinenumbersforeground, item.foregroundcolor);
                        break;
                    case "selection":
                        applytodynamicresource(texteditlib.themes.resourcekeys.editorselectionbrush, item.backgroundcolor);
                        applytodynamicresource(texteditlib.themes.resourcekeys.editorselectionborder, item.bordercolor);
                        break;
                    case "hyperlink":
                        applytodynamicresource(texteditlib.themes.resourcekeys.editorlinktextbackgroundbrush, item.backgroundcolor);
                        applytodynamicresource(texteditlib.themes.resourcekeys.editorlinktextforegroundbrush, item.foregroundcolor);
                        break;
                    case "nonprintablecharacter":
                        applytodynamicresource(texteditlib.themes.resourcekeys.editornonprintablecharacterbrush, item.foregroundcolor);
                        break;
                    default:
                        throw new system.argumentoutofrangeexception("globalstyle named '{0}' is not supported.", item.typename);
                }
            }
        }
    }
    /// <summary>
    /// re-define an existing <seealso cref="solidcolorbrush"/> and backup the originial color
    /// as it was before the application of the custom coloring.
    /// </summary>
    /// <param name="key"></param>
    /// <param name="newcolor"></param>
    private static void applytodynamicresource(componentresourcekey key, color? newcolor)
    {
        if (application.current.resources[key] == null || newcolor == null)
            return;
        // re-coloring works with solidcolorbrushs linked as dynamicresource
        if (application.current.resources[key] is solidcolorbrush)
        {
            //backupdynresources.add(resourcename);
            var newcolorbrush = new solidcolorbrush((color)newcolor);
            newcolorbrush.freeze();
            application.current.resources[key] = newcolorbrush;
        }
    }
    private static void applytodynamicresource(object key, object newvalue)
    {
        if (application.current.resources[key] == null || newvalue == null)
            return;
        application.current.resources[key] = newvalue;
    }
}

使用方法:

texteditorthemehelper.setcurrenttheme("vs2019_dark");

或者 texteditorthemehelper.setcurrenttheme("trueblue");

或者 texteditorthemehelper.setcurrenttheme("dark");

或者 texteditorthemehelper.setcurrenttheme("light");

是不是超级简单。

代码编辑模块的编译与测试

wpf打印控制台数据

///控制台打印方法支持切换运行输出方法console.setout,核心代码如下:
public class consolewriter : textwriter
{
    private readonly action<string> _write;
    private readonly action<string> _writeline;
    private readonly action<string, string, string, int> _writecallerinfo;
    public consolewriter()
    {
    }
    /// <summary>
    /// console 输出重定向
    /// </summary>
    /// <param name="write">日志方法委托(针对于 write)</param>
    /// <param name="writeline">日志方法委托(针对于 writeline)</param>
    public consolewriter(action<string> write, action<string> writeline, action<string, string, string, int> writecallerinfo)
    {
        _write = write;
        _writeline = writeline?? write;
        _writecallerinfo = writecallerinfo;
    }
    /// <summary>
    /// console 输出重定向
    /// </summary>
    /// <param name="write">日志方法委托(针对于 write)</param>
    /// <param name="writeline">日志方法委托(针对于 writeline)</param>
    public consolewriter(action<string> write, action<string> writeline)
    {
        _write = write;
        _writeline = writeline;
    }
    /// <summary>
    /// console 输出重定向
    /// </summary>
    /// <param name="write">日志方法委托</param>
    public consolewriter(action<string> write)
    {
        _write = write;
        _writeline = write;
    }
    /// <summary>
    /// console 输出重定向(带调用方信息)
    /// </summary>
    /// <param name="write">日志方法委托(后三个参数为 callerfilepath、callermembername、callerlinenumber)</param>
    public consolewriter(action<string, string, string, int> write)
    {
        _writecallerinfo = write;
    }
    /// <summary>
    /// 使用 utf-16 避免不必要的编码转换
    /// </summary>
    public override encoding encoding => encoding.unicode;
    /// <summary>
    /// 最低限度需要重写的方法
    /// </summary>
    /// <param name="value">消息</param>
    public override void write(string value)
    {
        if (_writecallerinfo != null)
        {
            writewithcallerinfo(value);
            return;
        }
        _write(value);
    }
    /// <summary>
    /// 为提高效率直接处理一行的输出
    /// </summary>
    /// <param name="value">消息</param>
    public override void writeline(string value)
    {
        if (_writecallerinfo != null)
        {
            writewithcallerinfo(value);
            return;
        }
        _writeline(value);
    }
    /// <summary>
    /// 带调用方信息进行写消息
    /// </summary>
    /// <param name="value">消息</param>
    private void writewithcallerinfo(string value)
    {
        //3、system.console.writeline -> 2、system.io.textwriter + synctextwriter.writeline -> 1、dotnet.utilities.consolehelper.consolewriter.writeline -> 0、dotnet.utilities.consolehelper.consolewriter.writewithcallerinfo
        var callinfo = classhelper.getmethodinfo(4);
        _writecallerinfo(value, callinfo?.filename, callinfo?.methodname, callinfo?.linenumber ?? 0);
    }
    public override void close()
    {
        var standardoutput = new streamwriter(console.openstandardoutput());
        standardoutput.autoflush = true;
        console.setout(standardoutput);
        base.close();
    }
}

使用:

consolewriter consolewriter = new consolewriter(_write, _writeline);

console.setout(consolewriter);

动态编译模块的输入输出自动生成

1.输入输出模块:public string value{ get; set;}

2.输入模块:public string value{private get; set;}

3.输出模块:public string value{get;private set;}

4.与外部交互模块:private string value{ get; set;} ,必须同名同属性。 核心代码如下:

public static dictionary<string, list<propertyinfo>> getpropertyinfo(type type)
{
    dictionary<string, list<propertyinfo>> puts = new dictionary<string, list<propertyinfo>>()
    {
        {"input", new list<propertyinfo>() },
        {"output", new list<propertyinfo>() },
        {"input_output", new list<propertyinfo>() },
        {"inner", new list<propertyinfo>() }
    };
    try
    {
        foreach (system.reflection.propertyinfo info in type.getproperties(bindingflags.public | bindingflags.instance))
        {
            if (info.canread && info.canwrite)
            {
                if (info.setmethod.ispublic && info.getmethod.ispublic)
                {
                    puts["input_output"].add(info);
                }
                else if (info.setmethod.ispublic)
                {
                    puts["input"].add(info);
                }
                else if (info.getmethod.ispublic)
                {
                    puts["output"].add(info);
                }
            }
            else if (info.canread)
            {
                if (info.getmethod.ispublic)
                {
                    puts["output"].add(info);
                }
            }
        }
        foreach (system.reflection.propertyinfo info in type.getproperties(bindingflags.nonpublic | bindingflags.instance))
        {
            if (info.canread)
            {
                puts["inner"].add(info);
            }
        }
    }
    catch (exception ex)
    {
    }
    return puts;
}

最后介绍一下demo的实现

1#.int整数模块,界面定义一个textbox绑定int模块的输入管脚。 2#.box产生模块,如果内部数组为空,那么按照输入管脚的数量初始化一个容量为输入整数数量的数组(随机颜色与形状),然后把数据放到输出管脚,当数据被取走后,下一个数据再次放到输出管脚。 3#.bool模块,为false的时候按照颜色进行分配,为true的时候按照形状进行分配。4#.box分配模块,当输入管脚为空的时候,2#模块的输出可以移动到4#的输入管脚,移动时间为1s,移动完成后,清除2#模块的输出。同时把数据按照颜色或者形状分配到输出,同时把输入管脚清除。 按照颜色分配时: (1.如果颜色为红色,那么输出到1号 (2.如果颜色为橙色,那么输出到2号 (3.如果颜色为黄色,那么输出到3号 (4.如果颜色为绿色,那么输出到4号 (5.如果颜色为青色,那么输出到5号 (6.如果颜色为蓝色,那么输出到6号 (7.如果颜色为紫色,那么输出到7号 按照形状分配时: (1.如果形状为圆形,那么输出到1号 (2.如果形状为三角形,那么输出到2号 (3.如果形状为方形,那么输出到3号 (4.如果形状为菱形,那么输出到4号 (5.如果形状为梯形,那么输出到5号 (6.如果形状为五角星,那么输出到6号 (7.如果形状为六边形,那么输出到7号 6#.有两个红色|圆形收集器(7#,8#),按两个容器中的数量比较反馈,均匀分配到这两个收集器中。 9#,10#,11#,12#,13#,14#按照管脚取走数据即可。

以上就是基于wpf制作一个可编程画板的详细内容,更多关于wpf可编程画板的资料请关注硕编程其它相关文章!

下一节:c#中@字符d是个什么意思

c# 教程

相关文章