1  /  1  页   1 跳转 查看:988

写Form设计器尝试

写Form设计器尝试

原文: http://www.cnblogs.com/panjiwen/category/36995.html

  Eddie Sheffield作为微软之外的第一个发现启用Form Designer的人,确实是了不起
    几年过后的今天,已经可以找到一些关于Form Designer的资料,虽然不多也不全面,但毕竟还是有一些。我手上的资料是:


1、《Dissecting a C# Application Inside SharpDevelop》中文版,第16章专门讲Form设计器;
   
2windowsforms.net上下载的一个小例子,显然也是从SharpDevelop得到的思路;


3WinRes这个工具,随VS2005/.net FrameWork2.0 带的一个资源本地化的工具。可以反编译看看它的源码。

对比123, 我发现两者虽然都实现了Form Designer,但设计思路迥然不同显然WinRes用的办法要简明。原因是.net 2.0增强了设计器的功能并简化了设计器的调用方法。由于时间限制,短期内写一个功能完善的Form设计器可能难。我想做的是做一些测试,得出基于.net 2.0写一个Form设计器的基本方法。

我想先写一个没有任何功能但能看到设计器样式的小例子来测试。以后再慢慢增加设计的功能,比如控件的添加删除,属性的设置,保存文件等功能。


我们的步骤如下:


VS2005(我用的版本是RC)新建一个C# WindowsForm方案, 在工程的引用中增加System. Design,
Form1的代码中先添加
using System.ComponentModel.Design;



然后双击Form, 在Form的Load事件中写以下代码:
DesignSurface surface =
new DesignSurface();
surface.BeginLoad(
typeof(Form));
Control view
= (Control)surface.View;
view.Dock
= DockStyle.Fill;
this.Controls.Add(view);


然后运行方案,这是最简单也没任何用处的窗体设计器了,但至少我们能调整那个被设计Form的大小
Sinoprise Network Studio
 

写Form设计器尝试(二) PropertyGrid

如果要修改这个设计时Form的其它属性该怎么办呢?接下来的更简单了。
切换到
Form1的设计界面,往上面放一个SplitContainer, 再在这个SplitContainer的右panel上放一个PropertyGrid, 并将其Dock属性值设置成Fill
切换到代码编辑界面,将上篇中输入的代码的最后一行改成:
this.splitContainer1.Panel1.Controls.Add(view);
再加上一行: this.propertyGrid1.SelectedObject = surface.ComponentContainer.Components[0];

运行项目,现在就可以编辑那个设计时Form的其它属性了。


除了一点点拖放操作之外,只写了六行代码,就实现这样的功能,有意思吧?

我的这个专题的目标是写一个能用的窗体设计器,有什么用呢?比如您的程序发布后,用户觉得某个控件的位置需要调整,某个控件的字体颜色需要修改,没关系,用户自行修改就是了。更复杂的一点的应用是用户希望在某个单据中增加一个字段,没关系,用户或者实施人员自已加就是了,不要改代码,不要重新编译。甚至用户想增加一些处理,也可以由实施人员现场在设计器中写代码,系统能将其编译好,并在运行时调用。
Sinoprise Network Studio
 

写Form设计器尝试(三) 在窗体上添加控件

在.net 2.0以前的版本中想实现在窗体设计器中添加控件的做法是定义一个实现IToolboxService接口的“服务”,然后添加到ServiceContainer中,具体方式可以参照http://www.divil.co.uk/net/articles/designers/hosting.asp 。这个地址在www.windowsforms.net 的Code hero中也可以找到。
.net 2.0简化了这个作业,它已经为我们提供了一个实现IToolboxService接口的抽象类ToolboxService,我们要做的就是写一个类继承自ToolboxService, 要简便很多。
具体操作是新建一个继承自ToolboxService的类,名为DemoToolboxService, 加上必要的using语句,在所继承的类名上按鼠标右键,点“Implement Abstract Class”,已经帮我们自动完成了DemoToolboxService的框架,由于我们需要在设计器窗体上显示一个工具箱,就象VS左侧的那个工具面板,不过我们现在做一个简单一点的,就用ListBox, 在DemoToolboxService中添加一个类型为ListBox的私有成员,并封装成属性。稍微改动一下,实现几个必要的方法,代码如下:

