可复用代码:组件的来龙去脉

相关文章链接

编程之基础:数据类型(一)

编程之基础:数据类型(二)

高屋建瓴:梳理编程约定

动力之源:代码中的泵

难免的尴尬:代码依赖

重中之重:委托与事件

物以类聚:对象也有生命

可复用代码:组件的来龙去脉

平时常说的"组件"包含的范围比较广泛,一个程序集、一个链接库甚至代码中的一个类都可以称为"组件",而本章讲到的"组件"仅指.NET编程过程中实现了System.ComponentModel.IComponent接口的类型,包含范围相对比较狭隘。本章介绍了组件的定义及其作用、组件的两种状态(设计时与运行时),还讲到了"容器-组件-服务模型"以及应用了该模型的"窗体设计器"(Form Designer),最后提到了组件中的一个分支:控件以及自定义控件的分类。

7.1 .NET中的组件

7.1.1 组件的定义

本章讨论的组件与传统意义中的组件不同。传统上讲,任何一个模块均可以称为"组件",大到一个程序集或者一个动态链接库,小到代码层面上的一个类型,这些都能称之为组件,它们被当作一个整体,能对外提供特定的功能。

本章讨论的组件范围相对来说更狭隘。在.NET编程中,我们把实现(直接或者间接)System.ComponentModel.IComponent接口的类型称为"组件",其余都不是本章将要讨论的范畴。IComponent接口的定义如下:

1 //Code 7-1
2 public interface IComponent : IDisposable
3 {
4        event EventHandler Disposed;
5        ISite Site { get; set; }
6 }

如上代码Code 7-1所示,IComponent接口实现了IDisposable接口,说明它具有IDisposable接口的特性(言下之意便是,我们必须按照第四章中讲IDisposable接口那样去定义组件)。另外它还包含一个Disposed事件,顾名思义,该事件会在组件Dispose时激发。IComponent接口还包含一个ISite类型的属性,从该属性的名称基本上可以确定,该属性跟定位有关,没错,它就是用来帮助组件定位的,后面将会介绍到该属性。

.NET框架中有一个IComponent接口的默认实现:System.ComponentModel.Component类型。该类型默认实现了接口中的方法、事件以及属性,框架中包含的其它预定义组件大部分均派生自该Component类。

7.1.2 Windows Forms中的组件

前面讲到过,在.NET中只要实现了IComponent接口的类型均叫组件。整个.NET框架中有许许多多的类型实现了IComponent接口,因此它们均称为组件。下图7-1显示了Windows Forms框架中包含常见组件的关系:

 

图7-1 Windows Forms中组件之间的关系

如上图7-1所示,在System.Windows.Forms命名空间中,有许多组件均派生自Component类,我们需要注意到,Control类型也派生自Component类型,因此,Control类(及其派生类)均属于组件。

这里需要强调一下,组件和控件不是相等的,组件包含控件,控件只是组件中的一个分类。另外我们常见的System.Windows.Forms.Timer以及System.Windows.Forms.ToolTip等等严格上讲不能称之为"控件"(虽然我们已经习惯这样称呼)。

7.1.3 Windows Forms中的控件

控件的种类也有很多,本章中我们只讨论Windows Forms中的控件,我们把派生自System.Windows.Forms.Control类的类型称为"控件"。控件中包含有处理Windows消息的功能(比如窗口过程,详见第八章),因此控件都能以某一种方式在屏幕上呈现出来。在Winform编程中,窗体也属于控件,我们可以看到窗体类(Form)也派生自Control类:

 

图7-2 Windows Forms中控件之间的关系

如上图7-2所示,所有的控件均派生自Control类,Control类又属于组件,因此所有控件均具备组件的特性。

不管组件还是控件,它们都是可以重复使用的代码集合,都实现了IDisposable接口,都需要遵循第四章中讲到的Dispose模式。如果一个类型使用了非托管资源,它实现IDisposable接口就可以,那为什么.NET编程中又要提出组件的概念呢?对于这个问题,并没有官方答案,不过从.NET框架的结构和微软为开发者提供的可视化开发环境(比如Visual Studio)来看,这样做可以说完全是为了实现程序的"可视化开发",也就是我们常说的"所见即所得"。在类似Visual Studio这样的开发环境中,一切"组件"均可被可视化设计,换句话说,只要我们定义的类型实现了IComponent接口,那么在开发阶段,该类型就可以出现在窗体设计器中,我们可以使用窗体设计器编辑它的属性、给它注册事件,它还能被窗体设计器中别的组件识别等等。具体介绍请参见本章接下来的几节。

注:控件是.NET编程中的一个重点,它能够提高程序开发效率。Asp.NET中的所有服务器控件均继承自System.Web.UI.Control类,它们之间的关系与Windows Forms中的控件结构类似。

7.2 容器-组件-服务模型

7.2.1 容器的另类定义

这里讨论的容器与我们所熟知的传统容器不同。谈到容器,我们或许会想到ArraList、Queue又或者HashTable等类,它们的共同特征就是可以存放数据,更具体一点的描述就是:我们能将一个元素放入到容器内部。但本章讨论的容器跟这些都不相同,它们没有物理包含的意思,只是逻辑上包含元素。如果称传统容器为物理容器,那么称这种只是逻辑上包含元素的容器为逻辑容器。两种容器的差别如下图7-3:

 

图7-3 物理容器和逻辑容器的区别

如上图7-3所示,物理容器中可以存放元素,但是逻辑容器却没有空间上的限制,物理容器中的元素可以属于另外一个逻辑容器。

注:物理容器强调空间上的包含与被包含。对于像QueueStatck或者ArrayList这样的物理容器,如果是引用类型对象,存放在这些容器内部的大部分都是对象的引用,而对象实例本身则在容器外部。

.NET编程中,把所有实现(直接或间接)System.ComponentModel.IContainer接口的类型称为逻辑容器(以下简称"容器")。其余都不在本章讨论的范畴之内,IContainer接口定义如下:

1 //Code 7-2
2 public interface IContainer : IDisposable
3 {
4       void Add(IComponent component);
5       void Add(IComponent component, string name);
6       void Remove(IComponent component);
7       ComponentCollection Components { get; }
8 }

如上代码Code 7-2所示,IContainer接口也实现了IDisposable接口,说明它具有IDisposable接口的特性(言下之意就是,我们必须按照第四章中讲IDisposable接口那样去定义容器)。另外它还包含几个添加、移除元素的方法,以及一个组件集合成员。我们可以发现,不管是方法的参数还是集合元素类型,都是组件类型,可以猜测到,容器当然是为组件服务的。没错,本章讨论到的容器只能逻辑包含组件,只有组件才能成为它的逻辑元素。

.NET框架中有一个IContainer接口的默认实现:System.ComponentModel.Container类型,该类型默认实现了IContainer接口中的方法以及属性。

现在我们已经知道,容器包含的逻辑元素是组件,容器也实现了IDisposable接口,很显然,我们可以通过容器统一管理各个逻辑元素的非托管资源。但容器对于组件来讲,仅仅是为了更方便地管理组件的非托管资源吗?如果是这样,完全没必要存在本章讨论的这种容器,看来,容器和组件相结合起来,还有其它的作用。

7.2.2 容器与组件的合作

前面讲到过,传统容器中可以物理包含元素,但是这些元素之间是相互独立、不能互相通讯的。也就是说,如果ArrayList中包含A和B两个元素,那么A是不知道B的存在,B也不知道A的存在,B也更不可能告诉A:我今年25岁。传统容器仅仅是在空间上简单地将数据组织在一起,并不能为数据之间的交互提供支持。而本章讨论的逻辑容器,在某种意义上讲,更高级,它能为组件(逻辑元素)之间的通讯提供支持,组件与组件之间不再是独立存在的,此外,它还能直接给组件提供某些服务。下图7-4显示物理容器和逻辑容器分别与元素之间的关系:

 

图7-4 容器与元素之间的关系

如上图7-4所示,物理容器中的元素之间不能相互通讯,物理容器也不可能为内部元素提供服务,逻辑容器中的组件之间可以通过逻辑容器作为桥梁,进行数据交换,同时,逻辑容器还能给各个组件提供服务。

上面提到过"服务",所谓服务,就是指逻辑容器能够给组件提供一些访问支持,比如某个组件需要知道它所属容器共包含有多少个组件,那么它就可以向容器请求,容器为它返回一个获取组件总数的接口。类似下图7-5:

 

图7-5 组件向容器请求服务

如上图7-5所示,组件向容器请求"计数服务",容器给组件返回一个ICountService的接口,该接口专门负责计数相关的操作,组件可以使用该接口获取当前容器中所有组件的总数。同理,组件还可以向容器请求其它类型的服务,只要容器可以提供。

到此,我们已经大概知道了逻辑容器存在的真正目的,那就是为所有属于该容器的组件提供服务,使组件与组件之间能够自由交互。那么,容器和组件内部到底有着怎样的实现,才会达到如此效果?在本章7.1.1小节中就提到过IComponent接口中有一个ISite类型的属性,当时说是起到一个"定位"的作用,现在看来,组件与容器之间的纽带就是它,组件通过该属性可以与它所属容器取得联系。

