在前面的文章中,我们在向导的帮助下创建了一些小的VSPackages。在第五讲中我们整理了VSX的一些思路和概念,深入VSPackages 了解了packages如何工作以及服务的机制。在这篇文章中我们继续向前。
本文我们开始创建一个工具集来帮助我们创建容易编写和理解的代码。我计划用如下三个主题来讨论:
序幕:我们创建示例包的第一部分,这将是toolset的基础。在这篇中我们将手动添加菜单命令探索一下command table configuration文件。
完成示例:我们在第一部分的基础上创建第二部分。在这篇中手动添加一个自定义的tool window,探索一下output window。
重构:我们修改package,提取一些在package开发中公共的可复用的类型。
在这个系列中我们将要创建一个tool window允许我们对两个整数进行算术运算。
(译者注:原图失效,界面是自己设计的,图是自己截的)
写这个系列的目的,并不是为了实现这个工具集的功能,而是为了熟悉创建类似应用的步骤。通过创建这个简单的工具集,要比直接讲解VS SDK中的interop程序集和MPF类更容易理解。我会在这个系列的重构篇中来告诉你这些。
创建一个空的VSPackage
我们先创建一个空的VSPackage,在前面的文章中我说明了创建空package的步骤,这里就省略掉截图了。创建一个熟悉的Visual Studio Integration Package 工程,并使用我们的朋友—VSPackage向导,命名工程为StartupToolset。选择C#语言,根据下面的图片填写基本的信息:
(译者注:原图失效,图是自己截的)
在下一个向导页面不要勾选Menu command, Tool window 和 Command editor中的任何一个;再下一步也不要勾选任何测试项目,最后点击完成。向导生成了一个空package的源代码。运行后检查Help|About对话框,以确认StartupToolset包是否在VS实验室环境下被正确的注册了。(注意:为了减少代码量,我把向导生成的注释删掉了,但这些注释有利于理解代码的含义,很值得一读)
在前面的文章中我们通过向导添加了菜单命令和工具窗口。在这个例子中我们将手动做这些。
手动添加新的菜单项
为了显示一个菜单项,我们要这样做:
1.为命令创建一个ID、名字和显示的文本,该命令用于显示tool window
2.创建.vsct文件设置所谓的command table configuration
3.为package类添加ProvideMenu属性
4.设置.vsct文件的编译选项
5.创建菜单项的事件处理函数
6.建立命令和该事件处理函数的关联
什么是command table configuration文件?
在之前的文章中我提到过VSPackages是"按需加载(on-demand loaded)"的,当packages中的对象将要生成或者其中的服务被使用的时候IDE才将他们装载进内存。听起来不错,不过有个问题:如果对象表示了菜单或者工具栏对象,并且和package的源代码编译在一起,那么IDE不得不仅仅为了展示这些UI而加载这个package,哪怕这个package并没有被使用。为了显示这些跟package相关的菜单和工具栏(而避免上述情况的发生),这些被设计成package的二进制资源。当package被注册(通过regpkg.exe)时,这些资源被提取并分开存放,这样Visual Studio就可以在不加载package的情况下显示这些资源。
要实现这个,command table configuration文件是关键。这个文件的职责是定义与命令相关的UI元素。当我们编译一个package时,command table configuration文件转换成一个cto文件(command table output file),并作为一个资源,编译到package中。
在vs2005版本的VS SDK中,使用一种文本形式的command table configuration文件(.ctc后缀)。理解和编辑.ctc文件不是件容易的事。随着Visual Studio 2008 SDK的发布,微软创建了一种基于XML的文件格式(.vsct: Visual Studio Command Table),并且配以一种新的编译器(VSCTCompile)来将.vsct文件编译成.cto文件。
.vsct文件主要的优点是沿袭了XML容易编辑的优点,比如自动生成结束标签和基于.vsct XML 架构的智能感知。尽管仍然可以编辑.ctc文件,但微软推荐使用.vsct文件。
第一步:增加一个command ID
这样做是为了将这里的命令项和Visual Studio中的命令项或其他VSPackages中的加以区分。Commands是一组唯一的相关的对象,比如菜单项或者bitmaps。UI相关对象的IDs是分层次的,由一个GUID和32位无符号整数标识。GUID表示逻辑上拥有这些UI对象的容器,而32位无符号数则用来在容器内部区分不同的对象。
向导生成的Guids.cs文件包含了一个代表package的GUID和一个代表被称为命令集(command set)的GUID,这个命令集是从属于package的。
using System;
namespace MyCompany.StartupToolset
{
static class GuidList
{
public const string guidStartupToolsetPkgString = "1376bfe2-5278-493d-867e-2b5ba828368d";
public const string guidStartupToolsetCmdSetString = "ec3d3ea6-2261-4a18-a458-78591688e06d";
public static readonly Guid guidStartupToolsetCmdSet = new Guid(guidStartupToolsetCmdSetString);
}
}
我们要显示的菜单项是从属于command set容器中的一个对象,所以我们还需要一个32位无符号数标识在command set容器内部标识我们的菜单项。我们把这个ID作为一个常量放在一个新的文件PkgCmdID.cs中,我们根据习惯来命名这个文件,如果在向导中勾选了创建Menu Command的话,向导也会生成这么一个文件。
新建一个PkgCmdID.cs并写入如下代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyCompany.StartupToolset
{
static class PkgCmdIDList
{
public const uint cmdidCalculateTool = 0x101;
}
}
第二步:建立.vsct文件
.vsct文件就是command table configuration文件,它是XML格式的。为了显示一个菜单项,我们必须创建一个.vsct文件,定义用户对象和所需的资源,并且与代码绑定以实现相关的行为。在以后的文章中,我会非常详细地解释vsct文件的格式和用法,但这一次我们只是简单的看一下它。
因为我们创建的是一个空的package,向导没有创建任何command table文件,我们需要手动添加一个StartupToolset.vsct文件。在Add New Item中选择XML模板,并命名为StartupToolset.vsct,写入如下代码:
<?xmlversion="1.0" encoding="utf-8"?>
<CommandTable xmlns=
"http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<Extern href="stdidcmd.h"/>
<Extern href="vsshlids.h"/>
<Extern href="msobtnid.h"/>
<Commands package="guidStartupToolsetPkg">
<Buttons>
<Button guid="guidStartupToolsetCmdSet" id="cmdidCalculateTool"
priority="0x0100" type="Button">
<Parent guid="guidSHLMainMenu" id="IDG_VS_WNDO_OTRWNDWS1"/>
<Icon guid="guidImage" id="bmpPic1"/>
<Strings>
<CommandName>cmdidCalculateTool</CommandName>
<ButtonText>Calculate Tool Window</ButtonText>
</Strings>
</Button>
</Buttons>
<Bitmaps>
<Bitmap guid="guidImage" href="Resources\Clock.bmp" usedList="bmpPic1"/>
</Bitmaps>
</Commands>
<Symbols>
<GuidSymbol name="guidStartupToolsetPkg"
value="{1376bfe2-5278-493d-867e-2b5ba828368d}"/>
<GuidSymbol name="guidStartupToolsetCmdSet"
value="{ec3d3ea6-2261-4a18-a458-78591688e06d}">
<IDSymbol name="cmdidCalculateTool" value="0x0101"/>
</GuidSymbol>
<GuidSymbol name="guidImage" value="{91CB158E-29BC-4818-8C1F-967AF94D96B1}">
<IDSymbol name="bmpPic1" value="1"/>
</GuidSymbol>
</Symbols>
</CommandTable>
.vsct文件的根元素是CommandTable,指定了名字空间和XML架构。
我之前提到过,对象用GUIDs或者GUID,unsigned int对定义的。在CommandTable中我们必须知道在Visual Studio中使用的IDs。Extern元素允许从外部文件(头文件)加载一些IDs,在这个CommandTable中我们使用了如下头文件:
文件 |
内容 |
stdidcmd.h |
这个文件表示所有Visual Studio可用的命令ID。包括可见的和不可见的菜单项ID,这些ID都都以cmdid 开头,标准编辑器命令以ECMD_ 开头等。 |
vsshlids.h |
这个文件包括了Visual Studio shell 提供的菜单的命令IDs。在文件中能找到一些guid 开头的"宏定义"表示GUID,一些像IDM_VS, IDG_VS 开头的宏则定义了command IDs的无符号整数部分。 |
msobtnid.h |
这个文件表示在Microsoft Office 中用到的命令IDs。 |
这些头文件可以在VS 2008 SDK目录下找到VisualStudioIntegration\Common\Inc
我们的package自定义了GUIDs和command IDs。我们可能在.vsct 文件中多次使用在些值,为了使用方便,我们在command table中定义Symbols元素,用来定义这些GUIDs和command IDs。
<Symbols>
<GuidSymbol name="guidStartupToolsetPkg"
value="{1376bfe2-5278-493d-867e-2b5ba828368d}"/>
<GuidSymbol name="guidStartupToolsetCmdSet"
value="{ec3d3ea6-2261-4a18-a458-78591688e06d}">
<IDSymbol name="cmdidCalculateTool" value="0x0101"/>
</GuidSymbol>
<GuidSymbol name="guidImage" value="{91CB158E-29BC-4818-8C1F-967AF94D96B1}">
<IDSymbol name="bmpPic1" value="1"/>
</GuidSymbol>
</Symbols>
这样我们就可以用这些符号名而不是直接使用ID的值了,像下面的例子:
<Bitmap guid="guidImage" href="Resources\Clock.bmp" usedList="bmpPic1"/>
如你所见,GuidSymbol元素(逻辑容器)可以包含IDSymbol元素(在容器内部使用的IDs)。
现在,我们准备好了足够的IDs的集合,是时候看一下对象定义了。
Commands元素涵盖所有的定义,我们知道所有commands都从属于VSPackage,package属性定义了这种关系:
<Commands package="guidStartupToolsetPkg">
…
</Commands>
Commands元素包括的子元素有Groups, Buttons, Bitmaps等。我们可以在Groups中定义Group元素用以放置一组相关的命令项,Buttons中定义的Button元素就是菜单项,同时可以与Bitmaps区域中的Bitmap关联。
在我们的.vsct文件中,我们像下面这样定义菜单项:
<Buttons>
<Button guid="guidStartupToolsetCmdSet" id="cmdidCalculateTool"
priority="0x0100" type="Button">
<Parent guid="guidSHLMainMenu" id="IDG_VS_WNDO_OTRWNDWS1"/>
<Icon guid="guidImage" id="bmpPic1"/>
<Strings>
<CommandName>cmdidCalculateTool</CommandName>
<ButtonText>Calculate Tool Window</ButtonText>
</Strings>
</Button>
</Buttons>
我们定义的菜单项是一个Button类型的,用一组相关的guid 和id标识,guid 和id相应的值在Symbol区域中定义过。Button有一些子节点表示菜单项的属性:
元素 |
描述 |
Parent |
这个元素表示Button的父元素。一个Button可以有一个或多个父元素,从效果上看,Button可以放到一个或多个位置上。例如,可以同时放在菜单、工具栏、以及右键菜单。在这个例子中guidSHLMainMenu 表示以Visual Studio的主菜单作为逻辑上的容器。IDG_VS_WNDO_OTRWNDWS1 是 View|Other Windows菜单项的ID |
Icon |
定义了命令将要显示的图标 |
Strings\CommandName |
定义了命令的名称,可以通过名称来定位命令 |
Strings\ButtonText |
命令显示的文本 |
我们再看一下命令的icon(或者bitmap)的定义:
<Bitmaps>
<Bitmap guid="guidImage" href="Resources\Clock.bmp" usedList="bmpPic1"/>
</Bitmaps>
Icons,images和其他的bitmap定义在Bitmaps元素中,其中的每个Bitmap节点声明了一个bitmap strip。Guid属性定义了bitmap strip的ID,href指定了资源所在的工程的相对路径,usedList属性用头号区分了在strip中的IDs。IDs在Symbols中定义了,这个IDs是以1开始的。我们使用的时候直接引用了ID。
第三步:为package类增加ProvideMenu属性
为了保证regpkg.exe注册我们的菜单资源,必须用ProvideMenu属性装饰一下package类。这个属性命名了拥有菜单和命令信息的资源并且提供了设置菜单版本的属性。为了实现我们的需要,我们添加如下属性:
[ProvideMenuResource(1000,1)]
public sealed class StartupToolsetPackage : Package{…}
第四步:为.vsct文件设置编译选项
在文章的开头我描述Command Table Configuration地位的地方,我提到.vsct文件被编译成二进制资源。当我们向工程中添加StartupToolset.vsct时,该文件的编译行为是默认设置为None的。
为了把command table编译成二进制资源,编译行为需要被设置为VSCTCompile(如果通过向导生成的话,向导会自动这样设置)。
这里出现了个关于Visual Studio的问题(Visual Studio 2008 SDK第一版的问题)。当我们试图设置编译行为为VSCTCompile时,在列表中没有这个选项!!如果试图手动修改这个值时,将出现一个Invalid Property错误!事实上你看到的是一个BUG!
为了解决这个问题我找了一些方法。最稳妥的最直接的方法是手动修改.csproj工程文件。
在文本编辑器中打开.csproj文件查找StartupToolset.vsct。你会发现如下代码:
<ItemGroup>
<None Include="StartupToolset.vsct" />
</ItemGroup>
修改代码为:
<ItemGroup>
<VSCTCompile Include="StartupToolset.vsct">
<ResourceName>1000</ResourceName>
</VSCTCompile>
</ItemGroup>
VSCTCompile元素会设置正确的编译行为。子节点ResourceName会使得.cto文件(编译生成的结果)以资源号为1000的资源形式嵌入你的VSPackage中。当使用这样的ProvideMenuResource属性时候就保证了regpkg.exe会正确注册我们的package中的菜单。
[ProvideMenuResource(1000,1)]
public sealed class StartupToolsetPackage : Package{…}
第五步:创建命令处理函数
目前我们还没有一个可显示的tool window来测试我们新创建的菜单项,所以我简单的显示一个消息框代替tool window。我们在StartupToolsetPackage类中增加一个私有的事件处理函数:
public sealed class StartupToolsetPackage : Package
{
…
private void ShowCalculateToolCallback(object sender, EventArgs e)
{
MessageBox.Show("Calculate Tool Window is about to be displayed...", "Tool Window");
}
}
第六步:关联命令和事件处理函数
我们用之前例子(SimpleCommand and SimpleToolWindow)中相同的模板。初始化代码写在Initialize方法中,这个方法是基类的重载。我们使用了在.vsct文件中定义好的GUID 和 ID对:
protected override void Initialize()
{
Trace.WriteLine (string.Format(CultureInfo.CurrentCulture, "Entering Initialize() of: {0}", this.ToString()));
base.Initialize();
OleMenuCommandService mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
if (null != mcs)
{
CommandID menuCommandID = new CommandID(GuidList.guidStartupToolsetCmdSet,(int)PkgCmdIDList.cmdidCalculateTool);
MenuCommand menuItem = new MenuCommand(ShowCalculateToolCallback,menuCommandID);
mcs.AddCommand(menuItem);
}
}
尝一尝布丁吧!
完成以上步骤后我们拥有了一个手动建立菜单栏的package,会弹出一个消息框。编译运行package!当Visual Studio 2008 Experimental Hive启动时你会发现我们的菜单栏在View|Other Windows中:
(译者注:原图失效,图是自己截的)
总结
我开始编写代码创建一个toolset。我们创建了一个空的package,手动添加了一个菜单命令。我们发现Visual Studio Command Table 文件在UI资源表现中的扮演的角色。
当设置.vsct文件的编译行为时,我们发现了一个BUG。通过手动修改.csproj文件解决了这个问题。
在接下来的文章中我们将要手动创建tool window,添加简单的功能。