(翻譯)LearnVSXNow!-#7 創建我們第一個工具集-完成這個示例

  • 2019 年 10 月 5 日
  • 筆記

在上一篇文章中,我們創建了一個例子:我們為一個空的package添加了一個菜單命令,並且在這個過程中了解了Visual Studio Command Table文件的作用和用法。

在這篇文章中,我們繼續這個例子,手動為它添加一個工具窗。

為項目添加工具窗

我們將創建如下圖所示的工具窗:

這個工具窗的功能非常簡單:在FirstArgEditSecondArgEdit文本框里輸入數字,在OperatorCombo下拉框里選擇運算符(+、-、*或%) ,點擊Calculate按鈕後,運算結果顯示在ResultEdit文本框中。

為了在StartupToolset示例中創建我們的工具窗,我們需要做下面的工作:

  1. 設計工具窗的介面
  2. 實現工具窗的功能
  3. 設置工具窗需要的資源
  4. 創建ToolWindowPane類,以便將這個工具窗嵌入到IDE中
  5. 將工具窗和package關聯起來
  6. 編寫顯示工具窗的程式碼

我們曾在第4篇中為package添加過工具窗。正如我們在第4篇看到的那樣,為了創建一個工具窗,我們至少需要兩個類。第一個類是一個WinForm用戶控制項,它是工具窗的介面;第二個類繼承自ToolWindowPane,通過它可以把工具窗的介面嵌入到Visual Studio IDE中。

第一步:設計用戶介面

在StartupToolset項目里,添加一個名為CalculationControl的用戶控制項。把相應的控制項從Toolbox中拖到該用戶控制項上,並且按照上圖中給出的名字來命名各控制項。設置ResultEdit控制項的Anchor屬性為[Top,Left,Right];設置OperationCombo控制項的DropDownStyle屬性為DropDownList,並給它的Items屬性添加「+」, 「-」, 「*」, 「/」, 「%」五個選項。

第二步:實現工具窗的功能

實現一個工具窗的功能可以有很多種方式(設計模式)。特別是對於複雜的功能,我們可以創建一些互相協作的類來共同完成這些功能,我們也可以為VSPackage創建服務,這樣我們的package和其他的package可以共用這些服務。

但是,在這篇文章里我們採用最簡單的方式:直接在用戶控制項里添加實現功能的程式碼。

CalculationControl用戶控制項的Load事件和CalculateButton按鈕的Click事件添加事件處理方法,如下所示:

using System;using System.Windows.Forms;  namespace MyCompany.StartupToolset{  public partial class CalculationControl : UserControl  {    public CalculationControl()    {      InitializeComponent();    }      private void CalculationControl_Load(object sender, EventArgs e)    {      OperatorCombo.SelectedIndex = 0;      FirstArgEdit.Text = "0";    }      private void CalculateButton_Click(object sender, EventArgs e)    {      try      {        int firstArg = Int32.Parse(FirstArgEdit.Text);        int secondArg = Int32.Parse(SecondArgEdit.Text);        int result = 0;        switch (OperatorCombo.Text)        {          case "+":            result = firstArg + secondArg;            break;          case "-":            result = firstArg - secondArg;            break;          case "*":            result = firstArg * secondArg;            break;          case "/":            result = firstArg / secondArg;            break;          case "%":            result = firstArg % secondArg;            break;        }        ResultEdit.Text = result.ToString();      }      catch (SystemException)      {        ResultEdit.Text = "#Error";      }    }  }}

我想程式碼就不用解釋了吧。

第三步:設置資源

當我們的工具窗顯示的時候,Visual Studio IDE會在這個工具窗的窗口標籤那裡顯示一個圖片。例如,當我們的工具窗和Solution Explorer顯示在一起的時候,你可以在窗口標籤那裡看到這個圖片:

不能把圖片直接傳給工具窗,必須利用圖片資源:在初始化工具窗的時候,我們只能傳遞資源的標識。另外,由於這些資源標識是由VS IDE來處理的,所以這個圖片必須放在VSPackage.resx文件中。

為了給工具窗添加「clock」圖片,我們可以把這個圖片文件添加到VSPackage.resx文件中,並用一個數字作為該圖片資源的ID,在這裡我們用300作為這個圖片資源的ID。 (譯者註:如果不知道怎樣做bmp資源,可以從以前的示例SimpleToolWindow的Resources目錄下拷貝一個bmp文件過來)

