前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >VsxHowTo -- 把Windows Forms Designer作为自己的编辑器(2)

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

作者头像
明年我18
发布2019-09-18 11:39:51
4070
发布2019-09-18 11:39:51
举报
文章被收录于专栏:明年我18

我们在上一篇文章里利用Windows Forms Designer做了一个简单的表单设计器,但这个设计器还存在一些问题,比如控件不能自动命名;文档窗口不会自动加入dirty标记;不能undo/redo和copy/paste;不能保存和读取数据等等。这一篇我们来逐一解决这些问题。

控件自动命名

从toolbox里拖入一个控件时,如果想让控件自动命名,我们需要往DesignerHost里加一个INameCreationService的服务,我没有研究过为什么BasicDesignerLoader不默认帮我们加上,不过好在msdn上有一个例子,我稍微简化了一下它,如下:

代码语言:javascript
复制
using System;using System.ComponentModel;using System.ComponentModel.Design.Serialization;using System.Collections.Generic;using System.Collections; namespace Company.WinFormDesigner{    class NameCreationService : INameCreationService    {        #region INameCreationService 成员         public string CreateName(IContainer container, Type dataType)        {            IList<string> list = new List<string>();            for (int i = 0; i < container.Components.Count; i++)            {                list.Add(container.Components[i].Site.Name);            }                        return CreateNameByList(list, dataType.Name);         }         public bool IsValidName(string name)        {            //name is always valid            return true;        }        public void ValidateName(string name)        {            //do nothing        }         #endregion         /// <summary>        /// 创建一个基于baseName并且在array中不存在的名称        /// </summary>        public static string CreateNameByList(IList<string> list, string baseName)        {            int uniqueID = 1;            bool unique = false;            while (!unique)            {                unique = true;                foreach (string s in list)                {                    if (s.StartsWith(baseName + uniqueID.ToString()))                    {                        unique = false;                        uniqueID++;                        break;                    }                }            }            return baseName + uniqueID.ToString();        }    }}

我们需要把它的实例加到DesignerHost中。在DesignerLoader中重写Initialize方法:

代码语言:javascript
复制
protected override void Initialize(){    base.Initialize();              LoaderHost.AddService(typeof(INameCreationService), new NameCreationService());        }

按ctrl + F5运行,启动VS实验室之后,打开一个.form文件,在工具箱中拖入一个Button,看,是不是已经自动有了Name了呢?

支持Undo/Redo

这个问题纠结了我好久,因为实在有太多的方法实现Undo和Redo了,最后通过reflector才找到最佳的解决方案。和控件的自动命名一样,不能Undo/Redo也是由于少了一个Service:ComponentSerializationService。.net framework提供了一个默认的实现:System.ComponentModel.Design.Serialization.CodeDomComponentSerializationService。我一度以为不能直接使用这个类,因为我们并没有CodeDom,不过在尝试了很多方法后,最后在无奈的情况下才去使用这个类,居然成功了,看来绝不能以貌取人。由于已经有实现了,我们只需要把它加到DesignerHost里就行了,依然要修改DesignerLoader的Initialize方法:

代码语言:javascript
复制
protected override void Initialize(){    base.Initialize();        LoaderHost.AddService(typeof(INameCreationService), new NameCreationService());    LoaderHost.AddService(typeof(ComponentSerializationService), new CodeDomComponentSerializationService(LoaderHost));}

大家可以测试一下效果,真的可以undo/redo了,呵呵。

支持Copy/Paste

到目前为止,我们的设计器似乎不支持复制和粘贴。选中一个控件后,复制的按钮是可用的,但粘贴却一直是灰色的,这是怎么回事呢?没错,你猜对了,DesignerHost里少了相应的Service。这个Service叫做System.ComponentModel.Design.Serialization.IDesignerSerializationService。这个接口有两个方法,分别处理序列化和反序列化的工作,我没有找到.net framework里公开的实现,所以我们不得不自己实现这个接口了。不过好在我们可以充分利用undo/redo里提到的ComponentSerializationService来帮助我们去序列化和反序列化控件。我的实现类如下:

代码语言:javascript
复制
using System;using System.ComponentModel.Design.Serialization;using System.ComponentModel.Design;using System.Collections; namespace Company.WinFormDesigner{    class DesignerSerializationService : IDesignerSerializationService    {        private IServiceProvider serviceProvider;        private ComponentSerializationService serializer;         public DesignerSerializationService(IServiceProvider sp)        {            serviceProvider = sp;            serializer = serviceProvider.GetService(typeof(ComponentSerializationService)) as ComponentSerializationService;        }         #region IDesignerSerializationService 成员         public ICollection Deserialize(object serializationData)        {            if (serializer != null && serializationData is SerializationStore)            {                return serializer.Deserialize((SerializationStore)serializationData);            }            return null;         }         public object Serialize(ICollection objects)        {            if (objects == null)            {                objects = new object[0];            }            if (serializer != null)            {                SerializationStore store = serializer.CreateStore();                using (store)                {                    foreach (object obj in objects)                    {                        serializer.Serialize(store, obj);                    }                }                return store;            }            return null;        }         #endregion    }}

接下来,我们需要把它加到DesignerHost里:

代码语言:javascript
复制
protected override void Initialize(){    base.Initialize();    LoaderHost.AddService(typeof(INameCreationService), new NameCreationService());    LoaderHost.AddService(typeof(ComponentSerializationService), new CodeDomComponentSerializationService(LoaderHost));    LoaderHost.AddService(typeof(IDesignerSerializationService), new DesignerSerializationService(LoaderHost));}

SetDirty

设计器里的控件改变之后,我们希望在document window那里能出现dirty的标记。为了实现这个功能,我们需要修改在上一篇中创建的DocumentData类,这个类实现了IVsPersistDocData接口,其中IsDocDataDirty就是用来判断当前的文档是否是dirty的。让我们修改一些这个类:

代码语言:javascript
复制
namespace Company.WinFormDesigner{    class DocumentData : IVsPersistDocData    {              ....       public bool Dirty { get; set; }                    int IVsPersistDocData.IsDocDataDirty(out int pfDirty)        {            pfDirty = Dirty ? 1 : 0;            return VSConstants.S_OK;        }        ....    }}

我们在DocumentData里添加了一个Dirty的bool属性,并且修改了IVsPersistDocData.IsDocDataDirty,根据Dirty的属性值来确定pfDirty的值。

下面我们需要在控件修改的时候给这个Dirty属性赋值。怎样捕获控件修改的事件呢?可以通过IComponentChangeService服务的ComponentChanged事件来捕获。在DesignerLoader的初始化方法里,取得这个服务,并添加ComponentChanged的事件处理:

代码语言:javascript
复制
protected override void Initialize(){    base.Initialize();     LoaderHost.AddService(typeof(INameCreationService), new NameCreationService());    LoaderHost.AddService(typeof(ComponentSerializationService), new CodeDomComponentSerializationService(LoaderHost));    LoaderHost.AddService(typeof(IDesignerSerializationService), new DesignerSerializationService(LoaderHost));    IComponentChangeService service = LoaderHost.GetService(typeof(IComponentChangeService)) as IComponentChangeService;    service.ComponentChanged += new ComponentChangedEventHandler(service_ComponentChanged); }

然后就可以在service_ComponentChanged方法里给DocumentData的Dirty属性赋值了,当然,在这之前,我们需要修改一下DesignerLoader的构造函数,以便我们可以取得DocumentData的引用:

代码语言:javascript
复制
class DesignerLoader : BasicDesignerLoader{    private DocumentData _data;    public DesignerLoader(DocumentData data)    {        _data = data;    }     protected override void Initialize()    {        base.Initialize();         LoaderHost.AddService(typeof(INameCreationService), new NameCreationService());        LoaderHost.AddService(typeof(ComponentSerializationService), new CodeDomComponentSerializationService(LoaderHost));        LoaderHost.AddService(typeof(IDesignerSerializationService), new DesignerSerializationService(LoaderHost));        IComponentChangeService service = LoaderHost.GetService(typeof(IComponentChangeService)) as IComponentChangeService;        service.ComponentChanged += new ComponentChangedEventHandler(service_ComponentChanged);     }     void service_ComponentChanged(object sender, ComponentChangedEventArgs e)    {        _data.Dirty = true;    }     protected override void PerformFlush(IDesignerSerializationManager serializationManager)    {    }     protected override void PerformLoad(IDesignerSerializationManager serializationManager)    {        LoaderHost.Container.Add(new UserControl());    }}

最后,记得要修改EditorFactory里构造DesignerLoader时的代码:

代码语言:javascript
复制
...var data = new DocumentData();var designerLoader = new DesignerLoader(data);...

编译并运行项目,可以然后拖一个控件到设计器,Dirty的标记出来了。

保存/加载文档

要实现文档的保存与加载,需要让DocumentData实现IVsPersistDocData和IPersistFileFormat接口。在上次我们已经简单实现了IVsPersistDocData了,现在我们需要再让他实现IPersistFileFormat接口。用于保存的方法是IVsPersistDocData.SaveDocData和IPersistFileFormat.Save。

保存文档,无非就是把DesignerHost中正在设计的UserControl以及它的子控件用某种方式序列化到文件里,而加载文档则相反:读取文件,并反序列化成控件,并把控件加到DesignerHost里。

我们定义一个类用于处理控件的序列化和反序列化:

代码语言:javascript
复制
using System.Windows.Forms; namespace Company.WinFormDesigner{    class ControlSerializer    {        public string DocumentMoniker { get; set; }         public ControlSerializer(string documentMoniker)        {            DocumentMoniker = documentMoniker;        }         public Control Deserialize()        {            /*             * 读取文件DocumentMoniker的内容,并把它反序列化成Control。             * 下面的代码只是模拟这个过程,并没有真正读取文件并反序列化             * 注意控件有可能是复合控件,这种控件的子控件是不需要加到DesignerHost里的,             * 所以我给控件的Tag属性设了一个Designable的字符串,目的是在后面             * 区分出哪些控件是需要设计的,哪些控件是属于不需要设计的             * */             const string designable = "Designable";            UserControl uc = new UserControl();            uc.Controls.Add(new Label { Text = "Label1", Tag = designable });            uc.Controls.Add(new TextBox { Tag = designable, Location = new System.Drawing.Point(200, 500) });            return uc;        }         public void Serialize(Control control)        {            /*             * 序列化control,并保存到文件DocumentMoniker中。有些属性比如Site之类的,最好不要序列化             * 下面的代码只是模拟这个过程,没有真正的序列化控件             * */            MessageBox.Show("序列化。。。");        }    }}

注意到我并没有实现序列化和反序列化的真正逻辑,因为这和vsx无关,大家可以自己去实现。不过要注意的是Control的部分属性是没有必要序列化到文件里的,所以在序列化的时候要过滤些属性,例如根据BrowsableAttribute来决定哪些属性可以被序列化。

我们需要把文件的路径传给DocumentData,并且在DocumentData里定义一个Control类型的属性:

代码语言:javascript
复制
class DocumentData : IVsPersistDocData, IPersistFileFormat{       ...    public Control Control { get; set; }    public string DocumentMoniker { get; set; }    public DocumentData(string docPath)    {        DocumentMoniker = docPath;        _fileName = docPath;    }    ...}

在DesignerLoader的PerformLoad方法里,调用反序列化方法,并把反序列化出来的控件加到DesignerHost里:

代码语言:javascript
复制
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);    }     private void AddControl(Control parent)    {        LoaderHost.Container.Add(parent);        foreach (Control child in parent.Controls)        {            if (child.Tag == "Designable")            {                AddControl(child);            }        }    }}

在DocumentData的Save方法里,调用如下的代码:

代码语言:javascript
复制
class DocumentData : IVsPersistDocData, IPersistFileFormat{    ....    private void SaveFile(string fileName)    {        ControlSerializer serializer = new ControlSerializer(fileName);        serializer.Serialize(Control);        Dirty = false;    }    ....}

这样就可以保存和加载文档了。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2010-08-06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 控件自动命名
  • 支持Undo/Redo
  • 支持Copy/Paste
  • SetDirty
  • 保存/加载文档
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档