240
一线老司机

VsxHowTo -- 把Windows Forms Designer作为自己的编辑器(3)

     在前两篇里,我向大家介绍了如何把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中了:

image

让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文件,应该可以看到效果了吧,但是却报了个错误:

image

     不知道为什么,我们的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的值改大一点,或者重置一下工具箱:

image

 

 

让属性窗只显示我们关心的属性

     可以在属性窗里编辑控件的属性,但有时候我们只会用到其中少数的属性,

并不想让它显示那么多,应该怎么办呢?这里介绍两种方法来过滤属性:

  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实验室里看到效果了:

image

     另:如果想让属性窗里的属性名显示为中文,只需要给相应的属性加上DisplayNameAttribute就行了。

     Windows Forms Designer这个系列就介绍到这里了,如果大家有什么问题可以给我留言。

源码下载:WinFormDesigner_Final

posted @ 2010-08-09 08:20  明年我18  阅读(3424)  评论(14编辑  收藏  举报