using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing.Design;
using System.Windows.Forms;

namespace FormDesigner
{
   
class DemoToolboxService oolboxService
   
{

       
private ListBox toolBox;

       
public ListBox ToolBox
       
{
           
get
{ return toolBox; }
           
set
{ toolBox = value; }
        }


       
protected
override CategoryNameCollection CategoryNames
       
{
           
get
           
{
               
return
null;
            }

        }


       
//实现带分类的工具列表,由于目前不分类,所以即为全部工具

protected
override System.Collections.IList GetItemContainers(string categoryName)
       
{
            ToolboxItem[] t
=
new ToolboxItem[this.toolBox.Items.Count];
           
this.toolBox.Items.CopyTo(t, 0);

           
return t;
        }


       
//实现工具列表

protected
override System.Collections.IList GetItemContainers()
       
{
            ToolboxItem[] t
=
new ToolboxItem[this.toolBox.Items.Count];
           
this.toolBox.Items.CopyTo(t, 0);

           
return t;
        }


       
protected
override
void Refresh()
       
{
        }


       
protected
override
string SelectedCategory
       
{
           
get
           
{
               
return
null;
            }

           
set
           
{
            }

        }


       
//实现工具选择

protected
override ToolboxItemContainer SelectedItemContainer
       
{
           
get
           
{
               
if (toolBox.SelectedIndex >
0)
               
{
                   
return
new ToolboxItemContainer((ToolboxItem)toolBox.SelectedItem);
                }

               
return
null;
            }

           
set
           
{
            }

        }

    }

}



    切换到主Form的设计界面,在那个SplitContainer左侧再放上一个Panel, 做为将要完成的工具箱的容器,再切换到Form的代码编辑界面,添加一个类型为DemoToolService的私有成员toolBoxService, 修改Load事件代码,加上以下语句:
toolBoxService =
new DemoToolboxService();

toolBoxService.ToolBox
=
new ListBox();

toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(Button)));
toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(TextBox)));
toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(Label)));
toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(TabControl)));
toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(StatusBar)));

toolBoxService.ToolBox.Dock
= DockStyle.Fill;
this.panel1.Controls.Add(toolBoxService.ToolBox);

IServiceContainer container
= surface.GetService(typeof(IServiceContainer)) as IServiceContainer;

if (container !=
null)
{
container.AddService(
typeof(IToolboxService), toolBoxService);
}


  运行方案,并试着在所设计的窗体上加上几个控件,界面如下:

 
我们画呀画,很有成就感了,但是左侧的工具箱也太丑了一点,右边那个PropertyGrid居然不因我们选择了不同的控件而改变,一点都不听话。
但无论怎样,到现在为止,我们写的代码也只有30行左右。
Sinoprise Network Studio
 

写Form设计器尝试(四) 修改窗体上的控件属性

在上一次的尝试中,我们已经可以进行控件的添加了。但后来我发现了一个bug, 就是画好一个控件后,居然还可以接着画出这个控件,这不符合我们的习惯。一般情况下我们希望画好控件后,鼠标变回选择状态。这个功能在.net 2.0以前的做法是实现IToolboxServicevoid SelectedToolboxItemUsed()方法,但是在.net 2.0中我们已经可以用更简单的办法,前面讨论过,在.net 2.0中我们是通过继承ToolboxService类而不是完全实现IToolboxService的方式来简化工具箱功能。分析ToolboxService的源代码,可以看到它已经实现了SelectedToolboxItemUsed()方法, 其代码如下: void IToolboxService.SelectedToolboxItemUsed()
{
     
this.SelectedItemContainerUsed();
}


protected
virtual
void SelectedItemContainerUsed()
{
     
this.SelectedItemContainer =
null;
}


很显然,它缺省的方式将SelectedItemContainer属性值赋值为null, 这时候我们可以想到一个简便的办法是并不需要重载SelectedItemContainerUsed(), 而是修改SelectedItemContainer属性的set方法,找到上次尝试的代码,在SelectedItemContainer属性的set方法上写上:
if (value ==
null)
{
    toolBox.SelectedIndex
=
-1;
}



