ドット絵エディタを作る(Undo機能をつけるぞ編)

・・・う…うーん,飲みすぎ・・・
頭いたーい.(;´Д`)
わしゃ,酒(洋酒オンリーだけどね)が大好きじゃが,全く強くないのよねぇ・・・


が・・がんばるぞぉ…(何が?)


さっ.というわけでございまして,今日はいよいよUndo機能をつけようー!!
こういうエディタ系には必須だよね!!

じゃが,いかんせん作り方がわかんねーにゃー.
いや,わからないこともないんだけどね…
無限Undoの実装方法はわかんないね.
ちょっと調べたけど,「ザ・Undo」みたいなページはなかったにゃー(あるワケない)
でも,色々な方法がありそうな部分やね.
メモリを節約するために,差分だけ記録するとかぁ,データではなくメッセージみたいなのを記録するとかぁ・・・

とりあえずこんな仕様でつくってみようかね.

■ Undo仕様&設計方針 ■

 ・Undo回数は各ウィンドウごとに20回まで.
 ・Redo機能もつける.
 ・記録するデータは画像そのもの+α.


あ,全然仕様ってほどじゃなかった.(-_-;)

まぁ,今回は簡単に実装するために,画像データ本体を記録することにしました.
(256x256限定だし,そんなに膨大なデータにはならないんじゃないかなー)

と,それに+αして,幾つかのデータを一まとめにしてパックおきましょ.
例えば,画像のサイズとか,操作の種類を示すIDとか…

  type TUndoData = record
    bmp           : TBitmap ;
    width, height : Integer ;//(TBitmapが既にもってるから実際には必要なし)
    type          : Byte ;
  end;
で,このパックしたデータを記録するためのリストを作成.
今回は,リスト作成にTList を使用するのにゃ.


ところで TList ってなんだろにゃ?
よく聞くけど.
いかにもリストが作れそうなクラスじゃね.

C++のSTLのstd::listみたいなもんかね?

確かにリストを作るみたいだけど,以下のポイントがSLTのlistとは違うみたいだにゃー.

(TListの場合)
 ・リストで確保するのはポインタのみ.
 ・ポインタの参照を確保,開放するのはプログラマの責任(重要).
 ・ランダムアクセスが可能.


ふーむ.どっちかっていうと,リスト構造そのものっていうより,「ポインタのバンク(銀行)」みたいなもんかねぇ
生成したオブジェクトのポインタをTListに預けておいて,必要になったら,そのポインタを引き出す…
TListを使用することで,ポインタ自身をプログラマが管理しなくて良くなる…っていうようなノリ.

さて,Undo,Redo機能を実現するには,このUndoデータのリストだけじゃダメだね.
「現在リスト中のどの位置なのか」を示す変数がひとつ必要.
仮にこれを「UndoIndex」と呼ぶとすると,イメージ的にはこんな感じ.


Undoデータ追加時

UndoIndexの初期値は-1.
Undoデータ追加時に,UndoIndexをインクリメントして,そこに新しいUndoDataを追加.
このときに,データが多く(今回は20以上)になってたら,インデクス0のほうからデータを削除する.


Undo実行時


ユーザのUndo操作によってUndoを実行!
このとき,UndoIndexのところにデータがちゃんとあるかを調べておきましょ.
UndoIndex-1のところのUndoデータを画像(FBackBmp)として復帰し,
その後,UndoIndexをデクリメント.
(先にデクリメントしてから,そこを復帰してもOKだったね…)


Redo実行時

Redo実行もUndo実行と基本的に一緒.
ただし,UndoIndexをインクリメントってところが違うだけ.

とまぁ,コレを基本に,「データが多くなりすぎた場合は消す」とか「Undo中にデータを追加された場合は,UndoIndexより 先のデータは破棄する」なんかの操作を追加すればUndo・Redo機能の実装が完了!



さて,実際のプログラム的には,こんな感じ.

(実際のプログラムから一部抜粋)
{
  Undoデータを追加します.

  ・データ追加のタイミングは基本的にペンを上げた時です.(新規作成時も)
  ・Undoデータは最大数を超えないようにします.
  ・Undoデータ追加じは,FUndoIndex にセットされる値は,
    データ数-1 と同じにします.
}
procedure TMDIChild.AddUndo;
var
  undo_data : TChildUndoData ;
  rc        : TRect ;
  i         : Integer ;
begin

  // 保存するデータが無い(既にクローズしてるとか)場合は抜ける.
  if ( FBackBmp = nil ) then Exit ;

  // 多くなりすぎた場合は古いのから開放する.

  if ( FUndoList.Count >= UndoNumMax ) then begin

    undo_data := FUndoList.Items[0] ;
    undo_data.Free ;

    FUndoList.Delete( 0 );

    dec( FUndoIndex ); // 一旦減らす.
  end;


  inc( FUndoIndex );   // データが増えるからインクリメント♪


  // index より多くても開放する.
  // (リドゥが無い場合はこんなことしなくてもいいのにねぇ.)

  i := FUndoIndex ;
  while ( i <= FUndoList.Count-1 ) do begin

    undo_data := FUndoList.Items[i] ;
    undo_data.Free ;
    undo_Data := nil ;

    FUndoList.Items[i] := nil ; // Pack でまとめて消す.
    inc(i);
  end;

  FUndoList.Pack ;




  // 追加すべきデータを作る

  SetRect( rc, 0, 0, FBackBmp.Width, FBackBmp.Height );

  try
    undo_data := TChildUndoData.Create ; // 開放はデストラクタでやる.
    with undo_data do begin
      Bmp.PixelFormat := FBackBmp.PixelFormat ;
      Bmp.Width       := FBackBmp.Width ;
      Bmp.Height      := FBackBmp.Height ;
      Bmp.Canvas.CopyRect( rc, FBackBmp.Canvas, rc );
    end;

    FUndoList.Add( undo_data );

  except

  end;

  // この段階で,FUndoIndex と FUndoList.Counter-1 は等しくなってるはずじゃが.

end;



{
  Undo できるかチェック.

  ・データが無い場合
  ・ツールで処理中である場合
  などは Undo できません.
}
function TMDIChild.CanUndo: Boolean;
begin
  Result := False ;

  if ( FUndoList.Count <= 0 )   then Exit ;
  if ( FUndoIndex      <= 0 )   then Exit ;
  if ( g_ToolData.Processing )  then Exit ;

  Result := True ;
end;

{
  Undo実行

  ・読み出すデータの位置は,FUndoIndex-1 のデータです.
  ・Undo なら FUndoIndex-- します.
  ・Redo なら FUndoIndex++ します.
}
procedure TMDIChild.DoUndo;
var
  undo_data : TChildUndoData ;
  rc        : TRect ;
begin

  if not CanUndo then Exit ;

  // こうなったらは本当はおかしい.
  if ( FUndoIndex > FUndoList.Count-1 ) then begin
    MessageDlg( 'Undoのつじつまがあいません', mtError, [mbOK], 0 );
    FUndoIndex := FUndoList.Count-1 ;
  end;


  { Undo データ取得 }
  undo_data := FUndoList.Items[ FUndoIndex-1 ] ;

  SetRect( rc, 0, 0, undo_data.Bmp.Width, undo_data.Bmp.Height );

  FBackBmp.Width  := undo_data.Bmp.Width ;
  FBackBmp.Height := undo_data.Bmp.Height ;
  FBackBmp.Canvas.CopyRect( rc, undo_data.bmp.Canvas, rc );

  { インデクスデクリメント }
  dec( FUndoIndex );
  if ( FUndoIndex < 0 ) then FUndoIndex := -1 ;


  RedrawAll ; //再描画
end;



{
  Redo できるか

  ・データが無い場合
  ・ツールで処理中である間
  などは Redo できません.
}
function TMDIChild.CanRedo: Boolean;
begin
  Result := False ;

  if ( FUndoList.Count <= 0  ) then Exit ;
  if ( FUndoIndex      <= -1 ) then Exit ;
  if ( g_ToolData.Processing ) then Exit ;

  if ( FUndoIndex >= FUndoList.Count-1 ) then Exit ;//リドゥするデータがない.

  Result := True ;
end;


{
  Redo 実行

  ・Redo は UndoIndex+1 のデータを読み出します.
}
procedure TMDIChild.DoRedo;
var
  undo_data : TChildUndoData ;
  rc        : TRect ;
begin


  if not CanRedo then Exit ;

  { Redo データ取得 }
  undo_data := FUndoList.Items[ FUndoIndex + 1 ];

  SetRect( rc, 0, 0, undo_data.Bmp.Width, undo_data.Bmp.Height );

  FBackBmp.Width  := undo_data.Bmp.Width ;
  FBackBmp.Height := undo_data.Bmp.Height ;
  FBackBmp.Canvas.CopyRect( rc, undo_data.bmp.Canvas, rc );

  { インデクスインクリメント }
  inc( FUndoIndex );
  if ( FUndoIndex > FUndoList.Count -1 ) then FUndoIndex := FUndoList.Count -1 ;


  RedrawAll ; //再描画
end;

ふぅーむ.今日の日記は書くの時間かかったぁ〜(A;´Д`)
でも,すっかり気分もよくなったぞ.
うーん,ほかにエディタに実装した機能って何があったけ?
作ったエディタの機能が少ないだけに,ネタも尽きたって感じ.

・・・明日は,コピペ機能をつけよう!について書こうかにゃ.(゚ω゚)/~ ほじゃ〜



<< Back to Diary...