for WPF developers
Home Profile Tips 全記事一覧

元に戻す/やり直し機能を実装する

(2016/12/03 0:23:15 created.)

それでは本格的に機能を実装していきましょう。まずは根幹となる History クラスを次のように定義します。元に戻す操作とやり直す操作は表裏一体なので、ひとつのクラスにまとめておくとわかりやすくなります。

Program.cs
  1. namespace Tips_Undo
  2. {
  3.     using System;
  4.  
  5.     /// <summary>
  6.     /// 特定のアクションを保持し、任意のタイミングで実行するためのクラスです。
  7.     /// </summary>
  8.     public class History
  9.     {
  10.         /// <summary>
  11.         /// 新しいインスタンスを生成します。
  12.         /// </summary>
  13.         /// <param name="undoAction">アンドゥアクションを指定します。</param>
  14.         /// <param name="undoAction">リドゥアクションを指定します。</param>
  15.         /// <param name="name">操作名を指定します。</param>
  16.         public History(Action undoAction, Action redoAction, string name)
  17.         {
  18.             if (undoAction == null)
  19.                 throw new ArgumentNullException("必ずアンドゥアクションを指定してください。");
  20.             if (redoAction == null)
  21.                 throw new ArgumentNullException("必ずリドゥアクションを指定してください。");
  22.             this._undoAction = undoAction;
  23.             this._redoAction = redoAction;
  24.             this.Name = name;
  25.         }
  26.  
  27.         /// <summary>
  28.         /// アンドゥアクションを保持します。
  29.         /// </summary>
  30.         private Action _undoAction;
  31.  
  32.         /// <summary>
  33.         /// リドゥアクションを保持します。
  34.         /// </summary>
  35.         private Action _redoAction;
  36.  
  37.         /// <summary>
  38.         /// 保持しているアンドゥアクションを実行します。
  39.         /// </summary>
  40.         public void UnDo()
  41.         {
  42.             this._undoAction();
  43.         }
  44.  
  45.         /// <summary>
  46.         /// 保持しているリドゥアクションを実行します。
  47.         /// </summary>
  48.         public void ReDo()
  49.         {
  50.             this._redoAction();
  51.         }
  52.  
  53.         /// <summary>
  54.         /// 操作名を取得します。
  55.         /// </summary>
  56.         public string Name { get; private set; }
  57.  
  58.         /// <summary>
  59.         /// 自身を文字列として表現します。
  60.         /// </summary>
  61.         /// <returns>自身を表現する文字列を返します。</returns>
  62.         public override string ToString()
  63.         {
  64.             return this.Name;
  65.         }
  66.     }
  67. }

このクラスを利用して、例えば WPF アプリケーションプロジェクトの ViewModel で機能を実現する場合は次のようなコードになります。

何か操作をするときは、コードの最後にある DoAction() メソッドを使用します。このメソッドの入力引数には、元に戻すときの操作と、やり直すときの操作を Action デリゲートとして与えます。また、操作名を指定することで、操作履歴のリストに表示することができます。

