T.T Cinq

FF 02 30 F7 01 01 02 FE FD FF

ASP.NET可交互式位图窗体设计(1)

请您检查作为 Microsoft ASP.NET 应用程序运行的示例(带有源代码)。或者仅在新窗口中查看源代码。请注意,程序员的注释在示例程序中是英文的,而在本文中被翻译成中文,以便更好地解释该程序。另外,使用了此新功能后(在此感谢 MSDN Web Publishing Team!),您可以将两个窗口都放在屏幕上,这样便可以方便地查看相应代码。
  
    简介
    在本文,我们将通过一个灵活的绘图应用程序提供一个有关继承、abstract (MustInherit) 基类和接口的更为完整的示例。这不是一个控制台应用程序;由于其图形化的特征,更适合作为一个 Microsoft Windows 窗体应用程序。(这就给了我们一个了解 Windows 窗体的机会。)
  
    该 ASP.NET 版本将演示如何在 Web 页上使用自定义绘制的位图 -- 这在大多数 Web 编程系统中是非常难以实现的,但使用 ASP.NET 则很简单。Dr. GUI 相信您会喜欢这一点。而且您还可以运行该应用程序。
  
    经典的多态示例
  
    在教授编程时,有一些常用的、非常标准的示例程序。而我最初曾发誓不使用这些示例:我不会使用一个字符串类作为示例,也不会使用复杂的数字或绘图应用程序。毕竟,这样做就不是原创了。
  
    然而随着事情的发展,使用这些示例显得很有必要(不仅仅是因为懒惰):这些示例非常丰富,易于解释和理解,并且可以非常清晰地揭示核心概念。
  
    以下是该程序 Windows 窗体版本的屏幕快照:
  
  图 1:经典多态示例的 Windows 窗体版本
  
    以下是 ASP.NET 版本在浏览器中的显示:
  
  图 2:经典多态示例的 ASP.NET 版本
  
    您可以运行上面显示的 ASP.NET 版本。
