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 { /// /// VP 工具「输出终端」编辑窗体 —— 迁移 VisionPro ToolBlock「添加终端」的选树体验: /// 树 = 工具对象图(属性/结果),节点按「名称 (类型) = 值」标注; /// 支持按类型筛选(默认 ALL):只显示匹配类型及其结构路径,勾选任意节点作为输出终端。 /// 树实时读取工具对象(与流程运行同一实例),上限 400 节点 / 深度 4,防大对象图卡 UI。 /// public partial class VpTerminalEditorWindow : Window { private readonly VpToolModelBase _model; private readonly object _tool; private readonly Dictionary _checks = new Dictionary(); private readonly HashSet _selectable = new HashSet(); private readonly HashSet _typeNames = new HashSet(StringComparer.Ordinal); private int _nodes; /// 打开输出终端编辑窗体(模态)。 public static void Show(VpToolModelBase model) { if (model == null) return; var win = new VpTerminalEditorWindow(model); var owner = Application.Current?.Windows.OfType().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 { "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); }; } /// 按类型筛选重建树(ALL=全部;其它=只显示匹配类型及其结构路径)。 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); } /// /// 构建树节点:name = 显示名;typeName = 类型标注(副色);valueText = 实时值(主色); /// path 非 null 且 selectable 时节点带勾选框(勾选即作为输出终端)。 /// 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()).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; } /// /// 对 Blob 结果对象注入「测量项」子节点(GetMeasure 机制:CogBlobResult 不暴露 Area 等属性, /// 只能经 GetMeasure(枚举) 查询)——没有官方测量项的注入 0 项。返回是否有注入节点。 /// 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() ?? ""; } /// 递归展开对象图。返回:本层(含子树)是否有节点被包含。 /// 筛选模式下:类型匹配的节点可勾选;不匹配但子树有匹配的作为结构路径保留。 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().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; } /// 按值的形态渲染节点(标量/集合/复杂对象/null),递归深度受控。返回是否有节点被包含。 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().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(); Collect(TermTree, selected); _model.OutputTerminals = selected; DialogResult = true; } private void Collect(ItemsControl parent, List into) { foreach (var item in parent.Items.OfType()) { 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(); } }