Dynamic TList of TList - delphi

i have this problem: as i can add list to one list? i have tried so, but main list return always one list, and not understand where i mistake.
The structure is this:
PCombArray = array of Integer;
PStatsRecord = record
Comb: PCombArray;
Freq: Integer;
end;
PStatsList = TList<PStatsRecord>;
TStatsList = TList<PStatsList>;
Where Comb is a field that work as primary key. But here all ok. i define main list as:
var
MainList: TStatsList;
MySubList: PStatsList;
and create it, without problem. A procedure work for populate a subList; for esample i call this procedure as MakeSubList and assign a MySubList the list maked, then i add it to main list:
MainList := TList<PStatsList>.Create;
try
MainList.Clear;
for index := 1 to N do // generate N subList
begin
...
MySubList := MakeSubList(index); // contain correct sub list, no problem here
...
MainList.Add(MySubList); // add mysublist to mainlist
end;
writeln (mainList.count); // return 1, and not N-1 sublist
finally
MainList.Free;
end;
Thank who help me to understand so i can solve it. Thanks again.

Your make sub list function might be wrong, or you might have confused yourself with your naming conventions which did not follow in any way the usual Delphi/Pascal naming conventions for types.
The following code works, though.
Note that a TList is an Object so if you want to make a List of TList be sure to use TObjectList instead of plain TList unless you like memory leaks. Your inner list is around a record type and does not need to be TObjectList, but if you changed StatsRecord to TStatsData as a TObject (class) type, you should also change to TObjectList.
unit aUnit5;
interface
uses Generics.Collections;
procedure Test;
implementation
type
CombArray = array of Integer;
StatsRecord = record
Comb: CombArray;
Freq: Integer;
end;
TStatsList2 = TList<StatsRecord>;
TStatsList = TObjectList<TStatsList2>;
var
MainList: TStatsList;
MySubList: TStatsList2;
index:Integer;
procedure Test;
begin
MainList := TStatsList.Create;
try
for index := 1 to 10 do // generate N subList
begin
MySubList := TStatsList2.Create;
MainList.Add(MySubList); // add mysublist to mainlist
end;
finally
MainList.Free;
end;
end;
end.

MainList is a list of PStatsList, so you're certainly allowed to add instances of PStatsList to it. If you weren't allowed, your code would not have compiled or run at all.
If the loop runs N times, then MainList.Add will be called N times, and MainList.Count will be N. So, if WriteLn(MainList.Count) prints 1, then we can only conclude that N = 1.
TList allows duplicates, so even if MakeSubList is returning the same object each time, it can still get added to the main list multiple times.

..
implementation
type
S64 = string[64];
art_ptr = ^art_rec;
art_rec = record
sDes: S64;
rCost: real;
iQty: integer;
end;
art_file = file of art_rec;
var
lCatalog: TList;
procedure disp_all;
var
lArt: TList;
pA: art_ptr;
c,
a: integer;
begin
for c:= 0 to lCatalog.Count -1 do begin
lArt:= lCatalog[c];
for a:= 0 to lArt.Count -1 do begin
pA:= lArt[a];
Dispose(pA);
end;
lArt.Clear;
lArt.Free;
end;
lCatalog.Clear;
end;// disp_all
procedure TfrmMain.FormCreate(Sender: TObject);
begin
lCatalog:= TList.Create;
end;// FormCreate
procedure TfrmMain.FormDestroy(Sender: TObject);
begin
disp_all;
lCatalog.Free;
end;// FormDestroy
// a normal BitButton click that adds 3 records to each art list (2)
procedure TfrmMain.bbMCreate_Click(Sender: TObject);
const
nArt = 2;
nRec = 3;
var
pA: art_ptr;
lArt: TList;
c: integer;
begin
// clears any previous added pointer record to art list that was added to the catalog
disp_all;
// creates art lists and add 'em to the catalog list
for c:= 0 to nArt -1 do begin
lArt:= TList.Create;
lCatalog.Add(lArt);
// creates records and add 'em to the art list
for a:= 0 to nArt -1 do begin
pA:= New(art_ptr);
with pA^ do begin
sDes:= Format('cat%d-art%d', [c, a]);
rCost:= (c+1) * (a +1) *1.0;
iQty:= (c+1) *10 + (a +1);
end;
lArt.Add(pA);
end;
end;
end;// bbMCreate_Click
// a normal BitButton click that reads 1 record from 1 art list
procedure TfrmMain.bbMRead_Click(Sender: TObject);
const
c = 1;
a = 2;
var
pA: art_ptr;
lArt: TList;
begin
// gets art list #2 from catalog (index is 0 based. c=1)
lArt:= lCatalog[c];
// gets record #3 from art list (index is 0 based. a=2)
pA:= lArt[a];
// displays the record in a string grid called sgArt... at row a+1
with sgArt, pA^ do begin
// col 0 contains cat index and art index, can be useful...
cells[0,a+1]:= Format('\%d\%d', [c,a]);
cells[1,a+1]:= sDes;
cells[2,a+1]:= FormatFloat('0.00', rCost);
cells[3,a+1]:= IntToStr(iQty);
end;
end;// bbMRead_Click