我们的任务
    这个程序的基本思想如下:我们有一个 abstract 基类(在 Microsoft Visual Basic? 中是 MustInherit),其中包含公共数据(如边框)和一套虚拟方法,虚拟方法多数是抽象的(在 Visual Basic 中是 MustOverride),例如 Draw。请注意,Draw 的多态性很重要,因为每个可绘制对象类型(如点、线、矩形、圆等)都是用完全不同的代码绘制的。
  
    虽然方法可以是多态的,但数据不能。因此,我们只将确实应用于所有可能的可绘制对象的数据放在程序中 -- 在本例中,放置了一个边框和颜色(在其中绘制对象的线)。
  
    与特定类型的可绘制对象相关的数据(例如圆的中心和半径、矩形相对点的坐标,或者一条线的端点)都应该在与该类型的可绘制对象对应的特定类(从抽象基类中派生)中声明。请注意,可以使用二次派生合并相似的对象。例如,可以从椭圆中派生出圆,因为所有的圆都是椭圆。与此类似,也可以从矩形中派生出方形,因为所有的方形都是矩形(也都是四边形、多边形)。所选择的派生树会反映类之间的关系,以及常用的预期使用模式,这样您经常执行的操作便会非常快速、方便。
  
    以下是我们的类派生图:
  
  
  
  图 3:类派生图
  
    因为构造函数(在 Visual Basic 中为 New)存在的主要原因是用于初始化数据,因此构造函数不是(实际上也不能是)多态的。这意味着初始创建操作不能是多态的,因为数据要求随类型的不同而不同。但是,一个好的设计在对象创建后,可在之后的使用中将对象作为多态处理,这里我们就是这样做的。
  
    让我们看看这个类集中包含什么,从根抽象基类开始:
  
    抽象 (MustInherit) 基类
    以下是 C# 中抽象基类的代码。单击此处在新窗口中查看全部源文件。
  
  
  
    C#
  
  public abstract class DShape {
  public abstract void Draw(Graphics g);
  protected Rectangle bounding;
  protected Color penColor; // 还应具有属性
  // 还应具有移动、调整大小等方法。
  }
  
    以下是等同的 Visual Basic .NET 代码。单击此处在新窗口中查看全部源文件。
  
    Visual Basic .NET
  
  Public MustInherit Class DShape
  Public MustOverride Sub Draw(ByVal g As Graphics)
  Protected bounding As Rectangle
  Protected penColor As Color ' 还应具有属性
  ' 还应具有移动、调整大小等方法。
  End Class
  
    语法虽然不同,但很明显这是相同的类。
  
    请注意,Draw 方法被暗示为 virtual (Overridable),因为它被声明为 abstract (MustOverride)。还要注意在这个类中我们并没有提供一个实现。因为我们尚不知道在这个类中执行的对象,因此不可能写出绘图代码。
  
    包含哪些数据?
    另请注意,这里并没有很多数据 -- 但我们已经为这样一个抽象类声明了所有数据。
  
    每一个可绘制对象(无论其形状如何)都有一个边框 -- 即可以完全包含该对象的最小可能矩形。边框用于绘制点(作为很小的矩形)、长方形和圆 -- 并且对于其他形状,可以作为第一个用于点击或碰撞测试的快速估计。
  
    适用于所有对象的其他共同点并没有很多;中心对于某些对象有用,例如圆和长方形,对于其他对象(如三角形)则没有意义。并且通常都是使用角来表示矩形,而不是使用中心。但您不能使用角来指定圆,因为圆没有角。Dr. GUI 确信您已经看到了为一个普通可绘制对象指定其他数据的困难之处。
  
    每个可绘制对象还有一个与绘制它的线相关联的颜色,这里我们也做了声明。
  
    某些派生类
    如上所述,我们不能真正创建一个抽象基类类型的对象,虽然我们可以将从抽象基类(或任何基类)中派生的任何对象作为基类对象处理。
  
    所以,为创建一个绘图对象,我们必须从抽象基类中派生一个新类 -- 并确保覆盖所有 abstract/MustOverride 方法。
  
    在本例中我们将使用 DHollowCircle 类。DHollowRectangle 类和 DPoint 类非常相似。
  
    以下是 C# 中的 DHollowCircle。单击此处在新窗口中查看其他类。
  
  
    C#
  public class DHollowCircle : DShape
  {
  public DHollowCircle(Point p, int radius, Color penColor) {
  p.Offset(-radius, -radius); // 需要转换到左上角
  int diameter = radius * 2;
  bounding = new Rectangle(p, new Size(diameter, diameter));
  this.penColor = penColor;
  }
  
  public override void Draw(Graphics g) {
  using (Pen p = new Pen(penColor)) {
  g.DrawEllipse(p, bounding);
  }
  }
  }
  
  
  以下是等同的 Visual Basic .NET 类。单击此处在新窗口中查看其他类。
  
  
  Visual Basic .NET
  Public Class DHollowCircle
  Inherits DShape
  
  Public Sub New(ByVal p As Point, ByVal radius As Integer, _
  ByVal penColor As Color)
  p.Offset(-radius, -radius) ' 需要转换到左上角
  Dim diameter As Integer = radius * 2
  bounding = New Rectangle(p, New Size(diameter, diameter))
  Me.penColor = penColor
  End Sub
  
  Public Overrides Sub Draw(ByVal g As Graphics)
  Dim p = New Pen(penColor)
  Try
  g.DrawEllipse(p, bounding)
  Finally
  p.Dispose()
  End Try
  End Sub
  End Class
  
  
    请注意,我们没有为这个类声明其他数据 -- 它给出的边框和笔已经足够了。(对于点和矩形是这样,但对于三角形和其他多边形就不够了。)我们的应用程序不需要在设置圆后知道圆的中心或半径,因此将它们忽略掉。(如果需要中心和半径,我们可以存储这些数据,或者根据边框计算得出。)
  
    但我们确实需要边框,因为它是用于绘制圆的 Graphics.DrawEllipse 方法的一个参数。因此我们根据在构造函数中传递的中心点和半径计算边框。
  
    下面我们深入了解每一个方法。