我们看一下ISite接口的默认实现Site类型内部结构:

 1 //Code 7-3
 2 private class Site : ISite, IServiceProvider
 3 {
 4         private IComponent component;
 5         private Container container;
 6         private string name;
 7         internal Site(IComponent component, Container container, string name);
 8         public object GetService(Type service); //NO.1
 9         public IComponent Component { get; } //NO.2
10         public IContainer Container { get; } //NO.3
11         public bool DesignMode { get; }
12         public string Name { get; set; }
13 }

如上代码Code 7-3所示,Site类型中包含容器Container以及组件Component属性(NO.3和NO.2处),它两正是分别代表当前组件以及该组件所属容器。Site类型中还包含一个GetService方法(NO.1处),该方法是组件向容器请求服务的关键。

我们再来看一下IComponent接口的默认实现Component类型内部结构(不全):

 1 //Code 7-4
 2 public class Component : MarshalByRefObject, IComponent, IDisposable
 3 {
 4      private static readonly object EventDisposed;
 5      private EventHandlerList events;
 6      private ISite site;
 7      [Browsable(false),  EditorBrowsable(EditorBrowsableState.Advanced)]
 8      public event EventHandler Disposed;
 9      static Component();
10      public Component();
11      public void Dispose();
12      protected virtual void Dispose(bool disposing);
13      protected override void Finalize();
14      protected virtual object GetService(Type service); //NO.1
15      public override string ToString();
16      //
17 }

如上代码Code 7-4所示,Component类中包含一个GetService方法(NO.1处),该方法的作用就是向它所在容器请求服务(见图7-5),所有Component的派生类均可以使用GetService请求服务。

最后再看一下IContainer接口的默认实现Container类型内部结构(不全):

 1 //Code 7-5
 2 public class Container : IContainer, IDisposable
 3 {
 4       private ComponentCollection components;
 5       private int siteCount;
 6       private ISite[] sites;
 7       private object syncObj;
 8       public Container();
 9       public virtual void Add(IComponent component);
10       public virtual void Add(IComponent component, string name);
11       protected virtual ISite CreateSite(IComponent component, string name);
12       public void Dispose();
13       protected virtual void Dispose(bool disposing);
14       protected override void Finalize();
15       protected virtual object GetService(Type service); //NO.1
16       public virtual void Remove(IComponent component);
17       private void Remove(IComponent component, bool preserveSite);
18       //
19 }

如上代码Code 7-5所示,Container类中也包含一个GetService方法,它专门为组件提供服务,注意它是一个虚方法,也就是说,如果我们从它派生出来一个新的容器,我们完全可以在新容器中重写该虚方法,增加新的服务(Container容器默认不提供任何服务)。下面代码创建一个新容器,为组件提供计数服务:

 1 //Code 7-6
 2 public class MyContainer:Container
 3 {
 4     protected override object GetService(Type service) //NO.1
 5     {
 6         if(service == typeof(ICountService))
 7         {
 8             return new CountService(this); //NO.2
 9         }
10         return base.GetService(service);
11     }
12 }
13 public interface ICountService //NO.3
14 {
15     //
16     int GetAllComponentsCount();
17 }
18 class CountService:ICountService //NO.4
19 {
20     MyContainer _container;
21     public CountService(MyContainer container)
22     {
23         _container = container;
24     }
25     public int GetAllComponentsCount() //NO.5
26     {
27         //
28     }
29 }

如上代码Code 7-6所示,新容器MyContainer重写了GetService方法(NO.1处),当组件请求计数服务时,GetService方法为组件返回一个ICountService接口(NO.2处),组件之后就可以通过该接口获取当前容器中组件的总数目(NO.5处)。

上面可以看到,Component、Site以及Container三个类型均包含有获取服务的方法GetService,现在我们可以整理一下,组件向容器请求服务的流程:

 

图7-6 组件请求服务流程

如上图7-6所示,组件内部在请求服务的时候,先要判断自己是否在某一个容器中,如果不在容器中,那么返回null;如果在容器中,则以Site作为桥梁(Site.GetService),去获取容器中的服务(Container.GetService)。

注:容器将组件添加进来的时候(执行Container.Add),会初始化该组件的Site属性,让该组件与容器产生关联,只有当这一过程发生之后,组件才能获取容器的服务。有关ComponentSite以及Container类型的详细信息请使用反编译工具查看.NET源码。

