(翻譯)LearnVSXNow!-#4 創建一個帶有工具窗的Package

  • 2019 年 10 月 5 日
  • 筆記

上一次我們實現了一個帶有命令(Command)的package,這一次讓我們更進一步:創建一個被稱為工具窗(Tool Window)的介面。那麼,什麼是工具窗呢?讓我們想像一下:解決方案瀏覽器(Solution Explorer)、工具箱(Toolbox)、錯誤列表(Error List),它們都是工具窗(Tool Window)。

像前幾篇一樣,我們依然選擇選擇Visual Studio Integration Package類型作為項目類型,這一次我們把它命名為SimpleToolWindow。當項目嚮導出現後,我們選擇C#做為開發語言,並利用嚮導為我們的程式集自動生成一個key文件。在VSPackage Information頁面,我們輸入如下內容:

在下一步,我們選中Tool Window複選框,以便為我們的package創建一個工具窗。

緊接著,嚮導會要求我們填入工具窗口的名字(標題)和對應命令的ID,請按照下圖填入:

雖然我們沒有選擇菜單命令(Menu Command),但嚮導會幫我們在「視圖|其他窗口」子菜單下幫我們創建一個菜單項。該菜單項會和我們的工具窗關聯起來。

在嚮導的最後一步我們可以建立集成測試項目和單元測試項目,請勾掉這兩個選項並且點擊Finish按鈕。嚮導會在幾秒鐘內幫我們創建項目的源文件。

生成並運行SimpleToolWindow項目。當Visual Studio實驗室啟動後,你可以在「視圖|其他窗口」菜單下看到一個新的菜單項:

單擊這個菜單項,就會打開我們的工具窗。通過拖動它的標題欄,可以移動它到任何位置或者固定它,就像其他的工具窗一樣:

同時,嚮導幫這個工具窗生成了程式碼邏輯:當點擊這個窗口的按鈕時,它會彈出一個消息框。

源文件分析(What is inside?)

嚮導幫我們生成了PkgCmdID.cs文件,這個文件的功能和上一篇SimpleCommand中的一樣。在這裡這個文件定義了「視圖|其他窗口」菜單下的命令MyToolWindow的標識符。

嚮導也生成了用於定義菜單資源的SimpleToolWindow.vsct文件,這和上一篇的SimpleCommand一樣。

和上一篇的SimpleCommand相比,真正不一樣的地方是這裡多了兩個新文件。MyControl.cs文件定義了工具窗用到的用戶控制項MyControl類,MyToolWindow.cs文件定義了應用MyControl實例的工具窗類。當我們改變工具窗的大小時,會自動改變嵌入的MyControl的大小。

現在讓我們看一下MyControl控制項的實例是怎樣嵌入在工具窗中的,下面是MyToolWindow.cs文件中的程式碼:

1: using System;   2: using System.Windows.Forms;   3: using System.Runtime.InteropServices;   4: using Microsoft.VisualStudio.Shell;     5: namespace MyCompany.SimpleToolWindow   6: {   7:   [Guid("4469031d-23e0-483c-8566-ce978f6c9a6f")]   8:   public class MyToolWindow : ToolWindowPane   9:   {  10:     private MyControl control;    11:    12:     public MyToolWindow() : base(null)  13:     {  14:       this.Caption = Resources.ToolWindowTitle;  15:       this.BitmapResourceID = 301;  16:       this.BitmapIndex = 1;  17:       control = new MyControl();  18:     }  19:     20:     override public IWin32Window Window  21:     {  22:       get { return (IWin32Window)control; }  23:     }  24:   }  25: }

MyToolWindow類繼承了ToolWindowPane,這個基類又繼承了基類WindowPane。WindowPane實現了很多介面,包括IVsWindowPane。所有在Visual Studio里的窗口形式的用戶介面,都必須實現IVsWindowPane介面。另外,大家都知道,所有的VS-Managed對象都是COM對象,所以我們的工具窗的類必須要有一個GUID。

MyToolWidow類很簡單:它嵌入了一個MyControl控制項的實例,並在默認構造函數中初始化它。IDE和介面之間的聯繫是通過重寫Window屬性實現的,Window屬性是一個實現了IWin32Window介面的對象,它返回的就是MyControl的實例。構造函數負責設置窗口的基本資訊(標題和圖標)。

現在是時候去瞧一瞧工具窗里用到的用戶控制項的程式碼了:

1: using System.Security.Permissions;   2: using System.Windows.Forms;   3:      4: namespace MyCompany.SimpleToolWindow   5: {   6:   public partial class MyControl : UserControl   7:   {   8:     public MyControl()   9:     {  10:       InitializeComponent();  11:     }  12:     13:     [UIPermission(SecurityAction.LinkDemand,   14:       Window = UIPermissionWindow.AllWindows)]  15:     protected override bool ProcessDialogChar(char charCode)  16:     {  17:       if (charCode != ' ' && ProcessMnemonic(charCode))  18:       {  19:         return true;  20:       }  21:       return base.ProcessDialogChar(charCode);  22:     }  23:     24:     protected override bool CanEnableIme  25:     {  26:       get { return true; }  27:     }  28:     29:     private void button1_Click(object sender, System.EventArgs e)  30:     {  31:       MessageBox.Show(this,  32:         string.Format(System.Globalization.CultureInfo.CurrentUICulture,   33:         "We are inside {0}.button1_Click()", this.ToString()),  34:         "My First Tool Window");  35:     }  36:   }  37: }

