はじめに
標準アプリなどでよく使われる「元に戻す」「やり直し」の実装において効率的な実装に迷ったことはないでしょうか。
正解はないと思いますが私が利用しているベースを紹介します。
環境
VS2022 C# .Net Framework4.8
実装
Undo Redoにおいて必要なのは実行した後の記憶になります。そのため私はメメントパターンを利用することで効率よく実装できるのではと考えました。
というわけでメメントの実装をしましょう。
IMementoOriginator.cs
namespace UndoRedoManager
{
/// <summary>
/// 発信者であること提供します
/// </summary>
/// <typeparam name="TMemento">メメントクラス</typeparam>
public interface IMementoOriginator<TMemento>
{
/// <summary>
/// メメント(記録)を取得します。
/// </summary>
/// <remarks>参照型の場合必ずディープコピーを行ってください</remarks>
/// <returns>記録</returns>
TMemento CreateMemento();
/// <summary>
/// メメントを現在の値にセットします。
/// </summary>
/// <param name="memento"></param>
void SetMemento(TMemento memento);
}
}
上記コードでは記憶の生成と記憶の呼出の実装を定義します。
「TMemento」はジェネリクスとして記憶用のクラスを設定します。
一点だけであれば「int」や「string」などでも良いですが複数記憶する場合には構造体やクラスなどを専用に作成するのが良いでしょう。
「CreateMemento」メソッドは現在状態の記憶を作成するときに呼び出すためのメソッドです。新しいインスタンスで「TMemento」型を返しましょう。
「SetMemento」メソットは引数の記憶を元に現在状態を更新をするためのメソットになります。
IUndoRedoble.cs
一旦UndoRedoの実装に必要な最低限必要なものを定義しておきます。
namespace UndoRedoManager
{
public interface IUndoRedoble
{
/// <summary>
/// 巻き戻し可能
/// </summary>
bool UseUndo { get; }
/// <summary>
/// やり直し可能
/// </summary>
bool UseRedo {get; }
/// <summary>
/// 元に戻します。
/// </summary>
void Undo();
/// <summary>
/// やり直しをします。
/// </summary>
void Redo();
/// <summary>
/// 履歴をリセットします。
/// </summary>
void ResetUndoRedo();
}
}
UndoRedoManagerLimitHistory.cs
記憶を管理するためのUndoRedoクラスを作成します。
using System.Collections.Generic;
namespace UndoRedoManager
{
public class UndoRedoManagerLimitHistory<TMemento>:IUndoRedoble
{
/// <summary>
///
/// </summary>
/// <param name="originator">発信者</param>
public UndoRedoManagerLimitHistory(IMementoOriginator<TMemento> originator, int historyMax)
{
this._originator = originator;
this._undoRedoList = new List<TMemento>();
this._nowMemento = this._originator.CreateMemento();
this._undoRedoList .Add(this._nowMemento);
this._nowIndex = 0;
this._historyMax = historyMax;
}
protected bool _isMementoSettingProcessing;
protected bool _isAddable = true;
protected int _historyMax;
protected int _nowIndex;
protected IMementoOriginator<TMemento> _originator;
protected TMemento _nowMemento;
protected List<TMemento> _undoRedoList;
/// <summary>
/// Undo可能か
/// </summary>
public bool UseUndo
{
get
{
return this._nowIndex >0;
}
}
/// <summary>
/// Redo可能か
/// </summary>
public bool UseRedo
{
get
{
return this._nowIndex != this._undoRedoList .Count-1 ;
}
}
public void SetUndoMemento()
{
if (!this._isMementoSettingProcessing && this._isAddable)
{
if (this._nowIndex != -1)
{
while (this._nowIndex != this._undoRedoList.Count - 1)
{
this._undoRedoList.RemoveAt(this._undoRedoList.Count - 1);
}
}
this._nowIndex++;
this._nowMemento = this._originator.CreateMemento();
this._undoRedoList.Add(this._nowMemento);
if (this._historyMax > 0)
{
if (this._nowIndex >= this._historyMax)
{
this._nowIndex--;
this._undoRedoList.RemoveAt(0);
}
}
}
}
public void Undo()
{
this._isMementoSettingProcessing = true;
this._nowIndex--;
this._nowMemento = this._undoRedoList[this._nowIndex];
this._originator.SetMemento (this._nowMemento);
this._isMementoSettingProcessing = false;
}
public void Redo()
{
this._isMementoSettingProcessing = true;
this._nowIndex++;
this._nowMemento = this._undoRedoList[this._nowIndex];
this._originator.SetMemento(this._nowMemento);
this._isMementoSettingProcessing = false;
}
public void ResetUndoRedo()
{
this._undoRedoList.Clear();
this._undoRedoList.Add(this._nowMemento);
this._nowIndex = 0;
}
}
}
public UndoRedoManagerLimitHistory(IMementoOriginator<TMemento> originator, int historyMax)
{
this._originator = originator;
this._undoRedoList = new List<TMemento>();
this._nowMemento = this._originator.CreateMemento();
this._undoRedoList .Add(this._nowMemento);
this._nowIndex = 0;
this._historyMax = historyMax;
}
コンストラクタの引数では発信者の登録と記憶量を指定します。
今回は上限を指定するためRemoveとAddが楽な「List」を記憶の保存領域として選定します。
保存領域作成後は現在の状態をリストの先頭に登録するよう指定します。
また、リストのどの位置の記憶を現在利用しているかを登録しておきます。
public void SetUndoMemento()
{
if (!this._isMementoSettingProcessing && this._isAddable)
{
if (this._nowIndex != -1)
{
while (this._nowIndex != this._undoRedoList.Count - 1)
{
this._undoRedoList.RemoveAt(this._undoRedoList.Count - 1);
}
}
this._nowIndex++;
this._nowMemento = this._originator.CreateMemento();
this._undoRedoList.Add(this._nowMemento);
if (this._historyMax > 0)
{
if (this._nowIndex >= this._historyMax)
{
this._nowIndex--;
this._undoRedoList.RemoveAt(0);
}
}
}
}
「SetUndoMemento」メソットでは記憶が変更されたときに呼び出されることを想定しています。
初回の処理では元に戻すをすでに行なっていてやり直すことができるようになっていた場合、やり直し出来るものを削除します。
次の処理では現在のインデックスを更新をし、現状記憶の取得とリストへの登録を行います。
最後の処理では指定している記憶容量よりも大きい場合に限り、一番古い記憶の削除を行います。
コード
使用例
適当なアプリでの実装例です。
namespace Tester
{
public partial class Form1 : Form, IMementoOriginator<Memento>
{
public Form1()
{
InitializeComponent();
this._undoredo=new UndoRedoManagerLimitHistory<Memento>(this,3);
this.SetBtnEnable();
}
IUndoRedoble _undoredo;
public Memento CreateMemento()
{
return new Memento(this.textBox1.Text);
}
public void SetMemento(Memento memento)
{
this.textBox1.Text = memento.Value;
this.SetBtnEnable();
}
public void SetBtnEnable()
{
this.BtnRedo.Enabled = this._undoredo.UseRedo;
this.BtnUndo.Enabled = this._undoredo.UseUndo;
}
private void BtnConfirm_Click(object sender, EventArgs e)
{
(this._undoredo as UndoRedoManagerLimitHistory<Memento>).SetUndoMemento();
this.SetBtnEnable();
}
private void BtnUndo_Click(object sender, EventArgs e)
{
this._undoredo.Undo();
this.SetBtnEnable();
}
private void BtnRedo_Click(object sender, EventArgs e)
{
this._undoredo.Redo();
this.SetBtnEnable();
}
}
public class Memento
{
public Memento(string val)
{
this.Value = val;
}
public string Value { get;private set; }
}
}