Solved

Steps property for my VCL

Posted on 2004-09-13
17
330 Views
Last Modified: 2012-05-05
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.
0
Comment
Question by:ginsonic
  • 8
  • 4
  • 3
  • +1
17 Comments
 
LVL 14

Expert Comment

by:Pierre Cornelius
Comment Utility
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
0
 
LVL 13

Expert Comment

by:BlackTigerX
Comment Utility
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
0
 
LVL 17

Expert Comment

by:Wim ten Brink
Comment Utility
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...
0
 
LVL 14

Expert Comment

by:Pierre Cornelius
Comment Utility
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.
0
 
LVL 14

Expert Comment

by:Pierre Cornelius
Comment Utility
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.

0
 
LVL 14

Expert Comment

by:Pierre Cornelius
Comment Utility
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

0
 
LVL 14

Expert Comment

by:Pierre Cornelius
Comment Utility
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)
0
 
LVL 17

Expert Comment

by:Wim ten Brink
Comment Utility
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.
0
6 Surprising Benefits of Threat Intelligence

All sorts of threat intelligence is available on the web. Intelligence you can learn from, and use to anticipate and prepare for future attacks.

 
LVL 17

Assisted Solution

by:Wim ten Brink
Wim ten Brink earned 100 total points
Comment Utility
And as a class:

program Test1;

{$APPTYPE CONSOLE}

const
  Max = 10;

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

type
  TDataList = class
  private
    FItems: TDataItemList;
    FIndex: Integer;
    FLength: Integer;
    function GetItem(I: Integer): TDataItem;
  protected
  public
    constructor Create;
    procedure Add(Number: Integer; const Name: string);
    procedure DeleteFirst;
    procedure DeleteLast;
    property Item[I: Integer]: TDataItem read GetItem; default;
    property Length: Integer read FLength;
  end;

procedure TDataList.Add(Number: Integer; const Name: string);
begin
  FItems[FIndex].Number := Number;
  FItems[FIndex].Name := Name;
  FIndex := (FIndex + 1) mod Max;
  if (FLength < Max) then FLength := FLength + 1;
end;

constructor TDataList.Create;
begin
  inherited Create;
  FIndex := 0;
  FLength := 0;
end;

procedure TDataList.DeleteFirst;
begin
  FItems[(FIndex + Max - FLength) mod Max].Number := 0; // Optional...
  FItems[(FIndex + Max - FLength) mod Max].Name := ''; // Optional...
  if (FLength > 0) then FLength := FLength - 1;
end;

procedure TDataList.DeleteLast;
begin
  FIndex := (FIndex + Max - 1) mod Max;
  FItems[FIndex].Number := 0; // Optional...
  FItems[FIndex].Name := ''; // Optional...
  if (FLength > 0) then FLength := FLength - 1;
end;

function TDataList.GetItem(I: Integer): TDataItem;
begin
  Result := FItems[(FIndex + I + Max - FLength) mod Max];
end;

var
  Line: string;
  I: Integer;
  DataList: TDataList;
begin
  Randomize;
  DataList := TDataList.Create;
  repeat
    ReadLn(Line);
    if (Line = '+') then begin
      DataList.Add(Random(10), 'Name ' + Char(Ord('A') + Random(26)));
    end
    else if (Line = '-') then begin
      DataList.DeleteLast;
    end
    else if (Line = '*') then begin
      DataList.DeleteFirst;
    end;
    // Display the list.
    for I := 0 to Pred(DataList.Length) do begin
      with DataList[I] do
        WriteLn(I: 2, ':', Number: 2, ': ', Name);
    end;
  until (Line = 'x');
  DataList.Free;
end.

However, if you want to remove an item in the middle of the array, it becomes a bit more complicated because then you would have to move records around. But in a history list you often don't want to do such a thing. This code is like a stack/queue combined. You add items to one ens and can remove items from both ends. If it gets bigger than a maximim size, it just kicks out the oldest value...
And, I don't use any special unit. Not even SysUtils. This is basic Pascal functionality and can be applied to many similar situations. Just keep it simple... K.I.S.S...
0
 
LVL 14

Accepted Solution

by:
Pierre Cornelius earned 400 total points
Comment Utility
Alex,

