Visual Studio 扩展入门(四)菜单篇 上
IDE 菜单栏包含 " 文件"、" 编辑"、" 视图"、" 窗口" 和 " 帮助" 等菜单类别。用户扩展Visual Studio的菜单建议参考官方准则说明:Visual Studio 的菜单和命令。
扩展的菜单在项目的 .vsct 文件中声明。
从 Visual Studio 2019 开始,由扩展提供的顶级菜单放置在 " 扩展 " 菜单下。
一、示例一:在 IDE 菜单栏上创建新菜单
1、创建菜单命令
- 创建 VSIX 项目模板,并命名为TopLevelMenu。
- 通过" Visual c # 项> Extensibility(扩展性) > Command(命令)",添加自定义命令 TopLevelMenuCommand.cs。
此时解决方案目录如下:
2、通过修改_.vsct_ 文件创建新菜单
在
<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>
二、示例二:向菜单中添加子菜单
此示例基于示例一,在示例一的基础上继续扩展。
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>
运行调试,效果如下:
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);
}
运行调试,点击子菜单命令效果如下:
三、示例三:创建动态项列表命令
动态菜单列表以菜单上的占位符开头。 每次显示菜单时,IDE请求VSPackage,显示在占位符中的所有命令 。 动态列表可以出现在菜单上的任何位置。 但是,动态列表通常存储在子菜单上或菜单底部。如窗口菜单底部动态显示当前打开窗口:
通过使用这些设计模式,可以在不影响菜单上其他命令的位置的情况下,使命令的动态列表展开和收缩。
从技术上讲,动态列表还可以应用于工具栏。 但是,官方不建议这种用法,因为工具栏应保持不变,除非用户执行特定步骤来更改它。
在本示例中,动态 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>
运行调试,效果如下:
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
运行调试效果,选中当前“查看当前选择脚本”,效果如下:
再次打开列表,选中C# Scripts 2.cs:
发现列表更新,点击“查看当前选择脚本”,弹出“打开C# Scripts 2.cs”,效果如下:
四、示例四:动态更改菜单命令的文本
在示例三中使用了BeforeQueryStatus事件接口和OleMenuCommand,通过示例四继续加深对这两个部分的理解。为了避免和其他菜单命令混淆,进行清理卸载扩展插件,并创建新的Visx工程。
1、创建菜单命令
- 创建 VSIX 项目模板,并命名为MenuText。
- 通过" Visual c # 项> Extensibility(扩展性) > Command(命令)",添加自定义命令 ChangeMenuText.cs
此时解决方案目录如下:
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程序集查找到相关信息:
BeforeQueryStatus事件处理和OleStatus有关,当调用OleStatus的get访问器就会触发。
运行调试效果,效果如下:
点击"Invoke ChangeMenuText"按钮,弹出默认提示框后,发现按钮名称改变:
五、示例五:更改菜单命令按钮的外观
在示例四的基础上,添加更改菜单命令按钮外观的功能。
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;
}
}
运行调试效果,效果如下:
点击"Invoke ChangeMenuText"按钮,弹出默认提示框后,发现按钮名称改变:
再次点击按钮,按钮不可用: