一个简单的框架,用于添加撤销/重做支持
本文介绍了一个用于向 Windows Forms 应用程序添加撤销/重做支持的框架。
引言
本文介绍了一个用于向 Windows Forms 应用程序添加撤销/重做支持的简单框架。该框架由一小组类和接口组成,可帮助您管理撤销/重做功能的调用。当然,框架本身并不执行底层的撤销或重做功能。这是特定于应用程序的,您需要在扩展框架时提供。
框架
我想描述三个主要的类/接口。它们都包含在附加演示项目中的同一个源文件(UndoSupport.cs)和命名空间(UndoSupport
)中。
UndoCommand
:这是一个抽象类,表示一个可撤销或可重做的操作或命令。它提供了 virtual Undo()
和 Redo()
方法,您的派生命令类可以重写这些方法来执行底层的撤销/重做功能。
public abstract class UndoCommand : IUndoable
{
// Return a short description of the cmd
// that can be used to update the Text
// property of an undo or redo menu item.
public virtual string GetText()
{
// Empty string.
return "";
}
public virtual void Undo()
{
// Empty implementation.
}
public virtual void Redo()
{
// Empty implementation.
}
}
在继承自 UndoCommand
的类中,您也可以选择不重写 virtual Undo()
和 Redo()
方法。相反,您可以将派生命令类视为数据类,只需提供一个外部类(实现 IUndoHandler
接口的类,如下所述)可以使用其来执行实际撤销/重做功能的额外字段、属性或方法。
IUndoHandler
:这是一个可选接口,如果不想让特定的 UndoCommand
类自行执行底层的撤销/重做功能,您的应用程序类可以实现此接口。使用此接口可以使所有撤销/重做逻辑保留在一个类中(例如,实现 IUndoHandler
的类),而 UndoCommand
类仅用于存储执行撤销/重做所需的数据(例如应用程序状态的快照)。
public interface IUndoHandler
{
void Undo(UndoCommand cmd);
void Redo(UndoCommand cmd);
}
UndoManager
:这是框架中的主要类。当您在应用程序中执行操作时,会创建命令对象并将其添加到撤销管理器中。撤销管理器会为您处理何时调用撤销/重做功能。添加新命令时,您可以选择指定一个 IUndoHandler
来执行该命令的撤销/重做。可以将能够自行执行撤销/重做的命令对象与依赖 IUndoHandler
实现的命令对象混合使用。UndoManager
类设计为直接在撤销/重做菜单项事件处理程序和撤销/重做菜单项状态更新函数中使用(这使得实现标准的“编辑 | 撤销”和“编辑 | 重做”菜单项功能变得容易)。
public class MyForm : System.Windows.Forms.Form
{
...
private void OnEditUndoClick(object sender, System.EventArgs e)
{
// Perform undo.
m_undoManager.Undo();
}
}
供参考,以下是 UndoManager
类的公共接口
public class UndoManager
{
// Constructor which initializes the
// manager with up to 8 levels
// of undo/redo.
public UndoManager() {...}
// Property for the maximum undo level.
public int MaxUndoLevel {...}
// Register a new undo command. Use this method after your
// application has performed an operation/command that is
// undoable.
public void AddUndoCommand(UndoCommand cmd) {...}
// Register a new undo command along with an undo handler. The
// undo handler is used to perform the actual undo or redo
// operation later when requested.
public void AddUndoCommand(UndoCommand cmd,
IUndoHandler undoHandler) {...}
// Clear the internal undo/redo data structures. Use this method
// when your application performs an operation that cannot be undone.
// For example, when the user "saves" or "commits" all the changes in
// the application, or when a form is closed.
public void ClearUndoRedo() {...}
// Check if there is something to undo. Use this method to decide
// whether your application's "Undo" menu item should be enabled
// or disabled.
public bool CanUndo() {...}
// Check if there is something to redo. Use this method to decide
// whether your application's "Redo" menu item should be enabled
// or disabled.
public bool CanRedo() {...}
// Perform the undo operation. If an undo handler was specified, it
// will be used to perform the actual operation. Otherwise, the
// command instance is asked to perform the undo.
public void Undo() {...}
// Perform the redo operation. If an undo handler was specified, it
// will be used to perform the actual operation. Otherwise, the
// command instance is asked to perform the redo.
public void Redo() {...}
// Get the text value of the next undo command. Use this method
// to update the Text property of your "Undo" menu item if
// desired. For example, the text value for a command might be
// "Draw Circle". This allows you to change your menu item Text
// property to "&Undo Draw Circle".
public string GetUndoText() {...}
// Get the text value of the next redo command. Use this method
// to update the Text property of your "Redo" menu item if desired.
// For example, the text value for a command might be "Draw Line".
// This allows you to change your menu item text to "&Redo Draw Line".
public string GetRedoText() {...}
// Get the next (or newest) undo command. This is like a "Peek"
// method. It does not remove the command from the undo list.
public UndoCommand GetNextUndoCommand() {...}
// Get the next redo command. This is like a "Peek"
// method. It does not remove the command from the redo stack.
public UndoCommand GetNextRedoCommand() {...}
// Retrieve all of the undo commands. Useful for debugging,
// to analyze the contents of the undo list.
public UndoCommand[] GetUndoCommands() {...}
// Retrieve all of the redo commands. Useful for debugging,
// to analyze the contents of the redo stack.
public UndoCommand[] GetRedoCommands() {...}
}
TestUndo 应用程序
演示项目(TestUndo)展示了框架的实际应用。它是一个简单的 Windows 应用程序,只有一个窗体。所有撤销/重做框架代码都位于前面提到的 UndoSupport.cs 文件中。所有使用该框架的应用程序代码都包含在 MainForm.cs 文件中。下面是 TestUndo 应用程序的快照
MainForm 分为两个分组框部分。顶部部分是测试区域,提供了一个简单的 GUI,允许您执行一些可撤销的操作。这些操作包括将短字符串追加到多行显示文本框中。有三个按钮可以允许您添加特定字符串。与每个按钮关联的是一个不同的撤销命令类(继承自 UndoCommand
)。前两个命令类(AddABCCommand
和 Add123Command
)不实现自己的撤销/重做功能。它们依赖于 IUndoHandler
实现来执行实际的撤销/重做。在将这些类型的命令添加到撤销管理器时,必须指定 IUndoHandler
引用。
public class MainForm : System.Windows.Forms.Form, IUndoHandler
{
...
private void OnAddABC(object sender, System.EventArgs e)
{
// Create a new command that saves the "current state"
// of the display textbox before performing the AddABC
// operation.
AddABCCommand cmd = new AddABCCommand(m_displayTB.Text);
// Perform the AddABC operation.
m_displayTB.Text += "ABC ";
// Add the new command to the undo manager. We pass in
// "this" as the IUndoHandler.
m_undoManager.AddUndoCommand(cmd, this);
}
}
第三个命令类(AddXYZCommand
)确实实现了自己的撤销/重做功能。这就是为什么在它的构造函数中传递了显示文本框。AddXYZCommand
类需要访问显示文本框才能自行执行撤销/重做。
class AddXYZCommand : UndoCommand
{
private string m_beforeText;
private TextBox m_textBox;
public AddXYZCommand(string beforeText, TextBox textBox)
{
m_beforeText = beforeText;
m_textBox = textBox;
}
public override string GetText()
{
return "Add XYZ";
}
public override void Undo()
{
m_textBox.Text = m_beforeText;
}
public override void Redo()
{
m_textBox.Text += "XYZ ";
}
}
要撤销添加操作,MainForm 提供了一个主菜单,其中包含“编辑 | 撤销”和“编辑 | 重做”菜单项。您可以使用这些菜单项或它们的键盘快捷方式来执行这三种添加操作的撤销/重做。“清除”按钮清除显示文本框,并清除所有待定的撤销/重做命令(因为我已经将此特定操作视为不可撤销的操作)。当您执行添加、撤销或重做操作时,可以访问“编辑”菜单,并查看撤销和重做菜单项文本如何变化。例如,在按下“添加 ABC”按钮后,撤销菜单项可以显示“撤销 添加 ABC”,而不是简单地显示“撤销”。
public class MainForm : System.Windows.Forms.Form, IUndoHandler
{
...
private void OnEditMenuPopup(object sender, System.EventArgs e)
{
// Update the enabled state of the undo/redo menu items.
m_undoMenuItem.Enabled = m_undoManager.CanUndo();
m_redoMenuItem.Enabled = m_undoManager.CanRedo();
// Change the text of the menu items in order
// specify what operation to undo or redo.
m_undoMenuItem.Text = "&Undo";
if ( m_undoMenuItem.Enabled )
{
string undoText = m_undoManager.GetUndoText();
if ( undoText.Length > 0 )
{
m_undoMenuItem.Text += " " + undoText;
}
}
m_redoMenuItem.Text = "&Redo";
if ( m_redoMenuItem.Enabled )
{
string redoText = m_undoManager.GetRedoText();
if ( redoText.Length > 0 )
{
m_redoMenuItem.Text += " " + redoText;
}
}
}
}
MainForm 的底部部分显示了 UndoManager
类维护的数据结构中正在发生的事情。UndoManager
使用 ArrayList
来存储用于撤销的命令历史记录,并使用 Stack
来存储用于重做的命令。您可以看到命令对象在调用撤销或重做菜单项时在这两个数据结构之间移动。默认情况下,UndoManager
支持最多八个级别的撤销(意味着它可以回溯最多 8 条命令)。您可以使用 MainForm GUI 来测试最大撤销级别的不同值(但请注意,更改级别将导致清除现有的撤销/重做命令)。
基本上,这就是 TestUndo 应用程序的全部内容。我编写它(MainForm.cs)主要是为了说明如何使用 UndoManager
类,来执行其所有公共方法,并让读者相信内部撤销/重做状态得到了妥善管理(根据“标准”撤销/重做行为)。我的意图是创建一个简单的应用程序来演示以上内容,而不是专注于创建具有实际图形、剪切粘贴或文本编辑命令的真实应用程序。CodeProject 上还有其他关于此主题的文章,我鼓励您也去看看。
摘要
所提出的框架可以作为向您自己的 Windows Forms 应用程序添加撤销/重做支持的起点。使用框架最重要的部分是弄清楚如何将用户可撤销的操作划分为命令类,决定需要在这些类中存储哪些额外的字段,然后弄清楚如何实际撤销和重做这些操作。在简单的情况下,可以通过在执行操作之前保存某些应用程序变量的当前状态来实现撤销(这样在需要撤销时,只需恢复该存档状态)。在更复杂的情况下,例如当您的用户操作涉及数据库或数据结构操作时,您需要能够编写例程来反转(撤销)或重新应用(重做)这些操作。我认为关于如何实现此类命令的讨论是特定于应用程序的,超出了本文档的范围。
历史
- 2005 年 7 月 9 日
- 初始版本。
- 2005 年 7 月 10 日
- 根据 Marc Clifton 的初步反馈,对文章文本中的代码块进行了一些澄清,说明了测试应用程序的范围和目的。
- 2005 年 7 月 19 日
- 对文章文本中的代码块进行了少量更新。