Link to home
Start Free TrialLog in
Avatar of ginsonic
ginsonicFlag for Romania

asked on

Steps property for my VCL

I have write an edit box replacement and now I wish to add a defined number of undos. To say 10.

I wish to create an array of 10 elements and to add the undo steps.
In this array I wish to store 5 integers and 2 strings values fo each element of the array. How to do that?

On this moment I declare a class derived from TPersistent:

type TgnUndo:...
    FUndo1:integer;
    ........
    FUndo5:string;
........
   property Undo1:integer .....

But maybe exist a better way. This way create a problem when add the 11 step. Then I must delete ( clear ) the first undo, and move all next 9 elements down. The 10 will become clear and to let me to store this step.

I use :
  for i:=1 to 10 do begin
    FgnUndo[i].Undo1:=FgnUndo[i+1].Undo1
    ....
   end;

Can somebody figure out a better solution ? When I clear the first element to autodecrease the index for first 9 elements and to let me to have 10 element free.
Avatar of Pierre Cornelius
Pierre Cornelius
Flag of South Africa image

Use a TList.

1.) Declare your data type:

  type
    PYourData = ^TYourData;
    TYourData = record
      Int1,Int2,Int3,Int4,Int5: integer;
      Str1,Str2: ShortString;
    end;

2.) Create an instance of your list:
    YourUndoList:= TList.Create;

3.) To add to your list do something like:
    procedure AddToList(SomeData: TYourData);
    var d: PYourData;
    begin
      New(d);
      d:= SomeData;
      yl.Add(d);
    end;

4.) You can remove items like this:
     var d : PYourData;
     ...
     d := YourUndoList.Items[0];
     YourUndoList.Delete(0);
     Dispose(d);

5.) remember to free the data before freeing the list when you're done:
      var d : PYourData;
     ...
      for i := 0 to (YourUndoList.Count - 1) do
      begin
        d := YourUndoList.Items[i];
        Dispose(d);
      end;

6.) Free the list when your done


Regards
Pierre
Avatar of BlackTigerX
BlackTigerX

I would use a collection, as the collection itself would encapsulate all the details of adding/removing and it would be it's own class, you could just call the Add method and that would take care of both, adding the new item and removing the last (or first) item, and even raising and exception or show an error if there are no more "undos" available if you wanted
I would say, collections are the Delphi OOP way of doing what you need

The only thing I don't like about collections is that they get quite bulky (lot of code), but I can live with that because of all the benefits

