如何实现撤销?(Undo)(10分)

  • 主题发起人 主题发起人 delnus
  • 开始时间 开始时间
D

delnus

Unregistered / Unconfirmed
GUEST, unregistred user!
小弟写的图形处理软件,想对初次装入的原图进行再处理,却不知如何实现撤销功能?

我见到一个:ControlName.Perform(EM_UNDO,0,0)却不知如何使用?请各位大虾指点。
thx!!!
 
以前我也遇到同样问题,试一下下面的方法:
Implementing Multiple Undo in a Graphics Application
by Steve Harman - sharman1@uswest.net
The methodology presented here is useful for 24-bit bitmaps and small images, although the technique could be used for any kind of records. It takes advantage of the dynamic array, which became available in Delphi 4. You can accomplish the same thing in older versions of Delphi, but you have to set up your own routines for memory management.
I wrote and maintain a graphics application that creates small images for use as tiled wallpaper or web page backgrounds. I use 24-bit bitmaps, for a couple of different reasons: color quality is outstanding, and it makes it much easier to massage the pixels because the color values are stored in individual bytes representing the R-G-B values. My application tends to change every pixel in the image, so the entire image must be 'saved' after each operation to have a viable undo. For other circumstances, it would be better to just 'save' the portion of the image that changed, to reduce memory requirements.
Initially, I only allowed one Undo in my app. I allocated a TBitmap up front (at form create) and kept it for the application duration (until Form destroy) for storing the 'undo' image. It was low-overhead, fast, and easy to code; for an undo all I had to do was copy from the 'undo' bitmap. However, many users asked me for more than one undo - and being the obliging person that I am, I decided to increase this up to 5 levels of undo. I just expanded on the same principle; allocate five undo bitmaps at form create, and free them at form destroy. Each time a change was done, I cascaded the undo images by looping through and copying five times. Conversely, when an undo was performed I also looped through and copied in the opposite direction. This added much more overhead - each time a change was done to the image, I was copying bitmaps like crazy! It was still fairly fast, though, as long as the image was a reasonable size. However, when attempting to edit a large-ish image, it was tying up my test machine each time I did a change, and I decided I had to do something different. Walter Schick got me interested in multiple undo, and I played with a couple of different ideas until I hit on this technique. Walter suggested the pointer idea, and after several mis-fires, I finally got it working. It works pretty well for my application, but is not suited for applications that use large images.
I don't use TBitmap at all for storing the undo image, I just store the pixel information in memory. This reduces the GDI resource consumption, because I don't need Handles for bitmaps. I also decided that I needed to release the memory whenever an undo was executed, since I no longer needed the information, and keeping memory consumption to a minimum was a good thing. Basically, I was saving the information I needed to create an image; not the image itself. I needed the image dimensions and the RGB values of each pixel, so I made the following declarations:
Type
Trgb = record // For undo - copy values to array
r : Word;
g : Word;
b : Word;
end;
TUndoMap = array of array of Trgb; // Each TUndoMap stores RGB values
And in the form's private section
Pic : array of TUndoMap; // Undo array
Sizes : array of TPoint; // Track sizes of Pics
The TPoint seemed to be a great pre-defined type to store x and y values, which for my purposes is the width and height of each image. The two arrays (Pic and Sizes) must be kept in synch; I was initially going to add the size as part of the TUndoMap, but it was easier for my dim brain to follow if it was kept separate. Thus, each time I have to make a copy of an image for undo purposes, I do (for example) the following:
Sizes[0].x := MyBitmap.Width; // Store the width
Sizes[0].y := MyBitmap.Height; // Store the height
SetLength(Pic[0],MyBitmap.Width,MyBitmap.Height); // Allocate the memory for the array
for h := 0 to MyBitmap.Height-1 do begin // Populate the array
sl := MyBitmap.Scanline[h];
for w := 0 to MyBitmap.Width - 1 do begin
Pic[0,w,h].r := sl[w].rgbtRed;
Pic[0,w,h].g := sl[w].rgbtGreen;
Pic[0,w,h].b := sl[w].rgbtBlue;
end;
end;
I now have a copy of the information I need to re-build the image when an undo is requested. To perform the undo:
MyBitmap.Width := Sizes[0].x; // Set MyBitmap dimensions from stored values
MyBitmap.Height := Sizes[0].y;
for h := 0 to MyBitmap.Height-1 do begin // Re-build the image from the RGB values
sl := MyBitmap.Scanline[h];
for w := 0 to MyBitmap.Width - 1 do begin
sl[w].rgbtRed := Pic[0,w,h].r;
sl[w].rgbtGreen := Pic[0,w,h].g;
sl[w].rgbtBlue := Pic[0,w,h].b;
end;
end;
SetLength(Pic[0],0,0); // Free the memory for this entry
While this may seem like a lot of overhead to perform each time the image changes, it's really not bad. The Scanline method of accessing pixel values is very fast, and it's writing/reading the values to memory. Of course, the code snips above are not complete, there are still a few issues to deal with. How many 'undo' images can be used? How are they tracked? And the big question, what happens when the maximum is reached?
I initially allowed unlimited undo's; I just kept adding to a LIFO type system whenever a TUndoMap was created. However, this had the potential to eventually use all the memory in the system, which is probably not a good thing. So, I needed a means of limiting the number of undo's, which was easy enough - I thought. When the maximum number of undo images has been filled, how do you rotate them so that the oldest saved image gets re-used for the newest saved image? Looping through the array and copying Pic[1] to Pic[0], Pic[2] to Pic[1], etc. can take a lot of time if there are more than a few images to deal with. And this has to happen each time the image is changed, once the maximum is reached. There goes performance!
The answer to this dilemma is to create another array, which keeps track of the order of the Pic (and Sizes) array. I call this array Entry, and when the maximum number of undo's is reached I re-order the Entry array, rather than copying memory all over the place. It required some additional variables to keep track of everything, but it has the advantage of being very fast even when the maximum undo level has been reached. Additional declarations:

