命令历史记录单元的意外行为

时间:2013-10-23 17:01:20

标签: delphi delphi-7 paint undo-redo

我写了这样的模块来存储我的绘画应用程序中的图片的最后更改"在德尔福

    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;
  {...}

但问题是它的工作方式与我的预期无关。一个例子:

如果我按下撤销按钮一次 - 没有任何反应。两次 - 它会立刻做到它应该做的事情。

如果我绘制一个椭圆,然后单击撤消一次并开始绘制新的椭圆 - 最后绘制的椭圆只是消失了!

这里是elipse draw方法,如果它有助于找出问题

    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;

1 个答案:

答案 0 :(得分:8)

答案

  

如果我按下撤销按钮一次 - 没有任何反应。两次 - 它会立刻完成它应该做的事情。

这确实是你的代码所做的:

  • imgMainMouseUp中,您将当前图片添加到撤消列表,然后
  • btnUndoClick中,您从撤消列表中检索最后一个位图,该位图与图像上当前显示的位图相同。

针对此特定问题的解决方案是将之前的位图添加到撤消列表而不是当前的位置。

加成

为了解决有关泄漏问题的David's comment,您的实现会泄漏Bitmaps,因为:

  • 例程PopgetLast返回一个新的本地创建的位图。这将其破坏的责任放在调用者的例程上。您的MainForm代码不会破坏这些位图,因此它们是内存泄漏。解决方案是简单地返回数组中的项目,而不是创建新的位图。
  • Offset例程中,您再次创建新的位图并泄漏所有以前的位图。只需将Queue[I]分配给Queue[I + 1]
  • Push方法中,您忘记释放最后一项。
  • 该类没有析构函数,它再次负责销毁对象用户的所有位图,并且需要调用Clean,而不需要调用Clean。解决方案是向您的对象添加一个析构函数,该对象调用isFull

除了这些泄漏之外,您的代码还有更多问题。这里有一些修复和提示:

  • 由于动态数组是从零开始的,因此Result := historyIndex = hSize - 1;例程不会返回True。它应该实现为Pop
  • 您的数组不是队列(FIFO),而是堆栈(LIFO)。
  • 您的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单元中存在专门的课程{{1}}。
  • 您的MainForm代码也存在问题,但我礼貌地将此由您解决。