构造函数
    构造函数传递三个参数:包含圆的中心坐标的点、圆的半径以及一个 System.Drawing.Color 结构(包含用于绘制圆轮廓的颜色)。
  
    然后我们根据中心和半径计算边框,并将笔颜色项设置为我们传递的颜色对象。
  
    绘图代码
    Draw 方法重载实际上非常简单:它根据我们保存在构造函数中的颜色对象创建一个笔对象,然后使用该笔,调用 Graphics.DrawEllipse 方法绘制圆,同时传递了我们早先创建的边框。
  
    图形、笔和画笔
    这里我们需要解释一下 Graphics、Pen 和 Brush 对象。(在开始填充我们的可填充对象时,就会看到 Brush 对象。)
  
    Graphics 对象代表一个与某个真实绘图空间相关联的虚拟化的绘图空间。虚拟化是指,通过在 Graphics 对象上绘图,我们可以使用相同的 Graphics 方法在与该对象相关联的任何类型的实际表面上绘图。对于那些习惯于使用 MFC 或 Microsoft Windows? SDK 编程的用户,Graphics 对象相当于 Windows 中称为“设备上下文”(或 DC)的 .NET 版本。
  
    在此 Windows 窗体应用程序中,传递到 Draw 方法的 Graphics 对象将与屏幕上的一个窗口相关联 -- 这里是 PictureBox。在我们的 Microsoft ASP.NET 应用程序中使用这一代码时,传递给 Draw 方法的 Graphics 对象将与一个位图图像相关联。它也可以和打印机或其他设备相关联。
  
    这个方案的优点是我们可以使用相同的绘图代码在不同的表面上绘图。在我们的绘图代码中,我们不需要知道任何有关屏幕、位图、打印机等等之间的不同 -- .NET Framework(以及底层的操作系统)可以为我们处理所有细节。
  
    利用相同的标记,笔和画笔成为虚拟化的绘图工具。笔代表线条属性 -- 颜色、宽度、样式,甚至可以是用来绘制线的位图。画笔代表一个填充区域的属性 -- 颜色、样式,甚至可以是用来填充区域的位图。
  
    在使用 Using 后清除(或至少 Dispose)
    Graphics、Pen 和 Brush 对象都与相似类型的 Windows 对象相关联。这些 Windows 对象分配在操作系统的内存中 -- 这些内存尚未被 .NET 运行时管理。长时间将这些对象驻留在内存中会导致性能问题,并且在 Microsoft Windows 98 下,当图形堆填满时会导致绘图问题。因此,我们应尽快释放这些 Windows 对象。
  
    当相应的 .NET Framework 对象完成操作并回收内存后,.NET 运行时会自动释放 Windows 对象。但回收内存的时间会很长 -- 如果我们不迅速释放这些对象,所有不幸的事情(包括填满 Windows 堆)都可能发生。在该应用程序的 ASP.NET 版本中,由于很多用户在同一台服务器上访问该应用程序,所以这种现象会更加严重。
  
    因为这些对象与未管理的资源相关联,所以它们实现 IDisposable 接口。该接口有一个方法,即 Dispose,它将 .NET Framework 对象从 Windows 对象中分离出来,并释放 Windows 对象,从而使计算机处于良好的状态。
  
    这时您只需完成一项任务:确保在使用完该对象后,调用 Dispose 方法。
  
    Visual Basic .NET 代码中显示了这一内容的经典形式:首先创建对象,然后在一个 Try 块中使用该对象,最后在 Finally 块中清理该对象。Try/Finally 能够确保即使出现异常也会清理对象。(在本例中,我们调用的绘图方法可能不会引发异常,所以可能并不需要 Try/Finally 块。但掌握这些技巧很有用,因此 Dr. GUI 也希望向您演示这一正确方法。)
  
    这种形式很常见,因此 C# 为其提供了一个私有语句:using。C# 代码中的 using 语句等同于 Visual Basic .NET 代码中的声明和 Try/Finally -- 但更为简洁、方便,并减少了发生错误的可能性。(Dr. GUI 不清楚为什么 Visual Basic .NET 不包含一些诸如 using 的语句。)
  
    接口
    有些(但不是全部)可绘制对象可以被填充。某些对象(如点和线)不能被填充,因为它们不是封闭的区域,而矩形和圆等对象可以是中空的,或者被填充。
  
    另一方面,就象将所有可绘制对象作为多态处理一样,我们也可以将所有可填充对象作为多态处理,这会很方便。例如,如同我们将所有可绘制对象放到一个集合中,通过遍历集合并在每个对象上调用 Draw 来绘制对象一样,我们可以将所有可填充对象放到一个集合中,而不考虑这些可填充对象的实际类型。因此,我们使用某种机制(如继承)来获得真正的多态。
  
    因为不是所有的可绘制对象都可以被填充,因此不能将 Fill 方法的声明放在抽象基类中。.NET Framework 不允许类的多重继承,所以也不能将其放在另一个抽象基类中。并且如果我们从不是非可填充类的其他基类中派生出可填充对象,则不能将所有可绘制对象作为多态处理。
  
    但 .NET Framework 支持接口 -- 并提供了一个可实现任意数量的接口的类。接口不具有任何实现 -- 没有代码,也没有任何数据。因此,实现接口的类必须提供所有内容。
  
    接口所能包含的只有声明。以下是我们在 C# 中的接口 IFillable。单击此处在新窗口中查看全部源文件。
  
  
  
    C#
  public interface IFillable {
  void Fill(Graphics g);
  Color FillBrushColor { get; set; }
  }
  
  
    以下是等同的 Visual Basic .NET 代码。单击此处在新窗口中查看全部源文件。
  
    Visual Basic .NET
  
  Public Interface IFillable
  Sub Fill(ByVal g As Graphics)
  Property FillBrushColor() As Color
  End Interface
  
    我们不需要声明方法或者 virtual/Overridable 或 abstract/MustOverride 属性以及任何其他项,因为接口中的所有方法和属性都自动设置为公开的和 abstract/MustOverride。
  
    使用一个属性:不能在接口中包含数据
    请注意,虽然我们不能在接口中声明字段,但可以声明一个属性,因为属性实际上是作为方法实现的。
  
    但这样做会给接口的实现者带来负担,下面就会看到。实现者必须实现 get 和 set 方法,以及实现该属性所必需的任何数据。如果实现非常复杂,则可以编写一个 helper 类以封装某些部分。在本文稍后我们将就一个略微不同的上下文环境显示如何使用 helper 类。
  
    实现接口
    我们已经定义了接口,现在可以在类中实现它了。请注意,我们必须提供所实现接口的完整实现:不能只从中选取一部分。
  
    下面我们看看 C# 中 DFilledRectangle 类的代码。
  
  
    C#
  public class DFilledCircle : DHollowCircle, IFillable
  {
  public DFilledCircle(Point center, int radius, Color penColor,
  Color brushColor) : base(center, radius, penColor) {
  this.brushColor = brushColor;
  }
  
  public void Fill(Graphics g) {
  using (Brush b = new SolidBrush(brushColor)) {
  g.FillEllipse(b, bounding);
  }
  }
  protected Color brushColor;
  public Color FillBrushColor {
  get {
  return brushColor;
  }
  set {
  brushColor = value;
  }
  }
  public override void Draw(Graphics g) {
  Fill(g);
  base.Draw(g);
  }
  }
  
  
    以下是 Visual Basic .NET 中 DFilledRectangle 类的代码。
  
  
    Visual Basic .NET
  Public Class DFilledRectangle
  Inherits DHollowRectangle
  Implements IFillable
  Public Sub New(ByVal rect As Rectangle, _
  ByVal penColor As Color, ByVal brushColor As Color)
  MyBase.New(rect, penColor)
  Me.brushColor = brushColor
  End Sub
  Public Sub Fill(ByVal g As Graphics) Implements IFillable.Fill
  Dim b = New SolidBrush(FillBrushColor)
  Try
  g.FillRectangle(b, bounding)
  Finally
  b.dispose()
  End Try
  End Sub
  Protected brushColor As Color
  Public Property FillBrushColor() As Color _
  Implements IFillable.FillBrushColor
  Get
  Return brushColor
  End Get
  Set(ByVal Value As Color)
  brushColor = Value
  End Set
  End Property
  Public Overrides Sub Draw(ByVal g As Graphics)
  Dim p = New Pen(penColor)
  Try
  Fill(g)
  MyBase.Draw(g)
  Finally
  p.Dispose()
  End Try
  End Sub
  End Class
  
  
    以下是有关这些类的注意事项。
  
    从 HollowRectangle 中派生
    我们从这个类的空心版本中派生出填充类。这个类中的多数内容都发生了改变:Draw 方法和构造函数都是新的(但两者都调用基类的版本),并且为 IFillable 接口的 Fill 方法以及 FillBrushColor 属性添加了实现。
  
    需要新构造函数的原因是我们在这个类中包含了需要初始化的其他数据,即填充画笔。(您可以回顾我们前面讨论的画笔。)请注意此构造函数是如何调用基类构造函数的:在 C# 中,该调用被内置到声明 (: base(center, radius, penColor)) 中;在 Visual Basic .NET 中,我们将它明确放在 New 方法(即构造函数)的第一行 (MyBase.New(rect, penColor))。
  
    因为我们已经向基类构造函数中传递了三个参数中的两个,现在只需初始化最后的字段即可。

