因为我们要改变对象的填充颜色以实现 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 |
C# 代码几乎完全相同(当然,其行数更少)。
C#
foreach (DShape d in wholeList) |
由于列表是封装的,我们知道它具有类型安全特性,因此可以仅调用 Draw 方法而不必检查对象的类型。
返回可填充列表 最后,我们的 Change fills to hot pink(将填充色更改为粉红)按钮需要一个对所有可填充对象的引用数组,以便更改其 FillBrushColor 属性。虽然可以编写一个方法遍历列表并将颜色更改为传入的值,但这一次 Dr. GUI 选择了返回一个对象引用数组。幸运的是,ArrayList 类具有一个 ToArray 方法,利用它可以创建一个传递数组。该方法获取我们需要的数组元素类型 -- 从而可以传递回所需的类型 -- IFillable 数组。
C# public IFillable[] GetFilledList() { return (IFillable[])filledList.ToArray(typeof(IFillable)); }
.NET Public Function GetFilledList() As IFillable() |
在两种语言中,我们都使用了一个内置运算符获取给定类型的 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 |
您可能注意到,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 时,可以使用、也可以不使用这些方法,这里旨在向大家展示这些方法,以便在遇到具有大量公共代码的情况时使用。
维护两个列表
因为我们要改变对象的填充颜色以实现 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 |
C# 代码几乎完全相同(当然,其行数更少)。
C#
foreach (DShape d in wholeList) |
由于列表是封装的,我们知道它具有类型安全特性,因此可以仅调用 Draw 方法而不必检查对象的类型。
返回可填充列表
最后,我们的 Change fills to hot pink(将填充色更改为粉红)按钮需要一个对所有可填充对象的引用数组,以便更改其 FillBrushColor 属性。虽然可以编写一个方法遍历列表并将颜色更改为传入的值,但这一次 Dr. GUI 选择了返回一个对象引用数组。幸运的是,ArrayList 类具有一个 ToArray 方法,利用它可以创建一个传递数组。该方法获取我们需要的数组元素类型 -- 从而可以传递回所需的类型 -- IFillable 数组。
C# public IFillable[] GetFilledList() { return (IFillable[])filledList.ToArray(typeof(IFillable)); }
.NET Public Function GetFilledList() As IFillable() |
在两种语言中,我们都使用了一个内置运算符获取给定类型的 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 |
您可能注意到,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 时,可以使用、也可以不使用这些方法,这里旨在向大家展示这些方法,以便在遇到具有大量公共代码的情况时使用。
在 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] |
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();
.NET Dim drawingList As New DShapeList() |
我们还编写了一个 helper 方法以获得一个随机点:
C#
private Point GetRandomPoint() { Visual Basic .NET Private Function GetRandomPoint() As Point |
它生成两个位于 30 和 320 之间的随机数,作为随机点的坐标。
按钮单击事件处理程序
接下来就是每个按钮的按钮单击事件处理程序。多数仅仅是向绘图列表中添加一个新的可绘制对象,然后调用 PictureBox 上的 Invalidate,从而使用更新的绘图列表进行重绘。典型的按钮事件处理程序代码如下所示:
C#
private void AddPoint_Click(object sender, System.EventArgs e) { Visual Basic .NET Private Sub AddPoint_Click(ByVal sender As System.Object, _ |
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,
.NET Private Sub Drawing_Paint(ByVal sender As Object, _ |
我们的 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.Load randomGen = 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 If End Sub |
首先,我们尝试从视图状态加载随机数发生器状态。如果存在,则使用存储的值。如果不存在,则创建一个新的 Random 对象。
接下来,我们尝试从会话状态加载绘图列表。同样,如果不存在绘图列表,则创建一个新的空列表。
如果需要,视图状态和会话状态都会自动序列化我们的对象。视图状态始终被序列化,因此可以表示为浏览器隐藏输入字段中的编码的字符串。会话状态当存储在数据库中或者在服务器间进行传递时被序列化,但是如果应用程序运行在单个服务器上(例如在开发机器上进行测试时),则不会将其序列化。
被注释的代码试图从 cookie 加载绘图列表。请注意,处理 cookie 要比处理视图或会话状态复杂得多。首先就是不能自动序列化。为序列化为一个字符串,我们在一个新类当中编写了 helper 函数,如下所示:
Public Shared Function DeserializeFromBase64String( _ ByVal base64String As String) As Object Dim 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.PreRender ViewState("randomGen") = randomGen @# 选项之一:编写会话状态 @# 选项之二:编写一个 cookie。必须编写代码进行序列化。 |
此代码稍微有些简单,因为我们不必查看状态是否已经存在于视图或会话状态对象中,而只需将其无条件写出。
同样,视图状态和会话状态可以自动对自身进行序列化,而 cookie 则不能,因此我们需要亲自执行。Dr. GUI 编写了下面的 helper 函数(在单独的类中),代码如下所示:
Public Shared Function SerializeToBase64String(ByVal o As Object) _ As String Dim 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.Load Dim drawingList As DShapeList @# 获取绘图列表选项之一:使用会话状态... @# 获取绘图列表选项之二:使用 cookie... Response.ContentType = "image/gif" |
首先,我们从会话状态或 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 的扩展版上尝试…)