在编程中,使用"容器-组件-服务"模型时,只要容器不变(意味着提供服务的规则不变),我们就可以根据需要开发出各种各样的组件,组件像一个U盘,随时可以插在容器上工作。最重要的是,它能通过容器访问到容器中其它组件,容器这时候更像一个总线(Bus),如下图7-7:

图7-7 容器充当总线功能

如上图7-7所示,在总线不变的情况下,意味着通讯协议不变(容器不变,那么提供服务的规则也不变),各种扩展组件(Component派生类)均可以加入这条总线。

7.2.3 窗体设计器

首先我们应该搞清楚一件事情,只有源代码经过编译器编译之后才会生成可执行文件。在这个过程中,所有其它的开发工具都只是起到一个辅助作用,无论我们的源代码是怎样来的,是用记事本手动编写还是通过某些工具自动生成,最后本质上都一样,编译器只认结果,并不关心源代码是怎样产生的。现在流行的一些集成开发环境(IDE)中基本都会带有可视化设计界面,通常称作"窗体设计器",该窗体设计器的功能就是帮助我们自动生成源代码。没错,我们点点鼠标、拖拖控件,窗体设计器就会为我们生成对应的源程序,窗体设计器的作用如下图7-8:

图7-8 窗体设计器在开发中的作用

如上图7-8所示,窗体设计器最终也是为了生成源代码,本质上跟代码编辑器的作用一样。

其次,我们同时也要清楚,我们向窗体设计器中添加的各个组件(比如Timer、BackgroundWorker、ImageList等等)以及各种控件(比如Button、Label以及Form等),它们到底是个什么东西?我们向窗体设计器中拖进去的Button控件和程序跑起来之后显示在窗体上的Button按钮是一样的吗?答案是肯定的,也就是说,我们向窗体设计器中拖进去的控件(组件)都是内存中存在的对象实例。如果我们使用Spy++等工具,可以找到窗体设计中控件的句柄,同时我们还可以通过属性窗体(PropertyGrid控件)修改窗体设计器中控件(组件)的属性,这些修改在设计器中立即就会产生效果,这些都足以说明,在我们向窗体设计器中拖动控件的时候,是会执行类似"new Button();"这样的代码,在内存中实例化一个组件实例。那么,是所有的类型均可以拖放到窗体设计器中吗?答案是否定的,不难发现,能够被窗体设计器设计的像Imagelist、Timer等以及所有的控件都属于"组件",它们都派生自System.ComponentModel.Component类型。我们好像发现了什么,没错,前面说到过,容器(System.ComponentModel.Container或其派生类)只能包含组件,并且能够为组件提供服务,组件之间也能够相互通信,那么,我们是不是也可以把窗体设计器当作这样的一种容器呢?事实证明,窗体设计器确实是我们前面提到过了容器,我们在使用窗体设计器时,各个组件之间确实是可以相互通信的,当我们往窗体设计器中拖放一个ImageList组件时,设计器中所有其它包含有ImageList类型属性的控件都会知道这个拖放进来的ImageList组件,因此在我们编辑其它控件的ImageList类型属性时,就会有选项供选择。我们不仅仅知道当前可以用来赋值的ImageList组件,还可以看到ImageList组件中的Image列表,从而设置控件的ImageList属性值。这一过程的主要贡献在于窗体设计器,它能够为组件与组件之间建立关联。

微软的Visual Studio开发环境中的窗体设计器很好的应用了本节之前讲到"容器-组件-服务模型",这也印证了本章开始之前的一个疑问:.NET编程中为什么要提出组件的概念?由于窗体设计器中只能容纳组件,所以如果我们想开发出来一个可以被窗体设计器可视化设计的类型,那么我们必须让该类型正确实现IComponent接口(或派生自Component类)。

既然组件能够请求窗体设计器的服务,那么我们在编写组件时,怎样去取得这些服务呢?又怎样去使用这些服务呢?注意在一般开发中,我们并不需要在组件中编写与窗体设计器交互的代码,原因有两个:

(1)这些事情是由组件开发者来做的,而我们大多数人只是充当组件的使用者;

(2)与窗体设计器交互的代码一般非常复杂,建议没有特殊需要,不要在组件中编写与窗体设计器(组件所在容器)交互的代码,因为这会影响IDE的稳定性。