Related

create a popupmenu from a SQL [duplicate]

I have a table like this:
id parent_id name
1 1 Root
2 1 Car
3 1 Plane
4 2 BMW
5 4 CLK
How can I dynamically create popup menu with all subitems in Delphi?
This is how it should look like:
Assuming root element has NULL as Parent_ID you can issue the request
Select ID, Parent_ID, Name from all_my_menus
order by Parent_ID nulls first, ID
where Menu_ID = :MenuIDParameter
1 <NULL> Root
8 <NULL> another root
2 1 Car
4 1 Plane
3 2 BMW
5 4 CLK
You would also cache in-memory created menu items: var MI_by_id: TDictionary<integer, TMenuItem>;
The traversing through the results would look like
var MI: TMenuItem;
MI_by_id: TDictionary<integer, TMenuItem>;
begin
MI_by_id := TDictionary<integer, TMenuItem>.Create;
try
While not Query.EOF do begin
MI := TMenuItem.Create(Self);
MI.Caption := Query.Fields[2].AsString;
MI.Tag := Query.Fields[0].AsInteger; // ID, would be helpful for OnClick event
MI.OnClick := ...some click handler
if Query.Fields[1].IsNull {no parent}
then MainMenu.Items.Add(MI)
else MI_by_id.Items[Query.Fields[1].AsInteger].Add(MI);
MI_by_id.Add(MI.Tag, MI); //save shortcut to potential parent for future searching
Query.Next;
end;
finally
MI_by_id.Free;
end;
end;
Actually, since we made sort upon Parent_ID on the query, all the children for given parent make single continuous list, so could be better to remove populated parents from the dictionary after we populated last child (i.e. after parent_ID got new value) and caching previously found parent otherwise in another local variable (instead of making yet another search through the dictionary).
However reasonable size for human-targeted menu should be much less to worth this. But you have to understand this approach most probably scales as O(n*n) thus would start loose speed very fast as the table grows.
http://docwiki.embarcadero.com/Libraries/XE3/en/Vcl.Menus.TMenuItem.Add
http://docwiki.embarcadero.com/CodeExamples/XE2/en/Generics.Collections.TDictionary_(Delphi)
Note: this also requires that for every non-root element ID > ParentID (put CHECK CONSTRAINT on the table)
1 <NULL> Root
8 <NULL> another root
7 1 Plane
3 4 BMW
4 7 CLK
5 8 Car
This would lead to BMW tied to create before its parent CLK created.
Violation for that conditions can be overcome by few means:
recursive load: select <items> where Parent_id is null, then for each of the added menu items do select <items> where Parent_id = :current_memuitem_id and so on that. This is like VirtualTreeView would work
ask SQL server to sort and flatten the tree - this is usually called self-recursive SQL selection and is server-dependant.
introduce one more collection variable - menu items w/o parent. After each new item added to the menu this collection should be searched if there are pending children to extract from it and move into the newly created parent.
Too many solutions for such a simple problem. Too bad you got ordered ID's because without ordered ID's things would have been more fun. Here's my own solution. On an empty form drop a button, a TClientDataSet and a TPopupMenu. Make the form's PopupMenu = PopupMenu1 so you can see the result. Add this to Button1.OnClick:
Note: I'm intentionally using TClientDataSet and not a real Query. This question is not about the query and this solution works with whatever TDataSet descendant you throw at it. Just make sure the result set is ordered on id, or else you could see the child nodes before the parents. Also note, half the code is used to fill up the ClientDataSet with the sample data in the question!
procedure TForm16.Button1Click(Sender: TObject);
var Prev: TDictionary<Integer, TMenuItem>; // We will use this to keep track of previously generated nodes so we do not need to search for them
CurrentItem, ParentItem: TMenuItem;
begin
if not ClientDataSet1.Active then
begin
// Prepare the ClientDataSet1 structure
ClientDataSet1.FieldDefs.Add('id', ftInteger);
ClientDataSet1.FieldDefs.Add('parent_id', ftInteger);
ClientDataSet1.FieldDefs.Add('name', ftString, 100);
ClientDataSet1.CreateDataSet;
// Fill the dataset
ClientDataSet1.AppendRecord([1, 1, 'Root']);
ClientDataSet1.AppendRecord([2, 1, 'Car']);
ClientDataSet1.AppendRecord([3, 1, 'Plane']);
ClientDataSet1.AppendRecord([4, 2, 'BMW']);
ClientDataSet1.AppendRecord([5, 4, 'CLK']);
end;
// Clear the existing menu
PopupMenu1.Items.Clear;
// Prepare the loop
Prev := TDictionary<Integer, TMenuItem>.Create;
try
ClientDataSet1.First; // Not required for a true SQL Query, only required here for re-entry
while not ClientDataSet1.Eof do
begin
CurrentItem := TMenuItem.Create(Self);
CurrentItem.Caption := ClientDataSet1['name'];
if (not ClientDataSet1.FieldByName('parent_id').IsNull) and Prev.TryGetValue(ClientDataSet1['parent_id'], ParentItem) then
ParentItem.Add(CurrentItem)
else
PopupMenu1.Items.Add(CurrentItem);
// Put the current Item in the dictionary for future reference
Prev.Add(ClientDataSet1['id'], CurrentItem);
ClientDataSet1.Next;
end;
finally Prev.Free;
end;
end;
Try this
procedure TForm1.MyPopup(Sender: TObject);
begin
with Sender as TMenuItem do ShowMessage(Caption);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
MyItem,MySubItem1: TMenuItem;
begin
Inc(Num);
MyItem:=TMenuItem.Create(Self);
MySubItem1:=TMenuItem.Create(Self);
MyItem.Caption:='Hello'+IntToStr(Num);
MySubItem1.Caption:='Good Bye'+IntToStr(Num);
MainMenu1.Items.Add(MyItem);
MainMenu1.Items[0].Insert(num-1,MySubItem1);
MyItem.OnClick:=MyPopUp;
MySubItem1.OnClick:=MyPopUp;
end;
Taken from http://www.greatis.com/delphicb/tips/lib/components-addmenuitem.html
This solution requires parent_id of root to be 0, tested with
Select 1 as ID, 0 as Parent_ID, 'Root' as Name
union
Select 2, 1, ' Car'
union
Select 3 , 1, 'Plane'
union
Select 4, 2, 'BMW'
union
Select 5, 4, 'CLK'
should by optimized, have just a lack of time ...
Function GetMenu(pop:TPopupmenu;ID:Integer):TMenuItem;
var
i:Integer;
Function CheckItem(mi:TMenuItem):TMenuItem;
var
i:Integer;
begin
Result := nil;
if mi.Name = 'DYN_' + INtToStr(ID) then Result := mi
else for i := 0 to mi.Count-1 do
if not Assigned(Result) then Result := CheckItem(mi[i]);
end;
begin
Result := nil;
for i := 0 to pop.Items.Count-1 do
begin
if not Assigned(Result) then Result := CheckItem(pop.Items[i]);
if Assigned(Result) then Break;
end;
end;
Function InsertMenuItem(pop:TPopupMenu;mi:TMenuItem;ID:Integer;Const caption:String):TMenuItem;
begin
Result := TMenuItem.Create(pop);
Result.Caption := caption;
Result.Name := 'DYN_' + INtToStr(ID) ;
if not Assigned(mi) then pop.Items.Add(Result) else mi.Add(Result);
end;
Function AddMenuItem(pop:TPopupmenu;ID:Integer;Ads:TDataset):TMenuItem;
begin
Ads.Locate('ID',ID,[]);
Result := GetMenu(pop,id);
if (not Assigned(Result)) then
begin
if (Ads.FieldByName('parent_ID').AsInteger<>0) then
begin
result := AddMenuItem(pop,Ads.FieldByName('parent_ID').AsInteger,Ads);
Ads.Locate('ID',ID,[]);
end;
Result := InsertMenuItem(pop,Result,ID,Ads.FieldByName('Name').AsString);
end;
Ads.Locate('ID',ID,[]);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
while not ADS.Eof do
begin
AddMenuItem(Popupmenu1,ads.FieldByName('ID').AsInteger,Ads);
Ads.Next
end;
end;
Interesting conundrum ...another late night thought, a practical answer for re-use :)
Make a derived component:
type
TCascadeMenuItem = class(TMenuItem)
private
Id: Integer;
public
function AddItem(const ToId, WithId: Integer; AName: string): Boolean;
end;
with code
function TCascadeMenuItem.AddItem(const ToId, WithId: Integer; AName: string): Boolean;
var
i: Integer;
cmi: TCascadeMenuItem;
begin
if ToId = Id then
begin
cmi := TCascadeMenuItem.Create(Owner);
cmi.Caption := AName;
cmi.Id := WithId;
Add(cmi);
Result := True;
end
else begin
i := 0;
Result := False;
while (i < Count) and (not Result) do
begin
Result := TCascadeMenuItem(Items[i]).AddItem(ToId,WithId, ANAme);
inc(i);
end;
end;
end;
Main form, Assumes your data:
procedure TForm4.Button2Click(Sender: TObject);
var
mi: TCascadeMenuItem;
i: Integer;
Added: Boolean;
begin
cds1.First;
while not cds1.Eof do
begin
i := 0;
Added := False;
while (i < pup.Items.Count) and (not Added) do
begin
Added := TCascadeMenuItem(pup.Items[i]).AddItem(cds1Parent_Id.AsInteger, cds1id.AsInteger, cds1name.AsString);
inc(i);
end;
if not Added then
begin // new root
mi := TCasCadeMenuItem.Create(Self);
mi.Caption := cds1name.AsString;
mi.id := cds1Parent_Id.AsInteger;
pup.Items.Add(mi);
end;
cds1.Next;
end;
end;
You could derive a TCascasePopupMenu and put it on the palette :)