不过后来我觉得还是象vs的工具箱一样,提供一个”Point”的“虚”工具更方便。这个功能只要在我们前面的代码上对几个小地方稍作修改就可以了,就不帖代码了。
接下来的步骤是实现能在PropertyGrid中随意修改任何控件的属性。要做到这点也很简便,只要为DesignSurface的SelectionService实现一个SelectionChanged事件就行了。
切换到Form1的代码窗口,为窗体类添加一个私有成员:

private ISelectionService selectionService;
然后在Load事件的最后加上: selectionService = surface.GetService(typeof(ISelectionService)) as ISelectionService;
selectionService.SelectionChanged
+=
new EventHandler(selectionService_SelectionChanged);

当然也为窗体类加上以下事件方法: void selectionService_SelectionChanged(object sender, EventArgs e)
{
   
object[] selection;
   
if (selectionService.SelectionCount ==
0)
        propertyGrid1.SelectedObject
=
null;
   
else
   
{
        selection
=
new
object[selectionService.SelectionCount];
        selectionService.GetSelectedComponents().CopyTo(selection,
0);
        propertyGrid1.SelectedObjects
= selection;
    }

}


以下是运行时界面:


好象是大功告成了,不过控件只能添加,却怎么删除不了。 当然,还有一些必不可少的功能,如将设计内容序列化到资源文件或者生成代码文件等等。胡适先生说:“自古成功尝试始”,我想一定会在以后几节的尝试中成功实现这些功能的。
Sinoprise Network Studio
 

写Form设计器尝试(五) 让设计器使用自定义控件

在“写Form设计器尝试() 在窗体上添加控件”的评论中,热心关注者Leejee提出了自定义控件的问题。我于是作了一个小测试,来实现设计器中使用自定义控件。 先要准备一个自定义控件。新建一个Windows控件方案,命名为MyControl, 添加一个名为ComboBoxField的用户控件,在该用户控件上放一个Label和一个ComboBox,
生成解决方案。将生成的dll文件复制到测试目录D:\Dotnet
打开设计器方案,在工程中添加对MyControl.Dll的引用,在主窗体的代码中添加Using MyControl; 然后在有形如toolBoxService.ToolBox.Items.Add(.);的最后加上一行代码:
toolBoxService.ToolBox.Items.Add(new ToolboxItem(typeof(MyControl.ComboBoxField)));
运行方案,确实可以象使用其它标准控件一样使用这个自定义控件。嘿嘿,没有一点意外,还是和以前的试验一样简单。
但是仔细想一想,出问题了,我们需要在工程中添加对控件所在文件的引用,需要在代码中写控件的类名。也就是说我们在写设计器时,就要知道我们要使用哪些自定义控件。而我们在VS中添加自定义控件时,VS事先并不知道我们要加的是什么。要实现这个功能怎么办?理所当然地要用“反射”。

在工程中删除我们刚才添加的引用,并在主窗体代码中删除我们刚写的那两条语句。
在前面写第二条语句的地方写上:Assembly a1=Assembly.LoadFrom(@"D:\Dotnet\MyControl.dll");
toolBoxService.ToolBox.Items.Add(
new ToolboxItem(a1.GetType("MyControl.ComboBoxField")));

运行方案,效果和前面的一样。好像这里也将动态库文件名以及类型名称硬编码到了代码中,但是可以很简单地做到让这两个字符串从配置文件中读出,这样就可以实现在设计器使用时随意使用自定义控件了


后记: 这篇是昨晚写的, 我后来在睡觉时觉得还有点不对, 就是我这个方法是在载入工具箱前就要使用反射,我猜测VS的做法是在需要使用此控件时才使用反射。这个问题的解决方法是修改ToolboxService。我会在以后的一个较为完善的版本中实现这个功能。2005/11/07
Sinoprise Network Studio
 

写Form设计器尝试(六) 实现菜单命令

