VsxHowTo — 把Windows Forms Designer作為自己的編輯器(3)

  • 2019 年 10 月 5 日
  • 筆記

在前兩篇里,我向大家介紹了如何把vs的windows forms designer作為自己的自定義編輯器,這這篇文章里我再介紹一些大家可能關心的和設計器相關的其他問題。

給toolbox添加自己的控件

首先我們要開發自己的控件。我們在WinFormsDesigner項目里添加一個Controls文件夾,用於放置自己的控件。然後添加一個MyTextBox的控件,繼承自TextBox:

using System.Windows.Forms;using System.Drawing;using System.ComponentModel; namespace Company.WinFormDesigner.Controls{    [ToolboxBitmap(typeof(TextBox))]        [DisplayName("我的文本框")]    [Description("我的文本框控件")]    [ToolboxItem(true)]        public class MyTextBox : TextBox    {        public string MyProperty { get; set; }    }}

我們需要一段代碼來把這個控件自動加道toolbox里。這裡需要用到System.Drawing.Design.IToolboxService和Microsoft.VisualStudio.Shell.Interop.IVsToolbox這兩個服務以及Package.ToolboxInitialized和Package.ToolboxUpgraded這兩個事件。目的是在初始化工具箱和重置工具箱的時候調用我們的邏輯。我們在Package的Initialize方法中來註冊這兩個事件:

