| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463 |
- using System;
- using System.Linq;
- using System.Runtime.InteropServices;
- using System.Windows;
- using System.Windows.Controls;
- using System.Windows.Controls.Primitives;
- using System.Windows.Documents;
- using System.Windows.Input;
- using System.Windows.Media;
- using System.Windows.Media.Animation;
- using System.Windows.Shapes;
- using System.Windows.Threading;
- using System.Windows.Interop;
- using TeamAAS.FlowEditor.Controls;
- using TeamAAS.FlowEditor.Models;
- using TeamAAS.FlowEditor.Plugins;
- namespace TeamAAS.FlowEditor
- {
- /// <summary>
- /// 流程编辑器视图
- /// </summary>
- public partial class FlowEditorView : UserControl
- {
- [StructLayout(LayoutKind.Sequential)]
- private struct POINT { public int X, Y; }
- [DllImport("user32.dll")]
- private static extern bool GetCursorPos(out POINT lpPoint);
- [DllImport("user32.dll")]
- private static extern uint GetDpiForWindow(IntPtr hwnd);
- /// <summary>
- /// 物理屏幕坐标 → 视觉的 WPF 坐标。不能用 PointFromScreen:PerMonitorV2 下
- /// net48 WPF 的 TransformToDevice 可能是陈旧的(换屏/改缩放后不刷新),
- /// 导致拖拽幽灵预览的移动速度和鼠标不一致。这里用 GetDpiForWindow 拿真实 DPI 自己换算。
- /// </summary>
- private Point ScreenPointToVisual(Visual visual, Point pt)
- {
- var origin = visual.PointToScreen(new Point(0, 0)); // 视觉原点的物理屏幕坐标(与缩放因子无关)
- double scale = 1.0;
- var hwnd = (PresentationSource.FromVisual(visual) as HwndSource)?.Handle ?? IntPtr.Zero;
- if (hwnd != IntPtr.Zero)
- {
- try { scale = GetDpiForWindow(hwnd) / 96.0; } catch { }
- if (scale <= 0) scale = 1.0;
- }
- return new Point((pt.X - origin.X) / scale, (pt.Y - origin.Y) / scale);
- }
- private FlowEditorViewModel _vm;
- private Point _dragStart;
- private bool _isDragging;
- private bool _eventsHooked;
- private DispatcherTimer _scrollSyncTimer;
- private bool _isScrollingFromCode;
- public FlowEditorView()
- {
- InitializeComponent();
- Loaded += OnLoaded;
- DataContextChanged += OnDataContextChanged;
- }
- private void OnLoaded(object sender, RoutedEventArgs e)
- {
- _vm = DataContext as FlowEditorViewModel;
- if (_vm == null)
- {
- _vm = new FlowEditorViewModel();
- DataContext = _vm;
- }
- HookViewModel(_vm);
- EditorCanvas.Graph = _vm.Graph;
- SyncCanvasSize();
- InitCanvas();
- }
- private FlowEditorViewModel _hookedVm;
- private UIElement _defaultNodeInfo;
- /// <summary>
- /// 平台子流程编辑器用:把右侧面板「节点信息」区整块替换为自定义内容(如视觉显示框)。
- /// 传 null 恢复默认节点信息。仅影响本编辑器实例,主流程/组合模块编辑器不受影响。
- /// </summary>
- public void ReplaceNodeInfoPanel(UIElement content)
- {
- if (content == null)
- {
- if (_defaultNodeInfo != null) NodeInfoSlot.Content = _defaultNodeInfo;
- return;
- }
- if (_defaultNodeInfo == null) _defaultNodeInfo = NodeInfoSlot.Content as UIElement;
- NodeInfoSlot.Content = content;
- }
- private void HookViewModel(FlowEditorViewModel vm)
- {
- if (vm == null) return;
- // Unsubscribe from previous VM
- if (_hookedVm != null)
- _hookedVm.PropertyChanged -= OnHookedVmPropertyChanged;
- _hookedVm = vm;
- // Immediately sync button state for this VM
- SyncEditorState(vm);
- LoadingOverlay.Visibility = vm.IsLoading ? Visibility.Visible : Visibility.Collapsed;
- vm.PropertyChanged += OnHookedVmPropertyChanged;
- }
- private void SyncEditorState(FlowEditorViewModel vm)
- {
- // 只读监控模式:编辑器运行/停止按钮禁用,画布进入只读(可选中查看结果,禁改)
- BtnRun.IsEnabled = !vm.IsRunning && !vm.IsReadOnly;
- BtnStop.IsEnabled = vm.IsRunning && !vm.IsReadOnly;
- EditorCanvas.IsRunning = vm.IsRunning;
- EditorCanvas.IsReadOnly = vm.IsReadOnly;
- }
- private void OnHookedVmPropertyChanged(object s, System.ComponentModel.PropertyChangedEventArgs e)
- {
- if (_hookedVm == null) return;
- if (e.PropertyName == nameof(FlowEditorViewModel.IsRunning)
- || e.PropertyName == nameof(FlowEditorViewModel.IsReadOnly))
- {
- SyncEditorState(_hookedVm);
- }
- else if (e.PropertyName == nameof(FlowEditorViewModel.IsLoading))
- {
- LoadingOverlay.Visibility = _hookedVm.IsLoading ? Visibility.Visible : Visibility.Collapsed;
- }
- else if (e.PropertyName == nameof(FlowEditorViewModel.CanvasWidth))
- {
- EditorCanvas.Width = _hookedVm.CanvasWidth;
- }
- else if (e.PropertyName == nameof(FlowEditorViewModel.CanvasHeight))
- {
- EditorCanvas.Height = _hookedVm.CanvasHeight;
- }
- }
- private void InitCanvas()
- {
- if (!_eventsHooked)
- {
- _eventsHooked = true;
- EditorCanvas.NodeDropRequested += (x, y, info) => _vm.CreateNode(x, y, info);
- EditorCanvas.NodeDoubleClicked += node => _vm.OpenNodeEditor(node);
- EditorCanvas.NodeSelected += node => { var vm = DataContext as FlowEditorViewModel; if (vm != null) vm.SelectedNode = node; };
- EditorCanvas.SelectionCleared += () => { var vm = DataContext as FlowEditorViewModel; if (vm != null) vm.SelectedNode = null; };
- EditorCanvas.ZoomChanged += zoom =>
- TxtZoom.Text = $"{zoom * 100:F0}%";
- EditorCanvas.NodeRunRequested += async node => { if (_vm != null && !_vm.IsReadOnly) await _vm.RunSingleNodeAsync(node); };
- EditorCanvas.NodeRunFromHereRequested += async node => { if (_vm != null && !_vm.IsReadOnly) await _vm.RunFromNodeAsync(node); };
- EditorCanvas.NodePropertiesRequested += node => _vm?.ShowNodeProperties(node);
- // 工具箱拖拽预览:预览节点是画布子元素,直接画在将要落下的位置(与放置同一坐标系)
- EditorCanvas.DragOver += CanvasDragOver_PositionPreview;
- EditorCanvas.DragLeave += (s, e) => DragPreviewNode.Visibility = Visibility.Collapsed;
- _scrollSyncTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(60) };
- _scrollSyncTimer.Tick += SyncScrollBars;
- _scrollSyncTimer.Start();
- }
- //if (!_vm.IsSubFlowEditor && _vm.Graph.Nodes.Count == 0)
- //{
- // var infos = PluginLoader.GetAllPluginInfos();
- // if (infos.Count > 0)
- // {
- // string baseName = infos[0].DisplayName;
- // int suffix = 1;
- // while (_vm.Graph.Nodes.Any(n => n.NodeName == baseName))
- // baseName = $"{baseName}{++suffix}";
- // var node = FlowEditorViewModel.CreateNodeFromPlugin(baseName,_vm.Graph, infos[0], 200, 150);
- // _vm.Graph.AddNode(node);
- // }
- //}
- }
- private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
- {
- var newVm = e.NewValue as FlowEditorViewModel;
- if (newVm != null && newVm != _vm)
- {
- _vm = newVm;
- HookViewModel(_vm);
- EditorCanvas.Graph = _vm.Graph;
- SyncCanvasSize();
- }
- }
- private void SyncCanvasSize()
- {
- if (_vm == null || EditorCanvas == null) return;
- EditorCanvas.Width = _vm.CanvasWidth;
- EditorCanvas.Height = _vm.CanvasHeight;
- }
- /// <summary>
- /// 右侧面板顶部可选插槽内容(“节点信息”上面)。默认 null=折叠不可见;
- /// HALCON 子流程编辑器可注入一个显示窗体(如 HalconWindow)。主流程/组合模块不设置,保持折叠,零影响。
- /// </summary>
- public object RightTopContent
- {
- get => RightTopSlot.Content;
- set
- {
- RightTopSlot.Content = value;
- RightTopSlot.Visibility = value == null ? Visibility.Collapsed : Visibility.Visible;
- }
- }
- #region 滚动条同步
- private void SyncScrollBars(object sender, EventArgs e)
- {
- if (EditorCanvas == null) return;
- _isScrollingFromCode = true;
- var vp = EditorCanvas.ViewportSize;
- double canvasW = (EditorCanvas.Width > 0 ? EditorCanvas.Width : 5000) * EditorCanvas.Zoom;
- double canvasH = (EditorCanvas.Height > 0 ? EditorCanvas.Height : 3500) * EditorCanvas.Zoom;
- double vMax = Math.Max(0, canvasH - vp.Height);
- VScrollBar.Maximum = vMax;
- VScrollBar.ViewportSize = vp.Height;
- VScrollBar.Value = Math.Max(0, Math.Min(vMax, -EditorCanvas.TranslateY));
- VScrollBar.Visibility = vMax > 1 ? Visibility.Visible : Visibility.Collapsed;
- double hMax = Math.Max(0, canvasW - vp.Width);
- HScrollBar.Maximum = hMax;
- HScrollBar.ViewportSize = vp.Width;
- HScrollBar.Value = Math.Max(0, Math.Min(hMax, -EditorCanvas.TranslateX));
- HScrollBar.Visibility = hMax > 1 ? Visibility.Visible : Visibility.Collapsed;
- _isScrollingFromCode = false;
- }
- private void VScrollBar_Scroll(object sender, ScrollEventArgs e)
- {
- if (_isScrollingFromCode) return;
- EditorCanvas.SetTranslateY(-e.NewValue);
- }
- private void HScrollBar_Scroll(object sender, ScrollEventArgs e)
- {
- if (_isScrollingFromCode) return;
- EditorCanvas.SetTranslateX(-e.NewValue);
- }
- #endregion
- #region 工具箱拖拽
- // 预览节点是画布子元素(XAML: DragPreviewNode),用画布内容坐标 Canvas.Left/Top 定位,
- // 与落点(OnDrop)和节点完全同一坐标系,不经过 adorner/窗口坐标换算,高 DPI 下不会错位。
- private void ToolboxItem_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- _dragStart = e.GetPosition(null);
- _isDragging = false;
- }
- private void ToolboxItem_MouseMove(object sender, MouseEventArgs e)
- {
- if (e.LeftButton != MouseButtonState.Pressed) return;
- if (_isDragging) return;
- // 只读监控模式 / 流程运行中:禁止从工具箱拖入新插件
- if (_vm?.IsReadOnly == true || _vm?.IsRunning == true) return;
- var pos = e.GetPosition(null);
- var diff = _dragStart - pos;
- if (Math.Abs(diff.X) > SystemParameters.MinimumHorizontalDragDistance ||
- Math.Abs(diff.Y) > SystemParameters.MinimumVerticalDragDistance)
- {
- _isDragging = true;
- var pluginInfo = (sender as FrameworkElement)?.DataContext as NodePluginInfo;
- if (pluginInfo != null)
- {
- // 预览节点由 EditorCanvas.DragOver 驱动(CanvasDragOver_PositionPreview),
- // 与放置共用画布内容坐标系;DoDragDrop 返回(拖拽结束)后隐藏
- DragPreviewIcon.Kind = IconKindConverter.ToKind(pluginInfo.IconGeometry);
- DragPreviewText.Text = pluginInfo.DisplayName;
- DragPreviewNode.Visibility = Visibility.Collapsed;
- DragDrop.DoDragDrop((DependencyObject)sender, pluginInfo, DragDropEffects.Copy);
- DragPreviewNode.Visibility = Visibility.Collapsed;
- }
- _isDragging = false;
- }
- }
- /// <summary>
- /// 工具箱拖拽中:把预览节点画在画布上将要落下的位置。
- /// 预览是画布子元素(DragPreviewNode),用 Canvas.Left/Top(画布内容坐标)定位,
- /// 与 OnDrop 落点、节点完全同一坐标系,不经过 adorner/窗口坐标换算。
- /// 鼠标位置不能直接用 DragEventArgs.GetPosition:DragOver 事件在 PerMonitorV2 +
- /// 高 DPI 下换算因子错误(会把预览定位到画布可视区外);Mouse.GetPosition 在 OLE
- /// 拖放循环中不刷新。改用系统光标物理坐标 + GetDpiForWindow 手工换算成画布本地坐标。
- /// </summary>
- private void CanvasDragOver_PositionPreview(object sender, DragEventArgs e)
- {
- if (!e.Data.GetDataPresent(typeof(NodePluginInfo)) &&
- !e.Data.GetDataPresent(DataFormats.StringFormat)) return;
- GetCursorPos(out POINT pt);
- var pos = ScreenPointToVisual(EditorCanvas, new Point(pt.X, pt.Y));
- pos = new Point(pos.X / EditorCanvas.Zoom, pos.Y / EditorCanvas.Zoom);
- // 预览直接显示最终落点(含碰撞避让),所见即所得
- var dropX = pos.X - Controls.NodeControl.DefaultWidth / 2;
- var dropY = pos.Y - Controls.NodeControl.DefaultHeight / 2;
- var free = EditorCanvas.FindFreePosition(dropX, dropY);
- Canvas.SetLeft(DragPreviewNode, free.X);
- Canvas.SetTop(DragPreviewNode, free.Y);
- DragPreviewNode.Visibility = Visibility.Visible;
- }
- #endregion
- #region 工具栏
- private void ZoomIn_Click(object sender, RoutedEventArgs e)
- {
- // ZoomAt 接收画布本地坐标;ActualWidth/2 已是本地坐标,无需再经 ToCanvasPoint 换算
- var center = new Point(EditorCanvas.ActualWidth / 2, EditorCanvas.ActualHeight / 2);
- EditorCanvas.ZoomAt(center, EditorCanvas.Zoom * 1.2);
- }
- private void ZoomOut_Click(object sender, RoutedEventArgs e)
- {
- var center = new Point(EditorCanvas.ActualWidth / 2, EditorCanvas.ActualHeight / 2);
- EditorCanvas.ZoomAt(center, EditorCanvas.Zoom / 1.2);
- }
- private void FitView_Click(object sender, RoutedEventArgs e)
- {
- EditorCanvas.FitToContent();
- }
- #endregion
- #region 工具箱折叠/展开(图标模式)
- private bool _toolboxExpanded;
- private const double ToolboxExpandedWidth = 200;
- private const double ToolboxCollapsedWidth = 56;
- private void BtnToolboxToggle_Click(object sender, RoutedEventArgs e)
- {
- if (_toolboxExpanded)
- CollapseToolbox();
- else
- ExpandToolbox();
- }
- private void CollapseToolbox()
- {
- _toolboxExpanded = false;
- var widthAnim = new DoubleAnimation(ToolboxExpandedWidth, ToolboxCollapsedWidth,
- TimeSpan.FromMilliseconds(250))
- {
- EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut }
- };
- ToolboxPanel.BeginAnimation(FrameworkElement.WidthProperty, widthAnim);
- // 宽内容淡出 → 切换 → 窄内容淡入
- var fadeOut = new DoubleAnimation(1.0, 0.0, TimeSpan.FromMilliseconds(120));
- fadeOut.Completed += (s, args) =>
- {
- ToolboxExpanded.Visibility = Visibility.Hidden;
- ToolboxCollapsed.Visibility = Visibility.Visible;
- ToolboxCollapsed.Opacity = 0;
- var fadeIn = new DoubleAnimation(0.0, 1.0, TimeSpan.FromMilliseconds(130));
- ToolboxCollapsed.BeginAnimation(UIElement.OpacityProperty, fadeIn);
- };
- ToolboxExpanded.BeginAnimation(UIElement.OpacityProperty, fadeOut);
- }
- private void ExpandToolbox()
- {
- _toolboxExpanded = true;
- var widthAnim = new DoubleAnimation(ToolboxCollapsedWidth, ToolboxExpandedWidth,
- TimeSpan.FromMilliseconds(250))
- {
- EasingFunction = new CubicEase { EasingMode = EasingMode.EaseInOut }
- };
- ToolboxPanel.BeginAnimation(FrameworkElement.WidthProperty, widthAnim);
- // 窄内容淡出 → 切换 → 宽内容淡入
- var fadeOut = new DoubleAnimation(1.0, 0.0, TimeSpan.FromMilliseconds(120));
- fadeOut.Completed += (s, args) =>
- {
- ToolboxCollapsed.Visibility = Visibility.Hidden;
- ToolboxExpanded.Visibility = Visibility.Visible;
- ToolboxExpanded.Opacity = 0;
- var fadeIn = new DoubleAnimation(0.0, 1.0, TimeSpan.FromMilliseconds(130));
- ToolboxExpanded.BeginAnimation(UIElement.OpacityProperty, fadeIn);
- };
- ToolboxCollapsed.BeginAnimation(UIElement.OpacityProperty, fadeOut);
- }
- private readonly DispatcherTimer _popupCloseTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) };
- private void GroupTag_MouseEnter(object sender, MouseEventArgs e)
- {
- _popupCloseTimer.Stop();
- if (sender is Border border && border.DataContext is ToolboxGroup group)
- {
- PopupGroupName.Text = group.GroupName;
- PopupItems.ItemsSource = group.Items;
- ToolboxGroupPopup.PlacementTarget = border;
- ToolboxGroupPopup.IsOpen = true;
- }
- }
- private void GroupTag_MouseLeave(object sender, MouseEventArgs e)
- {
- _popupCloseTimer.Stop();
- _popupCloseTimer.Tick -= PopupCloseTimer_Tick;
- _popupCloseTimer.Tick += PopupCloseTimer_Tick;
- _popupCloseTimer.Start();
- }
- private void ToolboxGroupPopup_MouseEnter(object sender, MouseEventArgs e)
- {
- _popupCloseTimer.Stop();
- }
- private void ToolboxGroupPopup_MouseLeave(object sender, MouseEventArgs e)
- {
- _popupCloseTimer.Stop();
- _popupCloseTimer.Tick -= PopupCloseTimer_Tick;
- _popupCloseTimer.Tick += PopupCloseTimer_Tick;
- _popupCloseTimer.Start();
- }
- private void PopupCloseTimer_Tick(object sender, EventArgs e)
- {
- _popupCloseTimer.Stop();
- ToolboxGroupPopup.IsOpen = false;
- }
- #endregion
- #region 流程执行
- private async void RunFlow_Click(object sender, RoutedEventArgs e)
- {
- if (_vm == null || _vm.IsRunning || _vm.IsReadOnly) return;
- await _vm.RunFlowAsync();
- }
- private void StopFlow_Click(object sender, RoutedEventArgs e)
- {
- _vm?.StopFlow();
- }
- #endregion
- #region 属性编辑
- private void EditProperties_Click(object sender, RoutedEventArgs e)
- {
- _vm?.OpenNodeEditor(_vm?.SelectedNode);
- }
- #endregion
- #region Tab管理
- #endregion
- }
- }
|