请您检查作为 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 DShapePublic MustOverride Sub Draw(ByVal g As Graphics)Protected bounding As RectangleProtected 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 .NETPublic Class DHollowCircleInherits DShapePublic Sub New(ByVal p As Point, ByVal radius As Integer, _ByVal penColor As Color)p.Offset(-radius, -radius) ' 需要转换到左上角Dim diameter As Integer = radius * 2bounding = New Rectangle(p, New Size(diameter, diameter))Me.penColor = penColorEnd SubPublic Overrides Sub Draw(ByVal g As Graphics)Dim p = New Pen(penColor)Tryg.DrawEllipse(p, bounding)Finallyp.Dispose()End TryEnd SubEnd 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 .NETPublic Interface IFillableSub Fill(ByVal g As Graphics)Property FillBrushColor() As ColorEnd 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 .NETPublic Class DFilledRectangleInherits DHollowRectangleImplements IFillablePublic Sub New(ByVal rect As Rectangle, _ByVal penColor As Color, ByVal brushColor As Color)MyBase.New(rect, penColor)Me.brushColor = brushColorEnd SubPublic Sub Fill(ByVal g As Graphics) Implements IFillable.FillDim b = New SolidBrush(FillBrushColor)Tryg.FillRectangle(b, bounding)Finallyb.dispose()End TryEnd SubProtected brushColor As ColorPublic Property FillBrushColor() As Color _Implements IFillable.FillBrushColorGetReturn brushColorEnd GetSet(ByVal Value As Color)brushColor = ValueEnd SetEnd PropertyPublic Overrides Sub Draw(ByVal g As Graphics)Dim p = New Pen(penColor)TryFill(g)MyBase.Draw(g)Finallyp.Dispose()End TryEnd SubEnd 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 DShapeListDim 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 SubPublic Sub DrawList(ByVal g As Graphics)If wholeList.Count = 0 ThenDim f As New Font("Arial", 10)g.DrawString("没有任何要绘制的内容;列表为空...", _f, Brushes.Gray, 50, 50)ElseDim d As DShapeFor Each d In wholeListd.Draw(g)NextEnd IfEnd SubPublic Function GetFilledList() As IFillable()Return filledList.ToArray(GetType(IFillable))End FunctionEnd 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 DShapeFor Each d In wholeListd.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 IFillableFor Each i In filledListi.FillBrushColor = Color.HotPinkNext
用于分解出公共代码的 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)TryJustDraw(g, p)Finallyp.Dispose()End TryEnd Sub' 这里是需要成为多态的部分 -- 因此是抽象的Protected MustOverride Sub JustDraw(ByVal g As Graphics, _ByVal p As Pen)Protected bounding As RectangleProtected 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 FillHelperPublic 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)Tryf(g, b)Finallyb.dispose()End TryEnd SubEnd 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.FillFillHelper.SafeFill(Me, g, AddressOf JustFill)End SubPrivate 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 时,可以使用、也可以不使用这些方法,这里旨在向大家展示这些方法,以便在遇到具有大量公共代码的情况时使用。
使我们的对象可序列化
为在 ASP.net 中使用可绘制对象类,我们需要对其再进行一项更改。这些类需要是可序列化的,以便能够在主要的 Web 页和生成该图像的 Web 页之间传递数据(后面将详述)。序列化是这样的过程:将某个类的数据以某种方式写入存储介质,以便存储和/或传递数据并在以后反序列化。反序列化是从序列化数据中重新创建对象的过程。我们会在将来的专栏中深入讨论这个问题。
Dr. GUI 最开始作为 Windows 窗体应用程序编写此应用程序时,只使用了 .NET Framework 和操作系统预先分配的 Brushes 和 Pens 类中的可用常用画笔和笔。因为这些已经分配完毕,保持对它们的引用不会有任何妨碍,同时也无需对其进行 Dispose。
但由于笔和画笔是非常复杂的对象,不能是可序列化的,因此 Dr. GUI 必须改变其策略,转而决定存储笔和画笔的颜色,然后在需要绘制和填充对象时动态创建笔和画笔。
如何使之可序列化?
序列化是 .NET Framework 的一个重要部分,因此也使序列化对象的工作变得很简单。
我们只需使用 Serializable 属性标记一个类便可使之可序列化。(这与我们以前用于在枚举上将其标记为一套标志的属性是同一种属性。)在 C# 和 Visual Basic .NET 中的语法如下所示:
C#[Serializable]class Foo // ...Visual Basic.NET _Class Foo ' ...
注意:除了将类标记为可序列化外,还必须使类中包含的所有数据可序列化,否则在试图序列化数据时,序列化框架会引发一个异常。
使容器可序列化
.NET Framework 的一大优点是可以使容器类可序列化。这意味着如果将对象存储在可序列化的容器中,容器可以自动序列化对象。
因此在本例中,DShapeList 类包含了两个 ArrayList 对象。由于 ArrayList 是可序列化的,因此要使 DShapeList 可序列化,只需将其标记为 Serializable 属性即可,如下所示:
Visual Basic.NET _Public Class DShapeListDim wholeList As New ArrayList()Dim filledList As New ArrayList()' ...C#[Serializable]public class DShapeList {ArrayList wholeList = new ArrayList();ArrayList filledList = new ArrayList();
假设我们放在 DShapeList 中的对象都是可序列化的,这时便可以使用单个语句序列化和反序列化整个列表!
顺便说一下,这对于该应用程序的 Windows 窗体版本也是一个很好的改变,因为它使我们能够将绘图写入磁盘文件并重新加载。
可绘制对象的三个版本;任何一个都可以在任何上下文中使用
您可能已经注意到,我们有三种版本的可绘制对象代码:在 C# 和 Visual Basic .NET 中各有一个不使用我们在上面编写的 helper 方法的版本,另一个是 Visual Basic .NET 中使用 helper 方法的版本。
在这里还有一点微小的差别:使用 helper 的文件中的数据类被标记为可序列化;其他文件中的数据类则没有标记为可序列化。
但是,请注意下面很重要的一点:如果我们返回去并将所有文件中的数据类标记为可序列化,那么将能够在任何应用程序中使用任何类。我们将能够混合使用 C# 和 Visual Basic .NET。并且能够在 ASP.NET 应用程序中使用最初为 Windows 窗体应用程序编写的代码。
这种简便的代码重用意味着您编写的代码更具价值,因为代码可以在很多不同的环境中重复使用。
在 Windows 窗体应用程序中使用可绘制对象
我们已经讨论了可绘制对象类,下面谈谈如何在 Windows 窗体应用程序中使用这些类。首先谈一下 Windows 窗体应用程序是怎样工作的。
Windows 窗体应用程序的主要部分
简单的 Windows 窗体应用程序包含一个主窗口(或窗体),其中包含控件子项。如果您是一位 Visual Basic 程序员,就会发现这个模型非常熟悉。
主窗口
任何 Windows 窗体应用程序中的关键对象都是主窗口。该窗体将在应用程序的 static/Shared Main 方法中创建,如下所示。
在一个简单的 Windows 窗体应用程序(例如我们所编写的)中,所有其他控件都是此主窗体的子项。
按钮和文本框
我们的窗体具有一套按钮和一些文本框。每个按钮有一个处理程序,可以向列表中添加形状,并绘制列表。所包含的文本框用于显示如何从窗体中获得输入。还有一个分组框,提供了有关文本框和相关按钮的可视指示。
PictureBox
左边是最重要的控件:PictureBox。这是绘制和显示图像的位置。在 Windows 应用程序中,您可能需要随时重绘图像 -- 例如,如果窗口被最小化或被其他窗口覆盖,则再次显示窗口时便需要进行重绘。
在响应画图 (Paint) 消息时便会完成这种按需绘图,由父窗体窗口类中的一个事件处理程序处理。
Windows 窗体应用程序中的主要例程
我们简单看一下 Windows 窗体应用程序中的重要例程。请注意,用户界面的代码与可绘制对象的代码相比非常简短。这就是使用 .net Framework 完成诸多工作的好处。(这也表明我们使用可绘制对象类完成的工作确实很好。)
窗体方法
窗体(或主窗口)是从 System.Windows.Forms.Form 中派生的,所以继承了其所有行为。所有这些控件都声明为这个类的成员,这样在清理类时它们也将被清理(清理是在 Dispose 方法中实际明确完成的)。
它还包含了我们所需数据的声明(DShapeList 和一个随机数生成器对象)、Main 以及用于按钮单击事件和 PictureBox 画图事件的事件处理程序。
Main
Main 的任务就是创建和运行主窗口对象。其 C# 代码如下所示。
C#[STAThread]static void Main(){Application.Run(new MainWindow());}
STAThread 属性对于 Windows 窗体应用程序的 Main 非常重要 -- 您应当始终使用该项,以便依赖于 OLE Automation(例如拖放和剪贴板)的功能能够正常工作。
在 Microsoft Visual Studio? 生成的 Visual Basic .NET 源代码中不会找到此方法,但是如果使用 ILDASM 在 .exe 中查找,便会找到一个与上面所述功能相同的 Main -- 可能是由 Visual Basic .NET 编译器生成的。
InitializeComponent
在 Windows Form Designer generated code(Windows 窗体设计器生成的代码)下(如果不能看到此区域中的代码,单击小加号),会看到用于创建和初始化所有按钮和窗体上其他控件的代码。
数据声明/随机数生成
除了在代码的隐藏区域中声明的所有控件外,我们还需要声明两个变量:存放绘图列表的数据结构,以及一个 Random 类型的对象。我们使用 Random 对象为所创建的对象的位置生成随机数。
数据声明位于 MainWindow 类内,但位于任何方法之外。在 C# 和 Visual Basic .NET 中,其代码如下所示:
C#DShapeList drawingList = new DShapeList();Random randomGen = new Random();
Visual Basic.NET Dim drawingList As New DShapeList()Dim randomGen As New Random()
我们还编写了一个 helper 方法以获得一个随机点:
C#private Point GetRandomPoint() {return new Point(randomGen.Next(30, 320), randomGen.Next(30, 320));}Visual Basic.NET Private Function GetRandomPoint() As PointReturn New Point(randomGen.Next(30, 320), randomGen.Next(30, 320))End Function
它生成两个位于 30 和 320 之间的随机数,作为随机点的坐标。
按钮单击事件处理程序
接下来就是每个按钮的按钮单击事件处理程序。多数仅仅是向绘图列表中添加一个新的可绘制对象,然后调用 PictureBox 上的 Invalidate,从而使用更新的绘图列表进行重绘。典型的按钮事件处理程序代码如下所示:
C#private void AddPoint_Click(object sender, System.EventArgs e) {drawingList.Add(new DPoint(GetRandomPoint(), Color.Blue));Drawing.Invalidate();}Visual Basic.net Private Sub AddPoint_Click(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles AddPoint.ClickdrawingList.Add(New DPoint(GetRandomPoint(), Color.Blue))Drawing.Invalidate()End Sub
Change fills to hot pink(将填充色更改为粉红)按钮有一些不同 -- 它在列表中获得一个所有可填充对象的数组,然后将它们的画笔颜色更改为粉红。这部分代码显示在前面“返回可填充列表”一节的末尾。(此外还必须使 PictureBox 无效。)
最后,Erase All(全部删除)按钮简单地创建了一个新的绘图列表,并将我们的 drawingList 字段指向该列表。这样便释放了旧的绘图列表以进行最后的内存回收。然后使 PictureBox 无效,把自己也删除掉。
PictureBox 画图事件处理程序
我们要注意的最后一项就是在 PictureBox 中画出图像。为此,需要处理 PictureBox 生成的 Paint 事件,然后使用通过此事件传递的 Graphics 对象在其上进行绘图。要进行绘图,只需调用绘图列表的 DrawList 方法 -- 一个 for each 循环和多态将负责处理剩下的工作!
C#private void Drawing_Paint(object sender,System.Windows.Forms.PaintEventArgs e) {drawingList.DrawList(e.Graphics);}
Visual Basic.NET Private Sub Drawing_Paint(ByVal sender As Object, _ByVal e As System.Windows.Forms.PaintEventArgs) _Handles Drawing.PaintdrawingList.DrawList(e.Graphics)End Sub
我们的 Windows 窗体应用程序之旅到此结束 -- 请斟酌这些代码并进行修改,这样可以学到更多内容!
在 ASP.NET 应用程序中使用可绘制对象
虽然 ASP.NET Web 应用程序和 Windows 窗体应用程序之间存在某些不同,但两者的相似性还是令 Dr. GUI 感到惊奇!
Web 窗体应用程序的主要部分
ASP.NET Web 窗体应用程序的主要部分与 Windows 窗体应用程序的各部分非常对应。
页面
此项对应 Windows 窗体应用程序中的主窗口。页面是所有按钮和其他控件的容器。
按钮
同样,这里有一组按钮,可用于在窗体上执行各种操作。请注意,与以前的应用程序不同,我们将页面文档的 pageLayout 属性设置为 GridLayout 而不是 FlowLayout。这意味着我们可以通过像素位置定位每个按钮(以及其他控件)。
请注意,这里也有一些文本框。
还要注意,您不能向 Web 复制和粘贴 Windows 窗体控件 -- 必须重新创建整个页面。
图像控件
图像控件对应于 Windows 窗体应用程序中的 PictureBox。但两者有一些重要的差别:图像控件不生成 Paint 消息,而是包含加载图像的 URL。
我们将这个 URL 设置为第二个 Web 页,ImageGen.aspx。换句话说,我们有一个 Web 页,它的全部工作就是从我们的绘图列表中生成图像中的位,然后将图像发送到客户端的 Web 浏览器。
我们将在下面讨论其代码。
Web 窗体应用程序的主要例程
Windows 窗体应用程序和 Web 窗体应用程序的代码之间存在一些重要不同 -- 但也有某些有趣的相似之处。还要注意,可绘制对象文件中的所有代码都可以用于三种应用程序中的任何一种。
我们的页面是从 System.Web.UI.Page 派生的,除了以下内容外,还包含一组用于所有控件的声明:
完全相同的内容:数据声明和 GetRandomPoint
此代码与 Visual Basic .NET Windows 窗体应用程序中的代码几乎完全相同。如果愿意,可以再看一下上面的这段代码。它们之间只有一个不同之处,就是对字段进行了声明而没有将其初始化。它们将在 Page_Load 方法中被初始化(如后面所示)。
GetRandomPoint 方法与其他应用程序完全相同。能够重复使用代码真的不错,不是吗?
非常相似的内容:按钮单击事件处理程序
按钮单击事件处理程序与 Windows 窗体应用程序相同,只有一个例外:在 Web 窗体中,由于每次单击按钮时都将重绘图像,因此无需(也不能)使图像控件无效。此外,它还将自动进行重绘 -- 因此唯一要调用的就是绘图列表的 Add 方法。
以下是一个典型的按钮事件处理程序 -- 用于绘制一个点。
Private Sub AddPoint_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles AddPoint.Click drawingList.Add(New DPoint(GetRandomPoint(), Color.Blue)) End Sub
其他按钮事件处理程序都与 Windows 窗体的情况类似,当然,有一种例外情况除外,即不调用任何一种方法使图像无效。
差别很大的内容:页面加载和卸载处理程序
页面加载和卸载处理程序方法是完全不同的。
请记住,我们的页面对象是使用每一个 HTTP 请求重新创建的。由于每个请求都将创建一个新页面,因此我们不能象在 Windows 窗体中那样将数据作为成员变量存储,在 Windows 窗体中,主窗口将伴随应用程序而存在。
因此我们必须在某种状态变量中存储请求和页面之间所需的信息。这里有几种选择 -- 下面将就此进行讨论。
在页面和请求之间传递状态
为使应用程序能够工作,它需要能够维护请求之间的状态并将状态传递给绘图页面(如下所示)。
维护和传递状态有多种方式。如果应用程序是严格的单页面应用程序(和以前的应用程序一样),则可以使用视图状态,其中数据被编码存储在 Web 页的隐藏输入字段中。
但是我们的图像控件是在单独的页面中进行绘图的,因此需要某些更灵活的东西。最好的选择就是 cookie 和会话状态。会话状态非常灵活,但要求使用服务器资源。浏览器可以保留 cookie,但其大小非常有限。
Page_Load
Page_Load 是在创建页面对象之后以及在运行所有事件处理程序之前被调用的。因此 Page_Load 方法是加载永久数据的理想所在。如果找不到数据,就创建新的数据。以下是相关代码:
Private Sub Page_Load(ByVal sender As System.Object, _ByVal e As System.EventArgs) _Handles MyBase.LoadrandomGen = ViewState("randomGen")If randomGen Is Nothing Then randomGen = New Random()' 选项之一:使用会话状态获得绘图列表'(保存在 Page_Unload 中)'(注意:要求服务器上的状态存储)drawingList = Session("drawingList")If drawingList Is Nothing Then drawingList = New DShapeList()' 选择之二:从用户浏览器上的 cookie 中' 检索绘图状态'(注意:不需要服务器存储,但有些用户会禁用 cookie)'(注意之二:cookie 不会自动反序列化!:( )' 注意之三:使用 cookie 将限制能够绘制的形状数量'Dim drawingListCookie As HttpCookie'drawingListCookie = Request.Cookies("drawingList")'If drawingListCookie Is Nothing Then' drawingList = New DShapeList()'Else' drawingList = _' SerialHelper.DeserializeFromBase64String( _' drawingListCookie.Value)'End IfEnd Sub
首先,我们尝试从视图状态加载随机数发生器状态。如果存在,则使用存储的值。如果不存在,则创建一个新的 Random 对象。
接下来,我们尝试从会话状态加载绘图列表。同样,如果不存在绘图列表,则创建一个新的空列表。
如果需要,视图状态和会话状态都会自动序列化我们的对象。视图状态始终被序列化,因此可以表示为浏览器隐藏输入字段中的编码的字符串。会话状态当存储在数据库中或者在服务器间进行传递时被序列化,但是如果应用程序运行在单个服务器上(例如在开发机器上进行测试时),则不会将其序列化。
被注释的代码试图从 cookie 加载绘图列表。请注意,处理 cookie 要比处理视图或会话状态复杂得多。首先就是不能自动序列化。为序列化为一个字符串,我们在一个新类当中编写了 helper 函数,如下所示:
Public Shared Function DeserializeFromBase64String( _ByVal base64String As String) As ObjectDim formatter As New BinaryFormatter()Dim bytes() As Byte = Convert.FromBase64String(base64String)Dim serialMemoryStream As New MemoryStream(bytes)Return formatter.Deserialize(serialMemoryStream)End Function
Dr. GUI 使用了二进制格式化程序并转换为可打印的 base 64 字符串,因为无论是 SOAP 还是 XML 格式化程序都不适用于此应用程序。我们必须从纯二进制表示转换为 base 64 字符串,以避免因简单复制字节而产生字符串中控制字符的潜在问题。base 64 字符串使用一个字符 A-Z、a-z、0-9、+ 或 /(共 64 个或 2^6 个字符)来表示二进制字符串中的每六位,因此四个字符表示三个字节 -- 第一个字符表示第一个字节中的六位,第二个字符表示第一个字节的末两位和第二个字节的前四位,以此类推。同样,使用 base 64 字符串关键在于可以将字符串限制为可打印字符,这样就避免了任何控制字符出现潜在问题。
XML 格式化程序不会序列化私有数据 -- 而 Dr. GUI 也不打算为绘图列表中的私有数据添加公开访问权限。SOAP 格式化程序不存在这种限制,但它不会序列化空列表以便进行反序列化。相反,它不为空列表向数据流写入任何东西,这样当尝试反序列化时就会引发一个异常。(Dr. GUI 认为这是一个错误。)
Dr. GUI 更喜欢以可读的 XML 格式进行序列化,但由于两种 XML 序列化格式化程序都无法完成此项工作,所以最终选择了二进制格式化程序并转换为 base 64 字符串。
Page_Unload
Page_Unload 是在破坏页面对象(包括任何所包含的数据)之前被调用的,因此是永久放置重要数据的理想位置,这样我们便可以在将来从 Page_Load(或者从图像的 Page_Load)中取出这些数据。
因此,我们将数据保存在 Page_Unload 中,并从 Page_Load 中检索数据。虽然这有些奇怪,但却是正确的。
以下是 Page_Unload 的代码:
Private Sub Page_Unload(ByVal sender As Object, _ByVal e As System.EventArgs) _Handles MyBase.PreRenderViewState("randomGen") = randomGen' 选项之一:编写会话状态Session("drawingList") = drawingList' 选项之二:编写一个 cookie。必须编写代码进行序列化。' 注意:使用 cookie 将限制能够绘制的形状数量!'Dim drawingListString As String =' SerialHelper.SerializeToBase64String(drawingList)'Response.Cookies.Add(New HttpCookie("drawingList", _' drawingListString))End Sub
此代码稍微有些简单,因为我们不必查看状态是否已经存在于视图或会话状态对象中,而只需将其无条件写出。
同样,视图状态和会话状态可以自动对自身进行序列化,而 cookie 则不能,因此我们需要亲自执行。Dr. GUI 编写了下面的 helper 函数(在单独的类中),代码如下所示:
Public Shared Function SerializeToBase64String(ByVal o As Object) _As StringDim formatter As New BinaryFormatter()Dim serialMemoryStream As New MemoryStream()formatter.Serialize(serialMemoryStream, o)Dim bytes() As Byte = serialMemoryStream.ToArray()Return Convert.ToBase64String(bytes)End Function
在单独的页面中绘图
正如前面提到的,绘图是在单独的页面中进行的。以下是该页面的代码:
Private Sub Page_Load(ByVal sender As System.Object, _ByVal e As System.EventArgs) Handles MyBase.LoadDim drawingList As DShapeList' 获取绘图列表选项之一:使用会话状态...drawingList = Session("drawingList")If drawingList Is Nothing Then drawingList = New DShapeList()' 获取绘图列表选项之二:使用 cookie...'(请查看主页代码以了解更多注释)'Dim drawingListCookie As HttpCookie'drawingListCookie = Request.Cookies("drawingList")'If drawingListCookie Is Nothing Then'drawingList = New DShapeList()'Else' drawingList = _' SerialHelper.DeserializeFromBase64String( _' drawingListCookie.Value)'End IfResponse.ContentType = "image/gif"Dim bitMap As New Bitmap(368, 376)Dim g As Graphics = Graphics.FromImage(bitMap)Tryg.Clear(Color.White)drawingList.DrawList(g)bitMap.Save(Response.OutputStream, ImageFormat.Gif)Finallyg.Dispose()bitMap.Dispose()End TryEnd Sub
首先,我们从会话状态或 cookie 中获取绘图列表。(这部分代码与上面的 Page_Load 方法类似。)
然后,我们将正在编写的响应流的 ContentType 设置为一个 GIF 图像。
接下来,我们要做一些真正美妙的事情:按照所需的大小(本例按照与 Windows 窗体应用程序中相同的大小)创建一个位图。
然后,我们得到一个与该位图相关联的 Graphics 对象,清除该对象,并在其中绘制我们的列表。
下面的步骤很重要:接下来,我们将 GIF 格式的图像内容写出到响应流(即浏览器)中。我们设置了这种响应类型以确保浏览器能够正确解释图像,然后发送图像的位。(.net Framework 使该操作变得相当简单。而在原来的 Windows GDI 时代,仅在位图上进行绘制都是非常痛苦的!)
另一个重要步骤就是要记住清理 Graphics 和 Bitmap 对象 -- 并使用 Try/Finally,以便即使出现异常也会清理对象。
嗨!步骤真多。但是为了让此应用程序能够作为 ASP.NET 应用程序运行,还是值得的 -- 并且更好的是,这种应用程序不需要依赖客户端脚本。
试一试!
如果您手头有 .NET,学习它的最好方法就是试一试。如果没有,就请想办法得到。如果您每周在 Dr. GUI .NET 上花费一个小时左右,那么在了解 .NET 之前您将已经成为一名 .NET Framework 专家了。
从您开始 -- 并邀请您的朋友!
作为第一个学习新技术的人,感觉一定不错,但如果和朋友们分享则乐趣更多!为享受更多乐趣,邀请朋友共同学习 .NET 吧!
应进行的尝试...
首先试一下这里给出的代码。其中有一些是从大型程序中节选下来的,围绕这些代码片断创建程序会取得不错的效果。(如果必须,也可以使用 Dr. GUI 提供的代码。)琢磨一下代码。
向绘图程序添加一些不同的形状,包括填充和不填充的形状。注意:虽然我们没有对其进行转换,但所添加的类可能存在于来自其他文件的不同程序集中(因而是不同的可执行文件)。这意味着所添加的类的语言甚至可以和其他类不同。当您阅读并尝试有关的必要工作后,会更加确信这一点。
请向这里的类添加一些方法,并尝试改动用户界面。自己制作一个可爱的 CAD 小程序。
在自己选择的项目中使用继承、abstract/MustInherit 类、接口和多态。当类系列具有很多共同部分时,使用继承的效果最佳。如果类并不具有很多共同部分,但功能相似,这时使用接口的效果最好。
尝试用 ASP.NET 编写自己的绘图应用程序。请注意,如果能够运行 .NET Framework,则只需 Microsoft Internet Information Server (IIS) 便可以在自己的计算机上运行 ASP.NET -- 无需服务器!Dr. GUI 认为,在便携式计算机上仅使用标准操作系统和免费的 .NET Framework 创建和测试 Web 应用程序,感觉实在好极了。(但 Dr. GUI 还是倾向于使用 Visual Studio,或者至少 Visual Basic 或 Microsoft Visual C#? 标准版。)看看吧!不需要服务器!甚至不需要 Internet 连接!(可在 Brand J 的扩展版上尝试…)