续集:工作空间一切换,我的插件菜单就消失?------MenuBar与Ribbon的自动重载方案
上一篇我们给插件同时加上了 Ribbon 和传统 MenuBar,以为大功告成。结果用户一句话把我问懵了:
"怎么我切了一下'三维建模',你的工具栏就不见了?"
今天接着聊聊这个现实又烦人的"消失问题",顺便给出一套能自动恢复的监听方案。
一、问题复现:菜单是怎么没的?
上一篇的代码已经能在 AutoCAD 启动时,根据版本和用户习惯把 Ribbon 面板和 MenuBar 菜单都挂上。但测试过程中发现一个规律:
- 用户在"草图与注释"工作空间下一切正常。
- 一旦通过状态栏或
WSCURRENT命令切换到"三维基础""三维建模" "AutoCAD经典",自定义的 Ribbon 选项卡直接蒸发。 - 更离谱的是,如果用户在切换前手动打开了 MenuBar(
MENUBAR=1),切换后自定义下拉菜单有时也会不翼而飞。
原因其实不复杂:AutoCAD 的每一个工作空间(Workspace)都会保存一套独立的界面布局(CUIx),包括 Ribbon 选项卡、菜单栏内容等。插件在启动时临时注入的界面元素,在切换到另一个工作空间后,新工作空间并不会自动包含这些注入项,相当于你的插件只"嫁"给了初始那个工作空间。
所以解决方案的思路很明确------监听工作空间切换事件,一旦检测到切换行为,立即把丢失的自定义菜单和面板重新加载回来 。

