在VB6中用命令行为模式控制GUI动作
在现实生活中,次序是难以控制的。一旦某种东西处于运动状态,我们就很难操作这种动作的离散部分。当然,在现实生活中是不可能撤销某种动作的。但是在编程过程中,次序却不是难以琢磨的。如果你的计划是正确的,你就可以定义行动,接着用你喜欢的方式来控制这些行动。实现这种操作的一个非常有用的工具是命令行为(Command Behavior)模式。
命令行为模式是我们可以使用的一种简单模式。它在行为(action)概念的具体化和撤销行为方面显得尤其有益。把行为转换到对象中也是一条非常有序的途径,它可以确保每个操作都会集中在实现该操作的一部分代码上。
在本文中我们将研究命令行为模式的使用方法,你会感觉它比较有趣。我给窗体添加了一个球的图片,并且实现了表现球的移动过程的命令。每个球命令都被放入栈中,允许你撤销球的移动,或者重新查看球的移动。在稍微修补一下代码之后,你可以发现把表现层(GUI)的操作转换为命令对象使得我们使用多种方式(例如按钮和菜单)封装、组织、跟踪、撤销和调用操作容易多了。
定义命令(Command)类
实现命令行为的一个普通的途径是定义一个带有Do和Undo方法的基类(或接口)。Undo执行与Do操作相反的行为。Do操作是什么样子都无关紧要,它可以是任何东西("Do"在VB6中是保留字,因此我把"Execute"作为方法的名字)。
我的例子实现了四个移动命令。每个命令从上下左右四个方向中选择一个方向执行移动操作。每个命令的Undo操作采用相反的方向调用移动操作。很明显,我并没有限定两维的、直线方向。我可以模拟三维的或者三角法(trigonometric)运算规则的基本移动。现在我聚焦于该命令类。
使用公用基类的原因在于代码可以多形态地(polymorphically)调用Do或Undo操作,而不用关心命令对象的特定实例。列表1显示了基本的命令和所有四个衍生命令类的实现。由于VB6不支持类继承,我就使用了接口继承。
列表1
'' ICommand.cls Public Sub Execute() End Sub Public Sub Undo() End Sub Public Property Set Form(ByVal Form As Form1) End Property '' DownCommand.cls Option Explicit Implements ICommand Private FForm As Form1 Private Sub ICommand_Execute() FForm.MoveDown End Sub Private Sub ICommand_Undo() FForm.MoveUp End Sub '' LeftCommand.cls Public Property Set ICommand_Form(ByVal Form As Form1) Set FForm = Form End Property Option Explicit Implements ICommand Private FForm As Form1 Private Sub ICommand_Execute() FForm.MoveLeft End Sub Private Sub ICommand_Undo() FForm.MoveRight End Sub Public Property Set ICommand_Form(ByVal Form As Form1) Set FForm = Form End Property '' RightCommand.cls Option Explicit Implements ICommand Private FForm As Form1 Private Sub ICommand_Execute() FForm.MoveRight End Sub Private Sub ICommand_Undo() FForm.MoveLeft End Sub Public Property Set ICommand_Form(ByVal Form As Form1) Set FForm = Form End Property '' UpCommand.cls Option Explicit Implements ICommand Private FForm As Form1 Private Sub ICommand_Execute() FForm.MoveUp End Sub Private Property Set ICommand_Form(ByVal RHS As Form1) Set FForm = RHS End Property Private Sub ICommand_Undo() FForm.MoveDown End Sub |
请注意在列表1中ICommand使用了前缀I。这是接口的一个通俗的前缀符号,在多种语言中被广泛的应用。它的目的是帮助读者记住该模块只包含定义。同时还要注意所有的方向命令中都使用了Implements语句。这确保了每个类最少拥有ICommand接口。其结果是我可以定义一个ICommand变量,并给它指定实现了ICommand类的任何实例。
最后我还要指出,每个命令都保持了特定的Form(Form1)的指针。其原因在于Form1包含了自己的边界和我希望用命令操作的控件的信息。
建立GUI
该程序的GUI由Form1表现,它包含了一个图像(image)控件,在该控件中有一个球的位图。该窗体还包含了一个对象集合,我把这个集合作为先前命令的堆栈。通过把每条命令添加到该集合的末尾,以及从集合末尾弹出命令,我可以相反地跟踪命令的执行过程。从本质上来说,该集合扮演了堆栈的角色。我在命令执行之后压入(push)命令,弹出(pop)命令调用Undo操作。
为了演示Undo操作,我定义了EditUndo菜单和EditRewind操作。Undo从堆栈中弹出一条命令并调用Undo。Rewind弹出所有命令,在每个命令上调用Undo。列表2包含了Form1的代码。
列表2
Option Explicit Private stack As Collection Private Const MOVEMENT_AMOUNT As Integer = 50 Private Sub AboutMenu_Click() Const About As String = "Command Pattern Demo" + vbCrLf + _ "Copyright (c) 2004. All Rights Reserved" + vbCrLf + _ "Written By Paul Kimmel. pkimmel@softconcepts.com" MsgBox About, vbInformation + vbOKOnly, "About" End Sub Private Sub ExitMenu_Click() End End Sub Private Sub Form_KeyDown(KeyCode As Integer, Shift As Integer) Select Case KeyCode Case vbKeyBack UndoCommand Case vbKeyLeft Call ProcessCommand(New LeftCommand) Case vbKeyUp Call ProcessCommand(New UpCommand) Case vbKeyRight Call ProcessCommand(New RightCommand) Case vbKeyDown Call ProcessCommand(New DownCommand) End Select End Sub Private Function PopStack() As ICommand On Error GoTo Catch Debug.Print "Stack count: " & stack.Count If (stack.Count = 0) Then Beep Set PopStack = Nothing Exit Function End If Set PopStack = stack.Item(stack.Count) Call stack.Remove(stack.Count) Exit Function Catch: Debug.Print Err.Description Set PopStack = Nothing End Function Private Sub PushStack(ByVal command As ICommand) On Error GoTo Catch Call stack.Add(command) Debug.Print "Stack count: " & stack.Count Exit Sub Catch: Debug.Print Err.Description End Sub Public Sub UndoCommand() Dim command As ICommand Set command = PopStack() If (command Is Nothing) Then Exit Sub command.Undo End Sub Private Sub ProcessCommand(ByVal command As ICommand) Set command.Form = Me command.Execute Call PushStack(command) End Sub Public Sub MoveDown() Debug.Print "Moving Down" If (Image1.Top > Height) Then Image1.Top = 0 - Image1.Height + MOVEMENT_AMOUNT Else Image1.Top = Image1.Top + MOVEMENT_AMOUNT End If End Sub Public Sub MoveUp() Debug.Print "Moving Up" If (Image1.Top + Image1.Height < 0) Then Image1.Top = Height - MOVEMENT_AMOUNT Else Image1.Top = Image1.Top - MOVEMENT_AMOUNT End If End Sub Public Sub MoveLeft() Debug.Print "Moving Left" If (Image1.Left + Image1.Width < 0) Then Image1.Left = Width - MOVEMENT_AMOUNT Else Image1.Left = Image1.Left - MOVEMENT_AMOUNT End If End Sub Public Sub MoveRight() Debug.Print "Moving Right" If (Image1.Left > Width) Then Image1.Left = 0 - Image1.Width + MOVEMENT_AMOUNT Else Image1.Left = Image1.Left + MOVEMENT_AMOUNT End If End Sub Private Sub Form_Load() Set stack = New Collection End Sub Private Sub RewindMenu_Click() While stack.Count > 0 UndoCommand Wend End Sub Private Sub UndoMenu_Click() Call UndoCommand End Sub |
下一步,我通过把窗体的KeyPreview属性设置为True并建立与该按键相关的命令对象,把有关的方向箭头按键与每条命令关联起来。例如vbKeyUp应该建立一个UpCommand对象。接着我处理这些命令(我也可以使用工厂创建型模式把命令对象的创建过程移动到命令类的内部)。
ProcessCommand被定义为把Form1的指针赋予某个命令的Form属性、执行该命令并把该命令压入集合堆栈中。你没有必要实现一个堆栈,但是如果你没有跟踪命令执行的次序,那么使用Undo操作是不可能的。
图1显示的UML模型显示了示例程序中不同的类之间的关系。
图1:命令中的类:描述我们的命令模式中关系的UML类图表 |
检查UML中的工作
如果你了解UML,那么你应该知道图1基本上概括了本文的内容。如果你不了解UML(它是一个很好的技巧),这种简单的类图表也不难理解。线(Line)表现了关系。菱形(Diamond)表现了完整的和局部的关系,也就是集合体。完整的关系带有菱形,而部分的却没有。方向键表现了联合关系(associations),它与集合关系大致相当,三角形表现了继承(inheritance),被继承的东西带有三角形。虚线和方向线表明了依赖关系和必要的信息。
请查看图1,从逻辑上讲,LeftCommand、RightCommand、DownCommand和UpCommand都继承自ICommand。命令与窗体有关联,但是并不是拥有窗体--这是由它们的Form属性表现出来的。Form依赖于命令,这是因为它是该Form的操作被调用的方式,并且Form与集合有聚合关系,因为Form负责处理集合的生命周期。
这些定义并非UML中精确的技术。例如,我们可以区分继承(称为一般化)和接口继承(称为实现),但是说明却是遵循实用主义的。
回顾简单的堆栈表现
本文中的技术要求你通晓一些堆栈表现的知识。当我还是一名密歇根州大学(Michigan State University)大学生的时候,我就必须实现堆栈,在那个时候这是很困难的。初学编程的人可以从这儿的堆栈快速回顾中受益,在VB6中很容易使用集合实现堆栈,并且非常有用。
堆栈是一种遵循后进先出(LIFO)规则的数据结构,它受到两种基本的操作(push和pop)。给堆栈添加内容的时候必须使用push操作,删除内容的时候使用pop操作。调用push添加的最后的内容在调用pop的时候会被最先取出。例如,如果你有一个整数堆栈,并且压入了1、2、3、4、5,那么5次调用pop的时候会得到5、4、3、2、1。因此,堆栈是用于向后跟踪的很好的数据结构,有趣的是,它也是你的CPU跟踪运行的代码路径的基本原理。
但是,集合要成为堆栈仅仅依赖于你插入和删除内容的位置。通过在集合的末尾插入和删除内容,设计堆栈只需要少量的代码。例如,用collection.Add实现push调用,用collection.Remove(collection.Count)实现pop调用。简单地把集合的尾部作为数据结构的头部高效地把集合转换成了简单的堆栈。你在列表2的PushStack和PopStack方法中可以看到例子。
最后,你还可以对本文的例子进行很好的修改。把集合、PushStack和PopStack移动到一个Stack类可以使窗体更加整洁,并且建立了一个良好的通用堆栈类。采用这种方式改善代码的设计称为重构(refactoring)。学习代码重构是学习模式的良好补充。
工具的品质
在本文中你学习了一种工具--命令行为模式。如果发现被调用的行为需要被撤销,或者使用的代码过于复杂,那么我希望你能够记起这个简单但是功能强大的工具,其实本文讨论的其它一些概念--UML、模式和重构本质上也是工具。
任何任务都不可能有完美的工具(有句谚语是这样说的"如果你只拥有锤子,那么一切问题都是钉子")。我的父亲常说工匠通常是由于他们工具的品质而知名的。换句话说,你所拥有的工具越多,你能实现的功能也就越多。
Tags:
作者:佚名评论内容只代表网友观点,与本站立场无关!
评论摘要(共 0 条,得分 0 分,平均 0 分)
查看完整评论