Visual Studio 扩展入门(四)菜单篇 上

IDE 菜单栏包含 " 文件"、" 编辑"、" 视图"、" 窗口" 和 " 帮助" 等菜单类别。用户扩展Visual Studio的菜单建议参考官方准则说明:Visual Studio 的菜单和命令
扩展的菜单在项目的 .vsct 文件中声明。

从 Visual Studio 2019 开始,由扩展提供的顶级菜单放置在 " 扩展 " 菜单下。

一、示例一:在 IDE 菜单栏上创建新菜单

1、创建菜单命令

  1. 创建 VSIX 项目模板,并命名为TopLevelMenu。
  2. 通过" Visual c # 项> Extensibility(扩展性) > Command(命令)",添加自定义命令 TopLevelMenuCommand.cs。

此时解决方案目录如下:
image.png
2、通过修改_.vsct_ 文件创建新菜单
 节点内包含多个节点,找到name属性为"guidTopLevelMenuPackageCmdSet "的节点,添加元素,如下:

<IDSymbol name="TopLevelMenu" value="0x1021"/>

在<Commands>节点之内,<Groups>节点之前创建<Menus>节点,并添加<Menu>节点,如下:

    <Menus>
	    <Menu guid="guidTopLevelMenuPackageCmdSet" id="TopLevelMenu" priority="0x700" type="Menu">
		    <Parent guid="guidSHLMainMenu" id="IDG_VS_MM_TOOLSADDINS" />
		    <Strings>
			    <ButtonText>Test Menu</ButtonText>
		    </Strings>
	    </Menu>
    </Menus>

在 <Groups> 部分中,找到 <Group> 并将元素更改 <Parent> 为指向刚刚添加的菜单:

      <Group guid="guidTopLevelMenuPackageCmdSet" id="MyMenuGroup" priority="0x0600">
	      <Parent guid="guidTopLevelMenuPackageCmdSet" id="TopLevelMenu"/>
      </Group>

在 <Buttons> 部分中,找到 <Button> 节点。 然后,在 <Strings> 节点中,将 <ButtonText> 元素更改为 调用 TopLevelMenuCommand:

<ButtonText>调用 TopLevelMenuCommand</ButtonText>

调试效果如下:
image.png
点击命令效果:
image.png

二、示例二:向菜单中添加子菜单

此示例基于示例一,在示例一的基础上继续扩展。
1、通过修改.vsct 文件创建子菜单
打开TopLevelMenuPackage. vsct。
继续找到name属性为"guidTopLevelMenuPackageCmdSet "的<GuidSymbol>节点,添加<IDSymbol>元素,如下:

			<IDSymbol name="SubMenu" value="0x1100"/>
			<IDSymbol name="SubMenuGroup" value="0x1150"/>
			<IDSymbol name="cmdidTestSubCommand" value="0x0105"/>

将新创建的子菜单添加到 <Menus> 部分:

			<Menu guid="guidTopLevelMenuPackageCmdSet" id="SubMenu" priority="0x0100" type="Menu">
				<Parent guid="guidTopLevelMenuPackageCmdSet" id="MyMenuGroup"/>
				<Strings>
					<ButtonText>Sub Menu</ButtonText>
					<CommandName>Sub Menu</CommandName>
				</Strings>
			</Menu>

将定义的菜单组添加到 <Groups> 部分,并使其成为子菜单的子菜单。

			<Group guid="guidTopLevelMenuPackageCmdSet" id="SubMenuGroup" priority="0x0000">
				<Parent guid="guidTopLevelMenuPackageCmdSet" id="SubMenu"/>
			</Group>

向元素<Buttons>部分添加一个新 <Button>  ,以将定义的命令cmdidTestSubCommand成为子菜单上的一项。

			<Button guid="guidTopLevelMenuPackageCmdSet" id="cmdidTestSubCommand" priority="0x0000" type="Button">
				<Parent guid="guidTopLevelMenuPackageCmdSet" id="SubMenuGroup" />
				<Icon guid="guidImages" id="bmpPic2" />
				<Strings>
					<CommandName>cmdidTestSubCommand</CommandName>
					<ButtonText>调用Sub Command</ButtonText>
				</Strings>
			</Button>

