VpTerminalEditorWindow.xaml.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Windows;
  6. using System.Windows.Controls;
  7. using TeamAAS.FlowEngine;
  8. namespace Plugins.Vpp.Views
  9. {
  10. /// <summary>
  11. /// VP 工具「输出终端」编辑窗体 —— 迁移 VisionPro ToolBlock「添加终端」的选树体验:
  12. /// 树 = 工具对象图(属性/结果),节点按「名称 (类型) = 值」标注;
  13. /// 支持按类型筛选(默认 ALL):只显示匹配类型及其结构路径,勾选任意节点作为输出终端。
  14. /// 树实时读取工具对象(与流程运行同一实例),上限 400 节点 / 深度 4,防大对象图卡 UI。
  15. /// </summary>
  16. public partial class VpTerminalEditorWindow : Window
  17. {
  18. private readonly VpToolModelBase _model;
  19. private readonly object _tool;
  20. private readonly Dictionary<TreeViewItem, CheckBox> _checks = new Dictionary<TreeViewItem, CheckBox>();
  21. private readonly HashSet<TreeViewItem> _selectable = new HashSet<TreeViewItem>();
  22. private readonly HashSet<string> _typeNames = new HashSet<string>(StringComparer.Ordinal);
  23. private int _nodes;
  24. /// <summary>打开输出终端编辑窗体(模态)。</summary>
  25. public static void Show(VpToolModelBase model)
  26. {
  27. if (model == null) return;
  28. var win = new VpTerminalEditorWindow(model);
  29. var owner = Application.Current?.Windows.OfType<Window>().FirstOrDefault(w => w.IsActive);
  30. if (owner != null) win.Owner = owner;
  31. win.ShowDialog();
  32. }
  33. public VpTerminalEditorWindow(VpToolModelBase model)
  34. {
  35. InitializeComponent();
  36. _model = model;
  37. TxtTitle.Text = $"{model.ToolTypeName ?? "VisionPro 工具"} · 输出终端";
  38. var tool = _model?.Tool;
  39. if (tool == null)
  40. {
  41. try { _model?.EnsureTool(); } catch { }
  42. tool = _model?.Tool;
  43. }
  44. _tool = tool;
  45. // 首次打开且还没有任何勾选时,种入预设终端(树中呈勾选态,用户可取消/增选)
  46. if ((_model.OutputTerminals == null || _model.OutputTerminals.Count == 0) && _model is IVpPresetSource ps)
  47. {
  48. var presets = ps.GetPresetPaths();
  49. if (presets.Count > 0) _model.OutputTerminals = presets;
  50. }
  51. // 首次按 ALL 构建树(同时收集全部出现过的类型 → 填充类型筛选下拉)
  52. RebuildTree("ALL");
  53. var items = new List<string> { "ALL" };
  54. items.AddRange(_typeNames.Where(t => t != "ALL").OrderBy(t => t));
  55. TypeFilterCombo.ItemsSource = items;
  56. TypeFilterCombo.SelectedIndex = 0;
  57. TypeFilterCombo.SelectionChanged += (s, e) =>
  58. {
  59. if (TypeFilterCombo.SelectedItem is string f) RebuildTree(f);
  60. };
  61. }
  62. /// <summary>按类型筛选重建树(ALL=全部;其它=只显示匹配类型及其结构路径)。</summary>
  63. private void RebuildTree(string typeFilter)
  64. {
  65. _checks.Clear();
  66. _selectable.Clear();
  67. _nodes = 0;
  68. TermTree.Items.Clear();
  69. if (_tool == null)
  70. {
  71. TermTree.Items.Add(new TreeViewItem
  72. {
  73. Header = new TextBlock { Text = "工具尚未创建(先运行一次流程)", IsEnabled = false }
  74. });
  75. return;
  76. }
  77. var root = MakeNode(_tool.GetType().Name, null, null, null, selectable: false, expanded: true, typeFilter);
  78. TermTree.Items.Add(root);
  79. AddChildren(root, _tool, "", 0, typeFilter);
  80. }
  81. /// <summary>
  82. /// 构建树节点:name = 显示名;typeName = 类型标注(副色);valueText = 实时值(主色);
  83. /// path 非 null 且 selectable 时节点带勾选框(勾选即作为输出终端)。
  84. /// </summary>
  85. private TreeViewItem MakeNode(string name, string typeName, string valueText, string path, bool selectable, bool expanded, string typeFilter)
  86. {
  87. var panel = new StackPanel { Orientation = Orientation.Horizontal };
  88. CheckBox cb = null;
  89. if (selectable)
  90. {
  91. cb = new CheckBox
  92. {
  93. VerticalAlignment = VerticalAlignment.Center,
  94. Margin = new Thickness(0, 0, 5, 0),
  95. IsChecked = path != null && (_model.OutputTerminals ?? new List<string>()).Contains(path),
  96. };
  97. panel.Children.Add(cb);
  98. }
  99. var nameTb = new TextBlock { Text = name, VerticalAlignment = VerticalAlignment.Center, FontSize = 12 };
  100. panel.Children.Add(nameTb);
  101. if (!string.IsNullOrEmpty(typeName))
  102. {
  103. if (typeFilter == "ALL") _typeNames.Add(typeName);
  104. var typeTb = new TextBlock
  105. {
  106. Text = " (" + typeName + ")",
  107. VerticalAlignment = VerticalAlignment.Center,
  108. FontSize = 11,
  109. Margin = new Thickness(2, 0, 0, 0),
  110. };
  111. typeTb.SetResourceReference(TextBlock.ForegroundProperty, "SecondaryTextBrush");
  112. panel.Children.Add(typeTb);
  113. }
  114. if (!string.IsNullOrEmpty(valueText))
  115. {
  116. var valTb = new TextBlock
  117. {
  118. Text = " = " + valueText,
  119. VerticalAlignment = VerticalAlignment.Center,
  120. FontSize = 11,
  121. Margin = new Thickness(6, 0, 0, 0),
  122. };
  123. valTb.SetResourceReference(TextBlock.ForegroundProperty, "PrimaryTextBrush");
  124. panel.Children.Add(valTb);
  125. }
  126. var item = new TreeViewItem { Header = panel, IsExpanded = expanded };
  127. if (cb != null)
  128. {
  129. _checks[item] = cb;
  130. _selectable.Add(item);
  131. item.Tag = path;
  132. }
  133. _nodes++;
  134. return item;
  135. }
  136. /// <summary>
  137. /// 对 Blob 结果对象注入「测量项」子节点(GetMeasure 机制:CogBlobResult 不暴露 Area 等属性,
  138. /// 只能经 GetMeasure(枚举) 查询)——没有官方测量项的注入 0 项。返回是否有注入节点。
  139. /// </summary>
  140. private bool AddMeasureChildren(ItemsControl parent, object obj, string path, string typeFilter)
  141. {
  142. if (obj == null) return false;
  143. var t = obj.GetType();
  144. // 仅对 CogBlobResult(含 GetMeasure(CogBlobMeasureConstants) 方法)的对象注入
  145. if (t.GetMethod("GetMeasure") == null) return false;
  146. var mi = t.GetMethods().FirstOrDefault(m => m.Name == "GetMeasure" && m.GetParameters().Length == 1);
  147. if (mi == null) return false;
  148. var enumType = mi.GetParameters()[0].ParameterType;
  149. if (!enumType.IsEnum) return false;
  150. // 测量项节点的路径标记:对象路径 + "@Measure:测量名"
  151. var all = typeFilter == "ALL";
  152. var any = false;
  153. var branch = MakeNode("测量项 (GetMeasure)", null, null, null, selectable: false, expanded: false, typeFilter);
  154. foreach (var measure in Enum.GetNames(enumType))
  155. {
  156. var mPath = path + VpPresetTerminal.MeasureMarker + measure;
  157. var typeOk = all || string.Equals(measure, typeFilter, StringComparison.OrdinalIgnoreCase);
  158. // 测量节点在 ALL 下全展示;筛选模式下按测量名匹配
  159. if (!typeOk) continue;
  160. double? val = null;
  161. try
  162. {
  163. var parsed = Enum.Parse(enumType, measure);
  164. var r = mi.Invoke(obj, new[] { parsed });
  165. if (r is double d) val = d;
  166. }
  167. catch { /* 未启用的测量:显示 (未启用) 不可取值,但仍可勾选 */ }
  168. var valueText = val.HasValue ? val.Value.ToString("G6") : "(未启用)";
  169. branch.Items.Add(MakeNode(measure, "Double", valueText, mPath, selectable: true, expanded: false, typeFilter));
  170. any = true;
  171. }
  172. if (any)
  173. {
  174. parent.Items.Add(branch);
  175. return true;
  176. }
  177. return false;
  178. }
  179. private static string FormatValue(object v)
  180. {
  181. if (v is string s) return "\"" + (s.Length > 48 ? s.Substring(0, 48) + "…" : s) + "\"";
  182. if (v is DateTime dt) return dt.ToString("yyyy-MM-dd HH:mm:ss");
  183. return v.ToString() ?? "";
  184. }
  185. /// <summary>递归展开对象图。返回:本层(含子树)是否有节点被包含。
  186. /// 筛选模式下:类型匹配的节点可勾选;不匹配但子树有匹配的作为结构路径保留。</summary>
  187. private bool AddChildren(ItemsControl parent, object obj, string prefix, int depth, string typeFilter)
  188. {
  189. if (obj == null || depth > 4 || _nodes > 400) return false;
  190. var all = typeFilter == "ALL";
  191. var any = false;
  192. foreach (var prop in obj.GetType().GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
  193. {
  194. if (_nodes > 400) return any;
  195. if (prop.GetIndexParameters().Length > 0) continue;
  196. var name = prop.Name;
  197. if (name == "RunStatus" || name.StartsWith("Site") || name.StartsWith("Tag")) continue;
  198. object v;
  199. try { v = prop.GetValue(obj); } catch { continue; }
  200. var path = string.IsNullOrEmpty(prefix) ? name : prefix + "." + name;
  201. if (v == null)
  202. {
  203. // 标量 null(如 Blob 未启用的测量项):路径有效、此刻无值 → 可勾选(输出 null);
  204. // 复杂对象 null:仅结构展示
  205. var pt = prop.PropertyType;
  206. var scalarNull = pt.IsPrimitive || pt == typeof(string) || pt == typeof(decimal) || pt == typeof(DateTime);
  207. if (scalarNull)
  208. {
  209. parent.Items.Add(MakeNode(name, pt.Name, "null", path, selectable: true, expanded: false, typeFilter));
  210. any = true;
  211. }
  212. else if (all)
  213. {
  214. parent.Items.Add(MakeNode(name, pt.Name, "null", null, selectable: false, expanded: false, typeFilter));
  215. any = true;
  216. }
  217. continue;
  218. }
  219. var vt = v.GetType();
  220. var typeOk = all || string.Equals(vt.Name, typeFilter, StringComparison.OrdinalIgnoreCase);
  221. if (vt.IsPrimitive || v is string || v is decimal || v is DateTime)
  222. {
  223. if (typeOk)
  224. {
  225. parent.Items.Add(MakeNode(name, vt.Name, FormatValue(v), path, selectable: true, expanded: false, typeFilter));
  226. any = true;
  227. }
  228. continue;
  229. }
  230. if (v is IEnumerable en && !(v is string))
  231. {
  232. object first = null;
  233. var count = 0;
  234. try { foreach (var e in en) { count++; if (first == null && e != null) first = e; } } catch { }
  235. if (first == null) continue;
  236. var ft = first.GetType();
  237. var elemOk = all || string.Equals(ft.Name, typeFilter, StringComparison.OrdinalIgnoreCase);
  238. var primitiveElem = ft.IsPrimitive || first is string || first is decimal || first is DateTime;
  239. if (all)
  240. {
  241. // ALL:完整分支(集合节点不勾选,元素可勾选)
  242. if (primitiveElem)
  243. {
  244. var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: depth < 1, typeFilter);
  245. branch.Items.Add(MakeNode("[0]", ft.Name, FormatValue(first), path + "[0]", selectable: true, expanded: false, typeFilter));
  246. parent.Items.Add(branch);
  247. }
  248. else
  249. {
  250. var child = MakeNode("[0]", ft.Name, null, path + "[0]", selectable: true, expanded: depth < 1, typeFilter);
  251. var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: depth < 1, typeFilter);
  252. branch.Items.Add(child);
  253. parent.Items.Add(branch);
  254. AddChildren(child, first, path + "[0]", depth + 1, typeFilter);
  255. }
  256. any = true;
  257. }
  258. else
  259. {
  260. // 筛选模式:仅当元素类型匹配时展示 [0] 项
  261. if (elemOk)
  262. {
  263. var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: true, typeFilter);
  264. if (primitiveElem)
  265. branch.Items.Add(MakeNode("[0]", ft.Name, FormatValue(first), path + "[0]", selectable: true, expanded: false, typeFilter));
  266. else
  267. branch.Items.Add(MakeNode("[0]", ft.Name, null, path + "[0]", selectable: true, expanded: depth < 1, typeFilter));
  268. parent.Items.Add(branch);
  269. any = true;
  270. if (!primitiveElem)
  271. AddChildren(branch.Items.OfType<TreeViewItem>().Last(), first, path + "[0]", depth + 1, typeFilter);
  272. }
  273. else if (depth < 3)
  274. {
  275. // 元素不匹配:继续往首元素深处找匹配(作为结构路径)
  276. var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: true, typeFilter);
  277. parent.Items.Add(branch);
  278. var child = MakeNode("[0]", ft.Name, null, path + "[0]", selectable: false, expanded: true, typeFilter);
  279. branch.Items.Add(child);
  280. var subAny = AddChildren(child, first, path + "[0]", depth + 1, typeFilter);
  281. if (!subAny) branch.Items.Remove(child);
  282. if (subAny) any = true; else parent.Items.Remove(branch);
  283. }
  284. }
  285. continue;
  286. }
  287. if ((vt.Namespace ?? "").StartsWith("System"))
  288. {
  289. if (all)
  290. {
  291. parent.Items.Add(MakeNode(name, vt.Name, null, null, selectable: false, expanded: false, typeFilter));
  292. any = true;
  293. }
  294. continue;
  295. }
  296. // 复杂对象:ALL 模式可勾选且展开;筛选模式类型匹配可勾选,不匹配则作为结构路径深入
  297. if (all)
  298. {
  299. var node = MakeNode(name, vt.Name, null, path, selectable: true, expanded: depth < 1, typeFilter);
  300. parent.Items.Add(node);
  301. AddChildren(node, v, path, depth + 1, typeFilter);
  302. AddMeasureChildren(node, v, path, typeFilter);
  303. any = true;
  304. }
  305. else
  306. {
  307. var node = MakeNode(name, vt.Name, null, path, selectable: typeOk, expanded: true, typeFilter);
  308. var subAny = AddChildren(node, v, path, depth + 1, typeFilter);
  309. var measureAny = AddMeasureChildren(node, v, path, typeFilter);
  310. if (typeOk || subAny || measureAny)
  311. {
  312. parent.Items.Add(node);
  313. any = true;
  314. }
  315. }
  316. }
  317. // 无参 Get/Create 方法:VisionPro 结果集合对象(CogBlobResults 等)的数据全在
  318. // GetBlobs()/CreateBlobImage() 这类方法里 —— 属性扫描在这里一无所获(Results 节点断层的根因)
  319. foreach (var mi in obj.GetType().GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
  320. {
  321. if (_nodes > 400) return any;
  322. if (mi.IsSpecialName || mi.IsGenericMethod) continue;
  323. if (mi.GetParameters().Length > 0) continue;
  324. if (mi.DeclaringType == typeof(object)) continue;
  325. var name = mi.Name;
  326. if (!(name.StartsWith("Get") || name.StartsWith("Create"))) continue;
  327. if (name == "GetHashCode" || name == "GetType" || name == "Dispose") continue;
  328. object v;
  329. try { v = mi.Invoke(obj, null); } catch { continue; }
  330. // 方法节点路径必须带 "()"(与 NormalizePath/预设存储格式一致),
  331. // 否则子节点路径(如 Results.GetBlobs[0])和勾选存储(Results.GetBlobs()[0])对不上 → 预勾选不回显
  332. var path = string.IsNullOrEmpty(prefix) ? name + "()" : prefix + "." + name + "()";
  333. var displayName = name + "()";
  334. if (WalkValue(parent, displayName, v, mi.ReturnType, path, depth, typeFilter)) any = true;
  335. }
  336. return any;
  337. }
  338. /// <summary>按值的形态渲染节点(标量/集合/复杂对象/null),递归深度受控。返回是否有节点被包含。</summary>
  339. private bool WalkValue(ItemsControl parent, string name, object v, Type declaredType, string path, int depth, string typeFilter)
  340. {
  341. var all = typeFilter == "ALL";
  342. if (v == null)
  343. {
  344. // 标量 null(如 Blob 未启用的测量项):路径有效、此刻无值 → 可勾选(输出 null);
  345. // 复杂对象 null:仅结构展示
  346. var pt = declaredType;
  347. var scalarNull = pt.IsPrimitive || pt == typeof(string) || pt == typeof(decimal) || pt == typeof(DateTime);
  348. if (scalarNull)
  349. {
  350. parent.Items.Add(MakeNode(name, pt.Name, "null", path, selectable: true, expanded: false, typeFilter));
  351. return true;
  352. }
  353. if (all)
  354. {
  355. parent.Items.Add(MakeNode(name, pt.Name, "null", null, selectable: false, expanded: false, typeFilter));
  356. return true;
  357. }
  358. return false;
  359. }
  360. var vt = v.GetType();
  361. var typeOk = all || string.Equals(vt.Name, typeFilter, StringComparison.OrdinalIgnoreCase);
  362. if (vt.IsPrimitive || v is string || v is decimal || v is DateTime)
  363. {
  364. if (typeOk)
  365. {
  366. parent.Items.Add(MakeNode(name, vt.Name, FormatValue(v), path, selectable: true, expanded: false, typeFilter));
  367. return true;
  368. }
  369. return false;
  370. }
  371. if (v is IEnumerable en && !(v is string))
  372. {
  373. object first = null;
  374. var count = 0;
  375. try { foreach (var e in en) { count++; if (first == null && e != null) first = e; } } catch { }
  376. if (first == null) return false;
  377. var ft = first.GetType();
  378. var elemOk = all || string.Equals(ft.Name, typeFilter, StringComparison.OrdinalIgnoreCase);
  379. var primitiveElem = ft.IsPrimitive || first is string || first is decimal || first is DateTime;
  380. if (all)
  381. {
  382. if (primitiveElem)
  383. {
  384. var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: depth < 1, typeFilter);
  385. branch.Items.Add(MakeNode("[0]", ft.Name, FormatValue(first), path + "[0]", selectable: true, expanded: false, typeFilter));
  386. parent.Items.Add(branch);
  387. }
  388. else
  389. {
  390. var child = MakeNode("[0]", ft.Name, null, path + "[0]", selectable: true, expanded: depth < 1, typeFilter);
  391. var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: depth < 1, typeFilter);
  392. branch.Items.Add(child);
  393. parent.Items.Add(branch);
  394. AddChildren(child, first, path + "[0]", depth + 1, typeFilter);
  395. }
  396. return true;
  397. }
  398. if (elemOk)
  399. {
  400. var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: true, typeFilter);
  401. if (primitiveElem)
  402. branch.Items.Add(MakeNode("[0]", ft.Name, FormatValue(first), path + "[0]", selectable: true, expanded: false, typeFilter));
  403. else
  404. branch.Items.Add(MakeNode("[0]", ft.Name, null, path + "[0]", selectable: true, expanded: depth < 1, typeFilter));
  405. parent.Items.Add(branch);
  406. if (!primitiveElem)
  407. AddChildren(branch.Items.OfType<TreeViewItem>().Last(), first, path + "[0]", depth + 1, typeFilter);
  408. return true;
  409. }
  410. if (depth < 3)
  411. {
  412. var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: true, typeFilter);
  413. parent.Items.Add(branch);
  414. var child = MakeNode("[0]", ft.Name, null, path + "[0]", selectable: false, expanded: true, typeFilter);
  415. branch.Items.Add(child);
  416. var subAny = AddChildren(child, first, path + "[0]", depth + 1, typeFilter);
  417. if (!subAny) branch.Items.Remove(child);
  418. return subAny;
  419. }
  420. return false;
  421. }
  422. if ((vt.Namespace ?? "").StartsWith("System"))
  423. {
  424. if (all)
  425. {
  426. parent.Items.Add(MakeNode(name, vt.Name, null, null, selectable: false, expanded: false, typeFilter));
  427. return true;
  428. }
  429. return false;
  430. }
  431. // 复杂对象:ALL 模式可勾选且展开;筛选模式类型匹配可勾选,不匹配则作为结构路径深入
  432. if (all)
  433. {
  434. var node = MakeNode(name, vt.Name, null, path, selectable: true, expanded: depth < 1, typeFilter);
  435. parent.Items.Add(node);
  436. AddChildren(node, v, path, depth + 1, typeFilter);
  437. AddMeasureChildren(node, v, path, typeFilter);
  438. return true;
  439. }
  440. var node2 = MakeNode(name, vt.Name, null, path, selectable: typeOk, expanded: true, typeFilter);
  441. var sub = AddChildren(node2, v, path, depth + 1, typeFilter);
  442. var measureAny = AddMeasureChildren(node2, v, path, typeFilter);
  443. if (typeOk || sub || measureAny)
  444. {
  445. parent.Items.Add(node2);
  446. return true;
  447. }
  448. return false;
  449. }
  450. private void BtnOk_Click(object sender, RoutedEventArgs e)
  451. {
  452. var selected = new List<string>();
  453. Collect(TermTree, selected);
  454. _model.OutputTerminals = selected;
  455. DialogResult = true;
  456. }
  457. private void Collect(ItemsControl parent, List<string> into)
  458. {
  459. foreach (var item in parent.Items.OfType<TreeViewItem>())
  460. {
  461. if (_selectable.Contains(item)
  462. && _checks.TryGetValue(item, out var cb)
  463. && cb.IsChecked == true
  464. && item.Tag is string p
  465. && !into.Contains(p))
  466. into.Add(p);
  467. Collect(item, into);
  468. }
  469. }
  470. private void BtnCancel_Click(object sender, RoutedEventArgs e) => Close();
  471. }
  472. }