(翻譯)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的初始化程式碼里,加入菜單命令和事件處理方法的關聯程式碼。