| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 |
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Linq;
- using System.Windows;
- using System.Windows.Controls;
- using TeamAAS.FlowEngine;
- namespace Plugins.Vpp.Views
- {
- /// <summary>
- /// VP 工具「输出终端」编辑窗体 —— 迁移 VisionPro ToolBlock「添加终端」的选树体验:
- /// 树 = 工具对象图(属性/结果),节点按「名称 (类型) = 值」标注;
- /// 支持按类型筛选(默认 ALL):只显示匹配类型及其结构路径,勾选任意节点作为输出终端。
- /// 树实时读取工具对象(与流程运行同一实例),上限 400 节点 / 深度 4,防大对象图卡 UI。
- /// </summary>
- public partial class VpTerminalEditorWindow : Window
- {
- private readonly VpToolModelBase _model;
- private readonly object _tool;
- private readonly Dictionary<TreeViewItem, CheckBox> _checks = new Dictionary<TreeViewItem, CheckBox>();
- private readonly HashSet<TreeViewItem> _selectable = new HashSet<TreeViewItem>();
- private readonly HashSet<string> _typeNames = new HashSet<string>(StringComparer.Ordinal);
- private int _nodes;
- /// <summary>打开输出终端编辑窗体(模态)。</summary>
- public static void Show(VpToolModelBase model)
- {
- if (model == null) return;
- var win = new VpTerminalEditorWindow(model);
- var owner = Application.Current?.Windows.OfType<Window>().FirstOrDefault(w => w.IsActive);
- if (owner != null) win.Owner = owner;
- win.ShowDialog();
- }
- public VpTerminalEditorWindow(VpToolModelBase model)
- {
- InitializeComponent();
- _model = model;
- TxtTitle.Text = $"{model.ToolTypeName ?? "VisionPro 工具"} · 输出终端";
- var tool = _model?.Tool;
- if (tool == null)
- {
- try { _model?.EnsureTool(); } catch { }
- tool = _model?.Tool;
- }
- _tool = tool;
- // 首次打开且还没有任何勾选时,种入预设终端(树中呈勾选态,用户可取消/增选)
- if ((_model.OutputTerminals == null || _model.OutputTerminals.Count == 0) && _model is IVpPresetSource ps)
- {
- var presets = ps.GetPresetPaths();
- if (presets.Count > 0) _model.OutputTerminals = presets;
- }
- // 首次按 ALL 构建树(同时收集全部出现过的类型 → 填充类型筛选下拉)
- RebuildTree("ALL");
- var items = new List<string> { "ALL" };
- items.AddRange(_typeNames.Where(t => t != "ALL").OrderBy(t => t));
- TypeFilterCombo.ItemsSource = items;
- TypeFilterCombo.SelectedIndex = 0;
- TypeFilterCombo.SelectionChanged += (s, e) =>
- {
- if (TypeFilterCombo.SelectedItem is string f) RebuildTree(f);
- };
- }
- /// <summary>按类型筛选重建树(ALL=全部;其它=只显示匹配类型及其结构路径)。</summary>
- private void RebuildTree(string typeFilter)
- {
- _checks.Clear();
- _selectable.Clear();
- _nodes = 0;
- TermTree.Items.Clear();
- if (_tool == null)
- {
- TermTree.Items.Add(new TreeViewItem
- {
- Header = new TextBlock { Text = "工具尚未创建(先运行一次流程)", IsEnabled = false }
- });
- return;
- }
- var root = MakeNode(_tool.GetType().Name, null, null, null, selectable: false, expanded: true, typeFilter);
- TermTree.Items.Add(root);
- AddChildren(root, _tool, "", 0, typeFilter);
- }
- /// <summary>
- /// 构建树节点:name = 显示名;typeName = 类型标注(副色);valueText = 实时值(主色);
- /// path 非 null 且 selectable 时节点带勾选框(勾选即作为输出终端)。
- /// </summary>
- private TreeViewItem MakeNode(string name, string typeName, string valueText, string path, bool selectable, bool expanded, string typeFilter)
- {
- var panel = new StackPanel { Orientation = Orientation.Horizontal };
- CheckBox cb = null;
- if (selectable)
- {
- cb = new CheckBox
- {
- VerticalAlignment = VerticalAlignment.Center,
- Margin = new Thickness(0, 0, 5, 0),
- IsChecked = path != null && (_model.OutputTerminals ?? new List<string>()).Contains(path),
- };
- panel.Children.Add(cb);
- }
- var nameTb = new TextBlock { Text = name, VerticalAlignment = VerticalAlignment.Center, FontSize = 12 };
- panel.Children.Add(nameTb);
- if (!string.IsNullOrEmpty(typeName))
- {
- if (typeFilter == "ALL") _typeNames.Add(typeName);
- var typeTb = new TextBlock
- {
- Text = " (" + typeName + ")",
- VerticalAlignment = VerticalAlignment.Center,
- FontSize = 11,
- Margin = new Thickness(2, 0, 0, 0),
- };
- typeTb.SetResourceReference(TextBlock.ForegroundProperty, "SecondaryTextBrush");
- panel.Children.Add(typeTb);
- }
- if (!string.IsNullOrEmpty(valueText))
- {
- var valTb = new TextBlock
- {
- Text = " = " + valueText,
- VerticalAlignment = VerticalAlignment.Center,
- FontSize = 11,
- Margin = new Thickness(6, 0, 0, 0),
- };
- valTb.SetResourceReference(TextBlock.ForegroundProperty, "PrimaryTextBrush");
- panel.Children.Add(valTb);
- }
- var item = new TreeViewItem { Header = panel, IsExpanded = expanded };
- if (cb != null)
- {
- _checks[item] = cb;
- _selectable.Add(item);
- item.Tag = path;
- }
- _nodes++;
- return item;
- }
- /// <summary>
- /// 对 Blob 结果对象注入「测量项」子节点(GetMeasure 机制:CogBlobResult 不暴露 Area 等属性,
- /// 只能经 GetMeasure(枚举) 查询)——没有官方测量项的注入 0 项。返回是否有注入节点。
- /// </summary>
- private bool AddMeasureChildren(ItemsControl parent, object obj, string path, string typeFilter)
- {
- if (obj == null) return false;
- var t = obj.GetType();
- // 仅对 CogBlobResult(含 GetMeasure(CogBlobMeasureConstants) 方法)的对象注入
- if (t.GetMethod("GetMeasure") == null) return false;
- var mi = t.GetMethods().FirstOrDefault(m => m.Name == "GetMeasure" && m.GetParameters().Length == 1);
- if (mi == null) return false;
- var enumType = mi.GetParameters()[0].ParameterType;
- if (!enumType.IsEnum) return false;
- // 测量项节点的路径标记:对象路径 + "@Measure:测量名"
- var all = typeFilter == "ALL";
- var any = false;
- var branch = MakeNode("测量项 (GetMeasure)", null, null, null, selectable: false, expanded: false, typeFilter);
- foreach (var measure in Enum.GetNames(enumType))
- {
- var mPath = path + VpPresetTerminal.MeasureMarker + measure;
- var typeOk = all || string.Equals(measure, typeFilter, StringComparison.OrdinalIgnoreCase);
- // 测量节点在 ALL 下全展示;筛选模式下按测量名匹配
- if (!typeOk) continue;
- double? val = null;
- try
- {
- var parsed = Enum.Parse(enumType, measure);
- var r = mi.Invoke(obj, new[] { parsed });
- if (r is double d) val = d;
- }
- catch { /* 未启用的测量:显示 (未启用) 不可取值,但仍可勾选 */ }
- var valueText = val.HasValue ? val.Value.ToString("G6") : "(未启用)";
- branch.Items.Add(MakeNode(measure, "Double", valueText, mPath, selectable: true, expanded: false, typeFilter));
- any = true;
- }
- if (any)
- {
- parent.Items.Add(branch);
- return true;
- }
- return false;
- }
- private static string FormatValue(object v)
- {
- if (v is string s) return "\"" + (s.Length > 48 ? s.Substring(0, 48) + "…" : s) + "\"";
- if (v is DateTime dt) return dt.ToString("yyyy-MM-dd HH:mm:ss");
- return v.ToString() ?? "";
- }
- /// <summary>递归展开对象图。返回:本层(含子树)是否有节点被包含。
- /// 筛选模式下:类型匹配的节点可勾选;不匹配但子树有匹配的作为结构路径保留。</summary>
- private bool AddChildren(ItemsControl parent, object obj, string prefix, int depth, string typeFilter)
- {
- if (obj == null || depth > 4 || _nodes > 400) return false;
- var all = typeFilter == "ALL";
- var any = false;
- foreach (var prop in obj.GetType().GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
- {
- if (_nodes > 400) return any;
- if (prop.GetIndexParameters().Length > 0) continue;
- var name = prop.Name;
- if (name == "RunStatus" || name.StartsWith("Site") || name.StartsWith("Tag")) continue;
- object v;
- try { v = prop.GetValue(obj); } catch { continue; }
- var path = string.IsNullOrEmpty(prefix) ? name : prefix + "." + name;
- if (v == null)
- {
- // 标量 null(如 Blob 未启用的测量项):路径有效、此刻无值 → 可勾选(输出 null);
- // 复杂对象 null:仅结构展示
- var pt = prop.PropertyType;
- var scalarNull = pt.IsPrimitive || pt == typeof(string) || pt == typeof(decimal) || pt == typeof(DateTime);
- if (scalarNull)
- {
- parent.Items.Add(MakeNode(name, pt.Name, "null", path, selectable: true, expanded: false, typeFilter));
- any = true;
- }
- else if (all)
- {
- parent.Items.Add(MakeNode(name, pt.Name, "null", null, selectable: false, expanded: false, typeFilter));
- any = true;
- }
- continue;
- }
- var vt = v.GetType();
- var typeOk = all || string.Equals(vt.Name, typeFilter, StringComparison.OrdinalIgnoreCase);
- if (vt.IsPrimitive || v is string || v is decimal || v is DateTime)
- {
- if (typeOk)
- {
- parent.Items.Add(MakeNode(name, vt.Name, FormatValue(v), path, selectable: true, expanded: false, typeFilter));
- any = true;
- }
- continue;
- }
- if (v is IEnumerable en && !(v is string))
- {
- object first = null;
- var count = 0;
- try { foreach (var e in en) { count++; if (first == null && e != null) first = e; } } catch { }
- if (first == null) continue;
- var ft = first.GetType();
- var elemOk = all || string.Equals(ft.Name, typeFilter, StringComparison.OrdinalIgnoreCase);
- var primitiveElem = ft.IsPrimitive || first is string || first is decimal || first is DateTime;
- if (all)
- {
- // ALL:完整分支(集合节点不勾选,元素可勾选)
- if (primitiveElem)
- {
- var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: depth < 1, typeFilter);
- branch.Items.Add(MakeNode("[0]", ft.Name, FormatValue(first), path + "[0]", selectable: true, expanded: false, typeFilter));
- parent.Items.Add(branch);
- }
- else
- {
- var child = MakeNode("[0]", ft.Name, null, path + "[0]", selectable: true, expanded: depth < 1, typeFilter);
- var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: depth < 1, typeFilter);
- branch.Items.Add(child);
- parent.Items.Add(branch);
- AddChildren(child, first, path + "[0]", depth + 1, typeFilter);
- }
- any = true;
- }
- else
- {
- // 筛选模式:仅当元素类型匹配时展示 [0] 项
- if (elemOk)
- {
- var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: true, typeFilter);
- if (primitiveElem)
- branch.Items.Add(MakeNode("[0]", ft.Name, FormatValue(first), path + "[0]", selectable: true, expanded: false, typeFilter));
- else
- branch.Items.Add(MakeNode("[0]", ft.Name, null, path + "[0]", selectable: true, expanded: depth < 1, typeFilter));
- parent.Items.Add(branch);
- any = true;
- if (!primitiveElem)
- AddChildren(branch.Items.OfType<TreeViewItem>().Last(), first, path + "[0]", depth + 1, typeFilter);
- }
- else if (depth < 3)
- {
- // 元素不匹配:继续往首元素深处找匹配(作为结构路径)
- var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: true, typeFilter);
- parent.Items.Add(branch);
- var child = MakeNode("[0]", ft.Name, null, path + "[0]", selectable: false, expanded: true, typeFilter);
- branch.Items.Add(child);
- var subAny = AddChildren(child, first, path + "[0]", depth + 1, typeFilter);
- if (!subAny) branch.Items.Remove(child);
- if (subAny) any = true; else parent.Items.Remove(branch);
- }
- }
- continue;
- }
- if ((vt.Namespace ?? "").StartsWith("System"))
- {
- if (all)
- {
- parent.Items.Add(MakeNode(name, vt.Name, null, null, selectable: false, expanded: false, typeFilter));
- any = true;
- }
- continue;
- }
- // 复杂对象:ALL 模式可勾选且展开;筛选模式类型匹配可勾选,不匹配则作为结构路径深入
- if (all)
- {
- var node = MakeNode(name, vt.Name, null, path, selectable: true, expanded: depth < 1, typeFilter);
- parent.Items.Add(node);
- AddChildren(node, v, path, depth + 1, typeFilter);
- AddMeasureChildren(node, v, path, typeFilter);
- any = true;
- }
- else
- {
- var node = MakeNode(name, vt.Name, null, path, selectable: typeOk, expanded: true, typeFilter);
- var subAny = AddChildren(node, v, path, depth + 1, typeFilter);
- var measureAny = AddMeasureChildren(node, v, path, typeFilter);
- if (typeOk || subAny || measureAny)
- {
- parent.Items.Add(node);
- any = true;
- }
- }
- }
- // 无参 Get/Create 方法:VisionPro 结果集合对象(CogBlobResults 等)的数据全在
- // GetBlobs()/CreateBlobImage() 这类方法里 —— 属性扫描在这里一无所获(Results 节点断层的根因)
- foreach (var mi in obj.GetType().GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
- {
- if (_nodes > 400) return any;
- if (mi.IsSpecialName || mi.IsGenericMethod) continue;
- if (mi.GetParameters().Length > 0) continue;
- if (mi.DeclaringType == typeof(object)) continue;
- var name = mi.Name;
- if (!(name.StartsWith("Get") || name.StartsWith("Create"))) continue;
- if (name == "GetHashCode" || name == "GetType" || name == "Dispose") continue;
- object v;
- try { v = mi.Invoke(obj, null); } catch { continue; }
- // 方法节点路径必须带 "()"(与 NormalizePath/预设存储格式一致),
- // 否则子节点路径(如 Results.GetBlobs[0])和勾选存储(Results.GetBlobs()[0])对不上 → 预勾选不回显
- var path = string.IsNullOrEmpty(prefix) ? name + "()" : prefix + "." + name + "()";
- var displayName = name + "()";
- if (WalkValue(parent, displayName, v, mi.ReturnType, path, depth, typeFilter)) any = true;
- }
- return any;
- }
- /// <summary>按值的形态渲染节点(标量/集合/复杂对象/null),递归深度受控。返回是否有节点被包含。</summary>
- private bool WalkValue(ItemsControl parent, string name, object v, Type declaredType, string path, int depth, string typeFilter)
- {
- var all = typeFilter == "ALL";
- if (v == null)
- {
- // 标量 null(如 Blob 未启用的测量项):路径有效、此刻无值 → 可勾选(输出 null);
- // 复杂对象 null:仅结构展示
- var pt = declaredType;
- var scalarNull = pt.IsPrimitive || pt == typeof(string) || pt == typeof(decimal) || pt == typeof(DateTime);
- if (scalarNull)
- {
- parent.Items.Add(MakeNode(name, pt.Name, "null", path, selectable: true, expanded: false, typeFilter));
- return true;
- }
- if (all)
- {
- parent.Items.Add(MakeNode(name, pt.Name, "null", null, selectable: false, expanded: false, typeFilter));
- return true;
- }
- return false;
- }
- var vt = v.GetType();
- var typeOk = all || string.Equals(vt.Name, typeFilter, StringComparison.OrdinalIgnoreCase);
- if (vt.IsPrimitive || v is string || v is decimal || v is DateTime)
- {
- if (typeOk)
- {
- parent.Items.Add(MakeNode(name, vt.Name, FormatValue(v), path, selectable: true, expanded: false, typeFilter));
- return true;
- }
- return false;
- }
- if (v is IEnumerable en && !(v is string))
- {
- object first = null;
- var count = 0;
- try { foreach (var e in en) { count++; if (first == null && e != null) first = e; } } catch { }
- if (first == null) return false;
- var ft = first.GetType();
- var elemOk = all || string.Equals(ft.Name, typeFilter, StringComparison.OrdinalIgnoreCase);
- var primitiveElem = ft.IsPrimitive || first is string || first is decimal || first is DateTime;
- if (all)
- {
- if (primitiveElem)
- {
- var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: depth < 1, typeFilter);
- branch.Items.Add(MakeNode("[0]", ft.Name, FormatValue(first), path + "[0]", selectable: true, expanded: false, typeFilter));
- parent.Items.Add(branch);
- }
- else
- {
- var child = MakeNode("[0]", ft.Name, null, path + "[0]", selectable: true, expanded: depth < 1, typeFilter);
- var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: depth < 1, typeFilter);
- branch.Items.Add(child);
- parent.Items.Add(branch);
- AddChildren(child, first, path + "[0]", depth + 1, typeFilter);
- }
- return true;
- }
- if (elemOk)
- {
- var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: true, typeFilter);
- if (primitiveElem)
- branch.Items.Add(MakeNode("[0]", ft.Name, FormatValue(first), path + "[0]", selectable: true, expanded: false, typeFilter));
- else
- branch.Items.Add(MakeNode("[0]", ft.Name, null, path + "[0]", selectable: true, expanded: depth < 1, typeFilter));
- parent.Items.Add(branch);
- if (!primitiveElem)
- AddChildren(branch.Items.OfType<TreeViewItem>().Last(), first, path + "[0]", depth + 1, typeFilter);
- return true;
- }
- if (depth < 3)
- {
- var branch = MakeNode(name, ft.Name + "[]", count + " 项", null, selectable: false, expanded: true, typeFilter);
- parent.Items.Add(branch);
- var child = MakeNode("[0]", ft.Name, null, path + "[0]", selectable: false, expanded: true, typeFilter);
- branch.Items.Add(child);
- var subAny = AddChildren(child, first, path + "[0]", depth + 1, typeFilter);
- if (!subAny) branch.Items.Remove(child);
- return subAny;
- }
- return false;
- }
- if ((vt.Namespace ?? "").StartsWith("System"))
- {
- if (all)
- {
- parent.Items.Add(MakeNode(name, vt.Name, null, null, selectable: false, expanded: false, typeFilter));
- return true;
- }
- return false;
- }
- // 复杂对象:ALL 模式可勾选且展开;筛选模式类型匹配可勾选,不匹配则作为结构路径深入
- if (all)
- {
- var node = MakeNode(name, vt.Name, null, path, selectable: true, expanded: depth < 1, typeFilter);
- parent.Items.Add(node);
- AddChildren(node, v, path, depth + 1, typeFilter);
- AddMeasureChildren(node, v, path, typeFilter);
- return true;
- }
- var node2 = MakeNode(name, vt.Name, null, path, selectable: typeOk, expanded: true, typeFilter);
- var sub = AddChildren(node2, v, path, depth + 1, typeFilter);
- var measureAny = AddMeasureChildren(node2, v, path, typeFilter);
- if (typeOk || sub || measureAny)
- {
- parent.Items.Add(node2);
- return true;
- }
- return false;
- }
- private void BtnOk_Click(object sender, RoutedEventArgs e)
- {
- var selected = new List<string>();
- Collect(TermTree, selected);
- _model.OutputTerminals = selected;
- DialogResult = true;
- }
- private void Collect(ItemsControl parent, List<string> into)
- {
- foreach (var item in parent.Items.OfType<TreeViewItem>())
- {
- if (_selectable.Contains(item)
- && _checks.TryGetValue(item, out var cb)
- && cb.IsChecked == true
- && item.Tag is string p
- && !into.Contains(p))
- into.Add(p);
- Collect(item, into);
- }
- }
- private void BtnCancel_Click(object sender, RoutedEventArgs e) => Close();
- }
- }
|