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