动态添加菜单项
适用范围:Visual Studio
Visual Studio for Mac
Visual Studio Code
可以在运行时添加菜单项,方法是在 Visual Studio 命令表 (.vsct) 文件的占位符按钮定义上指定DynamicItemStart
命令标志,然后在代码中定义 () 要显示和处理命令 () 。 加载 VSPackage 时,占位符将替换为动态菜单项。
Visual Studio 使用“ 最近使用的 (MRU) 列表中的动态列表,该列表显示最近打开的文档的名称, Windows 列表显示 当前打开的窗口的名称。 DynamicItemStart
命令定义上的 标志指定命令是一个占位符,直到打开 VSPackage。 打开 VSPackage 时,占位符将替换为在运行时创建并添加到动态列表的 0 个或多个命令。 在打开 VSPackage 之前,你可能无法在显示动态列表的菜单上看到该位置。 为了填充动态列表,Visual Studio 要求 VSPackage 查找 ID 为的命令,该命令的开头字符与占位符的 ID 相同。 当 Visual Studio 找到匹配的命令时,它会将该命令的名称添加到动态列表中。 然后,它会递增 ID 并查找另一个要添加到动态列表的匹配命令,直到没有更多动态命令。
本演练演示如何使用解决方案资源管理器工具栏上的 命令在 Visual Studio 解决方案中设置启动项目。 它使用菜单控制器,该控制器具有活动解决方案中项目的动态下拉列表。 为了防止在未打开解决方案或打开的解决方案只有一个项目时显示此命令,仅当解决方案具有多个项目时才加载 VSPackage。
有关 .vsct 文件的详细信息,请参阅 Visual Studio 命令表 (.vsct) 文件。
使用菜单命令创建扩展
创建名为 的
DynamicMenuItems
VSIX 项目。当项目打开时,添加自定义命令项模板并将其命名为 DynamicMenu。 有关详细信息,请参阅 使用菜单命令创建扩展。
设置 .vsct 文件中的元素
若要在工具栏上创建包含动态菜单项的菜单控制器,请指定以下元素:
两个命令组,一个包含菜单控制器,另一个包含下拉列表中的菜单项
一个 类型的菜单元素
MenuController
两个按钮,一个按钮充当菜单项的占位符,另一个按钮提供工具栏上的图标和工具提示。
在 DynamicMenuPackage.vsct 中,定义命令 ID。 转到“符号”部分,替换 guidDynamicMenuPackageCmdSet GuidSymbol 块中的 IDSymbol 元素。 需要为两个组(菜单控制器、占位符命令和定位点命令)定义 IDSymbol 元素。
<GuidSymbol name="guidDynamicMenuPackageCmdSet" value="{ your GUID here }"> <IDSymbol name="MyToolbarItemGroup" value="0x1020" /> <IDSymbol name="MyMenuControllerGroup" value="0x1025" /> <IDSymbol name="MyMenuController" value ="0x1030"/> <IDSymbol name="cmdidMyAnchorCommand" value="0x0103" /> <!-- NOTE: The following command expands at run time to some number of ids. Try not to place command ids after it (e.g. 0x0105, 0x0106). If you must add a command id after it, make the gap very large (e.g. 0x200) --> <IDSymbol name="cmdidMyDynamicStartCommand" value="0x0104" /> </GuidSymbol>
在“组”部分中,删除现有组并添加刚刚定义的两个组:
<Groups> <!-- The group that adds the MenuController on the Solution Explorer toolbar. The 0x4000 priority adds this group after the group that contains the Preview Selected Items button, which is normally at the far right of the toolbar. --> <Group guid="guidDynamicMenuPackageCmdSet" id="MyToolbarItemGroup" priority="0x4000" > <Parent guid="guidSHLMainMenu" id="IDM_VS_TOOL_PROJWIN" /> </Group> <!-- The group for the items on the MenuController drop-down. It is added to the MenuController submenu. --> <Group guid="guidDynamicMenuPackageCmdSet" id="MyMenuControllerGroup" priority="0x4000" > <Parent guid="guidDynamicMenuPackageCmdSet" id="MyMenuController" /> </Group> </Groups>
添加 MenuController。 设置 DynamicVisibility 命令标志,因为它并不总是可见的。 不显示 ButtonText。
<Menus> <!-- The MenuController to display on the Solution Explorer toolbar. Place it in the ToolbarItemGroup.--> <Menu guid="guidDynamicMenuPackageCmdSet" id="MyMenuController" priority="0x1000" type="MenuController"> <Parent guid="guidDynamicMenuPackageCmdSet" id="MyToolbarItemGroup" /> <CommandFlag>DynamicVisibility</CommandFlag> <Strings> <ButtonText></ButtonText> </Strings> </Menu> </Menus>
添加两个按钮,一个作为动态菜单项的占位符,一个作为 MenuController 的定位点。
占位符按钮的父级是 MyMenuControllerGroup。 将 DynamicItemStart、DynamicVisibility 和 TextChanges 命令标志添加到占位符按钮。 不显示 ButtonText。
定位点按钮保存图标和工具提示文本。 定位点按钮的父级也是 MyMenuControllerGroup。 添加 NoShowOnMenuController 命令标志以确保按钮不会实际显示在菜单控制器下拉列表中,并添加 FixMenuController 命令标志,使其成为永久定位点。
<!-- The placeholder for the dynamic items that expand to N items at run time. --> <Buttons> <Button guid="guidDynamicMenuPackageCmdSet" id="cmdidMyDynamicStartCommand" priority="0x1000" > <Parent guid="guidDynamicMenuPackageCmdSet" id="MyMenuControllerGroup" /> <CommandFlag>DynamicItemStart</CommandFlag> <CommandFlag>DynamicVisibility</CommandFlag> <CommandFlag>TextChanges</CommandFlag> <!-- This text does not appear. --> <Strings> <ButtonText>Project</ButtonText> </Strings> </Button> <!-- The anchor item to supply the icon/tooltip for the MenuController --> <Button guid="guidDynamicMenuPackageCmdSet" id="cmdidMyAnchorCommand" priority="0x0000" > <Parent guid="guidDynamicMenuPackageCmdSet" id="MyMenuControllerGroup" /> <!-- This is the icon that appears on the Solution Explorer toolbar. --> <Icon guid="guidImages" id="bmpPicArrows"/> <!-- Do not show on the menu controller's drop down list--> <CommandFlag>NoShowOnMenuController</CommandFlag> <!-- Become the permanent anchor item for the menu controller --> <CommandFlag>FixMenuController</CommandFlag> <!-- The text that appears in the tooltip.--> <Strings> <ButtonText>Set Startup Project</ButtonText> </Strings> </Button> </Buttons>
将图标添加到 resources 文件夹) 中的项目 (,然后在 .vsct 文件中添加对它的引用。 在本演练中,我们使用项目模板中包含的箭头图标。
在“命令”部分的前面添加 VisibilityConstraints 节。 (如果在 Symbols.) 本部分确保仅在加载具有多个项目的解决方案时显示菜单控制器,可能会收到警告。
<VisibilityConstraints> <!--Make the MenuController show up only when there is a solution with more than one project loaded--> <VisibilityItem guid="guidDynamicMenuPackageCmdSet" id="MyMenuController" context="UICONTEXT_SolutionHasMultipleProjects"/> </VisibilityConstraints>
实现动态菜单命令
创建继承自 OleMenuCommand的动态菜单命令类。 在此实现中,构造函数指定用于匹配命令的谓词。 必须重写 方法才能 DynamicItemMatch 使用此谓词来设置 MatchedCommandId 属性,该属性标识要调用的命令。
创建名为 DynamicItemMenuCommand.cs 的新 C# 类文件,并添加继承自 OleMenuCommand的名为 DynamicItemMenuCommand 的类:
class DynamicItemMenuCommand : OleMenuCommand { }
添加以下 using 指令:
using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using System.ComponentModel.Design;
添加专用字段以存储匹配谓词:
private Predicate<int> matches;
添加继承自构造函数的构造函数, OleMenuCommand 并指定命令处理程序和 BeforeQueryStatus 处理程序。 添加谓词以匹配命令:
public DynamicItemMenuCommand(CommandID rootId, Predicate<int> matches, EventHandler invokeHandler, EventHandler beforeQueryStatusHandler) : base(invokeHandler, null /*changeHandler*/, beforeQueryStatusHandler, rootId) { if (matches == null) { throw new ArgumentNullException("matches"); } this.matches = matches; }
重写 方法, DynamicItemMatch 以便它调用匹配谓词并设置 MatchedCommandId 属性:
public override bool DynamicItemMatch(int cmdId) { // Call the supplied predicate to test whether the given cmdId is a match. // If it is, store the command id in MatchedCommandid // for use by any BeforeQueryStatus handlers, and then return that it is a match. // Otherwise clear any previously stored matched cmdId and return that it is not a match. if (this.matches(cmdId)) { this.MatchedCommandId = cmdId; return true; } this.MatchedCommandId = 0; return false; }
添加命令
可在 DynamicMenu 构造函数中设置菜单命令,包括动态菜单和菜单项。
在 DynamicMenuPackage.cs 中,添加命令集的 GUID 和命令 ID:
public const string guidDynamicMenuPackageCmdSet = "00000000-0000-0000-0000-00000000"; // get the GUID from the .vsct file public const uint cmdidMyCommand = 0x104;
在 DynamicMenu.cs 文件中,添加以下 using 指令:
using EnvDTE; using EnvDTE80; using System.ComponentModel.Design;
在 类中
DynamicMenu
,添加私有字段 dte2。private DTE2 dte2;
添加专用 rootItemId 字段:
private int rootItemId = 0;
在 DynamicMenu 构造函数中,添加菜单命令。 在下一部分中,我们将定义命令处理程序、
BeforeQueryStatus
事件处理程序和 match 谓词。private DynamicMenu(Package package) { if (package == null) { throw new ArgumentNullException(nameof(package)); } this.package = package; OleMenuCommandService commandService = this.ServiceProvider.GetService(typeof(IMenuCommandService)) as OleMenuCommandService; if (commandService != null) { // Add the DynamicItemMenuCommand for the expansion of the root item into N items at run time. CommandID dynamicItemRootId = new CommandID( new Guid(DynamicMenuPackageGuids.guidDynamicMenuPackageCmdSet), (int)DynamicMenuPackageGuids.cmdidMyCommand); DynamicItemMenuCommand dynamicMenuCommand = new DynamicItemMenuCommand( dynamicItemRootId, IsValidDynamicItem, OnInvokedDynamicItem, OnBeforeQueryStatusDynamicItem); commandService.AddCommand(dynamicMenuCommand); } this.dte2 = (DTE2)this.ServiceProvider.GetService(typeof(DTE)); }
实现处理程序
若要在菜单控制器上实现动态菜单项,必须在单击动态项时处理命令。 还必须实现设置菜单项状态的逻辑。 将处理程序添加到 DynamicMenu
类。
若要实现 “设置启动项目” 命令,请添加 OnInvokedDynamicItem 事件处理程序。 它会查找其名称与已调用的命令文本相同的项目,并通过在 属性中 StartupProjects 设置其绝对路径将其设置为启动项目。
private void OnInvokedDynamicItem(object sender, EventArgs args) { DynamicItemMenuCommand invokedCommand = (DynamicItemMenuCommand)sender; // If the command is already checked, we don't need to do anything if (invokedCommand.Checked) return; // Find the project that corresponds to the command text and set it as the startup project var projects = dte2.Solution.Projects; foreach (Project proj in projects) { if (invokedCommand.Text.Equals(proj.Name)) { dte2.Solution.SolutionBuild.StartupProjects = proj.FullName; return; } } }
OnBeforeQueryStatusDynamicItem
添加 事件处理程序。 这是在事件之前调用的QueryStatus
处理程序。 它确定菜单项是否为“真实”项,即不是占位符项,以及该项是否已 (表示该项目已设置为启动项目) 。private void OnBeforeQueryStatusDynamicItem(object sender, EventArgs args) { DynamicItemMenuCommand matchedCommand = (DynamicItemMenuCommand)sender; matchedCommand.Enabled = true; matchedCommand.Visible = true; // Find out whether the command ID is 0, which is the ID of the root item. // If it is the root item, it matches the constructed DynamicItemMenuCommand, // and IsValidDynamicItem won't be called. bool isRootItem = (matchedCommand.MatchedCommandId == 0); // The index is set to 1 rather than 0 because the Solution.Projects collection is 1-based. int indexForDisplay = (isRootItem ? 1 : (matchedCommand.MatchedCommandId - (int) DynamicMenuPackageGuids.cmdidMyCommand) + 1); matchedCommand.Text = dte2.Solution.Projects.Item(indexForDisplay).Name; Array startupProjects = (Array)dte2.Solution.SolutionBuild.StartupProjects; string startupProject = System.IO.Path.GetFileNameWithoutExtension((string)startupProjects.GetValue(0)); // Check the command if it isn't checked already selected matchedCommand.Checked = (matchedCommand.Text == startupProject); // Clear the ID because we are done with this item. matchedCommand.MatchedCommandId = 0; }
实现命令 ID 匹配谓词
现在实现匹配谓词。 我们需要确定两个事项:第一,命令 ID 是否有效 (它大于或等于) 声明的命令 ID;第二,它是否指定了一个可能的项目 (它小于解决方案) 中的项目数。
private bool IsValidDynamicItem(int commandId)
{
// The match is valid if the command ID is >= the id of our root dynamic start item
// and the command ID minus the ID of our root dynamic start item
// is less than or equal to the number of projects in the solution.
return (commandId >= (int)DynamicMenuPackageGuids.cmdidMyCommand) && ((commandId - (int)DynamicMenuPackageGuids.cmdidMyCommand) < dte2.Solution.Projects.Count);
}
将 VSPackage 设置为仅在解决方案具有多个项目时加载
由于 “设置启动项目” 命令没有意义,除非活动解决方案具有多个项目,因此可以仅在这种情况下将 VSPackage 设置为自动加载。 ProvideAutoLoadAttribute与 UI 上下文 SolutionHasMultipleProjects一起使用。 在 DynamicMenuPackage.cs 文件中,将以下属性添加到 DynamicMenuPackage 类:
[PackageRegistration(UseManagedResourcesOnly = true)]
[InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
[ProvideMenuResource("Menus.ctmenu", 1)]
[ProvideAutoLoad(UIContextGuids.SolutionHasMultipleProjects)]
[Guid(DynamicMenuPackage.PackageGuidString)]
public sealed class DynamicMenuItemsPackage : Package
{}
测试设置启动项目命令
现在可以测试代码。
生成项目并启动调试。 应显示实验实例。
在实验实例中,打开具有多个项目的解决方案。
你应该会在解决方案资源管理器工具栏上看到箭头图标。 展开它时,应显示表示解决方案中不同项目的菜单项。
检查其中一个项目时,它将成为启动项目。
关闭解决方案或打开只有一个项目的解决方案时,工具栏图标应会消失。