(翻譯)LearnVSXNow!-#6 創建我們第一個工具集-序幕

  • 2019 年 10 月 5 日
  • 筆記

在前面的文章中,我們在嚮導的幫助下創建了一些小的VSPackages。在第五講中我們整理了VSX的一些思路和概念,深入了解了packages是如何工作的以及服務的機制。在這篇文章中我們繼續前進。

為了創建創建「容易編寫和理解」的程式碼,從本文開始,我們開始創建一個工具集示例Package。我計劃用至少如下三個主題來討論:

  1. 序幕:創建示例package的第一部分,它是這個工具集的基礎。在這篇中我們將手動添加菜單命令來探討一下command table configuration文件。
  2. 完成示例:在這篇文章里,我們創建示例package的第二部分。手動添加一個自定義Tool Window,並且探索一下output window。
  3. 重構:我們修改package,提取一些在package開發中公共的可復用的類型。

在這個系列中,我們會創建一個工具窗,它可以對兩個整數進行算術運算。

寫這個系列的目的,並不是為了實現這個工具集的功能,而是為了熟悉創建類似應用的步驟。通過創建這個簡單的工具集,可以使我們更熟悉package的開發,這要比直接講解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類添加ProvideMenuAttribute
  4. 設置.vsct文件的Build Action
  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文件一樣,很容易編輯,並且沿襲了XML所有的好的特性,比如自動生成結束標籤和基於vsct XML 架構的智慧感知。儘管仍然可以使用ctc文件,但微軟推薦使用vsct文件。

第一步:增加一個command ID

為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文件

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="ResourcesClock.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安裝目錄的VisualStudioIntegrationCommonInc子目錄中找到。

我們的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="ResourcesClock.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

定義與命令相關的圖標。

StringsCommandName

定義命令的名字,可以通過命令的名字來查找命令。

StringsButtonText

定義該命令的顯示文本。

讓我們看一下這個命令的圖標是怎樣定義的:

1: <Bitmaps>   2:   <Bitmap guid="guidImage" href="ResourcesClock.bmp" mce_href="ResourcesClock.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)

第三步:為package類添加ProvideMenuResourceAttribute

為了保證regpkg.exe能註冊我們的菜單,我們必須為package類添加ProvideMenuResourceAttribute。這個attribute指定了保存有這個菜單和命令資訊的資源ID,並且可以設置這個菜單的版本號,程式碼如下:

1: ...   2: [ProvideMenuResource(1000, 1)]    3: public sealed class StartupToolsetPackage: Package { ... }   4: ...

為什麼我們要把資源ID指定為1000?你很快就會知道答案…

第四步:設置vsct文件的編譯選項

在這篇文章的開頭,我講了一下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: }

第六步:把事件處理方法和命令關聯起來

在這裡,我們採用和前面幾篇文章中(SimpleCommandSimpleToolWindow)差不多的程式碼。把事件處理方法和命令關聯起來的程式碼寫在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。

在下一篇里,我們將手動創建一個工具窗,並添加簡單的功能。