下面示例代码演示了在组件中怎样请求窗体设计器的服务:

 1 //Code 7-7
 2 //…include other namespace
 3 using System.ComponentModel;
 4 using System.ComponentModel.Design;
 5 class MyLabel:Label
 6 {
 7     //
 8     protected override void OnHandleCreated(EventArgs e)
 9    {
10           ISelectionService iss = GetService(typeof(ISelectionService)) as ISelectionService;
11           if(iss != null) //NO.1
12           {
13                  iss.SelectionChanged += new EventHandler(iss_SelectionChanged); //NO.2
14           }
15            base.OnHandleCreated(e);
16     }
17     void iss_SelectionChanged(object sender, EventArgs e)
18    {
19           ISelectionService iss = GetService(typeof(ISelectionService)) as ISelectionService;
20           if(iss != null) //NO.3
21           {
22                  Text = "窗体设计器中当前选中了" + iss.SelectionCount + "个组件\n"+ "第一个组件是" + (iss.PrimarySelection as Component).Site.Name; //NO.4
23          }
24     }
25 }

如上代码Code 7-7所示,在Label的派生类MyLabel中,我们重写了OnHandleCreated虚方法,在该虚方法中向窗体设计器(容器)请求"组件选择有关"的服务,窗体设计器返回一个ISelectionService的服务接口。注意我们需要先判断返回来的服务接口是否为null(NO.1处),如果为null,说明当前组件不在任何容器中或者容器不提供该服务;如果不为null,我们使用该服务接口注册窗体设计器的SelectionChanged事件(NO.2处),该事件会在窗体设计器选择发生变化后被激发。之后在SelectionChanged的事件处理程序中,我们还要向窗体设计器请求服务,请求完成之后依旧要判断返回来的服务接口是否为null(NO.3处),如果不为null,就使用返回来的ISelectionService接口将窗体设计器中选择组件的个数显示在Label.Text中(NO.4处)。示例代码最终的效果是:我们向窗体设计器拖放一个MyLabel控件,之后任何时候,只要窗体设计器中选择组件发生了变化,MyLabel.Text属性就会显示当前选中组件的个数,而这一切都发生在窗体设计器中,也就是程序的开发阶段,下图7-9为效果图:

图7-9 组件与窗体设计器的交互

如上图7-9所示,图中窗体最上面的控件是一个MyLabel控件,我们在窗体设计器中选中了4个组件(注意窗体本身也算一个),MyLabel的Text属性会根据窗体设计器中选择组件的改变而变化。换句话说,组件与窗体设计器之间进行了交互,同理组件与组件之间通过窗体设计器提供的某些服务也可以进行交互。

注:Visual Studio中窗体设计器默认提供许多与设计有关的服务,服务接口大部分定义在System.ComponentModel.Design命名空间中。只要我们知道服务规则,我们就可以在组件中请求这些服务,组件就可以与窗体设计器进行交互。容器永远是服务规则的制定者,组件必须遵守这些规则方可正常工作。

前面说到过,程序开发过程中,源代码才是我们最终所需要的东西。窗体设计器一关闭,设计器中所有的组件全部从内存中销毁,我们用鼠标键盘操作窗体设计器的时候,我们对设计器的任何一个操作,设计器都会帮助我们生成源代码,我们通过属性窗体编辑一个组件的属性,组件能马上产生效果的同时(比如背景色),窗体设计器也能为该操作生成源代码。在Winform中,这些代码集中在InitializeComponent方法中。只要我们有了源代码文件,窗体设计器中的"假象"就可以随时消失,窗体设计器中的组件和源代码中的组件不是同一个东西,下图7-10显示了窗体设计器中的组件和设计器为我们生成的代码:

图7-10 窗体设计器中的组件与生成的源代码

如上图7-10所示,图中左边显示我们拖放到设计器中的一个Button控件,在这个过程中,窗体设计器除了会实例化一个Button控件(图中左边Form2中),还会为我们生成图中右边的代码。我们看到,生成的代码会实例化一个Button对象,然后将其加入到Form2.Controls的子控件集合中。图中左边设计器中的button1实例对象与图中右边生成代码中的button1变量不是同一个东西。

注意:组件不是一定要存在于容器中,它可以和其它对象一样,单独存在。换句话说,组件可以不存在于窗体设计器中,比如程序运行之后,程序中任何组件都不属于窗体设计器,这将是下一节要讨论的话题。

7.3 设计时(Design-Time)与运行时(Run-Time)

7.3.1 组件的设计时与运行时

7.2节中讲窗体设计器时已经说过,窗体设计器中的各个组件都是真实存在的实例对象。我们向窗体设计器中拖放一个Button控件时,必然会调用Button类的构造方法实例化一个Button对象,Button对象必然会激发它的HandleCreated事件等等,也就是说,无论组件在哪里,它都是以对象的形式真实存在的。

