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();
}
}