## 用Wpf做一个可编程画板(续4-Diagram画板)

Posted WPF学习分享

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了## 用Wpf做一个可编程画板(续4-Diagram画板)相关的知识,希望对你有一定的参考价值。

先上一张效果动图。

同样老规矩,先上源码地址: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 Valueprivate get; set; 3.输出模块:public string Valueget;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#按照管脚取走数据即可。

最后选择C#做脚本编辑,对C#程序员比较省事,但是如果需要给一些非专业编程人员使用,还是用python做脚本编辑比较好,下期是不是做这个呢?

WPF 内部Template 动画板 无法冻结此 Storyboard 时间线树供跨线程使用

原文:WPF 内部Template 动画板 无法冻结此 Storyboard 时间线树供跨线程使用

解决此问题,需要一定的想象力。

换个思路即可

大体是

使用Tag或者别无用的可以输入数值的属性,或者附加属性也可以的。来绑定到你要动画的属性

当然这个过程中要使用转换器了

我给出一个关于Button 的Width的内部模板小栗子,各位朋友可以针对自己的项目/控件进行修改

 

XAML代码

技术图片
    <Window.Resources>
        <local:ToCon x:Key="To"/>
        <Style x:Key="FocusVisual">
            <Setter Property="Control.Template">
                <Setter.Value>
                    <ControlTemplate>
                        <Rectangle Margin="2" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <SolidColorBrush x:Key="Button.Static.Background" Color="#FFDDDDDD"/>
        <SolidColorBrush x:Key="Button.Static.Border" Color="#FF707070"/>
        <SolidColorBrush x:Key="Button.MouseOver.Background" Color="#FFBEE6FD"/>
        <SolidColorBrush x:Key="Button.MouseOver.Border" Color="#FF3C7FB1"/>
        <SolidColorBrush x:Key="Button.Pressed.Background" Color="#FFC4E5F6"/>
        <SolidColorBrush x:Key="Button.Pressed.Border" Color="#FF2C628B"/>
        <SolidColorBrush x:Key="Button.Disabled.Background" Color="#FFF4F4F4"/>
        <SolidColorBrush x:Key="Button.Disabled.Border" Color="#FFADB2B5"/>
        <SolidColorBrush x:Key="Button.Disabled.Foreground" Color="#FF838383"/>
        <Style x:Key="ButtonStyle1" TargetType="{x:Type Button}">
            <Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}"/>
            <Setter Property="Background" Value="{StaticResource Button.Static.Background}"/>
            <Setter Property="BorderBrush" Value="{StaticResource Button.Static.Border}"/>
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="HorizontalContentAlignment" Value="Center"/>
            <Setter Property="VerticalContentAlignment" Value="Center"/>
            <Setter Property="Padding" Value="1"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type Button}">
                        <ControlTemplate.Resources>
                            <local:ToCon  x:Key="ToConHeight"/>
                        </ControlTemplate.Resources>
                        <Border  Height="{Binding Tag,RelativeSource={RelativeSource Mode=Self},  Converter={ StaticResource ToConHeight }}" x:Name="border"  BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="true">
                            <Border.Tag>
                                <Binding Path="Tag" RelativeSource="{RelativeSource Mode=TemplatedParent}" Converter="{StaticResource ToConHeight }"/>
                            </Border.Tag>
                            <ContentPresenter x:Name="contentPresenter" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsDefaulted" Value="true">
                                <Setter Property="BorderBrush" TargetName="border" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
                            </Trigger>
                            <Trigger Property="IsMouseOver" Value="true">
                                <Trigger.EnterActions>
                                    <BeginStoryboard>
                                        <Storyboard>
                                            <DoubleAnimation  Storyboard.TargetProperty="Tag" To="200" Duration="0:0:2"/>
                                        </Storyboard>
                                    </BeginStoryboard>
                                </Trigger.EnterActions>
                            </Trigger>
                            <Trigger Property="IsPressed" Value="true">
                                <Setter Property="Background" TargetName="border" Value="{StaticResource Button.Pressed.Background}"/>
                                <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Pressed.Border}"/>
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Background" TargetName="border" Value="{StaticResource Button.Disabled.Background}"/>
                                <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Disabled.Border}"/>
                                <Setter Property="TextElement.Foreground" TargetName="contentPresenter" Value="{StaticResource Button.Disabled.Foreground}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid>
        <Button Style="{DynamicResource ButtonStyle1}" Tag="{Binding Width ,
            RelativeSource={RelativeSource Mode=Self},
            Converter={StaticResource ResourceKey=To}}" Width="100" Background="Red">

        </Button>
    </Grid>
技术图片

 

 

转换器

技术图片
    public class ToCon : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null)
                return 0;
            return  double.Parse(value.ToString());
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return value.ToString();
        }
    }
}
技术图片

 

 

截图

技术图片

以上是关于## 用Wpf做一个可编程画板(续4-Diagram画板)的主要内容,如果未能解决你的问题,请参考以下文章

用Wpf做一个思维导图(续3-Diagram画板)

WPF 利用后台C#代码实现一个平移动画。!!!

pythonGUI编程用Canvas模拟画板

WPF 内部Template 动画板 无法冻结此 Storyboard 时间线树供跨线程使用

WPF ControlTemplate 动画板 结束事件不触发

WPF 内部Template 动画板 无法冻结此 Storyboard 时间线树供跨线程使用