(翻譯)LearnVSXNow!-#6 創建我們第一個工具集-序幕
- 2019 年 10 月 5 日
- 筆記
在前面的文章中,我們在嚮導的幫助下創建了一些小的VSPackages。在第五講中我們整理了VSX的一些思路和概念,深入了解了packages是如何工作的以及服務的機制。在這篇文章中我們繼續前進。
為了創建創建「容易編寫和理解」的程式碼,從本文開始,我們開始創建一個工具集示例Package。我計劃用至少如下三個主題來討論:
- 序幕:創建示例package的第一部分,它是這個工具集的基礎。在這篇中我們將手動添加菜單命令來探討一下command table configuration文件。
- 完成示例:在這篇文章里,我們創建示例package的第二部分。手動添加一個自定義Tool Window,並且探索一下output window。
- 重構:我們修改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實驗室環境下被正確的註冊了。(注意:為了減少程式碼量和提高可讀性,這個時候我刪除了嚮導生成的注釋,你當然也可以這麼做,但這些注釋有利於理解程式碼的含義,很值得一讀)
在前面的文章中我們通過嚮導添加了菜單命令和工具窗口。在這個例子中我們將手動添加。
手動添加新的菜單項
為了顯示一個菜單項,我們要這樣做:
- 為命令創建一個ID、名字和顯示的文本,該命令用於顯示tool window
- 創建.vsct文件來設置所謂的command table configuration
- 為package類添加ProvideMenuAttribute
- 設置.vsct文件的Build Action
- 創建菜單項的事件處理函數
- 建立命令和該事件處理函數的關聯
什麼是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: }
第六步:把事件處理方法和命令關聯起來
在這裡,我們採用和前面幾篇文章中(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。
在下一篇里,我們將手動創建一個工具窗,並添加簡單的功能。