From 652707f98048f41bd6b6b27dd9928a55493c8139 Mon Sep 17 00:00:00 2001 From: fengjiayi <12821976+ning_xi@user.noreply.gitee.com> Date: Sun, 5 Jan 2025 08:52:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=81=A2=E5=A4=8DWPF=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WorkBench/Node/View/ActionNodeControl.xaml.cs | 3 +- Workbench/App.xaml | 22 + Workbench/App.xaml.cs | 75 + Workbench/AssemblyInfo.cs | 10 + Workbench/Extension/LineExtension.cs | 53 + Workbench/Extension/MyExtension.cs | 42 + Workbench/LogWindow.xaml | 24 + Workbench/LogWindow.xaml.cs | 162 + Workbench/MainWindow.xaml | 259 ++ Workbench/MainWindow.xaml.cs | 3027 +++++++++++++++++ Workbench/MainWindowViewModel.cs | 101 + Workbench/Node/INodeContainerControl.cs | 33 + Workbench/Node/INodeJunction.cs | 52 + .../Node/Junction/ConnectionLineShape.cs | 234 ++ .../Node/Junction/JunctionControlBase.cs | 378 ++ Workbench/Node/Junction/JunctionData.cs | 161 + .../Node/Junction/NodeJunctionViewBase.cs | 237 ++ .../Node/Junction/View/ArgJunctionControl.cs | 74 + .../Junction/View/ExecuteJunctionControl.cs | 84 + .../Junction/View/NextStepJunctionControl.cs | 60 + .../Junction/View/ResultJunctionControl.cs | 61 + Workbench/Node/NodeControlBase.cs | 198 ++ Workbench/Node/NodeControlViewModelBase.cs | 48 + Workbench/Node/RelayCommand.cs | 26 + Workbench/Node/View/ActionNodeControl.xaml | 118 + Workbench/Node/View/ConditionNodeControl.xaml | 110 + .../Node/View/ConditionNodeControl.xaml.cs | 58 + .../Node/View/ConditionRegionControl.xaml | 22 + .../Node/View/ConditionRegionControl.xaml.cs | 94 + Workbench/Node/View/ConnectionControl.cs | 297 ++ Workbench/Node/View/DllControlControl.xaml | 34 + Workbench/Node/View/DllControlControl.xaml.cs | 164 + Workbench/Node/View/ExpOpNodeControl.xaml | 47 + Workbench/Node/View/ExpOpNodeControl.xaml.cs | 56 + Workbench/Node/View/FlipflopNodeControl.xaml | 110 + .../Node/View/FlipflopNodeControl.xaml.cs | 72 + Workbench/Node/View/GlobalDataControl.xaml | 87 + Workbench/Node/View/GlobalDataControl.xaml.cs | 87 + Workbench/Node/View/ScriptNodeControl.xaml | 93 + Workbench/Node/View/ScriptNodeControl.xaml.cs | 162 + .../ViewModel/ActionNodeControlViewModel.cs | 13 + .../ConditionNodeControlViewModel.cs | 54 + .../ConditionRegionNodeControlViewModel.cs | 18 + .../ViewModel/ExpOpNodeControlViewModel.cs | 26 + .../ViewModel/FlipflopNodeControlViewModel.cs | 14 + .../GlobalDataNodeControlViewModel.cs | 51 + .../ViewModel/ScriptNodeControlViewModel.cs | 62 + .../Node/ViewModel/TypeToStringConverter.cs | 27 + Workbench/Properties/launchSettings.json | 7 + Workbench/Serein.WorkBench.csproj | 74 + .../Serein.WorkBench_d2hd4tgu_wpftmp.csproj | 288 ++ .../Serein.Workbench_wjzi1sgn_wpftmp.csproj | 292 ++ Workbench/Themes/BindableRichTextBox.cs | 28 + Workbench/Themes/IOCObjectViewControl.xaml | 28 + Workbench/Themes/IOCObjectViewControl.xaml.cs | 128 + Workbench/Themes/InputDialog.xaml | 16 + Workbench/Themes/InputDialog.xaml.cs | 42 + Workbench/Themes/MethodDetailsControl.xaml | 129 + Workbench/Themes/MethodDetailsControl.xaml.cs | 90 + Workbench/Themes/NodeTreeItemViewControl.xaml | 59 + .../Themes/NodeTreeItemViewControl.xaml.cs | 280 ++ Workbench/Themes/NodeTreeViewControl.xaml | 47 + Workbench/Themes/NodeTreeViewControl.xaml.cs | 85 + Workbench/Themes/ObjectViewerControl.xaml | 31 + Workbench/Themes/ObjectViewerControl.xaml.cs | 670 ++++ Workbench/Themes/TypeViewerWindow.xaml | 16 + Workbench/Themes/TypeViewerWindow.xaml.cs | 279 ++ Workbench/Themes/WindowDialogInput.xaml | 30 + Workbench/Themes/WindowDialogInput.xaml.cs | 70 + .../InvertableBooleanToVisibilityConverter.cs | 41 + .../Tool/Converters/ThumbPositionConverter.cs | 79 + .../Tool/Converters/TypeToColorConverter.cs | 26 + Workbench/Tool/GuidReplacer.cs | 68 + 73 files changed, 10202 insertions(+), 1 deletion(-) create mode 100644 Workbench/App.xaml create mode 100644 Workbench/App.xaml.cs create mode 100644 Workbench/AssemblyInfo.cs create mode 100644 Workbench/Extension/LineExtension.cs create mode 100644 Workbench/Extension/MyExtension.cs create mode 100644 Workbench/LogWindow.xaml create mode 100644 Workbench/LogWindow.xaml.cs create mode 100644 Workbench/MainWindow.xaml create mode 100644 Workbench/MainWindow.xaml.cs create mode 100644 Workbench/MainWindowViewModel.cs create mode 100644 Workbench/Node/INodeContainerControl.cs create mode 100644 Workbench/Node/INodeJunction.cs create mode 100644 Workbench/Node/Junction/ConnectionLineShape.cs create mode 100644 Workbench/Node/Junction/JunctionControlBase.cs create mode 100644 Workbench/Node/Junction/JunctionData.cs create mode 100644 Workbench/Node/Junction/NodeJunctionViewBase.cs create mode 100644 Workbench/Node/Junction/View/ArgJunctionControl.cs create mode 100644 Workbench/Node/Junction/View/ExecuteJunctionControl.cs create mode 100644 Workbench/Node/Junction/View/NextStepJunctionControl.cs create mode 100644 Workbench/Node/Junction/View/ResultJunctionControl.cs create mode 100644 Workbench/Node/NodeControlBase.cs create mode 100644 Workbench/Node/NodeControlViewModelBase.cs create mode 100644 Workbench/Node/RelayCommand.cs create mode 100644 Workbench/Node/View/ActionNodeControl.xaml create mode 100644 Workbench/Node/View/ConditionNodeControl.xaml create mode 100644 Workbench/Node/View/ConditionNodeControl.xaml.cs create mode 100644 Workbench/Node/View/ConditionRegionControl.xaml create mode 100644 Workbench/Node/View/ConditionRegionControl.xaml.cs create mode 100644 Workbench/Node/View/ConnectionControl.cs create mode 100644 Workbench/Node/View/DllControlControl.xaml create mode 100644 Workbench/Node/View/DllControlControl.xaml.cs create mode 100644 Workbench/Node/View/ExpOpNodeControl.xaml create mode 100644 Workbench/Node/View/ExpOpNodeControl.xaml.cs create mode 100644 Workbench/Node/View/FlipflopNodeControl.xaml create mode 100644 Workbench/Node/View/FlipflopNodeControl.xaml.cs create mode 100644 Workbench/Node/View/GlobalDataControl.xaml create mode 100644 Workbench/Node/View/GlobalDataControl.xaml.cs create mode 100644 Workbench/Node/View/ScriptNodeControl.xaml create mode 100644 Workbench/Node/View/ScriptNodeControl.xaml.cs create mode 100644 Workbench/Node/ViewModel/ActionNodeControlViewModel.cs create mode 100644 Workbench/Node/ViewModel/ConditionNodeControlViewModel.cs create mode 100644 Workbench/Node/ViewModel/ConditionRegionNodeControlViewModel.cs create mode 100644 Workbench/Node/ViewModel/ExpOpNodeControlViewModel.cs create mode 100644 Workbench/Node/ViewModel/FlipflopNodeControlViewModel.cs create mode 100644 Workbench/Node/ViewModel/GlobalDataNodeControlViewModel.cs create mode 100644 Workbench/Node/ViewModel/ScriptNodeControlViewModel.cs create mode 100644 Workbench/Node/ViewModel/TypeToStringConverter.cs create mode 100644 Workbench/Properties/launchSettings.json create mode 100644 Workbench/Serein.WorkBench.csproj create mode 100644 Workbench/Serein.WorkBench_d2hd4tgu_wpftmp.csproj create mode 100644 Workbench/Serein.Workbench_wjzi1sgn_wpftmp.csproj create mode 100644 Workbench/Themes/BindableRichTextBox.cs create mode 100644 Workbench/Themes/IOCObjectViewControl.xaml create mode 100644 Workbench/Themes/IOCObjectViewControl.xaml.cs create mode 100644 Workbench/Themes/InputDialog.xaml create mode 100644 Workbench/Themes/InputDialog.xaml.cs create mode 100644 Workbench/Themes/MethodDetailsControl.xaml create mode 100644 Workbench/Themes/MethodDetailsControl.xaml.cs create mode 100644 Workbench/Themes/NodeTreeItemViewControl.xaml create mode 100644 Workbench/Themes/NodeTreeItemViewControl.xaml.cs create mode 100644 Workbench/Themes/NodeTreeViewControl.xaml create mode 100644 Workbench/Themes/NodeTreeViewControl.xaml.cs create mode 100644 Workbench/Themes/ObjectViewerControl.xaml create mode 100644 Workbench/Themes/ObjectViewerControl.xaml.cs create mode 100644 Workbench/Themes/TypeViewerWindow.xaml create mode 100644 Workbench/Themes/TypeViewerWindow.xaml.cs create mode 100644 Workbench/Themes/WindowDialogInput.xaml create mode 100644 Workbench/Themes/WindowDialogInput.xaml.cs create mode 100644 Workbench/Tool/Converters/InvertableBooleanToVisibilityConverter.cs create mode 100644 Workbench/Tool/Converters/ThumbPositionConverter.cs create mode 100644 Workbench/Tool/Converters/TypeToColorConverter.cs create mode 100644 Workbench/Tool/GuidReplacer.cs diff --git a/WorkBench/Node/View/ActionNodeControl.xaml.cs b/WorkBench/Node/View/ActionNodeControl.xaml.cs index a35d624..f26e7ba 100644 --- a/WorkBench/Node/View/ActionNodeControl.xaml.cs +++ b/WorkBench/Node/View/ActionNodeControl.xaml.cs @@ -18,7 +18,8 @@ namespace Serein.Workbench.Node.View InitializeComponent(); if(ExecuteJunctionControl.MyNode != null) { - ExecuteJunctionControl.MyNode.Guid = viewModel.NodeModel.Guid; + + ExecuteJunctionControl.MyNode.Guid = viewModel.NodeModel.Guid; } } diff --git a/Workbench/App.xaml b/Workbench/App.xaml new file mode 100644 index 0000000..baa41de --- /dev/null +++ b/Workbench/App.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/Workbench/App.xaml.cs b/Workbench/App.xaml.cs new file mode 100644 index 0000000..a90c015 --- /dev/null +++ b/Workbench/App.xaml.cs @@ -0,0 +1,75 @@ +using Newtonsoft.Json; +using Serein.Library; +using Serein.Library.Utils; +using System.Diagnostics; +using System.IO; +using System.Windows; + +namespace Serein.Workbench +{ + + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + private async Task LoadLocalProjectAsync() + { + +#if DEBUG + if (1 == 1) + { + // 这里是测试代码,可以删除 + string filePath; + filePath = @"C:\Users\Az\source\repos\CLBanyunqiState\CLBanyunqiState\bin\Release\net8.0\PLCproject.dnf"; + filePath = @"C:\Users\Az\source\repos\CLBanyunqiState\CLBanyunqiState\bin\Release\banyunqi\project.dnf"; + filePath = @"C:\Users\Az\source\repos\CLBanyunqiState\CLBanyunqiState\bin\debug\net8.0\project.dnf"; + //filePath = @"C:\Users\Az\source\repos\CLBanyunqiState\CLBanyunqiState\bin\debug\net8.0\test.dnf"; + string content = System.IO.File.ReadAllText(filePath); // 读取整个文件内容 + App.FlowProjectData = JsonConvert.DeserializeObject(content); + App.FileDataPath = System.IO.Path.GetDirectoryName(filePath)!; // filePath;// + var dir = Path.GetDirectoryName(filePath); + } +#endif + } + + public static SereinProjectData? FlowProjectData { get; set; } + public static string FileDataPath { get; set; } = ""; + + private async void Application_Startup(object sender, StartupEventArgs e) + { + // 检查是否传入了参数 + if (e.Args.Length == 1) + { + // 获取文件路径 + string filePath = e.Args[0]; + // 检查文件是否存在 + if (!System.IO.File.Exists(filePath)) + { + MessageBox.Show($"文件未找到:{filePath}"); + Shutdown(); // 关闭应用程序 + return; + } + + try + { + // 读取文件内容 + string content = System.IO.File.ReadAllText(filePath); // 读取整个文件内容 + FlowProjectData = JsonConvert.DeserializeObject(content); + FileDataPath = System.IO.Path.GetDirectoryName(filePath) ?? ""; + } + catch (Exception ex) + { + MessageBox.Show($"读取文件时发生错误:{ex.Message}"); + Shutdown(); // 关闭应用程序 + } + + } + await this.LoadLocalProjectAsync(); + + + } + } + +} + diff --git a/Workbench/AssemblyInfo.cs b/Workbench/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/Workbench/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/Workbench/Extension/LineExtension.cs b/Workbench/Extension/LineExtension.cs new file mode 100644 index 0000000..ce0eb42 --- /dev/null +++ b/Workbench/Extension/LineExtension.cs @@ -0,0 +1,53 @@ +using Serein.Library; +using Serein.Workbench.Node.View; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media; + +namespace Serein.Workbench.Extension +{ + /// + /// 线条颜色 + /// + public static class LineExtension + { + /// + /// 根据连接类型指定颜色 + /// + /// + /// + /// + public static SolidColorBrush ToLineColor(this ConnectionInvokeType currentConnectionType) + { + return currentConnectionType switch + { + ConnectionInvokeType.IsSucceed => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#04FC10")), // 04FC10 & 027E08 + ConnectionInvokeType.IsFail => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F18905")), + ConnectionInvokeType.IsError => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FE1343")), + ConnectionInvokeType.Upstream => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4A82E4")), + ConnectionInvokeType.None => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#56CEF6")), + _ => throw new Exception(), + }; + } + /// + /// 根据连接类型指定颜色 + /// + /// + /// + /// + public static SolidColorBrush ToLineColor(this ConnectionArgSourceType connection) + { + return connection switch + { + ConnectionArgSourceType.GetPreviousNodeData => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#56CEF6")), // 04FC10 & 027E08 + ConnectionArgSourceType.GetOtherNodeData => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#56CEF6")), + ConnectionArgSourceType.GetOtherNodeDataOfInvoke => new SolidColorBrush((Color)ColorConverter.ConvertFromString("#B06BBB")), + _ => throw new Exception(), + }; + } + + } +} diff --git a/Workbench/Extension/MyExtension.cs b/Workbench/Extension/MyExtension.cs new file mode 100644 index 0000000..0bacb12 --- /dev/null +++ b/Workbench/Extension/MyExtension.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace Serein.Workbench.Extension +{ + public static class PointExtension + { + public static Point Add(this Point a, Point b) + { + return new Point(a.X + b.X, a.Y + b.Y); + } + + public static Point Sub(this Point a, Point b) + { + return new Point(a.X - b.X, a.Y - b.Y); + } + + public static Vector ToVector(this Point me) + { + return new Vector(me.X, me.Y); + } + } + public static class VectorExtension + { + public static double DotProduct(this Vector a, Vector b) + { + return a.X * b.X + a.Y * b.Y; + } + + public static Vector NormalizeTo(this Vector v) + { + var temp = v; + temp.Normalize(); + + return temp; + } + } +} diff --git a/Workbench/LogWindow.xaml b/Workbench/LogWindow.xaml new file mode 100644 index 0000000..29bc07e --- /dev/null +++ b/Workbench/LogWindow.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/Workbench/LogWindow.xaml.cs b/Workbench/LogWindow.xaml.cs new file mode 100644 index 0000000..495a979 --- /dev/null +++ b/Workbench/LogWindow.xaml.cs @@ -0,0 +1,162 @@ +using System.Windows; + +namespace Serein.Workbench +{ + /// + /// DebugWindow.xaml 的交互逻辑 + /// + using System; + using System.IO; + using System.Text; + using System.Threading.Tasks; + using System.Timers; + using System.Windows; + + /// + /// LogWindow.xaml 的交互逻辑 + /// + public partial class LogWindow : Window + { + private StringBuilder logBuffer = new StringBuilder(); + private int logUpdateInterval = 200; // 批量更新的时间间隔(毫秒) + private Timer logUpdateTimer; + private const int MaxLines = 1000; // 最大显示的行数 + private bool autoScroll = true; // 自动滚动标识 + private int flushThreshold = 5; // 设置日志刷新阈值 + private const int maxFlushSize = 1000; // 每次最大刷新字符数 + + public LogWindow() + { + InitializeComponent(); + + // 初始化定时器,用于批量更新日志 + logUpdateTimer = new Timer(logUpdateInterval); + logUpdateTimer.Elapsed += (s, e) => FlushLog(); // 定时刷新日志 + logUpdateTimer.Start(); + + // 添加滚动事件处理,判断用户是否手动滚动 + // LogTextBox.ScrollChanged += LogTextBox_ScrollChanged; + } + + /// + /// 添加日志到缓冲区 + /// + public void AppendText(string text) + { + lock (logBuffer) + { + logBuffer.Append(text); + + // 异步写入日志到文件 + // Task.Run(() => File.AppendAllText("log.txt", text)); + //FlushLog(); + // 如果日志达到阈值,立即刷新 + if (logBuffer.Length > flushThreshold) + { + FlushLog(); + } + } + } + + /// + /// 清空日志缓冲区并更新到 TextBox 中 + /// + private void FlushLog() + { + if (logBuffer.Length == 0) return; + + Dispatcher.InvokeAsync(() => + { + lock (logBuffer) + { + // 仅追加部分日志,避免一次更新过多内容 + string logContent = logBuffer.Length > maxFlushSize + ? logBuffer.ToString(0, maxFlushSize) + : logBuffer.ToString(); + logBuffer.Remove(0, logContent.Length); // 清空已更新的部分 + + LogTextBox.Dispatcher.Invoke(() => + { + LogTextBox.AppendText(logContent); + }); + + } + + // 不必每次都修剪日志,当行数超过限制20%时再修剪 + if (LogTextBox.LineCount > MaxLines * 1.2) + { + TrimLog(); + } + + ScrollToEndIfNeeded(); // 根据是否需要自动滚动来决定 + }, System.Windows.Threading.DispatcherPriority.Background); + } + + /// + /// 限制日志输出的最大行数,超出时删除旧日志 + /// + private void TrimLog() + { + if (LogTextBox.LineCount > MaxLines) + { + // 删除最早的多余行 + LogTextBox.Text = LogTextBox.Text.Substring( + LogTextBox.GetCharacterIndexFromLineIndex(LogTextBox.LineCount - MaxLines)); + } + } + + /// + /// 检测用户是否手动滚动了文本框 + /// + private void LogTextBox_ScrollChanged(object sender, System.Windows.Controls.ScrollChangedEventArgs e) + { + if (e.ExtentHeightChange == 0) // 用户手动滚动时 + { + // 判断是否滚动到底部 + //autoScroll = LogTextBox.VerticalOffset == LogTextBox.ScrollableHeight; + } + } + + /// + /// 根据 autoScroll 标志决定是否滚动到末尾 + /// + private void ScrollToEndIfNeeded() + { + if (autoScroll) + { + LogTextBox.ScrollToEnd(); // 仅在需要时滚动到末尾 + } + } + + /// + /// 清空日志 + /// + public void Clear() + { + Dispatcher.BeginInvoke(() => + { + LogTextBox.Clear(); + }); + } + + /// + /// 点击清空日志按钮时触发 + /// + private void ClearLog_Click(object sender, RoutedEventArgs e) + { + LogTextBox.Clear(); + } + + /// + /// 窗口关闭事件,隐藏窗体而不是关闭 + /// + private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) + { + logBuffer?.Clear(); + Clear(); + e.Cancel = true; // 取消关闭操作 + this.Hide(); // 隐藏窗体而不是关闭 + } + } + +} diff --git a/Workbench/MainWindow.xaml b/Workbench/MainWindow.xaml new file mode 100644 index 0000000..87cef37 --- /dev/null +++ b/Workbench/MainWindow.xaml @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Workbench/MainWindow.xaml.cs b/Workbench/MainWindow.xaml.cs new file mode 100644 index 0000000..f8c3a5a --- /dev/null +++ b/Workbench/MainWindow.xaml.cs @@ -0,0 +1,3027 @@ +using Microsoft.Win32; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Serein.Library; +using Serein.Library.Api; +using Serein.Library.Utils; +using Serein.NodeFlow; +using Serein.NodeFlow.Tool; +using Serein.Workbench.Extension; +using Serein.Workbench.Node; +using Serein.Workbench.Node.View; +using Serein.Workbench.Node.ViewModel; +using Serein.Workbench.Themes; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Animation; +using DataObject = System.Windows.DataObject; + +namespace Serein.Workbench +{ + /// + /// 拖拽创建节点类型 + /// + public static class MouseNodeType + { + /// + /// 创建来自DLL的节点 + /// + public static string CreateDllNodeInCanvas { get; } = nameof(CreateDllNodeInCanvas); + /// + /// 创建基础节点 + /// + public static string CreateBaseNodeInCanvas { get; } = nameof(CreateBaseNodeInCanvas); + } + + + + /// + /// Interaction logic for MainWindow.xaml,第一次用git,不太懂 + /// + public partial class MainWindow : Window + { + /// + /// 全局捕获Console输出事件,打印在这个窗体里面 + /// + private readonly LogWindow LogOutWindow = new LogWindow(); + + /// + /// 流程环境装饰器,方便在本地与远程环境下切换 + /// + private IFlowEnvironment EnvDecorator => ViewModel.FlowEnvironment; + private IFlowEnvironmentEvent EnvEventDecorator => ViewModel.FlowEnvironment as IFlowEnvironmentEvent; + private MainWindowViewModel ViewModel { get; set; } + + + /// + /// 节点对应的控件类型 + /// + // private Dictionary NodeUITypes { get; } = []; + + /// + /// 存储所有与节点有关的控件 + /// 任何情景下都应避免直接操作 ViewModel 中的 NodeModel 节点, + /// 而是应该调用 FlowEnvironment 提供接口进行操作, + /// 因为 Workbench 应该更加关注UI视觉效果,而非直接干扰流程环境运行的逻辑。 + /// 之所以暴露 NodeModel 属性,因为有些场景下不可避免的需要直接获取节点的属性。 + /// + private Dictionary NodeControls { get; } = []; + + /// + /// 存储所有的连接。考虑集成在运行环境中。 + /// + private List Connections { get; } = []; + + /// + /// 起始节点 + /// + //private NodeControlBase StartNodeControl{ get; set; } + + #region 与画布相关的字段 + + /// + /// 标记是否正在尝试选取控件 + /// + private bool IsSelectControl; + /// + /// 标记是否正在进行连接操作 + /// + //private bool IsConnecting; + /// + /// 标记是否正在拖动控件 + /// + private bool IsControlDragging; + /// + /// 标记是否正在拖动画布 + /// + private bool IsCanvasDragging; + private bool IsSelectDragging; + + /// + /// 当前选取的控件 + /// + private readonly List selectNodeControls = []; + + /// + /// 记录开始拖动节点控件时的鼠标位置 + /// + private Point startControlDragPoint; + /// + /// 记录移动画布开始时的鼠标位置 + /// + private Point startCanvasDragPoint; + /// + /// 记录开始选取节点控件时的鼠标位置 + /// + private Point startSelectControolPoint; + + + /// + /// 记录开始连接的文本块 + /// + //private NodeControlBase? startConnectNodeControl; + /// + /// 当前正在绘制的连接线 + /// + //private Line? currentLine; + /// + /// 当前正在绘制的真假分支属性 + /// + //private ConnectionInvokeType currentConnectionType; + + + /// + /// 组合变换容器 + /// + private readonly TransformGroup canvasTransformGroup; + /// + /// 缩放画布 + /// + private readonly ScaleTransform scaleTransform; + /// + /// 平移画布 + /// + private readonly TranslateTransform translateTransform; + #endregion + + + public MainWindow() + { + ViewModel = new MainWindowViewModel(this); + this.DataContext = ViewModel; + InitializeComponent(); + + ViewObjectViewer.FlowEnvironment = EnvDecorator; // 设置 节点树视图 的环境为装饰器 + IOCObjectViewer.FlowEnvironment = EnvDecorator; // 设置 IOC容器视图 的环境为装饰器 + IOCObjectViewer.SelectObj += ViewObjectViewer.LoadObjectInformation; // 使选择 IOC容器视图 的某项(对象)时,可以在 数据视图 呈现数据 + + #region 为 NodeControlType 枚举 不同项添加对应的 Control类型 、 ViewModel类型 + NodeMVVMManagement.RegisterUI(NodeControlType.Action, typeof(ActionNodeControl), typeof(ActionNodeControlViewModel)); + NodeMVVMManagement.RegisterUI(NodeControlType.Flipflop, typeof(FlipflopNodeControl), typeof(FlipflopNodeControlViewModel)); + NodeMVVMManagement.RegisterUI(NodeControlType.ExpOp, typeof(ExpOpNodeControl), typeof(ExpOpNodeControlViewModel)); + NodeMVVMManagement.RegisterUI(NodeControlType.ExpCondition, typeof(ConditionNodeControl), typeof(ConditionNodeControlViewModel)); + NodeMVVMManagement.RegisterUI(NodeControlType.ConditionRegion, typeof(ConditionRegionControl), typeof(ConditionRegionNodeControlViewModel)); + NodeMVVMManagement.RegisterUI(NodeControlType.GlobalData, typeof(GlobalDataControl), typeof(GlobalDataNodeControlViewModel)); + NodeMVVMManagement.RegisterUI(NodeControlType.Script, typeof(ScriptNodeControl), typeof(ScriptNodeControlViewModel)); + #endregion + + + #region 缩放平移容器 + canvasTransformGroup = new TransformGroup(); + scaleTransform = new ScaleTransform(); + translateTransform = new TranslateTransform(); + canvasTransformGroup.Children.Add(scaleTransform); + canvasTransformGroup.Children.Add(translateTransform); + FlowChartCanvas.RenderTransform = canvasTransformGroup; + #endregion + + InitFlowEnvironmentEvent(); // 配置环境事件 + + + } + + + + /// + /// 初始化环境事件 + /// + private void InitFlowEnvironmentEvent() + { + EnvEventDecorator.OnDllLoad += FlowEnvironment_DllLoadEvent; + EnvEventDecorator.OnProjectSaving += EnvDecorator_OnProjectSaving; + EnvEventDecorator.OnProjectLoaded += FlowEnvironment_OnProjectLoaded; + EnvEventDecorator.OnStartNodeChange += FlowEnvironment_StartNodeChangeEvent; + EnvEventDecorator.OnNodeConnectChange += FlowEnvironment_NodeConnectChangeEvemt; + EnvEventDecorator.OnNodeCreate += FlowEnvironment_NodeCreateEvent; + EnvEventDecorator.OnNodeRemove += FlowEnvironment_NodeRemoveEvent; + EnvEventDecorator.OnNodePlace += EnvDecorator_OnNodePlaceEvent; + EnvEventDecorator.OnNodeTakeOut += EnvDecorator_OnNodeTakeOutEvent; + EnvEventDecorator.OnFlowRunComplete += FlowEnvironment_OnFlowRunCompleteEvent; + + + EnvEventDecorator.OnMonitorObjectChange += FlowEnvironment_OnMonitorObjectChangeEvent; + EnvEventDecorator.OnNodeInterruptStateChange += FlowEnvironment_OnNodeInterruptStateChangeEvent; + EnvEventDecorator.OnInterruptTrigger += FlowEnvironment_OnInterruptTriggerEvent; + + EnvEventDecorator.OnIOCMembersChanged += FlowEnvironment_OnIOCMembersChangedEvent; + + EnvEventDecorator.OnNodeLocated += FlowEnvironment_OnNodeLocateEvent; + EnvEventDecorator.OnNodeMoved += FlowEnvironment_OnNodeMovedEvent; + EnvEventDecorator.OnEnvOut += FlowEnvironment_OnEnvOutEvent; + } + + + + /// + /// 移除环境事件 + /// + private void ResetFlowEnvironmentEvent() + { + EnvEventDecorator.OnDllLoad -= FlowEnvironment_DllLoadEvent; + EnvEventDecorator.OnProjectSaving -= EnvDecorator_OnProjectSaving; + EnvEventDecorator.OnProjectLoaded -= FlowEnvironment_OnProjectLoaded; + EnvEventDecorator.OnStartNodeChange -= FlowEnvironment_StartNodeChangeEvent; + EnvEventDecorator.OnNodeConnectChange -= FlowEnvironment_NodeConnectChangeEvemt; + EnvEventDecorator.OnNodeCreate -= FlowEnvironment_NodeCreateEvent; + EnvEventDecorator.OnNodeRemove -= FlowEnvironment_NodeRemoveEvent; + EnvEventDecorator.OnNodePlace -= EnvDecorator_OnNodePlaceEvent; + EnvEventDecorator.OnNodeTakeOut -= EnvDecorator_OnNodeTakeOutEvent; + EnvEventDecorator.OnFlowRunComplete -= FlowEnvironment_OnFlowRunCompleteEvent; + + + EnvEventDecorator.OnMonitorObjectChange -= FlowEnvironment_OnMonitorObjectChangeEvent; + EnvEventDecorator.OnNodeInterruptStateChange -= FlowEnvironment_OnNodeInterruptStateChangeEvent; + EnvEventDecorator.OnInterruptTrigger -= FlowEnvironment_OnInterruptTriggerEvent; + + EnvEventDecorator.OnIOCMembersChanged -= FlowEnvironment_OnIOCMembersChangedEvent; + EnvEventDecorator.OnNodeLocated -= FlowEnvironment_OnNodeLocateEvent; + EnvEventDecorator.OnNodeMoved -= FlowEnvironment_OnNodeMovedEvent; + + EnvEventDecorator.OnEnvOut -= FlowEnvironment_OnEnvOutEvent; + + } + + #region 窗体加载方法 + private async void Window_Loaded(object sender, RoutedEventArgs e) + { + var currentPath = System.IO.Directory.GetCurrentDirectory(); // 当前目录 + var baseLibraryFilePath = Path.Combine(currentPath, FlowLibraryManagement.SereinBaseLibrary); + if (File.Exists(baseLibraryFilePath)) + { + EnvDecorator.LoadLibrary(baseLibraryFilePath); // 默认加载 + } + + if (App.FlowProjectData is not null) + { + try + { + await Task.Run(() => + { + EnvDecorator.LoadProject(new FlowEnvInfo { Project = App.FlowProjectData }, App.FileDataPath); // 加载项目 + }); + } + catch (Exception ex) + { + SereinEnv.WriteLine(ex); + return; + } + } + + // + } + private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) + { + LogOutWindow.Close(); + System.Windows.Application.Current.Shutdown(); + } + private void Window_ContentRendered(object sender, EventArgs e) + { + SereinEnv.WriteLine(InfoType.INFO, "load project..."); + var project = App.FlowProjectData; + if (project is null) + { + return; + } + InitializeCanvas(project.Basic.Canvas.Width, project.Basic.Canvas.Height);// 设置画布大小 + //foreach (var connection in Connections) + //{ + // connection.RefreshLine(); // 窗体完成加载后试图刷新所有连接线 + //} + SereinEnv.WriteLine(InfoType.INFO, $"运行环境当前工作目录:{System.IO.Directory.GetCurrentDirectory()}"); + + var canvasData = project.Basic.Canvas; + if (canvasData is not null) + { + scaleTransform.ScaleX = 1; + scaleTransform.ScaleY = 1; + translateTransform.X = 0; + translateTransform.Y = 0; + scaleTransform.ScaleX = canvasData.ScaleX; + scaleTransform.ScaleY = canvasData.ScaleY; + translateTransform.X += canvasData.ViewX; + translateTransform.Y += canvasData.ViewY; + // 应用变换组 + FlowChartCanvas.RenderTransform = canvasTransformGroup; + } + + + } + + + + + #endregion + + #region 运行环境事件 + + /// + /// 环境内容输出 + /// + /// + /// + private void FlowEnvironment_OnEnvOutEvent(InfoType type, string value) + { + LogOutWindow.AppendText($"{DateTime.Now} [{type}] : {value}{Environment.NewLine}"); + } + + /// + /// 需要保存项目 + /// + /// + /// + private void EnvDecorator_OnProjectSaving(ProjectSavingEventArgs eventArgs) + { + SereinProjectData projectData; + try + { + projectData = EnvDecorator.GetProjectInfoAsync() + .GetAwaiter().GetResult(); // 保存项目 + + } + catch (Exception ex) + { + SereinEnv.WriteLine(ex); + return; + } + + projectData.Basic = new Basic + { + Canvas = new FlowCanvas + { + Height = FlowChartCanvas.Height, + Width = FlowChartCanvas.Width, + ViewX = translateTransform.X, + ViewY = translateTransform.Y, + ScaleX = scaleTransform.ScaleX, + ScaleY = scaleTransform.ScaleY, + }, + Versions = "1", + }; + + // 创建一个新的保存文件对话框 + SaveFileDialog saveFileDialog = new() + { + Filter = "DynamicNodeFlow Files (*.dnf)|*.dnf", + DefaultExt = "dnf", + FileName = "project.dnf" + // FileName = System.IO.Path.GetFileName(App.FileDataPath) + }; + + // 显示保存文件对话框 + bool? result = saveFileDialog.ShowDialog(); + // 如果用户选择了文件并点击了保存按钮 + if (result == false) + { + SereinEnv.WriteLine(InfoType.ERROR, "取消保存文件"); + return; + } + + var savePath = saveFileDialog.FileName; + string? librarySavePath = System.IO.Path.GetDirectoryName(savePath); + if (string.IsNullOrEmpty(librarySavePath)) + { + SereinEnv.WriteLine(InfoType.ERROR, "保存项目DLL时返回了意外的文件保存路径"); + return; + } + + + Uri saveProjectFileUri = new Uri(savePath); + SereinEnv.WriteLine(InfoType.INFO, "项目文件保存路径:" + savePath); + for (int index = 0; index < projectData.Librarys.Length; index++) + { + NodeLibraryInfo? library = projectData.Librarys[index]; + string sourceFilePath = new Uri(library.FilePath).LocalPath; // 源文件夹 + string targetFilePath = System.IO.Path.Combine(librarySavePath, library.FileName); // 目标文件夹 + + try + { + if (File.Exists(sourceFilePath)) + { + if (!File.Exists(targetFilePath)) + { + SereinEnv.WriteLine(InfoType.INFO, $"源文件路径 : {sourceFilePath}"); + SereinEnv.WriteLine(InfoType.INFO, $"目标路径 : {targetFilePath}"); + File.Copy(sourceFilePath, targetFilePath, true); + + } + else + { + SereinEnv.WriteLine(InfoType.WARN, $"目标路径已有类库文件: {targetFilePath}"); + } + } + else + { + SereinEnv.WriteLine(InfoType.WARN, $"源文件不存在 : {targetFilePath}"); + } + } + catch (IOException ex) + { + + SereinEnv.WriteLine(InfoType.ERROR, ex.Message); + } + var dirName = System.IO.Path.GetDirectoryName(targetFilePath); + if (!string.IsNullOrEmpty(dirName)) + { + var tmpUri2 = new Uri(targetFilePath); + var relativePath = saveProjectFileUri.MakeRelativeUri(tmpUri2).ToString(); // 转为类库的相对文件路径 + + + + + //string relativePath = System.IO.Path.GetRelativePath(savePath, targetPath); + projectData.Librarys[index].FilePath = relativePath; + } + + } + + JObject projectJsonData = JObject.FromObject(projectData); + File.WriteAllText(savePath, projectJsonData.ToString()); + + + } + + /// + /// 加载完成 + /// + /// + private void FlowEnvironment_OnProjectLoaded(ProjectLoadedEventArgs eventArgs) + { + } + + /// + /// 运行完成 + /// + /// + /// + private void FlowEnvironment_OnFlowRunCompleteEvent(FlowEventArgs eventArgs) + { + SereinEnv.WriteLine(InfoType.INFO, "-------运行完成---------\r\n"); + this.Dispatcher.Invoke(() => + { + IOCObjectViewer.ClearObjItem(); + }); + } + + /// + /// 加载了DLL文件,dll内容 + /// + private void FlowEnvironment_DllLoadEvent(LoadDllEventArgs eventArgs) + { + NodeLibraryInfo nodeLibraryInfo = eventArgs.NodeLibraryInfo; + List methodDetailss = eventArgs.MethodDetailss; + + var dllControl = new DllControl(nodeLibraryInfo); + + foreach (var methodDetailsInfo in methodDetailss) + { + if (!EnumHelper.TryConvertEnum(methodDetailsInfo.NodeType, out var nodeType)) + { + continue; + } + switch (nodeType) + { + case Library.NodeType.Action: + dllControl.AddAction(methodDetailsInfo); // 添加动作类型到控件 + break; + case Library.NodeType.Flipflop: + dllControl.AddFlipflop(methodDetailsInfo); // 添加触发器方法到控件 + break; + } + + } + var menu = new ContextMenu(); + menu.Items.Add(CreateMenuItem("卸载", (s, e) => + { + if (this.EnvDecorator.TryUnloadLibrary(nodeLibraryInfo.AssemblyName)) + { + DllStackPanel.Children.Remove(dllControl); + } + else + { + SereinEnv.WriteLine(InfoType.INFO, "卸载失败"); + } + })); + + dllControl.ContextMenu = menu; + + DllStackPanel.Children.Add(dllControl); // 将控件添加到界面上显示 + + } + + /// + /// 节点连接关系变更 + /// + /// + private void FlowEnvironment_NodeConnectChangeEvemt(NodeConnectChangeEventArgs eventArgs) + { + string fromNodeGuid = eventArgs.FromNodeGuid; + string toNodeGuid = eventArgs.ToNodeGuid; + if (!TryGetControl(fromNodeGuid, out var fromNodeControl) + || !TryGetControl(toNodeGuid, out var toNodeControl)) + { + return; + } + + if (eventArgs.JunctionOfConnectionType == JunctionOfConnectionType.Invoke) + { + ConnectionInvokeType connectionType = eventArgs.ConnectionInvokeType; + #region 创建/删除节点之间的调用关系 + #region 创建连接 + if (eventArgs.ChangeType == NodeConnectChangeEventArgs.ConnectChangeType.Create) // 添加连接 + { + if (fromNodeControl is not INodeJunction IFormJunction || toNodeControl is not INodeJunction IToJunction) + { + SereinEnv.WriteLine(InfoType.INFO, "非预期的连接"); + return; + } + JunctionControlBase startJunction = IFormJunction.NextStepJunction; + JunctionControlBase endJunction = IToJunction.ExecuteJunction; + + // 添加连接 + var connection = new ConnectionControl( + FlowChartCanvas, + connectionType, + startJunction, + endJunction + ); + + if (toNodeControl is FlipflopNodeControl flipflopControl + && flipflopControl?.ViewModel?.NodeModel is NodeModelBase nodeModel) // 某个节点连接到了触发器,尝试从全局触发器视图中移除该触发器 + { + NodeTreeViewer.RemoveGlobalFlipFlop(nodeModel); // 从全局触发器树树视图中移除 + } + Connections.Add(connection); + fromNodeControl.AddCnnection(connection); + toNodeControl.AddCnnection(connection); + EndConnection(); // 环境触发了创建节点连接事件 + + } + #endregion + #region 移除连接 + else if (eventArgs.ChangeType == NodeConnectChangeEventArgs.ConnectChangeType.Remove) // 移除连接 + { + // 需要移除连接 + var removeConnections = Connections.Where(c => + c.Start.MyNode.Guid.Equals(fromNodeGuid) + && c.End.MyNode.Guid.Equals(toNodeGuid) + && (c.Start.JunctionType.ToConnectyionType() == JunctionOfConnectionType.Invoke + || c.End.JunctionType.ToConnectyionType() == JunctionOfConnectionType.Invoke)) + .ToList(); + + + foreach (var connection in removeConnections) + { + Connections.Remove(connection); + fromNodeControl.RemoveConnection(connection); // 移除连接 + toNodeControl.RemoveConnection(connection); // 移除连接 + if (NodeControls.TryGetValue(connection.End.MyNode.Guid, out var control)) + { + JudgmentFlipFlopNode(control); // 连接关系变更时判断 + } + } + } + #endregion + #endregion + } + else + { + ConnectionArgSourceType connectionArgSourceType = eventArgs.ConnectionArgSourceType; + #region 创建/删除节点之间的参数传递关系 + #region 创建连接 + if (eventArgs.ChangeType == NodeConnectChangeEventArgs.ConnectChangeType.Create) // 添加连接 + { + if (fromNodeControl is not INodeJunction IFormJunction || toNodeControl is not INodeJunction IToJunction) + { + SereinEnv.WriteLine(InfoType.INFO, "非预期的情况"); + return; + } + + JunctionControlBase startJunction = eventArgs.ConnectionArgSourceType switch + { + ConnectionArgSourceType.GetPreviousNodeData => IFormJunction.ReturnDataJunction, // 自身节点 + ConnectionArgSourceType.GetOtherNodeData => IFormJunction.ReturnDataJunction, // 其它节点的返回值控制点 + ConnectionArgSourceType.GetOtherNodeDataOfInvoke => IFormJunction.ReturnDataJunction, // 其它节点的返回值控制点 + _ => throw new Exception("窗体事件 FlowEnvironment_NodeConnectChangeEvemt 创建/删除节点之间的参数传递关系 JunctionControlBase 枚举值错误 。非预期的枚举值。") // 应该不会触发 + }; + + if(IToJunction.ArgDataJunction.Length <= eventArgs.ArgIndex) + { + _ = Task.Run(async () => + { + await Task.Delay(500); + FlowEnvironment_NodeConnectChangeEvemt(eventArgs); + }); + return; + } + JunctionControlBase endJunction = IToJunction.ArgDataJunction[eventArgs.ArgIndex]; + LineType lineType = LineType.Bezier; + // 添加连接 + var connection = new ConnectionControl( + lineType, + FlowChartCanvas, + eventArgs.ArgIndex, + eventArgs.ConnectionArgSourceType, + startJunction, + endJunction, + IToJunction + ); + Connections.Add(connection); + fromNodeControl.AddCnnection(connection); + toNodeControl.AddCnnection(connection); + EndConnection(); // 环境触发了创建节点连接事件 + + + } + #endregion + #region 移除连接 + else if (eventArgs.ChangeType == NodeConnectChangeEventArgs.ConnectChangeType.Remove) // 移除连接 + { + // 需要移除连接 + var removeConnections = Connections.Where(c => c.Start.MyNode.Guid.Equals(fromNodeGuid) + && c.End.MyNode.Guid.Equals(toNodeGuid)) + .ToList(); // 获取这两个节点之间的所有连接关系 + + + + foreach (var connection in removeConnections) + { + if (connection.End is ArgJunctionControl junctionControl && junctionControl.ArgIndex == eventArgs.ArgIndex) + { + // 找到符合删除条件的连接线 + Connections.Remove(connection); // 从本地记录中移除 + fromNodeControl.RemoveConnection(connection); // 从节点持有的记录移除 + toNodeControl.RemoveConnection(connection); // 从节点持有的记录移除 + } + + + //if (NodeControls.TryGetValue(connection.End.MyNode.Guid, out var control)) + //{ + // JudgmentFlipFlopNode(control); // 连接关系变更时判断 + //} + } + } + #endregion + #endregion + } + } + + /// + /// 节点移除事件 + /// + /// + private void FlowEnvironment_NodeRemoveEvent(NodeRemoveEventArgs eventArgs) + { + var nodeGuid = eventArgs.NodeGuid; + if (!TryGetControl(nodeGuid, out var nodeControl)) + { + return; + } + + if (nodeControl is null) return; + if (selectNodeControls.Count > 0) + { + if (selectNodeControls.Contains(nodeControl)) + { + selectNodeControls.Remove(nodeControl); + } + } + + if (nodeControl is FlipflopNodeControl flipflopControl) // 判断是否为触发器 + { + var node = flipflopControl?.ViewModel?.NodeModel; + if (node is not null) + { + NodeTreeViewer.RemoveGlobalFlipFlop(node); // 从全局触发器树树视图中移除 + } + } + + + + FlowChartCanvas.Children.Remove(nodeControl); + nodeControl.RemoveAllConection(); + NodeControls.Remove(nodeControl.ViewModel.NodeModel.Guid); + } + + /// + /// 添加节点事件 + /// + /// 添加节点事件参数 + /// + private void FlowEnvironment_NodeCreateEvent(NodeCreateEventArgs eventArgs) + { + var nodeModel = eventArgs.NodeModel; + if (NodeControls.ContainsKey(nodeModel.Guid)) + { + SereinEnv.WriteLine(InfoType.WARN, $"OnNodeCreateEvent 事件接收到意外的返回值:节点Guid重复 - {nodeModel.Guid}"); + return; + } + + PositionOfUI position = eventArgs.Position; + + if(!NodeMVVMManagement.TryGetType(nodeModel.ControlType, out var nodeMVVM)) + { + SereinEnv.WriteLine(InfoType.INFO, $"无法创建{nodeModel.ControlType}节点,节点类型尚未注册。"); + return; + } + if(nodeMVVM.ControlType == null + || nodeMVVM.ViewModelType == null) + { + SereinEnv.WriteLine(InfoType.INFO, $"无法创建{nodeModel.ControlType}节点,UI类型尚未注册(请通过 NodeMVVMManagement.RegisterUI() 方法进行注册)。"); + return; + } + + var nodeCanvas = FlowChartCanvas; + NodeControlBase nodeControl; + try + { + nodeControl = CreateNodeControl(nodeMVVM.ControlType, // 控件UI类型 + nodeMVVM.ViewModelType, // 控件VIewModel类型 + nodeModel, // 控件数据实体 + nodeCanvas); // 所在画布 + } + catch (Exception ex) + { + SereinEnv.WriteLine(ex); + return; + } + + NodeControls.TryAdd(nodeModel.Guid, nodeControl); // 添加到 + if (TryPlaceNodeInRegion(nodeControl, position, out var regionControl)) // 判断添加到区域容器 + { + // 通知运行环境调用加载节点子项的方法 + _ = EnvDecorator.PlaceNodeToContainerAsync(nodeControl.ViewModel.NodeModel.Guid, // 待移动的节点 + regionControl.ViewModel.NodeModel.Guid); // 目标的容器节点 + } + else + { + // 并非添加在容器中,直接放置节点 + PlaceNodeOnCanvas(nodeControl, position.X, position.Y); + } + + + #region 节点树视图 + if (nodeModel.ControlType == NodeControlType.Flipflop) + { + var node = nodeControl?.ViewModel?.NodeModel; + if (node is not null) + { + NodeTreeViewer.AddGlobalFlipFlop(EnvDecorator, node); // 新增的触发器节点添加到全局触发器 + } + } + + GC.Collect(); + #endregion + + } + + /// + /// 放置一个节点 + /// + /// + /// + private void EnvDecorator_OnNodePlaceEvent(NodePlaceEventArgs eventArgs) + { + string nodeGuid = eventArgs.NodeGuid; + string containerNodeGuid = eventArgs.ContainerNodeGuid; + if (!TryGetControl(nodeGuid, out var nodeControl) + || !TryGetControl(containerNodeGuid, out var containerNodeControl)) + { + return; + } + if(containerNodeControl is not INodeContainerControl containerControl) + { + SereinEnv.WriteLine(InfoType.WARN, + $"节点[{nodeGuid}]无法放置于节点[{containerNodeGuid}]," + + $"因为后者并不实现 INodeContainerControl 接口"); + return; + } + nodeControl.PlaceToContainer(containerControl); // 放置在容器节点中 + } + + /// + /// 取出一个节点 + /// + /// + private void EnvDecorator_OnNodeTakeOutEvent(NodeTakeOutEventArgs eventArgs) + { + string nodeGuid = eventArgs.NodeGuid; + if (!TryGetControl(nodeGuid, out var nodeControl)) + { + return; + } + nodeControl.TakeOutContainer(); // 从容器节点中取出 + + } + + + + /// + /// 设置了流程起始控件 + /// + /// + /// + private void FlowEnvironment_StartNodeChangeEvent(StartNodeChangeEventArgs eventArgs) + { + string oldNodeGuid = eventArgs.OldNodeGuid; + string newNodeGuid = eventArgs.NewNodeGuid; + if (!TryGetControl(newNodeGuid, out var newStartNodeControl)) return; + if (!string.IsNullOrEmpty(oldNodeGuid)) + { + if (!TryGetControl(oldNodeGuid, out var oldStartNodeControl)) return; + oldStartNodeControl.BorderBrush = Brushes.Black; + oldStartNodeControl.BorderThickness = new Thickness(0); + } + + newStartNodeControl.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#04FC10")); + newStartNodeControl.BorderThickness = new Thickness(2); + var node = newStartNodeControl?.ViewModel?.NodeModel; + if (node is not null) + { + NodeTreeViewer.LoadNodeTreeOfStartNode(EnvDecorator, node); + } + + } + + /// + /// 被监视的对象发生改变 + /// + /// + private void FlowEnvironment_OnMonitorObjectChangeEvent(MonitorObjectEventArgs eventArgs) + { + string nodeGuid = eventArgs.NodeGuid; + + string monitorKey = MonitorObjectEventArgs.ObjSourceType.NodeFlowData switch + { + MonitorObjectEventArgs.ObjSourceType.NodeFlowData => nodeGuid, + _ => eventArgs.NewData.GetType().FullName, + }; + + //NodeControlBase nodeControl = GuidToControl(nodeGuid); + if (ViewObjectViewer.MonitorObj is null) // 如果没有加载过对象 + { + ViewObjectViewer.LoadObjectInformation(monitorKey, eventArgs.NewData); // 加载对象 ViewObjectViewerControl.MonitorType.Obj + } + else + { + if (monitorKey.Equals(ViewObjectViewer.MonitorKey)) // 相同对象 + { + ViewObjectViewer.RefreshObjectTree(eventArgs.NewData); // 刷新 + } + else + { + ViewObjectViewer.LoadObjectInformation(monitorKey, eventArgs.NewData); // 加载对象 + } + } + + } + + /// + /// 节点中断状态改变。 + /// + /// + private void FlowEnvironment_OnNodeInterruptStateChangeEvent(NodeInterruptStateChangeEventArgs eventArgs) + { + string nodeGuid = eventArgs.NodeGuid; + if (!TryGetControl(nodeGuid, out var nodeControl)) return; + + //if (eventArgs.Class == InterruptClass.None) + //{ + // nodeControl.ViewModel.IsInterrupt = false; + //} + //else + //{ + // nodeControl.ViewModel.IsInterrupt = true; + //} + if(nodeControl.ContextMenu == null) + { + return; + } + foreach (var menuItem in nodeControl.ContextMenu.Items) + { + if (menuItem is MenuItem menu) + { + if ("取消中断".Equals(menu.Header)) + { + menu.Header = "在此中断"; + } + else if ("在此中断".Equals(menu.Header)) + { + menu.Header = "取消中断"; + } + + } + } + + } + + /// + /// 节点触发了中断 + /// + /// + /// + private void FlowEnvironment_OnInterruptTriggerEvent(InterruptTriggerEventArgs eventArgs) + { + string nodeGuid = eventArgs.NodeGuid; + if (!TryGetControl(nodeGuid, out var nodeControl)) return; + if(eventArgs.Type == InterruptTriggerEventArgs.InterruptTriggerType.Exp) + { + SereinEnv.WriteLine(InfoType.INFO, $"表达式触发了中断:{eventArgs.Expression}"); + } + else + { + SereinEnv.WriteLine(InfoType.INFO, $"节点触发了中断:{nodeGuid}"); + } + } + + /// + /// IOC变更 + /// + /// + /// + private void FlowEnvironment_OnIOCMembersChangedEvent(IOCMembersChangedEventArgs eventArgs) + { + IOCObjectViewer.AddDependenciesInstance(eventArgs.Key, eventArgs.Instance); + + } + + /// + /// 节点需要定位 + /// + /// + /// + private void FlowEnvironment_OnNodeLocateEvent(NodeLocatedEventArgs eventArgs) + { + if (!TryGetControl(eventArgs.NodeGuid, out var nodeControl)) return; + //scaleTransform.ScaleX = 1; + //scaleTransform.ScaleY = 1; + // 获取控件在 FlowChartCanvas 上的相对位置 + Rect controlBounds = VisualTreeHelper.GetDescendantBounds(nodeControl); + Point controlPosition = nodeControl.TransformToAncestor(FlowChartCanvas).Transform(new Point(0, 0)); + + // 获取控件在画布上的中心点 + double controlCenterX = controlPosition.X + controlBounds.Width / 2; + double controlCenterY = controlPosition.Y + controlBounds.Height / 2; + + // 考虑缩放因素计算目标位置的中心点 + double scaledCenterX = controlCenterX * scaleTransform.ScaleX; + double scaledCenterY = controlCenterY * scaleTransform.ScaleY; + + + //// 计算画布的可视区域大小 + //double visibleAreaLeft = scaledCenterX; + //double visibleAreaTop = scaledCenterY; + //double visibleAreaRight = scaledCenterX + FlowChartStackGrid.ActualWidth; + //double visibleAreaBottom = scaledCenterY + FlowChartStackGrid.ActualHeight; + //// 检查控件中心点是否在可视区域内 + //bool isInView = scaledCenterX >= visibleAreaLeft && scaledCenterX <= visibleAreaRight && + // scaledCenterY >= visibleAreaTop && scaledCenterY <= visibleAreaBottom; + + //Console.WriteLine($"isInView :{isInView}"); + + //if (!isInView) + //{ + //} + // 计算平移偏移量,使得控件在可视区域的中心 + double translateX = scaledCenterX - FlowChartStackGrid.ActualWidth / 2; + double translateY = scaledCenterY - FlowChartStackGrid.ActualHeight / 2; + + var translate = this.translateTransform; + // 应用平移变换 + translate.X = 0; + translate.Y = 0; + translate.X -= translateX; + translate.Y -= translateY; + + // 设置RenderTransform以实现移动效果 + TranslateTransform translateTransform = new TranslateTransform(); + nodeControl.RenderTransform = translateTransform; + ElasticAnimation(nodeControl, translateTransform, 4, 1, 0.5); + + } + + /// + /// 控件抖动 + /// 来源:https://www.cnblogs.com/RedSky/p/17705411.html + /// 作者:HotSky + /// (……太好用了) + /// + /// + /// 需要抖动的控件 + /// 抖动第一下偏移量 + /// 减弱幅度(小于等于power,大于0) + /// 持续系数(大于0),越大时间越长, + private static void ElasticAnimation(NodeControlBase nodeControl, TranslateTransform translate, int power, int range = 1, double speed = 1) + { + DoubleAnimationUsingKeyFrames animation1 = new DoubleAnimationUsingKeyFrames(); + for (int i = power, j = 1; i >= 0; i -= range) + { + animation1.KeyFrames.Add(new LinearDoubleKeyFrame(-i, TimeSpan.FromMilliseconds(j++ * 100 * speed))); + animation1.KeyFrames.Add(new LinearDoubleKeyFrame(i, TimeSpan.FromMilliseconds(j++ * 100 * speed))); + } + translate.BeginAnimation(TranslateTransform.YProperty, animation1); + DoubleAnimationUsingKeyFrames animation2 = new DoubleAnimationUsingKeyFrames(); + for (int i = power, j = 1; i >= 0; i -= range) + { + animation2.KeyFrames.Add(new LinearDoubleKeyFrame(-i, TimeSpan.FromMilliseconds(j++ * 100 * speed))); + animation2.KeyFrames.Add(new LinearDoubleKeyFrame(i, TimeSpan.FromMilliseconds(j++ * 100 * speed))); + } + translate.BeginAnimation(TranslateTransform.XProperty, animation2); + + animation2.Completed += (s, e) => + { + nodeControl.RenderTransform = null; // 或者重新设置为默认值 + }; + } + + /// + /// 节点移动 + /// + /// + private void FlowEnvironment_OnNodeMovedEvent(NodeMovedEventArgs eventArgs) + { + if (!TryGetControl(eventArgs.NodeGuid, out var nodeControl)) return; + nodeControl.UpdateLocationConnections(); + + //var newLeft = eventArgs.X; + //var newTop = eventArgs.Y; + //// 限制控件不超出FlowChartCanvas的边界 + //if (newLeft >= 0 && newLeft + nodeControl.ActualWidth <= FlowChartCanvas.ActualWidth) + //{ + // Canvas.SetLeft(nodeControl, newLeft); + + //} + //if (newTop >= 0 && newTop + nodeControl.ActualHeight <= FlowChartCanvas.ActualHeight) + //{ + // Canvas.SetTop(nodeControl, newTop); + //} + + + } + + /// + /// Guid 转 NodeControl + /// + /// + /// + /// + private bool TryGetControl(string nodeGuid,out NodeControlBase nodeControl) + { + if (string.IsNullOrEmpty(nodeGuid)) + { + nodeControl = null; + return false; + } + if (!NodeControls.TryGetValue(nodeGuid, out nodeControl)) + { + nodeControl = null; + return false; + } + if(nodeControl is null) + { + return false; + } + return true; + } + + #endregion + + + #region 节点控件的创建 + + + /// + /// 创建了节点,添加到画布。配置默认事件 + /// + /// + /// + /// + private void PlaceNodeOnCanvas(NodeControlBase nodeControl, double x, double y) + { + // 添加控件到画布 + FlowChartCanvas.Children.Add(nodeControl); + Canvas.SetLeft(nodeControl, x); + Canvas.SetTop(nodeControl, y); + + ConfigureContextMenu(nodeControl); // 配置节点右键菜单 + ConfigureNodeEvents(nodeControl); // 配置节点事件 + } + + /// + /// 配置节点事件(移动,点击相关) + /// + /// + private void ConfigureNodeEvents(NodeControlBase nodeControl) + { + nodeControl.MouseLeftButtonDown += Block_MouseLeftButtonDown; + nodeControl.MouseMove += Block_MouseMove; + nodeControl.MouseLeftButtonUp += Block_MouseLeftButtonUp; + } + + + #endregion + + #region 配置右键菜单 + + /// + /// 配置节点右键菜单 + /// + /// + /// 任何情景下都尽量避免直接修改 ViewModel 中的 NodeModel 节点实体相关数据。 + /// 而是应该调用 FlowEnvironment 提供接口进行操作。 + /// 因为 Workbench 应该更加关注UI视觉效果,而非直接干扰流程环境运行的逻辑。 + /// 之所以暴露 NodeModel 属性,因为有些场景下不可避免的需要直接获取节点的属性。 + /// + private void ConfigureContextMenu(NodeControlBase nodeControl) + { + + var contextMenu = new ContextMenu(); + var nodeGuid = nodeControl.ViewModel?.NodeModel?.Guid; + #region 触发器节点 + + if(nodeControl.ViewModel?.NodeModel.ControlType == NodeControlType.Flipflop) + { + contextMenu.Items.Add(CreateMenuItem("启动触发器", (s, e) => + { + if (s is MenuItem menuItem) + { + if (menuItem.Header.ToString() == "启动触发器") + { + EnvDecorator.ActivateFlipflopNode(nodeGuid); + + menuItem.Header = "终结触发器"; + } + else + { + EnvDecorator.TerminateFlipflopNode(nodeGuid); + menuItem.Header = "启动触发器"; + + } + } + })); + } + + #endregion + + if (nodeControl.ViewModel?.NodeModel?.MethodDetails?.ReturnType is Type returnType && returnType != typeof(void)) + { + contextMenu.Items.Add(CreateMenuItem("查看返回类型", (s, e) => + { + DisplayReturnTypeTreeViewer(returnType); + })); + } + + + + contextMenu.Items.Add(CreateMenuItem("设为起点", (s, e) => EnvDecorator.SetStartNodeAsync(nodeGuid))); + contextMenu.Items.Add(CreateMenuItem("删除", async (s, e) => + { + var result = await EnvDecorator.RemoveNodeAsync(nodeGuid); + })); + + #region 右键菜单功能 - 控件对齐 + + var AvoidMenu = new MenuItem(); + AvoidMenu.Items.Add(CreateMenuItem("群组对齐", (s, e) => + { + AlignControlsWithGrouping(selectNodeControls, AlignMode.Grouping); + })); + AvoidMenu.Items.Add(CreateMenuItem("规划对齐", (s, e) => + { + AlignControlsWithGrouping(selectNodeControls, AlignMode.Planning); + })); + AvoidMenu.Items.Add(CreateMenuItem("水平中心对齐", (s, e) => + { + AlignControlsWithGrouping(selectNodeControls, AlignMode.HorizontalCenter); + })); + AvoidMenu.Items.Add(CreateMenuItem("垂直中心对齐 ", (s, e) => + { + AlignControlsWithGrouping(selectNodeControls, AlignMode.VerticalCenter); + })); + + AvoidMenu.Items.Add(CreateMenuItem("垂直对齐时水平斜分布", (s, e) => + { + AlignControlsWithGrouping(selectNodeControls, AlignMode.Vertical); + })); + AvoidMenu.Items.Add(CreateMenuItem("水平对齐时垂直斜分布", (s, e) => + { + AlignControlsWithGrouping(selectNodeControls, AlignMode.Horizontal); + })); + + AvoidMenu.Header = "对齐"; + contextMenu.Items.Add(AvoidMenu); + + + #endregion + + nodeControl.ContextMenu = contextMenu; + } + + /// + /// 查看返回类型(树形结构展开类型的成员) + /// + /// + private void DisplayReturnTypeTreeViewer(Type type) + { + try + { + var typeViewerWindow = new TypeViewerWindow + { + Type = type, + }; + typeViewerWindow.LoadTypeInformation(); + typeViewerWindow.Show(); + } + catch (Exception ex) + { + SereinEnv.WriteLine(InfoType.ERROR, ex.ToString()); + } + } + #endregion + + #region 拖拽DLL文件到左侧功能区,加载相关节点清单 + /// + /// 当拖动文件到窗口时触发,加载DLL文件 + /// + /// + /// + private void Window_Drop(object sender, DragEventArgs e) + { + if (e.Data.GetDataPresent(DataFormats.FileDrop)) + { + string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); + foreach (string file in files) + { + if (file.EndsWith(".dll")) + { + try + { + EnvDecorator.LoadLibrary(file); + } + catch (Exception ex) + { + SereinEnv.WriteLine(ex); + return; + } + } + } + } + } + + /// + /// 当拖动文件经过窗口时触发,设置拖放效果为复制 + /// + /// + /// + private void Window_DragOver(object sender, DragEventArgs e) + { + e.Effects = DragDropEffects.Copy; + e.Handled = true; + } + + #endregion + + #region 与流程图/节点相关 + + /// + /// 鼠标在画布移动。 + /// 选择控件状态下,调整选择框大小 + /// 连接状态下,实时更新连接线的终点位置。 + /// 移动画布状态下,移动画布。 + /// + private void FlowChartCanvas_MouseMove(object sender, MouseEventArgs e) + { + var myData = GlobalJunctionData.MyGlobalConnectingData; + if (myData.IsCreateing && e.LeftButton == MouseButtonState.Pressed) + { + + if (myData.Type == JunctionOfConnectionType.Invoke) + { + ViewModel.IsConnectionInvokeNode = true; // 正在连接节点的调用关系 + + } + else + { + ViewModel.IsConnectionArgSourceNode = true; // 正在连接节点的调用关系 + } + var currentPoint = e.GetPosition(FlowChartCanvas); + currentPoint.X -= 2; + currentPoint.Y -= 2; + myData.UpdatePoint(currentPoint); + return; + } + + + + if (IsCanvasDragging && e.MiddleButton == MouseButtonState.Pressed) // 正在移动画布(按住中键) + { + Point currentMousePosition = e.GetPosition(this); + double deltaX = currentMousePosition.X - startCanvasDragPoint.X; + double deltaY = currentMousePosition.Y - startCanvasDragPoint.Y; + + translateTransform.X += deltaX; + translateTransform.Y += deltaY; + + startCanvasDragPoint = currentMousePosition; + + foreach (var line in Connections) + { + line.RefreshLine(); // 画布移动时刷新所有连接线 + } + } + + if (IsSelectControl) // 正在选取节点 + { + IsSelectDragging = e.LeftButton == MouseButtonState.Pressed; + // 获取当前鼠标位置 + Point currentPoint = e.GetPosition(FlowChartCanvas); + + // 更新选取矩形的位置和大小 + double x = Math.Min(currentPoint.X, startSelectControolPoint.X); + double y = Math.Min(currentPoint.Y, startSelectControolPoint.Y); + double width = Math.Abs(currentPoint.X - startSelectControolPoint.X); + double height = Math.Abs(currentPoint.Y - startSelectControolPoint.Y); + + Canvas.SetLeft(SelectionRectangle, x); + Canvas.SetTop(SelectionRectangle, y); + SelectionRectangle.Width = width; + SelectionRectangle.Height = height; + + } + } + + /// + /// 基础节点的拖拽放置创建 + /// + /// + /// + private void BaseNodeControl_PreviewMouseMove(object sender, MouseEventArgs e) + { + if (sender is UserControl control) + { + if(e.LeftButton == MouseButtonState.Pressed) + { + // 创建一个 DataObject 用于拖拽操作,并设置拖拽效果 + var dragData = new DataObject(MouseNodeType.CreateBaseNodeInCanvas, control.GetType()); + DragDrop.DoDragDrop(control, dragData, DragDropEffects.Move); + } + + } + } + + /// + /// 放置操作,根据拖放数据创建相应的控件,并处理相关操作 + /// + /// + /// + private void FlowChartCanvas_Drop(object sender, DragEventArgs e) + { + try + { + var canvasDropPosition = e.GetPosition(FlowChartCanvas); // 更新画布落点 + PositionOfUI position = new PositionOfUI(canvasDropPosition.X, canvasDropPosition.Y); + if (e.Data.GetDataPresent(MouseNodeType.CreateDllNodeInCanvas)) + { + if (e.Data.GetData(MouseNodeType.CreateDllNodeInCanvas) is MoveNodeData nodeData) + { + Task.Run(async () => + { + await EnvDecorator.CreateNodeAsync(nodeData.NodeControlType, position, nodeData.MethodDetailsInfo); // 创建DLL文件的节点对象 + }); + } + } + else if (e.Data.GetDataPresent(MouseNodeType.CreateBaseNodeInCanvas)) + { + if (e.Data.GetData(MouseNodeType.CreateBaseNodeInCanvas) is Type droppedType) + { + NodeControlType nodeControlType = droppedType switch + { + Type when typeof(ConditionRegionControl).IsAssignableFrom(droppedType) => NodeControlType.ConditionRegion, // 条件区域 + Type when typeof(ConditionNodeControl).IsAssignableFrom(droppedType) => NodeControlType.ExpCondition, + Type when typeof(ExpOpNodeControl).IsAssignableFrom(droppedType) => NodeControlType.ExpOp, + Type when typeof(GlobalDataControl).IsAssignableFrom(droppedType) => NodeControlType.GlobalData, + Type when typeof(ScriptNodeControl).IsAssignableFrom(droppedType) => NodeControlType.Script, + _ => NodeControlType.None, + }; + if (nodeControlType != NodeControlType.None) + { + Task.Run(async () => + { + await EnvDecorator.CreateNodeAsync(nodeControlType, position); // 创建基础节点对象 + }); + } + } + } + e.Handled = true; + } + catch (Exception ex) + { + SereinEnv.WriteLine(InfoType.ERROR, ex.ToString()); + } + } + + /// + /// 尝试判断是否为区域,如果是,将节点放置在区域中 + /// + /// + /// + /// 目标节点控件 + /// + private bool TryPlaceNodeInRegion(NodeControlBase nodeControl, + PositionOfUI position, + out NodeControlBase targetNodeControl) + { + var point = new Point(position.X, position.Y); + HitTestResult hitTestResult = VisualTreeHelper.HitTest(FlowChartCanvas, point); + if (hitTestResult != null && hitTestResult.VisualHit is UIElement hitElement) + { + // 准备放置条件表达式控件 + if (nodeControl.ViewModel.NodeModel.ControlType == NodeControlType.ExpCondition) + { + ConditionRegionControl? conditionRegion = GetParentOfType(hitElement); + if (conditionRegion is not null) + { + targetNodeControl = conditionRegion; + //// 如果存在条件区域容器 + //conditionRegion.AddCondition(nodeControl); + return true; + } + } + + else + { + // 准备放置全局数据控件 + GlobalDataControl? globalDataControl = GetParentOfType(hitElement); + if (globalDataControl is not null) + { + targetNodeControl = globalDataControl; + return true; + } + } + } + targetNodeControl = null; + return false; + } + + ///// + ///// 将节点放在目标区域中 + ///// + ///// 区域容器 + ///// 节点控件 + //private void TryPlaceNodeInRegion(NodeControlBase regionControl, NodeControlBase nodeControl) + //{ + // // 准备放置条件表达式控件 + // if (nodeControl.ViewModel.NodeModel.ControlType == NodeControlType.ExpCondition) + // { + // if (regionControl is ConditionRegionControl conditionRegion) + // { + // conditionRegion.AddCondition(nodeControl); // 条件区域容器 + // } + // } + // else if(regionControl.ViewModel.NodeModel.ControlType == NodeControlType.GlobalData) + // { + // if (regionControl is GlobalDataControl globalDataControl) + // { + // // 全局数据节点容器 + // globalDataControl.SetDataNodeControl(nodeControl); + // } + // } + //} + + + + /// + /// 拖动效果,根据拖放数据是否为指定类型设置拖放效果 + /// + /// + /// + private void FlowChartCanvas_DragOver(object sender, DragEventArgs e) + { + if (e.Data.GetDataPresent(MouseNodeType.CreateDllNodeInCanvas) + || e.Data.GetDataPresent(MouseNodeType.CreateBaseNodeInCanvas)) + { + e.Effects = DragDropEffects.Move; + } + else + { + e.Effects = DragDropEffects.None; + } + e.Handled = true; + } + + /// + /// 控件的鼠标左键按下事件,启动拖动操作。同时显示当前正在传递的数据。 + /// + private void Block_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + //if (GlobalJunctionData.IsCreatingConnection) + //{ + // return; + //} + if(sender is NodeControlBase nodeControl) + { + ChangeViewerObjOfNode(nodeControl); + if (nodeControl?.ViewModel?.NodeModel?.MethodDetails?.IsProtectionParameter == true) return; + IsControlDragging = true; + startControlDragPoint = e.GetPosition(FlowChartCanvas); // 记录鼠标按下时的位置 + ((UIElement)sender).CaptureMouse(); // 捕获鼠标 + e.Handled = true; // 防止事件传播影响其他控件 + } + } + + /// + /// 控件的鼠标移动事件,根据鼠标拖动更新控件的位置。批量移动计算移动逻辑。 + /// + private void Block_MouseMove(object sender, MouseEventArgs e) + { + if (IsCanvasDragging) + return; + if (IsSelectControl) + return; + + if (IsControlDragging) // 如果正在拖动控件 + { + Point currentPosition = e.GetPosition(FlowChartCanvas); // 获取当前鼠标位置 + + if (selectNodeControls.Count > 0 && sender is NodeControlBase nodeControlMain && selectNodeControls.Contains(nodeControlMain)) + { + // 进行批量移动 + // 获取旧位置 + var oldLeft = Canvas.GetLeft(nodeControlMain); + var oldTop = Canvas.GetTop(nodeControlMain); + + // 计算被选择控件的偏移量 + var deltaX = /*(int)*/(currentPosition.X - startControlDragPoint.X); + var deltaY = /*(int)*/(currentPosition.Y - startControlDragPoint.Y); + + // 移动被选择的控件 + var newLeft = oldLeft + deltaX; + var newTop = oldTop + deltaY; + + this.EnvDecorator.MoveNode(nodeControlMain.ViewModel.NodeModel.Guid, newLeft, newTop); // 移动节点 + + // 计算控件实际移动的距离 + var actualDeltaX = newLeft - oldLeft; + var actualDeltaY = newTop - oldTop; + + // 移动其它选中的控件 + foreach (var nodeControl in selectNodeControls) + { + if (nodeControl != nodeControlMain) // 跳过已经移动的控件 + { + var otherNewLeft = Canvas.GetLeft(nodeControl) + actualDeltaX; + var otherNewTop = Canvas.GetTop(nodeControl) + actualDeltaY; + this.EnvDecorator.MoveNode(nodeControl.ViewModel.NodeModel.Guid, otherNewLeft, otherNewTop); // 移动节点 + } + } + + // 更新节点之间线的连接位置 + foreach (var nodeControl in selectNodeControls) + { + nodeControl.UpdateLocationConnections(); + } + } + else + { // 单个节点移动 + if (sender is not NodeControlBase nodeControl) + { + return; + } + double deltaX = currentPosition.X - startControlDragPoint.X; // 计算X轴方向的偏移量 + double deltaY = currentPosition.Y - startControlDragPoint.Y; // 计算Y轴方向的偏移量 + double newLeft = Canvas.GetLeft(nodeControl) + deltaX; // 新的左边距 + double newTop = Canvas.GetTop(nodeControl) + deltaY; // 新的上边距 + this.EnvDecorator.MoveNode(nodeControl.ViewModel.NodeModel.Guid, newLeft, newTop); // 移动节点 + nodeControl.UpdateLocationConnections(); + } + startControlDragPoint = currentPosition; // 更新起始点位置 + } + + } + + + // 改变对象树? + private void ChangeViewerObjOfNode(NodeControlBase nodeControl) + { + var node = nodeControl.ViewModel.NodeModel; + //if (node is not null && (node.MethodDetails is null || node.MethodDetails.ReturnType != typeof(void)) + if (node is not null && node.MethodDetails?.ReturnType != typeof(void)) + { + var key = node.Guid; + object instance = null; + //Console.WriteLine("WindowXaml 后台代码中 ChangeViewerObjOfNode 需要重新设计"); + //var instance = node.GetFlowData(); // 对象预览树视图获取(后期更改) + if(instance is not null) + { + ViewObjectViewer.LoadObjectInformation(key, instance); + ChangeViewerObj(key, instance); + } + } + } + public void ChangeViewerObj(string key, object instance) + { + if (ViewObjectViewer.MonitorObj is null) + { + // EnvDecorator.SetMonitorObjState(key, true); // 通知环境,该节点的数据更新后需要传到UI + return; + } + if (instance is null) + { + return; + } + if (key.Equals(ViewObjectViewer.MonitorKey) == true) + { + ViewObjectViewer.RefreshObjectTree(instance); + return; + } + else + { + //EnvDecorator.SetMonitorObjState(ViewObjectViewer.MonitorKey,false); // 取消对旧节点的监视 + //EnvDecorator.SetMonitorObjState(key, true); // 通知环境,该节点的数据更新后需要传到UI + } + } + #endregion + + #region UI连接控件操作 + + /// + /// 控件的鼠标左键松开事件,结束拖动操作 + /// + private void Block_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (IsControlDragging) + { + IsControlDragging = false; + ((UIElement)sender).ReleaseMouseCapture(); // 释放鼠标捕获 + + } + + //if (IsConnecting) + //{ + // var formNodeGuid = startConnectNodeControl?.ViewModel.NodeModel.Guid; + // var toNodeGuid = (sender as NodeControlBase)?.ViewModel.NodeModel.Guid; + // if (string.IsNullOrEmpty(formNodeGuid) || string.IsNullOrEmpty(toNodeGuid)) + // { + // return; + // } + // EnvDecorator.ConnectNodeAsync(formNodeGuid, toNodeGuid,0,0, currentConnectionType); + //} + //GlobalJunctionData.OK(); + } + + + /// + /// 结束连接操作,清理状态并移除虚线。 + /// + private void EndConnection() + { + Mouse.OverrideCursor = null; // 恢复视觉效果 + ViewModel.IsConnectionArgSourceNode = false; + ViewModel.IsConnectionInvokeNode = false; + GlobalJunctionData.OK(); + } + + #region 拖动画布实现缩放平移效果 + private void FlowChartCanvas_MouseDown(object sender, MouseButtonEventArgs e) + { + IsCanvasDragging = true; + startCanvasDragPoint = e.GetPosition(this); + FlowChartCanvas.CaptureMouse(); + e.Handled = true; // 防止事件传播影响其他控件 + } + + private void FlowChartCanvas_MouseUp(object sender, MouseButtonEventArgs e) + { + + + + if (IsCanvasDragging) + { + IsCanvasDragging = false; + FlowChartCanvas.ReleaseMouseCapture(); + } + } + + // 单纯缩放画布,不改变画布大小 + private void FlowChartCanvas_MouseWheel(object sender, MouseWheelEventArgs e) + { + // if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl)) + { + if (e.Delta < 0 && scaleTransform.ScaleX < 0.05) return; + if (e.Delta > 0 && scaleTransform.ScaleY > 2.0) return; + // 获取鼠标在 Canvas 内的相对位置 + var mousePosition = e.GetPosition(FlowChartCanvas); + + // 缩放因子,根据滚轮方向调整 + //double zoomFactor = e.Delta > 0 ? 0.1 : -0.1; + double zoomFactor = e.Delta > 0 ? 1.1 : 0.9; + + // 当前缩放比例 + double oldScale = scaleTransform.ScaleX; + double newScale = oldScale * zoomFactor; + //double newScale = oldScale + zoomFactor; + // 更新缩放比例 + scaleTransform.ScaleX = newScale; + scaleTransform.ScaleY = newScale; + + // 计算缩放前后鼠标相对于 Canvas 的位置差异 + // double offsetX = mousePosition.X - (mousePosition.X * zoomFactor); + // double offsetY = mousePosition.Y - (mousePosition.Y * zoomFactor); + + // 更新 TranslateTransform,确保以鼠标位置为中心进行缩放 + translateTransform.X -= (mousePosition.X * (newScale - oldScale)); + translateTransform.Y -= (mousePosition.Y * (newScale - oldScale)); + } + } + + // 设置画布宽度高度 + private void InitializeCanvas(double width, double height) + { + FlowChartCanvas.Width = width; + FlowChartCanvas.Height = height; + } + + + #region 动态调整区域大小 + //private void Thumb_DragDelta_TopLeft(object sender, DragDeltaEventArgs e) + //{ + // // 从左上角调整大小 + // double newWidth = Math.Max(FlowChartCanvas.ActualWidth - e.HorizontalChange, 0); + // double newHeight = Math.Max(FlowChartCanvas.ActualHeight - e.VerticalChange, 0); + + // FlowChartCanvas.Width = newWidth; + // FlowChartCanvas.Height = newHeight; + + // Canvas.SetLeft(FlowChartCanvas, Canvas.GetLeft(FlowChartCanvas) + e.HorizontalChange); + // Canvas.SetTop(FlowChartCanvas, Canvas.GetTop(FlowChartCanvas) + e.VerticalChange); + //} + + //private void Thumb_DragDelta_TopRight(object sender, DragDeltaEventArgs e) + //{ + // // 从右上角调整大小 + // double newWidth = Math.Max(FlowChartCanvas.ActualWidth + e.HorizontalChange, 0); + // double newHeight = Math.Max(FlowChartCanvas.ActualHeight - e.VerticalChange, 0); + + // FlowChartCanvas.Width = newWidth; + // FlowChartCanvas.Height = newHeight; + + // Canvas.SetTop(FlowChartCanvas, Canvas.GetTop(FlowChartCanvas) + e.VerticalChange); + //} + + //private void Thumb_DragDelta_BottomLeft(object sender, DragDeltaEventArgs e) + //{ + // // 从左下角调整大小 + // double newWidth = Math.Max(FlowChartCanvas.ActualWidth - e.HorizontalChange, 0); + // double newHeight = Math.Max(FlowChartCanvas.ActualHeight + e.VerticalChange, 0); + + // FlowChartCanvas.Width = newWidth; + // FlowChartCanvas.Height = newHeight; + + // Canvas.SetLeft(FlowChartCanvas, Canvas.GetLeft(FlowChartCanvas) + e.HorizontalChange); + //} + + private void Thumb_DragDelta_BottomRight(object sender, DragDeltaEventArgs e) + { + // 获取缩放后的水平和垂直变化 + double horizontalChange = e.HorizontalChange * scaleTransform.ScaleX; + double verticalChange = e.VerticalChange * scaleTransform.ScaleY; + + // 计算新的宽度和高度,确保不会小于400 + double newWidth = Math.Max(FlowChartCanvas.ActualWidth + horizontalChange, 400); + double newHeight = Math.Max(FlowChartCanvas.ActualHeight + verticalChange, 400); + + newHeight = newHeight < 400 ? 400 : newHeight; + newWidth = newWidth < 400 ? 400 : newWidth; + + InitializeCanvas(newWidth, newHeight); + + //// 从右下角调整大小 + //double newWidth = Math.Max(FlowChartCanvas.ActualWidth + e.HorizontalChange * scaleTransform.ScaleX, 0); + //double newHeight = Math.Max(FlowChartCanvas.ActualHeight + e.VerticalChange * scaleTransform.ScaleY, 0); + + //newWidth = newWidth < 400 ? 400 : newWidth; + //newHeight = newHeight < 400 ? 400 : newHeight; + + //if (newWidth > 400 && newHeight > 400) + //{ + // FlowChartCanvas.Width = newWidth; + // FlowChartCanvas.Height = newHeight; + + // double x = e.HorizontalChange > 0 ? -0.5 : 0.5; + // double y = e.VerticalChange > 0 ? -0.5 : 0.5; + + // double deltaX = x * scaleTransform.ScaleX; + // double deltaY = y * scaleTransform.ScaleY; + // Test(deltaX, deltaY); + //} + } + + //private void Thumb_DragDelta_Left(object sender, DragDeltaEventArgs e) + //{ + // // 从左侧调整大小 + // double newWidth = Math.Max(FlowChartCanvas.ActualWidth - e.HorizontalChange, 0); + + // FlowChartCanvas.Width = newWidth; + // Canvas.SetLeft(FlowChartCanvas, Canvas.GetLeft(FlowChartCanvas) + e.HorizontalChange); + //} + + private void Thumb_DragDelta_Right(object sender, DragDeltaEventArgs e) + { + //从右侧调整大小 + // 获取缩放后的水平变化 + double horizontalChange = e.HorizontalChange * scaleTransform.ScaleX; + + // 计算新的宽度,确保不会小于400 + double newWidth = Math.Max(FlowChartCanvas.ActualWidth + horizontalChange, 400); + + newWidth = newWidth < 400 ? 400 : newWidth; + InitializeCanvas(newWidth, FlowChartCanvas.Height); + + } + + //private void Thumb_DragDelta_Top(object sender, DragDeltaEventArgs e) + //{ + // // 从顶部调整大小 + // double newHeight = Math.Max(FlowChartCanvas.ActualHeight - e.VerticalChange, 0); + + // FlowChartCanvas.Height = newHeight; + // Canvas.SetTop(FlowChartCanvas, Canvas.GetTop(FlowChartCanvas) + e.VerticalChange); + //} + + private void Thumb_DragDelta_Bottom(object sender, DragDeltaEventArgs e) + { + // 获取缩放后的垂直变化 + double verticalChange = e.VerticalChange * scaleTransform.ScaleY; + // 计算新的高度,确保不会小于400 + double newHeight = Math.Max(FlowChartCanvas.ActualHeight + verticalChange, 400); + newHeight = newHeight < 400 ? 400 : newHeight; + InitializeCanvas(FlowChartCanvas.Width, newHeight); + } + + + private void Test(double deltaX, double deltaY) + { + //Console.WriteLine((translateTransform.X, translateTransform.Y)); + //translateTransform.X += deltaX; + //translateTransform.Y += deltaY; + } + + #endregion + #endregion + + + + #endregion + + #region 画布中框选节点控件动作 + + /// + /// 在画布中尝试选取控件 + /// + /// + /// + private void FlowChartCanvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (GlobalJunctionData.MyGlobalConnectingData.IsCreateing) + { + return; + } + if (!IsSelectControl) + { + // 进入选取状态 + IsSelectControl = true; + IsSelectDragging = false; // 初始化为非拖动状态 + + // 记录鼠标起始点 + startSelectControolPoint = e.GetPosition(FlowChartCanvas); + + // 初始化选取矩形的位置和大小 + Canvas.SetLeft(SelectionRectangle, startSelectControolPoint.X); + Canvas.SetTop(SelectionRectangle, startSelectControolPoint.Y); + SelectionRectangle.Width = 0; + SelectionRectangle.Height = 0; + + // 显示选取矩形 + SelectionRectangle.Visibility = Visibility.Visible; + SelectionRectangle.ContextMenu ??= ConfiguerSelectionRectangle(); + + // 捕获鼠标,以便在鼠标移动到Canvas外部时仍能处理事件 + FlowChartCanvas.CaptureMouse(); + } + else + { + // 如果已经是选取状态,单击则认为结束框选 + CompleteSelection(); + } + + e.Handled = true; // 防止事件传播影响其他控件 + } + + /// + /// 在画布中释放鼠标按下,结束选取状态 / 停止创建连线,尝试连接节点 + /// + /// + /// + private void FlowChartCanvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (IsSelectControl) + { + // 松开鼠标时判断是否为拖动操作 + if (IsSelectDragging) + { + // 完成拖动框选 + CompleteSelection(); + } + + // 释放鼠标捕获 + FlowChartCanvas.ReleaseMouseCapture(); + } + + // 创建连线 + if (GlobalJunctionData.MyGlobalConnectingData is ConnectingData myData && myData.IsCreateing) + { + + if (myData.IsCanConnected) + { + var canvas = this.FlowChartCanvas; + var currentendPoint = e.GetPosition(canvas); // 当前鼠标落点 + var changingJunctionPosition = myData.CurrentJunction.TranslatePoint(new Point(0, 0), canvas); + var changingJunctionRect = new Rect(changingJunctionPosition, new Size(myData.CurrentJunction.Width, myData.CurrentJunction.Height)); + + if (changingJunctionRect.Contains(currentendPoint)) // 可以创建连接 + { + #region 方法调用关系创建 + if (myData.Type == JunctionOfConnectionType.Invoke) + { + this.EnvDecorator.ConnectInvokeNodeAsync(myData.StartJunction.MyNode.Guid, myData.CurrentJunction.MyNode.Guid, + myData.StartJunction.JunctionType, + myData.CurrentJunction.JunctionType, + myData.ConnectionInvokeType); + } + #endregion + + #region 参数来源关系创建 + else if (myData.Type == JunctionOfConnectionType.Arg) + { + var argIndex = 0; + if (myData.StartJunction is ArgJunctionControl argJunction1) + { + argIndex = argJunction1.ArgIndex; + } + else if (myData.CurrentJunction is ArgJunctionControl argJunction2) + { + argIndex = argJunction2.ArgIndex; + } + + this.EnvDecorator.ConnectArgSourceNodeAsync(myData.StartJunction.MyNode.Guid, myData.CurrentJunction.MyNode.Guid, + myData.StartJunction.JunctionType, + myData.CurrentJunction.JunctionType, + myData.ConnectionArgSourceType, + argIndex); + } + #endregion + } + EndConnection(); + } + + } + e.Handled = true; + + } + + /// 完成选取操作 + /// + private void CompleteSelection() + { + IsSelectControl = false; + + // 隐藏选取矩形 + SelectionRectangle.Visibility = Visibility.Collapsed; + + // 获取选取范围 + Rect selectionArea = new Rect(Canvas.GetLeft(SelectionRectangle), + Canvas.GetTop(SelectionRectangle), + SelectionRectangle.Width, + SelectionRectangle.Height); + + // 处理选取范围内的控件 + // selectNodeControls.Clear(); + foreach (UIElement element in FlowChartCanvas.Children) + { + Rect elementBounds = new Rect(Canvas.GetLeft(element), Canvas.GetTop(element), + element.RenderSize.Width, element.RenderSize.Height); + + if (selectionArea.Contains(elementBounds)) + { + if (element is NodeControlBase control) + { + if (!selectNodeControls.Contains(control)) + { + selectNodeControls.Add(control); + } + } + } + } + + // 选中后的操作 + SelectedNode(); + } + private ContextMenu ConfiguerSelectionRectangle() + { + var contextMenu = new ContextMenu(); + contextMenu.Items.Add(CreateMenuItem("删除", (s, e) => + { + if (selectNodeControls.Count > 0) + { + foreach (var node in selectNodeControls.ToArray()) + { + var guid = node?.ViewModel?.NodeModel?.Guid; + if (!string.IsNullOrEmpty(guid)) + { + EnvDecorator.RemoveNodeAsync(guid); + } + } + } + SelectionRectangle.Visibility = Visibility.Collapsed; + })); + return contextMenu; + // nodeControl.ContextMenu = contextMenu; + } + private void SelectedNode() + { + + if (selectNodeControls.Count == 0) + { + //Console.WriteLine($"没有选择控件"); + SelectionRectangle.Visibility = Visibility.Collapsed; + return; + } + if(selectNodeControls.Count == 1) + { + // ChangeViewerObjOfNode(selectNodeControls[0]); + } + + //Console.WriteLine($"一共选取了{selectNodeControls.Count}个控件"); + foreach (var node in selectNodeControls) + { + //node.ViewModel.IsSelect =true; + // node.ViewModel.CancelSelect(); + node.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FFC700")); + node.BorderThickness = new Thickness(4); + } + } + private void CancelSelectNode() + { + IsSelectControl = false; + foreach (var nodeControl in selectNodeControls) + { + //nodeControl.ViewModel.IsSelect = false; + nodeControl.BorderBrush = Brushes.Black; + nodeControl.BorderThickness = new Thickness(0); + if (nodeControl.ViewModel.NodeModel.IsStart) + { + nodeControl.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#04FC10")); + nodeControl.BorderThickness = new Thickness(2); + } + } + selectNodeControls.Clear(); + } + #endregion + + #region 节点对齐 (有些小瑕疵) + + //public void UpdateConnectedLines() + //{ + // //foreach (var nodeControl in selectNodeControls) + // //{ + // // UpdateConnections(nodeControl); + // //} + // this.Dispatcher.Invoke(() => + // { + // foreach (var line in Connections) + // { + // line.AddOrRefreshLine(); // 节点完成对齐 + // } + // }); + + //} + + + #region Plan A 群组对齐 + + public void AlignControlsWithGrouping(List selectNodeControls, double proximityThreshold = 50, double spacing = 10) + { + if (selectNodeControls is null || selectNodeControls.Count < 2) + return; + + // 按照控件的相对位置进行分组 + var horizontalGroups = GroupByProximity(selectNodeControls, proximityThreshold, isHorizontal: true); + var verticalGroups = GroupByProximity(selectNodeControls, proximityThreshold, isHorizontal: false); + + // 对每个水平群组进行垂直对齐 + foreach (var group in horizontalGroups) + { + double avgY = group.Average(c => Canvas.GetTop(c)); // 计算Y坐标平均值 + foreach (var control in group) + { + Canvas.SetTop(control, avgY); // 对齐Y坐标 + } + } + + // 对每个垂直群组进行水平对齐 + foreach (var group in verticalGroups) + { + double avgX = group.Average(c => Canvas.GetLeft(c)); // 计算X坐标平均值 + foreach (var control in group) + { + Canvas.SetLeft(control, avgX); // 对齐X坐标 + } + } + } + + // 基于控件间的距离来分组,按水平或垂直方向 + private List> GroupByProximity(List controls, double proximityThreshold, bool isHorizontal) + { + var groups = new List>(); + + foreach (var control in controls) + { + bool addedToGroup = false; + + // 尝试将控件加入现有的群组 + foreach (var group in groups) + { + if (IsInProximity(group, control, proximityThreshold, isHorizontal)) + { + group.Add(control); + addedToGroup = true; + break; + } + } + + // 如果没有加入任何群组,创建新群组 + if (!addedToGroup) + { + groups.Add(new List { control }); + } + } + + return groups; + } + + // 判断控件是否接近某个群组 + private bool IsInProximity(List group, NodeControlBase control, double proximityThreshold, bool isHorizontal) + { + foreach (var existingControl in group) + { + double distance = isHorizontal + ? Math.Abs(Canvas.GetTop(existingControl) - Canvas.GetTop(control)) // 垂直方向的距离 + : Math.Abs(Canvas.GetLeft(existingControl) - Canvas.GetLeft(control)); // 水平方向的距离 + + if (distance <= proximityThreshold) + { + return true; + } + } + return false; + } + + #endregion + + #region Plan B 规划对齐 + public void AlignControlsWithDynamicProgramming(List selectNodeControls, double spacing = 10) + { + if (selectNodeControls is null || selectNodeControls.Count < 2) + return; + + int n = selectNodeControls.Count; + double[] dp = new double[n]; + int[] split = new int[n]; + + // 初始化动态规划数组 + for (int i = 1; i < n; i++) + { + dp[i] = double.MaxValue; + for (int j = 0; j < i; j++) + { + double cost = CalculateAlignmentCost(selectNodeControls, j, i, spacing); + if (dp[j] + cost < dp[i]) + { + dp[i] = dp[j] + cost; + split[i] = j; + } + } + } + + // 回溯找到最优的对齐方式 + AlignWithSplit(selectNodeControls, split, n - 1, spacing); + } + + // 计算从控件[j]到控件[i]的对齐代价,并考虑控件的大小和间距 + private double CalculateAlignmentCost(List controls, int start, int end, double spacing) + { + double totalWidth = 0; + double totalHeight = 0; + + for (int i = start; i <= end; i++) + { + totalWidth += controls[i].ActualWidth; + totalHeight += controls[i].ActualHeight; + } + + // 水平和垂直方向代价计算,包括控件大小和间距 + double widthCost = totalWidth + (end - start) * spacing; + double heightCost = totalHeight + (end - start) * spacing; + + // 返回较小的代价,表示更优的对齐方式 + return Math.Min(widthCost, heightCost); + } + + // 根据split数组调整控件位置,确保控件不重叠 + private void AlignWithSplit(List controls, int[] split, int end, double spacing) + { + if (end <= 0) + return; + + AlignWithSplit(controls, split, split[end], spacing); + + // 从split[end]到end的控件进行对齐操作 + double currentX = Canvas.GetLeft(controls[split[end]]); + double currentY = Canvas.GetTop(controls[split[end]]); + + for (int i = split[end] + 1; i <= end; i++) + { + // 水平或垂直对齐,确保控件之间有间距 + if (currentX + controls[i].ActualWidth + spacing <= Canvas.GetLeft(controls[end])) + { + Canvas.SetLeft(controls[i], currentX + controls[i].ActualWidth + spacing); + currentX += controls[i].ActualWidth + spacing; + } + else + { + Canvas.SetTop(controls[i], currentY + controls[i].ActualHeight + spacing); + currentY += controls[i].ActualHeight + spacing; + } + } + } + + #endregion + + public enum AlignMode + { + /// + /// 水平对齐 + /// + Horizontal, + /// + /// 垂直对齐 + /// + Vertical, + /// + /// 水平中心对齐 + /// + HorizontalCenter, + /// + /// 垂直中心对齐 + /// + VerticalCenter, + + /// + /// 规划对齐 + /// + Planning, + /// + /// 群组对齐 + /// + Grouping, + } + + + public void AlignControlsWithGrouping(List selectNodeControls, AlignMode alignMode, double proximityThreshold = 50, double spacing = 10) + { + if (selectNodeControls is null || selectNodeControls.Count < 2) + return; + + switch (alignMode) + { + case AlignMode.Horizontal: + AlignHorizontally(selectNodeControls, spacing);// AlignToCenter + break; + + case AlignMode.Vertical: + + AlignVertically(selectNodeControls, spacing); + break; + + case AlignMode.HorizontalCenter: + AlignToCenter(selectNodeControls, isHorizontal: false, spacing); + break; + + case AlignMode.VerticalCenter: + AlignToCenter(selectNodeControls, isHorizontal: true, spacing); + break; + + case AlignMode.Planning: + AlignControlsWithDynamicProgramming(selectNodeControls, spacing); + break; + case AlignMode.Grouping: + AlignControlsWithGrouping(selectNodeControls, proximityThreshold, spacing); + break; + } + + + } + + // 垂直对齐并避免重叠 + private void AlignHorizontally(List controls, double spacing) + { + double avgY = controls.Average(c => Canvas.GetTop(c)); // 计算Y坐标平均值 + double currentY = avgY; + + foreach (var control in controls.OrderBy(c => Canvas.GetTop(c))) // 按Y坐标排序对齐 + { + Canvas.SetTop(control, currentY); + currentY += control.ActualHeight + spacing; // 保证控件之间有足够的垂直间距 + } + } + + // 水平对齐并避免重叠 + private void AlignVertically(List controls, double spacing) + { + double avgX = controls.Average(c => Canvas.GetLeft(c)); // 计算X坐标平均值 + double currentX = avgX; + + foreach (var control in controls.OrderBy(c => Canvas.GetLeft(c))) // 按X坐标排序对齐 + { + Canvas.SetLeft(control, currentX); + currentX += control.ActualWidth + spacing; // 保证控件之间有足够的水平间距 + } + } + + // 按中心点对齐 + private void AlignToCenter(List controls, bool isHorizontal, double spacing) + { + double avgCenter = isHorizontal + ? controls.Average(c => Canvas.GetLeft(c) + c.ActualWidth / 2) // 水平中心点 + : controls.Average(c => Canvas.GetTop(c) + c.ActualHeight / 2); // 垂直中心点 + + foreach (var control in controls) + { + if (isHorizontal) + { + double left = avgCenter - control.ActualWidth / 2; + Canvas.SetLeft(control, left); + } + else + { + double top = avgCenter - control.ActualHeight / 2; + Canvas.SetTop(control, top); + } + } + } + + #endregion + + #region 静态方法:创建节点,创建菜单子项,获取区域 + + /// + /// 创建节点控件 + /// + /// 节点控件视图控件类型 + /// 节点控件ViewModel类型 + /// 节点Model实例 + /// 节点所在画布 + /// + /// 无法创建节点控件 + private static NodeControlBase CreateNodeControl(Type controlType, Type viewModelType, NodeModelBase model, Canvas nodeCanvas) + { + if ((controlType is null) + || viewModelType is null + || model is null) + { + throw new Exception("无法创建节点控件"); + } + if (typeof(NodeControlBase).IsSubclassOf(controlType) || typeof(NodeControlViewModelBase).IsSubclassOf(viewModelType)) + { + throw new Exception("无法创建节点控件"); + } + + if (string.IsNullOrEmpty(model.Guid)) + { + model.Guid = Guid.NewGuid().ToString(); + } + + var viewModel = Activator.CreateInstance(viewModelType, [model]); + var controlObj = Activator.CreateInstance(controlType, [viewModel]); + if (controlObj is NodeControlBase nodeControl) + { + nodeControl.NodeCanvas = nodeCanvas; + return nodeControl; + } + else + { + throw new Exception("无法创建节点控件"); + } + } + + + /// + /// 创建菜单子项 + /// + /// + /// + /// + public static MenuItem CreateMenuItem(string header, RoutedEventHandler handler) + { + var menuItem = new MenuItem { Header = header }; + menuItem.Click += handler; + return menuItem; + } + + + + /// + /// 穿透元素获取区域容器 + /// + /// + /// + /// + public static T? GetParentOfType(DependencyObject element) where T : DependencyObject + { + while (element != null) + { + if (element is T e) + { + return e; + } + element = VisualTreeHelper.GetParent(element); + } + return null; + } + + #endregion + + #region 节点树、IOC视图管理 + + private void JudgmentFlipFlopNode(NodeControlBase nodeControl) + { + if (nodeControl is FlipflopNodeControl flipflopControl + && flipflopControl?.ViewModel?.NodeModel is NodeModelBase nodeModel) // 判断是否为触发器 + { + int count = 0; + foreach (var ct in NodeStaticConfig.ConnectionTypes) + { + count += nodeModel.PreviousNodes[ct].Count; + } + if (count == 0) + { + NodeTreeViewer.AddGlobalFlipFlop(EnvDecorator, nodeModel); // 添加到全局触发器树树视图 + } + else + { + NodeTreeViewer.RemoveGlobalFlipFlop(nodeModel); // 从全局触发器树树视图中移除 + } + } + } + void LoadIOCObjectViewer() + { + + } + #endregion + + + + #region 顶部菜单栏 - 调试功能区 + + /// + /// 运行测试 + /// + /// + /// + private async void ButtonDebugRun_Click(object sender, RoutedEventArgs e) + { + LogOutWindow?.Show(); + + + +#if WINDOWS + //Dispatcher uiDispatcher = Application.Current.MainWindow.Dispatcher; + //SynchronizationContext? uiContext = SynchronizationContext.Current; + //EnvDecorator.IOC.CustomRegisterInstance(typeof(SynchronizationContextk).FullName, uiContext, false); +#endif + + // 获取主线程的 SynchronizationContext + Action uiInvoke = (uiContext, action) => uiContext?.Post(state => action?.Invoke(), null); + + SereinEnv.WriteLine(InfoType.INFO, "流程开始运行"); + try + { + await EnvDecorator.StartFlowAsync(); + } + catch (Exception ex) + { + SereinEnv.WriteLine(ex); + return; + } + + // await EnvDecorator.StartAsync(); + //await Task.Factory.StartNew(FlowEnvironment.StartAsync); + } + + /// + /// 退出 + /// + /// + /// + private async void ButtonDebugFlipflopNode_Click(object sender, RoutedEventArgs e) + { + try + { + await EnvDecorator.ExitFlowAsync(); // 在运行平台上点击了退出 + } + catch (Exception ex) + { + SereinEnv.WriteLine(ex); + return; + } + } + + /// + /// 从选定的节点开始运行 + /// + /// + /// + private async void ButtonStartFlowInSelectNode_Click(object sender, RoutedEventArgs e) + { + if (selectNodeControls.Count == 0) + { + SereinEnv.WriteLine(InfoType.INFO, "请至少选择一个节点"); + } + else if (selectNodeControls.Count > 1) + { + SereinEnv.WriteLine(InfoType.INFO, "请只选择一个节点"); + } + try + { + await this.EnvDecorator.StartAsyncInSelectNode(selectNodeControls[0].ViewModel.NodeModel.Guid); + } + catch (Exception ex) + { + SereinEnv.WriteLine(ex); + return; + } + } + + + + + #endregion + + #region 顶部菜单栏 - 项目文件菜单 + + + /// + /// 保存为项目文件 + /// + /// + /// + private async void ButtonSaveFile_Click(object sender, RoutedEventArgs e) + { + try + { + EnvDecorator.SaveProject(); + } + catch (Exception ex) + { + SereinEnv.WriteLine(ex); + return; + } + } + + + /// + /// 打开本地项目文件 + /// + /// + /// + private void ButtonOpenLocalProject_Click(object sender, RoutedEventArgs e) + { + + } + + + +#endregion + + #region 顶部菜单栏 - 视图管理 + /// + /// 重置画布 + /// + /// + /// + private void ButtonResetCanvas_Click(object sender, RoutedEventArgs e) + { + translateTransform.X = 0; + translateTransform.Y = 0; + scaleTransform.ScaleX = 1; + scaleTransform.ScaleY = 1; + } + /// + /// 查看输出日志窗口 + /// + /// + /// + private void ButtonOpenConsoleOutWindow_Click(object sender, RoutedEventArgs e) + { + LogOutWindow?.Show(); + } + /// + /// 定位节点 + /// + /// + /// + private void ButtonLocationNode_Click(object sender, RoutedEventArgs e) + { + InputDialog inputDialog = new InputDialog(); + inputDialog.Closed += (s, e) => + { + var nodeGuid = inputDialog.InputValue; + EnvDecorator.NodeLocated(nodeGuid); + }; + inputDialog.ShowDialog(); + } + + #endregion + + #region 顶部菜单栏 - 远程管理 + private async void ButtonStartRemoteServer_Click(object sender, RoutedEventArgs e) + { + try + { + await this.EnvDecorator.StartRemoteServerAsync(); + } + catch (Exception ex) + { + SereinEnv.WriteLine(ex); + return; + } + } + + /// + /// 连接远程运行环境 + /// + /// + /// + private void ButtonConnectionRemoteEnv_Click(object sender, RoutedEventArgs e) + { + var windowEnvRemoteLoginView = new WindowEnvRemoteLoginView(async (addres, port, token) => + { + ResetFlowEnvironmentEvent();// 移除事件 + (var isConnect, var _) = await this.EnvDecorator.ConnectRemoteEnv(addres, port, token); + InitFlowEnvironmentEvent(); // 重新添加事件(如果没有连接成功,那么依然是原本的环境) + if (isConnect) + { + // 连接成功,加载远程项目 + _ = Task.Run(async () => + { + try + { + var flowEnvInfo = await EnvDecorator.GetEnvInfoAsync(); + EnvDecorator.LoadProject(flowEnvInfo, string.Empty);// 加载远程环境的项目 + } + catch (Exception ex) + { + SereinEnv.WriteLine(ex); + return; + } + }); + + } + }); + windowEnvRemoteLoginView.Show(); + + } + #endregion + + + + /// + /// 窗体按键监听。 + /// + /// + /// + private void Window_PreviewKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Tab) + { + e.Handled = true; // 禁止默认的Tab键行为 + } + + #region 复制粘贴选择的节点 + if (Keyboard.Modifiers == ModifierKeys.Control) + { + if (e.Key == Key.C && selectNodeControls.Count > 0) + { + CpoyNodeInfo(); + } + else if (e.Key == Key.V) + { + PasteNodeInfo(); + } + } + #endregion + if (e.KeyStates == Keyboard.GetKeyStates(Key.Escape)) + { + IsControlDragging = false; + IsCanvasDragging = false; + SelectionRectangle.Visibility = Visibility.Collapsed; + CancelSelectNode(); + EndConnection(); + } + + if(GlobalJunctionData.MyGlobalConnectingData is ConnectingData myData && myData.IsCreateing) + { + if(myData.Type == JunctionOfConnectionType.Invoke) + { + ConnectionInvokeType connectionInvokeType = e.KeyStates switch + { + KeyStates k when k == Keyboard.GetKeyStates(Key.D1) => ConnectionInvokeType.Upstream, + KeyStates k when k == Keyboard.GetKeyStates(Key.D2) => ConnectionInvokeType.IsSucceed, + KeyStates k when k == Keyboard.GetKeyStates(Key.D3) => ConnectionInvokeType.IsFail, + KeyStates k when k == Keyboard.GetKeyStates(Key.D4) => ConnectionInvokeType.IsError, + _ => ConnectionInvokeType.None, + }; + + if (connectionInvokeType != ConnectionInvokeType.None) + { + myData.ConnectionInvokeType = connectionInvokeType; + myData.MyLine.Line.UpdateLineColor(connectionInvokeType.ToLineColor()); + } + } + else if (myData.Type == JunctionOfConnectionType.Arg) + { + ConnectionArgSourceType connectionArgSourceType = e.KeyStates switch + { + KeyStates k when k == Keyboard.GetKeyStates(Key.D1) => ConnectionArgSourceType.GetOtherNodeData, + KeyStates k when k == Keyboard.GetKeyStates(Key.D2) => ConnectionArgSourceType.GetOtherNodeDataOfInvoke, + _ => ConnectionArgSourceType.GetPreviousNodeData, + }; + + if (connectionArgSourceType != ConnectionArgSourceType.GetPreviousNodeData) + { + myData.ConnectionArgSourceType = connectionArgSourceType; + myData.MyLine.Line.UpdateLineColor(connectionArgSourceType.ToLineColor()); + } + } + myData.CurrentJunction.InvalidateVisual(); // 刷新目标节点控制点样式 + + } + + + } + + #region 复制节点,粘贴节点 + + /// + /// 复制节点 + /// + private void CpoyNodeInfo() + { + if(selectNodeControls.Count == 0) + { + return; + } + // 处理复制操作 + var dictSelection = selectNodeControls + .Select(control => control.ViewModel.NodeModel).ToList(); + + + // 遍历当前已选节点 + foreach (var node in dictSelection.ToArray()) + { + if(node.ChildrenNode.Count == 0) + { + continue; + } + // 遍历这些节点的子节点,添加过来 + foreach (var childNode in node.ChildrenNode) + { + dictSelection.Add(childNode); + } + } + + var nodeInfos = dictSelection.Select(item => item.ToInfo()); + + JObject json = new JObject() + { + ["nodes"] = JArray.FromObject(nodeInfos) + }; + + var jsonText = json.ToString(); + + + try + { + //Clipboard.SetDataObject(result, true); // 持久性设置 + Clipboard.SetDataObject(jsonText, true); // 持久性设置 + SereinEnv.WriteLine(InfoType.INFO, $"复制已选节点({dictSelection.Count}个)"); + } + catch (Exception ex) + { + SereinEnv.WriteLine(InfoType.ERROR, $"复制失败:{ex.Message}"); + } + } + + /// + /// 粘贴节点 + /// + private void PasteNodeInfo() + { + if (Clipboard.ContainsText()) + { + try + { + + string clipboardText = Clipboard.GetText(TextDataFormat.Text); + string jsonText = JObject.Parse(clipboardText)["nodes"].ToString(); + List nodes = JsonConvert.DeserializeObject>(jsonText); + if (nodes is null || nodes.Count < 0) + { + return; + } + + #region 节点去重 + Dictionary guids = new Dictionary(); // 记录 Guid + // 遍历当前已选节点 + foreach (var node in nodes.ToArray()) + { + if (NodeControls.ContainsKey(node.Guid) && !guids.ContainsKey(node.Guid)) + { + // 如果是没出现过、且在当前记录中重复的Guid,则记录并新增对应的映射。 + guids.TryAdd(node.Guid, Guid.NewGuid().ToString()); + } + else + { + // 出现过的Guid,说明重复添加了。应该不会走到这。 + continue; + } + + if (node.ChildNodeGuids is null) + { + continue; // 跳过没有子节点的节点 + } + + // 遍历这些节点的子节点,获得完整的已选节点信息 + foreach (var childNodeGuid in node.ChildNodeGuids) + { + if (NodeControls.ContainsKey(node.Guid) && !NodeControls.ContainsKey(node.Guid)) + { + // 当前Guid并不重复,跳过替换 + continue; + } + if (!guids.ContainsKey(childNodeGuid)) + { + // 如果是没出现过的Guid,则记录并新增对应的映射。 + guids.TryAdd(node.Guid, Guid.NewGuid().ToString()); + } + + if (!string.IsNullOrEmpty(childNodeGuid) + && NodeControls.TryGetValue(childNodeGuid, out var nodeControl)) + { + + var newNodeInfo = nodeControl.ViewModel.NodeModel.ToInfo(); + nodes.Add(newNodeInfo); + } + } + } + + //var flashText = new FlashText.NET.TextReplacer(); + + //var t = guids.Select(kvp => (kvp.Key, kvp.Value)).ToArray(); + //var result = flashText.ReplaceWords(jsonText, t); + + StringBuilder sb = new StringBuilder(jsonText); + foreach (var kv in guids) + { + sb.Replace(kv.Key, kv.Value); + } + string result = sb.ToString(); + + + /*var replacer = new GuidReplacer(); + foreach (var kv in guids) + { + replacer.AddReplacement(kv.Key, kv.Value); + } + string result = replacer.Replace(jsonText);*/ + + + //SereinEnv.WriteLine(InfoType.ERROR, result); + nodes = JsonConvert.DeserializeObject>(result); + + if (nodes is null || nodes.Count < 0) + { + return; + } + #endregion + + Point mousePosition = Mouse.GetPosition(FlowChartCanvas); + PositionOfUI positionOfUI = new PositionOfUI(mousePosition.X, mousePosition.Y); // 坐标数据 + + // 获取第一个节点的原始位置 + var index0NodeX = nodes[0].Position.X; + var index0NodeY = nodes[0].Position.Y; + + // 计算所有节点相对于第一个节点的偏移量 + foreach (var node in nodes) + { + + var offsetX = node.Position.X - index0NodeX; + var offsetY = node.Position.Y - index0NodeY; + + // 根据鼠标位置平移节点 + node.Position = new PositionOfUI(positionOfUI.X + offsetX, positionOfUI.Y + offsetY); + } + + _ = EnvDecorator.LoadNodeInfosAsync(nodes); + } + catch (Exception ex) + { + + //SereinEnv.WriteLine(InfoType.ERROR, $"粘贴节点时发生异常:{ex}"); + } + // SereinEnv.WriteLine(InfoType.INFO, $"剪贴板文本内容: {clipboardText}"); + } + else if (Clipboard.ContainsImage()) + { + // var image = Clipboard.GetImage(); + } + else + { + SereinEnv.WriteLine(InfoType.INFO, "剪贴板中没有可识别的数据。"); + } + } + + #endregion + + + + /* /// + /// 对象装箱测试 + /// + /// + /// + private void ButtonTestExpObj_Click(object sender, RoutedEventArgs e) + { + //string jsonString = + //""" + //{ + // "Name": "张三", + // "Age": 24, + // "Address": { + // "City": "北京", + // "PostalCode": "10000" + // } + //} + //"""; + + var externalData = new Dictionary + { + { "Name", "John" }, + { "Age", 30 }, + { "Addresses", new List> + { + new Dictionary + { + { "Street", "123 Main St" }, + { "City", "New York" } + }, + new Dictionary + { + { "Street", "456 Another St" }, + { "City", "Los Angeles" } + } + } + } + }; + + if (!ObjDynamicCreateHelper.TryResolve(externalData, "RootType",out var result)) + { + SereinEnv.WriteLine(InfoType.ERROR, "赋值过程中有错误,请检查属性名和类型!"); + return; + } + ObjDynamicCreateHelper.PrintObjectProperties(result!); + var exp = "@set .Addresses[1].Street = 233"; + var data = SerinExpressionEvaluator.Evaluate(exp, result!, out bool isChange); + exp = "@get .Addresses[1].Street"; + data = SerinExpressionEvaluator.Evaluate(exp,result!, out isChange); + SereinEnv.WriteLine(InfoType.INFO, $"{exp} => {data}"); + } +*/ + } +} \ No newline at end of file diff --git a/Workbench/MainWindowViewModel.cs b/Workbench/MainWindowViewModel.cs new file mode 100644 index 0000000..0aedffd --- /dev/null +++ b/Workbench/MainWindowViewModel.cs @@ -0,0 +1,101 @@ +using Serein.Library.Api; +using Serein.Library.Utils; +using Serein.NodeFlow.Env; +using System.ComponentModel; +using System.Windows; + +namespace Serein.Workbench +{ + /// + /// 工作台数据视图 + /// + /// + public class MainWindowViewModel: INotifyPropertyChanged + { + private readonly MainWindow window ; + + /// + /// 运行环境 + /// + public IFlowEnvironment FlowEnvironment { get; set; } + + /// + /// 工作台数据视图 + /// + /// + public MainWindowViewModel(MainWindow window) + { + UIContextOperation? uIContextOperation = null; + Application.Current.Dispatcher.Invoke(() => + { + SynchronizationContext? uiContext = SynchronizationContext.Current; // 在UI线程上获取UI线程上下文信息 + if (uiContext != null) + { + uIContextOperation = new UIContextOperation(uiContext); // 封装一个调用UI线程的工具类 + } + }); + + if (uIContextOperation is null) + { + throw new Exception("无法封装 UIContextOperation "); + } + else + { + FlowEnvironment = new FlowEnvironmentDecorator(uIContextOperation); + //_ = FlowEnvironment.StartRemoteServerAsync(); + this.window = window; + } + } + + + private bool _isConnectionInvokeNode = false; + /// + /// 是否正在连接节点的方法调用关系 + /// + public bool IsConnectionInvokeNode { get => _isConnectionInvokeNode; set + { + if (_isConnectionInvokeNode != value) + { + SetProperty(ref _isConnectionInvokeNode, value); + } + } + } + + private bool _isConnectionArgSouceNode = false; + /// + /// 是否正在连接节点的参数传递关系 + /// + public bool IsConnectionArgSourceNode { get => _isConnectionArgSouceNode; set + { + if (_isConnectionArgSouceNode != value) + { + SetProperty(ref _isConnectionArgSouceNode, value); + } + } + } + + + /// + /// 略 + /// 此事件为自动生成 + /// + public event PropertyChangedEventHandler? PropertyChanged; + /// + /// 通知属性变更 + /// + /// 类型 + /// 绑定的变量 + /// 新的数据 + /// + protected void SetProperty(ref T storage, T value, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = null) + { + if (Equals(storage, value)) + { + return; + } + + storage = value; + PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/Workbench/Node/INodeContainerControl.cs b/Workbench/Node/INodeContainerControl.cs new file mode 100644 index 0000000..4c01eff --- /dev/null +++ b/Workbench/Node/INodeContainerControl.cs @@ -0,0 +1,33 @@ +using Serein.Workbench.Node.View; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Serein.Workbench.Node +{ + + /// + /// 约束具有容器功能的节点控件应该有什么方法 + /// + public interface INodeContainerControl + { + /// + /// 放置一个节点 + /// + /// + bool PlaceNode(NodeControlBase nodeControl); + + /// + /// 取出一个节点 + /// + /// + bool TakeOutNode(NodeControlBase nodeControl); + + /// + /// 取出所有节点(用于删除容器) + /// + void TakeOutAll(); + } +} diff --git a/Workbench/Node/INodeJunction.cs b/Workbench/Node/INodeJunction.cs new file mode 100644 index 0000000..ae74d0d --- /dev/null +++ b/Workbench/Node/INodeJunction.cs @@ -0,0 +1,52 @@ +using Serein.Workbench.Node.View; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace Serein.Workbench.Node +{ + + + + /// + /// 约束一个节点应该有哪些控制点 + /// + public interface INodeJunction + { + /// + /// 方法执行入口控制点 + /// + JunctionControlBase ExecuteJunction { get; } + /// + /// 执行完成后下一个要执行的方法控制点 + /// + JunctionControlBase NextStepJunction { get; } + + /// + /// 参数节点控制点 + /// + JunctionControlBase[] ArgDataJunction { get; } + /// + /// 返回值控制点 + /// + JunctionControlBase ReturnDataJunction { get; } + + /// + /// 获取目标参数控制点,用于防止wpf释放资源导致找不到目标节点,返回-1,-1的坐标 + /// + /// + /// + JunctionControlBase GetJunctionOfArgData(int index) + { + var arr = ArgDataJunction; + if (index >= arr.Length) + { + return null; + } + return arr[index]; + } + } +} diff --git a/Workbench/Node/Junction/ConnectionLineShape.cs b/Workbench/Node/Junction/ConnectionLineShape.cs new file mode 100644 index 0000000..122adaf --- /dev/null +++ b/Workbench/Node/Junction/ConnectionLineShape.cs @@ -0,0 +1,234 @@ +using Serein.Library; +using Serein.Workbench.Extension; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; + +namespace Serein.Workbench.Node.View +{ + /// + /// 连接线的类型 + /// + public enum LineType + { + /// + /// 贝塞尔曲线 + /// + Bezier, + /// + /// 半圆线 + /// + Semicircle, + } + + + + /// + /// 贝塞尔曲线 + /// + public class ConnectionLineShape : Shape + { + private readonly double strokeThickness; + + private readonly LineType lineType; + + /// + /// 确定起始坐标和目标坐标、外光样式的曲线 + /// + /// 线条类型 + /// 起始坐标 + /// 结束坐标 + /// 颜色 + /// 是否为虚线 + public ConnectionLineShape(LineType lineType, + Point start, + Point end, + Brush brush, + bool isDotted = false, + bool isTop = false) + { + this.lineType = lineType; + this.brush = brush; + startPoint = start; + endPoint = end; + this.strokeThickness = 4; + InitElementPoint(isDotted, isTop); + InvalidateVisual(); // 触发重绘 + } + + + public void InitElementPoint(bool isDotted , bool isTop = false) + { + hitVisiblePen = new Pen(Brushes.Transparent, 1.0); // 初始化碰撞检测线 + hitVisiblePen.Freeze(); // Freeze以提高性能 + visualPen = new Pen(brush, 3.0); // 默认可视化Pen + opacity = 1.0d; + if (isDotted) + { + opacity = 0.42d; + visualPen.DashStyle = DashStyles.Dash; // 选择虚线样式 + } + visualPen.Freeze(); // Freeze以提高性能 + + linkSize = 4; // 整线条粗细 + int zIndex = -999999; + if (isTop) + { + zIndex *= -1; + } + Panel.SetZIndex(this, zIndex); // 置底 + } + + /// + /// 更新线条落点位置 + /// + /// + /// + public void UpdatePoints(Point start, Point end) + { + startPoint = start; + endPoint = end; + InvalidateVisual(); // 触发重绘 + } + + /// + /// 更新线条落点位置 + /// + /// + public void UpdateEndPoints(Point point) + { + endPoint = point; + InvalidateVisual(); // 触发重绘 + } + /// + /// 更新线条落点位置 + /// + /// + public void UpdateStartPoints(Point point) + { + startPoint = point; + InvalidateVisual(); // 触发重绘 + } + + /// + /// 控件重绘事件 + /// + /// + protected override void OnRender(DrawingContext drawingContext) + { + // 刷新线条显示位置 + switch (this.lineType) + { + case LineType.Bezier: + DrawBezierCurve(drawingContext, startPoint, endPoint); + break; + case LineType.Semicircle: + DrawSemicircleCurve(drawingContext, startPoint, endPoint); + break; + default: + break; + } + + } + #region 重绘 + + private readonly StreamGeometry streamGeometry = new StreamGeometry(); + private Point rightCenterOfStartLocation; // 目标节点选择左侧边缘中心 + private Point leftCenterOfEndLocation; // 起始节点选择右侧边缘中心 + private Pen hitVisiblePen; // 初始化碰撞检测线 + private Pen visualPen; // 默认可视化Pen + private Point startPoint; // 连接线的起始节点 + private Point endPoint; // 连接线的终点 + private Brush brush; // 线条颜色 + private double opacity; // 透明度 + + double linkSize; // 根据缩放比例调整线条粗细 + protected override Geometry DefiningGeometry => streamGeometry; + + public void UpdateLineColor(Brush brush) + { + visualPen = new Pen(brush, 3.0); // 默认可视化Pen + InvalidateVisual(); // 触发重绘 + } + + + private Point c0, c1; // 用于计算贝塞尔曲线控制点逻辑 + private Vector axis = new Vector(1, 0); + private Vector startToEnd; + private void DrawBezierCurve(DrawingContext drawingContext, + Point start, + Point end) + { + // 控制点的计算逻辑 + double power = 140; // 控制贝塞尔曲线的“拉伸”强度 + drawingContext.PushOpacity(opacity); + // 计算轴向向量与起点到终点的向量 + //var axis = new Vector(1, 0); + startToEnd = (end.ToVector() - start.ToVector()).NormalizeTo(); + + // 计算拉伸程度k,拉伸与水平夹角正相关 + var k = 1 - Math.Pow(Math.Max(0, axis.DotProduct(startToEnd)), 10.0); + + // 如果起点x大于终点x,增加额外的偏移量,避免重叠 + var bias = start.X > end.X ? Math.Abs(start.X - end.X) * 0.25 : 0; + + // 控制点的实际计算 + c0 = new Point(+(power + bias) * k + start.X, start.Y); + c1 = new Point(-(power + bias) * k + end.X, end.Y); + + // 准备StreamGeometry以用于绘制曲线 + streamGeometry.Clear(); + using (var context = streamGeometry.Open()) + { + context.BeginFigure(start, true, false); // 曲线起点 + context.BezierTo(c0, c1, end, true, false); // 画贝塞尔曲线 + } + drawingContext.DrawGeometry(null, visualPen, streamGeometry); + + } + + + + private void DrawSemicircleCurve(DrawingContext drawingContext, Point start, Point end) + { + // 计算中心点和半径 + // 计算圆心和半径 + double x = 35; + // 创建一个弧线路径 + streamGeometry.Clear(); + using (var context = streamGeometry.Open()) + { + // 开始绘制 + context.BeginFigure(start, false, false); + + // 生成弧线 + context.ArcTo( + end, // 结束点 + new Size(x, x), // 椭圆的半径 + 0, // 椭圆的旋转角度 + false, // 是否大弧 + SweepDirection.Counterclockwise, // 方向 + true, // 是否连接到起始点 + true // 是否使用高质量渲染 + ); + + // 结束绘制 + context.LineTo(start, false, false); // 连接到起始点(可选) + } + + // 绘制弧线 + drawingContext.DrawGeometry(null, visualPen, streamGeometry); + + } + #endregion + } + + +} diff --git a/Workbench/Node/Junction/JunctionControlBase.cs b/Workbench/Node/Junction/JunctionControlBase.cs new file mode 100644 index 0000000..5ce870c --- /dev/null +++ b/Workbench/Node/Junction/JunctionControlBase.cs @@ -0,0 +1,378 @@ +using Serein.Library; +using Serein.Library.Utils; +using System; +using System.Net; +using System.Reflection; +using System.Windows; +using Serein.Workbench.Extension; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Shapes; +using System.Windows.Media.Media3D; +using System.Windows.Documents; +using System.Threading; + +namespace Serein.Workbench.Node.View +{ + internal static class MyUIFunc + { + public static Pen CreateAndFreezePen() + { + // 创建Pen + Pen pen = new Pen(Brushes.Black, 1); + + // 冻结Pen + if (pen.CanFreeze) + { + pen.Freeze(); + } + return pen; + } + } + + public class ParamsArgControl: Shape + { + + + public ParamsArgControl() + { + this.MouseDown += ParamsArg_OnMouseDown; // 增加或删除 + this.MouseMove += ParamsArgControl_MouseMove; + this.MouseLeave += ParamsArgControl_MouseLeave; + AddOrRemoveParamsTask = AddAsync; + } + + + + protected readonly StreamGeometry StreamGeometry = new StreamGeometry(); + protected override Geometry DefiningGeometry => StreamGeometry; + + + #region 控件属性,所在的节点 + public static readonly DependencyProperty NodeProperty = + DependencyProperty.Register(nameof(MyNode), typeof(NodeModelBase), typeof(ParamsArgControl), new PropertyMetadata(default(NodeModelBase))); + //public NodeModelBase NodeModel; + + /// + /// 所在的节点 + /// + public NodeModelBase MyNode + { + get { return (NodeModelBase)GetValue(NodeProperty); } + set { SetValue(NodeProperty, value); } + } + #endregion + + #region 控件属性,连接器类型 + public static readonly DependencyProperty ArgIndexProperty = + DependencyProperty.Register(nameof(ArgIndex), typeof(int), typeof(ParamsArgControl), new PropertyMetadata(default(int))); + + /// + /// 参数的索引 + /// + public int ArgIndex + { + get { return (int)GetValue(ArgIndexProperty); } + set { SetValue(ArgIndexProperty, value.ToString()); } + } + #endregion + + + /// + /// 控件重绘事件 + /// + /// + protected override void OnRender(DrawingContext drawingContext) + { + Brush brush = isMouseOver ? Brushes.Red : Brushes.Green; + double height = ActualHeight; + // 定义圆形的大小和位置 + double connectorSize = 10; // 连接器的大小 + double circleCenterX = 8; // 圆心 X 坐标 + double circleCenterY = height / 2; // 圆心 Y 坐标 + var circlePoint = new Point(circleCenterX, circleCenterY); + + // 圆形部分 + var ellipse = new EllipseGeometry(circlePoint, connectorSize / 2, connectorSize / 2); + + drawingContext.DrawGeometry(brush, MyUIFunc.CreateAndFreezePen(), ellipse); + } + + + private bool isMouseOver; // 鼠标悬停状态 + + private Func AddOrRemoveParamsTask; // 增加或删除参数 + + public async void ParamsArg_OnMouseDown(object sender, MouseButtonEventArgs e) + { + await AddOrRemoveParamsTask.Invoke(); + } + + private void ParamsArgControl_MouseMove(object sender, MouseEventArgs e) + { + isMouseOver = true; + if (cancellationTokenSource.IsCancellationRequested) { + cancellationTokenSource = new CancellationTokenSource(); + Task.Run(async () => + { + await Task.Delay(380); + + }, cancellationTokenSource.Token).ContinueWith((t) => + { + // 如果焦点仍在控件上时,则改变点击事件 + if (isMouseOver) + { + AddOrRemoveParamsTask = RemoveAsync; + this.Dispatcher.Invoke(InvalidateVisual);// 触发一次重绘 + + } + }); + } + + } + private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + + + private void ParamsArgControl_MouseLeave(object sender, MouseEventArgs e) + { + isMouseOver = false; + AddOrRemoveParamsTask = AddAsync; // 鼠标焦点离开时恢复点击事件 + cancellationTokenSource?.Cancel(); + this.Dispatcher.Invoke(InvalidateVisual);// 触发一次重绘 + + } + + + private async Task AddAsync() + { + await this.MyNode.Env.ChangeParameter(MyNode.Guid, true, ArgIndex); + } + private async Task RemoveAsync() + { + await this.MyNode.Env.ChangeParameter(MyNode.Guid, false, ArgIndex); + } + + } + + + + public abstract class JunctionControlBase : Shape + { + protected JunctionControlBase() + { + this.Width = 25; + this.Height = 20; + this.MouseDown += JunctionControlBase_MouseDown; + this.MouseMove += JunctionControlBase_MouseMove; + this.MouseLeave += JunctionControlBase_MouseLeave; ; + } + + + #region 控件属性,所在的节点 + public static readonly DependencyProperty NodeProperty = + DependencyProperty.Register(nameof(MyNode), typeof(NodeModelBase), typeof(JunctionControlBase), new PropertyMetadata(default(NodeModelBase))); + //public NodeModelBase NodeModel; + + /// + /// 所在的节点 + /// + public NodeModelBase MyNode + { + get { return (NodeModelBase)GetValue(NodeProperty); } + set { SetValue(NodeProperty, value); } + } + #endregion + + #region 控件属性,连接器类型 + public static readonly DependencyProperty JunctionTypeProperty = + DependencyProperty.Register(nameof(JunctionType), typeof(string), typeof(JunctionControlBase), new PropertyMetadata(default(string))); + + /// + /// 控制点类型 + /// + public JunctionType JunctionType + { + get { return EnumHelper.ConvertEnum(GetValue(JunctionTypeProperty).ToString()); } + set { SetValue(JunctionTypeProperty, value.ToString()); } + } + #endregion + + protected readonly StreamGeometry StreamGeometry = new StreamGeometry(); + protected override Geometry DefiningGeometry => StreamGeometry; + + /// + /// 重绘方法 + /// + /// + public abstract void Render(DrawingContext drawingContext); + /// + /// 中心点 + /// + public abstract Point MyCenterPoint { get; } + + + + /// + /// 禁止连接 + /// + private bool IsConnectionDisable; + + /// + /// 处理鼠标悬停状态 + /// + private bool _isMouseOver; + public bool IsMouseOver + { + get => _isMouseOver; + set + { + if(_isMouseOver != value) + { + GlobalJunctionData.MyGlobalConnectingData.CurrentJunction = this; + _isMouseOver = value; + InvalidateVisual(); + } + + } + } + + /// + /// 控件重绘事件 + /// + /// + protected override void OnRender(DrawingContext drawingContext) + { + Render(drawingContext); + } + + /// + /// 获取背景颜色 + /// + /// + protected Brush GetBackgrounp() + { + var myData = GlobalJunctionData.MyGlobalConnectingData; + if(!myData.IsCreateing) + { + return Brushes.Transparent; + } + if (IsMouseOver) + { + if (myData.IsCanConnected) + { + if (myData.Type == JunctionOfConnectionType.Invoke) + { + return myData.ConnectionInvokeType.ToLineColor(); + } + else + { + return myData.ConnectionArgSourceType.ToLineColor(); + } + } + else + { + return Brushes.Red; + } + } + else + { + return Brushes.Transparent; + } + } + + private object lockObj = new object(); + + /// + /// 控件获得鼠标焦点事件 + /// + /// + /// + private void JunctionControlBase_MouseMove(object sender, MouseEventArgs e) + { + //if (!GlobalJunctionData.MyGlobalConnectingData.IsCreateing) return; + + //if (IsMouseOver) return; + IsMouseOver = true; + + //this.InvalidateVisual(); + } + + /// + /// 控件失去鼠标焦点事件 + /// + /// + /// + private void JunctionControlBase_MouseLeave(object sender, MouseEventArgs e) + { + IsMouseOver = false; + e.Handled = true; + + } + + + /// + /// 在碰撞点上按下鼠标控件开始进行移动 + /// + /// + /// + protected void JunctionControlBase_MouseDown(object sender, MouseButtonEventArgs e) + { + if (e.LeftButton == MouseButtonState.Pressed) + { + var canvas = MainWindow.GetParentOfType(this); + if (canvas != null) + { + var myData = GlobalJunctionData.MyGlobalConnectingData; + myData.Reset(); + myData.IsCreateing = true; // 表示开始连接 + myData.StartJunction = this; + myData.CurrentJunction = this; + myData.StartPoint = this.TranslatePoint(new Point(this.Width / 2, this.Height / 2), canvas); + + var junctionOfConnectionType = this.JunctionType.ToConnectyionType(); + ConnectionLineShape bezierLine; // 类别 + Brush brushColor; // 临时线的颜色 + if (junctionOfConnectionType == JunctionOfConnectionType.Invoke) + { + brushColor = ConnectionInvokeType.IsSucceed.ToLineColor(); + } + else if(junctionOfConnectionType == JunctionOfConnectionType.Arg) + { + brushColor = ConnectionArgSourceType.GetOtherNodeData.ToLineColor(); + } + else + { + return; + } + bezierLine = new ConnectionLineShape(LineType.Bezier, + myData.StartPoint, + myData.StartPoint, + brushColor, + isTop: true); // 绘制临时的线 + + Mouse.OverrideCursor = Cursors.Cross; // 设置鼠标为正在创建连线 + myData.MyLine = new MyLine(canvas, bezierLine); + } + } + e.Handled = true; + } + + private Point GetStartPoint() + { + return new Point(this.ActualWidth / 2, this.ActualHeight / 2); // 起始节点选择右侧边缘中心 + } + + + + + + } + + + + + + + +} diff --git a/Workbench/Node/Junction/JunctionData.cs b/Workbench/Node/Junction/JunctionData.cs new file mode 100644 index 0000000..ceb3048 --- /dev/null +++ b/Workbench/Node/Junction/JunctionData.cs @@ -0,0 +1,161 @@ +using Serein.Library; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Shapes; + +namespace Serein.Workbench.Node.View +{ + + #region Model,不科学的全局变量 + public class MyLine + { + public MyLine(Canvas canvas, ConnectionLineShape line) + { + Canvas = canvas; + Line = line; + canvas?.Children.Add(line); + } + + public Canvas Canvas { get; set; } + public ConnectionLineShape Line { get; set; } + + public void Remove() + { + Canvas?.Children.Remove(Line); + } + } + + public class ConnectingData + { + + /// + /// 是否正在创建连线 + /// + public bool IsCreateing { get; set; } + /// + /// 起始控制点 + /// + public JunctionControlBase StartJunction { get; set; } + /// + /// 当前的控制点 + /// + public JunctionControlBase CurrentJunction { get; set; } + /// + /// 开始坐标 + /// + public Point StartPoint { get; set; } + /// + /// 线条样式 + /// + public MyLine MyLine { get; set; } + + /// + /// 线条类别(方法调用) + /// + public ConnectionInvokeType ConnectionInvokeType { get; set; } = ConnectionInvokeType.IsSucceed; + /// + /// 线条类别(参数传递) + /// + public ConnectionArgSourceType ConnectionArgSourceType { get; set; } = ConnectionArgSourceType.GetOtherNodeData; + + /// + /// 判断当前连接类型 + /// + public JunctionOfConnectionType Type => StartJunction.JunctionType.ToConnectyionType(); + + + /// + /// 是否允许连接 + /// + + public bool IsCanConnected { get + { + + if(StartJunction is null + || CurrentJunction is null + ) + { + return false; + } + + + if (!StartJunction.MyNode.Equals(CurrentJunction.MyNode) + && StartJunction.JunctionType.IsCanConnection(CurrentJunction.JunctionType)) + { + return true; + } + else + { + return false; + } + } + } + + /// + /// 更新临时的连接线 + /// + /// + public void UpdatePoint(Point point) + { + if (StartJunction is null + || CurrentJunction is null + ) + { + return; + } + if (StartJunction.JunctionType == Library.JunctionType.Execute + || StartJunction.JunctionType == Library.JunctionType.ArgData) + { + MyLine.Line.UpdateStartPoints(point); + } + else + { + MyLine.Line.UpdateEndPoints(point); + + } + } + + /// + /// 重置 + /// + public void Reset() + { + IsCreateing = false; + StartJunction = null; + CurrentJunction = null; + MyLine?.Remove(); + ConnectionInvokeType = ConnectionInvokeType.IsSucceed; + ConnectionArgSourceType = ConnectionArgSourceType.GetOtherNodeData; + } + + + + } + + public static class GlobalJunctionData + { + //private static ConnectingData? myGlobalData; + //private static object _lockObj = new object(); + + /// + /// 创建节点之间控制点的连接行为 + /// + public static ConnectingData MyGlobalConnectingData { get; } = new ConnectingData(); + + /// + /// 删除连接视觉效果 + /// + public static void OK() + { + MyGlobalConnectingData.Reset(); + } + } + #endregion +} diff --git a/Workbench/Node/Junction/NodeJunctionViewBase.cs b/Workbench/Node/Junction/NodeJunctionViewBase.cs new file mode 100644 index 0000000..92e6a59 --- /dev/null +++ b/Workbench/Node/Junction/NodeJunctionViewBase.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media; +using System.Windows; +using Serein.Workbench.Node.View; +using System.Windows.Controls; +using Serein.Library; +using System.Windows.Data; + +namespace Serein.Workbench.Node.View +{ + + + public abstract class NodeJunctionViewBase : ContentControl, IDisposable + { + public NodeJunctionViewBase() + { + var transfromGroup = new TransformGroup(); + transfromGroup.Children.Add(_Translate); + RenderTransform = transfromGroup; + } + + /// + /// 每个连接器都有一个唯一标识符(Guid),用于标识连接器。 + /// + public Guid Guid + { + get => (Guid)GetValue(GuidProperty); + set => SetValue(GuidProperty, value); + } + public static readonly DependencyProperty GuidProperty = DependencyProperty.Register( + nameof(Guid), + typeof(Guid), + typeof(NodeJunctionViewBase), // NodeConnectorContent + new PropertyMetadata(Guid.Empty)); + + /// + /// 连接器当前的连接数,表示有多少条 NodeLink 连接到此连接器。该属性为只读。 + /// + public int ConnectedCount + { + get => (int)GetValue(ConnectedCountProperty); + private set => SetValue(ConnectedCountPropertyKey, value); + } + public static readonly DependencyPropertyKey ConnectedCountPropertyKey = DependencyProperty.RegisterReadOnly( + nameof(ConnectedCount), + typeof(int), + typeof(NodeJunctionViewBase), // NodeConnectorContent + new PropertyMetadata(0)); + + public static readonly DependencyProperty ConnectedCountProperty = ConnectedCountPropertyKey.DependencyProperty; + + /// + /// 布尔值,指示此连接器是否有任何连接。 + /// + public bool IsConnected + { + get => (bool)GetValue(IsConnectedProperty); + private set => SetValue(IsConnectedPropertyKey, value); + } + public static readonly DependencyPropertyKey IsConnectedPropertyKey = DependencyProperty.RegisterReadOnly( + nameof(IsConnected), + typeof(bool), + typeof(NodeJunctionViewBase), // NodeConnectorContent + new PropertyMetadata(false)); + + public static readonly DependencyProperty IsConnectedProperty = IsConnectedPropertyKey.DependencyProperty; + + /// + /// 这些属性控制连接器的外观(颜色、边框厚度、填充颜色)。 + /// + public Brush Stroke + { + get => (Brush)GetValue(StrokeProperty); + set => SetValue(StrokeProperty, value); + } + public static readonly DependencyProperty StrokeProperty = DependencyProperty.Register( + nameof(Stroke), + typeof(Brush), + typeof(NodeJunctionViewBase), // NodeConnectorContent + new FrameworkPropertyMetadata(Brushes.Blue)); + + /// + /// 这些属性控制连接器的外观(颜色、边框厚度、填充颜色)。 + /// + public double StrokeThickness + { + get => (double)GetValue(StrokeThicknessProperty); + set => SetValue(StrokeThicknessProperty, value); + } + public static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register( + nameof(StrokeThickness), + typeof(double), + typeof(NodeJunctionViewBase), // NodeConnectorContent + new FrameworkPropertyMetadata(1.0)); + + /// + /// 这些属性控制连接器的外观(颜色、边框厚度、填充颜色)。 + /// + public Brush Fill + { + get => (Brush)GetValue(FillProperty); + set => SetValue(FillProperty, value); + } + public static readonly DependencyProperty FillProperty = DependencyProperty.Register( + nameof(Fill), + typeof(Brush), + typeof(NodeJunctionViewBase),// NodeConnectorContent + new FrameworkPropertyMetadata(Brushes.Gray)); + + /// + /// 指示该连接器是否可以与其他连接器进行连接。 + /// + public bool CanConnect + { + get => (bool)GetValue(CanConnectProperty); + set => SetValue(CanConnectProperty, value); + } + public static readonly DependencyProperty CanConnectProperty = DependencyProperty.Register( + nameof(CanConnect), + typeof(bool), + typeof(NodeJunctionViewBase),// NodeConnectorContent + new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsRender)); + + + private Point _Position = new Point(); + /// + /// 该连接器的当前坐标(位置)。 + /// + public Point Position + { + get => _Position; + set => UpdatePosition(value); + } + + /// + /// (重要数据)表示连接器所属的节点。 + /// + public NodeModelBase NodeModel { get; private set; } = null; + + /// + /// 该连接器所连接的所有 NodeLink 的集合。 + /// + public IEnumerable NodeLinks => _NodeLinks; + List _NodeLinks = new List(); + + protected abstract FrameworkElement ConnectorControl { get; } + TranslateTransform _Translate = new TranslateTransform(); + void UpdatePosition(Point pos) + { + _Position = pos; + _Translate.X = _Position.X; + _Translate.Y = _Position.Y; + + InvalidateVisual(); + } + + /// + /// 将 NodeLink 添加到连接器,并更新 ConnectedCount 和 IsConnected。 + /// + /// + public void Connect(ConnectionControl nodeLink) + { + _NodeLinks.Add(nodeLink); + ConnectedCount = _NodeLinks.Count; + IsConnected = ConnectedCount > 0; + } + + /// + /// 断开与某个 NodeLink 的连接,更新连接状态。 + /// + /// + public void Disconnect(ConnectionControl nodeLink) + { + _NodeLinks.Remove(nodeLink); + ConnectedCount = _NodeLinks.Count; + IsConnected = ConnectedCount > 0; + } + + /// + /// 获取连接器相对于指定 Canvas 的位置。 + /// + /// + /// + /// + /// + public Point GetContentPosition(Canvas canvas, double xScaleOffset = 0.5, double yScaleOffset = 0.5) + { + // it will be shifted Control position if not called UpdateLayout(). + ConnectorControl.UpdateLayout(); + var transformer = ConnectorControl.TransformToVisual(canvas); + + var x = ConnectorControl.ActualWidth * xScaleOffset; + var y = ConnectorControl.ActualHeight * yScaleOffset; + return transformer.Transform(new Point(x, y)); + } + + /// + /// 更新与此连接器相连的所有 NodeLink 的位置。这个方法是抽象的,要求子类实现。 + /// + /// + public abstract void UpdateLinkPosition(Canvas canvas); + + /// + /// 用于检查此连接器是否可以与另一个连接器相连接,要求子类实现。 + /// + /// + /// + public abstract bool CanConnectTo(NodeJunctionViewBase connector); + + /// + /// 释放连接器相关的资源,包括样式、绑定和已连接的 NodeLink + /// + public void Dispose() + { + // You need to clear Style. + // Because implemented on style for binding. + Style = null; + + // Clear binding for subscribing source changed event from old control. + // throw exception about visual tree ancestor different if you not clear binding. + BindingOperations.ClearAllBindings(this); + + var nodeLinks = _NodeLinks.ToArray(); + + // it must instance to nodeLinks because change node link collection in NodeLink Dispose. + foreach (var nodeLink in nodeLinks) + { + // nodeLink.Dispose(); + } + } + + } +} diff --git a/Workbench/Node/Junction/View/ArgJunctionControl.cs b/Workbench/Node/Junction/View/ArgJunctionControl.cs new file mode 100644 index 0000000..6ef6927 --- /dev/null +++ b/Workbench/Node/Junction/View/ArgJunctionControl.cs @@ -0,0 +1,74 @@ +using System.Windows; +using System.Windows.Media; +using System.Windows.Shapes; +using Serein.Library; + +namespace Serein.Workbench.Node.View +{ + public class ArgJunctionControl : JunctionControlBase + { + public ArgJunctionControl() + { + base.JunctionType = JunctionType.ArgData; + this.InvalidateVisual(); + } + + #region 控件属性,对应的参数 + public static readonly DependencyProperty ArgIndexProperty = + DependencyProperty.Register("ArgIndex", typeof(int), typeof(ArgJunctionControl), new PropertyMetadata(default(int))); + + /// + /// 所在的节点 + /// + public int ArgIndex + { + get { return (int)GetValue(ArgIndexProperty); } + set { SetValue(ArgIndexProperty, value); } + } + + + #endregion + private Point _myCenterPoint; + public override Point MyCenterPoint { get => _myCenterPoint; } + + + public override void Render(DrawingContext drawingContext) + { + double width = ActualWidth; + double height = ActualHeight; + var background = GetBackgrounp(); + // 输入连接器的背景 + var connectorRect = new Rect(0, 0, width, height); + drawingContext.DrawRectangle(Brushes.Transparent, null, connectorRect); + + // 定义圆形的大小和位置 + double connectorSize = 10; // 连接器的大小 + double circleCenterX = 8; // 圆心 X 坐标 + double circleCenterY = height / 2; // 圆心 Y 坐标 + var circlePoint = new Point(circleCenterX, circleCenterY); + _myCenterPoint = new Point(circleCenterX - connectorSize / 2, circleCenterY); // 中心坐标 + + // 绘制连接器的圆形部分 + var ellipse = new EllipseGeometry(circlePoint, connectorSize / 2, connectorSize / 2); + drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), ellipse); + + // 定义三角形的间距 + double triangleOffsetX = 4; // 三角形与圆形的间距 + double triangleCenterX = circleCenterX + connectorSize / 2 + triangleOffsetX; // 三角形中心 X 坐标 + double triangleCenterY = circleCenterY; // 三角形中心 Y 坐标 + + // 绘制三角形 + var pathGeometry = new StreamGeometry(); + using (var context = pathGeometry.Open()) + { + context.BeginFigure(new Point(triangleCenterX, triangleCenterY - 4.5), true, true); + context.LineTo(new Point(triangleCenterX + 5, triangleCenterY), true, false); + context.LineTo(new Point(triangleCenterX, triangleCenterY + 4.5), true, false); + context.LineTo(new Point(triangleCenterX, triangleCenterY - 4.5), true, false); + } + drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), pathGeometry); + } + } + + +} diff --git a/Workbench/Node/Junction/View/ExecuteJunctionControl.cs b/Workbench/Node/Junction/View/ExecuteJunctionControl.cs new file mode 100644 index 0000000..9247df1 --- /dev/null +++ b/Workbench/Node/Junction/View/ExecuteJunctionControl.cs @@ -0,0 +1,84 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Shapes; +using Serein.Library; + +namespace Serein.Workbench.Node.View +{ + public class ExecuteJunctionControl : JunctionControlBase + { + + + + public ExecuteJunctionControl() + { + base.JunctionType = JunctionType.Execute; + this.InvalidateVisual(); + + } + private Point _myCenterPoint; + public override Point MyCenterPoint { get => _myCenterPoint; } + public override void Render(DrawingContext drawingContext) + { + double width = ActualWidth; + double height = ActualHeight; + var background = GetBackgrounp(); + // 绘制边框 + //var borderBrush = new SolidColorBrush(Colors.Black); + //var borderThickness = 1.0; + //var borderRect = new Rect(0, 0, width, height); + //drawingContext.DrawRectangle(null, new Pen(borderBrush, borderThickness), borderRect); + + // 输入连接器的背景 + var connectorRect = new Rect(0, 0, width, height); + drawingContext.DrawRectangle(Brushes.Transparent,null, connectorRect); + //drawingContext.DrawRectangle(Brushes.Transparent, new Pen(background,2), connectorRect); + + // 定义圆形的大小和位置 + double connectorSize = 10; // 连接器的大小 + double circleCenterX = 8; // 圆心 X 坐标 + double circleCenterY = height / 2; // 圆心 Y 坐标 + _myCenterPoint = new Point(circleCenterX - connectorSize / 2, circleCenterY); // 中心坐标 + + var circlePoint = new Point(circleCenterX, circleCenterY); + // 绘制连接器的圆形部分 + var ellipse = new EllipseGeometry(circlePoint, connectorSize / 2, connectorSize / 2); + + + drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), ellipse); + + + + + // 定义三角形的间距 + double triangleOffsetX = 4; // 三角形与圆形的间距 + double triangleCenterX = circleCenterX + connectorSize / 2 + triangleOffsetX; // 三角形中心 X 坐标 + double triangleCenterY = circleCenterY; // 三角形中心 Y 坐标 + + // 绘制三角形 + var pathGeometry = new StreamGeometry(); + using (var context = pathGeometry.Open()) + { + context.BeginFigure(new Point(triangleCenterX, triangleCenterY - 4.5), true, true); + context.LineTo(new Point(triangleCenterX + 5, triangleCenterY), true, false); + context.LineTo(new Point(triangleCenterX, triangleCenterY + 4.5), true, false); + context.LineTo(new Point(triangleCenterX, triangleCenterY - 4.5), true, false); + } + drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), pathGeometry); + + // 绘制标签 + //var formattedText = new FormattedText( + // "执行", + // System.Globalization.CultureInfo.CurrentCulture, + // FlowDirection.LeftToRight, + // new Typeface("Segoe UI"), + // 12, + // Brushes.Black, + // VisualTreeHelper.GetDpi(this).PixelsPerDip); + //drawingContext.DrawText(formattedText, new Point(18,1)); + } + } + + +} diff --git a/Workbench/Node/Junction/View/NextStepJunctionControl.cs b/Workbench/Node/Junction/View/NextStepJunctionControl.cs new file mode 100644 index 0000000..7c5bde9 --- /dev/null +++ b/Workbench/Node/Junction/View/NextStepJunctionControl.cs @@ -0,0 +1,60 @@ +using System.Windows; +using System.Windows.Media; +using System.Windows.Shapes; +using Serein.Library; + +namespace Serein.Workbench.Node.View +{ + + public class NextStepJunctionControl : JunctionControlBase + { + //public override JunctionType JunctionType { get; } = JunctionType.NextStep; + public NextStepJunctionControl() + { + base.JunctionType = JunctionType.NextStep; + this.InvalidateVisual(); + } + private Point _myCenterPoint; + public override Point MyCenterPoint { get => _myCenterPoint; } + public override void Render(DrawingContext drawingContext) + { + double width = ActualWidth; + double height = ActualHeight; + var background = GetBackgrounp(); + // 输入连接器的背景 + var connectorRect = new Rect(0, 0, width, height); + drawingContext.DrawRectangle(Brushes.Transparent, null, connectorRect); + + // 定义圆形的大小和位置 + double connectorSize = 10; // 连接器的大小 + double circleCenterX = 8; // 圆心 X 坐标 + double circleCenterY = height / 2; // 圆心 Y 坐标 + _myCenterPoint = new Point(circleCenterX - connectorSize / 2, circleCenterY); // 中心坐标 + + var circlePoint = new Point(circleCenterX, circleCenterY); + // 绘制连接器的圆形部分 + var ellipse = new EllipseGeometry(circlePoint, connectorSize / 2, connectorSize / 2); + drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), ellipse); + + // 绘制连接器的圆形部分 + //var ellipse = new EllipseGeometry(circlePoint, connectorSize / 2, connectorSize / 2); + + + // 定义三角形的间距 + double triangleOffsetX = 4; // 三角形与圆形的间距 + double triangleCenterX = circleCenterX + connectorSize / 2 + triangleOffsetX; // 三角形中心 X 坐标 + double triangleCenterY = circleCenterY; // 三角形中心 Y 坐标 + + // 绘制三角形 + var pathGeometry = new StreamGeometry(); + using (var context = pathGeometry.Open()) + { + context.BeginFigure(new Point(triangleCenterX, triangleCenterY - 4.5), true, true); + context.LineTo(new Point(triangleCenterX + 5, triangleCenterY), true, false); + context.LineTo(new Point(triangleCenterX, triangleCenterY + 4.5), true, false); + context.LineTo(new Point(triangleCenterX, triangleCenterY - 4.5), true, false); + } + drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), pathGeometry); + } + } +} diff --git a/Workbench/Node/Junction/View/ResultJunctionControl.cs b/Workbench/Node/Junction/View/ResultJunctionControl.cs new file mode 100644 index 0000000..aaad0d3 --- /dev/null +++ b/Workbench/Node/Junction/View/ResultJunctionControl.cs @@ -0,0 +1,61 @@ +using System.Windows; +using System.Windows.Media; +using System.Windows.Shapes; +using Serein.Library; + +namespace Serein.Workbench.Node.View +{ + + public class ResultJunctionControl : JunctionControlBase + { + //public override JunctionType JunctionType { get; } = JunctionType.ReturnData; + + public ResultJunctionControl() + { + base.JunctionType = JunctionType.ReturnData; + this.InvalidateVisual(); + } + private Point _myCenterPoint; + public override Point MyCenterPoint { get => _myCenterPoint; } + + public override void Render(DrawingContext drawingContext) + { + double width = ActualWidth; + double height = ActualHeight; + + // 输入连接器的背景 + var connectorRect = new Rect(0, 0, width, height); + drawingContext.DrawRectangle(Brushes.Transparent, null, connectorRect); + + var background = GetBackgrounp(); + + // 定义圆形的大小和位置 + double connectorSize = 10; // 连接器的大小 + double circleCenterX = 8; // 圆心 X 坐标 + double circleCenterY = height / 2; // 圆心 Y 坐标 + var circlePoint = new Point(circleCenterX, circleCenterY); + + _myCenterPoint = new Point(circleCenterX - connectorSize / 2 , circleCenterY); // 中心坐标 + + // 绘制连接器的圆形部分 + var ellipse = new EllipseGeometry(circlePoint, connectorSize / 2, connectorSize / 2); + drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), ellipse); + + // 定义三角形的间距 + double triangleOffsetX = 4; // 三角形与圆形的间距 + double triangleCenterX = circleCenterX + connectorSize / 2 + triangleOffsetX; // 三角形中心 X 坐标 + double triangleCenterY = circleCenterY; // 三角形中心 Y 坐标 + + // 绘制三角形 + var pathGeometry = new StreamGeometry(); + using (var context = pathGeometry.Open()) + { + context.BeginFigure(new Point(triangleCenterX, triangleCenterY - 4.5), true, true); + context.LineTo(new Point(triangleCenterX + 5, triangleCenterY), true, false); + context.LineTo(new Point(triangleCenterX, triangleCenterY + 4.5), true, false); + context.LineTo(new Point(triangleCenterX, triangleCenterY - 4.5), true, false); + } + drawingContext.DrawGeometry(background, MyUIFunc.CreateAndFreezePen(), pathGeometry); + } + } +} diff --git a/Workbench/Node/NodeControlBase.cs b/Workbench/Node/NodeControlBase.cs new file mode 100644 index 0000000..2487cfb --- /dev/null +++ b/Workbench/Node/NodeControlBase.cs @@ -0,0 +1,198 @@ +using Serein.Library; +using Serein.Library.Api; +using Serein.Workbench.Node.ViewModel; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Media; + +namespace Serein.Workbench.Node.View +{ + + /// + /// 节点控件基类(控件) + /// + public abstract class NodeControlBase : UserControl, IDynamicFlowNode + { + /// + /// 节点所在的画布(以后需要将画布封装出来,实现多画布的功能) + /// + public Canvas NodeCanvas { get; set; } + + private INodeContainerControl nodeContainerControl; + /// + /// 如果该节点放置在了某个容器节点,就会记录这个容器节点 + /// + private INodeContainerControl NodeContainerControl { get; } + + /// + /// 记录与该节点控件有关的所有连接 + /// + private readonly List connectionControls = new List(); + + public NodeControlViewModelBase ViewModel { get; set; } + + + protected NodeControlBase() + { + this.Background = Brushes.Transparent; + } + + protected NodeControlBase(NodeControlViewModelBase viewModelBase) + { + ViewModel = viewModelBase; + this.Background = Brushes.Transparent; + this.DataContext = viewModelBase; + SetBinding(); + } + + /// + /// 放置在某个节点容器中 + /// + public void PlaceToContainer(INodeContainerControl nodeContainerControl) + { + this.nodeContainerControl = nodeContainerControl; + NodeCanvas.Children.Remove(this); // 临时从画布上移除 + var result = nodeContainerControl.PlaceNode(this); + if (!result) // 检查是否放置成功,如果不成功,需要重新添加回来 + { + NodeCanvas.Children.Add(this); // 从画布上移除 + + } + } + + /// + /// 从某个节点容器取出 + /// + public void TakeOutContainer() + { + var result = nodeContainerControl.TakeOutNode(this); // 从控件取出 + if (result) // 移除成功时才添加到画布上 + { + NodeCanvas.Children.Add(this); // 重新添加到画布上 + if (nodeContainerControl is NodeControlBase containerControl) + { + this.ViewModel.NodeModel.Position.X = containerControl.ViewModel.NodeModel.Position.X + containerControl.Width + 10; + this.ViewModel.NodeModel.Position.Y = containerControl.ViewModel.NodeModel.Position.Y; + } + } + + } + + /// + /// 添加与该节点有关的连接后,记录下来 + /// + /// + public void AddCnnection(ConnectionControl connection) + { + connectionControls.Add(connection); + } + + /// + /// 删除了连接之后,还需要从节点中的记录移除 + /// + /// + public void RemoveConnection(ConnectionControl connection) + { + connectionControls.Remove(connection); + connection.Remote(); + } + + /// + /// 删除所有连接 + /// + public void RemoveAllConection() + { + foreach (var connection in this.connectionControls) + { + connection.Remote(); + } + } + + /// + /// 更新与该节点有关的数据 + /// + public void UpdateLocationConnections() + { + foreach (var connection in this.connectionControls) + { + connection.RefreshLine(); // 主动更新连线位置 + } + } + + + /// + /// 设置绑定: + /// Canvas.X and Y : 画布位置 + /// + public void SetBinding() + { + // 绑定 Canvas.Left + Binding leftBinding = new Binding("X") + { + Source = ViewModel.NodeModel.Position, // 如果 X 属性在当前 DataContext 中 + Mode = BindingMode.TwoWay + }; + BindingOperations.SetBinding(this, Canvas.LeftProperty, leftBinding); + + // 绑定 Canvas.Top + Binding topBinding = new Binding("Y") + { + Source = ViewModel.NodeModel.Position, // 如果 Y 属性在当前 DataContext 中 + Mode = BindingMode.TwoWay + }; + BindingOperations.SetBinding(this, Canvas.TopProperty, topBinding); + } + + /// + /// 穿透视觉树获取指定类型的第一个元素 + /// + /// + /// + /// + protected T FindVisualChild(DependencyObject parent) where T : DependencyObject + { + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + if (child is T typedChild) + { + return typedChild; + } + + var childOfChild = FindVisualChild(child); + if (childOfChild != null) + { + return childOfChild; + } + } + return null; + } + + + + } + + + + + //public class FLowNodeObObservableCollection : ObservableCollection + //{ + + // public void AddRange(IEnumerable items) + // { + // foreach (var item in items) + // { + // this.Items.Add(item); + // } + // OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add)); + // } + //} +} + + + + + + diff --git a/Workbench/Node/NodeControlViewModelBase.cs b/Workbench/Node/NodeControlViewModelBase.cs new file mode 100644 index 0000000..ed58ff9 --- /dev/null +++ b/Workbench/Node/NodeControlViewModelBase.cs @@ -0,0 +1,48 @@ +using System.ComponentModel; +using Serein.Library; +using System.Runtime.CompilerServices; +using System.Windows.Controls; +using System.Windows.Data; +using System; + +namespace Serein.Workbench.Node.ViewModel +{ + public abstract class NodeControlViewModelBase + { + ///// + ///// 对应的节点实体类 + ///// + public NodeModelBase NodeModel { get; } + + public NodeControlViewModelBase(NodeModelBase nodeModel) + { + NodeModel = nodeModel; + + } + + + private bool isInterrupt; + ///// + ///// 控制中断状态的视觉效果 + ///// + public bool IsInterrupt + { + get => NodeModel.DebugSetting.IsInterrupt; + set + { + NodeModel.DebugSetting.IsInterrupt = value; + OnPropertyChanged(); + } + } + + + + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + + } +} diff --git a/Workbench/Node/RelayCommand.cs b/Workbench/Node/RelayCommand.cs new file mode 100644 index 0000000..a19b531 --- /dev/null +++ b/Workbench/Node/RelayCommand.cs @@ -0,0 +1,26 @@ +using System.Windows.Input; + +namespace Serein.Workbench.Node +{ + + public class RelayCommand : ICommand + { + private readonly Action _execute; + private readonly Func _canExecute; + + public RelayCommand(Action execute, Func canExecute = null) + { + _execute = execute; + _canExecute = canExecute; + } + + public event EventHandler CanExecuteChanged; + + public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter); + + public void Execute(object parameter) => _execute(parameter); + + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + +} diff --git a/Workbench/Node/View/ActionNodeControl.xaml b/Workbench/Node/View/ActionNodeControl.xaml new file mode 100644 index 0000000..fb71a3f --- /dev/null +++ b/Workbench/Node/View/ActionNodeControl.xaml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Workbench/Node/View/ConditionNodeControl.xaml b/Workbench/Node/View/ConditionNodeControl.xaml new file mode 100644 index 0000000..f6f8c07 --- /dev/null +++ b/Workbench/Node/View/ConditionNodeControl.xaml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Workbench/Node/View/ConditionNodeControl.xaml.cs b/Workbench/Node/View/ConditionNodeControl.xaml.cs new file mode 100644 index 0000000..f1ce898 --- /dev/null +++ b/Workbench/Node/View/ConditionNodeControl.xaml.cs @@ -0,0 +1,58 @@ +using Serein.NodeFlow.Model; +using Serein.Workbench.Node.ViewModel; + +namespace Serein.Workbench.Node.View +{ + /// + /// ConditionNode.xaml 的交互逻辑 + /// + public partial class ConditionNodeControl : NodeControlBase, INodeJunction + { + public ConditionNodeControl() : base() + { + // 窗体初始化需要 + base.ViewModel = new ConditionNodeControlViewModel (new SingleConditionNode(null)); + DataContext = ViewModel; + InitializeComponent(); + } + + public ConditionNodeControl(ConditionNodeControlViewModel viewModel):base(viewModel) + { + DataContext = viewModel; + InitializeComponent(); + } + + /// + /// 入参控制点(可能有,可能没) + /// + JunctionControlBase INodeJunction.ExecuteJunction => this.ExecuteJunctionControl; + + /// + /// 下一个调用方法控制点(可能有,可能没) + /// + JunctionControlBase INodeJunction.NextStepJunction => this.NextStepJunctionControl; + + /// + /// 返回值控制点(可能有,可能没) + /// + JunctionControlBase INodeJunction.ReturnDataJunction => this.ResultJunctionControl; + + /// + /// 方法入参控制点(可能有,可能没) + /// + private JunctionControlBase[] argDataJunction; + /// + /// 方法入参控制点(可能有,可能没) + /// + JunctionControlBase[] INodeJunction.ArgDataJunction + { + get + { + argDataJunction = new JunctionControlBase[1]; + argDataJunction[0] = this.ArgJunctionControl; + return argDataJunction; + } + } + + } +} diff --git a/Workbench/Node/View/ConditionRegionControl.xaml b/Workbench/Node/View/ConditionRegionControl.xaml new file mode 100644 index 0000000..c7932cc --- /dev/null +++ b/Workbench/Node/View/ConditionRegionControl.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Workbench/Node/View/GlobalDataControl.xaml.cs b/Workbench/Node/View/GlobalDataControl.xaml.cs new file mode 100644 index 0000000..f3f4bf3 --- /dev/null +++ b/Workbench/Node/View/GlobalDataControl.xaml.cs @@ -0,0 +1,87 @@ +using Serein.NodeFlow.Model; +using Serein.Workbench.Node.ViewModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Serein.Workbench.Node.View +{ + /// + /// UserControl1.xaml 的交互逻辑 + /// + public partial class GlobalDataControl : NodeControlBase, INodeJunction, INodeContainerControl + { + public GlobalDataControl() : base() + { + // 窗体初始化需要 + base.ViewModel = new GlobalDataNodeControlViewModel(new SingleGlobalDataNode(null)); + DataContext = ViewModel; + InitializeComponent(); + } + + public GlobalDataControl(GlobalDataNodeControlViewModel viewModel) : base(viewModel) + { + DataContext = viewModel; + InitializeComponent(); + } + + + /// + /// 入参控制点(可能有,可能没) + /// + JunctionControlBase INodeJunction.ExecuteJunction => this.ExecuteJunctionControl; + + /// + /// 下一个调用方法控制点(可能有,可能没) + /// + JunctionControlBase INodeJunction.NextStepJunction => this.NextStepJunctionControl; + + /// + /// 返回值控制点(可能有,可能没) + /// + JunctionControlBase INodeJunction.ReturnDataJunction => throw new NotImplementedException(); + + /// + /// 方法入参控制点(可能有,可能没) + /// + JunctionControlBase[] INodeJunction.ArgDataJunction => throw new NotImplementedException(); + + + public bool PlaceNode(NodeControlBase nodeControl) + { + if (GlobalDataPanel.Children.Contains(nodeControl)) + { + return false; + } + GlobalDataPanel.Children.Add(nodeControl); + return true; + } + + public bool TakeOutNode(NodeControlBase nodeControl) + { + if (!GlobalDataPanel.Children.Contains(nodeControl)) + { + return false; + } + GlobalDataPanel.Children.Remove(nodeControl); + return true; + } + + public void TakeOutAll() + { + GlobalDataPanel.Children.Clear(); + } + + } +} diff --git a/Workbench/Node/View/ScriptNodeControl.xaml b/Workbench/Node/View/ScriptNodeControl.xaml new file mode 100644 index 0000000..b7d0808 --- /dev/null +++ b/Workbench/Node/View/ScriptNodeControl.xaml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Workbench/Node/View/ScriptNodeControl.xaml.cs b/Workbench/Node/View/ScriptNodeControl.xaml.cs new file mode 100644 index 0000000..f16decf --- /dev/null +++ b/Workbench/Node/View/ScriptNodeControl.xaml.cs @@ -0,0 +1,162 @@ +using Serein.NodeFlow.Model; +using Serein.Workbench.Node.ViewModel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using System.Windows.Threading; + +namespace Serein.Workbench.Node.View +{ + /// + /// ScriptNodeControl.xaml 的交互逻辑 + /// + public partial class ScriptNodeControl : NodeControlBase , INodeJunction + { + private ScriptNodeControlViewModel viewModel => (ScriptNodeControlViewModel)ViewModel; + private DispatcherTimer _debounceTimer; // 用于延迟更新 + private bool _isUpdating = false; // 防止重复更新 + + public ScriptNodeControl() + { + InitializeComponent(); + } + public ScriptNodeControl(ScriptNodeControlViewModel viewModel) : base(viewModel) + { + DataContext = viewModel; + InitializeComponent(); + +#if false + // 初始化定时器 + _debounceTimer = new DispatcherTimer(); + _debounceTimer.Interval = TimeSpan.FromMilliseconds(500); // 停止输入 500ms 后更新 + _debounceTimer.Tick += DebounceTimer_Tick; +#endif + } + + + + + /// + /// 入参控制点(可能有,可能没) + /// + JunctionControlBase INodeJunction.ExecuteJunction => this.ExecuteJunctionControl; + + /// + /// 下一个调用方法控制点(可能有,可能没) + /// + JunctionControlBase INodeJunction.NextStepJunction => this.NextStepJunctionControl; + + /// + /// 返回值控制点(可能有,可能没) + /// + JunctionControlBase INodeJunction.ReturnDataJunction => this.ResultJunctionControl; + + /// + /// 方法入参控制点(可能有,可能没) + /// + JunctionControlBase[] INodeJunction.ArgDataJunction + { + get + { + // 获取 MethodDetailsControl 实例 + var methodDetailsControl = this.MethodDetailsControl; + var itemsControl = FindVisualChild(methodDetailsControl); // 查找 ItemsControl + if (itemsControl != null) + { + var argDataJunction = new JunctionControlBase[base.ViewModel.NodeModel.MethodDetails.ParameterDetailss.Length]; + var controls = new List(); + + for (int i = 0; i < itemsControl.Items.Count; i++) + { + var container = itemsControl.ItemContainerGenerator.ContainerFromIndex(i) as FrameworkElement; + if (container != null) + { + var argControl = FindVisualChild(container); + if (argControl != null) + { + controls.Add(argControl); // 收集 ArgJunctionControl 实例 + } + } + } + return argDataJunction = controls.ToArray(); + } + else + { + return []; + } + } + + + } + + + + + + + + + + + + + +#if false + // 每次输入时重置定时器 + private void RichTextBox_TextChanged(object sender, TextChangedEventArgs e) + { + _debounceTimer.Stop(); + _debounceTimer.Start(); + } + + // 定时器事件,用户停止输入后触发 + private async void DebounceTimer_Tick(object sender, EventArgs e) + { + _debounceTimer.Stop(); + + if (_isUpdating) + return; + + // 开始后台处理语法分析和高亮 + _isUpdating = true; + await Task.Run(() => HighlightKeywordsAsync(viewModel.Script)); + } + + // 异步执行语法高亮操作 + private async Task HighlightKeywordsAsync(string text) + { + if (string.IsNullOrEmpty(text)) + { + return; + } + // 模拟语法分析和高亮(可以替换为实际逻辑) + var highlightedText = text; + + // 在 UI 线程中更新 RichTextBox 的内容 + await Dispatcher.BeginInvoke(() => + { + var range = new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd); + range.Text = highlightedText; + }); + + _isUpdating = false; + } + +#endif + + + + + } +} diff --git a/Workbench/Node/ViewModel/ActionNodeControlViewModel.cs b/Workbench/Node/ViewModel/ActionNodeControlViewModel.cs new file mode 100644 index 0000000..8325690 --- /dev/null +++ b/Workbench/Node/ViewModel/ActionNodeControlViewModel.cs @@ -0,0 +1,13 @@ +using Serein.NodeFlow.Model; +using Serein.Workbench.Node.View; + +namespace Serein.Workbench.Node.ViewModel +{ + public class ActionNodeControlViewModel : NodeControlViewModelBase + { + public ActionNodeControlViewModel(SingleActionNode node) : base(node) + { + // this.NodelModel = node; + } + } +} diff --git a/Workbench/Node/ViewModel/ConditionNodeControlViewModel.cs b/Workbench/Node/ViewModel/ConditionNodeControlViewModel.cs new file mode 100644 index 0000000..81ab99d --- /dev/null +++ b/Workbench/Node/ViewModel/ConditionNodeControlViewModel.cs @@ -0,0 +1,54 @@ +using Serein.NodeFlow.Model; +using Serein.Workbench.Node.View; + +namespace Serein.Workbench.Node.ViewModel +{ + /// + /// 条件节点 + /// + public class ConditionNodeControlViewModel : NodeControlViewModelBase + { + public new SingleConditionNode NodeModel { get; } + + /// + /// 是否为自定义参数 + /// + public bool IsCustomData + { + get => NodeModel.IsExplicitData; + set { NodeModel.IsExplicitData = value; OnPropertyChanged(); } + } + /// + /// 自定义参数值 + /// + public string? CustomData + { + get => NodeModel.ExplicitData; + set { NodeModel.ExplicitData = value ; OnPropertyChanged(); } + } + /// + /// 表达式 + /// + public string Expression + { + get => NodeModel.Expression; + set { NodeModel.Expression = value; OnPropertyChanged(); } + } + + /// + /// 条件节点 + /// + /// + public ConditionNodeControlViewModel(SingleConditionNode node) : base(node) + { + this.NodeModel = node; + if(node is null) + { + IsCustomData = false; + CustomData = ""; + Expression = "PASS"; + } + } + + } +} diff --git a/Workbench/Node/ViewModel/ConditionRegionNodeControlViewModel.cs b/Workbench/Node/ViewModel/ConditionRegionNodeControlViewModel.cs new file mode 100644 index 0000000..5f86836 --- /dev/null +++ b/Workbench/Node/ViewModel/ConditionRegionNodeControlViewModel.cs @@ -0,0 +1,18 @@ +using Serein.NodeFlow.Model; +using Serein.Workbench.Node.View; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Serein.Workbench.Node.ViewModel +{ + public class ConditionRegionNodeControlViewModel : NodeControlViewModelBase + { + public ConditionRegionNodeControlViewModel(CompositeConditionNode node):base(node) + { + + } + } +} diff --git a/Workbench/Node/ViewModel/ExpOpNodeControlViewModel.cs b/Workbench/Node/ViewModel/ExpOpNodeControlViewModel.cs new file mode 100644 index 0000000..d9aaa08 --- /dev/null +++ b/Workbench/Node/ViewModel/ExpOpNodeControlViewModel.cs @@ -0,0 +1,26 @@ +using Serein.NodeFlow.Model; +using Serein.Workbench.Node.View; + +namespace Serein.Workbench.Node.ViewModel +{ + public class ExpOpNodeControlViewModel: NodeControlViewModelBase + { + public new SingleExpOpNode NodeModel { get; } + + //public string Expression + //{ + // get => node.Expression; + // set + // { + // node.Expression = value; + // OnPropertyChanged(); + // } + //} + + + public ExpOpNodeControlViewModel(SingleExpOpNode nodeModel) : base(nodeModel) + { + this.NodeModel = nodeModel; + } + } +} diff --git a/Workbench/Node/ViewModel/FlipflopNodeControlViewModel.cs b/Workbench/Node/ViewModel/FlipflopNodeControlViewModel.cs new file mode 100644 index 0000000..3ab4e2e --- /dev/null +++ b/Workbench/Node/ViewModel/FlipflopNodeControlViewModel.cs @@ -0,0 +1,14 @@ +using Serein.NodeFlow.Model; +using Serein.Workbench.Node.View; + +namespace Serein.Workbench.Node.ViewModel +{ + public class FlipflopNodeControlViewModel : NodeControlViewModelBase + { + public new SingleFlipflopNode NodelModel { get;} + public FlipflopNodeControlViewModel(SingleFlipflopNode node) : base(node) + { + this.NodelModel = node; + } + } +} diff --git a/Workbench/Node/ViewModel/GlobalDataNodeControlViewModel.cs b/Workbench/Node/ViewModel/GlobalDataNodeControlViewModel.cs new file mode 100644 index 0000000..a396cba --- /dev/null +++ b/Workbench/Node/ViewModel/GlobalDataNodeControlViewModel.cs @@ -0,0 +1,51 @@ +using Serein.Library; +using Serein.NodeFlow.Model; +using Serein.Workbench.Node.View; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; + +namespace Serein.Workbench.Node.ViewModel +{ + public class GlobalDataNodeControlViewModel : NodeControlViewModelBase + { + private SingleGlobalDataNode NodeModel => (SingleGlobalDataNode)base.NodeModel; + + /// + /// 复制全局数据表达式 + /// + public ICommand CommandCopyDataExp { get; } + + /// + /// 刷新数据 + /// + public ICommand CommandRefreshData { get; } + + + public GlobalDataNodeControlViewModel(SingleGlobalDataNode node) : base(node) + { + CommandCopyDataExp = new RelayCommand( o => + { + string exp = NodeModel.KeyName; + string copyValue = $"@Get #{exp}#"; + Clipboard.SetDataObject(copyValue); + }); + } + + /// + /// 自定义参数值 + /// + public string? KeyName + { + get => NodeModel?.KeyName; + set { NodeModel.KeyName = value; OnPropertyChanged(); } + } + + + + } +} diff --git a/Workbench/Node/ViewModel/ScriptNodeControlViewModel.cs b/Workbench/Node/ViewModel/ScriptNodeControlViewModel.cs new file mode 100644 index 0000000..6b03113 --- /dev/null +++ b/Workbench/Node/ViewModel/ScriptNodeControlViewModel.cs @@ -0,0 +1,62 @@ +using Serein.Library; +using Serein.Library.Core; +using Serein.Library.Utils; +using Serein.NodeFlow.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; + +namespace Serein.Workbench.Node.ViewModel +{ + public class ScriptNodeControlViewModel : NodeControlViewModelBase + { + private SingleScriptNode NodeModel => (SingleScriptNode)base.NodeModel; + + public string? Script + { + get => NodeModel?.Script; + set { NodeModel.Script = value; OnPropertyChanged(); } + } + + + + public ScriptNodeControlViewModel(NodeModelBase nodeModel) : base(nodeModel) + { + CommandExecuting = new RelayCommand(async o => + { + try + { + var result = await NodeModel.ExecutingAsync(new DynamicContext(nodeModel.Env)); + SereinEnv.WriteLine(InfoType.INFO, result?.ToString()); + } + catch (Exception ex) + { + SereinEnv.WriteLine(InfoType.ERROR, ex.ToString()); + } + }); + + CommandLoadScript = new RelayCommand( o => + { + NodeModel.ReloadScript(); + }); + } + + + /// + /// 加载脚本代码 + /// + public ICommand CommandLoadScript{ get; } + + /// + /// 尝试执行 + /// + public ICommand CommandExecuting { get; } + + + + } +} diff --git a/Workbench/Node/ViewModel/TypeToStringConverter.cs b/Workbench/Node/ViewModel/TypeToStringConverter.cs new file mode 100644 index 0000000..b80547c --- /dev/null +++ b/Workbench/Node/ViewModel/TypeToStringConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace Serein.Workbench.Node.ViewModel +{ + public class TypeToStringConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is Type type) + { + return type.ToString(); + } + return string.Empty; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/Workbench/Properties/launchSettings.json b/Workbench/Properties/launchSettings.json new file mode 100644 index 0000000..f8eb472 --- /dev/null +++ b/Workbench/Properties/launchSettings.json @@ -0,0 +1,7 @@ +{ + "profiles": { + "Serein.Workbench": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/Workbench/Serein.WorkBench.csproj b/Workbench/Serein.WorkBench.csproj new file mode 100644 index 0000000..ad7578f --- /dev/null +++ b/Workbench/Serein.WorkBench.csproj @@ -0,0 +1,74 @@ + + + + WinExe + net8.0-windows + enable + enable + True + D:\Project\C#\DynamicControl\SereinFlow\.Output + MIT + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Code + + + + diff --git a/Workbench/Serein.WorkBench_d2hd4tgu_wpftmp.csproj b/Workbench/Serein.WorkBench_d2hd4tgu_wpftmp.csproj new file mode 100644 index 0000000..c5a404c --- /dev/null +++ b/Workbench/Serein.WorkBench_d2hd4tgu_wpftmp.csproj @@ -0,0 +1,288 @@ + + + Serein.WorkBench + obj\Release\ + obj\ + D:\Project\C#\DynamicControl\SereinFlow\WorkBench\obj\ + <_TargetAssemblyProjectName>Serein.WorkBench + + + + WinExe + net8.0-windows + enable + enable + True + D:\Project\C#\DynamicControl\SereinFlow\.Output + MIT + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Workbench/Serein.Workbench_wjzi1sgn_wpftmp.csproj b/Workbench/Serein.Workbench_wjzi1sgn_wpftmp.csproj new file mode 100644 index 0000000..4810142 --- /dev/null +++ b/Workbench/Serein.Workbench_wjzi1sgn_wpftmp.csproj @@ -0,0 +1,292 @@ + + + Serein.Workbench + obj\Release\ + obj\ + D:\Project\C#\DynamicControl\SereinFlow\WorkBench\obj\ + <_TargetAssemblyProjectName>Serein.Workbench + Serein.Workbench + + + + WinExe + net8.0-windows + enable + enable + True + D:\Project\C#\DynamicControl\SereinFlow\.Output + MIT + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Workbench/Themes/BindableRichTextBox.cs b/Workbench/Themes/BindableRichTextBox.cs new file mode 100644 index 0000000..f7c9d1e --- /dev/null +++ b/Workbench/Themes/BindableRichTextBox.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Controls; +using System.Windows.Documents; +using System.Windows; + +namespace Serein.Workbench.Themes +{ + public partial class BindableRichTextBox : RichTextBox + { + public new FlowDocument Document + { + get { return (FlowDocument)GetValue(DocumentProperty); } + set { SetValue(DocumentProperty, value); } + } + // Using a DependencyProperty as the backing store for Document. This enables animation, styling, binding, etc... + public static readonly DependencyProperty DocumentProperty = + DependencyProperty.Register("Document", typeof(FlowDocument), typeof(BindableRichTextBox), new FrameworkPropertyMetadata(null, new PropertyChangedCallback(OnDucumentChanged))); + private static void OnDucumentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + RichTextBox rtb = (RichTextBox)d; + rtb.Document = (FlowDocument)e.NewValue; + } + } +} diff --git a/Workbench/Themes/IOCObjectViewControl.xaml b/Workbench/Themes/IOCObjectViewControl.xaml new file mode 100644 index 0000000..eba1b4b --- /dev/null +++ b/Workbench/Themes/IOCObjectViewControl.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + diff --git a/Workbench/Themes/IOCObjectViewControl.xaml.cs b/Workbench/Themes/IOCObjectViewControl.xaml.cs new file mode 100644 index 0000000..7d231d5 --- /dev/null +++ b/Workbench/Themes/IOCObjectViewControl.xaml.cs @@ -0,0 +1,128 @@ +using Serein.Library.Api; +using Serein.Library.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using System.Xml.Linq; + +namespace Serein.Workbench.Themes +{ + /// + /// IOCObjectViewControl.xaml 的交互逻辑 + /// + public partial class IOCObjectViewControl : UserControl + { + public Action SelectObj { get; set; } + + public IOCObjectViewControl() + { + InitializeComponent(); + } + + private class IOCObj + { + public string Key { get; set; } + public object Instance { get; set; } + } + + /// + /// 运行环境 + /// + public IFlowEnvironment FlowEnvironment { get; set; } + + /// + /// 添加一个实例 + /// + /// + /// + public void AddDependenciesInstance(string key,object instance) + { + IOCObj iOCObj = new IOCObj + { + Key = key, + Instance = instance, + }; + Application.Current.Dispatcher.Invoke(() => + { + TextBlock textBlock = new TextBlock(); + textBlock.Text = key; + textBlock.Tag = iOCObj; + textBlock.MouseDown += (s, e) => + { + if (s is TextBlock block && block.Tag is IOCObj iocObj) + { + SelectObj?.Invoke(iocObj.Key, iocObj.Instance); + //FlowEnvironment.SetMonitorObjState(iocObj.Instance, true); // 通知环境,该节点的数据更新后需要传到UI + } + }; + DependenciesListBox.Items.Add(textBlock); + SortLisbox(DependenciesListBox); + }); + + } + + /// + /// 刷新一个实例 + /// + /// + /// + public void RefreshDependenciesInstance(string key, object instance) + { + foreach (var item in DependenciesListBox.Items) + { + if (item is TextBlock block && block.Tag is IOCObj iocObj && iocObj.Key.Equals(key)) + { + iocObj.Instance = instance; + } + } + } + + public void ClearObjItem() + { + DependenciesListBox.Dispatcher.Invoke(() => + { + DependenciesListBox.Items.Clear(); + }); + + } + + private static void SortLisbox(ListBox listBox) + { + var sortedItems = listBox.Items.Cast().OrderBy(x => x.Text).ToList(); + listBox.Items.Clear(); + foreach (var item in sortedItems) + { + listBox.Items.Add(item); + } + } + + public void RemoveDependenciesInstance(string key) + { + object? itemControl = null; + foreach (var item in DependenciesListBox.Items) + { + if (item is TextBlock block && block.Tag is IOCObj iocObj && iocObj.Key.Equals(key)) + { + itemControl = item; + } + } + if (itemControl is not null) + { + DependenciesListBox.Items.Remove(itemControl); + } + } + + } +} diff --git a/Workbench/Themes/InputDialog.xaml b/Workbench/Themes/InputDialog.xaml new file mode 100644 index 0000000..d095e23 --- /dev/null +++ b/Workbench/Themes/InputDialog.xaml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/Workbench/Themes/WindowDialogInput.xaml.cs b/Workbench/Themes/WindowDialogInput.xaml.cs new file mode 100644 index 0000000..f5f5441 --- /dev/null +++ b/Workbench/Themes/WindowDialogInput.xaml.cs @@ -0,0 +1,70 @@ +using Serein.Library; +using Serein.Library.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; + +namespace Serein.Workbench.Themes +{ + /// + /// WindowDialogInput.xaml 的交互逻辑 + /// + public partial class WindowEnvRemoteLoginView : Window + { + private Action ConnectRemoteFlowEnv; + + /// + /// 弹窗输入 + /// + /// + public WindowEnvRemoteLoginView(Action connectRemoteFlowEnv) + { + WindowStartupLocation = WindowStartupLocation.CenterScreen; + InitializeComponent(); + ConnectRemoteFlowEnv = connectRemoteFlowEnv; + } + + private void ButtonTestConnect_Client(object sender, RoutedEventArgs e) + { + var addres = this.TextBlockAddres.Text; + _ = int.TryParse(this.TextBlockPort.Text, out var port); + _ = Task.Run(() => { + bool success = false; + try + { + TcpClient tcpClient = new TcpClient(); + var result = tcpClient.BeginConnect(addres, port, null, null); + success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(3)); + } + catch + { + success = false; + } + if (!success) + { + SereinEnv.WriteLine(InfoType.ERROR, $"无法连接远程:{addres}:{port}"); + } + }); + + } + + private void ButtonTestLoginEnv_Client(object sender, RoutedEventArgs e) + { + var addres = this.TextBlockAddres.Text; + _ = int.TryParse(this.TextBlockPort.Text, out var port); + var token = this.TextBlockToken.Text; + ConnectRemoteFlowEnv?.Invoke(addres, port, token); + } + } +} diff --git a/Workbench/Tool/Converters/InvertableBooleanToVisibilityConverter.cs b/Workbench/Tool/Converters/InvertableBooleanToVisibilityConverter.cs new file mode 100644 index 0000000..e830c6d --- /dev/null +++ b/Workbench/Tool/Converters/InvertableBooleanToVisibilityConverter.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; +using System.Windows; + +namespace Serein.Workbench.Tool.Converters +{ + /// + /// 根据bool类型控制可见性 + /// + [ValueConversion(typeof(bool), typeof(Visibility))] + public class InvertableBooleanToVisibilityConverter : IValueConverter + { + enum Parameters + { + Normal, Inverted + } + + public object Convert(object value, Type targetType, + object parameter, CultureInfo culture) + { + var boolValue = (bool)value; + var direction = (Parameters)Enum.Parse(typeof(Parameters), (string)parameter); + + if (direction == Parameters.Inverted) + return !boolValue ? Visibility.Visible : Visibility.Collapsed; + + return boolValue ? Visibility.Visible : Visibility.Collapsed; + } + + public object? ConvertBack(object value, Type targetType, + object parameter, CultureInfo culture) + { + return null; + } + } +} diff --git a/Workbench/Tool/Converters/ThumbPositionConverter.cs b/Workbench/Tool/Converters/ThumbPositionConverter.cs new file mode 100644 index 0000000..df6adbf --- /dev/null +++ b/Workbench/Tool/Converters/ThumbPositionConverter.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace Serein.Workbench.Tool.Converters +{ + /// + /// 画布拉动范围距离计算器 + /// + public class RightThumbPositionConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (value is double width) + return width - 10; // Adjust for Thumb width + return 0; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } + } + /// + /// 画布拉动范围距离计算器 + /// + public class BottomThumbPositionConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (value is double height) + return height - 10; // Adjust for Thumb height + return 0; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } + } + /// + /// 画布拉动范围距离计算器 + /// + public class VerticalCenterThumbPositionConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (value is double height) + return height / 2 - 5; // Centering Thumb vertically + return 0; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } + } + /// + /// 画布拉动范围距离计算器 + /// + public class HorizontalCenterThumbPositionConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (value is double width) + return width / 2 - 5; // Centering Thumb horizontally + return 0; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + throw new NotImplementedException(); + } + } + +} diff --git a/Workbench/Tool/Converters/TypeToColorConverter.cs b/Workbench/Tool/Converters/TypeToColorConverter.cs new file mode 100644 index 0000000..6d27425 --- /dev/null +++ b/Workbench/Tool/Converters/TypeToColorConverter.cs @@ -0,0 +1,26 @@ +using Serein.Library; +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; + +namespace Serein.Workbench.Tool.Converters +{ + /// + /// 根据控件类型切换颜色 + /// + public class TypeToColorConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + // 根据 ControlType 返回颜色 + return value switch + { + NodeControlType.Action => Brushes.Blue, + NodeControlType.Flipflop => Brushes.Green, + _ => Brushes.Black, + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); + } +} diff --git a/Workbench/Tool/GuidReplacer.cs b/Workbench/Tool/GuidReplacer.cs new file mode 100644 index 0000000..eb2cf77 --- /dev/null +++ b/Workbench/Tool/GuidReplacer.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Serein.Workbench.Tool +{ + /// + /// Guid替换工具类 + /// + public class GuidReplacer + { + private class TrieNode + { + public Dictionary Children = new(); + public string Replacement; // 替换后的值 + } + + private readonly TrieNode _root = new(); + + // 构建字典树 + public void AddReplacement(string guid, string replacement) + { + var current = _root; + foreach (var c in guid) + { + if (!current.Children.ContainsKey(c)) + { + current.Children[c] = new TrieNode(); + } + current = current.Children[c]; + } + current.Replacement = replacement; + } + + // 替换逻辑 + public string Replace(string input) + { + var result = new StringBuilder(); + var current = _root; + int i = 0; + + while (i < input.Length) + { + if (current.Children.ContainsKey(input[i])) + { + current = current.Children[input[i]]; + i++; + + if (current.Replacement != null) // 找到匹配 + { + result.Append(current.Replacement); + current = _root; // 回到根节点 + } + } + else + { + result.Append(input[i]); + current = _root; // 未匹配,回到根节点 + i++; + } + } + return result.ToString(); + } + } + +}