另外,我們自己的程式碼(不是IDE)也有可能用到一些資源,這些資源最好放在Resource.resx文件中,因為Visual Studio已經自動地幫我們創建了一個Resources類了,並且以靜態屬性的方式來表示放在該文件中的資源。

Resources.resx文件中,添加如下的字元串資源,我們在後面會用到它們:

資源名

資源值

ToolWindowTitle

Calculate Tool Windows

CanNotCreateWindow

Cannot create tool window.

第四步:創建ToolWindowPane

負責工具窗介面的用戶控制項並不知道如何嵌入到VS IDE中。嵌入到IDE中的窗口對象(工具窗是其中一種)會包含很多由IDE提供的特性:例如它們可以停靠、浮動或者固定。IDE通過Windows frame和Window pane來提供這些特性。為了使我們的用戶控制項也有這些特性,它必須嵌入到一個Window pane里。

所以,把用戶控制項嵌入到IDE的關鍵,是去創建一個Window pane的類,這個類繼承自ToolWindowPane,ToolWindowPane實現了IVsWindowPane介面。IDE利用這個介面來為工具窗提供上述特性。

在StartupToolset項目里,添加一個CalculationToolWindow.cs 文件,並且把下面的程式碼複製到這個文件里:

using System;using System.Collections.Generic;using System.Linq;using System.Text;using Microsoft.VisualStudio.Shell;using System.Runtime.InteropServices;using System.Windows.Forms; namespace Company.StartupToolset{    [Guid("4B1BBBA2-9D83-45a4-8899-E7CB0296D27F")]    public class CalculationToolWindow : ToolWindowPane    {        private CalculationControl control;         public CalculationToolWindow()            : base(null)        {            Caption = Resources.ToolWindowTitle;            BitmapResourceID = 300;            BitmapIndex = 0;            control = new CalculationControl();        }         override public IWin32Window Window        {            get { return control; }        }    }}

工具窗類以COM類的形式被IDE調用,所以我們需要為它指定一個GUID。在這個類的上面添加GuidAttribute,並指定一個guid。

用戶控制項CalculationControl的實例通過私有欄位control來嵌入到tool window pane中。在這個類的構造函數里,我們創建了一個CalculationControl控制項的實例,並利用Window屬性來返回該控制項實例的Win32句柄。

現在讓我們看一下構造函數的程式碼:

public CalculationToolWindow()    : base(null){    Caption = Resources.ToolWindowTitle;    BitmapResourceID = 300;    BitmapIndex = 0;    control = new CalculationControl();}

在上面這個構造函數里,我們用資源來設置工具窗的標題和圖片。Caption是一個字元串類型的屬性,所以我們可以給它一個字元串常量。但是在這裡我用了和VSPackage嚮導一樣的方式:通過在Resources.resx文件中指定的值來給Caption賦值。

工具窗的圖片是根據BitmapResourceIDBitmapIndex這兩個屬性來決定的。第一個必須是一個整型的ID,這個ID值就是我們在VSPackage.resx文件中添加的圖片資源的ID。IDE會把這個圖片看作一個點陣圖條(bitmap strip),BitmapIndex屬性則指定了工具窗的圖片在這個點陣圖條中的索引。

這個構造函數沒有參數,但是基類里的構造函數需要一個IServiceProvider類型的參數。由於我們並不需要這個參數,所以只需要傳一個null過去就行了。當然,如果我們需要在工具窗中調用service,我們可以給它傳一個IServiceProvider的實例。

我之所以提到這個,是因為VS 2008 SDK的文檔誤導我們說:「這個參數值不能是null(在Visual Basic里是Nothing),否則這個工具窗將不能加到vs殼裡」。這是不正確的說法,你如果運行起來我們這個例子的話,你會看到我們的工具窗照樣可以加到IDE里。

第五步:讓我們的package知道這個的工具窗

我們的工具窗本身並不是一個獨立的對象,它必須和package捆綁起來:包括何時或如何顯示工具窗的邏輯,甚至可能包括一些交互邏輯和服務。

我們可以利用ProvideToolWindowAttribute來把工具窗和package關聯起來,並且把工具窗的類型(在這裡是CalculationToolWindow)作為參數傳遞給這個attribute:

...[ProvideToolWindow(typeof(CalculationToolWindow))] public sealed class StartupToolsetPackage : Package{...}...