const
MaxUndo = 50; // Make this an appropriate number for your application
private
Entry : array of integer; // List of Full Pic's
Full, Empty : Boolean; // Flags for the Pic array
StackSize : integer; // How many Pic's are saved

And the procedures for maintaining the undo's:
procedure CopyUndo; // Copy image info
procedure DoUndo; // Re-create image from copy
procedure Reorder; // Reorder the pointers when max undo is reached
The premise is this: the highest member in the Entry array (Entry[MaxUndo - 1]) will always point to the most recent copy of the image, while the lowest member (Entry[0]) always points to the oldest, or last available copy for Undo. There are some checks made at the beginning of the CopyUndo to see if the MaxUndo value has been reached, to do the Reorder if necessary. It's important to set up some initial values at form create, for all this to work:
StackSize := 0; // No undo's available at startup
SetLength(Pic,MaxUndo); // Initialize the Pic length to the MaxUndo number
// (This is just an array of pointers!)
SetLength(Sizes,MaxUndo); // Keep Sizes in synch with Pic
Full := FALSE;
Empty := TRUE;

procedure TForm1.CopyUnDo;
var
w, h : integer;
sl : PRGBArray;
begin
Inc(StackSize);
if StackSize > MaxUndo then begin
Full := TRUE;
Dec(StackSize);
Reorder;
end;
if not Full then begin
SetLength(Entry,StackSize);
if Empty then begin
Entry[0] := 0;
Empty := FALSE;
end
else begin
Entry[StackSize-1] := Entry[StackSize-2] + 1;
if Entry[StackSize-1] > MaxUndo - 1 then
Entry[StackSize-1] := 0;
end;
end;
SetLength(Pic[Entry[StackSize-1]],MyBitmap.Width,MyBitmap.Height);
Sizes[Entry[StackSize-1]].x := MyBitmap.Width;
Sizes[Entry[StackSize-1]].y := MyBitmap.Height;
for h := 0 to MyBitmap.Height - 1 do begin
sl := MyBitmap.Scanline[h];
for w := 0 to MyBitmap.Width - 1 do begin
Pic[Entry[StackSize-1],w,h].r := sl[w].rgbtRed;
Pic[Entry[StackSize-1],w,h].g := sl[w].rgbtGreen;
Pic[Entry[StackSize-1],w,h].b := sl[w].rgbtBlue;
end;
end;
end;

// Reorder the list of pointers to undomaps
procedure TForm1.Reorder;
var
a, tmp : integer;
begin
tmp := Entry[0];
for a := 0 to StackSize - 2 do
Entry[a] := Entry[a+1];
Entry[StackSize-1] := tmp;
end;

procedure TForm1.DoUnDo;
var
w,h : integer;
sl : PRGBArray;
begin
if Empty then Exit;
MyBitmap.Width := Sizes[0].x;
MyBitmap.Height := Sizes[0].y;
for h := 0 to MyBitmap.Height-1 do begin
sl := MyBitmap.Scanline[h];
for w := 0 to MyBitmap.Width - 1 do begin
sl[w].rgbtRed := Pic[Entry[StackSize-1],w,h].r;
sl[w].rgbtGreen := Pic[Entry[StackSize-1],w,h].g;
sl[w].rgbtBlue := Pic[Entry[StackSize-1],w,h].b;
end;
end;
SetLength(Pic[Entry[StackSize-1]],0,0);
Dec(StackSize);
Full := FALSE;
SetLength(Entry,StackSize);
if StackSize = 0 then Empty := TRUE;
end;

To see how it all works together, here's the full project source code. If you have Memory Sleuth, it's interesting to watch the memory usage grow and shrink as the image is changed and undo's are performed. It does not incur any additional GDI resources, only memory (and pointer) allocations. It's a fairly minimal impact on system resources, unless you allow 100's of Undo operations on large images - it's a judgement call on your part. For my application, I am currently at 50 levels of Undo. This seems to be about right for me, since the images in my app tend to be smallish, but you may want to limit yours to something less than that. Keep in mind that although the number of MaxUndo can be small or large, the copy time for Undo remains constant, based mostly on image size and available memory.

 
image1.Picture.Assign(nil) ;
 
多人接受答案了。
 

Similar threads

D
回复
0
查看
2K
DelphiTeacher的专栏
D
D
回复
0
查看
1K
DelphiTeacher的专栏
D
后退
顶部