在前面的文章中,我们在向导的帮助下创建了一些小的VSPackages。在第五讲中我们整理了VSX的一些思路和概念,深入了解了packages是如何工作的以及服务的机制。在这篇文章中我们继续前进。
为了创建创建“容易编写和理解”的代码,从本文开始,我们开始创建一个工具集示例Package。我计划用至少如下三个主题来讨论:
在这个系列中,我们会创建一个工具窗,它可以对两个整数进行算术运算。
写这个系列的目的,并不是为了实现这个工具集的功能,而是为了熟悉创建类似应用的步骤。通过创建这个简单的工具集,可以使我们更熟悉package的开发,这要比直接讲解VS SDK中的interop程序集和MPF类更容易理解。
我们先创建一个空的VSPackage。因为在前面的文章中我说明了创建空package的步骤,所以在这里就省略掉截图了。选择Visual Studio Integration Package类型的项目,该项目模板会弹出我们的朋友—VSPackage向导。命名工程为StartupToolset。选择C#语言,根据下面的图片填写基本的信息:
在下一个向导页面不要勾选Menu command, Tool window 和 Command editor中的任何一个(因为我们要手动添加它们);再下一步也不要勾选任何测试项目,最后点击完成。向导生成了一个空package的项目。运行后检查Help|About对话框,以确认StartupToolset包是否在VS实验室环境下被正确的注册了。(注意:为了减少代码量和提高可读性,这个时候我删除了向导生成的注释,你当然也可以这么做,但这些注释有利于理解代码的含义,很值得一读)
在前面的文章中我们通过向导添加了菜单命令和工具窗口。在这个例子中我们将手动添加。
为了显示一个菜单项,我们要这样做:
在之前的文章中,我提到过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文件一样,很容易编辑,并且沿袭了XML所有的好的特性,比如自动生成结束标签和基于vsct XML 架构的智能感知。尽管仍然可以使用ctc文件,但微软推荐使用vsct文件。
为Command指定ID的目的,是为了将这个package里的命令项和Visual Studio中的命令项或其他package中的加以区分。Command是以ID作为标识的UI相关的对象,就像菜单项或者bitmaps那样。UI相关对象的ID是分层次的,由一个GUID和32位无符号整数组成。GUID表示逻辑上拥有这些UI对象的容器,而32位无符号数则用来在容器内部区分不同的对象。
向导生成的Guids.cs文件包含了一个用于标识package的GUID和一个用于标识命令集(command set)的GUID:
1: using System; 2: namespace MyCompany.StartupToolset 3: { 4: static class GuidList 5: { 6: public const string guidStartupToolsetPkgString = "1376bfe2-5278-493d-867e-2b5ba828368d"; 7: public const string guidStartupToolsetCmdSetString = "ec3d3ea6-2261-4a18-a458-78591688e06d"; 8: public static readonly Guid guidStartupToolsetCmdSet = new Guid(guidStartupToolsetCmdSetString); 9: } 10: }
我们要显示的菜单项是从属于command set容器中的一个对象,所以我们还需要在command set容器内部,用一个32位无符号数来标识我们的菜单项。我们把这个ID作为一个常量放在一个新的文件PkgCmdID.cs中(这个文件名的命名是根据惯例来命名的,如果在向导中勾选了Menu Command的话,向导也会生成这么一个文件)
新建一个PkgCmdID.cs并写入如下代码:
1: using System; 2: using System.Collections.Generic; 3: using System.Linq; 4: using System.Text; 5: namespace MyCompany.StartupToolset 6: { 7: static class PkgCmdIDList 8: { 9: public const uint cmdidCalculateTool = 0x101; 10: } 11: }
vsct文件用来定义command table configuration,它是XML格式的。为了显示一个菜单项,我们必须创建一个vsct文件,定义用户对象和所需的资源,并且与代码绑定以实现相关的行为。在以后的文章中,我会非常详细地解释vsct文件的格式和用法,但这一次我们只是简单的看一下它。
因为我们创建的是一个空的package,所以向导没有创建任何command table文件,我们需要手动添加一个StartupToolset.vsct文件。在“添加新项”对话框中选择XML文件,并命名为StartupToolset.vsct,写入如下代码:
1: <?xmlversion="1.0" encoding="utf-8"?> 2: <CommandTable xmlns= 3: "http://schemas.microsoft.com/VisualStudio/2005-10-18/CommandTable" 4: xmlns:xs="http://www.w3.org/2001/XMLSchema"> 5: <Extern href="stdidcmd.h"/> 6: <Extern href="vsshlids.h"/> 7: <Extern href="msobtnid.h"/> 8: <Commands package="guidStartupToolsetPkg"> 9: <Buttons> 10: <Button guid="guidStartupToolsetCmdSet" id="cmdidCalculateTool" 11: priority="0x0100" type="Button"> 12: <Parent guid="guidSHLMainMenu" id="IDG_VS_WNDO_OTRWNDWS1"/> 13: <Icon guid="guidImage" id="bmpPic1"/> 14: <Strings> 15: <CommandName>cmdidCalculateTool</CommandName> 16: <ButtonText>Calculate Tool Window</ButtonText> 17: </Strings> 18: </Button> 19: </Buttons> 20: <Bitmaps> 21: <Bitmap guid="guidImage" href="Resources\Clock.bmp" usedList="bmpPic1"/> 22: </Bitmaps> 23: </Commands> 24: <Symbols> 25: <GuidSymbol name="guidStartupToolsetPkg" 26: value="{1376bfe2-5278-493d-867e-2b5ba828368d}"/> 27: <GuidSymbol name="guidStartupToolsetCmdSet" 28: value="{ec3d3ea6-2261-4a18-a458-78591688e06d}"> 29: <IDSymbol name="cmdidCalculateTool" value="0x0101"/> 30: </GuidSymbol> 31: <GuidSymbol name="guidImage" value="{91CB158E-29BC-4818-8C1F-967AF94D96B1}"> 32: <IDSymbol name="bmpPic1" value="1"/> 33: </GuidSymbol> 34: </Symbols> 35: </CommandTable>
(译者注:作者并没有说明图片资源“Clock.bmp”是怎样做出来的,读者可以从之前的示例项目(例如SimpleToolWindow项目)中复制一个图片(如Images_32bit.bmp)过来)
.vsct文件的根元素是CommandTable,指定了名字空间和XML架构。
我之前提到过,对象的标识是由GUID和<GUID,数字>对来定义的。在CommandTable中我们必须涉及到在Visual Studio中使用的对象标识,Extern元素允许从外部文件(头文件)加载这些ID。在这个CommandTable中我们使用了如下头文件:
文件 | 内容 |
---|---|
stdidcmd.h | 这个文件包含了Visual Studio公开的所有命令的ID。可见的(和不可见的)菜单项的ID以cmdid 开头,标准编辑器命令以ECMD_ 开头等。 |
vsshlids.h | 这个文件包括了Visual Studio外壳提供的菜单命令的ID。由于命令的标识包含GUID,所以在这个文件中能找到一些guid 开头的"宏",Command标识中的无符号整数部分,则以IDM_VS、IDG_VS或一些其他的前缀开头。 |
msobtnid.h | 这个文件表示在Microsoft Office 中用到的命令的ID。 |
这些头文件可以在VS 2008 SDK安装目录的VisualStudioIntegration\Common\Inc子目录中找到。
我们的package定义了自己的GUID和命令的ID,并且可能在.vsct 文件中多次使用到这些值。为了使vsct文件定义更简单并减少打字错误,我们可以在command table中增加Symbols节点,为这些GUID和命令ID设定标识符:
1: <Symbols> 2: <GuidSymbol name="guidStartupToolsetPkg" 3: value="{1376bfe2-5278-493d-867e-2b5ba828368d}"/> 4: <GuidSymbol name="guidStartupToolsetCmdSet" 5: value="{ec3d3ea6-2261-4a18-a458-78591688e06d}"> 6: <IDSymbol name="cmdidCalculateTool" value="0x0101"/> 7: </GuidSymbol> 8: <GuidSymbol name="guidImage" value="{91CB158E-29BC-4818-8C1F-967AF94D96B1}"> 9: <IDSymbol name="bmpPic1" value="1"/> 10: </GuidSymbol> 11: </Symbols>
这样我们就可以用这些符号名而不是直接使用ID的值了,例如:
1: <Bitmap guid="guidImage" href="Resources\Clock.bmp" usedList="bmpPic1"/>
如你所见,GuidSymbol元素(用于定义逻辑容器的ID)可以包含IDSymbol元素(用于定义在容器内部的元素的ID)。
现在,我们可以利用这些ID来定义界面的相关对象了。
vsct文件用于定义命令,这些命令全部定义在Commands节点内。通过前面的文章我们可以知道,这些命令属于同一个package。Commands节点的package属性指定了这个package的ID:
1: <Commands package="guidStartupToolsetPkg"> 2: ... 3: </Commands>
为了定义一个命令,Commands节点下可以包含子节点,比如Groups、Buttons、Bitmaps等等。例如,如果我们要定义一个和命令相关的菜单项,我们可以把该菜单组定义在Groups下面的Group节点上,把菜单项定义在Buttons下面的Button节点上,把和该菜单相关的图片定义在Bitmaps节点内。
在我们的vsct文件内,我们通过下面的代码段来定义我们的菜单项:
1: <Buttons> 2: <Button guid="guidStartupToolsetCmdSet" id="cmdidCalculateTool" 3: priority="0x0100" type="Button"> 4: <Parent guid="guidSHLMainMenu" id="IDG_VS_WNDO_OTRWNDWS1"/> 5: <Icon guid="guidImage" id="bmpPic1" /> 6: <Strings> 7: <CommandName>cmdidCalculateTool</CommandName> 8: <ButtonText>Calculate Tool Window</ButtonText> 9: </Strings> 10: </Button> 11: </Buttons>
在上面的代码段中,我们定义了一个菜单项,它的type属性是Button,并且用了在Symbol节点下定义的guid-id对作为标识。Button节点有一些子节点,这些子节点定义了该菜单项的一些属性:
节点 | 描述 |
---|---|
Parent | 该节点表示按钮的父亲。一个按钮可以有一个或多个父亲,在界面上看,该按钮代表的命令可以放在多个地方。例如,可以同时把它放在主菜单、工具栏或右键菜单里。 在这个例子中,guidSHLMainMenu是Visual Studio主菜单的逻辑容器的标识,IDG_VS_WNDO_OTRWNDWS1是菜单项“视图|其他窗口”的ID。 |
Icon | 定义与命令相关的图标。 |
Strings\CommandName | 定义命令的名字,可以通过命令的名字来查找命令。 |
Strings\ButtonText | 定义该命令的显示文本。 |
让我们看一下这个命令的图标是怎样定义的:
1: <Bitmaps> 2: <Bitmap guid="guidImage" href="Resources\Clock.bmp" mce_href="Resources\Clock.bmp" 3: usedList="bmpPic1"/> 4: </Bitmaps>
图标、图片或bitmap定义在Bitmaps节点下。一个Bitmap节点定义一个bitmap strip。属性guid代表这个bitmap strip的ID,href属性表示该图片相对于项目所在目录的相对路径。usedList属性的值代表了bitmap strip中的bitmap ID,以逗号隔开。这些bitmap strip中的bitmap ID定义在GuidSymbol节点中代表bitmap的IDSymbol子节点中。Bitmap strip ID是从1开始的(1,2,3…),如果我们想用bitmap strip中的一个bitmap,我们可以把usedList属性的值设置为相应的ID值。(译者注:有关bitmap strip的概念,可以Google一下或参考这篇文章:http://www.axialis.com/tutorials/image-strip.html)
为了保证regpkg.exe能注册我们的菜单,我们必须为package类添加ProvideMenuResourceAttribute。这个attribute指定了保存有这个菜单和命令信息的资源ID,并且可以设置这个菜单的版本号,代码如下:
1: ... 2: [ProvideMenuResource(1000, 1)] 3: public sealed class StartupToolsetPackage: Package { ... } 4: ...
为什么我们要把资源ID指定为1000?你很快就会知道答案…
在这篇文章的开头,我讲了一下Command Table Configuration文件的职责,并且提到了vsct文件被编译到二进制资源中。当我们为项目添加StartupToolset.vsct文件后,该文件的生成动作(Build Action)默认是None。
为了能把vsct文件编译到资源中,应该设置Build Action为VSCTCompile(如果我们用VSPackage向导并选择Menu Command的话,这个文件会自动设置成VSCTCompile)。
在这里会遇到Visual Studio的一个问题(更确切的说是Visual Studio 2008 SDK第一版的问题)。当我们试图把Build Action改成VSCTCompile时,我们会发现在Build Action的下拉列表里根本没这个选项!如果我们手动敲入这个值时,会得到一个Invalid Property的错误。这其实是一个bug。
对于这个bug,我找到了一些解决办法。最稳妥的(也是最直接的)办法是手动修改.csproj文件。
用文本编辑器(例如记事本)打开这个项目文件,然后找到有关StartupToolset.vsct文件的节点。如下:
1: <ItemGroup> 2: <None Include="StartupToolset.vsct" /> 3: </ItemGroup>
修改代码为:
1: <ItemGroup> 2: <VSCTCompile Include="StartupToolset.vsct"> 3: <ResourceName>1000</ResourceName> 4: </VSCTCompile> 5: </ItemGroup>
使用VSCTCompile节点会正确的设置build action。ResourceName子节点使得编译器在编译过程中,用1000作为资源ID,把cto文件编译到VSPackage中。这样就会确保regpkg.exe能够利用ProvideMenuResource来正确的注册package中的菜单:(译者注:从这里我们就知道ProvideMenuAttribute的第一个参数为什么是1000了)
1: ... 2: [ProvideMenuResource(1000, 1)] 3: public sealed class StartupToolsetPackage: Package { ... } 4: ...
到目前为止,我们还没有创建工具窗来测试新创建的菜单,在这里可以简单的显示一个消息来代替工具窗。在StartupToolsetPackage类里,我们添加一个私有的事件处理方法:
1: ... 2: public sealed class StartupToolsetPackage : Package 3: { 4: ... 5: private void ShowCalculateToolCallback(object sender, EventArgs e) 6: { 7: MessageBox.Show("Calculate Tool Window is about to be displayed...", 8: "Tool Window"); 9: } 10: ... 11: }
在这里,我们采用和前面几篇文章中(SimpleCommand和SimpleToolWindow)差不多的代码。把事件处理方法和命令关联起来的代码写在package类的Initialize方法中,并且用到的<GUID,ID>对要和vsct文件中用于定义菜单项的一样。
1: protected override void Initialize() 2: { 3: Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, 4: "Entering Initialize() of: {0}", this.ToString())); 5: base.Initialize(); 6: 7: OleMenuCommandService mcs = 8: GetService(typeof(IMenuCommandService)) as OleMenuCommandService; 9: if (null != mcs) 10: { 11: CommandID menuCommandID = new CommandID(GuidList.guidStartupToolsetCmdSet, 12: (int)PkgCmdIDList.cmdidCalculateTool); 13: MenuCommand menuItem = new MenuCommand(ShowCalculateToolCallback, 14: menuCommandID); 15: mcs.AddCommand(menuItem); 16: } 17: }
完成上面这一步后,我们就创建好了一个package,它包含一个手动创建的菜单,点击这个菜单会弹出一个消息框。编译并且运行这个项目,当vs 2008 Experimental Hive启动后,你可以在菜单“视图|其他窗口”里看到我们的菜单项:
点击“Calculate Tool Window”菜单项,会弹出一个消息框:
这这一篇中,我们开始创建一个工具集来熟悉VSPackage的开发。作为这个系列的第一部分,我们创建了一个空的package,并手动添加了一个菜单命令。在这个过程中,我们探讨了Visual Studio Command Table文件在描述UI资源时的作用。
在设置vsct文件的build action时,我们发现了关于Build Action属性的一个bug。通过手动的修改.csproj文件,可以绕开这个bug。
在下一篇里,我们将手动创建一个工具窗,并添加简单的功能。