protected override void Initialize(){    Trace.WriteLine(string.Format(CultureInfo.CurrentCulture, "Entering Initialize() of: {0}", this.ToString()));    base.Initialize();     //註冊Editor Factory    RegisterEditorFactory(new DocumentEditorFactory());    //註冊toolbox事件    ToolboxInitialized += OnRefreshToolbox;    ToolboxUpgraded += OnRefreshToolbox; } void OnRefreshToolbox(object sender, EventArgs e){    IToolboxService tbService =       GetService(typeof(IToolboxService)) as IToolboxService;    IVsToolbox toolbox = GetService(typeof(IVsToolbox)) as IVsToolbox;    var assembly = typeof(WinFormDesignerPackage).Assembly;    Type[] types = assembly.GetTypes();    List<ToolboxItem> tools = new List<ToolboxItem>();    foreach (var type in types)    {        try        {            //要使用ToolboxService,需要添加System.Drawing.Design引用            ToolboxItem tool = ToolboxService.GetToolboxItem(type);            if (tool == null) continue;             AttributeCollection attributes = TypeDescriptor.GetAttributes(type);             //DisplayNameAttribute不能夠自動附加到ToolboxItem的DisplayName上,所以我們在這裡要給它賦一下值            var displayName = attributes[typeof(DisplayNameAttribute)] as DisplayNameAttribute;            if (displayName != null)            {                tool.DisplayName = displayName.DisplayName;            }            tools.Add(tool);        }        catch        {        }    }    var category = "我的控件";    toolbox.RemoveTab(category);    foreach (var tool in tools)    {        tbService.AddToolboxItem(tool, category);    }    tbService.Refresh();}

OnRefreshToolbox方法負責讀取當前程序集里所有附有ToolboxItemAttribute(true)的組件,並利用ToolboxService的GetToolboxItem方法取得一個type對應的ToolboxItem。我們在MyTextBox上添加了DisplayName和Description兩個Attribute,目的是想自定義ToolboxItem在顯示的時候的名稱和描述,但ToolboxService.GetToolboxItem似乎忽略了DisplayNameAttribute,所以我們不得不在多寫兩句話來給ToolboxItem的DisplayName屬性賦值。

要想讓toolbox里顯示我們的toolboxitem,還需要在Package註冊的時候告訴vs,我們的Package是會提供ToolboxItem的,所以我們要給Package加上ProvideToolboxItemsAttribute,如下:

[PackageRegistration(UseManagedResourcesOnly = true)][DefaultRegistryRoot("Software\Microsoft\VisualStudio\9.0")][InstalledProductRegistration(false, "#110", "#112", "1.0", IconResourceID = 400)][ProvideLoadKey("Standard", "1.0", "WinFormDesigner", "Company", 1)][Guid(GuidList.guidWinFormDesignerPkgString)]//將EditorFactory和文件擴展名關聯起來[ProvideEditorExtension(typeof(DocumentEditorFactory), ".form", 100)]//添加一條註冊表項,告訴vs我們的Package會提供ToolboxItem[ProvideToolboxItems(1, true)]public sealed class WinFormDesignerPackage : Package{    ...}

ProvideToolboxItems的第一個參數1是幹嘛的呢?它表示ToolboxItem的版本號:當我們改變了MyTextBox的DisplayName屬性,或者新增加了一個控件的時候,工具窗里很有可能出現的還是舊的ToolboxItem,當遇到這個情況的時候,我們就可以把這個參數1改成大一點的數字,比如2。vs在初始化toolbox的時候發現這個數字變大了,就會重新調用OnRefreshToolbox方法,這樣toolbox裏面的內容就更新了。當然,我們也可以不改變這個數字,而是在toolbox那裡點鼠標右鍵,選擇「重置工具箱」,也可以更新我們的toolbox。

編譯我們的Package之後,用vs實驗室打開一個.form文件,我們的toolboxitem就出現在toolbox中了:

讓toolbox只顯示我們的控件

現在toolbox可以顯示我們的控件了,但它同時也顯示了很多vs內置的其它控件。我們有時候想要的效果是:當打開.form文件後,toolbox里只顯示我們自己的控件,隱藏掉其他的控件。可以用ToolboxItemFilterAttribute來實現過濾。

簡單的說一下ToolboxItemFilterAttribute,不過我對它的理解的不是很透徹,只知道大概的意思。toolbox會根據當前的DesignerHost里的RootDesigner的ToolboxItemFilter,和所有的ToolboxItem的ToolboxItemFilter相匹配,匹配通過的就顯示,匹配不通過的不顯示。

所以我們需要同時給RootDesigner和希望顯示的控件添加相匹配的ToolboxItemFilter。先給控件加吧,稍後再給RootDesigner加:

[ToolboxBitmap(typeof(TextBox))][DisplayName("我的文本框")][Description("我的文本框控件")][ToolboxItem(true)][ToolboxItemFilter("MyControls")]public class MyTextBox : TextBox{    public string MyProperty { get; set; }}

這樣就給MyTextBox這個控件指定了Filter為「MyControls」,下面我們要給RootDesigner加。

RootDesigner是當前DesignerHost里的RootComponent的Designer。所謂RootComponent,其實就是第一個被加到DesignerHost里的組件。所以在我們這個例子里,RootComponent是一個UserControl。怎樣才能給UserControl對應的RootDesigner添加ToolboxItemFilterAttribute呢?這裡有兩個方法:

  1. 利用TypeDescriptor.AddAttribute方法來動態的為RootDesigner添加ToolboxItemFilterAttribute。
  2. 做一個控件,繼承UserControl,把它作為RootComponent,給這個控件指定自己的Designer,然後就可以在這個Designer上添加ToolboxItemFilterAttribute了。

我們先來看一下第一種方法:用TypeDescriptor.AddAttribute方法為RootDesigner動態的添加Attribute。這個方法很簡單,在DesignerLoader的PerformLoad里:

class DesignerLoader : BasicDesignerLoader{    protected override void PerformLoad(IDesignerSerializationManager serializationManager)    {        ControlSerializer serializer = new ControlSerializer(_data.DocumentMoniker);        Control control = serializer.Deserialize();        //把控件的引用傳給DocumentData,這樣它保存的時候就可以序列化這個控件了        _data.Control = control;        AddControl(control);            var rootDesigner = LoaderHost.GetDesigner(LoaderHost.RootComponent);        TypeDescriptor.AddAttributes(rootDesigner, new ToolboxItemFilterAttribute("MyControls", ToolboxItemFilterType.Require) );    } }

編譯並運行項目,打開.form文件後,可以看到toolbox里只剩下我們的MyTextBox了。如果工具箱里什麼都沒有顯示或顯示的不對,那就把ProvideToolboxItems的值改大一點,或者重置一下工具箱。

現在我們再來看一下第二種方法,這種方法比第一種要複雜一些,不過很多情況下需要採用這種方法,因為我們可能需要自定義RootComponent和RootDesigner。添加一個控件,繼承自UserControl,並為它做一個RootDesigner,繼承自DocumentDesigner:

using System.Windows.Forms;using System.Windows.Forms.Design;using System.ComponentModel;using System.ComponentModel.Design; namespace Company.WinFormDesigner.Controls{    [Designer(typeof(MyRootControlDesigner), typeof(IRootDesigner))]    class MyRootControl : UserControl    {    }     [ToolboxItemFilter("MyControls", ToolboxItemFilterType.Require)]    class MyRootControlDesigner : DocumentDesigner    {    }}

注意,MyRootControlDesigner一定要繼承自DocumentDesigner,否則就不是一個windows forms designer了。我們給MyRootControlDesigner添加了ToolboxItemFilter這個Attribute,並指定和MyTextBox一樣的字符串「MyControls」。

然後修改ControlSerializer里的反序列化方法,使其反序列化成MyRootControl:

public Control Deserialize(){    /*     * 讀取文件DocumentMoniker的內容,並把它反序列化成Control。     * 下面的代碼只是模擬這個過程,並沒有真正讀取文件並反序列化     * 注意控件有可能是複合控件,這種控件的子控件是不需要加到DesignerHost里的,     * 所以我給控件的Tag屬性設了一個Designable的字符串,目的是在後面     * 區分出哪些控件是需要設計的,哪些控件是屬於不需要設計的     * */     const string designable = "Designable";    MyRootControl uc = new MyRootControl() { Dock = DockStyle.Fill, BackColor = Color.White };    uc.Controls.Add(new MyTextBox { Tag = designable, Location = new System.Drawing.Point(200, 300) });    return uc;}

注意這裡我只是模擬了反序列化這個過程,並沒有真正實現反序列化和序列化。具體的反序列化和序列化邏輯大家可以自己實現,例如可以把它序列化到xml文件里。

編譯項目,然後在vs實驗室里打開.form文件,應該可以看到效果了吧,但是卻報了個錯誤:

不知道為什麼,我們的Package的程序集如果不在gac里的話,vs實驗室不能加載MyRootControlDesigner,調試的時候明明已經看到CurrentDomain里已經有我們這個程序集了。不過沒關係,我們可以給CurrentDomain註冊一個AssemblyResolve事件處理程序,這樣vs就能加載我們的RootDesigner了:

public sealed class WinFormDesignerPackage : Package{    ...    protected override void Initialize()    {       ...        //解決無法加載MyRootControl的設計器的問題        AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;    }        private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)    {        AppDomain domain = (AppDomain)sender;        foreach (Assembly asm in domain.GetAssemblies())        {            if (asm.FullName == args.Name)                return asm;        }        return null;    }    ...}

編譯項目,然後在vs實驗室里打開.form文件,這下應該可以了。如果工具箱里什麼都沒有顯示或顯示的不對,那就把ProvideToolboxItems的值改大一點,或者重置一下工具箱:

讓屬性窗只顯示我們關心的屬性

可以在屬性窗里編輯控件的屬性,但有時候我們只會用到其中少數的屬性,

並不想讓它顯示那麼多,應該怎麼辦呢?這裡介紹兩種方法來過濾屬性:

  1. 如果控件的designer是自己寫的話,可以重寫ControlDesigner的PreFilterProperties方法。
  2. 實現一個ITypeDescriptorFilterService服務,並添加到DesignerHost里。

在這裡推薦第二種方法,因為這種方法可以統一處理屬性的過濾邏輯。我們在項目里添加一個ControlTypeDescriptorFilter類,並讓它實現ITypeDescriptorFilterService,如下:

using System;using System.ComponentModel.Design;using System.ComponentModel;using System.Collections;namespace Company.WinFormDesigner{    class ControlTypeDescriptorFilter : ITypeDescriptorFilterService    {        //DesignerHost中默認的ITypeDescriptorFilterService        private readonly ITypeDescriptorFilterService oldService;        public ControlTypeDescriptorFilter(ITypeDescriptorFilterService oldService)        {            this.oldService = oldService;        }        public bool FilterAttributes(IComponent component, IDictionary attributes)        {            if (oldService != null)            {                oldService.FilterAttributes(component, attributes);            }            return true;        }        public bool FilterEvents(IComponent component, IDictionary events)        {            if (oldService != null)            {                oldService.FilterEvents(component, events);            }            return true;        }        public bool FilterProperties(IComponent component, IDictionary properties)        {            if (oldService != null)            {                oldService.FilterProperties(component, properties);            }        }            }}

在這個類里,保存了DesignerHost中默認的ITypeDescriptorFilterService服務的實例,並調用這個默認服務的相關方法。大家可能注意到了我上面這段代碼是一點用處都沒有,沒錯,我還沒有實現自己的過濾邏輯。下面我們來規定一下哪些屬性需要被過濾掉:我們定義一個BrowsablePropertyAttribute,只有顯式地指定了這個Attribute的屬性才顯示出來,否則就隱藏它。不過為了維持設計時的特性,我們還需要把「Locked」屬性顯示出來,要不然就喪失了「鎖定控件」的功能了。BrowsablePropertyAttribute類定義如下:

using System; namespace Company.WinFormDesigner{    [AttributeUsage(AttributeTargets.Property)]    class BrowsablePropertyAttribute : Attribute    {    }}

這個Attribute很簡單,沒什麼好說的,接下來我們給MyTextBox的MyProperty指定這個Attribute:

using System.Windows.Forms;using System.Drawing;using System.ComponentModel; namespace Company.WinFormDesigner.Controls{    [ToolboxBitmap(typeof(TextBox))]    [DisplayName("我的文本框")]    [Description("我的文本框控件")]    [ToolboxItem(true)]    [ToolboxItemFilter("MyControls")]    public class MyTextBox : TextBox    {        [BrowsableProperty]        public string MyProperty { get; set; }    }}

然後我們要修改一下ControlTypeDescriptorFilter的FilterProperties方法,對於沒有指定BrowsablePropertyAttribute的屬性,我們動態的把它們的BrowsableAttribute設置成false,這樣就不會顯示在屬性窗里了:

class ControlTypeDescriptorFilter : ITypeDescriptorFilterService{   ...    public bool FilterProperties(IComponent component, IDictionary properties)    {        if (oldService != null)        {            oldService.FilterProperties(component, properties);        }         var hiddenProperties = new List<string>();         foreach (string name in properties.Keys)        {            var property = properties[name] as PropertyDescriptor;            if (property == null) continue;             //不可見的屬性,就沒必要過濾它們了,反正不過濾也不可見            if (!property.IsBrowsable) continue;             //只有顯式的指定了BrowsablePropertyAttribute的屬性才顯示在屬性窗里            if (property.Attributes[typeof(BrowsablePropertyAttribute)] != null)            {                continue;            }            if (name == "Locked") continue;             hiddenProperties.Add(name);        }         foreach (var name in hiddenProperties)        {            var property = properties[name] as PropertyDescriptor;            if (property == null) continue;            properties[name] = TypeDescriptor.CreateProperty(property.ComponentType, property, new BrowsableAttribute(false));        }         return true;    }}

最後,我們需要把ControlTypeDescriptorFilter這個服務加到DesignerHost里,修改一下DesignerLoader的Initialize方法:

class DesignerLoader : BasicDesignerLoader{    private DocumentData _data;    public DesignerLoader(DocumentData data)    {        _data = data;    }     protected override void Initialize()    {        base.Initialize();        ....        var oldFilterService = LoaderHost.GetService(typeof(ITypeDescriptorFilterService)) as ITypeDescriptorFilterService;        if (oldFilterService != null)        {            LoaderHost.RemoveService(typeof(ITypeDescriptorFilterService));        }        var newService = new ControlTypeDescriptorFilter(oldFilterService);        LoaderHost.AddService(typeof(ITypeDescriptorFilterService), newService);    }}

編譯並運行項目,就可以在vs實驗室里看到效果了:

另:如果想讓屬性窗里的屬性名顯示為中文,只需要給相應的屬性加上DisplayNameAttribute就行了。

Windows Forms Designer這個系列就介紹到這裡了,如果大家有什麼問題可以給我留言。