0

这是我的第一个 C# 应用程序,完全自学,没有任何软件编程背景。我对撤消/重做进行了一些研究,但找不到任何有用的东西(或易于理解)。因此,我希望有人可以帮助我为我的程序(winforms 应用程序)设计撤消/重做功能。该应用程序由一个主窗体组成,其中将调用后续子窗体以在某些事件(按钮单击等)期间记录用户指定的值。处理完每个事件后,将在缓冲区中绘制一个位图,然后在主窗体的 OnPaint 事件期间将其加载到主窗体内的图片框。每个输入都分离成自定义类对象并添加到单独的 List 和 BindingList 中。List 中包含的对象用于图形(指示坐标等),而 BindingList 中的对象用于在 DataGridView 上显示一些重要值。只是为了给出一个简短的描述,代码看起来像这样:

public partial class MainForm : form
{
    public class DataClass_1
    {
        public double a { get; set; }
        public double b { get; set; }
        public SubDataClass_1 { get; set; }
    }

    public class SubDataClass_1
    {
        public double x { get; set; }
        public double y { get; set; }
        public string SomeString { get; set; }
        public CustomEnum Enum_SubDataClass_1 { get; set; }
    }

    public class DisplayDataClass
    {
        public string SomeString { get; set; }
        public double e { get; set; }
        public double f { get; set; }
    }

    public enum CustomEnum { Enum1, Enum2, Enum3 };

    // Lists that contain objects which hold the necessary values to be drawn and displayed
    public List<DataClass_1> List_1 = new List<DataClass_1>();
    public List<DataClass_2> List_2 = new List<DataClass_2>(); // Object has similar data types as DataClass_1
    public BindingList<DisplayDataClass> DisplayList = new BindingList<DisplayDataClass>();

    Bitmap buffer;

    public MainForm()
    {
        InitializeComponent();

        dgv.DataSource = DisplayList;
    }

    private void DrawObject_1()
    {
        // some drawing codes here
    }

    private void DrawObject_2()
    {
        // some drawing codes here
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        DrawObject_1();
        DrawObject_2();
        pictureBox1.Image = buffer;
    }

    // Event to get input
    private void action_button_click(object sender, EventArgs e)
    {
        ChildForm form = new ChildForm(this);
        form.ShowDialog();
        Invalidate();
    }
}

子表单的代码如下所示:

public partial class ChildForm : form
{
    public ChildForm(MainForm MainForm)
    {
        InitializeComponent();

        // Do something
    }

    private void ok_button_click(object sender, EventArgs e)
    {
        DataClass_1 Data_1 = new DataClass_1();
        DisplayDataClass DisplayData = new DisplayDataClass();

        // Parsing, calculations, set values to Data_1 and DisplayData

        MainForm.List_1.Add(Data_1);
        MainForm.DisplayList.Add(DisplayData);

        this.DialogResult = System.Windows.Forms.DialogResult.OK;
        this.Close();
    }
}

由于所有必要的数据都存储在列表中,并且只有在触发某些事件(主要是按钮单击)后才会更改,因此我尝试使用这些列表来确定运行时应用程序的状态。我实现撤消/重做功能的方法是添加以下代码:

public partial class MainForm : form
{
    public class State()
    {
        public List<DataClass_1> List_1 { get; set; }
        public List<DataClass_2> List_2 { get; set; }
        public BindingList<DisplayDataClass> DisplayList { get; set; }
        // and so on

        public State()
        {
            List_1 = new List<DataClass_1>();
            List_2 = new List<DataClass_2>();
            DisplayList = new BindingList<DisplayDataClass>();
        }
    }

    State currentState = new State();
    Stack<State> undoStack = new Stack<State>();
    Stack<State> redoStack = new Stack<State>();

    private void MainForm_Shown(object sender, EventArgs e)
    {
        // Saves original state as first item in undoStack
        undoStack.Push(currentState);            
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        // Update lists from currentState before drawing
        List_1 = new List<DataClass_1>(currentState.List_1);
        List_2 = new List<DataClass_2>(currentState.List_2);
        DisplayList = new BindingList<DisplayDataClass>(currentState.DisplayList);
    }

    // When undo button is clicked
    private void undo_button_Click(object sender, EventArgs e)
    {
        if (undoStack.Count > 0)
        {
            redoStack.Push(currentState);
            undoStack.Pop();
            currentState = undoStack.Last();
            Invalidate();
        }
    }

    // When redo button is clicked
    private void redo_button_Click(object sender, EventArgs e)
    {
        // Have not thought about this yet, trying to get undo done first
    }

    // Events that trigger changes to values held by data objects
    private void action_button_Click(object sender, EventArgs e)
    {
        // Replace the following code with previously stated version         
        if (form.ShowDialog() == System.Windows.Forms.DialogResult.OK)
        {
            ChildForm form = new ChildForm(this)
            UpdateState();
            undoStack.Push(currentState);
            Invalidate();
        }
    }

    // To update currentState to current values
    private void UpdateState()
    {
        currentState.List_1 = new List<DataClass_1>(List_1);
        currentState.List_2 = new List<DataClass_2>(List_2);
        currentState.DisplayList = new BindingList<DisplayDataClass>(DisplayList);
        // and so on
    }
}

结果: 应用程序未正确执行撤消功能。程序在正常情况下显示正确的输出,但是当触发撤消事件时,无论绘制了多少对象,应用程序都会恢复到初始状态(没有记录数据的状态)。在更改堆栈以检查 undoStack 中的计数数的事件期间,我使用了 System.Diagnostics.Debug.WriteLine() 并且它似乎给出了正确的计数。我猜这些列表需要以不同的方式复制/克隆?还是我在这里做错了什么?谁能指导我?无需考虑性能、可读性、资源管理、未来维护等。

4

2 回答 2

3

有很多方法可以使用,每种方法都有不同的优点和缺点,但我通常喜欢定义一个抽象的 Action 类,然后定义一个单独的 UndoRedoStack 类。

Action 类将有两个方法(Do 和 Undo),每个子类 Action 都可以实现。您将任何可以“更改状态”的逻辑隔离到这些 Action 子类,从而保持该逻辑的简洁封装。

UndoRedoStack 与常规堆栈类似,但具有三个核心方法。

  1. ApplyAction(如 Push)
  2. UndoAction(类似于 Pop,但确保只移动指针/索引而不截断或丢弃任何现有操作)。
  3. RedoAction(类似于 Push,但您使用已经在底层堆栈/列表中的下一个值,而不是推送/插入一个新值)。

通常我发现最大的挑战是设计每个 Action 子类,使其保持足够的信息来撤消和重做自身。但是,能够将所有状态操作逻辑封装到各个 Action 子类中,通常会使我在长期内更容易维护。

于 2012-04-26T23:58:37.507 回答
0

您将参考对象存储在堆栈中。如果你想让你的方法工作,你需要在你的状态对象中实现一个 clone() 方法,并且每次都存储一个新的克隆,否则,所做的更改是对堆栈的每个成员进行的,因为它们都指向同一个参考对象。

于 2012-09-26T22:50:06.263 回答