运行调试,效果如下:
image.png
2、添加命令
打开自定义命令TopLevelMenuCommand.cs,添加命令定义:

public const int cmdidTestSubCmd = 0x0105;

在构造方法,为子菜单添命令。

CommandID subCommandID = new CommandID(CommandSet, cmdidTestSubCmd);
MenuCommand subItem = new MenuCommand(new EventHandler(SubItemCallback), subCommandID);
commandService.AddCommand(subItem);

添加事件回调:

private void SubItemCallback(object sender, EventArgs e)
{
    ThreadHelper.ThrowIfNotOnUIThread();
    string message = string.Format(CultureInfo.CurrentCulture, "Inside {0}.SubItemCallback()", this.GetType().FullName);
    string title = "CmdIdTestSubCmd";

    // Show a message box to prove we were here
    VsShellUtilities.ShowMessageBox(
        this.package,
        message,
        title,
        OLEMSGICON.OLEMSGICON_INFO,
        OLEMSGBUTTON.OLEMSGBUTTON_OK,
        OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}

运行调试,点击子菜单命令效果如下:
image.png

三、示例三:创建动态项列表命令


动态菜单列表以菜单上的占位符开头。 每次显示菜单时,IDE请求VSPackage,显示在占位符中的所有命令 。 动态列表可以出现在菜单上的任何位置。 但是,动态列表通常存储在子菜单上或菜单底部。如窗口菜单底部动态显示当前打开窗口:
image.png
通过使用这些设计模式,可以在不影响菜单上其他命令的位置的情况下,使命令的动态列表展开和收缩。
从技术上讲,动态列表还可以应用于工具栏。 但是,官方不建议这种用法,因为工具栏应保持不变,除非用户执行特定步骤来更改它。

在本示例中,动态 MRU 列表显示在现有子菜单的底部,并与子菜单的其余部分分隔开。此示例基于示例二,在示例二的基础上继续扩展。
1、通过修改_.vsct_ 文件创建动态项占位
在 Symbols 部分中,在 GuidSymbol 名为 guidTestCommandPackageCmdSet 的节点中,添加 MRUListGroup 组和命令的符号 cmdidMRUList ,如下所示:

			<IDSymbol name="MRUListGroup" value="0x1200"/>
			<IDSymbol name="cmdidMRUList" value="0x0200"/>

在Groups元素部分,添加声明的组:

			<Group guid="guidTopLevelMenuPackageCmdSet" id="MRUListGroup" priority="0x0100">
				<Parent guid="guidTopLevelMenuPackageCmdSet" id="SubMenu"/>
			</Group>

添加动态命令“查看当前选择脚本”占位按钮,DynamicItemStart标志允许动态生成命令:

			<Button guid="guidTopLevelMenuPackageCmdSet" id="cmdidMRUList" type="Button" priority="0x0100">
				<Parent guid="guidTopLevelMenuPackageCmdSet" id="MRUListGroup" />
				<CommandFlag>DynamicItemStart</CommandFlag>
				<Strings>
					<CommandName>cmdidMRUList</CommandName>
					<ButtonText>查看当前选择脚本</ButtonText>
				</Strings>
			</Button>

运行调试,效果如下:
image.png
2、填充 MRU 列表
添加命名空间:

using System.Collections;

添加命令定义:

public const uint cmdidMRUList = 0x200;

在构造函数内添加以下代码:

this.InitMRUMenu(commandService);

添加以下代码:
InitMRUMenu():初始化 MRU list 菜单命令
InitializeMRUList():初始化在 MRU 列表中显示的项的字符串列表。通过mc.Visible = false;可以将命令设为不可见。
BeforeQueryStatus为显示菜单命令之前调用的事件处理。

        #region InitializeMRU

        private int numMRUItems = 4;//生成项数
        private int baseMRUID = (int)cmdidMRUList;//初始占位符ID
        private ArrayList mruList;//命令名称字符串

        /// <summary>
        /// 初始化在 MRU 列表中显示的项的字符串列表
        /// </summary>
        private void InitializeMRUList()
        {
            if (null != mruList) return;

            mruList = new ArrayList();

            for (int i = 0; i < this.numMRUItems; i++) mruList.Add(string.Format(CultureInfo.CurrentCulture, "C# Script {0}.cs", i + 1));
        }
        /// <summary>
        /// 初始化 MRU list 菜单命令
        /// </summary>
        /// <param name="mcs"></param>
        private void InitMRUMenu(OleMenuCommandService mcs)
        {
            InitializeMRUList();

            for (int i = 0; i < this.numMRUItems; i++)
            {
                var cmdID = new CommandID(CommandSet, this.baseMRUID + i);//赋值ID
                var mc = new OleMenuCommand(OnMRUExec, cmdID);
                mc.BeforeQueryStatus += OnMRUQueryStatus;//当客户端请求命令的状态时调用,准备打卡
                mcs.AddCommand(mc);
            }

        }
        /// <summary>
        /// 显示回调
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnMRUQueryStatus(object sender, EventArgs e)
        {
            OleMenuCommand menuCommand = sender as OleMenuCommand;
            if (null == menuCommand) return;

            int MRUItemIndex = menuCommand.CommandID.ID - this.baseMRUID;
            if (MRUItemIndex >= 0 && MRUItemIndex < this.mruList.Count)
            {
                menuCommand.Text = this.mruList[MRUItemIndex] as string;
            }

        }
        /// <summary>
        /// 触发处理命令回调
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnMRUExec(object sender, EventArgs e)
        {
            var menuCommand = sender as OleMenuCommand;
            if (null == menuCommand) return;

            int MRUItemIndex = menuCommand.CommandID.ID - this.baseMRUID;

            if (MRUItemIndex >= 0 && MRUItemIndex < this.mruList.Count)
            {
                string selection = this.mruList[MRUItemIndex] as string;
                //向后排序
                for (int i = MRUItemIndex; i > 0; i--)
                {
                    this.mruList[i] = this.mruList[i - 1];
                }
                this.mruList[0] = selection;

                System.Windows.Forms.MessageBox.Show(string.Format(CultureInfo.CurrentCulture, "打开脚本 {0}", selection));
            }

        }
        #endregion

运行调试效果,选中当前“查看当前选择脚本”,效果如下:
image.png
再次打开列表,选中C# Scripts 2.cs:
image.png
发现列表更新,点击“查看当前选择脚本”,弹出“打开C# Scripts 2.cs”,效果如下:
image.png
image.png

四、示例四:动态更改菜单命令的文本

在示例三中使用了BeforeQueryStatus事件接口和OleMenuCommand,通过示例四继续加深对这两个部分的理解。为了避免和其他菜单命令混淆,进行清理卸载扩展插件,并创建新的Visx工程。


1、创建菜单命令

  1. 创建 VSIX 项目模板,并命名为MenuText。
  2. 通过" Visual c # 项> Extensibility(扩展性) > Command(命令)",添加自定义命令 ChangeMenuText.cs

此时解决方案目录如下:
image.png

2、修改.vsct文件,添加标志
添加动态命令“Invoke ChangeMenuText”占位按钮,并添加添加<CommandFlag>元素,TextChanges标志命令文本可修改:

      <Button guid="guidMenuTextPackageCmdSet" id="ChangeMenuTextId" priority="0x0100" type="Button">
        <Parent guid="guidMenuTextPackageCmdSet" id="MyMenuGroup" />
        <Icon guid="guidImages" id="bmpPic1" />
        <CommandFlag>TextChanges</CommandFlag>
        <Strings>
          <ButtonText>Invoke ChangeMenuText</ButtonText>
        </Strings>
      </Button>

3、修改_ChangeMenuText.cs_添加事件
修改ChangeMenuText构造方法,使用OleMenuCommand命令替代MenuCommand,并注册BeforeQueryStatus的事件处理。代码如下:

        private ChangeMenuText(AsyncPackage package, OleMenuCommandService commandService)
        {
            this.package = package ?? throw new ArgumentNullException(nameof(package));
            commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));

            var menuCommandID = new CommandID(CommandSet, CommandId);
            var menuItem = new OleMenuCommand(this.Execute, menuCommandID);
            menuItem.BeforeQueryStatus += OnBeforeQueryStatus;
            commandService.AddCommand(menuItem);
        }