Hiding items in TListBox while filtering by String

Short Version: Is there any way to control or modify LisBox items individually? for example set their Visible property to False separately.
I found a TListBoxItem class in Fire Monkey when I was searching, but I don't want to use Fire Monkey and want it in VCL.
Detailed Version:
I tried to filter my ListBox using two TStringList and an Edit, one StringList is global to keep the original list (list_files_global) and another StringList to help filtering procedure (list_files_filter) and my primary list of files is my ListBox (list_files).
I created my global StringList on onCreate event while program is starting to store my original list:
procedure Tfrm_main.FormCreate(Sender: TObject);
Begin
list_files_global := TStringList.Create;
list_files_global.Assign(list_files.Items);
End;
and used Edit's onChange event for filtering:
procedure Tfrm_main.edit_files_filterChange(Sender: TObject);
Var
list_files_filter: TStringList;
i: Integer;
Begin
list_files_filter := TStringList.Create;
list_files_filter.Assign(list_files.Items);
list_files.Clear;
for i := 0 to list_files_filter.Count - 1 do
if pos(edit_files_filter.text, list_files_filter[i]) > 0 then
list_files.Items.Add(list_files_filter[i]);
End;
and for switching off the filter, just recover the list from my global list that I created at first:
list_files.Items := list_files_global;
here so far, everything works just fine, but problem is when I'm trying to edit/rename/delete items from filtered list, for example I change an item:
list_files.Items[i] := '-- Changed Item --';
list will be edited, but when I switch off the filter, the original list will be back and all changes are lost.
so I want to know is there any proper way to solve this problem? Something like hiding items individually or change items visibility, etc... so I can change the filtering algorithm and get rid of all this making extra lists.
I searched the internet and looked into Delphi's help file for a whole day and nothing useful came up.
The items of a VCL listbox, List Box in the API, does not have any visibility property. The only option for not showing an item is to delete it.
You can use the control in virtual mode however, where there are no items at all. You decide what data to keep, what to display. That's LBS_NODATA window style in the API. In VCL, set the style property to lbVirtual.
Extremely simplified example follows.
Let's keep an array of records, one record per virtual item.
type
TListItem = record
FileName: string;
Visible: Boolean;
end;
TListItems = array of TListItem;
You can extend the fields as per your requirements. Visibility is one of the main concerns in the question, I added that. You'd probably add something that represents the original name so that you know what name have been changed, etc..
Have one array per listbox. This example contains one listbox.
var
ListItems: TListItems;
Better make it a field though, this is for demonstration only.
Required units.
uses
ioutils, types;
Some initialization at form creation. Empty the filter edit. Set listbox style accordingly. Fill up some file names. All items will be visible at startup.
procedure TForm1.FormCreate(Sender: TObject);
var
ListFiles: TStringDynArray;
i: Integer;
begin
ListFiles := ioutils.TDirectory.GetFiles(TDirectory.GetCurrentDirectory);
SetLength(ListItems, Length(ListFiles));
for i := 0 to High(ListItems) do begin
ListItems[i].FileName := ListFiles[i];
ListItems[i].Visible := True;
end;
ListBox1.Style := lbVirtual;
ListBox1.Count := Length(ListFiles);
Edit1.Text := '';
end;
In virtual mode the listbox is only interested in the Count property. That will arrange how many items will show, accordingly the scrollable area.
Here's the filter part, this is case sensitive.
procedure TForm1.Edit1Change(Sender: TObject);
var
Text: string;
Cnt: Integer;
i: Integer;
begin
Text := Edit1.Text;
if Text = '' then begin
for i := 0 to High(ListItems) do
ListItems[i].Visible := True;
Cnt := Length(ListItems);
end else begin
Cnt := 0;
for i := 0 to High(ListItems) do begin
ListItems[i].Visible := Pos(Text, ListItems[i].FileName) > 0;
if ListItems[i].Visible then
Inc(Cnt);
end;
end;
ListBox1.Count := Cnt;
end;
The special case in the edit's OnChange is that when the text is empty. Then all items will show. Otherwise code is from the question. Here we also keep the total number of visible items, so that we can update the listbox accordingly.
Now the only interesting part, listbox demands data.
procedure TForm1.ListBox1Data(Control: TWinControl; Index: Integer;
var Data: string);
var
VisibleIndex: Integer;
i: Integer;
begin
VisibleIndex := -1;
for i := 0 to High(ListItems) do begin
if ListItems[i].Visible then
Inc(VisibleIndex);
if VisibleIndex = Index then begin
Data := ListItems[i].FileName;
Break;
end;
end;
end;
What happens here is that the listbox requires an item to show providing its index. We loop through the master list counting visible items to find out which one matches that index, and supply its text.
This is something I often do, but with list views instead of list boxes. The basic principles are the same, though.
I tend to store the individual items as objects, which are reference types in Delphi. And I keep them all in one main unfiltered list, which owns the objects, while I maintain a filtered list (which does not own the objects) for display purposes. Like #Sertac, I combine this with a virtual list view.
To see how this works in practice, create a new VCL application and drop a list view (lvDisplay) and an edit control (eFilter) on the main form:
Notice I have added three columns to the list view control: "Name", "Age", and "Colour". I also make it virtual (OwnerData = True).
Now define the class for the individual data items:
type
TDogInfo = class
Name: string;
Age: Integer;
Color: string;
constructor Create(const AName: string; AAge: Integer; const AColor: string);
function Matches(const AText: string): Boolean;
end;
where
{ TDogInfo }
constructor TDogInfo.Create(const AName: string; AAge: Integer;
const AColor: string);
begin
Name := AName;
Age := AAge;
Color := AColor;
end;
function TDogInfo.Matches(const AText: string): Boolean;
begin
Result := ContainsText(Name, AText) or ContainsText(Age.ToString, AText) or
ContainsText(Color, AText);
end;
And let us create the unfiltered list of dogs:
TForm1 = class(TForm)
eFilter: TEdit;
lvDisplay: TListView;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FList, FFilteredList: TObjectList<TDogInfo>;
public
end;
where
function GetRandomDogName: string;
const
DogNames: array[0..5] of string = ('Buster', 'Fido', 'Pluto', 'Spot', 'Bill', 'Rover');
begin
Result := DogNames[Random(Length(DogNames))];
end;
function GetRandomDogColor: string;
const
DogColors: array[0..2] of string = ('Brown', 'Grey', 'Black');
begin
Result := DogColors[Random(Length(DogColors))];
end;
procedure TForm1.FormCreate(Sender: TObject);
var
i: Integer;
begin
FList := TObjectList<TDogInfo>.Create(True); // Owns the objects
// Populate with sample data
for i := 1 to 1000 do
FList.Add(
TDogInfo.Create(GetRandomDogName, Random(15), GetRandomDogColor)
);
FFilteredList := FList;
lvDisplay.Items.Count := FFilteredList.Count;
lvDisplay.Invalidate;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
if FFilteredList <> FList then
FreeAndNil(FFilteredList);
FreeAndNil(FList);
end;
The idea is that the list view control always displays the FFilteredList, which either points to the same object instance as FList, or points to a filtered (or sorted) version of it:
// The list view's OnData event handler
procedure TForm1.lvDisplayData(Sender: TObject; Item: TListItem);
begin
if FFilteredList = nil then
Exit;
if not InRange(Item.Index, 0, FFilteredList.Count - 1) then
Exit;
Item.Caption := FFilteredList[Item.Index].Name;
Item.SubItems.Add(FFilteredList[Item.Index].Age.ToString);
Item.SubItems.Add(FFilteredList[Item.Index].Color);
end;
// The edit control's OnChange handler
procedure TForm1.eFilterChange(Sender: TObject);
var
i: Integer;
begin
if string(eFilter.Text).IsEmpty then // no filter, display all items
begin
if FFilteredList <> FList then
begin
FreeAndNil(FFilteredList);
FFilteredList := FList;
end;
end
else
begin
if (FFilteredList = nil) or (FFilteredList = FList) then
FFilteredList := TObjectList<TDogInfo>.Create(False); // doesn't own the objects
FFilteredList.Clear;
for i := 0 to FList.Count - 1 do
if FList[i].Matches(eFilter.Text) then
FFilteredList.Add(FList[i]);
end;
lvDisplay.Items.Count := FFilteredList.Count;
lvDisplay.Invalidate;
end;
The result:
Notice that there always is only one in-memory object for each dog, so if you rename a dog, the changes will reflect in the list view, filtered or not. (But don't forget to invalidate it!)

How do I sort a generic list using a custom comparer?

I'm kinda a Delphi-newbie and I don't get how the Sort method of a TList of Records is called in order to sort the records by ascending integer value.
I have a record like the following:
type
TMyRecord = record
str1: string;
str2: string;
intVal: integer;
end;
And a generic list of such records:
TListMyRecord = TList<TMyRecord>;
Have tried to find a code-example in the help files and found this one:
MyList.Sort(#CompareNames);
Which I can't use, since it uses classes. So I tried to write my own compare function with a little different parameters:
function CompareIntVal(i1, i2: TMyRecord): Integer;
begin
Result := i1.intVal - i2.intVal;
end;
But the compiler always throws a 'not enough parameters' - error when I call it with open.Sort(CompareIntVal);, which seems obvious; so I tried to stay closer to the help file:
function SortKB(Item1, Item2: Pointer): Integer;
begin
Result:=PMyRecord(Item1)^.intVal - PMyRecord(Item2)^.intVal;
end;
with PMyRecord as PMyRecord = ^TMyRecord;
I have tried different ways of calling a function, always getting some error...
The Sort overload you should be using is this one:
procedure Sort(const AComparer: IComparer<TMyRecord>);
Now, you can create an IComparer<TMyRecord> by calling TComparer<TMyRecord>.Construct. Like this:
var
Comparison: TComparison<TMyRecord>;
....
Comparison :=
function(const Left, Right: TMyRecord): Integer
begin
Result := Left.intVal-Right.intVal;
end;
List.Sort(TComparer<TMyRecord>.Construct(Comparison));
I've written the Comparison function as an anonymous method, but you could also use a plain old style non-OOP function, or a method of an object.
One potential problem with your comparison function is that you may suffer from integer overflow. So you could instead use the default integer comparer.
Comparison :=
function(const Left, Right: TMyRecord): Integer
begin
Result := TComparer<Integer>.Default.Compare(Left.intVal, Right.intVal);
end;
It might be expensive to call TComparer<Integer>.Default repeatedly so you could store it away in a global variable:
var
IntegerComparer: IComparer<Integer>;
....
initialization
IntegerComparer := TComparer<Integer>.Default;
Another option to consider is to pass in the comparer when you create the list. If you only ever sort the list using this ordering then that's more convenient.
List := TList<TMyRecord>.Create(TComparer<TMyRecord>.Construct(Comparison));
And then you can sort the list with
List.Sort;
The concise answer:
uses
.. System.Generics.Defaults // Contains TComparer
myList.Sort(
TComparer<TMyRecord>.Construct(
function(const Left, Right: TMyRecord): Integer
begin
Result := Left.intVal - Right.intVal;
end
)
);
I want to share my solution (based on the input I have gathered here).
It's a standard setup. A filedata class that holds data of a single file in a generic TObjectList. The list has the two private attributes fCurrentSortedColumn and fCurrentSortAscending to control the sort order. The AsString-method is the path and filename combined.
function TFileList.SortByColumn(aColumn: TSortByColums): boolean;
var
Comparison: TComparison<TFileData>;
begin
result := false;
Comparison := nil;
case aColumn of
sbcUnsorted : ;
sbcPathAndName: begin
Comparison := function(const Left, Right: TFileData): integer
begin
Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
end;
end;
sbcSize : begin
Comparison := function(const Left, Right: TFileData): integer
begin
Result := TComparer<int64>.Default.Compare(Left.Size,Right.Size);
if Result = 0 then
Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
end;
end;
sbcDate : begin
Comparison := function(const Left, Right: TFileData): integer
begin
Result := TComparer<TDateTime>.Default.Compare(Left.Date,Right.Date);
if Result = 0 then
Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
end;
end;
sbcState : begin
Comparison := function(const Left, Right: TFileData): integer
begin
Result := TComparer<TFileDataTestResults>.Default.Compare(Left.FileDataResult,Right.FileDataResult);
if Result = 0 then
Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
end;
end;
end;
if assigned(Comparison) then
begin
Sort(TComparer<TFileData>.Construct(Comparison));
// Control the sort order
if fCurrentSortedColumn = aColumn then
fCurrentSortAscending := not fCurrentSortAscending
else begin
fCurrentSortedColumn := aColumn;
fCurrentSortAscending := true;
end;
if not fCurrentSortAscending then
Reverse;
result := true;
end;
end;
I found a much simpler modified sort function to alphabetize a TList of records or nonstandard list of items.
Example
PList = ^TContact;
TContact = record //Record for database of user contact records
firstname1 : string[20];
lastname1 : string[20];
phonemobile : Integer; //Fields in the database for contact info
phonehome : Integer;
street1 : string;
street2 : string;
type
TListSortCompare = function (Item1,
Item2: TContact): Integer;
var
Form1: TForm1;
Contact : PList; //declare record database for contacts
arecord : TContact;
Contacts : TList; //List for the Array of Contacts
function CompareNames(i1, i2: TContact): Integer;
begin
Result := CompareText(i1.lastname1, i2.lastname1) ;
end;
and the function to call to sort your list
Contacts.Sort(#CompareNames);

Delphi - What object (multidimensional array, etc) will work?

I have a need to keep the top ten values in sorted order. My data structure is:
TMyRecord = record
Number: Integer;
Value: Float;
end
I will be calculating a bunch of float values. I need to keep the top 10 float values. Each value has an associated number. I want to add "sets"... If my float Value is higher than one of the top 10, it should add itself to the list, and then the "old" number 10, now 11, gets discarded. I should be able to access the list in (float value) sorted order...
It is almost like a TStringList, which maintains sorted order....
Is there anything like this already built into Delphi 2010?
You can use a combination of the generic list Generics.Collections.TList<TMyRecord> and insertion sort.
Your data structure is like this
TMyRecord = record
Number: Integer;
Value: Float;
end;
var
Top10: TList<TMyRecord>;
You'll need to use Generics.Collections to get the generic list.
Instantiate it like this:
Top10 := TList<TMyRecord>.Create;
Use this function to add to the list:
procedure Add(const Item: TMyRecord);
var
i: Integer;
begin
for i := 0 to Top10.Count-1 do
if Item.Value>Top10[i].Value then
begin
Top10.Insert(i, Item);
Top10.Count := Min(10, Top10.Count);
exit;
end;
if Top10.Count<10 then
Top10.Add(Item);
end;
This is a simple implementation of insertion sort. The key to making this algorithm work is to make sure the list is always ordered.
David's answer is great, but I think as you progress through the data, you'll fill the list pretty fast, and the odds of having a value greater than what's already in the list probably decreases over time.
So, for performance, I think you could add this line before the for loop to quickly discard values that don't make it into the top 10:
if (Item.Value <= Top10[Top10.Count - 1].Value) and (Top10.Count = 10) then
Exit;
If the floats are always going to be above a certain threshold, it might make sense to initialize the array with 10 place-holding records with values below the threshold just so you could change the first line to this:
if (Item.Value <= Top10[9].Value) then
Exit;
And improve the method to this:
procedure Add(const Item: TMyRecord);
var
i: Integer;
begin
// Throw it out if it's not bigger than our smallest top10
if (Item.Value <= Top10[9].Value) then
Exit;
// Start at the bottom, since it's more likely
for i := 9 downto 1 do
if Item.Value <= Top10[i - 1].Value then
begin
// We found our spot
Top10.Insert(i, Item);
// We're always setting it to 10 now
Top10.Count := 10;
// We're done
Exit;
end;
// Welcome, leader!
Top10.Insert(0, Item);
// We're always setting it to 10 now
Top10.Count := 10;
end;
Since you are working with a fixed number of items, you could use a plain TMyRecord array, eg:
type
TMyRecord = record
Number: Integer;
Value: Float;
end;
const
MaxRecordsInTopTen = 10;
var
TopTen: array[0..MaxRecordsInTopTen-1] of TMyRecord;
NumRecordsInTopTen: Integer = 0;
procedure CheckValueForTopTen(Value: Float; Number: Integer);
var
I, J, NumToMove: Integer;
begin
// see if the new Value is higher than an value already in the list
for I := 0 to (NumRecordsInTopTen-1) do
begin
if Value > TopTen[I].Value then
begin
// new Value is higher then this value, insert before
// it, moving the following values down a slot, and
// discarding the last value if the list is full
if NumRecordsInTopTen < MaxRecordsInTopTen then
NumToMove := NumRecordsInTopTen - I
else
NumToMove := MaxRecordsInTopTen - I - 1;
for J := 1 to NumToMove do
Move(TopTen[NumRecordsInTopTen-J], TopTen[NumRecordsInTopTen-J-1], SizeOf(TMyRecord));
// insert the new value now
TopTen[I].Number := Number;
TopTen[I].Value := Value;
NumRecordsInTopTen := Min(NumRecordsInTopTen+1, MaxRecordsInTopTen);
// all done
Exit;
end;
end;
// new value is lower then existing values,
// insert at the end of the list if room
if NumRecordsInTopTen < MaxRecordsInTopTen then
begin
TopTen[NumRecordsInTopTen].Number := Number;
TopTen[NumRecordsInTopTen].Value := Value;
Inc(NumRecordsInTopTen);
end;
end;
I wouldn't bother with anything other than straight Object Pascal.
{$APPTYPE CONSOLE}
program test2; uses sysutils, windows;
const
MAX_VALUE = $7FFF;
RANDNUMCOUNT = 1000;
var
topten: array[1..10] of Longint;
i, j: integer;
Value: Longint;
begin
randomize;
FillChar(topten, Sizeof(topten), 0);
for i := 1 to RANDNUMCOUNT do
begin
Value := Random(MAX_VALUE);
j := 1;
while j <= 10 do
begin
if Value > topten[j] then
begin
Move(topten[j], topten[j+1], SizeOf(Longint) * (10-j));
topten[j] := Value;
break;
end;
inc(j);
end;
end;
writeln('Top ten numbers generated were: ');
for j := 1 to 10 do
writeln(j:2, ': ', topten[j]);
readln;
end.

Delphi: how to set the length of a RTTI-accessed dynamic array using DynArraySetLength?

I'd like to set the length of a dynamic array, as suggested in this post. I have two classes TMyClass and the related TChildClass defined as
TChildClass = class
private
FField1: string;
FField2: string;
end;
TMyClass = class
private
FField1: TChildClass;
FField2: Array of TChildClass;
end;
The array augmentation is implemented as
var
RContext: TRttiContext;
RType: TRttiType;
Val: TValue; // Contains the TMyClass instance
RField: TRttiField; // A field in the TMyClass instance
RElementType: TRttiType; // The kind of elements in the dyn array
DynArr: TRttiDynamicArrayType;
Value: TValue; // Holding an instance as referenced by an array element
ArrPointer: Pointer;
ArrValue: TValue;
ArrLength: LongInt;
i: integer;
begin
RContext := TRTTIContext.Create;
try
RType := RContext.GetType(TMyClass.ClassInfo);
Val := RType.GetMethod('Create').Invoke(RType.AsInstance.MetaclassType, []);
RField := RType.GetField('FField2');
if (RField.FieldType is TRttiDynamicArrayType) then begin
DynArr := (RField.FieldType as TRttiDynamicArrayType);
RElementType := DynArr.ElementType;
// Set the new length of the array
ArrValue := RField.GetValue(Val.AsObject);
ArrLength := 3; // Three seems like a nice number
ArrPointer := ArrValue.GetReferenceToRawData;
DynArraySetLength(ArrPointer, ArrValue.TypeInfo, 1, #ArrLength);
{ TODO : Fix 'Index out of bounds' }
WriteLn(ArrValue.IsArray, ' ', ArrValue.GetArrayLength);
if RElementType.IsInstance then begin
for i := 0 to ArrLength - 1 do begin
Value := RElementType.GetMethod('Create').Invoke(RElementType.AsInstance.MetaclassType, []);
ArrValue.SetArrayElement(i, Value);
// This is just a test, so let's clean up immediatly
Value.Free;
end;
end;
end;
ReadLn;
Val.AsObject.Free;
finally
RContext.Free;
end;
end.
Being new to D2010 RTTI, I suspected the error could depend on getting ArrValue from the class instance, but the subsequent WriteLn prints "TRUE", so I've ruled that out. Disappointingly, however, the same WriteLn reports that the size of ArrValue is 0, which is confirmed by the "Index out of bounds"-exception I get when trying to set any of the elements in the array (through ArrValue.SetArrayElement(i, Value);). Do anyone know what I'm doing wrong here? (Or perhaps there is a better way to do this?) TIA!
Dynamic arrays are kind of tricky to work with. They're reference counted, and the following comment inside DynArraySetLength should shed some light on the problem:
// If the heap object isn't shared (ref count = 1), just resize it. Otherwise, we make a copy
Your object is holding one reference to it, and so is the TValue. Also, GetReferenceToRawData gives you a pointer to the array. You need to say PPointer(GetReferenceToRawData)^ to get the actual array to pass to DynArraySetLength.
Once you've got that, you can resize it, but you're left with a copy. Then you have to set it back onto the original array.
TValue.Make(#ArrPointer, dynArr.Handle, ArrValue);
RField.SetValue(val.AsObject, arrValue);
All in all, it's probably a lot simpler to just use a list instead of an array. With D2010 you've got Generics.Collections available, which means you can make a TList<TChildClass> or TObjectList<TChildClass> and have all the benefits of a list class without losing type safety.
I think you should define the array as a separate type:
TMyArray = array of TMyClass;
and use that.
From an old RTTI based XML serializer I know the general method that you use should work (D7..2009 tested):
procedure TXMLImpl.ReadArray(const Name: string; TypeInfo: TArrayInformation; Data: Pointer; IO: TParameterInputOutput);
var
P: PChar;
L, D: Integer;
BT: TTypeInformation;
begin
FArrayType := '';
FArraySize := -1;
ComplexTypePrefix(Name, '');
try
// Get the element type info.
BT := TypeInfo.BaseType;
if not Assigned(BT) then RaiseSerializationReadError; // Not a supported datatype!
// Typecheck the array specifier.
if (FArrayType <> '') and (FArrayType <> GetTypeName(BT)) then RaiseSerializationReadError;
// Do we have a fixed size array or a dynamically sized array?
L := FArraySize;
if L >= 0 then begin
// Set the array
DynArraySetLength(PPointer(Data)^,TypeInfo.TypeInformation,1,#L);
// And restore he elements
D := TypeInfo.ElementSize;
P := PPointer(Data)^;
while L > 0 do begin
ReadElement(''{ArrayItemName},BT,P,IO); // we allow any array item name.
Inc(P,D);
Dec(L);
end;
end else begin
RaiseNotSupported;
end;
finally
ComplexTypePostfix;
end;
end;
Hope this helps..

Resources