4

私は、Delphi のペイント アプリケーションで画像の最後の変更を保存するモジュールを作成しました。

    unit HistoryQueue;

    interface

    uses
      Graphics;

   type
myHistory = class
  constructor Create(Size:Integer);
  public
    procedure Push(Bmp:TBitmap);
    function Pop():TBitmap;
    procedure Clean();
    procedure Offset();
    function isEmpty():boolean;
    function isFull():boolean;
    function getLast():TBitmap;
  protected
    historyQueueArray: array of TBitmap;
    historyIndex, hSize:Integer;
  end;

implementation

procedure myHistory.Push(Bmp:TBitmap);
var tbmp:TBitmap;
begin
  if(not isFull) then begin
      Inc(historyIndex);
      historyQueueArray[historyIndex]:=TBitmap.Create;
      historyQueueArray[historyIndex].Assign(bmp);
  end else begin
      Offset();
      historyQueueArray[historyIndex]:=TBitmap.Create;
      historyQueueArray[historyIndex].Assign(bmp);
  end;

end;

procedure myHistory.Clean;
var i:Integer;
begin
{  for i:=0 to hSize do begin
    historyQueueArray[i].Free;
    historyQueueArray[i].Destroy;
  end;        }

end;

constructor myHistory.Create(Size:Integer);
begin
  hSize:=Size;
  SetLength(historyQueueArray, hSize);
  historyIndex:=-1;
end;

function myHistory.isEmpty: boolean;
begin
  Result:=(historyIndex = -1);
end;

function myHistory.isFull: boolean;
begin
  Result:=(historyIndex = hSize);
end;

procedure myHistory.Offset; {to handle overflow}
var i:integer;
begin
  //historyQueueArray[0]:=nil;
  for i:=0 to hSize-1 do begin
    historyQueueArray[i]:=TBitmap.Create;
    historyQueueArray[i].Assign(historyQueueArray[i+1]);
  end;
end;

function myHistory.Pop: TBitmap;
var
  popBmp:TBitmap;
begin
  popBmp:= TBitmap.Create;
  popBmp.Assign(historyQueueArray[historyIndex]);
  Dec(historyIndex);
  Result:=popBmp;
end;

function myHistory.getLast: TBitmap; {this function I use when I need refresh the cnvas when I draw ellipse or rect, to get rid of traces and safe previous changes of the picture}
var
  tBmp:TBitmap;
begin
  tBmp:= TBitmap.Create;
  tBmp.Assign(historyQueueArray[historyIndex]);
  Result:=tBmp;
end;

end.

そして、それが私がそれを使用する方法です

procedure TMainForm.FormCreate(Sender: TObject);
var
  cleanBmp:TBitmap;
begin
    {...}

    doneRedo:=false;

    redomode:=false; undomode:=false;
//init arrays
    picHistory:=myHistory.Create(10);   //FOR UNDO
    tempHistory:=myHistory.Create(10); //FOR REDO

    cleanbmp:=TBitmap.Create;
    cleanbmp.Assign(imgMain.Picture.Bitmap);
    picHistory.Push(cleanbmp);
    cleanbmp.Free;

    {...}

end;

 procedure TMainForm.btnUndoClick(Sender: TObject);
var redBmp:TBitmap;
begin

  undoMode:=true;
  //if there were some changes
  if(not picHistory.isEmpty) then begin
    redBmp:=TBitmap.Create;
    redBmp.Assign(picHistory.getLast);
    //clean canvas
    imgMain.Picture.Bitmap:=nil; 
    //get what was there before
    imgMain.Canvas.Draw(0,0, picHistory.Pop);
    //and in case if we will not make any changes after UNDO(clicked one or more times)
    //and call REDO then
    tempHistory.Push(redBmp);//we save what were on canvas before UNDOand push it to redo history
    redBmp.Free;
  end;

end;


procedure TMainForm.btnRedoClick(Sender: TObject);
var undBmp:TBitmap;
begin
  redoMode:=true;
  if(not tempHistory.isEmpty) then begin

    doneRedo:=True;

    undBmp:=TBitmap.Create;

    undBmp.Assign(tempHistory.getLast);
    imgMain.Picture.Bitmap:=nil;
    MainForm.imgMain.Canvas.Draw(0,0, tempHistory.Pop);

    //same history (like with UNDO implementation) here but reverse
    picHistory.Push(undBmp);
    undBmp.Free;
  end;
end;


{...}

procedure TMainForm.imgMainMouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var bmp:TBitmap;
begin
//if mouse were down and then it's up this means we drew something
//and must save changes into history to be able to make UNDO
  {...}
    bmp:=TBitmap.Create;
    try
      bmp.Assign(imgMain.Picture.Bitmap);
      picHistory.Push(bmp);
      //if there are some changes added after redo then we clean redo history
     if (doneRedo) then begin
        tempHistory.Clean;
        doneRedo:=false;
     end;
    finally
      bmp.Free;
      //sor of refresh
      imgMain.Canvas.Draw(0,0, picHistory.getLast);
    end;
  {...}

しかし、問題は、それが期待どおりに機能しないことです。例:

元に戻すボタンを 1 回押しても、何も起こりません。2 回オン - 一度にすべきことを行います。

また、楕円を描いた場合は、[元に戻す] を 1 回クリックして、新しい楕円を描き始めます。最後に描いた楕円が消えるだけです。

問題を見つけるのに役立つ場合に備えて、楕円の描画方法を次に示します。

    procedure TMainForm.ellipseDraw(X, Y: Integer);
    begin
      imgMain.Canvas.Pen.Color:=useColor;
      imgMain.Canvas.Brush.Color:=scndColor;

      imgMain.Canvas.Pen.Width:=size;

      if(mouseIsDown) then  begin
        imgMain.Canvas.Draw(0,0, picHistory.getLast); //there gonna be no bizzare traces from figures
        imgMain.Canvas.Ellipse(dX, dY, X,Y);
      end;
    end;
4

1 に答える 1

8

答え

元に戻すボタンを 1 回押しても、何も起こりません。2 回オン - 一度にすべきことを行います。

それはまさにあなたのコードが行うことです:

  • imgMainMouseUp現在の画像を元に戻すリストに追加します。
  • 元に戻すリストから最後のビットマップを取得します。これbtnUndoClickは、現在イメージに表示されているものと同じです。

この特定の質問に対する解決策は、現在のビットマップではなく、以前のビットマップを元に戻すリストに追加することです。

ボーナス

また、リークに関するDavid のコメントに対処するために、次の理由により、実装でビットマップがリークします。

  • ルーチンPopgetLastは、新しくローカルに作成されたビットマップを返します。これにより、ルーチンの呼び出し元に破棄の責任が生じます。MainForm コードはこれらのビットマップを破棄しないため、メモリ リークになります。解決策は、新しいビットマップを作成する代わりに、単純に配列内のアイテムを返すことです。
  • ルーチンでは、Offset新しいビットマップを再度作成し、以前のものをすべてリークします。に割り当てるだけQueue[I]ですQueue[I + 1]
  • このPushメソッドでは、最後のアイテムを解放するのを忘れています。
  • このクラスにはデストラクタがありません。この場合も、オブジェクトのユーザーにすべてのビットマップの破棄の責任を負わせる必要がありますがClean、これは必要ありません。解決策は、を呼び出すオブジェクトにデストラクタを追加することCleanです。

これらのリークに加えて、コードにはさらに多くの問題があります。ここにいくつかの修正とヒントがあります:

  • 動的配列は 0 から始まるため、isFullルーチンは True を返す必要があるときに True を返しません。として実装する必要がありますResult := historyIndex = hSize - 1;
  • 配列はキュー ( FIFO ) ではなく、スタック ( LIFO ) です。
  • あなたのPopルーチンは空のリストをチェックしません。

全体として、履歴クラスは次のようになります。

uses
  SysUtils, Graphics;

type
  TBitmapHistory = class(TObject)
  private
    FIndex: Integer;
    FStack: array of TBitmap;
    procedure Offset;
  public
    procedure Clear;
    function Count: Integer;
    constructor Create(ACount: Integer);
    destructor Destroy; override;
    function Empty: Boolean;
    function Full: Boolean;
    function Last: TBitmap;
    function Pop: TBitmap;
    procedure Push(ABitmap: TBitmap);
  end;

implementation

{ TBitmapHistory }

procedure TBitmapHistory.Clear;
var
  I: Integer;
begin
  for I := 0 to Count - 1 do
    FreeAndNil(FStack[I]);
  FIndex := -1;
end;

function TBitmapHistory.Count: Integer;
begin
  Result := Length(FStack);
end;

constructor TBitmapHistory.Create(ACount: Integer);
begin
  inherited Create;
  SetLength(FStack, ACount);
  FIndex := -1;
end;

destructor TBitmapHistory.Destroy;
begin
  Clear;
  inherited Destroy;
end;

function TBitmapHistory.Empty: Boolean;
begin
  Result := FIndex = -1;
end;

function TBitmapHistory.Full: Boolean;
begin
  Result := FIndex = Count - 1;
end;

function TBitmapHistory.Last: TBitmap;
begin
  if Empty then
    Result := nil
  else
    Result := FStack[FIndex];
end;

procedure TBitmapHistory.Offset;
begin
  FStack[0].Free;
  Move(FStack[1], FStack[0], (Count - 1) * SizeOf(TBitmap));
end;

function TBitmapHistory.Pop: TBitmap;
begin
  if not Empty then
  begin
    Result := Last;
    Dec(FIndex);
  end;
end;

procedure TBitmapHistory.Push(ABitmap: TBitmap);
begin
  if Full then
    Offset
  else
    Inc(FIndex);
  FStack[Findex].Free;
  FStack[FIndex] := TBitmap.Create;
  FStack[Findex].Assign(ABitmap);
end;

備考:

  • オーバーライド/悪用できるユニットにTObjectStackは、これに特化したクラスも存在します。Contnrs
  • MainForm コードにも問題がありますが、それは丁寧に修正していただきます。
于 2013-10-26T18:03:20.767 に答える