mirror of
https://gitee.com/akwkevin/aistudio.-wpf.-diagram
synced 2026-04-30 13:13:24 +08:00
Merge branch 'master' of https://gitee.com/akwkevin/aistudio.-wpf.-diagram
This commit is contained in:
457
README.md
457
README.md
@@ -2,6 +2,463 @@
|
|||||||
|
|
||||||
先上源码地址:https://gitee.com/akwkevin/aistudio.-wpf.-diagram
|
先上源码地址:https://gitee.com/akwkevin/aistudio.-wpf.-diagram
|
||||||
|
|
||||||
|
### 2023年5月1号更新内容(做一个可编程画板):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 自定义一个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#按照管脚取走数据即可。
|
||||||
|
|
||||||
|
|
||||||
### 2023年4月5号更新内容(本次更新主要仿照百度脑图):
|
### 2023年4月5号更新内容(本次更新主要仿照百度脑图):
|
||||||
1.思维导图、目录组织图、鱼骨头图、逻辑结构图、组织结构图,入口在文件新建下。
|
1.思维导图、目录组织图、鱼骨头图、逻辑结构图、组织结构图,入口在文件新建下。
|
||||||
|
|
||||||
|
|||||||
BIN
editor.png.png
Normal file
BIN
editor.png.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
Reference in New Issue
Block a user