you can read more about collections here:
http://delphi.about.com/library/bluc/text/uc083101d.htm
Personally, I would just use a simple array and a second value for the current index. Whenever you need to add a new value, write the new record at the [Index] position, then increase the index with one to go to the next record. (Return to 0 if you're beyond the end of the array.) The index just tells you which is the next record to remove. You're not moving data. You're just circling... :-)

Oh, btw... You can easily walk through this list by using:

for I := 1 to 10 do FgnUndo[(Index+i) mod 10].DoSomething;

Be aware that normally, arrays are zero-based, though. But in this case, the +1 is useful because Index+1 is the first record in your list, and Index is last...
I don't agree with you Blacktiger. Using a collection would involve a lot more coding than simply using a TList which would also suffice for ginsonic's needs. You have indexed access to all the records, so you can easily Delete any of the "undo records" when applying a Redo command (e.g. YourUndoList.Delete(YourUndoList.Count-1) will delete the last item added to the undo list. The way I suggested though, you would keep inserting at index 0 so that the first item in the TList would also be the last thing done (and in essence be the first redo to be applied). When doing a redo, you would simply call YourUndoList.Delete(0) and the rest would fall in place)

I also considered suggesting using a TStack or TObjectStack but felt that a TList would be better suited for ginsonic's need.
Another thought, You could quite easily (and with much less overhead) derive a class from TList to suit your needs e.g. Something like TUndoList = class(TList)...etc. which can also take care of catching exceptions. I honestly don't think that is needed in this case though.

Alex,

The problem with using an array is that you would have to reassign the array when applying a redo.
i.e. Say you had the following items in your array:
0. Action 1
1. Action 2
...
9.Action 10

Now you want to redo the last done item, so you perform Action 1 and now you need to set the array to the following:
0. Action 2
1. Action 3
...
8. Action 10

Ginsonic,

Something I forgot:

To limit the undo records to the last 10 only, you would do the following right after you've added a new undo record to your list:

While YourUndoList.Count > 10
  do YourUndoList.Delete(YourUndoList.Count)
An example of what I mean:

program Test1;

{$APPTYPE CONSOLE}

const
  Max = 10;

type
  TDataItem = record
    Number: Integer;
    Name: string;
  end;
  TDataItemList = array[0..Max - 1] of TDataItem;

var
  DataItemList: TDataItemList;
  Index: Integer;
  Len: Integer;

var
  Line: string;
  I: Integer;
begin
  Randomize;
  Index := 0;
  Len := 0;
  repeat
    ReadLn(Line);
    if (Line = '+') then begin
      // Add an item to the list.
      DataItemList[Index].Number := Random(10);
      DataItemList[Index].Name := 'Name ' + Char(Ord('A') + Random(26));
      Index := (Index + 1) mod Max;
      if (Len < Max) then Len := Len + 1;
    end
    else if (Line = '-') then begin
      // Remove the last item in the list.
      Index := (Index + Max - 1) mod Max;
      DataItemList[Index].Number := 0; // Optional...
      DataItemList[Index].Name := ''; // Optional...
      if (Len > 0) then Len := Len - 1; // Simple, isn't it? :)
    end
    else if (Line = '*') then begin
      // Remove the first item from the list.
      DataItemList[(Index + Max - Len) mod Max].Number := 0; // Optional...
      DataItemList[(Index + Max - Len) mod Max].Name := ''; // Optional...
      // Don't adjust the index!
      if (Len > 0) then Len := Len - 1; // Simple, isn't it? :)
    end;
    // Display the list.
    for I := 0 to Pred(Max) do begin
      with DataItemList[(Index + I + Max - Len) mod Max] do
        WriteLn(I: 2, ' - ', (Index + I + Max - Len) mod Max, ':', Number: 2, ': ', Name);
    end;
    WriteLn('Length=', Len, ', index=', Index);
  until (Line = 'x');
end.

Simple console application. I use the +, - and * as commands. The + adds a record, the - a record from the end, and the * a record from the beginning. With a bit more programming, this could be put to use in a simple class.

And Pierre, if you look closely, you'll notice that I'm not moving any records. I'm just doing a bit of math to determine the start and end of the array, which is cycling through the fixed array in Delphi. Delphi uses similar tricks in the way it handles textfiles, for the textbuffer...
True, you could do it too with a list or with a collection but I tend to like just plain vanilla arrays. (Often dynamic arrays too.) The nicest part of my solution is that it doesn't depend on any unit or whatever. It's pure, basic PAscal code. Pretty primitive even.
SOLUTION
Avatar of Wim ten Brink
Wim ten Brink
Flag of Netherlands image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
ASKER CERTIFIED SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Yep, it's a very nifty trick what I used. :-) Thanks.
True, in general you can't get around the Classes unit in most applications anyway, but if you're creating a formless DLL then sometimes you can avoid using the Classes unit. Which is why I try to think of solutions that can avoid the usage of the classes unit. One thing I dislike about the TList is that it's a list of typeless pointers. Thus you need to typecast the pointer to get a pointer to the right type. And of course be aware for unassigned items. Besides, if you look at the source if the TList, you'll notice that it just has an array in the background anyways. Thus, adding and deleting items in a TList just means that pointers are moved around.

But moving pointers is faster than moving records. It's just less data. And the TList uses just the System.Move command to move the pointers up one level. (Or down, when inserting.) But if I used an array of class-types instead of an array of record-types then I could use the same technique for this array. An array of classes is just an array of pointers too. (Just don't forget to free them.)

And true... If you want to delete an item in the middle then my solution isn't optimal. But otherwise it's very powerful.
Avatar of ginsonic

ASKER

Thanks to all for support !
Glad to help.

Alex, Thanks for the new trick...
Hy Pierre,
I have a new problem. Can I somehow to insert a new record?
To say tht I have Rec:

0
1
2
3

I get a new record that must become 2 and not 4.

Now   Wished

0          0
1          1
2          2(4)
3          3(2)
4          4(3)

I will offer 500 point for a fast way. I use in my vcl max 50 steps (REC).
Simple. The TList.Add method adds to the end whilst the TList.Insert method adds a record where we tell it. e.g.

procedure TUndoList.InsertUndoRec(Index: integer; URec: TUndoRec);
var d: PUndoRec;
begin
  New(d);
  d^:= URec;
  Insert(Index, d);
end;


Remember the list is zero-based, so if you wanted to insert a record at position 2, you would call
   InsertUndoRec(1, SomeUndoRec)