在写这篇的正文之前,我要衷心感谢微软公司的Jeffrey Tan先生,他居然看懂了我的文理不通的英文提问,并且花费宝贵时间来钻研,帮我解决了一个星期来我百思不得其解的问题。我当时的问题是我写了一些语句来实现标准菜单命令,但是调试的时候怎么也得不到正确的结果,事实上我研究的两个例子用的方法和我的类似,它们却都可以正常执行。我一直没有找到原因,不得不求助支持。
在“写Form设计器尝试(四) 修改窗体上的控件属性”我提出了怎么删除控件的问题,我拙作的关注者山伟也提出过用什么方法实现控件对齐更简便。所有这些问题的答案是使用MenuCommandService, 在.net 1.0/1.1的办法是手工写一个实现IMenuCommandService接口的类,将其实例添加到服务容器中,而.net 2.0已经为我们提供了MenuCommandService。

打开主窗体代码界面,为Form1添加一个私有变量:
private MenuCommandService menuCommandService;
修改Load事件代码,由于我们已经改了几次了,所以我这里全部帖出:
private
void Form1_Load(object sender, EventArgs e)
{
    DesignSurface surface
=
new DesignSurface();

    toolBoxService
=
new DemoToolboxService();
    toolBoxService.ToolBox
=
new ListBox();

    toolBoxService.ToolBox.Items.Add(
"Point");

    toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(Button)));
    toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(TextBox)));
    toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(Label)));
    toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(TabControl)));
    toolBoxService.ToolBox.Items.Add(
new ToolboxItem(typeof(StatusBar)));

 
// Assembly a1=Assembly.LoadFrom(@"D:\Dotnet\MyControl.dll");
 
// toolBoxService.ToolBox.Items.Add(new ToolboxItem(a1.GetType("MyControl.ComboBoxField")));

    toolBoxService.ToolBox.Dock
= DockStyle.Fill;
   
this.panel1.Controls.Add(toolBoxService.ToolBox);


    IServiceContainer container
= surface.GetService(typeof(IServiceContainer)) as IServiceContainer;

    menuCommandService
=
new MenuCommandService(surface);

   
if (container !=
null)
   
{
        container.AddService(
typeof(IToolboxService), toolBoxService);
        container.AddService(
typeof(IMenuCommandService), menuCommandService);
    }


    surface.BeginLoad(
typeof(Form));
    Control view
= (Control)surface.View;
    view.Dock
= DockStyle.Fill;
   
this.splitContainer1.Panel1.Controls.Add(view);
   
this.propertyGrid1.SelectedObject = surface.ComponentContainer.Components[0];


    selectionService
= surface.GetService(typeof(ISelectionService)) as ISelectionService;
    selectionService.SelectionChanged
+=
new EventHandler(selectionService_SelectionChanged);
}


  我们以删除功能来做测试,在主窗体的设计界面上添加一个MenuStrip,
在MenuStrip上添加Edit菜单项,在Edit菜单项下添加子菜单Delete, 设置其快捷键为Del, 为Delete菜单项写事件代码:

private
void deleteToolStripMenuItem_Click(object sender, EventArgs e)
{
    menuCommandService.GlobalInvoke(StandardCommands.Delete);
}



运行方案,在设计器上添加几个控件,然后在选中一个或几个控件,按下Del键或者点击菜单Edit->Delete, 所选的控件就会被删除。

其它的菜单命令如全选,如对齐等等,皆可如此实现。StandardCommands包含的命令实在太多了。

我们的设计器除了序列化资源、生成代码、事件处理这三项功能没有实现外,其它的都已经大功告成。
最近我会比较忙,剩下的内容要过段时间才会写出来,谢谢一直支持鼓励我的各位朋友们!
Sinoprise Network Studio
 

窗体设计器试验程序下载地址

目前的代码比较粗糙,仅包含了系列随笔1-6的内容。
我计划在未来的时间里将之设计成一个基本可用的提供代码生成、事件支持的设计器, 以此做为.net 2.0下Form Designer的一个参考例子。

http://www.cnblogs.com/Files/panjiwen/FormDesigner.zip
Sinoprise Network Studio
 
1  /  1  页   1 跳转

版权所有 Sinoprise Network Studio   Sitemap

Powered by Discuz!NT 2.0.1214    Copyright © 2001-2008 Comsenz Inc.
Processed in 0.3125 second(s) , 3 queries.
返顶部