一個工具窗不僅能被所在的VSPackage調用,也能被其他的VSPackage調用。在前面的文章中(第5篇),我提到了一個按需載入package的模型。當其他的package調用這個package的工具窗的時候,該package才會被載入(前提是這個package在這之前沒有被用到,否則早就被載入了)。這是通過和菜單命令類似的註冊機制來實現的。regpkg.exe命令根據ProvideToolWindowAttribute去註冊我們的工具窗,並且把它和對應的package關聯起來。當其他的package試圖對我們的工具窗做任何操作時,IDE就會載入我們的package(除非它已經被載入進來了)。

第六步:顯示這個工具窗

在第四篇中我們看過顯示工具窗的程式碼,在這裡我們採用類似的方式來顯示CalculationToolWindow,我們把這段程式碼放在菜單命令的事件處理方法里(這個事件處理方法我們已經在上一篇中創建了,但在當時只是用來顯示一個消息框):

...public sealed class StartupToolsetPackage : Package{  ...  private void ShowCalculateToolCallback(object sender, EventArgs e)  {    ToolWindowPane window = FindToolWindow(typeof(CalculationToolWindow), 0, true);     if ((null == window) || (null == window.Frame))    {      throw new NotSupportedException(Resources.CanNotCreateWindow);    }    IVsWindowFrame windowFrame = (IVsWindowFrame)window.Frame;    Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show());  }  ...}

提醒一下你它是如何工作的:關鍵是FindToolWindow方法,該方法負責查找ID為0的CalculationToolWindow的實例。如果沒有找到,就會創建一個新的;通過調用工具窗的Frame屬性的Show方法,就可以顯示這個工具窗。

就這樣,我們的工具窗可以通過點擊相應的菜單項來顯示出來了。

我們需要的工具

如果我問你,在你開發的時候最想要的是什麼類型的工具,我猜排在前5的一定是「日誌」。利用日誌,調試和修復程式就容易的多。所以在這篇文章剩下的部分里,我們將為這個示例添加簡單的日誌功能。

為VSPackage添加日誌

有很多方式可以為程式添加日誌,例如,我們可以把文本消息發送到控制台,或發送到Trace或Debug output、Windows事件查看器甚至Windows調試日誌。另外,Visual Studio也提供了一些其他的可選方案:

  1. Visual Studio有一個被稱為活動日誌(activity log)的的xml文件。我們可以把日誌資訊記錄在這個文件里。對於記錄重要的資訊來說,活動日誌非常重要。
  2. 另外,Visual Studio有一個輸出窗口(output window),我們也可以把資訊記錄在這裡。為了把我們的日誌資訊和其他的資訊區分開來,我們可以在output window中創建自己的pane(例如版本控制工具或其他package創建的pane)。

在這篇文章中我們會在程式碼中加入這樣的日誌功能:當點擊我們的工具窗的Calculate按鈕時,我們把參數、操作符和計算結果記錄到日誌中。

什麼是活動日誌(activity log)?

在啟動Visual Studio時,添加/log開關即可以啟動Visual Studio的活動日誌模式。在這種模式下,寫在所謂的VS活動日誌里的資訊最終被保存在一個xml文件里,我們可以查看這個xml文件的內容,以便用於測試、驗證、或解決問題。

如果在啟動Visual Studio的時候沒有加/log開關,發送到活動日誌的資訊就不會記錄在這個xml文件里。

每一次你通過/log開關啟動VS,上一次記錄的ActivityLog.Xml日誌文件就會被覆蓋。這個文件位於你的用戶配置(user profile)目錄的MicrosoftVisualStudio<Hive>UserSettings子目錄中。<Hive>取決於你運行的Visual Studio的版本(例如如果是VS 2008的話,<Hive>是9.0),如果你另外加了/rootsuffix開關的話,表明是VS的Experimental hive版本。所以,如果你是用vs 2008 sdk來開發package的話,<Hive>通常是9.0Exp。還有,一定要注意你的用戶配置文件夾(user profile folder)的路徑是由很多因素決定的(例如你的登錄用戶名、配置類型、作業系統等等)。

例如,如果你的系統是Windows Vista,你的用戶名是jsmith,並且你有一個漫遊配置文件(roaming profile),你可以在這個目錄下找到活動日誌文件:C:UsersjsmithAppDataRoamingMicrosoftVisualStudio9.0ExpUserSettings