绘图如何改变
    您会注意到,Draw 方法与基类基本相同 -- 主要差别在于它调用了 Fill 方法,因为要完成绘制一个填充对象,所以需要对其进行填充。我们没有为绘制轮廓重写代码,而是再次调用了基类的方法:Visual Basic .NET 中的 MyBase.Draw(g) 或 C# 中的 base.Draw(g);。
  
    因为我们正在指派用于绘制轮廓的笔,因此需要使用 using 或 Try/Finally 和 Dispose 以确保迅速释放 Windows 笔对象。(同样,如果非常确信所调用的方法不会引发异常,可以在完成笔的处理后,跳过异常处理,而只调用 Dispose。但我们必须调用 Dispose,无论是直接调用,还是通过 using 语句。
  
    实现 Fill 方法
    Fill 方法很简单:指派一个画笔,然后在屏幕上填充对象 -- 并确保 Dispose 画笔。
  
    请注意,在 Visual Basic .NET 中,您必须明确指定实现一个接口的方法 (... Implements IFillable.Fill);而在 C# 中,实现接口中的方法或属性由方法或属性的签名确定(因为您编写了一个称为 Fill 的方法,该方法不返回任何内容并接受一个 Graphics,因此它必须是 IFillable.Fill 的实现)。非常奇怪,Dr. GUI 通常喜欢简洁的编程结构(如果不可能通过简单的编写完成),但实际上却倾向使用 Visual Basic 的语法,因为这种语法既清晰又灵活(Visual Basic 实现类中的方法名称不必与接口中的名称匹配,并且一个给定方法通常能够实现多个接口方法)。
  
    实现属性
  
    IFillable 接口还包含一个属性,从中可以 set 和 get 画笔颜色。(我们在 Change fills to hot pink [将填充色更改为粉红] 按钮处理程序中使用该属性。)
  
    为实现公开属性,我们需要一个私有或保护的字段。这里我们选择了保护字段,以便能够方便地从派生类(而不允许任何类)对其进行访问。
  
    具有该字段后,我们可以轻松地编写一个很简单的 set 和 get 方法对以实现属性。
  
    请再次注意,在 Visual Basic .NET 中,必须明确指定所实现的属性。
  
    接口还是抽象 (MustInherit) 基类?
  
    在面向对象的编程中,最常见的争论之一就是,是使用抽象基类还是使用接口。
  
    接口可以提供一些额外的灵活性,但也要付出一定代价:对于实现该接口的每一个类,必须实现其中的所有内容。我们可以使用一个 helper 类来协助这项工作(稍后会提供一个相关示例),但您仍然必须在所有地方实现所有内容。并且接口不能包含数据(虽然如此,与在 Brand J 的系统中不同,它们可以包含属性,因此它们可以看起来好象包含了数据)。
  
    在本例中,Dr. GUI 为 DShape 选择了使用一个抽象基类而不是一个接口,因为他不想在每个类中将数据作为属性重复实现。此外,还因为从 DShape 派生出的所有内容都是形状,由于可填充对象仍然是形状,因而也可以进行填充。
  
    您的选择可能有所不同,但 Dr. GUI 认为他在此做出的选择非常正确。
  
    绘图对象的容器
    因为要重复绘制我们的对象(在 Windows 窗体版本中,每次都将绘制图像;在 ASP.NET 版本中,每次都将重新加载 Web 页),因此需要将它们放在一个容器中,以便能够反复访问它们。
  
    Dr. GUI 更进一步,将容器变得智能化,使其知道如何绘制所包含的对象。以下是这个容器类的 C# 代码:
  
  
    C#
  public class DShapeList {
  ArrayList wholeList = new ArrayList();
  ArrayList filledList = new ArrayList();
  
  public void Add(DShape d) {
  wholeList.Add(d);
  if (d is IFillable)
  filledList.Add(d);
  }
  
  public void DrawList(Graphics g) {
  if (wholeList.Count == 0)
  {
  Font f = new Font("Arial", 10);
  g.DrawString("没有任何要绘制的内容;列表为空...",
  f, Brushes.Gray, 50, 50);
  }
  else
  {
  foreach (DShape d in wholeList)
  d.Draw(g);
  }
  }
  
  public IFillable[] GetFilledList() {
  return (IFillable[])filledList.ToArray(typeof(IFillable));
  }
  }
  
  
    以下为等同类的 Visual Basic .NET 代码:
  
  
    Visual Basic
  
  .NET Public Class DShapeList
  Dim wholeList As New ArrayList()
  Dim filledList As New ArrayList()
  Public Sub Add(ByVal d As DShape)
  wholeList.Add(d)
  If TypeOf d Is IFillable Then filledList.Add(d)
  End Sub
  
  Public Sub DrawList(ByVal g As Graphics)
  If wholeList.Count = 0 Then
  Dim f As New Font("Arial", 10)
  g.DrawString("没有任何要绘制的内容;列表为空...", _
  f, Brushes.Gray, 50, 50)
  Else
  Dim d As DShape
  For Each d In wholeList
  d.Draw(g)
  Next
  End If
  End Sub
  
  Public Function GetFilledList() As IFillable()
  Return filledList.ToArray(GetType(IFillable))
  End Function
  End Class

维护两个列表
    因为我们要改变对象的填充颜色以实现 Change fill to hot pink 按钮,因此维护了两个可绘制对象列表:一个列表是全部对象,另一个列表是可填充对象。我们为这两个列表都使用了 ArrayList 类。ArrayList 对象包含一组 Object 引用 -- 这样一个 ArrayList 可以包含系统中任何类型的混合。
  
    这实际上并没有什么帮助 -- 我们希望 ArrayList 仅仅包括可绘制/可填充对象。为此,我们将 ArrayList 对象设为私有;然后将向列表添加对象的过程设为一个方法,该方法只接受一个 DShape。
  
    当使用 Add 方法向列表中添加对象时,我们将所有对象添加到 wholeList 中,然后检查对象是否还应添加到 filledList 集合中。
  
    请记住,Add 方法(以及列表)具有类型安全特性:它只接受 DShape(或者从 DShape 派生的类型,例如我们在上面创建的所有类型)。您不能将整数或字符串添加到列表中,这样我们便可以知道这个列表只包含可绘制对象。能够确知这一点是很方便的!
  
    绘制项
  
    我们还有一个 DrawList 方法,用于在它作为参数传递的 Graphics 对象上绘制列表中的对象。此方法具有两种情况:如果列表为空,它绘制一个字符串,说明列表为空。如果列表不为空,它使用一个 for each 构造函数遍历该列表,并在每个对象上调用 Draw。实际的遍历和绘图代码再简单不过了,如下面的 Visual Basic 所示。
  
  
    Visual Basic
  .NET Dim d As DShape
  For Each d In wholeList
  d.Draw(g)
  Next
  
  
    C# 代码几乎完全相同(当然,其行数更少)。
  
  
    C#
  foreach (DShape d in wholeList)
  d.Draw(g);
  
  
    由于列表是封装的,我们知道它具有类型安全特性,因此可以仅调用 Draw 方法而不必检查对象的类型。
  
    返回可填充列表
    最后,我们的 Change fills to hot pink(将填充色更改为粉红)按钮需要一个对所有可填充对象的引用数组,以便更改其 FillBrushColor 属性。虽然可以编写一个方法遍历列表并将颜色更改为传入的值,但这一次 Dr. GUI 选择了返回一个对象引用数组。幸运的是,ArrayList 类具有一个 ToArray 方法,利用它可以创建一个传递数组。该方法获取我们需要的数组元素类型 -- 从而可以传递回所需的类型 -- IFillable 数组。
  
  
    C#
  
  public IFillable[] GetFilledList() {
  return (IFillable[])filledList.ToArray(typeof(IFillable));
  }
  
    Visual Basic
  
  .NET Public Function GetFilledList() As IFillable()
  Return filledList.ToArray(GetType(IFillable))
  End Function
  
  
    在两种语言中,我们都使用了一个内置运算符获取给定类型的 Type 对象 -- 在 C# 中,是 typeof(IFillable);在 Visual Basic 中,是 GetType(IFillable)。
  
    调用程序使用此数组在可填充对象引用数组中遍历。例如,将填充颜色更改为粉红的 Visual Basic 代码如下所示:
  
  
  Dim filledList As IFillable() = drawingList.GetFilledList()
  Dim i As IFillable
  For Each i In filledList
  i.FillBrushColor = Color.HotPink
  Next
  
    用于分解出公共代码的 Helper 方法和类
    您可能注意到,Draw 和 Fill 方法有很多共同的代码。确切地说,每个类中创建笔或画笔的代码、建立 Try/Finally 块的代码以及清理笔或画笔的代码都是相同的 -- 唯一的区别是进行绘图或填充时调用的实际方法。(由于 C# 中 using 语法非常简洁,因而多余代码的数量并不明显。)在 Visual Basic .NET 中,每五行代码中可能有一行特殊的代码在所有实现中都是相同的。
  
    总之,如果存在大量重复代码,就需要寻求分解出公共的代码,以便形成为所有类所共享的公共子例程。这类方法有很多,Dr. GUI 非常高兴为您展示其中的两种。第一种方法仅用于类,第二种方法可用于类或接口,在本例中只用于接口。
  
    方法 1:公共入口点调用虚拟方法
    在第一个方法中,我们利用了类(不同于接口)可以包含代码这一事实。所以我们提供了一个用于创建笔的 Draw 方法的实现,以及一个异常处理程序和 Dispose,然后调用实际进行绘图的 abstract/MustOverride 方法。确切地说,我们更改了 DShapes 类以适应新的 Draw 方法,然后声明了新的 JustDraw 方法:
  
  
  Public MustInherit Class DShape
  ' Draw 不是虚拟的,这似乎有些不寻常……
  ' Draw 本应是抽象的 (MustOverride)。
  ' 但此方法是绘图的框架,而不是绘图代码本身,
  ' 绘图代码在 JustDraw 中完成。
  ' 还请注意,这意味着同原版本相比,这些类具有
  ' 不同的接口,虽然它们完成的工作相同。
  Public Sub Draw(ByVal g As Graphics)
  Dim p = New Pen(penColor)
  Try
  JustDraw(g, p)
  Finally
  p.Dispose()
  End Try
  End Sub
  ' 这里是需要成为多态的部分 -- 因此是抽象的
  Protected MustOverride Sub JustDraw(ByVal g As Graphics, _
  ByVal p As Pen)
  Protected bounding As Rectangle
  Protected penColor As Color ' 还应具有属性
  ' 还应具有移动、调整大小等方法。
  End Class
  
    一个值得注意的有趣的地方:Draw 方法并不是 virtual/Overridable。因为所有派生类都将以相同的方式完成这部分绘图(如果在 Graphics 上绘图 [如本例中的定义],则必须指派并清理笔),因此它不需要是 virtual/Overridable。
  
    实际上,Dr. GUI 认为在本例中,Draw 不应该是 virtual/Overridable。如果确实要覆盖 Draw 的行为(而不仅是 JustDraw 的行为),则可以将它设置为 virtual/Overridable。但在本例中,没有理由覆盖 Draw 的行为,如果鼓励程序员进行覆盖还会带来隐患 -- 他们可能不会正确处理笔,或者使用其他方法绘制对象而不是调用 JustDraw,这就违反了我们内置到类中的假设。因此,将 Draw 设置为非虚拟(顺便说一下,在 Brand J 中没有这个选项)可能会降低代码的灵活性,但会更加可靠 -- Dr. GUI 认为在本例中,这样做非常值得。
  
    JustDraw 的典型实现如下所示:
  
  
  Protected Overrides Sub JustDraw(ByVal g As Graphics, ByVal p As Pen)
  g.DrawEllipse(p, bounding)
  End Sub
  
    如您所见,我们获得了所希望的简洁的派生类实现。(可填充类中的实现只是略微复杂一些 -- 稍后会看到。)
  
    请注意,我们在接口中添加了一个额外的公开方法 JustDraw,除了要绘制的 Graphics 对象外,该方法还引用我们在 Draw 中创建的 Pen 对象。因为该方法需要是 abstract/MustOverride,因此必须是公开的。
  
    这并不是一个大问题,但它确实更改了类的公开接口。所以即使这个分解出公共代码的方法非常简单方便,也应当尽可能选择其他方法以避免更改公开接口。
  
    方法 2:虚拟方法调用公共 helper 方法,使用回调
    在实现接口的 Fill 方法时,代码的复杂程度也很类似:每六行代码中可能有一行特殊的代码在所有实现中都是相同的。但是我们不能将公共的实现放到接口中,因为接口只是声明,它们不包含代码或数据。此外,上面列出的方法是不能接受的,因为它会更改接口 -- 我们可能并不希望这样,或者因为是其他人创建的接口,我们根本不可能更改!
  
    所以,我们需要编写一个 helper 方法以设置并回调我们的类,以便进行实际的填充。对于本例,Dr. GUI 将代码放在一个单独的类中,这样任何类都可以使用该代码。(如果采用该方法来实现 Draw,则可以将 helper 方法作为抽象基类中的私有方法实现。)
  
    暂时不进一步展开,以下是我们创建的类:
  
  
  ' 请注意,该 delegate 提供的帮助仍然具有多态行为。
  Class FillHelper
  Public Delegate Sub Filler(ByVal g As Graphics, ByVal b As Brush)
  Shared Sub SafeFill(ByVal i As IFillable, ByVal g As Graphics, _
  ByVal f As Filler)
  Dim b = New SolidBrush(i.FillBrushColor)
  Try
  f(g, b)
  Finally
  b.dispose()
  End Try
  End Sub
  End Class
  
    我们的 helper 方法调用了 SafeFill,该方法接受一个可填充对象(请注意,这里我们使用了 IFillable 接口类型,而不是 DShape,从而只能传递可填充对象)、一个要在其上进行绘图的 Graphics 和一个称为 delegate 的私有变量。我们可以将 delegate 视为一个对方法(而不是对象)的引用 -- 如果您经常使用 C 或 C++ 编程,则可以将其视为具有类型安全特性的函数指针。可以将 delegate 设置为指向任何具有相同参数类型和返回值的方法,无论是实例方法还是 static/Shared 方法。将 delegate 设置为指向相应的方法后(例如在调用 SafeFill 时),我们可以通过 delegate 间接调用该方法。(顺便说一下,Brand J 中没有 delegate,这时如果使用此方法,会非常困难并且很不灵活)。
  
    delegate 类型 Filler 的声明位于类声明之上 -- 它被声明为一个不返回任何内容(在 Visual Basic .NET 中是一个 Sub)并且将 Graphics 和 Brush 作为参数传递的方法。我们会在将来的专栏中深入讨论 delegate。
  
    SafeFill 的操作非常简单:它指派画笔并将 Try/Finally 和 Dispose 设置为公共代码。它通过调用我们作为参数接收的 delegate 所引用的方法进行各种操作:f(g, b)。
  
    要使用这个类,需要向可填充对象类中添加一个可以通过 delegate 调用的方法,并确保将该方法的引用(地址)传递到 SafeFill,我们将在接口的 Fill 实现中调用 SafeFill。以下是 DFilledCircle 的代码:
  
  
  Public Sub Fill(ByVal g As Graphics) Implements IFillable.Fill
  FillHelper.SafeFill(Me, g, AddressOf JustFill)
  End Sub
  Private Sub JustFill(ByVal g As Graphics, ByVal b As Brush)
  g.FillEllipse(b, bounding)
  End Sub
  
    这样,当需要填充对象时,便在该对象上调用 IFillable.Fill。它将调用我们的 Fill 方法,而 Fill 方法调用 FillHelper.SafeFill,后者传递一个对我们的可填充对象的引用、所传递的要在其上进行绘图的 Graphics 对象以及一个对实际完成填充的方法的引用 -- 在本例中,该方法是私有的 JustFill 方法。
  
    然后,SafeFill 通过 delegate -- JustFill 方法来设置画笔和调用,JustFill 方法通过调用 Graphics.FillEllipse 进行填充并返回值。SafeFill 将清理画笔并返回到 Fill,Fill 再返回到调用者。
  
    最后是 JustDraw,它和原始版本中的 Draw 很类似,因为我们都调用了 Fill,并调用了基类的 Draw 方法(这是我们以前所做的)。以下是相关代码:
  
  
  Protected Overrides Sub JustDraw(ByVal g As Graphics, ByVal p As Pen)
  Fill(g)
  MyBase.JustDraw(g, p)
  End Sub
  
    请记住,指派画笔和笔的复杂之处在于它在 helper 函数中的处理 -- 在 Draw 中,它位于基类中;在 Fill 中,它位于 helper 类中。
  
    如果您认为这比以前复杂了,那么确实如此。如果您认为由于额外的调用和需要处理 delegate,速度比以前缓慢了,也确实如此。在生活中总是有很多东西需要进行权衡。
  
    那么,这样做值得吗?也许值得。这取决于公共代码的复杂程度,以及该代码需要重复的次数。也就是说,需要权衡。如果我们决定删除 Try/Finally,而只在完成绘图后清理笔和画笔,代码便会非常简单,这些方法也就用不上。并且在 C# 中,using 语句非常简洁,我们也不必费神使用这些方法。Dr. GUI 认为,在 Visual Basic 中使用 Try/Finally 时,可以使用、也可以不使用这些方法,这里旨在向大家展示这些方法,以便在遇到具有大量公共代码的情况时使用。

posted on 2004-10-07 14:27  TPoI  阅读(463)  评论(0)    收藏  举报

导航