C# で Undo/Redo
C++ Undo 入門 にあるコードを移植しただけ
使用例
HistoryTest.cs
using System; using System.Collections.Generic; using System.Text; namespace csharp_undo_sample { static class HistoryTest { class CHistoryEvent_string_push_back : HistoryEvent { StringBuilder m_string; char m_c; void push_back( char c ){ m_string.Append( c ); } void pop_back(){ if( m_string.Length > 0 ){ m_string.Remove( m_string.Length - 1, 1 ); } } void undo(){ var string_prev = m_string.ToString(); pop_back(); System.Diagnostics.Trace.WriteLine( string.Format( "undo: \"{0}\" → \"{1}\"", string_prev, m_string.ToString())); } void redo(){ var string_prev = m_string.ToString(); push_back( m_c ); System.Diagnostics.Trace.WriteLine( string.Format( "redo: \"{0}\" → \"{1}\"", string_prev.ToString(), m_string.ToString())); } void undo_begin(){ System.Diagnostics.Trace.WriteLine( "undo:{" ); } void undo_end(){ System.Diagnostics.Trace.WriteLine( "undo:}" ); } void redo_begin(){ System.Diagnostics.Trace.WriteLine( "redo:{" ); } void redo_end(){ System.Diagnostics.Trace.WriteLine( "undo:}" ); } protected override bool OnUndo( uint dwId ){ undo(); return true; } protected override bool OnRedo( uint dwId ){ redo(); return true; } protected override bool OnUndoBegin( uint dwId ){ undo_begin(); return true; } protected override bool OnUndoEnd( uint dwId ){ undo_end(); return true; } protected override bool OnRedoBegin( uint dwId ){ redo_begin(); return true; } protected override bool OnRedoEnd( uint dwId ){ redo_end(); return true; } protected override void OnDispose(){} public CHistoryEvent_string_push_back( StringBuilder s, char c ):base(0){ m_string = s; m_c = c; } } static void string_push_back( History history, StringBuilder s, char c ){ history.AddEvent( new CHistoryEvent_string_push_back( s, c )); } static void output_periods( History history ){ var undo_periods = history.GetUndoPeriods(); var redo_periods = history.GetRedoPeriods(); System.Diagnostics.Trace.WriteLine( "{" ); foreach( var period_id in undo_periods ){ var period_name = history.GetPeriodName( period_id ); System.Diagnostics.Trace.WriteLine( string.Format( " {0}", period_name )); } System.Diagnostics.Trace.WriteLine( "---" ); foreach( var period_id in redo_periods ){ var period_name = history.GetPeriodName( period_id ); System.Diagnostics.Trace.WriteLine( string.Format( " {0}", period_name )); } System.Diagnostics.Trace.WriteLine( "}" ); } public static int test(){ var s = new StringBuilder(); var history = new History(); history.BeginPeriod( "[0] ABC" ); string_push_back( history, s, 'A' ); string_push_back( history, s, 'B' ); string_push_back( history, s, 'C' ); history.EndPeriod(); System.Diagnostics.Trace.WriteLine( string.Format( "s = \"{0}\"", s.ToString())); history.BeginPeriod( "[1] 123" ); string_push_back( history, s, '1' ); string_push_back( history, s, '2' ); string_push_back( history, s, '3' ); history.EndPeriod(); System.Diagnostics.Trace.WriteLine( string.Format( "s = \"{0}\"", s.ToString())); history.BeginPeriod( "[2] D" ); string_push_back( history, s, 'D' ); history.EndPeriod(); System.Diagnostics.Trace.WriteLine( string.Format( "s = \"{0}\"", s.ToString())); output_periods( history ); history.Undo(); System.Diagnostics.Trace.WriteLine( string.Format( "s = \"{0}\"", s.ToString())); output_periods( history ); history.Undo(); System.Diagnostics.Trace.WriteLine( string.Format( "s = \"{0}\"", s.ToString())); output_periods( history ); history.Undo(); System.Diagnostics.Trace.WriteLine( string.Format( "s = \"{0}\"", s.ToString())); output_periods( history ); history.BeginPeriod( "[3] E" ); string_push_back( history, s, 'E' ); history.EndPeriod(); System.Diagnostics.Trace.WriteLine( string.Format( "s = \"{0}\"", s.ToString())); output_periods( history ); return 0; } } }
出力
redo: "" → "A" redo: "A" → "AB" redo: "AB" → "ABC" s = "ABC" redo: "ABC" → "ABC1" redo: "ABC1" → "ABC12" redo: "ABC12" → "ABC123" s = "ABC123" redo: "ABC123" → "ABC123D" s = "ABC123D" { [2] D [1] 123 [0] ABC --- } undo:{ undo: "ABC123D" → "ABC123" undo:} s = "ABC123" { [1] 123 [0] ABC --- [2] D } undo:{ undo: "ABC123" → "ABC12" undo: "ABC12" → "ABC1" undo: "ABC1" → "ABC" undo:} s = "ABC" { [0] ABC --- [1] 123 [2] D } undo:{ undo: "ABC" → "AB" undo: "AB" → "A" undo: "A" → "" undo:} s = "" { --- [0] ABC [1] 123 [2] D } redo: "" → "E" s = "E" { [3] E --- }
実装
HistoryMessage.cs
using System; namespace csharp_undo_sample { enum HistoryMessage { HISTORY_DETACHED, HISTORY_ATTACHED, HISTORY_UNDO, HISTORY_REDO, HISTORY_UNDO_BEGIN, HISTORY_UNDO_END, HISTORY_REDO_BEGIN, HISTORY_REDO_END, HISTORY_ENABLED } }
HistoryEvent.cs
using System; namespace csharp_undo_sample { interface IHistoryEvent : IDisposable { bool Invoke( uint dwId, HistoryMessage uMsg ); } class HistoryEvent : Disposable, IHistoryEvent { public event EventHandler Undo = delegate{}; public event EventHandler Redo = delegate{}; public long RequestId = 0; public HistoryEvent( long requestId ){ RequestId = requestId; } protected virtual bool OnDetach( uint dwId ){ return true; } protected virtual bool OnAttach( uint dwId ){ return true; } protected virtual bool OnUndo( uint dwId ){ #if DEBUG System.Diagnostics.Trace.WriteLine( string.Format( "{0}_Undo", GetType().Name )); #endif // DEBUG Undo( this, EventArgs.Empty ); return true; } protected virtual bool OnRedo( uint dwId ){ #if DEBUG System.Diagnostics.Trace.WriteLine( string.Format( "{0}_Redo", GetType().Name )); #endif // DEBUG Redo( this, EventArgs.Empty ); return true; } protected virtual bool OnUndoBegin( uint dwId ){ return true; } protected virtual bool OnUndoEnd( uint dwId ){ return true; } protected virtual bool OnRedoBegin( uint dwId ){ return true; } protected virtual bool OnRedoEnd( uint dwId ){ return true; } protected virtual bool OnEnabled( uint dwId ){ return true; } public bool Invoke( uint dwId, HistoryMessage uMsg ){ var result = true; if( uMsg == HistoryMessage.HISTORY_DETACHED ){ result = OnDetach( dwId ); } else if( uMsg == HistoryMessage.HISTORY_ATTACHED ){ result = OnAttach( dwId ); } else if( uMsg == HistoryMessage.HISTORY_UNDO ){ result = OnUndo( dwId ); } else if( uMsg == HistoryMessage.HISTORY_REDO ){ result = OnRedo( dwId ); } else if( uMsg == HistoryMessage.HISTORY_UNDO_BEGIN ){ result = OnUndoBegin( dwId ); } else if( uMsg == HistoryMessage.HISTORY_UNDO_END ){ result = OnUndoEnd( dwId ); } else if( uMsg == HistoryMessage.HISTORY_REDO_BEGIN ){ result = OnRedoBegin( dwId ); } else if( uMsg == HistoryMessage.HISTORY_REDO_END ){ result = OnRedoEnd( dwId ); } else if( uMsg == HistoryMessage.HISTORY_ENABLED ){ result = OnEnabled( dwId ); } return result; } } }
History.cs
using System; using System.Collections.Generic; using System.Text; namespace csharp_undo_sample { class History { public const uint INVALID_EVENT_ID = uint.MaxValue; public const uint INVALID_PERIOD_ID = uint.MaxValue; public const uint MAXIMUM_PERIOD_LEVEL = uint.MaxValue; List<uint> m_redo = new List<uint>(); List<uint> m_undo = new List<uint>(); Dictionary<uint,HistoryItem> m_items = new Dictionary<uint,HistoryItem>(); Dictionary<uint,HistoryPeriod> m_periods = new Dictionary<uint,HistoryPeriod>(); uint m_period_id = INVALID_PERIOD_ID; uint m_period_level = 0; uint m_next_event = INVALID_EVENT_ID; uint m_saved_event = INVALID_EVENT_ID; uint m_current_event = INVALID_EVENT_ID; public bool Modified { get { return ( m_current_event != m_saved_event ); } } public void Save(){ m_saved_event = m_current_event; } void ClearRedo(){ foreach( var event_id in m_redo ){ var item = m_items[event_id]; m_periods.Remove( item.period_id()); m_items.Remove( event_id ); item.Detach(); } m_redo.Clear(); } public uint AddEvent( IHistoryEvent pHistoryEvent ){ uint result = INVALID_EVENT_ID; var item = new HistoryItem( m_period_id, m_period_level, ++m_next_event, pHistoryEvent ); m_items[item.id()] = item; m_periods[m_period_id].Add( item.id()); m_undo.Add( item.id()); m_current_event = m_next_event; ClearRedo(); if( item.Redo()){ result = m_next_event; } return result; } public void Clear(){ foreach( var item in m_items ){ item.Value.Detach(); } m_redo.Clear(); m_undo.Clear(); m_periods.Clear(); m_items.Clear(); m_period_id = INVALID_PERIOD_ID; m_period_level = 0; m_next_event = INVALID_EVENT_ID; m_saved_event = INVALID_EVENT_ID; m_current_event = INVALID_EVENT_ID; } public void DeleteEvent( uint event_id ){ if( m_items.ContainsKey( event_id )){ var item = m_items[event_id]; m_items.Remove( event_id ); m_periods[item.period_id()].Remove( event_id ); item.Detach(); } } public IHistoryEvent GetEvent( uint event_id ){ IHistoryEvent result = null; if( m_items.ContainsKey( event_id )){ result = m_items[event_id].GetEvent(); } return result; } public void BeginPeriod( string period_name ){ if( m_period_level > 0 ){ if( m_period_level + 1 >= MAXIMUM_PERIOD_LEVEL ){ throw new Exception("period level overflow"); } ++m_period_level; } else{ if( m_period_id + 1 == INVALID_PERIOD_ID ){ throw new Exception("period id overflow"); } ++m_period_level; ++m_period_id; m_periods.Add( m_period_id, new HistoryPeriod( m_period_id, period_name )); } } public void EndPeriod(){ if( m_period_level == 0 ){ throw new Exception("period level underflow"); } --m_period_level; } public void CancelPeriod(){ if( m_period_level == 0 ){ throw new Exception("period level underflow"); } while( m_undo.Count > 0 ){ var n = m_undo.Count-1; var event_id = m_undo[n]; var item = m_items[event_id]; if(( item.period_id() != m_period_id )||( item.period_level() != m_period_level )){ break; } item.Undo(); item.Detach(); m_undo.RemoveAt( n ); } m_periods.Remove( m_period_id ); --m_period_level; } public void Undo(){ if( m_undo.Count > 0 ){ var temp = new List<uint>(); do{ var n = m_undo.Count - 1; var event_id = m_undo[n]; temp.Add( event_id ); m_undo.RemoveAt( n ); }while(( m_undo.Count > 0 )&&( m_items[m_undo[m_undo.Count-1]].period_id() == m_items[temp[temp.Count-1]].period_id())); m_redo.AddRange( temp ); m_items[temp[0]].UndoBegin(); foreach( var event_id in temp ){ m_items[event_id].Undo(); } m_items[temp[temp.Count-1]].UndoEnd(); --m_current_event; } } public void Redo(){ if( m_redo.Count > 0 ){ var temp = new List<uint>(); do{ var n = m_redo.Count - 1; var event_id = m_redo[n]; temp.Add( event_id ); m_redo.RemoveAt( n ); }while(( m_redo.Count > 0 )&&( m_items[m_redo[m_redo.Count-1]].period_id() == m_items[temp[temp.Count-1]].period_id())); m_undo.AddRange( temp ); m_items[temp[0]].RedoBegin(); foreach( var event_id in temp ){ m_items[event_id].Redo(); } m_items[temp[temp.Count-1]].RedoEnd(); ++m_current_event; } } public bool IsUndoAvailable(){ return ( m_undo.Count > 0 ); } public bool IsRedoAvailable(){ return ( m_redo.Count > 0 ); } public uint GetUndoPeriodCount(){ return (uint)GetUndoPeriods().Count; } public uint GetRedoPeriodCount(){ return (uint)GetRedoPeriods().Count; } public List<uint> GetUndoPeriods(){ var result = new List<uint>(); var last_period_id = INVALID_PERIOD_ID; for( var n = 0; n < m_undo.Count; ++n ){ var m = m_undo.Count - n - 1; var event_id = m_undo[m]; var item = m_items[event_id]; if(( last_period_id == INVALID_PERIOD_ID )||( last_period_id != item.period_id())){ result.Add( item.period_id()); last_period_id = item.period_id(); } } return result; } public List<uint> GetRedoPeriods(){ var result = new List<uint>(); var last_period_id = INVALID_PERIOD_ID; for( var n = 0; n < m_redo.Count; ++n ){ var m = m_redo.Count - n - 1; var event_id = m_redo[m]; var item = m_items[event_id]; if(( last_period_id == INVALID_PERIOD_ID )||( last_period_id != item.period_id())){ result.Add( item.period_id()); last_period_id = item.period_id(); } } return result; } public List<uint> GetPeriods(){ var result = new List<uint>(); foreach( var kv in m_periods ){ result.Add( kv.Key ); } return result; } public string GetPeriodName( uint period_id ){ string result = null; if( m_periods.ContainsKey( period_id )){ result = m_periods[period_id].name(); } return result; } public bool IsPeriodEnabled( uint period_id ){ bool result = false; if( m_periods.ContainsKey( period_id )){ var period = m_periods[period_id]; for( var n = 0; n < period.Count; ++n ){ var event_id = period[n]; var item = m_items[event_id]; var pHistoryEvent = item.GetEvent(); if( pHistoryEvent != null ){ result = pHistoryEvent.Invoke( event_id, HistoryMessage.HISTORY_ENABLED ); if( !result ){ break; } } } } return result; } } }
HistoryItem.cs
using System; namespace csharp_undo_sample { class HistoryItem : Disposable { uint m_period_id = History.INVALID_PERIOD_ID; uint m_period_level = 0; uint m_id = History.INVALID_EVENT_ID; IHistoryEvent m_pHistoryEvent = null; bool SendMessage( HistoryMessage uMsg ){ bool result = true; if( m_pHistoryEvent != null ){ result = m_pHistoryEvent.Invoke( m_id, uMsg ); } return result; } protected override void OnDispose(){ Detach(); base.OnDispose(); } public void Detach(){ if( m_pHistoryEvent != null ){ SendMessage( HistoryMessage.HISTORY_DETACHED ); m_pHistoryEvent = null; } m_period_id = History.INVALID_PERIOD_ID; m_period_level = 0; m_id = History.INVALID_EVENT_ID; } public HistoryItem( uint period_id, uint period_level, uint id, IHistoryEvent pHistoryEvent ){ m_period_id = period_id; m_period_level = period_level; m_id = id; m_pHistoryEvent = pHistoryEvent; SendMessage( HistoryMessage.HISTORY_ATTACHED ); } public IHistoryEvent GetEvent(){ return m_pHistoryEvent; } public bool UndoBegin(){ return SendMessage( HistoryMessage.HISTORY_UNDO_BEGIN ); } public bool UndoEnd(){ return SendMessage( HistoryMessage.HISTORY_UNDO_END ); } public bool Undo(){ return SendMessage( HistoryMessage.HISTORY_UNDO ); } public bool RedoBegin(){ return SendMessage( HistoryMessage.HISTORY_REDO_BEGIN ); } public bool RedoEnd(){ return SendMessage( HistoryMessage.HISTORY_REDO_END ); } public bool Redo(){ return SendMessage( HistoryMessage.HISTORY_REDO ); } public bool IsEnabled(){ return SendMessage( HistoryMessage.HISTORY_ENABLED ); } public uint period_id(){ return m_period_id; } public uint period_level(){ return m_period_level; } public uint id(){ return m_id; } } }
HistoryPeriod.cs
using System; using System.Collections.Generic; using System.Text; namespace csharp_undo_sample { class HistoryPeriod : List<uint> { uint m_id = History.INVALID_PERIOD_ID; string m_name = null; public uint id(){ return m_id; } public string name(){ return m_name; } public HistoryPeriod( uint id, string name ):base(){ m_id = id; m_name = name; } } }
Disposable.cs
using System; namespace csharp_undo_sample { // public class Disposable : IDisposable { bool m_disposed = false; public bool IsDisposed { get { return m_disposed; } } public event EventHandler Disposed = delegate{}; ~Disposable(){ Dispose( false ); } // 概要: // System.Windows.Forms.ApplicationContext によって使用されているすべてのリソースを解放します。 public void Dispose(){ Dispose( true ); GC.SuppressFinalize( this ); } // // 概要: // System.Windows.Forms.ApplicationContext によって使用されているアンマネージ リソースを解放し、オプションでマネージ // リソースも解放します。 // // パラメーター: // disposing: // マネージ リソースとアンマネージ リソースの両方を解放する場合は true。アンマネージ リソースだけを解放する場合は false。 protected virtual void Dispose( bool disposing ){ if( !m_disposed ){ m_disposed = true; if( disposing ){ OnDisposing(); } Disposed( this, EventArgs.Empty ); OnDispose(); } } protected virtual void OnDisposing(){ // Disposeメソッドから呼ばれた。Managedリソースだけを解放せよ } protected virtual void OnDispose(){ // ファイナライザ(デストラクタ)または Disposeメソッドから呼ばれた。UnmanagedリソースとManagedリソースの両方を解放せよ } } }