I am writing a program in Delphi 10.4 that is reading multiple tables from a database into a dynamic array of records. The SQL query already sorts the values by the name during the initial load of the data.
These records are then displayed on a ListView under different columns. I want to give the user the option to click on a column to sort the values according to that column. Up to this point, everything works perfectly fine. I have the current code below, and you are welcome to point out any mistakes I made.
First, I declare the record type:
type
TDDNS = record
ID : Integer; --the ID in the database
Name : String; --the client name
Alias : string; --an alias for the client
Domain : string; --the DDNS address
Login : String; --DDNS login username
Password: string; --DDNS login password
Renewed: TDate; --Date DDNS account was renewed
IsActive: Boolean; --Boolean if account is still active
end;
Secondly, I create the dynamic array:
DDNSDetails : array of TDDNS;
The data is then read into the array.
The Login and Password data is not displayed in the ListView for obvious reasons.
For the sorting, I use the following code:
procedure lvDDNSColumnClick(Sender: TObject;
Column: TListColumn);
begin
SortList(Column.Index);
ReloadLV();
end;
procedure SortList(Col : Integer);
var
i, j : Integer;
begin
if Length(DDNSDetails) > 0 then
begin
for i := 0 to Length(DDNSDetails)-1 do
begin
for j := i 1 to Length(DDNSDetails)-1 do
begin
if Col = 0 then //Name
begin
if UpperCase(DDNSDetails[i].Name) > UpperCase(DDNSDetails[j].Name) then
Resort(i, j);
end else
if Col = 1 then //Alias
begin
if UpperCase(DDNSDetails[i].Alias) > UpperCase(DDNSDetails[j].Alias) then
Resort(i, j);
end else
if Col = 2 then //Domain
begin
if UpperCase(DDNSDetails[i].Domain) > UpperCase(DDNSDetails[j].Domain) then
Resort(i, j);
end else
if (Col = 3) or (Col = 4) then //Renewal date
begin
if DDNSDetails[i].Renewed > DDNSDetails[j].Renewed then
Resort(i, j);
end;
end;
end;
lvDDNS.Columns[0].Caption := 'Client Name';
lvDDNS.Columns[1].Caption := 'Trading As';
lvDDNS.Columns[2].Caption := 'Domain Address';
lvDDNS.Columns[3].Caption := 'Renewed';
lvDDNS.Columns[4].Caption := 'Active';
lvDDNS.Columns[Col].Caption := '|| ' lvDDNS.Columns[Col].Caption ' ||';
end;
end;
procedure Resort(var i, j : Integer);
var
tempInt : Integer;
temp : string;
tempDate : TDate;
tempBool : Boolean;
begin
tempInt := DDNSDetails[i].ID;
DDNSDetails[i].ID := DDNSDetails[j].ID;
DDNSDetails[j].ID := tempInt;
temp := DDNSDetails[i].Name;
DDNSDetails[i].Name := DDNSDetails[j].Name;
DDNSDetails[j].Name := temp;
temp := DDNSDetails[i].Alias;
DDNSDetails[i].Alias := DDNSDetails[j].Alias;
DDNSDetails[j].Alias := temp;
temp := DDNSDetails[i].Domain;
DDNSDetails[i].Domain := DDNSDetails[j].Domain;
DDNSDetails[j].Domain := temp;
tempDate := DDNSDetails[i].Renewed;
DDNSDetails[i].Renewed := DDNSDetails[j].Renewed;
DDNSDetails[j].Renewed := tempDate;
tempBool := DDNSDetails[i].IsActive;
DDNSDetails[i].IsActive := DDNSDetails[j].IsActive;
DDNSDetails[j].IsActive := tempBool;
temp := DDNSDetails[i].Login;
DDNSDetails[i].Login := DDNSDetails[j].Login;
DDNSDetails[j].Login := temp;
temp := DDNSDetails[i].Password;
DDNSDetails[i].Password := DDNSDetails[j].Password;
DDNSDetails[j].Password := temp;
end;
The purpose of this program is to display DDNS records and login credentials for different DDNS accounts and some clients have more than once account.
What happens is, for example, if you sort by the DDNS renewal date, there may be 50 entries for 23/07/2022 and client "f" has 5 entries under that day, however those 5 entries are not together. In the Name column you might see
z
w
g
x
f
z
a
f
.....
The result should be
a
f
f
f
f
f
g
w
x
z
z
.....
The sorting works perfectly for each column selected. I now need to sort the name column as a secondary if the user sorts any other column.
EDIT: As per a comment by dummzeuch, I changed procedure Resort to the following:
procedure SwapRecord(var i, j : Integer);
var
temp : TDDNS;
begin
temp := DDNSDetails[i];
DDNSDetails[i] := DDNSDetails[j];
DDNSDetails[j] := temp;
end;
CodePudding user response:
If you are using Delphi 10.4 – try to use generic types. Here what I recommend:
type
//declare new type to store sort rule
TSortRule = record
ColumnID : byte; //number of column
Desc : boolean; //reverse sort direction
end;
//change array to list for storing items, it's much esier to work with it
var
xList : TList<TDDNS>;
//we need somehow passed few sort rules, i prefer TList, something like that:
var
xSortOrder : TList<TSortRule>;
Here is procedure for sorting all this staff:
procedure TForm.SortRecords(AList : TList<TDDNS>; ASortOrder : TList<TSortRule>);
begin
AList.Sort(TComparer<TDDNS>.Construct(
function(const Left, Right: TDDNS): Integer
var
LeftValue, RightValue: TDDNS;
begin
//we go for all sorting rules
for var xSortItem in ASortOrder do begin
//check if current rule is reverse
if not xSortItem.Desc then begin
LeftValue := Left;
RightValue := Right;
end else begin
//it's reverse - switch sides
LeftValue := Right;
RightValue := Left;
end{if..else};
//let's do comparation by correct property
case xSortItem.ColumnID of
0: Result := CompareStr(Left.Name, Right.Name);
1: Result := CompareStr(Left.Alias, Right.Alias);
2: Result := CompareStr(Left.Domain, Right.Domain);
3, 4: Result := TComparer<TDate>.Default.Compare(Left.Renewed, Right.Renewed);
end{case};
//if items not equval by this rule, we skip next rules
if Result <> 0 then
break;
end{for};
end
));
end;
More info about sorting of TList<> you can read in oficial doc or Here example
CodePudding user response:
Based on my initial code I managed to modify that and get it working in a much simpler way.
Firstly, I created a new class for the record type and sort procedure and I declared them as follows:
type
TRec = record
dbID : Integer;
Name : String;
Alias : string;
Domain : string;
Login : String;
Password: string;
Renewed: TDate;
IsActive: Boolean;
end;
type
TData = array of TRec;
procedure SortData(Data : TData; const Field : Integer);
procedure SwapRecords(var Data : TData; const i, j : Integer);
SortData performs the comparing for the sorting and SwapRecords swaps the entries during the sorting procedure. SortData uses Goto to go to the bottom of the loop once it finds the field it needs to sort in order for it to save time and start with the next cycle.
The procedures are scripted as follows:
procedure SortData(Data : TData; const Field : Integer);
var
n, newn, i : integer;
label
bottom;
begin
n := length(Data);
repeat
newn := 0;
for i := 1 to n-1 do
begin
if Field = 1 then //Name
begin
if UpperCase(Data[i-1].Name) > UpperCase(Data[i].Name) then
begin
SwapRecords(Data, i-1, i);
newn := i;
Goto bottom;
end;
end;
if Field = 2 then //Alias
begin
if UpperCase(Data[i-1].Alias) > UpperCase(Data[i].Alias) then
begin
SwapRecords(Data, i-1, i);
newn := i;
Goto bottom;
end;
end;
if Field = 3 then //Domain
begin
if UpperCase(Data[i-1].Domain) > UpperCase(Data[i].Domain) then
begin
SwapRecords(Data, i-1, i);
newn := i;
Goto bottom;
end;
end;
if Field = 3 then //Login
begin
if UpperCase(Data[i-1].Login) > UpperCase(Data[i].Login) then
begin
SwapRecords(Data, i-1, i);
newn := i;
Goto bottom;
end;
end;
if Field = 4 then //Password
begin
if UpperCase(Data[i-1].Password) > UpperCase(Data[i].Password) then
begin
SwapRecords(Data, i-1, i);
newn := i;
Goto bottom;
end;
end;
if Field = 5 then //Renewed
begin
if Data[i-1].Renewed > Data[i].Renewed then
begin
SwapRecords(Data, i-1, i);
newn := i;
Goto bottom;
end;
end;
if Field = 6 then //IsActive
begin
if Data[i-1].IsActive > Data[i].IsActive then
begin
SwapRecords(Data, i-1, i);
newn := i;
Goto bottom;
end;
end;
bottom:
end;
n := newn;
until n < 1;
end;
procedure SwapRecords(var Data : TData; const i, j : Integer);
var
temp : TRec;
begin
temp := Data[i];
Data[i] := Data[j];
Data[j] := temp;
end;
Finally, in the main form I call this procedure after creating and filling a varialbe (DDNSInfo of TData).
procedure TfrmDDNS.lvDDNSColumnClick(Sender: TObject;
Column: TListColumn);
var
Field : Integer;
begin
ColActive := Column.Index;//ColActive is a global Integer in the form to keep track of which column is selected in the TListView
case ColActive of
0 : Field := 1;//Client Name
1 : Field := 2;//Trading As
2 : Field := 3;//Domain Address
3 : Field := 6;//Renewed
4 : Field := 7;//Active
else
Field := 0;
end;
//Sort array
if Field = 6 then
begin
SortData(DDNSInfo,1);//Sort according to Client Name
SortData(DDNSInfo,Field);//Sort according to Renewal Date
end else
SortData(DDNSInfo,Field);//Sort only according to selected column
//Output new array
UpdateLV(lvDDNS,DDNSInfo);//This is another procedure for updating the data displayed in the TListView
end;
I set this up to accomodate sorting according to more than one column and this can be expanded to accomodate more scenarios, such as websites, other types of passwords, etc.