diff --git a/55.gif b/55.gif new file mode 100644 index 0000000..72da0d4 Binary files /dev/null and b/55.gif differ diff --git a/README.md b/README.md index fd3855e..b3ecc54 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,457 @@ 先上源码地址:https://gitee.com/akwkevin/aistudio.-wpf.-diagram +### 2023年5月1号更新内容(做一个可编程画板): + +![输入图片说明](55.gif) + +## 自定义一个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 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实现自定义换肤。 +![](https://files.mdnice.com/user/17967/7eb5d6d0-ad52-47b0-a895-96c0a0f2f9b9.png) +官方Demo的换肤写的超级复杂,看不懂,但是我们只要理解换肤的核心部分就是动态资源字典,因此我简化下,改进后的核心换肤代码如下: + +``` +public class TextEditorThemeHelper +{ + static Dictionary ThemeDictionary = new Dictionary(); + + public static List Themes = new List() { "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); + } + + /// + /// 设置主题 + /// + /// + public static void SetCurrentTheme(string theme) + { + OnAppThemeChanged(theme);//切换到VS2019_Dark + CurrentTheme = theme; + } + + /// + /// 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.) + /// + 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); + } + } + } + + } + + /// + /// Re-define an existing and backup the originial color + /// as it was before the application of the custom coloring. + /// + /// + /// + 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 _Write; + private readonly Action _WriteLine; + private readonly Action _WriteCallerInfo; + + public ConsoleWriter() + { + + } + + /// + /// Console 输出重定向 + /// + /// 日志方法委托(针对于 Write) + /// 日志方法委托(针对于 WriteLine) + public ConsoleWriter(Action write, Action writeLine, Action writeCallerInfo) + { + _Write = write; + _WriteLine = writeLine?? write; + _WriteCallerInfo = writeCallerInfo; + } + + /// + /// Console 输出重定向 + /// + /// 日志方法委托(针对于 Write) + /// 日志方法委托(针对于 WriteLine) + public ConsoleWriter(Action write, Action writeLine) + { + _Write = write; + _WriteLine = writeLine; + } + + /// + /// Console 输出重定向 + /// + /// 日志方法委托 + public ConsoleWriter(Action write) + { + _Write = write; + _WriteLine = write; + } + + /// + /// Console 输出重定向(带调用方信息) + /// + /// 日志方法委托(后三个参数为 CallerFilePath、CallerMemberName、CallerLineNumber) + public ConsoleWriter(Action write) + { + _WriteCallerInfo = write; + } + + /// + /// 使用 UTF-16 避免不必要的编码转换 + /// + public override Encoding Encoding => Encoding.Unicode; + + /// + /// 最低限度需要重写的方法 + /// + /// 消息 + public override void Write(string value) + { + if (_WriteCallerInfo != null) + { + WriteWithCallerInfo(value); + return; + } + + _Write(value); + } + + /// + /// 为提高效率直接处理一行的输出 + /// + /// 消息 + public override void WriteLine(string value) + { + if (_WriteCallerInfo != null) + { + WriteWithCallerInfo(value); + return; + } + + _WriteLine(value); + } + + /// + /// 带调用方信息进行写消息 + /// + /// 消息 + 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> GetPropertyInfo(Type type) +{ + Dictionary> puts = new Dictionary>() + { + {"Input", new List() }, + {"Output", new List() }, + {"Input_Output", new List() }, + {"Inner", new List() } + }; + + 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#按照管脚取走数据即可。 + + ### 2023年4月5号更新内容(本次更新主要仿照百度脑图): 1.思维导图、目录组织图、鱼骨头图、逻辑结构图、组织结构图,入口在文件新建下。