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 { /// /// 流程编辑器视图 /// 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); /// /// 物理屏幕坐标 → 视觉的 WPF 坐标。不能用 PointFromScreen:PerMonitorV2 下 /// net48 WPF 的 TransformToDevice 可能是陈旧的(换屏/改缩放后不刷新), /// 导致拖拽幽灵预览的移动速度和鼠标不一致。这里用 GetDpiForWindow 拿真实 DPI 自己换算。 /// 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; /// /// 平台子流程编辑器用:把右侧面板「节点信息」区整块替换为自定义内容(如视觉显示框)。 /// 传 null 恢复默认节点信息。仅影响本编辑器实例,主流程/组合模块编辑器不受影响。 /// 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; } /// /// 右侧面板顶部可选插槽内容(“节点信息”上面)。默认 null=折叠不可见; /// HALCON 子流程编辑器可注入一个显示窗体(如 HalconWindow)。主流程/组合模块不设置,保持折叠,零影响。 /// 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; } } /// /// 工具箱拖拽中:把预览节点画在画布上将要落下的位置。 /// 预览是画布子元素(DragPreviewNode),用 Canvas.Left/Top(画布内容坐标)定位, /// 与 OnDrop 落点、节点完全同一坐标系,不经过 adorner/窗口坐标换算。 /// 鼠标位置不能直接用 DragEventArgs.GetPosition:DragOver 事件在 PerMonitorV2 + /// 高 DPI 下换算因子错误(会把预览定位到画布可视区外);Mouse.GetPosition 在 OLE /// 拖放循环中不刷新。改用系统光标物理坐标 + GetDpiForWindow 手工换算成画布本地坐标。 /// 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 } }