HalconFlowEditorView.xaml.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. using System.Collections.Generic;
  2. using System.Linq;
  3. using System.Windows;
  4. using System.Windows.Controls;
  5. using System.Windows.Controls.Primitives;
  6. using TeamAAS;
  7. using TeamAAS.FlowEditor;
  8. using TeamAAS.FlowEditor.Models;
  9. using TeamAAS.FlowEditor.Plugins;
  10. using TeamAAS.FlowEngine;
  11. using TeamAAS.Vision;
  12. using Plugins.Halcon.Models;
  13. namespace Plugins.Halcon.Views
  14. {
  15. /// <summary>
  16. /// HALCON 流程编辑器视图(UserControl)- 承载在 PluginViewDialog 中,镜像 GroupEditorView。
  17. /// 通过 DataContext 接收 <see cref="HalconFlowModel"/>,自动初始化子流程编辑器;
  18. /// 与组合模块的唯一区别:子流程编辑器工具箱作用域为 Halcon(只显示 HALCON 节点,按 VisionPlugin 分类)。
  19. /// 实现 IPluginRunFeedback(窗体“执行”期间同步内嵌画布运行态)、IPluginViewTip(底部提示栏 + 悬停提示)。
  20. ///
  21. /// 视觉显示(VisionMaster 风格):右侧大显示窗 + 图层筛选 ComboBox。
  22. /// 节点运行后经 HalconEditorPreview.Show(model,…) → VisionLayerStore 发布图层;
  23. /// 本视图订阅图层总线:选中某节点(画布点选或 ComboBox 选择)即切换显示该节点当前运行批次
  24. /// 的图层(底图 + 多彩叠加),旧批次由 VisionLayerStore 统一淘汰释放,不叠加不泄漏。
  25. /// </summary>
  26. public partial class HalconFlowEditorView : UserControl, IPluginViewSave, IPluginRunFeedback, IPluginViewTip
  27. {
  28. private FlowEditorViewModel _vm;
  29. private HalconFlowModel _model;
  30. /// <summary>图层筛选条目(SDK):NodeName=null=最新运行;LayerName=null=节点全部图层。</summary>
  31. private readonly List<LayerComboEntry> _layerEntries = new List<LayerComboEntry>();
  32. private bool _suppressComboSync;
  33. public HalconFlowEditorView()
  34. {
  35. InitializeComponent();
  36. // 视觉显示框注入子流程编辑器右侧面板(整区替换「节点信息」)——三个平台编辑器统一布局
  37. EditorRoot.Children.Remove(VisionPanel);
  38. SubFlowEditor.ReplaceNodeInfoPanel(VisionPanel);
  39. VisionPanel.Visibility = Visibility.Visible;
  40. Loaded += OnLoaded;
  41. Unloaded += OnUnloaded;
  42. }
  43. private void OnLoaded(object sender, RoutedEventArgs e)
  44. {
  45. if (DataContext is HalconFlowModel model)
  46. {
  47. _model = model;
  48. model.EnsureSubGraph();
  49. TxtTitle.Text = model.ToolName ?? "Halcon流程";
  50. // 关键:工具箱作用域=Halcon → 只显示 IsSubFlowNode 的 HALCON 节点,按 VisionPlugin 分组
  51. _vm = new FlowEditorViewModel(model.SubGraph, isSubFlowEditor: true, toolboxScope: FlowToolboxScope.Vision);
  52. SubFlowEditor.DataContext = _vm;
  53. _vm.GraphDataChanged += _vm_GraphDataChanged;
  54. _vm.PropertyChanged += _vm_PropertyChanged;
  55. // 大显示窗(注入右侧面板;图层总线是唯一显示驱动 —— 不再挂 HalconEditorPreview.Current
  56. // 经典通路,否则每个算子运行后都会直接推图覆盖掉用户的图层筛选结果)
  57. var preview = new HalconWindow(1);
  58. preview.SetCaption("HALCON 显示");
  59. DisplayHost.Content = preview;
  60. // 图层总线挂接:节点发布图层 → 重建筛选项 + 跟随显示
  61. VisionLayerStore.Instance.LayerPublished += OnLayerPublished;
  62. VisionLayerStore.Instance.FlowCleared += OnFlowCleared;
  63. ResetLayerCombo();
  64. // 让子节点的公式树能显示容器「输入」变量(如 &{输入.图像}):
  65. // PluginDataProvider.BuildInputsNode 反射读 ParentGroupModel.ModuleInputs。
  66. // FlowEditorViewModel.OpenNodeEditor 在子流程编辑器内会保留容器 ParentGroupModel(见其 keepParent 逻辑)。
  67. PluginLoader.Instance.ParentGroupModel = model;
  68. HookHoverHints(this);
  69. }
  70. }
  71. private void OnUnloaded(object sender, RoutedEventArgs e)
  72. {
  73. PluginLoader.Instance.SetGroupPluginModel = null;
  74. if (ReferenceEquals(PluginLoader.Instance.ParentGroupModel, _model))
  75. PluginLoader.Instance.ParentGroupModel = null;
  76. VisionLayerStore.Instance.LayerPublished -= OnLayerPublished;
  77. VisionLayerStore.Instance.FlowCleared -= OnFlowCleared;
  78. // 移除内嵌预览窗体(图层总线为唯一显示驱动,无经典通路挂接需要清理)
  79. var preview = DisplayHost.Content as HalconWindow;
  80. DisplayHost.Content = null;
  81. preview?.ClearImage();
  82. }
  83. private void _vm_GraphDataChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
  84. {
  85. if (DataContext is HalconFlowModel model)
  86. {
  87. if (sender is FlowGraph flowGraph)
  88. model.SubGraph = flowGraph;
  89. }
  90. }
  91. /// <summary>画布点选节点 → 图层筛选切到该节点(实时联动)。</summary>
  92. private void _vm_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
  93. {
  94. if (e == null || e.PropertyName != "SelectedNode") return;
  95. var node = _vm?.SelectedNode;
  96. if (node == null || string.IsNullOrEmpty(node.NodeName)) return;
  97. var index = _layerEntries.FindIndex(x => x.NodeName == node.NodeName && x.LayerName == null);
  98. if (index < 0)
  99. {
  100. // 该节点还没有图层:回落「最新运行」,等它运行发布后再联动
  101. index = 0;
  102. }
  103. if (LayerCombo.SelectedIndex != index)
  104. {
  105. _suppressComboSync = true;
  106. LayerCombo.SelectedIndex = index;
  107. _suppressComboSync = false;
  108. }
  109. ApplyLayerSelection();
  110. }
  111. #region 图层筛选与显示
  112. /// <summary>当前选中的条目;越界/未选返回 null。</summary>
  113. private LayerComboEntry SelectedEntry()
  114. {
  115. var i = LayerCombo.SelectedIndex;
  116. if (i < 0 || i >= _layerEntries.Count) return null;
  117. return _layerEntries[i];
  118. }
  119. private void ResetLayerCombo()
  120. {
  121. _layerEntries.Clear();
  122. _layerEntries.Add(new LayerComboEntry { Display = "最新运行" });
  123. _suppressComboSync = true;
  124. LayerCombo.ItemsSource = _layerEntries.Select(e => e.Display).ToList();
  125. LayerCombo.SelectedIndex = 0;
  126. _suppressComboSync = false;
  127. RunStatusText.Text = "等待运行…(节点运行后自动显示其图层)";
  128. }
  129. private static System.DateTime _lastMismatchLog = System.DateTime.MinValue;
  130. /// <summary>图层发布回调:重建筛选项并按“值”保留选中;正在看该节点(或最新运行)时实时刷新显示。
  131. /// 发布的流程 Id 与编辑器不一致时(旧流程文件图 Id 漂移)不再直接丢弃 —— 记录日志并按节点名兜底显示。</summary>
  132. private void OnLayerPublished(LayerPublishArgs args)
  133. {
  134. if (args == null) return;
  135. var flowId = _model?.SubGraph?.GraphId;
  136. var matched = string.Equals(args.FlowId, flowId, System.StringComparison.OrdinalIgnoreCase);
  137. if (!matched && (System.DateTime.Now - _lastMismatchLog).TotalSeconds > 5)
  138. {
  139. _lastMismatchLog = System.DateTime.Now;
  140. TeamAAS.AppLogger.Warning($"图层发布流程Id与编辑器不一致: 发布={args.FlowId} 节点={args.NodeName} 编辑器期望={flowId}(已按节点名兜底显示)", "Halcon编辑器");
  141. }
  142. try
  143. {
  144. System.Windows.Application.Current?.Dispatcher.Invoke(() =>
  145. {
  146. RebuildLayerEntries();
  147. var current = SelectedEntry();
  148. bool follow = current == null || current.NodeName == null ||
  149. string.Equals(current.NodeName, args.NodeName, System.StringComparison.OrdinalIgnoreCase);
  150. if (follow || !matched) ApplyLayerSelection();
  151. });
  152. }
  153. catch { }
  154. }
  155. private void OnFlowCleared(string flowId, string nodeName)
  156. {
  157. try
  158. {
  159. System.Windows.Application.Current?.Dispatcher.Invoke(() =>
  160. {
  161. RebuildLayerEntries();
  162. ApplyLayerSelection();
  163. });
  164. }
  165. catch { }
  166. }
  167. /// <summary>重建筛选条目:[最新运行] + 每节点(全部图层)+ 每图层。按 (节点,图层) 值保留当前选中。
  168. /// 本流程没有发布记录时(图 Id 漂移等)兜底展示全部发布,保证节点条目始终可见。</summary>
  169. private void RebuildLayerEntries()
  170. {
  171. var flowId = _model?.SubGraph?.GraphId;
  172. var own = VisionLayerStore.Instance.GetFlowLayers(flowId ?? "");
  173. var source = own.Count > 0 ? own : VisionLayerStore.Instance.GetAllLatest();
  174. var current = SelectedEntry();
  175. var entries = LayerComboHelper.BuildEntries(flowId ?? "", source);
  176. _suppressComboSync = true;
  177. _layerEntries.Clear();
  178. foreach (var e in entries)
  179. _layerEntries.Add(e);
  180. LayerCombo.ItemsSource = _layerEntries.Select(e => e.Display).ToList();
  181. var index = current == null
  182. ? 0
  183. : _layerEntries.FindIndex(x => x.NodeName == current.NodeName && x.LayerName == current.LayerName);
  184. LayerCombo.SelectedIndex = index >= 0 ? index : 0;
  185. _suppressComboSync = false;
  186. }
  187. private void LayerCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
  188. {
  189. if (_suppressComboSync) return;
  190. ApplyLayerSelection();
  191. }
  192. /// <summary>按当前筛选显示:最新运行=最后一次发布整体;节点=底图+全部叠加;
  193. /// 节点.图层=图像层单独显示、区域等叠加层自动附带该节点原图(便于理解)。</summary>
  194. private void ApplyLayerSelection()
  195. {
  196. var preview = DisplayHost.Content as HalconWindow;
  197. if (preview == null) return;
  198. var flowId = _model?.SubGraph?.GraphId;
  199. var entry = SelectedEntry();
  200. var store = VisionLayerStore.Instance;
  201. LayerPublishArgs args;
  202. if (entry == null || entry.NodeName == null)
  203. {
  204. // “最新运行”:显示图层库里最近发布的一组(整体替换式显示;图 Id 漂移时兜底全部)
  205. var all = store.GetFlowLayers(flowId ?? "");
  206. if (all.Count == 0) all = store.GetAllLatest();
  207. if (all.Count == 0)
  208. {
  209. preview.ClearImage();
  210. RunStatusText.Text = "等待运行…(节点运行后自动显示其图层)";
  211. return;
  212. }
  213. args = all[all.Count - 1];
  214. }
  215. else
  216. {
  217. // 图 Id 漂移兜底:按流程查不到时按节点名查(跨流程最近一次发布)
  218. args = store.GetLatest(flowId ?? "", entry.NodeName)
  219. ?? store.GetLatestByNode(entry.NodeName);
  220. if (args == null || args.Layers.Count == 0)
  221. {
  222. preview.ClearImage();
  223. RunStatusText.Text = $"{entry.NodeName} · 尚未运行";
  224. return;
  225. }
  226. }
  227. var layers = LayerComboHelper.ResolveLayers(args, entry?.LayerName, flowId);
  228. if (layers.Count == 0)
  229. {
  230. preview.ClearImage();
  231. RunStatusText.Text = $"{args.NodeName} · 无可显示图层";
  232. return;
  233. }
  234. preview.ShowLayers(layers);
  235. RunStatusText.Text = $"{args.NodeName} · {(entry?.LayerName ?? $"全部 {layers.Count} 图层")} · #{args.RunStamp}";
  236. }
  237. #endregion
  238. /// <summary>窗口关闭前保存更改回模型</summary>
  239. public void SaveChanges()
  240. {
  241. if (DataContext is HalconFlowModel model && _vm != null)
  242. model.SubGraph = _vm.Graph;
  243. }
  244. /// <summary>窗体“执行”开始:内嵌子流程编辑器进入运行态(UI 线程同步,避免跨线程访问控件)</summary>
  245. public void BeginRun()
  246. {
  247. if (_vm == null) return;
  248. System.Windows.Application.Current?.Dispatcher.Invoke(() => { _vm.IsRunning = true; });
  249. }
  250. /// <summary>窗体“执行”结束(含停止/异常):恢复非运行态</summary>
  251. public void EndRun()
  252. {
  253. if (_vm == null) return;
  254. System.Windows.Application.Current?.Dispatcher.Invoke(() => { _vm.IsRunning = false; });
  255. }
  256. #region IPluginViewTip 底部提示栏
  257. private readonly HashSet<System.Windows.DependencyObject> _hoverHooked =
  258. new HashSet<System.Windows.DependencyObject>();
  259. /// <summary>承载本视图的基窗体(PluginViewDialog),未承载时为 null</summary>
  260. private TeamAAS.Dialogs.Dialogs.PluginViewDialog HostDialog =>
  261. System.Windows.Window.GetWindow(this) as TeamAAS.Dialogs.Dialogs.PluginViewDialog;
  262. public void ShowTip(string message) => HostDialog?.ShowTip(message);
  263. public void ShowHoverHint(string message) => HostDialog?.ShowHoverHint(message);
  264. private void HoverEl_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
  265. {
  266. if ((sender as System.Windows.FrameworkElement)?.Tag is string tip && !string.IsNullOrWhiteSpace(tip))
  267. ShowHoverHint(tip);
  268. }
  269. private void HoverEl_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e) => ShowHoverHint(null);
  270. /// <summary>扫描视觉树,给所有 Tag 为非空字符串的按钮挂上悬停提示。</summary>
  271. private void HookHoverHints(System.Windows.DependencyObject root)
  272. {
  273. if (root == null) return;
  274. var count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(root);
  275. for (int i = 0; i < count; i++)
  276. {
  277. var child = System.Windows.Media.VisualTreeHelper.GetChild(root, i);
  278. if (child is System.Windows.Controls.Primitives.ButtonBase btn
  279. && !_hoverHooked.Contains(btn)
  280. && btn.Tag is string tip && !string.IsNullOrWhiteSpace(tip))
  281. {
  282. btn.MouseEnter += HoverEl_MouseEnter;
  283. btn.MouseLeave += HoverEl_MouseLeave;
  284. _hoverHooked.Add(btn);
  285. }
  286. HookHoverHints(child);
  287. }
  288. }
  289. #endregion
  290. private void BtnProperties_Click(object sender, RoutedEventArgs e)
  291. {
  292. if (DataContext is HalconFlowModel model)
  293. {
  294. // 编辑容器自身的输入/输出映射:输入用 InputSourceDataProvider(查主流程前置节点),
  295. // 输出用 PluginDataProvider(查子流程节点)——与 GroupEditorView 一致
  296. PluginLoader.Instance.SetGroupPluginModel = model.SubGraph;
  297. PluginLoader.Instance.ParentGroupModel = model;
  298. if (DialogHelper.EditProperties(model, $"HALCON 流程属性 - {model.NodeName}"))
  299. {
  300. TxtTitle.Text = model.ToolName ?? "Halcon流程";
  301. DialogHelper.Info($"已更新 HALCON 流程属性: {model.ToolName}");
  302. }
  303. PluginLoader.Instance.SetGroupPluginModel = null;
  304. // 保留 ParentGroupModel = model,供子节点公式树继续显示「输入」变量
  305. }
  306. }
  307. }
  308. }