我們的用戶控制項實在是太簡單了。它的主要功能就是顯示一個消息框,這個功能是在button1_click事件處理方法里實現的。這個工具窗的按鈕支援助記符號「C」,所以我們可以按快捷鍵Alt+C來代替點擊「Click Me"按鈕。這個功能是通過ProcessDialogChar方法實現的。另外,為了允許我們的package能夠支援IME(例如支援日本漢字輸入),我們必須確認CanEnableIme屬性返回true。

這就是做一個簡單的工具窗所需要做的所有事情,但是我們還有很多事情要了解。

如何顯示工具窗?

我們還需要利用「視圖|其他窗口」菜單來顯示這個工具窗。這個功能是在SimpleToolWindowPackage類中實現的。

工具窗自己並不是一個獨立的對象,它和我們的package是有聯繫的:package包含了什麼時候和怎樣去顯示工具窗的邏輯,當然也包含了和工具窗的互動邏輯以及其他服務。

我們可以通過在package類中標記ProvideToolWindowAttribute來關聯到工具窗類。同時我們必須將我們的工具窗的類型作為一個參數傳進去。

1: ...   2: [ProvideToolWindow(typeof(MyToolWindow))]    3: ...   4: public sealed class SimpleToolWindowPackage : Package   5: {   6:   ...   7: }

一個package可以(並且通常可以)包含多於一個的工具窗口,所以可以在package類上標記多個ProvideToolWindow屬性。regpkg.exe會用些Attribute來為package註冊工具窗。

僅僅註冊工具窗還不足以將它顯示出來,我們還必須要寫一些程式碼去顯示它。另外,由於一個工具窗可以有一個以上的實例,所以我們必須管理他們。在我們的例子中,VSPackage嚮導創建了MyToolWindow的一個單一的實例(姑且稱為它單例)以及下面的程式碼去顯示它(在SimpleToolWindowPackage類里):

1: private void ShowToolWindow(object sender, EventArgs e)   2: {   3:   ToolWindowPane window = this.FindToolWindow(typeof(MyToolWindow), 0, true);   4:   if ((null == window) || (null == window.Frame))   5:   {   6:     throw new NotSupportedException(Resources.CanNotCreateWindow);   7:   }   8:   IVsWindowFrame windowFrame = (IVsWindowFrame)window.Frame;   9:   Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show());  10: }  11:

工具窗實例管理的關鍵在於FindToolWindow方法。它有3個參數。第一個參數是工具窗的類型,第二個參數定義了工具窗實例的ID。從這個方法的名字上看來,我們猜測它將返回相應工具窗的實例。但是如果我們根本沒有創建它,我們又怎能返回一個工具窗的實例呢?答案是FindToolWindow的第三個參數:如果實例不存在的話,true將使這個方法創建該工具窗類的一個新實例(用指定的實例ID),並返回這個新創建的窗口實例。這就是這段程式碼實際上做的:它利用(創建或者查找)一個單一的MyToolWindow實例,該實例的ID是0。

工具窗(或任何Visual Studio里的其他窗口)顯示在實現了IVsWindowFrame介面的框(Frame)里,這個框提供了諸如位置、顯示、隱藏等功能。為了顯示工具窗,我們必須得到這個框,並調用它的Show方法。

只有成功了實例化了窗口並有一個有效的框(Frame)時,窗口才能夠顯示。

我們離完成這個例子只有一步之遙了:只剩下把事件處理邏輯關聯到菜單項了。它和我們上一個例子SimpleCommand採用相同的程式碼,這一點都不奇怪。下面是在Initialize方法中的程式碼,我們只是回顧一下:

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 toolwndCommandID =   12:       new CommandID(GuidList.guidSimpleToolWindowCmdSet,    13:       (int)PkgCmdIDList.cmdidMyFirstTool);  14:     MenuCommand menuToolWin = new MenuCommand(ShowToolWindow, toolwndCommandID);  15:     mcs.AddCommand( menuToolWin );  16:   }  17: }

如果你忘記了菜單命令和它們的事件處理邏輯是怎樣關聯的,請重新閱讀SimpleCommand這一篇。

總結

在這個非常簡單的package里,我們創建了一個工具窗,當點擊工具窗里的按鈕的時候,彈出一個消息框。我們用到了與SimpleCommand這個package里同樣的方法去創建一個菜單命令,這個命令負責顯示工具窗。另外,VSPackage嚮導增加了一些新的程式碼去實現期望的效果:

— 用戶介面(包含「Click Me!」按鈕的控制項)是一個簡單WinForm用戶控制項。

— 工具窗類繼承自ToolWindowPane,嵌入並實例化我們的用戶控制項;並且重寫Window屬性以便把這個用戶控制項的實例提供出去。

— 創建了一段事件處理方法,並調用package的FindToolWindow方法。通過調用工具窗所在的Frame的Show方法來顯示工具窗。

— 在package的初始化程式碼里,加入菜單命令和事件處理方法的關聯程式碼。