I like the fact that you are using a simple array to store the records as there is no further overhead in terms of memory allocation and de-allocation for new or removed records. Nifty trick! I must admit, though, I was a bit lost at first, but got the jist of it eventually.

If you wanted to use TList, you would need the Classes unit in your uses list. In a windows app, you can't get away from using this anyway, so there is no additional burden by using TList.

It would be nice though to facilitate undoing say the 3rd item in the list e.g. provide a listbox with available undo's and let the user select which one he wants to undo.

unit Unit1;

interface

uses
  SysUtils, Forms, Classes;

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

  TUndoList = class(TList)
    public
      procedure AddUndoRec(URec: TUndoRec);
      function GetUndoRec(Index: integer):TUndoRec;
      procedure DeleteUndoRec(Index: integer);
      procedure DeleteFirst;
      procedure DeleteLAst;
      procedure Clear; override;
      procedure ListToItems(SomeItems: TStrings);
  end;

  TForm1 = class(TForm)
      ListBox1: TListBox;
      Button1: TButton;
    Button2: TButton;
      procedure Button1Click(Sender: TObject);
      procedure FormCreate(Sender: TObject);
      procedure FormDestroy(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    private
      UndoList: TUndoList;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

{ TUndoList }

procedure TUndoList.AddUndoRec(URec: TUndoRec);
var d: PUndoRec;
begin
  New(d);
  d^:= URec;
  Add(d);
end;

procedure TUndoList.Clear;
var d: PUndoRec;
    i: integer;
begin
  for i := 0 to (Count - 1) do
  begin
    d := Items[i];
    Dispose(d);
  end;

  inherited;
end;

procedure TUndoList.DeleteFirst;
begin
  DeleteUndoRec(0);
end;

procedure TUndoList.DeleteLAst;
begin
  DeleteUndoRec(Count-1);
end;

procedure TUndoList.DeleteUndoRec(Index: integer);
var d : PUndoRec;
begin
  if (Index < 0) OR (Index >= Count)
    then raise exception.Create('Error deleting undo record!'#13+
                                'Index out of range.');

  d := Items[Index];
  Delete(Index);
  Dispose(d);
end;

function TUndoList.GetUndoRec(Index: integer): TUndoRec;
begin
  result:= PUndoRec(Items[Index])^;
end;

procedure TUndoList.ListToItems(SomeItems: TStrings);
var i: integer;
begin
  SomeItems.Clear;
  for i:= 0 to Count-1 do
    SomeItems.Add(PUndoRec(Items[i]).Str1);
end;

end.


0
 
LVL 17

Expert Comment

by:Wim ten Brink
Comment Utility
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.
0
 
LVL 9

Author Comment

by:ginsonic
Comment Utility
Thanks to all for support !
0
 
LVL 14

Expert Comment

by:Pierre Cornelius
Comment Utility
Glad to help.

Alex, Thanks for the new trick...
0
 
LVL 9

Author Comment

by:ginsonic
Comment Utility
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).
0
 
LVL 14

Expert Comment

by:Pierre Cornelius
Comment Utility
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)

0
 
LVL 9

Author Comment

by:ginsonic
Comment Utility
0

Featured Post

How to improve team productivity

Quip adds documents, spreadsheets, and tasklists to your Slack experience
- Elevate ideas to Quip docs
- Share Quip docs in Slack
- Get notified of changes to your docs
- Available on iOS/Android/Desktop/Web
- Online/Offline

Join & Write a Comment

This article explains how to create forms/units independent of other forms/units object names in a delphi project. Have you ever created a form for user input in a Delphi project and then had the need to have that same form in a other Delphi proj…
In my programming career I have only very rarely run into situations where operator overloading would be of any use in my work.  Normally those situations involved math with either overly large numbers (hundreds of thousands of digits or accuracy re…
Access reports are powerful and flexible. Learn how to create a query and then a grouped report using the wizard. Modify the report design after the wizard is done to make it look better. There will be another video to explain how to put the final p…
In this tutorial you'll learn about bandwidth monitoring with flows and packet sniffing with our network monitoring solution PRTG Network Monitor (https://www.paessler.com/prtg). If you're interested in additional methods for monitoring bandwidt…

772 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

11 Experts available now in Live!

Get 1:1 Help Now