添加OnBeforeQueryStatus实现:

        private void OnBeforeQueryStatus(object sender, EventArgs e)
        {
            var myCommand = sender as OleMenuCommand;
            if (null != myCommand)
            {
                myCommand.Text = "New Text";
            }
        }
  • OleMenuCommand继承自MenuCommand,并包含BeforeQueryStatus事件处理。对BeforeQueryStatus,官方文档的说明只有一句话:“当客户端请求命令时调用”。但在事件内加入弹窗或断点调试时发现,请求命令时机并不是特表明确,不像Execute()方法一样,点击即刻触发。我通过反编译Microsoft.VisualStudio.Shell.15.0.dll程序集查找到相关信息:

image.png
BeforeQueryStatus事件处理和OleStatus有关,当调用OleStatus的get访问器就会触发。

运行调试效果,效果如下:
image.png
点击"Invoke ChangeMenuText"按钮,弹出默认提示框后,发现按钮名称改变:
image.png

五、示例五:更改菜单命令按钮的外观

在示例四的基础上,添加更改菜单命令按钮外观的功能。

1、修改ChangeMenuText.cs

修改Execute命令执行方法,当命令按钮文本变成“New Text”时,触发命令按钮后,按钮不可选中:

        private void Execute(object sender, EventArgs e)
        {
            ThreadHelper.ThrowIfNotOnUIThread();
            var command = sender as OleMenuCommand;
            if (command.Text == "New Text") ChangeMyCommand(command.CommandID.ID, false);
        }
        public bool ChangeMyCommand(int cmdID, bool enableCmd)
        {
            bool cmdUpdated = false;
            var mcs = this.package.GetService<IMenuCommandService, OleMenuCommandService>();
            var newCmdID = new CommandID(CommandSet, cmdID);
            MenuCommand mc = mcs.FindCommand(newCmdID);
            if (mc != null)
            {
                mc.Enabled = enableCmd;
                //mc.Checked = true;
                cmdUpdated = true;
            }
            return cmdUpdated;
        }
  • 通过package.GetService<IMenuCommandService, OleMenuCommandService>():获取当前包的菜单服务。
  • 通过调用服务FindCommand:通过CommandID查找到菜单命令。
  • 通过Enabled属性修改命令按钮不可用。通过Visible可设为不可见。通过Checked可设置选中状态。

上述代码也可以直接简写为:

        private void Execute(object sender, EventArgs e)
        {
            ThreadHelper.ThrowIfNotOnUIThread();
            var command = sender as OleMenuCommand;
            if (command.Text == "New Text")
            {
                MenuCommand mc = package.GetService<IMenuCommandService, OleMenuCommandService>().FindCommand(command.CommandID);
                if (mc != null) mc.Enabled = false;
            }
        }

运行调试效果,效果如下:
image.png
点击"Invoke ChangeMenuText"按钮,弹出默认提示框后,发现按钮名称改变:
image.png
再次点击按钮,按钮不可用:
image.png

博客的示例源码:https://github.com/21thCenturyBoy/VSIX_HelloWorld

posted @ 2021-07-15 10:48  20世纪少年  阅读(553)  评论(0编辑  收藏  举报