using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using TeamAAS; using TeamAAS.FlowEditor; using TeamAAS.FlowEditor.Models; using TeamAAS.FlowEditor.Plugins; using TeamAAS.FlowEngine; using TeamAAS.Vision; using Plugins.Vm.Models; namespace Plugins.Vm.Views { /// /// VM(VisionMaster)流程编辑器视图(UserControl)- 承载在 PluginViewDialog 中, /// 与 HalconFlowEditorView 完全同构:左侧子流程画布(工具箱作用域=Vm,只显 Vm模块 算子), /// 右侧大显示窗 + 节点图像筛选 ComboBox(VisionMaster 风格)。 /// 实现 IPluginRunFeedback(窗体"执行"期间同步内嵌画布运行态)、IPluginViewTip(底部提示栏)。 /// /// 图像来源:订阅 VisionLayerStore 图层总线(VM 算子后续发布图层后自动跟随显示; /// VM 图像结果键目前经 VmWindow.RefreshImage 按平台对象类型解释,不支持的类型安全跳过)。 /// public partial class VmFlowEditorView : UserControl, IPluginViewSave, IPluginRunFeedback, IPluginViewTip { private FlowEditorViewModel _vm; private VmFlowModel _model; /// 图层筛选条目(SDK):NodeName=null=最新运行;LayerName=null=节点全部图层。 private readonly List _layerEntries = new List(); private bool _suppressComboSync; public VmFlowEditorView() { InitializeComponent(); // 视觉显示框注入子流程编辑器右侧面板(整区替换「节点信息」)——三个平台编辑器统一布局 EditorRoot.Children.Remove(VisionPanel); SubFlowEditor.ReplaceNodeInfoPanel(VisionPanel); VisionPanel.Visibility = Visibility.Visible; Loaded += OnLoaded; Unloaded += OnUnloaded; } private void OnLoaded(object sender, RoutedEventArgs e) { if (DataContext is VmFlowModel model) { _model = model; model.EnsureSubGraph(); TxtTitle.Text = model.ToolName ?? "VM流程"; // 关键:工具箱作用域=Vm → 只显示 IsSubFlowNode 的 Vm模块 算子,按 VisionPlugin 分组 _vm = new FlowEditorViewModel(model.SubGraph, isSubFlowEditor: true, toolboxScope: FlowToolboxScope.VisionVm); SubFlowEditor.DataContext = _vm; _vm.GraphDataChanged += _vm_GraphDataChanged; _vm.PropertyChanged += _vm_PropertyChanged; // 大显示窗(VM 原生渲染控件):独立面板随窗体尺寸自适应 var preview = new VmWindow(); preview.SetCaption("VM 显示"); DisplayHost.Content = preview; // 图像总线挂接:节点发布图像 → 重建筛选项 + 跟随显示 VisionLayerStore.Instance.LayerPublished += OnLayerPublished; VisionLayerStore.Instance.FlowCleared += OnFlowCleared; ResetLayerCombo(); // 让子节点的公式树能显示容器「输入」变量(如 &{输入.图像}) PluginLoader.Instance.ParentGroupModel = model; HookHoverHints(this); } } private void OnUnloaded(object sender, RoutedEventArgs e) { PluginLoader.Instance.SetGroupPluginModel = null; if (ReferenceEquals(PluginLoader.Instance.ParentGroupModel, _model)) PluginLoader.Instance.ParentGroupModel = null; VisionLayerStore.Instance.LayerPublished -= OnLayerPublished; VisionLayerStore.Instance.FlowCleared -= OnFlowCleared; DisplayHost.Content = null; } private void _vm_GraphDataChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { if (DataContext is VmFlowModel model) { if (sender is FlowGraph flowGraph) model.SubGraph = flowGraph; } } /// 画布点选节点 → 图像筛选自动切到该节点(点节点看该节点图)。 private void _vm_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e == null || e.PropertyName != "SelectedNode") return; var node = _vm?.SelectedNode; if (node == null || string.IsNullOrEmpty(node.NodeName)) return; var index = _layerEntries.FindIndex(x => x.NodeName == node.NodeName && x.LayerName == null); if (index < 0) index = 0; if (LayerCombo.SelectedIndex != index) { _suppressComboSync = true; LayerCombo.SelectedIndex = index; _suppressComboSync = false; } ApplyLayerSelection(); } #region 图层筛选与显示 /// 当前选中的条目;越界/未选返回 null。 private LayerComboEntry SelectedEntry() { var i = LayerCombo.SelectedIndex; if (i < 0 || i >= _layerEntries.Count) return null; return _layerEntries[i]; } private void ResetLayerCombo() { _layerEntries.Clear(); _layerEntries.Add(new LayerComboEntry { Display = "最新运行" }); _suppressComboSync = true; LayerCombo.ItemsSource = _layerEntries.Select(e => e.Display).ToList(); LayerCombo.SelectedIndex = 0; _suppressComboSync = false; RunStatusText.Text = "等待运行…(节点运行后自动显示其图像)"; } private static System.DateTime _lastMismatchLog = System.DateTime.MinValue; /// 图像发布回调:重建筛选项并按"值"保留选中;正在看该节点(或最新运行)时实时刷新显示。 /// 发布的流程 Id 与编辑器不一致时(旧流程文件图 Id 漂移)不再直接丢弃 —— 记录日志并按节点名兜底显示。 private void OnLayerPublished(LayerPublishArgs args) { if (args == null) return; var flowId = _model?.SubGraph?.GraphId; var matched = string.Equals(args.FlowId, flowId, System.StringComparison.OrdinalIgnoreCase); if (!matched && (System.DateTime.Now - _lastMismatchLog).TotalSeconds > 5) { _lastMismatchLog = System.DateTime.Now; TeamAAS.AppLogger.Warning($"图像发布流程Id与编辑器不一致: 发布={args.FlowId} 节点={args.NodeName} 编辑器期望={flowId}(已按节点名兜底显示)", "Vm编辑器"); } try { System.Windows.Application.Current?.Dispatcher.Invoke(() => { RebuildLayerEntries(); var current = SelectedEntry(); bool follow = current == null || current.NodeName == null || string.Equals(current.NodeName, args.NodeName, System.StringComparison.OrdinalIgnoreCase); if (follow || !matched) ApplyLayerSelection(); }); } catch { } } private void OnFlowCleared(string flowId, string nodeName) { try { System.Windows.Application.Current?.Dispatcher.Invoke(() => { RebuildLayerEntries(); ApplyLayerSelection(); }); } catch { } } /// 重建筛选条目:[最新运行] + 每节点(全部图层)+ 每图层。按 (节点,图层) 值保留当前选中。 /// 本流程没有发布记录时(图 Id 漂移等)兜底展示全部发布,保证节点条目始终可见。 private void RebuildLayerEntries() { var flowId = _model?.SubGraph?.GraphId; var own = VisionLayerStore.Instance.GetFlowLayers(flowId ?? ""); var source = own.Count > 0 ? own : VisionLayerStore.Instance.GetAllLatest(); var current = SelectedEntry(); var entries = LayerComboHelper.BuildEntries(flowId ?? "", source); _suppressComboSync = true; _layerEntries.Clear(); foreach (var e in entries) _layerEntries.Add(e); LayerCombo.ItemsSource = _layerEntries.Select(e => e.Display).ToList(); var index = current == null ? 0 : _layerEntries.FindIndex(x => x.NodeName == current.NodeName && x.LayerName == current.LayerName); LayerCombo.SelectedIndex = index >= 0 ? index : 0; _suppressComboSync = false; } private void LayerCombo_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (_suppressComboSync) return; ApplyLayerSelection(); } /// 按当前筛选显示:最新运行=最后一次发布整体;节点=底图+全部叠加; /// 节点.图层=图像层单独显示、其他叠加层自动附带该节点原图。 /// VmWindow.RefreshImage 按平台对象类型(IVmModule/IImageData)解释,其余类型安全跳过。 private void ApplyLayerSelection() { var preview = DisplayHost.Content as VmWindow; if (preview == null) return; var flowId = _model?.SubGraph?.GraphId; var entry = SelectedEntry(); var store = VisionLayerStore.Instance; LayerPublishArgs args; if (entry == null || entry.NodeName == null) { var all = store.GetFlowLayers(flowId ?? ""); if (all.Count == 0) all = store.GetAllLatest(); if (all.Count == 0) { RunStatusText.Text = "等待运行…(节点运行后自动显示其图像)"; return; } args = all[all.Count - 1]; } else { // 图 Id 漂移兜底:按流程查不到时按节点名查(跨流程最近一次发布) args = store.GetLatest(flowId ?? "", entry.NodeName) ?? store.GetLatestByNode(entry.NodeName); if (args == null || args.Layers.Count == 0) { RunStatusText.Text = $"{entry.NodeName} · 尚未运行"; return; } } var layers = LayerComboHelper.ResolveLayers(args, entry?.LayerName, flowId); var content = layers.FirstOrDefault(l => l.Kind == LayerKind.Image)?.Content; if (content != null) preview.RefreshImage(content); RunStatusText.Text = $"{args.NodeName} · {(entry?.LayerName ?? $"全部 {layers.Count} 图层")} · #{args.RunStamp}"; } #endregion /// 窗口关闭前保存更改回模型 public void SaveChanges() { if (DataContext is VmFlowModel model && _vm != null) model.SubGraph = _vm.Graph; } /// 窗体"执行"开始:内嵌子流程编辑器进入运行态(UI 线程同步,避免跨线程访问控件) public void BeginRun() { if (_vm == null) return; System.Windows.Application.Current?.Dispatcher.Invoke(() => { _vm.IsRunning = true; }); } /// 窗体"执行"结束(含停止/异常):恢复非运行态 public void EndRun() { if (_vm == null) return; System.Windows.Application.Current?.Dispatcher.Invoke(() => { _vm.IsRunning = false; }); } #region IPluginViewTip 底部提示栏 private readonly HashSet _hoverHooked = new HashSet(); /// 承载本视图的基窗体(PluginViewDialog),未承载时为 null private TeamAAS.Dialogs.Dialogs.PluginViewDialog HostDialog => System.Windows.Window.GetWindow(this) as TeamAAS.Dialogs.Dialogs.PluginViewDialog; public void ShowTip(string message) => HostDialog?.ShowTip(message); public void ShowHoverHint(string message) => HostDialog?.ShowHoverHint(message); private void HoverEl_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e) { if ((sender as System.Windows.FrameworkElement)?.Tag is string tip && !string.IsNullOrWhiteSpace(tip)) ShowHoverHint(tip); } private void HoverEl_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e) => ShowHoverHint(null); /// 扫描视觉树,给所有 Tag 为非空字符串的按钮挂上悬停提示。 private void HookHoverHints(System.Windows.DependencyObject root) { if (root == null) return; var count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(root); for (int i = 0; i < count; i++) { var child = System.Windows.Media.VisualTreeHelper.GetChild(root, i); if (child is System.Windows.Controls.Primitives.ButtonBase btn && !_hoverHooked.Contains(btn) && btn.Tag is string tip && !string.IsNullOrWhiteSpace(tip)) { btn.MouseEnter += HoverEl_MouseEnter; btn.MouseLeave += HoverEl_MouseLeave; _hoverHooked.Add(btn); } HookHoverHints(child); } } #endregion private void BtnProperties_Click(object sender, RoutedEventArgs e) { if (DataContext is VmFlowModel model) { // 编辑容器自身的输入/输出映射:输入用 InputSourceDataProvider(查主流程前置节点), // 输出用 PluginDataProvider(查子流程节点) PluginLoader.Instance.SetGroupPluginModel = model.SubGraph; PluginLoader.Instance.ParentGroupModel = model; if (DialogHelper.EditProperties(model, $"VM 流程属性 - {model.NodeName}")) { TxtTitle.Text = model.ToolName ?? "VM流程"; DialogHelper.Info($"已更新 VM 流程属性: {model.ToolName}"); } PluginLoader.Instance.SetGroupPluginModel = null; // 保留 ParentGroupModel = model,供子节点公式树继续显示「输入」变量 } } } }