我们把组件处于窗体设计器中的状态称为"设计时",把组件正常运行时的状态称为"运行时"。设计时的组件和运行时的组件都是以对象的形式存在,因此有很多的相同点,比如都会调用构造方法,都会激发相应事件等等。除了这些相同点以外,还有一些不同点,由于处于设计时的组件存在容器中,因此它可以获取窗体设计器中提供的服务,可以执行一些与窗体设计器交互的代码,而处于运行时状态的组件不存在窗体设计器中,因此它不可以执行与窗体设计器交互有关的代码,见下图7-11:

 

图7-11 设计时组件与运行时组件

如上图7-11所示,圆形代表组件,矩形代表窗体。图中左边显示处于设计器中的组件,这些组件可以与窗体设计器交互,图中右边显示处于运行状态中的组件,它们不能再执行与窗体设计器交互有关的代码,因为它们根本就不在窗体设计器之中。

7.3.2 区分组件的当前状态

任何组件都有两种状态:设计时和运行时。由于在设计时能执行的代码,在运行时可能执行失败,相反,有些代码可能只需要在运行时执行,比如连接数据库的代码,我们在设计时完全没必要让窗体设计器中的一个组件去连接数据库。因此,我们编写组件代码之前,一定要先搞清楚这些代码是在什么状态下去执行。我们可以先检查一下组件的状态,如果组件处于设计时,那么执行代码A,否则不执行代码A。有两种方式去判断组件的当前状态:

(1)组件的DesignMode属性。每个组件都有一个Bool类型的DesignMode属性,正如它的字面意思,如果该属性为true,那么代表组件当前处于设计时状态;否则组件处于运行时状态。我们将任何组件拖进窗体设计器后,设计器就会将组件的DesignMode设置为true(该属性默认为false)。假如现在要开发一个控件,它具有显示自己版本信息的功能,但是仅仅在开发阶段显示,用于提醒开发者当前所用组件的版本,而当整个程序运行之后,版本信息不再显示,那么该控件代码可以这样写:

 1 //Code 7-8
 2 class MyControl:Control
 3 {
 4     public MyControl()
 5     {
 6         //
 7     }
 8     protected override void OnPaint(PaintEventArgs e)
 9     {
10         e.Graphics.DrawRectangle(Pens.Blue,new Rectangle(0,0,Width-2,Height-2)); //NO.1
11         //
12         if(DesignMode) //NO.2
13         {
14             string v = "v2.30.109.1302"; //read from somewhere
15             using(Font f = new Font("arial",10))
16                 e.Graphics.DrawString(v,f,Brushes.Blue,new PointF(5,5));
17         }
18     }
19 }

如上代码Code 7-8所示,在MyControl控件的OnPaint方法中,我们先绘制了一个蓝色边框(NO.1),该行代码无论组件处在什么状态都会执行,因此我们可以看到拖到窗体设计器中的MyControl控件有一个蓝色边框,程序运行之后,窗体中的MyControl控件也有一个蓝色边框。接下来需要显示版本信息,因为只有当组件处于设计时状态,才会显示版本信息,因此我们需要先判断组件的当前状态(NO.2处),如果DesignMode属性为true,那么说明组件处在窗体设计器中,需要显示版本信息;否则,说明组件处在运行时状态,不显示版本信息。任何一个使用MyControl控件的开发者,都会在窗体设计器中看到当前MyControl控件的版本信息,而当程序运行后,该版本信息不再显示。

(2)随便请求一个服务,看返回来的服务接口是否为null。前面提到过,当一个组件不属于任何一个容器时,那么它通过GetService方法请求的服务肯定返回为null。因此,我们可以请求一个窗体设计器能够提供的服务(比如前面用到过的ISelectService服务),看请求的返回值是否为null,如果为null,说明当前组件处于运行时;否则,当前组件处于窗体设计器中。前面的MyControl示例代码可以改为:

 1 //Code 7-9
 2 class MyControl:Control
 3 {
 4     public MyControl()
 5     {
 6         //
 7     }
 8     protected override void OnPaint(PaintEventArgs e)
 9     {
10         e.Graphics.DrawRectangle(Pens.Blue,new Rectangle(0,0,Width-2,Height-2)); //NO.1
11         //
12         ISelectionService iss = GetService(typeof(ISelectionService)) as ISelectionService;
13         if(iss != null) //NO.2
14         {
15             string v = "v2.30.109.1302"; //read from somewhere
16             using(Font f = new Font("arial",10))
17                 e.Graphics.DrawString(v,f,Brushes.Blue,new PointF(5,5));
18         }
19     }
20 }