二、偷懒的监听办法:靠WSCURRENT系统变量
工作空间切换,本质上就是 WSCURRENT 这个系统变量的值发生了变化。那我们完全可以监听 Application.SystemVariableChanged 事件,当变化的是 WSCURRENT 时,执行重载逻辑。
csharp
// 注册监听
Application.SystemVariableChanged += OnSystemVariableChanged;
private void OnSystemVariableChanged(object sender, SystemVariableChangedEventArgs e)
{
if (e.Name == "WSCURRENT")
{
// 取到新工作空间名称
string newWs = (string)Application.GetSystemVariable("WSCURRENT");
Document doc = Application.DocumentManager.MdiActiveDocument;
doc?.Editor.WriteMessage($"\n工作空间已切换为: {newWs}");
// 重点:不能用当前线程直接操作 UI,必须用 Dispatcher 延后执行
Dispatcher.CurrentDispatcher.BeginInvoke(
new Action(() =>
{
ReloadRibbon();
ReloadMenuBar();
}),
DispatcherPriority.Background);
}
}
这里有一个很容易踩的坑:系统变量改变的事件触发时,AutoCAD 内部可能还没完成完整的 UI 刷新 ,如果立刻去访问 ComponentManager.Ribbon 或者 MenuBar,大概率得到 null 或者旧的引用。用 Dispatcher.BeginInvoke 延迟到后台执行,可以让 AutoCAD 先"喘口气",把工作空间彻底切完我们再动手。
三、Ribbon 面板重载:判断 -- 移除 -- 重建
我的 Ribbon 部分使用了抽象构建器 RibbonBuilderBase,但逻辑可以提炼成通用模式。核心就是三句话:
- 检查当前 Ribbon 中是否已存在自己的选项卡(通过
Id判断)。 - 如果不存在,就重新创建(顺便清理可能残留的无效引用)。
- 创建后强制刷新并激活它。
csharp
private void ReloadRibbon()
{
var ribbon = ComponentManager.Ribbon;
if (ribbon == null) return;
string myTabId = "MyTools_Home"; // 自己的唯一标识
bool exists = ribbon.Tabs
.OfType<RibbonTab>()
.Any(t => t.Id == myTabId);
if (!exists)
{
// 重新创建选项卡(你的自定义创建方法)
RibbonTab myTab = CreateMyRibbonTab();
ribbon.Tabs.Add(myTab);
ribbon.InvalidateVisual();
// 激活该选项卡,方便用户立刻看到
myTab.IsActive = true;
}
}
// 示例:简单的 Ribbon 选项卡创建
private RibbonTab CreateMyRibbonTab()
{
RibbonTab tab = new RibbonTab
{
Id = "MyTools_Home",
Name = "我的工具",
Title = "我的工具"
};
RibbonPanel panel = new RibbonPanel();
panel.Source = new RibbonPanelSource
{
Title = "常用功能"
};
RibbonButton btn = new RibbonButton
{
Name = "btnHello",
Text = "打个招呼",
Size = RibbonItemSize.Large,
ShowText = true,
Orientation = System.Windows.Controls.Orientation.Vertical,
LargeImage = GetMyIcon(), // 加载你喜欢的图标
CommandHandler = new RibbonCommandHandler((sender, e) =>
{
Application.DocumentManager.MdiActiveDocument?
.Editor.WriteMessage("\n你好!这是从 Ribbon 说出来的。");
})
};
panel.Source.Items.Add(btn);
tab.Panels.Add(panel);
return tab;
}
如果你像我一样用了 RibbonBuilderBase,那重建部分只要重新调用 _ribbonBuilder.CreateRibbon() 就行了,还会自动处理面板编排。原理完全相同。
四、MenuBar 菜单重载:宁可杀错,不可漏过
MenuBar 的情况要复杂一丢丢,因为通过 COM 操作时,残留的无效菜单引用可能会导致"已存在"的假象。我的处理策略是:
- 先从配置文件读取自己的菜单定义。
- 检查当前菜单栏中是否已有我的菜单(通过名称判断)。
- 无论是否存在,都先强制移除全部旧菜单,然后再重新创建一次。
这样虽然稍显暴力,但胜在彻底,绝不会出现"看着有,点不了"的僵尸菜单。
csharp
private void ReloadMenuBar()
{
try
{
// 你的菜单配置文件,里面存了菜单项、命令等
string configPath = @"D:\MyPlugin\MenubarInfo.xml";
if (!File.Exists(configPath)) return;
bool stillExists = MenuBarHelper.IsCustomMenuExists(configPath);
if (!stillExists)
{
Logger.Log("菜单丢失,正在重建...");
}
// 先清理所有旧菜单(避免重复或僵尸条目)
MenuBarHelper.RemoveAllCustomMenus(configPath);
// 再重新挂载
MenuBarHelper.CreateMenubar(configPath);
Application.DocumentManager.MdiActiveDocument?
.Editor.WriteMessage("\n自定义菜单已重新加载。");
}
catch (Exception ex)
{
Application.ShowAlertDialog($"菜单重载失败: {ex.Message}");
}
}
这里顺带贴一个简化的 CreateMenubar 核心代码,让没接触过 COM 调用的朋友有个直观感受:
csharp
public static void CreateMenubar(string configPath)
{
dynamic acadApp = Application.AcadApplication;
dynamic menuBar = acadApp.MenuBar;
dynamic menuGroup = acadApp.MenuGroups.Item(0);
string menuName = "我的工具(&M)";
// 1. 如果菜单组里已存在同名菜单,先删掉
foreach (dynamic existing in menuGroup.Menus)
{
if (existing.Name == menuName)
{
existing.Delete();
break;
}
}
// 2. 创建新菜单并塞子项
dynamic newMenu = menuGroup.Menus.Add(menuName);
newMenu.AddMenuItem(newMenu.Count + 1, "问候(&H)", "Hello\n");
newMenu.AddMenuItem(newMenu.Count + 1, "版本(&V)", "About\n");
// 3. 插入到菜单栏最后
newMenu.InsertInMenuBar(menuBar.Count + 1);
}
说明:
Hello和About是你在插件里注册的 AutoCAD 命令,末尾\n代表执行相当于按了回车。你也可以写成"Hello "带个空格,这样会在命令行留个参数输入的机会,视需求而定。
五、为什么 Dispatcher 延迟是个关键
可能有人会问:直接 EnsureExists() 不行吗,非得用 BeginInvoke?
实测结果是:切换工作空间的瞬间,ComponentManager.Ribbon 可能为 null,MenuBar 的 COM 对象也可能抛出异常。AutoCAD 内部在销毁旧 UI 和建立新 UI 之间的时间窗口,你的代码要是撞进去,就会变成"对着空气修电视"。
用 Dispatcher.BeginInvoke 配合 DispatcherPriority.Background,等于把你的重载任务排到当前 UI 线程的空闲时段,等 AutoCAD 自己把工作空间收拾利索了,你再去"补妆",成功率接近 100%。
六、再多说一句:初始化时别忘了注销
监听事件注册一次就好,但要小心热加载(NETLOAD 多次)导致事件重复绑定。我最常用的方法是启动时先解除再绑定,或在 Terminate() 里主动解绑:
csharp
Application.SystemVariableChanged -= OnSystemVariableChanged;
Application.SystemVariableChanged += OnSystemVariableChanged;
这样哪怕你在同一个 AutoCAD 会话里反复 NETLOAD,也只有一个处理程序,避免重载时出现"一次切换弹出三个菜单"的灵异现象。
七、效果与小结
加上这套工作空间监听重载机制后,用户的体验就变成:
- 打开 AutoCAD,我的 Ribbon 选项卡在,MenuBar 也在。
- 切换到"三维建模" → 选项卡消失 → 下一秒自动又冒出来了。
- 再切回"草图与注释" → 照常出现。
- 即便用户完全没打开 Ribbon,只开着顶部菜单栏,自定义的下拉菜单也始终坚挺。
对于最终用户来说,这种感觉就是"这插件挺稳的,怎么切都不会丢",而不会察觉背后我们做了多少次自动检查与重建。
技术服务于人。有些工程师习惯了 Ribbon,有些十几年只用下拉菜单,有些还在用 2014 甚至更早的版本。作为开发者,多花半天把兼容性和稳定性做好,换来的是更广的适用面和更靠谱的口碑。希望这篇续集能帮到你,也欢迎在评论区说说你遇到过哪些界面相关的疑难杂症,我们一起探讨。
以上是"从经典到现代:兼顾 Ribbon 与 MenuBar"系列的下篇。上篇讲了为何要双入口、如何创建 MenuBar,可以在这里回顾。
