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 { /// /// 别名导航树面板 — 核心 UI 逻辑 /// public partial class AliasTreeControl : UserControl { // ============================================================ // 状态 // ============================================================ private AliasDataStore _dataStore; private string _copiedAliasTreeJson; /// 当控件检测到模型就绪但数据存储未连接时触发 internal event Action OnDatabaseNeeded; /// 保存别名前触发(确保数据库已创建) internal event Action BeforeSaveAlias; private Dictionary _nodeMap = new Dictionary(); /// 记录 ExpandAndSelect 按需加载但无别名的深层节点,下次选择时清理 private HashSet _onDemandNodeKeys = new HashSet(); private bool _isInternalSelection = false; private Autodesk.Navisworks.Api.ModelItem _currentSelectionItem; private bool _built = false; /// 完整展示的最大层级深度(默认2层) 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(); 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(); } /// /// 获取当前数据存储(null 表示未连接) /// public AliasDataStore GetDataStore() => _dataStore; /// /// 设置数据存储(由外部在 PathDatabase 就绪后调用) /// 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); // EnsureAliasedPathsVisible 可能创建了新节点,需要再刷一次别名到这些节点上 if (_built) RefreshAliasesFromStore(); } 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) { int sepIdx = kvp.Key.IndexOf("||", StringComparison.Ordinal); string lookupKey = sepIdx >= 0 ? kvp.Key.Substring(sepIdx + 2) : kvp.Key; if (_nodeMap.TryGetValue(lookupKey, 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}"); } } /// /// 清理资源 /// public void Cleanup() { ConfigManager.Instance.CategoryConfigurationChanged -= OnAliasTreeConfigChanged; UnhookSelectionEvents(); _dataStore = null; _nodeMap.Clear(); } private void OnAliasTreeConfigChanged(object sender, Tuple 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; } /// /// 别名树选中 → 同步到 Navisworks 选择 /// 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; } /// /// Navisworks 选择变化 → 同步到别名树高亮 /// 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; if (rootNodes == null) return; rootNodes.Clear(); _nodeMap.Clear(); var doc = NavisApplication.ActiveDocument; if (doc == null || doc.Models == null) return; // 全量加载别名到内存 Dictionary aliasMap = null; Dictionary 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}"); } } /// /// 递归构建节点树(主线程调用)。 /// siblingIndex 用于同名兄弟去重,0 表示第一个(不追加后缀)。 /// private AliasNodeViewModel BuildNode(Autodesk.Navisworks.Api.ModelItem modelItem, Dictionary aliasMap, Dictionary 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 aliasMap, Dictionary 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(); 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}"); } } // ============================================================ /// /// 从 aliasMap 构建 DisplayPath → alias 索引(去掉 IndexPath|| 前缀) /// private static Dictionary BuildDisplayPathAliasMap(Dictionary aliasMap) { if (aliasMap == null || aliasMap.Count == 0) return null; var map = new Dictionary(); 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; } // ============================================================ /// /// 确保节点有完整 NodeKey(含 IndexPath)。首次保存别名时调用 CreatePathId 升级 key。 /// 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); node.NodeKey = identity.ToKey(); } 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(); var current = item; while (current != null) { chain.Insert(0, current); current = current.Parent; } // 2. 在 rootNodes 中找匹配的根节点 var rootNodes = AliasTreeView.ItemsSource as System.Collections.ObjectModel.ObservableCollection; 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); } /// /// 展开树节点(程序化,不触发 Expanded 事件) /// private void ExpandTreeNode(AliasNodeViewModel node) { var tvi = GetTreeViewItemForNode(node); if (tvi != null) { _isProgrammaticExpand = true; tvi.IsExpanded = true; _isProgrammaticExpand = false; tvi.UpdateLayout(); } } /// /// 选中并滚动到指定树节点 /// 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(); } } /// /// 清理上次按需加载的、无别名的深层节点。 /// ExpandAndSelect 每次调用前执行,确保前一次超限无别名节点被移除。 /// 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(); } /// /// 确保所有已存别名的节点路径在树中可见。 /// 在 RebuildTree 后调用,对深层别名节点逐级加载其祖先。 /// private void EnsureAliasedPathsVisible(Dictionary aliasMap) { LogManager.Info($"[别名树] EnsureAliasedPathsVisible: 共 {aliasMap.Count} 条别名"); foreach (var kvp in aliasMap) { LogManager.Debug($"[别名树] 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} 个别名节点路径可见"); } /// /// 沿 ModelItem 父链逐层加载节点到树中(不选中)。 /// 逻辑与 ExpandAndSelect 相同,只是最终不选中。 /// private bool EnsurePathLoaded(Autodesk.Navisworks.Api.ModelItem item) { // 1. 构建祖先链 var chain = new System.Collections.Generic.List(); 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; 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) { int siblingIdx = NavisworksTransport.Core.AliasTree.AliasNodeIdentity.ComputeSiblingIndex(targetModelItem); child = BuildNode(targetModelItem, aliasMap, displayAlias, treeNode.Depth + 1, treeNode.NodeKey, siblingIdx); 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; } /// /// 从 TreeView 根开始,沿 parent 链找 ViewModel 对应的 TreeViewItem。 /// 虚拟化下可能返回 null。 /// private TreeViewItem GetTreeViewItemForNode(AliasNodeViewModel node) { if (node == null) return null; // 收集从根到目标的路径 var path = new List(); 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(); 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>(json); var aliasesArr = data["aliases"] as System.Collections.ArrayList; if (aliasesArr == null) { SetStatus("无效的别名文件"); return; } var entries = new Dictionary(); foreach (Dictionary 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(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 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}"); } } // ============================================================ // 子树别名导出/导入 / 复制/粘贴 // ============================================================ /// /// 解析带 #N 后缀的段名 ("Room101#1" → "Room101", 1) /// private static void ParseSuffixedSegment(string segment, out string baseName, out int index) { int hashIdx = segment.LastIndexOf('#'); if (hashIdx > 0 && int.TryParse(segment.Substring(hashIdx + 1), out index)) { baseName = segment.Substring(0, hashIdx); } else { baseName = segment; index = 0; } } /// /// 收集源节点子树下所有别名,返回相对路径→别名 字典(不含源节点自身) /// private Dictionary CollectSubtreeAliases(AliasNodeIdentity sourceIdentity) { var result = new Dictionary(); var allAliases = _dataStore.LoadAll(); if (allAliases == null || allAliases.Count == 0) return result; string prefix = sourceIdentity.DisplayPath + "/"; foreach (var kvp in allAliases) { if (!AliasNodeIdentity.TryParseKey(kvp.Key, out var id)) continue; if (id.DisplayPath.StartsWith(prefix, StringComparison.Ordinal)) { string relativePath = id.DisplayPath.Substring(prefix.Length); result[relativePath] = kvp.Value; } } return result; } /// /// 将相对路径→别名 字典应用到目标节点子树。 /// 逐段 Children 寻径,任一缺失则返回错误信息取消全部。 /// private string TryApplySubtreeAliases(AliasNodeViewModel targetNode, Dictionary relativeAliases) { if (relativeAliases == null || relativeAliases.Count == 0) return "没有可导入的别名数据"; EnsureFullNodeKey(targetNode); if (!AliasNodeIdentity.TryParseKey(targetNode.NodeKey, out var targetIdentity)) return "无法解析目标节点标识"; var targetItem = FindModelItemByNodeKey(targetNode.NodeKey); if (targetItem == null) return "无法定位目标模型节点"; var batch = new Dictionary(); foreach (var kvp in relativeAliases) { Autodesk.Navisworks.Api.ModelItem current = targetItem; string[] segments = kvp.Key.Split('/'); bool found = true; foreach (var segment in segments) { ParseSuffixedSegment(segment, out var baseName, out var index); int matchCount = 0; Autodesk.Navisworks.Api.ModelItem matched = null; foreach (var child in current.Children) { if (string.Equals(child.DisplayName, baseName, StringComparison.Ordinal)) { if (matchCount == index) { matched = child; break; } matchCount++; } } if (matched == null) { found = false; break; } current = matched; } if (!found) return $"目标子树缺少路径:{kvp.Key},已取消"; var descIdentity = AliasNodeIdentity.FromModelItem(current); batch[descIdentity.ToKey()] = kvp.Value; } if (batch.Count > 0) { _dataStore.ImportBatch(batch); RefreshAliasesFromStore(); SetStatus($"已导入 {batch.Count} 条别名到目标子树"); } return null; } private void OnCtxExportSubtreeClick(object sender, RoutedEventArgs e) { var node = GetSelectedNode(); if (node == null || _dataStore == null) return; EnsureFullNodeKey(node); if (!AliasNodeIdentity.TryParseKey(node.NodeKey, out var identity)) { SetStatus("无法解析节点标识"); return; } var entries = CollectSubtreeAliases(identity); if (entries.Count == 0) { SetStatus("该节点下没有别名数据"); return; } var defaultName = string.Join("_", identity.DisplayPath.Split(System.IO.Path.GetInvalidFileNameChars(), StringSplitOptions.RemoveEmptyEntries)) + ".alias-subtree.json"; var dlg = new Microsoft.Win32.SaveFileDialog { Filter = "JSON 文件|*.alias-subtree.json", Title = "导出子树别名", FileName = defaultName }; if (dlg.ShowDialog() != true) return; try { var data = new { version = 1, type = "alias_subtree", exportedAt = DateTime.Now.ToString("s"), sourceDisplayPath = identity.DisplayPath, aliases = entries }; var serializer = new System.Web.Script.Serialization.JavaScriptSerializer(); File.WriteAllText(dlg.FileName, serializer.Serialize(data), System.Text.Encoding.UTF8); SetStatus($"已导出 {entries.Count} 条子树别名"); } catch (Exception ex) { LogManager.Error($"[别名树] 子树导出失败: {ex.Message}"); SetStatus("导出失败,见日志"); } } private void OnCtxImportSubtreeClick(object sender, RoutedEventArgs e) { var node = GetSelectedNode(); if (node == null || _dataStore == null) return; var dlg = new Microsoft.Win32.OpenFileDialog { Filter = "JSON 文件|*.alias-subtree.json;*.json", Title = "导入子树别名" }; if (dlg.ShowDialog() != true) return; try { var json = File.ReadAllText(dlg.FileName, System.Text.Encoding.UTF8); var serializer = new System.Web.Script.Serialization.JavaScriptSerializer(); var data = serializer.Deserialize>(json); if (!(data.TryGetValue("type", out var typeObj) && (typeObj as string) == "alias_subtree")) { SetStatus("无效的子树别名文件"); return; } if (!(data.TryGetValue("aliases", out var aliasesObj) && aliasesObj is Dictionary rawAliases)) { SetStatus("别名数据格式无效"); return; } var entries = new Dictionary(); foreach (var kvp in rawAliases) { var alias = kvp.Value as string; if (!string.IsNullOrEmpty(kvp.Key) && !string.IsNullOrEmpty(alias)) entries[kvp.Key] = alias; } if (entries.Count == 0) { SetStatus("文件不包含有效别名"); return; } var error = TryApplySubtreeAliases(node, entries); if (error != null) SetStatus(error); } catch (Exception ex) { LogManager.Error($"[别名树] 子树导入失败: {ex.Message}"); SetStatus("导入失败,见日志"); } } private void OnCtxCopyAliasTreeClick(object sender, RoutedEventArgs e) { var node = GetSelectedNode(); if (node == null || _dataStore == null) return; EnsureFullNodeKey(node); if (!AliasNodeIdentity.TryParseKey(node.NodeKey, out var identity)) { SetStatus("无法解析节点标识"); return; } var entries = CollectSubtreeAliases(identity); if (entries.Count == 0) { SetStatus("该节点下没有别名数据"); return; } var data = new { version = 1, type = "alias_subtree", exportedAt = DateTime.Now.ToString("s"), sourceDisplayPath = identity.DisplayPath, aliases = entries }; var serializer = new System.Web.Script.Serialization.JavaScriptSerializer(); _copiedAliasTreeJson = serializer.Serialize(data); SetStatus($"已复制 {entries.Count} 条别名树"); } private void OnCtxPasteAliasTreeClick(object sender, RoutedEventArgs e) { var node = GetSelectedNode(); if (node == null || _dataStore == null) return; if (string.IsNullOrEmpty(_copiedAliasTreeJson)) { SetStatus("没有已复制的别名树数据"); return; } try { var serializer = new System.Web.Script.Serialization.JavaScriptSerializer(); var data = serializer.Deserialize>(_copiedAliasTreeJson); if (!(data.TryGetValue("type", out var typeObj) && (typeObj as string) == "alias_subtree")) { SetStatus("内存数据无效"); return; } if (!(data.TryGetValue("aliases", out var aliasesObj) && aliasesObj is Dictionary rawAliases)) { SetStatus("别名数据格式无效"); return; } var entries = new Dictionary(); foreach (var kvp in rawAliases) { var alias = kvp.Value as string; if (!string.IsNullOrEmpty(kvp.Key) && !string.IsNullOrEmpty(alias)) entries[kvp.Key] = alias; } if (entries.Count == 0) { SetStatus("别名数据为空"); return; } var error = TryApplySubtreeAliases(node, entries); if (error != null) SetStatus(error); } catch (Exception ex) { LogManager.Debug($"[别名树] 粘贴别名树异常: {ex.Message}"); SetStatus("别名树数据格式无效"); } } // ============================================================ // 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) { int sepIdx = key.IndexOf("||", StringComparison.Ordinal); string lookupKey = sepIdx >= 0 ? key.Substring(sepIdx + 2) : key; if (_nodeMap.TryGetValue(lookupKey, 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(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(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; /// /// 树节点被选中时:如果 500ms 内再次选中同一节点 → 进入内联编辑 /// /// /// 右键前先选中节点,确保右键菜单操作的是正确节点 /// 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; } /// /// 左键点击已选中节点 → 进入内联编辑 /// 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); } } /// /// 选中状态下点击 → 进入内联编辑(模仿 DataGrid 行为) /// /// /// F2 键 → 进入内联编辑 /// private void UserControl_PreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.F2) { EnterEditSelectedNode(); e.Handled = true; } } /// /// 双击 → 进入内联编辑 /// 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); } } } }