NavisworksTransport/src/UI/WPF/Views/AliasTreeControl.xaml.cs
tian f38fba6506 fix: DisplayPath 所有层级加#N去重 + 移除旧格式兼容
- BuildNode 不再剥离父路径#N后缀
- FromModelItem 对每层祖先计算 siblingIndex 追加#N,与树加载一致
- 解决复制子树中节点别名泄露给其他子树的问题
- FromKey 移除纯 DisplayPath 旧格式兼容
2026-06-06 10:05:35 +08:00

1379 lines
50 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using NavisworksTransport.Core.AliasTree;
using NavisworksTransport.Core.Config;
using NavisworksTransport.UI.WPF.ViewModels;
using NavisworksTransport.Utils;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Autodesk.Navisworks.Api.DocumentParts;
using NavisApplication = Autodesk.Navisworks.Api.Application;
namespace NavisworksTransport.UI.WPF.Views
{
/// <summary>
/// 别名导航树面板 — 核心 UI 逻辑
/// </summary>
public partial class AliasTreeControl : UserControl
{
// ============================================================
// 状态
// ============================================================
private AliasDataStore _dataStore;
/// <summary>当控件检测到模型就绪但数据存储未连接时触发</summary>
internal event Action OnDatabaseNeeded;
/// <summary>保存别名前触发(确保数据库已创建)</summary>
internal event Action BeforeSaveAlias;
private Dictionary<string, AliasNodeViewModel> _nodeMap = new Dictionary<string, AliasNodeViewModel>();
/// <summary>记录 ExpandAndSelect 按需加载但无别名的深层节点,下次选择时清理</summary>
private HashSet<string> _onDemandNodeKeys = new HashSet<string>();
private bool _isInternalSelection = false;
private Autodesk.Navisworks.Api.ModelItem _currentSelectionItem;
private bool _built = false;
/// <summary>完整展示的最大层级深度默认2层</summary>
public int MaxFullDisplayDepth { get; set; } = 2;
// ============================================================
// 生命周期
// ============================================================
public AliasTreeControl()
{
InitializeComponent();
// 从配置文件读取初始值
MaxFullDisplayDepth = ConfigManager.Instance.Current.AliasTree?.MaxFullDisplayDepth ?? 2;
// 订阅配置变更,实时生效
ConfigManager.Instance.CategoryConfigurationChanged += OnAliasTreeConfigChanged;
// 将 AliasTreeView 的 items 源指向根节点集合
var rootNodes = new System.Collections.ObjectModel.ObservableCollection<AliasNodeViewModel>();
AliasTreeView.ItemsSource = rootNodes;
// 文档切换事件
NavisApplication.ActiveDocumentChanged += (s, e) => { _built = false; RebuildTree(); };
NavisApplication.ActiveDocumentChanging += (s, e) => { _built = false; };
// 模型加载事件:面板启动时文档已存在但模型未加载时触发
var doc = NavisApplication.ActiveDocument;
if (doc != null)
doc.Models.CollectionChanged += (s, e) =>
{
_built = false;
RebuildTree();
OnDatabaseNeeded?.Invoke();
};
HookSelectionEvents();
// 立即构建:面板打开时如果文档已存在,不走事件也要构建
RebuildTree();
}
/// <summary>
/// 获取当前数据存储null 表示未连接)
/// </summary>
public AliasDataStore GetDataStore() => _dataStore;
/// <summary>
/// 设置数据存储(由外部在 PathDatabase 就绪后调用)
/// </summary>
public void SetDataStore(AliasDataStore store)
{
_dataStore = store;
LogManager.Info($"[别名树] SetDataStore: _built={_built}, aliasCount={store?.Count}");
if (!_built)
RebuildTree();
else
RefreshAliasesFromStore();
// 无论树是否已构建,确保已存别名的深层节点路径被加载
var aliasMap = _dataStore?.LoadAll();
if (aliasMap != null && aliasMap.Count > 0)
EnsureAliasedPathsVisible(aliasMap);
}
private void RefreshAliasesFromStore()
{
if (_dataStore == null) return;
try
{
var aliasMap = _dataStore.LoadAll();
LogManager.Info($"[别名树] RefreshAliases: loaded={aliasMap.Count}, nodeMap={_nodeMap.Count}");
int matched = 0;
foreach (var kvp in aliasMap)
{
if (_nodeMap.TryGetValue(kvp.Key, out var node))
{
try
{
node.Alias = kvp.Value;
matched++;
}
catch (Exception ex)
{
LogManager.Error($"[别名树] RefreshAliases set error: key={kvp.Key}, {ex.Message}");
}
}
}
LogManager.Info($"[别名树] RefreshAliases: matched={matched}");
}
catch (Exception ex)
{
LogManager.Error($"[别名树] RefreshAliases 异常: {ex.Message}");
}
}
/// <summary>
/// 清理资源
/// </summary>
public void Cleanup()
{
ConfigManager.Instance.CategoryConfigurationChanged -= OnAliasTreeConfigChanged;
UnhookSelectionEvents();
_dataStore = null;
_nodeMap.Clear();
}
private void OnAliasTreeConfigChanged(object sender, Tuple<ConfigCategory, ConfigurationChangedEventArgs> args)
{
var config = ConfigManager.Instance.Current;
int newDepth = config.AliasTree?.MaxFullDisplayDepth ?? 2;
if (MaxFullDisplayDepth != newDepth)
{
MaxFullDisplayDepth = newDepth;
LogManager.Info($"[别名树] 配置变更: MaxFullDisplayDepth={newDepth}");
_built = false;
RebuildTree();
}
}
// ============================================================
// 选择事件(双向联动)
// ============================================================
private bool _selectionHooked = false;
private void HookSelectionEvents()
{
if (_selectionHooked) return;
try
{
var doc = NavisApplication.ActiveDocument;
if (doc != null)
{
doc.CurrentSelection.Changed += OnNavisSelectionChanged;
_selectionHooked = true;
}
}
catch (Exception ex) { LogManager.Debug($"[别名树] HookSelection事件异常: {ex.Message}"); }
}
private void UnhookSelectionEvents()
{
try
{
var doc = NavisApplication.ActiveDocument;
if (doc != null)
{
doc.CurrentSelection.Changed -= OnNavisSelectionChanged;
}
}
catch (Exception ex) { LogManager.Debug($"[别名树] UnhookSelection异常: {ex.Message}"); }
_selectionHooked = false;
}
/// <summary>
/// 别名树选中 → 同步到 Navisworks 选择
/// </summary>
private void OnTreeViewSelected(AliasNodeViewModel node)
{
// 由 NW 选择触发的 ExpandAndSelect 过程中的被动选中事件,不反向同步
if (_isInternalSelection) return;
LogManager.Debug($"[别名树] OnTreeViewSelected: {node.OriginalName}");
_isInternalSelection = true;
try
{
_currentSelectionItem = node.ModelItem;
TbCurrentNode.Text = $"当前: {node.OriginalName}";
var doc = NavisApplication.ActiveDocument;
if (doc != null && doc.CurrentSelection != null && node.ModelItem != null)
{
var coll = new Autodesk.Navisworks.Api.ModelItemCollection { node.ModelItem };
try
{
doc.CurrentSelection.Clear();
doc.CurrentSelection.CopyFrom(coll);
}
catch (NullReferenceException)
{
// NW API 内部 NRE文档过渡状态忽略
}
catch (Exception ex)
{
LogManager.Error($"[别名树] NW选择异常: {ex.GetType().Name}");
}
}
}
catch (Exception ex)
{
LogManager.Error($"[别名树] 选中同步异常: {ex.Message}");
}
finally
{
_isInternalSelection = false;
}
}
private void StartInlineEdit(AliasNodeViewModel node, TreeViewItem tvi)
{
// 直接用,不需要 tvi
StartInlineEdit(node);
}
private void StartInlineEdit(AliasNodeViewModel node)
{
ClearAllEditing();
node.IsEditing = true;
}
/// <summary>
/// Navisworks 选择变化 → 同步到别名树高亮
/// </summary>
private void OnNavisSelectionChanged(object sender, EventArgs e)
{
if (_isInternalSelection) return;
try
{
var doc = NavisApplication.ActiveDocument;
if (doc == null) return;
var selected = doc.CurrentSelection?.SelectedItems;
if (selected.Count == 1)
{
var item = selected.First();
_currentSelectionItem = item;
TbCurrentNode.Text = $"当前: {item.DisplayName}";
LogManager.Debug($"[别名树] NW选中: {item.DisplayName}");
ExpandAndSelect(item);
}
else
{
_currentSelectionItem = null;
TbCurrentNode.Text = selected.Count > 1 ? $"当前: ({selected.Count} 个选中)" : "当前: 无";
}
}
catch (Exception ex)
{
LogManager.Error($"[别名树] 选择变化处理异常: {ex.Message}");
}
}
// ============================================================
// 树构建
// ============================================================
private void RebuildTree()
{
if (_built) return;
_built = true;
try
{
var rootNodes = AliasTreeView.ItemsSource as System.Collections.ObjectModel.ObservableCollection<AliasNodeViewModel>;
if (rootNodes == null) return;
rootNodes.Clear();
_nodeMap.Clear();
var doc = NavisApplication.ActiveDocument;
if (doc == null || doc.Models == null) return;
// 全量加载别名到内存
Dictionary<string, string> aliasMap = null;
Dictionary<string, string> displayPathToAlias = null;
if (_dataStore != null)
{
aliasMap = _dataStore.LoadAll();
displayPathToAlias = BuildDisplayPathAliasMap(aliasMap);
}
// 遍历所有 root items
foreach (var model in doc.Models)
{
var rootItem = model.RootItem;
if (rootItem != null)
{
var node = BuildNode(rootItem, aliasMap, displayPathToAlias, 0, "", 0);
if (node != null)
rootNodes.Add(node);
}
}
// 重新挂载选择事件(可能文档刚切换)
HookSelectionEvents();
// 对已有别名的深层节点,强制加载其路径使其可见
if (aliasMap != null && aliasMap.Count > 0)
EnsureAliasedPathsVisible(aliasMap);
LogManager.Info($"[别名树] 树构建完成,共 {_nodeMap.Count} 个节点");
}
catch (Exception ex)
{
LogManager.Error($"[别名树] 构建失败: {ex.Message}");
}
}
/// <summary>
/// 递归构建节点树(主线程调用)。
/// siblingIndex 用于同名兄弟去重0 表示第一个(不追加后缀)。
/// </summary>
private AliasNodeViewModel BuildNode(Autodesk.Navisworks.Api.ModelItem modelItem,
Dictionary<string, string> aliasMap,
Dictionary<string, string> displayPathToAlias,
int depth, string parentDisplayPath, int siblingIndex)
{
if (modelItem == null) return null;
string displayName = !string.IsNullOrEmpty(modelItem.DisplayName) ? modelItem.DisplayName : "几何对象";
// 父节点 key 可能是完整格式 "IndexPath||DisplayPath",也可能是 ".../name#N"
// 只取纯 DisplayPath 部分(去掉 || 前缀和结尾的 #N 后缀)
string parentPath = parentDisplayPath;
int sepIdx = parentPath.IndexOf("||", StringComparison.Ordinal);
if (sepIdx >= 0) parentPath = parentPath.Substring(sepIdx + 2);
string displayPath = string.IsNullOrEmpty(parentPath)
? displayName
: parentPath + "/" + displayName;
// 同名兄弟去重:第二个起追加 #索引
string displayPathKey = siblingIndex > 0 ? displayPath + "#" + siblingIndex : displayPath;
// 别名匹配:精确匹配 displayPathKey
string alias = null;
displayPathToAlias?.TryGetValue(displayPathKey, out alias);
var node = new AliasNodeViewModel
{
NodeKey = displayPathKey,
ModelItem = modelItem,
OriginalName = displayName,
Alias = alias,
ChildCount = modelItem.Children.Count(),
Depth = depth
};
_nodeMap[displayPathKey] = node;
// 深度限制:超过阈值且无别名 → 不展开子节点,不加占位
bool hasChildren = node.ChildCount > 0;
if (hasChildren && depth >= MaxFullDisplayDepth && string.IsNullOrEmpty(alias))
hasChildren = false;
if (hasChildren)
{
node.Children.Add(new AliasNodeViewModel
{
DisplayName = "加载中...",
OriginalName = "加载中..."
});
}
return node;
}
private void LazyLoadChildren(AliasNodeViewModel parentNode, Dictionary<string, string> aliasMap,
Dictionary<string, string> displayPathToAlias)
{
// 检查是否只有占位节点
if (parentNode.Children.Count != 1 || parentNode.Children[0].OriginalName != "加载中...")
return;
parentNode.Children.Clear();
try
{
var item = parentNode.ModelItem;
if (item == null) return;
// 同名去重:统计每个名字已出现的次数
var nameCount = new Dictionary<string, int>();
foreach (var childItem in item.Children)
{
string name = !string.IsNullOrEmpty(childItem.DisplayName) ? childItem.DisplayName : "几何对象";
nameCount.TryGetValue(name, out int count);
int siblingIdx = count;
nameCount[name] = count + 1;
var childNode = BuildNode(childItem, aliasMap, displayPathToAlias,
parentNode.Depth + 1, parentNode.NodeKey, siblingIdx);
if (childNode != null)
{
childNode.Parent = parentNode;
parentNode.Children.Add(childNode);
}
}
}
catch (Exception ex)
{
LogManager.Error($"[别名树] 懒加载子节点失败: {ex.Message}");
}
}
// ============================================================
/// <summary>
/// 从 aliasMap 构建 DisplayPath → alias 索引(去掉 IndexPath|| 前缀)
/// </summary>
private static Dictionary<string, string> BuildDisplayPathAliasMap(Dictionary<string, string> aliasMap)
{
if (aliasMap == null || aliasMap.Count == 0) return null;
var map = new Dictionary<string, string>();
foreach (var kvp in aliasMap)
{
// key 格式: "IndexPath||DisplayPath" → 取 DisplayPath 部分
int sepIdx = kvp.Key.IndexOf("||", StringComparison.Ordinal);
string displayPath = sepIdx >= 0 ? kvp.Key.Substring(sepIdx + 2) : kvp.Key;
map[displayPath] = kvp.Value;
}
return map;
}
// ============================================================
/// <summary>
/// 确保节点有完整 NodeKey含 IndexPath。首次保存别名时调用 CreatePathId 升级 key。
/// </summary>
private void EnsureFullNodeKey(AliasNodeViewModel node)
{
if (node == null || node.ModelItem == null) return;
// 已是完整格式
if (node.NodeKey != null && node.NodeKey.Contains("||")) return;
try
{
var identity = AliasNodeIdentity.FromModelItem(node.ModelItem);
string newKey = identity.ToKey();
_nodeMap.Remove(node.NodeKey);
_nodeMap[newKey] = node;
node.NodeKey = newKey;
}
catch (Exception ex)
{
LogManager.Error($"[别名树] EnsureFullNodeKey 失败: {ex.Message}");
}
}
private Autodesk.Navisworks.Api.ModelItem FindModelItemByNodeKey(string key)
{
if (string.IsNullOrEmpty(key)) return null;
// 判断 key 格式:含 "||" 为完整格式 → ResolvePathId否则为 DisplayPath → _nodeMap 查找
int sepIdx = key.IndexOf("||", StringComparison.Ordinal);
if (sepIdx >= 0)
{
if (!AliasNodeIdentity.TryParseKey(key, out var identity) || string.IsNullOrEmpty(identity.IndexPath))
return null;
return ResolveByIndexPath(identity.IndexPath);
}
// 简化格式DisplayPath → 查 _nodeMap
if (_nodeMap.TryGetValue(key, out var node) && node.ModelItem != null)
return node.ModelItem;
return null;
}
private Autodesk.Navisworks.Api.ModelItem ResolveByIndexPath(string indexPath)
{
try
{
var doc = NavisApplication.ActiveDocument;
if (doc == null || doc.Models == null) return null;
int colonIdx = indexPath.IndexOf(':');
if (colonIdx < 0) return null;
if (!int.TryParse(indexPath.Substring(0, colonIdx), out int modelIndex))
return null;
string pathId = indexPath.Substring(colonIdx + 1);
return doc.Models.ResolvePathId(new ModelItemPathId { ModelIndex = modelIndex, PathId = pathId });
}
catch
{
return null;
}
}
// ============================================================
// 展开并高亮指定 ModelItem
// 策略:利用 ModelItem.Parent 链逐层匹配,不依赖 IndexPath。
// ============================================================
private void ExpandAndSelect(Autodesk.Navisworks.Api.ModelItem item)
{
if (item == null) return;
LogManager.Info($"[别名树] ExpandAndSelect: target={item.DisplayName}");
// 整个 ExpandAndSelect 期间阻止树→NW 反向联动
_isInternalSelection = true;
try
{
ExpandAndSelectInternal(item);
}
finally
{
_isInternalSelection = false;
}
}
private void ExpandAndSelectInternal(Autodesk.Navisworks.Api.ModelItem item)
{
// 0. 清理上次按需加载的无别名深层节点
CleanupOnDemandNodes();
// 1. 沿 Parent 链构建祖先列表(根 → 目标)
var chain = new System.Collections.Generic.List<Autodesk.Navisworks.Api.ModelItem>();
var current = item;
while (current != null)
{
chain.Insert(0, current);
current = current.Parent;
}
// 2. 在 rootNodes 中找匹配的根节点
var rootNodes = AliasTreeView.ItemsSource
as System.Collections.ObjectModel.ObservableCollection<AliasNodeViewModel>;
if (rootNodes == null || rootNodes.Count == 0) return;
AliasNodeViewModel treeNode = null;
foreach (var rn in rootNodes)
{
if (rn.ModelItem != null && rn.ModelItem.Equals(chain[0]))
{
treeNode = rn;
break;
}
}
if (treeNode == null)
{
LogManager.Warning($"[别名树] 根节点未找到: {chain[0].DisplayName}");
return;
}
// 3. 目标就是根节点
if (chain.Count == 1)
{
SelectTreeNode(treeNode);
return;
}
// 4. 沿链逐层向下,匹配/加载子节点
for (int i = 1; i < chain.Count; i++)
{
var targetModelItem = chain[i];
// 展开当前节点
ExpandTreeNode(treeNode);
// 加载子节点(有占位 → 全量懒加载;否则只找目标那一个)
var aliasMap = _dataStore?.LoadAll();
var displayAlias = BuildDisplayPathAliasMap(aliasMap);
if (treeNode.Children.Count == 1 && treeNode.Children[0].OriginalName == "加载中...")
{
LazyLoadChildren(treeNode, aliasMap, displayAlias);
}
// 在已加载的子节点中查找匹配
AliasNodeViewModel child = null;
foreach (var c in treeNode.Children)
{
if (c.OriginalName != "加载中..." && c.ModelItem != null && c.ModelItem.Equals(targetModelItem))
{
child = c;
break;
}
}
// 没找到(深度超限节点)→ 直接用 ModelItem 创建
if (child == null)
{
child = BuildNode(targetModelItem, aliasMap, displayAlias,
treeNode.Depth + 1, treeNode.NodeKey, 0);
if (child != null)
{
child.Parent = treeNode;
treeNode.Children.Add(child);
// 超限且无别名 → 下次选择时清理
if (child.Depth >= MaxFullDisplayDepth && !child.HasAlias)
_onDemandNodeKeys.Add(child.NodeKey);
}
}
if (child == null)
{
LogManager.Warning($"[别名树] 无法定位子节点: {targetModelItem.DisplayName}");
return;
}
treeNode = child;
}
// 5. 选中最终节点
SelectTreeNode(treeNode);
}
/// <summary>
/// 展开树节点(程序化,不触发 Expanded 事件)
/// </summary>
private void ExpandTreeNode(AliasNodeViewModel node)
{
var tvi = GetTreeViewItemForNode(node);
if (tvi != null)
{
_isProgrammaticExpand = true;
tvi.IsExpanded = true;
_isProgrammaticExpand = false;
tvi.UpdateLayout();
}
}
/// <summary>
/// 选中并滚动到指定树节点
/// </summary>
private void SelectTreeNode(AliasNodeViewModel node)
{
var parent = node.Parent;
if (parent != null)
{
var parentTvi = GetTreeViewItemForNode(parent);
if (parentTvi != null)
{
parentTvi.BringIntoView();
parentTvi.UpdateLayout();
Dispatcher.BeginInvoke(new Action(() =>
{
AliasTreeView.UpdateLayout();
var childTvi = parentTvi.ItemContainerGenerator.ContainerFromItem(node) as TreeViewItem;
if (childTvi != null)
{
_isProgrammaticSelect = true;
childTvi.IsSelected = true;
_isProgrammaticSelect = false;
childTvi.BringIntoView();
}
}), System.Windows.Threading.DispatcherPriority.Background);
return;
}
}
// 无父节点(根节点)直接选中
var tvi = GetTreeViewItemForNode(node);
if (tvi != null)
{
_isProgrammaticSelect = true;
tvi.IsSelected = true;
_isProgrammaticSelect = false;
tvi.BringIntoView();
}
}
/// <summary>
/// 清理上次按需加载的、无别名的深层节点。
/// ExpandAndSelect 每次调用前执行,确保前一次超限无别名节点被移除。
/// </summary>
private void CleanupOnDemandNodes()
{
if (_onDemandNodeKeys.Count == 0) return;
foreach (var key in _onDemandNodeKeys)
{
if (_nodeMap.TryGetValue(key, out var node))
{
node.Parent?.Children.Remove(node);
_nodeMap.Remove(key);
}
}
_onDemandNodeKeys.Clear();
}
/// <summary>
/// 确保所有已存别名的节点路径在树中可见。
/// 在 RebuildTree 后调用,对深层别名节点逐级加载其祖先。
/// </summary>
private void EnsureAliasedPathsVisible(Dictionary<string, string> aliasMap)
{
LogManager.Info($"[别名树] EnsureAliasedPathsVisible: 共 {aliasMap.Count} 条别名");
foreach (var kvp in aliasMap)
{
LogManager.Info($"[别名树] alias: key={kvp.Key} -> \"{kvp.Value}\"");
}
int loaded = 0;
foreach (var kvp in aliasMap)
{
try
{
LogManager.Debug($"[别名树] EnsureAliased: key={kvp.Key}, alias={kvp.Value}");
var item = FindModelItemByNodeKey(kvp.Key);
if (item == null)
{
LogManager.Debug($"[别名树] EnsureAliased: FindModelItemByNodeKey null, key={kvp.Key}");
continue;
}
if (EnsurePathLoaded(item))
loaded++;
}
catch (Exception ex)
{
LogManager.Error($"[别名树] 加载别名路径失败: {kvp.Key}, {ex.Message}");
}
}
if (loaded > 0)
LogManager.Info($"[别名树] 已确保 {loaded} 个别名节点路径可见");
}
/// <summary>
/// 沿 ModelItem 父链逐层加载节点到树中(不选中)。
/// 逻辑与 ExpandAndSelect 相同,只是最终不选中。
/// </summary>
private bool EnsurePathLoaded(Autodesk.Navisworks.Api.ModelItem item)
{
// 1. 构建祖先链
var chain = new System.Collections.Generic.List<Autodesk.Navisworks.Api.ModelItem>();
var current = item;
while (current != null)
{
chain.Insert(0, current);
current = current.Parent;
}
var chainStr = string.Join(" -> ", chain.ConvertAll(c => c.DisplayName ?? "?"));
LogManager.Debug($"[别名树] EnsurePathLoaded: chain=[{chainStr}], depth={chain.Count - 1}");
// 2. 匹配根节点
var rootNodes = AliasTreeView.ItemsSource
as System.Collections.ObjectModel.ObservableCollection<AliasNodeViewModel>;
if (rootNodes == null) return false;
AliasNodeViewModel treeNode = null;
foreach (var rn in rootNodes)
{
if (rn.ModelItem != null && rn.ModelItem.Equals(chain[0]))
{
treeNode = rn;
break;
}
}
if (treeNode == null) return false;
if (chain.Count == 1) return true;
// 3. 逐层加载
var aliasMap = _dataStore?.LoadAll();
var displayAlias = BuildDisplayPathAliasMap(aliasMap);
for (int i = 1; i < chain.Count; i++)
{
var targetModelItem = chain[i];
ExpandTreeNode(treeNode);
if (treeNode.Children.Count == 1 && treeNode.Children[0].OriginalName == "加载中...")
LazyLoadChildren(treeNode, aliasMap, displayAlias);
AliasNodeViewModel child = null;
foreach (var c in treeNode.Children)
{
if (c.OriginalName != "加载中..." && c.ModelItem != null && c.ModelItem.Equals(targetModelItem))
{
child = c;
break;
}
}
if (child == null)
{
child = BuildNode(targetModelItem, aliasMap, displayAlias,
treeNode.Depth + 1, treeNode.NodeKey, 0);
if (child != null)
{
child.Parent = treeNode;
treeNode.Children.Add(child);
}
}
if (child == null)
{
LogManager.Debug($"[别名树] EnsurePathLoaded: child null at L{i}");
return false;
}
treeNode = child;
}
return true;
}
/// <summary>
/// 从 TreeView 根开始,沿 parent 链找 ViewModel 对应的 TreeViewItem。
/// 虚拟化下可能返回 null。
/// </summary>
private TreeViewItem GetTreeViewItemForNode(AliasNodeViewModel node)
{
if (node == null) return null;
// 收集从根到目标的路径
var path = new List<AliasNodeViewModel>();
var current = node;
while (current != null)
{
path.Insert(0, current);
current = current.Parent;
}
// 从 TreeView 根开始找
ItemsControl container = AliasTreeView;
foreach (var item in path)
{
if (item == null) break;
AliasTreeView.UpdateLayout();
container.UpdateLayout();
var tvi = container.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
if (tvi == null) return null;
container = tvi;
}
return container as TreeViewItem;
}
// ============================================================
// 事件处理:工具栏
// ============================================================
private void OnAliasInputKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
BeforeSaveAlias?.Invoke();
if (_currentSelectionItem == null || _dataStore == null) return;
string input = TxtAliasInput.Text?.Trim();
if (string.IsNullOrEmpty(input)) return;
if (input == _currentSelectionItem?.DisplayName)
input = null;
var identity = AliasNodeIdentity.FromModelItem(_currentSelectionItem);
LogManager.Info($"[别名树] 保存别名: {input}, NodeKey={identity.ToKey()}");
_dataStore.SetAlias(identity, input);
TxtAliasInput.Clear();
RefreshNodeDisplay(identity.ToKey());
}
}
private void OnAutoGenClick(object sender, RoutedEventArgs e)
{
// 阶段 3 实现
SetStatus("自动生成尚未实现");
}
private void OnExportClick(object sender, RoutedEventArgs e)
{
if (_dataStore == null) { SetStatus("数据库未连接"); return; }
var dlg = new Microsoft.Win32.SaveFileDialog
{
Filter = "JSON 文件|*.alias.json",
Title = "导出别名映射"
};
if (dlg.ShowDialog() != true) return;
try
{
var allAliases = _dataStore.LoadAll();
var list = new List<object>();
foreach (var kvp in allAliases)
{
list.Add(new { key = kvp.Key, alias = kvp.Value });
}
var docName = NavisApplication.ActiveDocument?.FileName ?? "unknown";
var data = new
{
version = 1,
exportedAt = DateTime.Now.ToString("s"),
documentName = System.IO.Path.GetFileName(docName),
aliases = list
};
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
var json = serializer.Serialize(data);
System.IO.File.WriteAllText(dlg.FileName, json, System.Text.Encoding.UTF8);
SetStatus($"已导出 {list.Count} 条别名");
}
catch (Exception ex)
{
LogManager.Error($"[别名树] 导出失败: {ex.Message}");
}
}
private void OnImportClick(object sender, RoutedEventArgs e)
{
if (_dataStore == null) { SetStatus("数据库未连接"); return; }
var dlg = new Microsoft.Win32.OpenFileDialog
{
Filter = "JSON 文件|*.json",
Title = "导入别名映射"
};
if (dlg.ShowDialog() != true) return;
try
{
string json = System.IO.File.ReadAllText(dlg.FileName, System.Text.Encoding.UTF8);
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
var data = serializer.Deserialize<Dictionary<string, object>>(json);
var aliasesArr = data["aliases"] as System.Collections.ArrayList;
if (aliasesArr == null) { SetStatus("无效的别名文件"); return; }
var entries = new Dictionary<string, string>();
foreach (Dictionary<string, object> item in aliasesArr)
{
string key = item["key"] as string;
string alias = item["alias"] as string;
if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(alias))
entries[key] = alias;
}
_dataStore.ImportBatch(entries);
RefreshAliasesFromStore();
SetStatus($"已导入 {entries.Count} 条别名");
LogManager.Info($"[别名树] 导入完成: {entries.Count} 条");
}
catch (Exception ex)
{
LogManager.Error($"[别名树] 导入失败: {ex.Message}");
}
}
private void OnClearAllClick(object sender, RoutedEventArgs e)
{
if (_dataStore == null) return;
var result = System.Windows.MessageBox.Show(
"确定要清除全部别名吗?此操作不可撤销。",
"清除全部别名",
MessageBoxButton.YesNo,
MessageBoxImage.Warning);
if (result == MessageBoxResult.Yes)
{
_dataStore.ClearAll();
RebuildTree();
SetStatus("已清除全部别名");
}
}
private void OnRefreshClick(object sender, RoutedEventArgs e)
{
_built = false;
RebuildTree();
SetStatus("树已刷新");
}
// ============================================================
// 事件处理:内联编辑
// ============================================================
private void OnEditClick(object sender, RoutedEventArgs e)
{
if (!(sender is Button btn) || !(btn.Tag is AliasNodeViewModel node))
return;
// 清除所有编辑状态
ClearAllEditing();
node.IsEditing = true;
Dispatcher.BeginInvoke(new Action(() =>
{
var container = AliasTreeView.ItemContainerGenerator.ContainerFromItem(node) as TreeViewItem;
if (container == null) return;
var txt = FindVisualChild<TextBox>(container);
if (txt != null)
{
txt.Focus();
txt.SelectAll();
}
}), System.Windows.Threading.DispatcherPriority.Render);
}
private void OnAliasTextBoxKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
CommitEdit(sender as TextBox);
}
else if (e.Key == Key.Escape)
{
CancelEdit(sender as TextBox);
}
}
private void OnEditTextBoxVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (sender is TextBox tb && tb.IsVisible)
{
Dispatcher.BeginInvoke(new Action(() =>
{
tb.Focus();
tb.SelectAll();
}), System.Windows.Threading.DispatcherPriority.Loaded);
}
}
private void OnAliasTextBoxLostFocus(object sender, RoutedEventArgs e)
{
CommitEdit(sender as TextBox);
}
private void CommitEdit(TextBox textBox)
{
if (textBox?.DataContext is AliasNodeViewModel node)
{
string newAlias = textBox.Text?.Trim();
if (string.IsNullOrEmpty(newAlias) || newAlias == node.OriginalName)
newAlias = null;
BeforeSaveAlias?.Invoke();
if (_dataStore == null) return;
LogManager.Info($"[别名树] 内联编辑: {newAlias}, NodeKey={node.NodeKey}");
try
{
EnsureFullNodeKey(node);
AliasNodeIdentity.TryParseKey(node.NodeKey, out var identity);
_dataStore.SetAlias(identity, newAlias);
}
catch (Exception ex)
{
LogManager.Error($"[别名树] SetAlias 异常: {ex.Message}");
return;
}
node.Alias = newAlias;
node.IsEditing = false;
ApplyAliasHighlight(node.NodeKey, node.HasAlias);
}
}
private void CancelEdit(TextBox textBox)
{
if (textBox?.DataContext is AliasNodeViewModel node)
{
node.RefreshDisplay();
node.IsEditing = false;
}
}
private void ClearAllEditing()
{
foreach (var kvp in _nodeMap)
{
kvp.Value.IsEditing = false;
}
}
// ============================================================
// 事件处理:右键菜单
// ============================================================
private AliasNodeViewModel GetSelectedNode()
{
return AliasTreeView.SelectedItem as AliasNodeViewModel;
}
private void OnCtxEditClick(object sender, RoutedEventArgs e)
{
var node = GetSelectedNode();
LogManager.Info($"[别名树] OnCtxEditClick: node={node?.OriginalName ?? "null"}");
if (node == null) return;
// 从可视化树中查找 TreeViewItem不依赖 ContainerFromItem
var tvi = FindTreeViewItemByDataContext(AliasTreeView, node);
LogManager.Info($"[别名树] OnCtxEditClick: tvi found={tvi != null}");
if (tvi != null)
StartInlineEdit(node, tvi);
}
private static TreeViewItem FindTreeViewItemByDataContext(ItemsControl parent, object dataContext)
{
// 先检查直接子项
for (int i = 0; i < parent.Items.Count; i++)
{
var item = parent.Items[i];
if (item == dataContext)
{
return parent.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
}
if (item is AliasNodeViewModel vm)
{
// 递归搜索子项(先获取容器,再搜它的子项)
var tvi = parent.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
if (tvi != null && tvi.Items.Count > 0)
{
var found = FindTreeViewItemByDataContext(tvi, dataContext);
if (found != null) return found;
}
}
}
return null;
}
private void OnCtxBatchEditClick(object sender, RoutedEventArgs e)
{
var node = GetSelectedNode();
if (node == null) return;
// 阶段 3 实现批量对话框
SetStatus($"批量编辑 {node.NodeKey} 的子节点 — 尚未实现");
}
private void OnCtxClearClick(object sender, RoutedEventArgs e)
{
var node = GetSelectedNode();
if (node == null || _dataStore == null) return;
EnsureFullNodeKey(node);
AliasNodeIdentity.TryParseKey(node.NodeKey, out var identity);
_dataStore.DeleteAlias(identity);
node.Alias = null;
ApplyAliasHighlight(node.NodeKey, false);
}
private void OnCtxFocusClick(object sender, RoutedEventArgs e)
{
var node = GetSelectedNode();
if (node == null || node.IsOrphan) return;
var item = FindModelItemByNodeKey(node.NodeKey);
if (item == null) return;
try
{
var doc = NavisApplication.ActiveDocument;
if (doc == null) return;
var coll = new Autodesk.Navisworks.Api.ModelItemCollection { item };
doc.CurrentSelection.Clear();
doc.CurrentSelection.CopyFrom(coll);
}
catch (Exception ex)
{
LogManager.Error($"[别名树] 聚焦失败: {ex.Message}");
}
}
private void OnCtxCopyNameClick(object sender, RoutedEventArgs e)
{
var node = GetSelectedNode();
if (node == null) return;
try
{
Clipboard.SetText(node.OriginalName);
}
catch (Exception ex) { LogManager.Debug($"[别名树] Clipboard异常: {ex.Message}"); }
}
// ============================================================
// 3D 高亮
// ============================================================
private void ApplyAliasHighlight(string nodeKey, bool highlight)
{
var item = FindModelItemByNodeKey(nodeKey);
if (item == null) return;
var coll = new Autodesk.Navisworks.Api.ModelItemCollection { item };
if (highlight)
{
ModelHighlightHelper.HighlightItems("AliasHighlight", coll);
}
else
{
ModelHighlightHelper.ClearCategory("AliasHighlight");
}
}
// ============================================================
// 辅助
// ============================================================
private void RefreshNodeDisplay(string key)
{
if (_nodeMap.TryGetValue(key, out var node))
{
if (_dataStore != null)
{
AliasNodeIdentity.TryParseKey(key, out var identity);
node.Alias = _dataStore.GetAlias(identity);
}
node.RefreshDisplay();
}
}
private void SetStatus(string message)
{
TbStatus.Text = message;
StatusBarItem.Visibility = Visibility.Visible;
// 3 秒后自动隐藏
var timer = new System.Windows.Threading.DispatcherTimer
{
Interval = TimeSpan.FromSeconds(3)
};
timer.Tick += (s, e2) =>
{
timer.Stop();
StatusBarItem.Visibility = Visibility.Collapsed;
};
timer.Start();
}
private static T FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is T t) return t;
var result = FindVisualChild<T>(child);
if (result != null) return result;
}
return null;
}
// ============================================================
// TreeView 子节点懒加载
// ============================================================
private void OnTreeViewItemExpanded(object sender, RoutedEventArgs e)
{
if (_isProgrammaticExpand) return;
if (!(e.OriginalSource is TreeViewItem tvi) || !(tvi.DataContext is AliasNodeViewModel node))
return;
var aliasMap = _dataStore?.LoadAll();
var displayAlias = BuildDisplayPathAliasMap(aliasMap);
LazyLoadChildren(node, aliasMap, displayAlias);
}
private bool _isProgrammaticSelect;
private bool _isProgrammaticExpand;
/// <summary>
/// 树节点被选中时:如果 500ms 内再次选中同一节点 → 进入内联编辑
/// </summary>
/// <summary>
/// 右键前先选中节点,确保右键菜单操作的是正确节点
/// </summary>
private void OnTreeViewItemRightClick(object sender, MouseButtonEventArgs e)
{
try
{
var tvi = FindAncestor(e.OriginalSource as DependencyObject);
if (tvi != null)
{
tvi.IsSelected = true;
tvi.Focus();
}
}
catch (Exception ex)
{
LogManager.Error($"[别名树] RCK-异常: {ex.GetType().Name}");
}
}
private static TreeViewItem FindAncestor(DependencyObject current)
{
try
{
while (current != null)
{
if (current is TreeViewItem tvi) return tvi;
current = VisualTreeHelper.GetParent(current);
}
}
catch (Exception ex) { LogManager.Warning($"[别名树] FindAncestor异常: {ex.GetType().Name}"); }
return null;
}
/// <summary>
/// 左键点击已选中节点 → 进入内联编辑
/// </summary>
private void OnTreeViewItemLeftClick(object sender, MouseButtonEventArgs e)
{
try
{
// 从点击元素的 DataContext 获取节点,不遍历可视化树
if (e.OriginalSource is FrameworkElement fe && fe.DataContext is AliasNodeViewModel node && !node.IsOrphan)
{
var selNode = GetSelectedNode();
if (node == selNode)
StartInlineEdit(node);
}
}
catch (Exception ex)
{
LogManager.Error($"[别名树] LCK-异常: {ex.GetType().Name}");
}
}
private void OnTreeViewItemSelected(object sender, RoutedEventArgs e)
{
if (e.OriginalSource is TreeViewItem tvi && tvi.DataContext is AliasNodeViewModel node && !node.IsOrphan)
{
if (!_isProgrammaticSelect)
OnTreeViewSelected(node);
}
}
/// <summary>
/// 选中状态下点击 → 进入内联编辑(模仿 DataGrid 行为)
/// </summary>
/// <summary>
/// F2 键 → 进入内联编辑
/// </summary>
private void UserControl_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.F2)
{
EnterEditSelectedNode();
e.Handled = true;
}
}
/// <summary>
/// 双击 → 进入内联编辑
/// </summary>
private void OnTreeViewDoubleClick(object sender, MouseButtonEventArgs e)
{
EnterEditSelectedNode();
}
private void EnterEditSelectedNode()
{
var node = GetSelectedNode();
if (node != null)
{
var tvi = AliasTreeView.ItemContainerGenerator.ContainerFromItem(node) as TreeViewItem;
if (tvi != null) StartInlineEdit(node, tvi);
}
}
}
}