Visual Studio也會在同一個目錄下生成一個樣式表文件(ActivityLog.xsl),所以如果用IE打開活動日誌文件(ActivityLog.Xml)的話,會根據樣式表文件定義的格式來以列表的形式展現日誌。

活動日誌文件會經常被重寫,所以——根據我的經驗——你可以在開著VS的時候查看這個文件。(譯者註:本人認為關閉VS後再看這個文件內容也未嘗不可,因為在VS不關閉的情況下ActivityLog.xml無法在IE下正常顯示,只能用記事本之類的文件看。原文作者的意思應該是如果你在VS做了一個操作,可以在不關閉VS的情況下立刻用記事本之類的程式查看這個文件,以便檢查這段操作記錄下來的日誌。)當你關閉了VS之後,樣式表文件才會更新到這個目錄下。如果你在打開VS之前或開著VS的時候刪除了這個文件,那隻能等VS關了之後才能重新得到這個文件。

使用Visual Studio活動日誌(activity log)

你可以把活動日誌當作一個表格。當你用他來記錄一條消息的時候,會在活動日誌表格里新增一行記錄。每行記錄包括如下的列:

列名

描述

Record ID

標識每條日誌的順序號。IVsActivityLog服務會自動創建這個ID。

Type

表示消息的類型,是__ACTIVITYLOG_ENTRYTYPE枚舉的文本值。該枚舉有三個選項: ALE_ERRORALE_WARNINGALE_INFORMATION

Description

日誌的描述,由開發人員自定義。

GUID

和這條日誌相關的對象的GUID,是一個可選項。可以是任何值(例如一個CLSID、一個命令ID或一個package的ID等等)

Hr

和日誌相關的HRESULT,是一個可選項。通常在為了記錄一個COM方法的返回值時使用。

Source

標識消息的來源。可以是package的名字,或者是開發者認為可以用來作為來源標識的任意字元串。

Time

記錄某條日誌的時間,是由活動日誌來決定的,開發人員不能設置它的值。

Path

和日誌相關的文件路徑。如果用默認的樣式表來顯示活動日誌的話,這一列的內容會合併到Description列中。

  • ALE_ERROR
  • ALE_WARNING
  • ALE_INFORMATION

Description 日誌的描述,由開發人員自定義。 GUID 和這條日誌相關的對象的GUID,是一個可選項。可以是任何值(例如一個CLSID、一個命令ID或一個package的ID等等) Hr 和日誌相關的HRESULT,是一個可選項。通常在為了記錄一個COM方法的返回值時使用。 Source 標識消息的來源。可以是package的名字,或者是開發者認為可以用來作為來源標識的任意字元串。 Time 記錄某條日誌的時間,是由活動日誌來決定的,開發人員不能設置它的值。 Path 和日誌相關的文件路徑。如果用默認的樣式表來顯示活動日誌的話,這一列的內容會合併到Description列中。

如果你想使用活動日誌的話,必須要通過GetService方法來得到IVsActivityLog介面的實例。可以調用這個介面提供的一些方法來把消息記錄到活動日誌中。這些方法在被調用的時候,會往不同的列中寫數據。每個方法都必須指定日誌的類型,來源和日誌描述,並且會為該日誌自動創建一個Record ID,例如LogEntry方法和LogEntryGuidHr方法,但LogEntryGuidHr方法還會為該條日誌添加GUID和Hr,而LogEntry則不會。

讓我們看一下在程式碼里怎樣把資訊記錄到活動日誌里。在下面的程式碼段中,我們利用LogEntry方法記錄了一條簡單的資訊。在這段程式碼中,我們添加了一段簡單的邏輯:如果計算兩個數的運算結果失敗的話(例如除數為0),將會記錄一條類型為error的日誌;否則記錄一條類型為information的日誌。在CalculationButton_Click方法中,去調用LogCalculation方法:

private void CalculateButton_Click(object sender, EventArgs e){  try  {    int firstArg = Int32.Parse(FirstArgEdit.Text);    int secondArg = Int32.Parse(SecondArgEdit.Text);    int result = 0;    switch (OperatorCombo.Text)    {      case "+":        result = firstArg + secondArg;        break;      ... // --- Omitted for clarity    }    ResultEdit.Text = result.ToString();  }  catch (SystemException)  {    ResultEdit.Text = "#Error";  }   //調用LogCalculation方法來記錄日誌  LogCalculation(FirstArgEdit.Text, SecondArgEdit.Text, OperatorCombo.Text, ResultEdit.Text);}

當然,LogCalculation方法還沒有定義,下面是該私有方法的定義:

private void LogCalculation(string firstArg, string secondArg,   string operation, string result){  string message = String.Format("Calculation executed: {0} {1} {2} = {3}",    firstArg, operation, secondArg, result);  IVsActivityLog log =    Package.GetGlobalService(typeof(SVsActivityLog)) as IVsActivityLog;  if (log == null) return;    log.LogEntry(    (result == "#Error")      ?(UInt32) __ACTIVITYLOG_ENTRYTYPE.ALE_ERROR      : (UInt32)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION,    "Calculation", message);}

可以看出,用活動日誌是非常簡單的。不過要注意,在上面的程式碼中,我們用的是Package.GetGlobalService 這個靜態方法來得到service。

使用output window

活動日誌里的內容,是給package開發人員調試程式的時候用的。但在很多情況下,我們希望給package的最終用戶顯示一些消息。output window是用來顯示這些消息的理想的地方。

我想我不必再介紹output window了吧,這就是output window(它通常位於VS IDE的底部):

output window有很多pane(在上圖中顯示的是「生成」這個pane)。當我們向output window中寫資訊的時候,我們實際上是向其中一個pane里寫資訊。我們可以用已有的pane,也可以創建自己的pane。在這個例子里,我們用output window中已有的「General(常規)」這個pane。

如果我問你怎樣向output window里寫資訊,你一定會回答:「使用一個服務」,沒錯,是這樣的,IVsOutputWindow服務可以幫我們向output window中寫資訊。我們可以把SVsOutputWindow類型作為參數來調用GetService方法,這樣就可以得到IVsOutputWindow介面的實例。這個介面只有3個方法:GetPaneCreatePaneDeletePane。我想這三個方法名已經告訴我們一切了。我們可以用GetPane方法的返回值(是一個IVsOutputWindowPane介面的實例)來向一個pane中寫入資訊。

現在,讓我們修改一下在CalculateButton_Click方法中調用的LogCalculation方法:

private void LogCalculation(string firstArg, string secondArg, string operation, string result){    string message = String.Format("Calculation executed: {0} {1} {2} = {3} ",      firstArg, operation, secondArg, result);     IVsOutputWindow outWindow =      Package.GetGlobalService(typeof(SVsOutputWindow)) as IVsOutputWindow;     Guid generalWindowGuid = VSConstants.GUID_OutWindowGeneralPane;    IVsOutputWindowPane windowPane;    outWindow.GetPane(ref generalWindowGuid, out windowPane);    windowPane.OutputString(message); }

紅色部分是關鍵程式碼。為了向output window里的其中一個pane中寫入資訊,我們必須調用GetPane方法來獲得這個pane的引用。在上面的程式碼段中,我們獲得了General pane的引用。每一個pane都是由一個GUID標識的,VSConstants類的靜態欄位GUID_BuildOutputWindowPane的值就是General pane的GUID。OutputString方法負責把我們的資訊寫入該pane中。

運行我們的程式,然後在我們的CalculationToolWindow工具窗中試著做幾次算術運算,相應的資訊就會顯示在輸出來源為常規(General)的pane中:

總結

在這篇文章,我們完成了我們的例子:手動的添加了一個計算器的工具窗。我們的工具窗由兩個互相協作的部分組成,其中:用戶控制項負責用戶介面的展現和計算結果這個簡單的「業務邏輯」;ToolWindowPane負責把該用戶控制項以工具窗的形式嵌入到IDE中。然後,我們在上一篇里已經創建好的菜單命令處理方法里,使用相關的程式碼來把這個工具窗顯示出來。

接著,我們創建了我們這個工具集的第一個部分:為它添加了日誌功能,可以將我們的工具窗里執行的算式記錄下來。為了添加日誌功能,我們使用了VS的活動日誌和VS的output window兩種方式。

VS的活動日誌里的內容適合給package的開發者來看(可以用來檢查、調試或修復package);VS的output window里的日誌內容適合給package的最終用戶來看(可以用來了解package正在做什麼以及做了什麼)。

在下一篇文章中,我們會重構這個例子,抽取一些程式碼和方法,用於創建我們工具集的新的部分。