如上代码Code 7-9所示,我们先请求一个ISelectionService服务,再判断它的返回值是否为null(NO.2处),如果不为null,说明组件当前处于窗体设计器中;否则,组件处于运行时状态。

注:(1)(2)方法均不适合嵌套组件,因为窗体设计器只会将最外层组件的DesignMode属性值设置为true,如果这个组件内部还包含其它子组件,那么这些子组件的DesignMode属性还是原来的默认值false,因此(1)对嵌套组件中的子组件无效。我们拖放一个嵌套组件到窗体设计器中,只有最外层组件加入到了窗体设计器中,所以只有最外层组件能够通过GetService方法请求窗体设计器的服务,内部的子组件由于没有加入到容器,因此GetSeivice方法返回null,因此(2)对嵌套组件中的子组件也无效。有一种可以解决嵌套组件中无法判断其子组件状态的方法,那就是通过Process类来检查当前进程的名称,看是否包含"devenv"这个字符串,如果是,那么说明组件当前处于Visual Studio开发环境中(即组件处于设计时),if(Process.GetCurrentProcess().ProcessName.Contains("devenv"))为假,说明组件处于运行时。这种方法也有一个弊端,很明显,如果我们使用的不是Visual Studio开发环境(也就是进程名不包含devenv),或者我们自己的程序进程名称就包含devenv怎么办呢?

7.3.3 组件状态的应用

作为一名普通的开发人员,几乎不需要接触到组件状态这些概念,大部分开发人员只是使用组件,也就只需要编写好组件在运行时需要执行的代码即可,如果你是一个组件开发人员,那么你就可能需要与窗体设计器打交道,控制组件与设计器之间的交互,能让组件的使用者更加方便的去使用组件。

在开发一些需要授权的组件时,就可以用到组件的两种状态,这些需要授权的组件收费对象一般是开发者,因此,在开发者使用这些组件开发系统的时候(处于开发阶段),就应该有授权入口,而当程序运行之后,就不应该出现授权的界面,这时候就可以根据组件的当前状态来判断是否需要显示授权入口。

如果我们编写了与窗体设计器交互的代码,那么一定要谨慎小心,因为访问窗体设计器的代码很容易就会造成开发环境崩溃。

7.4 控件

7.4.1 控件基类

本章7.1.3小节中已经介绍了Windows Forms中的控件分类及其派生关系。控件作为组件中的一个分支,具有可视化显示的功能,言下之意就是控件内部具备Windows消息处理的功能(详见第八章)。System.Windows.Forms.Control类是所有控件的基类,它内部已经提供了所有控件必须具备的基础结构,比如窗口句柄、窗口过程以及基础的Windows消息路由。

Control类的默认外观显示为一个矩形,Windows Forms框架中其它所有控件均派生自Control类。

7.4.2 用户自定义控件

Windows Forms中包含有非常强大也非常完善的控件,比如基础控件Button、CheckBox等,容器布局控件Panel、TabControl等,菜单控件MenuStrip以及数据显示控件DataGridView控件等。这些控件在一般开发中可以满足我们的需要,但是有些时候对于一些特殊的功能需求,使用系统自带的控件远远不够,因此,我们需要自己开发满足功能要求的控件。有三种方式开发新的控件:

(1)复合控件(Composite Control);

这种方式很简单,就是将已有控件组合在一起,形成一个整体,将现有控件功能集中起来。我们平时开发的"用户控件",从UserControl类派生而来,将许许多多现有控件集中到一起,这种控件就属于复合控件,见下面代码:

1 //Code 7-10
2 class MyUserControl:UserControl
3 {
4     public MyUserControl()
5     {
6         InitializeComponent(); //NO.1
7     }
8     //
9 }

如上代码Code 7-10所示,在MyUserControl类中的InitializeComponent方法中(NO.1处),我们可以将现有的控件组合在一起,形成一个整体。注意InitializeComponent方法中的代码一般由窗体设计器生成,我们每次通过窗体设计器向MyUserControl中添加一个控件,在InitializeComponent方法中都会生成类似"this.Controls.Add(…)"这样的代码,意思就是将新添加的控件加入MyUserControl.Controls集合中。

(2)扩展控件(Extended Control);

从现有控件派生出一个新控件。如果现有控件基本已经满足需求,只是需要稍微增加一些小功能或者稍微修改现有功能,那么我们可以将已有控件作为基类,派生出一个新控件,在派生类中编写增加功能或者修改功能的代码,下面代码演示了一个从Button类派生的MyButton类,改变了原来Button的显示外观,在原来显示外观的基础上绘制了一个蓝色矩形:

1 //Code 7-11
2 class MyButton : Button
3 {
4     protected override void OnPaint(PaintEventArgs pevent)
5     {
6         base.OnPaint(pevent);
7         pevent.Graphics.DrawRectangle(Pens.Blue, new Rectangle(1, 1, Width - 3, Height - 3)); //NO.1
8     }
9 }

如上代码Code 7-11所示,我们在MyButton类中重写了OnPaint虚方法,每次控件需要重绘的时候,我们在控件界面绘制一个蓝色矩形(NO.1处),除了显示外观的差别,MyButton类与Button类具有完全一样的功能。本示例代码只是在Button类的基础上进行一个非常简单的修改。

(3)自定义控件(Custom Control)。

以Control为基类,直接派生出一个新控件。这种方式对开发者的技术能力要求较高,因为Control类中只是包含了所有控件应该具备的基础结构,它并不提供控件的特定功能以及显示界面,因此无论从功能的实现还是界面的显示均要求开发者自己去处理,比如控件的外观显示,开发者必须熟悉GDI和重写OnPaint虚方法,而对于控件的一些功能实现,开发者必须熟悉Windows消息和重写控件的窗口过程WndProc这个虚方法等。正是因为这种从底层都需要开发者自己去实现的做法,才让我们更灵活地控制控件的行为和外观,下面示例代码演示如何从Control类派生出新控件:

 1 //Code 7-12
 2 class MyControl:Control
 3 {
 4     public event EventHandler Event1; //NO.1
 5     public event EventHandler Event2; //NO.2
 6     public MyControl()
 7     {
 8         //
 9     }
10     protected override void OnPaint(PaintEventArgs e)
11     {
12         base.OnPaint(e);
13         // paint the surface of MyControl
14         e.Graphics.DrawRectangle(Pens.Blue, new Rectangle(1, 1, Width - 3, Height - 3)); //NO.3
15     }
16     protected override void WndProc(ref Message m)
17     {
18         if(m.Msg == ?) //NO.4
19         {
20             //raise Event1 with Message's arguments
21             return;
22         }
23         if(m.Msg == ?) //NO.5
24         {
25             //raise Event2 with Message's arguments
26             return;
27         }
28         //
29         base.WndProc(ref m);
30     }
31 }

如上代码Code 7-12所示,我们在MyControl类中重写了OnPaint虚方法,负责绘制控件的外观显示(NO.3处),还重写了WndProc虚方法,拦截Windows消息(NO.4和NO.5处),将消息参数转换成事件参数,最后激发相应事件(这个过程参考第八章),注意重写的虚方法中不要忘记调用基类的虚方法。

注:无论是复合控件、扩展控件还是自定义控件,我们均可以重写控件的窗口过程:WndProc虚方法,从根源上接触到Windows消息,这个做法并不是自定义控件的专利。

7.5 本章回顾

本章讲到的"组件"是指.NET编程中的组件,特指直接或间接实现了IComponent接口的类型,它跟我们通常意义上谈到的组件有很大的区别。组件在.NET编程中起到了重要作用,所有可在IDE中可视化设计的类型必须是组件,包括我们直接从工具箱中拖到窗体设计器中的UI相关组件(如Button按钮)和其它功能组件(如backgroundWorker)。本章还介绍了"容器-组件-服务"模型,介绍了窗体设计器与组件之间的关系,以及为什么一个组件可以放在设计器中进行可视化设计,之后还介绍了组件的两种状态:设计时(Design-Time)和运行时(Run-Time),组件的这两种状态对组件的行为表现起到了重要作用。

7.6 本章思考

1..NET编程代码中组件的定义是?

A:特指实现(直接或间接)了System.ComponentModel.IComponent接口的类型,只有组件才可以在窗体设计器中进行可视化设计。

2."容器-组件-服务"模型中容器的含义是?

A:特指实现(直接或间接)了System.ComponentModel.IContainer接口的类型,不包括ArrayList、Queue、Stack以及Array等物理容器。

3.组件有哪两种状态?怎样区分组件的当前状态?

A:组件有"设计时(Design-Time)"与"运行时(Run-Time)"两种状态,可以通过组件的DesignMode属性去判断它的当前状态,一般情况下,如果该属性为true,说明当前组件处于设计时,否则处于运行时(参见本章7.3.2小节)。在窗体设计器中创建的组件的状态即为设计时,程序运行后组件的状态即为运行时。

posted @ 2015-06-12 09:16  周见智  阅读(3553)  评论(1编辑  收藏  举报