2012-04-26 74 views
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 
    } 
} 

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

+4

这是太多的代码堆栈溢出后 – debracey 2012-04-26 23:29:48

+0

除了这是太多的代码,它读取像一个大块的文本与一堆代码塞入英寸你可以在这里使用分段符,所以你的文本不是一个大块 - 它使阅读和理解变得更容易。 (你可以在下面的地方立即预览你的帖子,在那里你用几乎所见即所得的方式来书写它,所以你可以在提交问题之前看到它是如何出现的。)我试图浏览大量的文本,但它们太难了读书。请编辑您的问题,正确编写段落,并将其缩减为*最少*可能的内容。)谢谢。 – 2012-04-26 23:33:55

+3

对于它的价值,通常使用[Command pattern](http://en.wikipedia.org/wiki/Command_pattern)来实现撤销/重做。不是将整个状态存储在撤消堆栈中,而是存储导致此状态的操作。 – 2012-04-26 23:57:17

回答

3

有很多方法可以工作,每种方法都有不同的长处和短处,但我通常喜欢定义一个抽象的Action类,然后是一个单独的UndoRedoStack类。

Action类将有两个方法(Do和Undo),每个子类Action可以实现。你将任何可以“改变状态”的逻辑都分离到这些Action子类中,从而保持整个逻辑的封装。

UndoRedoStack就像一个普通的堆栈,除了有三个核心方法。

  1. ApplyAction(如推送)
  2. UndoAction(喜欢流行音乐,但请务必只 移动指针/索引,而不截断或丢弃任何 现有操作)。
  3. 重做动作(与推,但您使用下一个值 已在底层的堆栈/列表中,而不是推/插入一个新的 )。

通常我觉得有个最大的挑战就是以这样的方式设计每个Action的子类,它保持足够的信息来都撤消和重做本身。但是能够将所有状态操作逻辑封装到单个Action子类中通常会使我从长远来看最容易维护。

+1

使用委托对于为每个不同的动作实现Action的子类是一个很好的选择(请参阅[本文]](http://tommulgrew.pixelati.com/2011/04/23/implementing-doundo-support-in-net-applications -with-lambda-expressions /)获取详细信息) – 2012-04-27 00:01:08

0

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