续集:工作空间一切换,我的插件菜单就消失?——MenuBar与Ribbon的自动重载方案

续集:工作空间一切换,我的插件菜单就消失?------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,但逻辑可以提炼成通用模式。核心就是三句话:

  1. 检查当前 Ribbon 中是否已存在自己的选项卡(通过 Id 判断)。
  2. 如果不存在,就重新创建(顺便清理可能残留的无效引用)。
  3. 创建后强制刷新并激活它。
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);
}

说明:HelloAbout 是你在插件里注册的 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,可以在这里回顾。

相关推荐
可乐ea2 小时前
【Spring Boot + MyBatis|第7篇】JWT 登录认证与拦截器实现
java·spring boot·后端·mybatis·状态模式
ysn111112 小时前
红点框架系统设计
系统架构·c#
步步为营DotNet2 小时前
借助 C# 14 特性强化 .NET 后端数据验证的深度实践
java·c#·.net
西安邮电大学2 小时前
有关栈的经典算法题
java·后端·其他·算法·面试
摇滚侠3 小时前
SpringMVC 入门到实战 配置类替换 XML 配置文件 86-91
xml·java·后端·spring·maven·intellij-idea
我登哥MVP3 小时前
SpringCloud Alibaba 核心组件解析:服务注册与发现(Nacos)
java·spring boot·后端·spring·spring cloud·java-ee·maven
摇滚侠3 小时前
SpringMVC 入门到实战 处理静态资源的过程 64
java·后端·spring·maven·intellij-idea
影寂ldy3 小时前
C# 泛型委托
java·算法·c#
Liquad Li3 小时前
ABP vNext 标准分层解决方案项目结构完整解析
后端