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
}
}