HistoryViewModel.cs
  1. namespace Tips_Undo.ViewModels
  2. {
  3.     using System;
  4.     using System.Collections.Generic;
  5.     using System.Linq;
  6.     using ObjectEditorSample.Models;
  7.     using YKToolkit.Bindings;
  8.  
  9.     public class HistoryViewModel : NotificationObject
  10.     {
  11.         #region シングルトンクラス
  12.         /// <summary>
  13.         /// インスタンスを保持します。
  14.         /// </summary>
  15.         private static readonly HistoryViewModel _instance;
  16.  
  17.         /// <summary>
  18.         /// インスタンスを取得します。
  19.         /// </summary>
  20.         public static HistoryViewModel Instance { get { return _instance; } }
  21.  
  22.         /// <summary>
  23.         /// 静的なコンストラクタです。
  24.         /// </summary>
  25.         static HistoryViewModel()
  26.         {
  27.             _instance = new HistoryViewModel();
  28.         }
  29.  
  30.         /// <summary>
  31.         /// private なコンストラクタを定義することで外部からインスタンスを生成されることを抑制します。
  32.         /// </summary>
  33.         private HistoryViewModel()
  34.         {
  35.         }
  36.         #endregion シングルトンクラス
  37.  
  38.         private int _stackCapacity = 2;
  39.         /// <summary>
  40.         /// 操作履歴のバッファサイズを取得または設定します。
  41.         /// </summary>
  42.         public int StackCapacity
  43.         {
  44.             get { return this._stackCapacity; }
  45.             set
  46.             {
  47.                 if (SetProperty(ref this._stackCapacity, value))
  48.                 {
  49.                     this.UndoStack = new Stack<History>(this._stackCapacity);
  50.                     this.RedoStack = new Stack<History>(this._stackCapacity);
  51.                 }
  52.             }
  53.         }
  54.  
  55.         public void ClearHistory()
  56.         {
  57.             this.UndoStack.Clear();
  58.             this.RedoStack.Clear();
  59.             RaisePropertyChanged("UndoElements");
  60.             RaisePropertyChanged("RedoElements");
  61.         }
  62.  
  63.         private Stack<History> _undoStack;
  64.         /// <summary>
  65.         /// アンドゥする操作を溜めておくスタックを取得します。
  66.         /// </summary>
  67.         private Stack<History> UndoStack
  68.         {
  69.             get { return _undoStack ?? (_undoStack = new Stack<History>(this.StackCapacity)); }
  70.             set { SetProperty(ref this._undoStack, value); }
  71.         }
  72.  
  73.         private Stack<History> _redoStack;
  74.         /// <summary>
  75.         /// リドゥする操作を溜めておくスタックを取得します。
  76.         /// </summary>
  77.         private Stack<History> RedoStack
  78.         {
  79.             get { return _redoStack ?? (_redoStack = new Stack<History>(this.StackCapacity)); }
  80.             set { SetProperty(ref this._redoStack, value); }
  81.         }
  82.  
  83.         /// <summary>
  84.         /// アンドゥ操作リストを取得します。
  85.         /// </summary>
  86.         public string[] UndoElements { get { return this.UndoStack.Select(x => x.Name).ToArray(); } }
  87.  
  88.         private bool _isChagnedFromUI = true;
  89.  
  90.         private int _selectedUndoElementIndex = -1;
  91.         public int SelectedUndoElementIndex
  92.         {
  93.             get { return this._selectedUndoElementIndex; }
  94.             set
  95.             {
  96.                 if (SetProperty(ref this._selectedUndoElementIndex, value))
  97.                 {
  98.                     if (this._isChagnedFromUI)
  99.                     {
  100.                         var undoCount = 1 + this._selectedUndoElementIndex;
  101.  
  102.                         for (var i = 0; i < undoCount; i++)
  103.                         {
  104.                             Undo();
  105.                         }
  106.  
  107.                         this._isChagnedFromUI = false;
  108.                         RaisePropertyChanged("UndoElements");
  109.                         RaisePropertyChanged("RedoElements");
  110.                         this._isChagnedFromUI = true;
  111.  
  112.                         System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() =>
  113.                         {
  114.                             this.SelectedUndoElementIndex = -1;
  115.                         }), System.Windows.Threading.DispatcherPriority.ApplicationIdle);
  116.                     }
  117.                 }
  118.             }
  119.         }
  120.  
  121.         /// <summary>
  122.         /// リドゥ操作リストを取得します。
  123.         /// </summary>
  124.         public string[] RedoElements { get { return this.RedoStack.Select(x => x.Name).ToArray(); } }
  125.  
  126.         private int _selectedRedoElementIndex = -1;
  127.         public int SelectedRedoElementIndex
  128.         {
  129.             get { return this._selectedRedoElementIndex; }
  130.             set
  131.             {
  132.                 if (SetProperty(ref this._selectedRedoElementIndex, value))
  133.                 {
  134.                     if (this._isChagnedFromUI)
  135.                     {
  136.                         var redoCount = 1 + this._selectedRedoElementIndex;
  137.  
  138.                         for (var i = 0; i < redoCount; i++)
  139.                         {
  140.                             Redo();
  141.                         }
  142.  
  143.                         this._isChagnedFromUI = false;
  144.                         RaisePropertyChanged("UndoElements");
  145.                         RaisePropertyChanged("RedoElements");
  146.                         this._isChagnedFromUI = true;
  147.  
  148.                         this.SelectedRedoElementIndex = -1;
  149.                     }
  150.                 }
  151.             }
  152.         }
  153.  
  154.         private void Undo()
  155.         {
  156.             var action = this.UndoStack.Pop();
  157.             action.UnDo();
  158.             this.RedoStack.Push(action);
  159.         }
  160.  
  161.         private void Redo()
  162.         {
  163.             var action = this.RedoStack.Pop();
  164.             action.ReDo();
  165.             this.UndoStack.Push(action);
  166.         }
  167.  
  168.         private DelegateCommand _undoCommand;
  169.         /// <summary>
  170.         /// 元に戻すコマンドを取得します。
  171.         /// </summary>
  172.         public DelegateCommand UndoCommand
  173.         {
  174.             get
  175.             {
  176.                 return _undoCommand ?? (_undoCommand = new DelegateCommand(
  177.                 _ =>
  178.                 {
  179.                     Undo();
  180.  
  181.                     this._isChagnedFromUI = false;
  182.                     RaisePropertyChanged("UndoElements");
  183.                     RaisePropertyChanged("RedoElements");
  184.                     this._isChagnedFromUI = true;
  185.                 },
  186.                 _ => this.UndoStack.Count > 0));
  187.             }
  188.         }
  189.  
  190.         private DelegateCommand _redoCommand;
  191.         /// <summary>
  192.         /// やり直しコマンドを取得します。
  193.         /// </summary>
  194.         public DelegateCommand RedoCommand
  195.         {
  196.             get
  197.             {
  198.                 return _redoCommand ?? (_redoCommand = new DelegateCommand(
  199.                 _ =>
  200.                 {
  201.                     Redo();
  202.  
  203.                     this._isChagnedFromUI = false;
  204.                     RaisePropertyChanged("UndoElements");
  205.                     RaisePropertyChanged("RedoElements");
  206.                     this._isChagnedFromUI = true;
  207.                 },
  208.                 _ => this.RedoStack.Count > 0));
  209.             }
  210.         }
  211.  
  212.         /// <summary>
  213.         /// 指定された操作をおこない、操作履歴に手順を登録します。
  214.         /// </summary>
  215.         /// <param name="undoAction">元に戻す操作を指定します。</param>
  216.         /// <param name="redoAction">やり直す操作を指定します。</param>
  217.         /// <param name="name">操作名を指定します。</param>
  218.         public void DoAction(Action undoAction, Action redoAction, string name)
  219.         {
  220.             redoAction();
  221.  
  222.             var history = new History(undoAction, redoAction, name);
  223.             this.UndoStack.Push(history);
  224.             this.RedoStack.Clear();
  225.  
  226.             this._isChagnedFromUI = false;
  227.             RaisePropertyChanged("UndoElements");
  228.             RaisePropertyChanged("RedoElements");
  229.             this._isChagnedFromUI = true;
  230.         }
  231.     }
  232. }