How can I update a data in a TList<T>? - delphi

I have this record (structure):
type
THexData = record
Address : Cardinal;
DataLen : Cardinal;
Data : string;
end;
And I've declared this list:
HexDataList: TList<THexData>;
I've filled the list with some data. Now I'd like scan ListHexData and sometimes update a element of a record inside HexDataList.
Is it possible? How can I do?

var
Item: THexData;
...
for i := 0 to HexDataList.Count-1 do begin
Item := HexDataList[i];
//update Item
HexDataList[i] := Item;
end;
The bind is that you would like to modify HexDataList[i] in place, but you can't. When I'm working with a TList<T> that holds records I actually sub-class TList<T> and replace the Items property with one that returns a pointer to the item, rather than a copy of the item. That allows for inplace modification.
EDIT
Looking at my code again I realise that I don't actually sub-class TList<T> because it the class is too private to extract pointers to the underlying data. That's probably a good decision. What I actually do is implement my own generic list class and that allows me the freedom to return pointers to records if needed.

Related

Storing Component Name as a String for later use

On my form I have a number of TMyQuery Components. Their names identify which MySQL Tables they work with. For example, COMPONENTSTABLE works with the COMPONENTS TABLE, etc.
There are about 30 tables, but that might change in the future.
I also use a basic String List to read field names from a Table called TIMESTAMPS. This table is updated via triggers when an UPDATE, INSERT, or DELETE occurs. Each field within the TIMESTAMPS Table refers to which Table was modified. There's only one record in the table! Based on the field values I can see which table changed so I can refresh it rather than refreshing all of them.
I don't want to do this;
If fieldbyname['COMPONENTSTABLE'] <> CurrentTimeStamp
then ComponentsTable.Refresh;
If fieldbyname['ORDERSTABLE'] <> CurrentTimeStamp
then OrdersTable.Refresh;
{ and so on forever }
What I want to do is;
Right now I have a String List with "Names / Values". Each "Name" is the Fieldname within the Table and "Value" is the TIMESTAMP provided by MySQL Triggers.
I've got the following;
For Idx := 0 to MyStringList.Count -1 do
Begin
If MyStringlist.ValueFromIndex[Idx] <> SomethingElse then
Begin
with (MyStringList.Names[Idx] as tMyQuery).Refresh;
End;
End;
I've got the String List functioning, the Names, the Values etc are all correct.
My question is this;
Is there a way I can use a String ("Names" column in the list) to refer to an Object if that Object exists?
I already have a function I use to refresh individual tables by passing an Object to it, but that's an Object and easy to work with. I'd like to pass the "Object" based on it's name retrieved from a String.
I hope this makes sense and you can follow what I'm after.
I am not sure what your question actually is. In the first part of the answer I assume that you don't really care about names of the objects but rather want some automated way of getting all the tables available refer to a field in another table. Below that, I answer your question about referring to an object if you know its name.
Automated way of handling all tables
It depends on what class your objects are.
From your description, I assume your TMyQuery are TComponent descendants owned by the form. Then the solution is very simple, as each TComponent has both a public Name and a list of owned components Components. You can then use something like this:
var
i: integer;
MyQuery: TMyQuery;
begin
for i := 0 to Pred(MyForm.ComponentCount) do
if MyForm.Components[i] <> TimeStampsTable then
if MyForm.Components[i] is TMyQuery then
begin
MyQuery := TMyQuery(MyForm.Components[i]);
if TimeStampsTable.FieldByName(MyQuery.Name).AsDateTime >= LastAccess then ...
end;
end;
Note that you may want to add extra checks, e.g. to make sure that MyQuery.Name is not empty or that it exists as a field in TimeStampsTable.
If your objects are only TObjects, then there is no "standard" name property and no standard registration of these objects. Name can be handled, apparently your component already has one so it's just a question of a proper type coercion, but object registration is a different matter. You may have to create some kind of a global list for all your created TMyQuery instances.
Getting an object instance based on that object's name
function TMyForm.GetQueryByName(const Name: string): TMyQuery;
var
Obj: TObject;
begin
Result := nil;
Obj := Self.FindComponent(Name);
if Obj <> nil then
if Obj is TMyQuery then
Result := TMyQuery(Obj);
end;
Or you could simply loop over all Components and use your own Name matching.
While the first part of the accepted Answer from #pepak isn't what I was looking for ( I've used similar code in the app previously and found it slow ), the second part of the Answer pointed my in the right direction.
My (thanks to Pepak) eventual solution was;
Function RefreshQueryByName(Const Name: String): Boolean;
Var
Obj: TComponent;
Begin
Result := False;
Obj := Self.FindComponent(Name);
If Obj <> nil Then
If Obj Is TMyQuery Then
With Obj As TMyQuery Do
If Active Then
Begin
Refresh;
Result := True;
End;
End;
Which I use by by passing a String I get from a Field Value that identifies which table I want to refresh.
Now, my Database App automatically refreshes a table changed by other users. It will now refresh any of the 30 tables of they are modified by another user without refreshing all tables.
Thanks for your help Pepak, I've accepted your answer and hope it is useful to others.

search a Generic list

I 'm trouble understanding in modifying the solution from GENERIC SEARCH
as my class is more complex and I need to create several different search functions
procedure TForm1.Button1Click(Sender: TObject);
var
activities: TList<TActivityCategory>;
search: TActivityCategory;
begin
activities := TObjectList<TActivityCategory>.Create(
TDelegatedComparer<TActivityCategory>.Create(
function(const Left, Right: TActivityCategory): Integer
begin
Result := CompareText(Left.Name, Right.Name);
end));
.....
Assume my TActivityCategory looks like
TActivityCategory = class
FirstName : String;
Secondname : String;
onemore .....
end;
How to implement a search for every String inside my activtity class ?
In your place I would write a subclass of TObjectList and add a custom Search method that would look like this:
TSearchableObjectList<T:class> = class(TObjectList<T>)
public
function Search(aFound: TPredicate<T>): T;
end;
The implementation for that method is
function TSearchableObjectList<T>.Search(aFound: TPredicate<T>): T;
var
item: T;
begin
for item in Self do
if aFound(item) then
Exit(item);
Result := nil;
end;
An example of this method is
var
myList: TSearchableObjectList<TActivitycategory>;
item: TActivitycategory;
searchKey: string;
begin
myList := TSearchableObjectList<TActivitycategory>.Create;
// Here you load your list
searchKey := 'WantedName';
// Let´s make it more interesting and perform a case insensitive search,
// by comparing with SameText() instead the equality operator
item := myList.Search(function(aItem : TActivitycategory): boolean begin
Result := SameText(aItem.FirstName, searchKey);
end);
// the rest of your code
end;
The TPredicate<T> type used above is declared in SysUtils, so be sure to add it to your uses clause.
I believe this is the closest we can get to lambda expressions in Delphi.
TList supports searching for items using either linear or binary search. With binary search, the algorithm assumes an ordering. That's not appropriate for your needs. Linear search seems to me to be what you need, and it's available through the Contains method.
The problem is that Contains assumes that you are searching for an entire instance of T. You want to pass a single string to Contains but it won't accept that. It wants a complete record, in your case.
You could provide a Comparer that only compares a single field. And then pass Contains a record with just that one field specified. But that's pretty ugly. Frankly the design of this class is very weak when it comes to searching and sorting. The fact that the comparer is a state variable rather than a parameter is a shocking lapse in my view.
The bottom line is that TList does not readily offer what you are looking for without resorting to ugliness. You should probably implement an old-fashion loop across the list to look for your match.
Note that I'm assuming you want to provide a single string and search for an entry that has a field matching the string. If in fact you do want to provide a complete record and match every field, then Contains does what you need, with suitable Comparer using a lexicographic ordering.

Efficiently change Record members in Generics in Delphi XE

AFAIK we cannot assign directly values to record members if the said record is in a generic structure.
For example, having:
type
TMyRec = record
Width: integer;
Height: integer;
end;
var
myList: TList<TMyRec>;
...
myList[888].Width:=1000; //ERROR here: Left side cannot be assigned.
...
Till now, I used a temporary variable in order to overcome this:
var
...
temp: TMyRec;
...
begin
...
temp:=myList[999];
temp.Width:=1000;
myList[999]:=temp;
...
end;
Ugly, slow, but works. But now I want to add to TMyRec a dynamic array:
type
TMyRec = record
Width: integer;
Height: integer;
Points: array or TPoint;
end;
...or any other data structure which can became big so copying back and forth in a temporary variable isn't a feasible option.
The question is: How to change a member of a record when this record is in a generic structure without needing to copy it in a temporary var?
TIA for your feedback
A dynamic array variable is just a reference to the array. It's stored in the record as a single pointer. So you can continue with your current approach without any excessive copying. Copying an element to a temporary variable only copies a reference to the array and does not copy the array's contents. And even better, if you are assigning to the array's items then you don't need the copy at all. You can write:
myList[666].Points[0] := ...
If you do have a record that really is big then you would be better using a class rather than a record. Because an instance of a class is a reference, the same argument as above applies. For this approach you may prefer TObjectList<> to TList<>. The advantage of TObjectList<> is that you can set the OwnsObjects property to True and let the list be in charge of destroying its members.
You could then write
var
myList: TObjectList<TMyClass>
....
myList[666].SomeProp := NewValue;

How do I add records of type record to a TList<>?

I have a treelist of data. I'm looping through the tree list to match certain records and adding them to a generic TList<>. This works except all record values become the last one added for all items in the TList.
Here's some code:
type
TCompInfo = record
private
class var
FCompanyName : string;
FCompanyPath : string;
FCompanyDataPath: string;
FCompanyVer : string;
public
class procedure Clear; static;
class property CompanyName : string read FCompanyName write FCompanyName;
class property CompanyPath : string read FCompanyPath write FCompanyPath;
class property CompanyDataPath : string read FCompanyDataPath write FCompanyDataPath;
class property CompanyVer : string read FCompanyVer write FCompanyVer;
end;
TCompList = TList<TCompInfo>;
// variablies defined ...
var
CompData : TCompData;
AList : TCompList;
Adding records like this:
tlCompanyList.GotoBOF;
for i := 0 to tlCompanyList.Count-1 do
begin
if colCompanyChecked.Value then
begin
inc(ItemsChecked);
CompData.CompanyName := colCompanyName.Value;
CompData.CompanyDataPath := colCompanyDataPath.Value;
CompData.CompanyPath := colCompanyPath.Value;
CompData.CompanyVer := colCompanyVersion.Value;
AList.Add(CompData);
end;
tlCompanyList.GotoNext;
...or adding records like this:
tlCompanyList.GotoBOF;
for i := 0 to tlCompanyList.Count-1 do
begin
if colCompanyChecked.Value then
begin
inc(ItemsChecked);
AList.Count := ItemsChecked;
AList.Items[ItemsChecked-1].CompanyName := colCompanyName.Value;
AList.Items[ItemsChecked-1].CompanyDataPath := colCompanyDataPath.Value;
AList.Items[ItemsChecked-1].CompanyPath := colCompanyPath.Value;
AList.Items[ItemsChecked-1].CompanyVer := colCompanyVersion.Value;
end;
tlCompanyList.GotoNext;
Results in the exact same thing. AList.Items[0...Count-1] all have the same values. Stepping through the code I can see the correct data is being captured but once I save a new record to AList all previous records take on the same values. This shows me that each item in the TList is a pointer to the same record in memory. If the memory the record is taking changes all items will change. This make since but is not what I want. How do I allocate for new records in a TList to hold different data?
I know I can accomplish the end result in other ways and indeed I have. This has become more of an educational thing for me now using generics and records. I am using Delphi XE.
Thanks
You have declared all the fields of the record as "class var". In a class a "class var"'s value is the same for all instances of the class. Actually I never used "class var" with records, but I guess that the semantic is the same for record types too. Meaning that whenever you change the value of the field in one record, it will change in all existent records.
Try it without "class var" and simple "property" instead of "class property".

Delphi: how to use TObjectList<T>?

I need to understand how to use the generic Delphi 2009 TObjectList. My non-TObjectList attempt looked like
TSomeClass = class(TObject)
private
FList1: Array of TList1;
FList2: Array of TList2;
public
procedure FillArray(var List: Array of TList1; Source: TSource); Overload;
procedure FillArray(var List: Array of TList2; Source: TSource); Overload;
end;
Here, TList1 and TList2 inherits the same constructor constructor TParent.Create(Key: string; Value: string);. However, due to different specialization (e.g. different private fields), they will not be of the same type. So I have to write two nearly identical fill methods:
procedure TSomeClass.FillArray(var List: Array of TList1; Source: TSource);
begin
for i := 0 to Source.List1.Count - 1 do begin
SetLength(List, Length(List) + 1);
List[i] := TList1.Create(Source.List1[i].Key, Source.List1[i].Value);
end;
end;
with FillArray(List: Array of TList2; Source: TSource); being identical, except for the replacement of TList1 with TList2 throughout. As far as I understand, this could be neatly circumvented by using TObjectList and a single fill method; yet, I don't have a clue how to go about this. Do anyone have some good pointers on this? Thanks!
You wouldn't be able to condense that down by using a generic list, since a generic's type is part of the class definition. So a TObjectList<TMyClass1> is different from (and incompatible with) a TObjectList<TMyClass2>. The main benefit of using generic lists over normal TList/TObjectList is improved type safety, with less casts and cleaner code.
Also, if you're using key/value pairs, are you putting them into a list and then retrieving them by searching for a key and returning the associated value? If so, take a look at TDictionary in Generics.Collections. It's a generic key/value hash table that will greatly simplify this process for you.
The Official Embarcadero documentation Wiki on the Generics.Collections.TObjectList contains a simple code example of the TObjectList in action.
I'm not certain exactly what the question is driving at but to address the broad use of a TObjectList, the example initialisation code for a TObjectList might look like this:
var
List: TObjectList<TNewObject>;
Obj: TNewObject;
begin
{ Create a new List. }
List := TObjectList<TNewObject>.Create();
{ Add some items to the List. }
List.Add(TNewObject.Create('One'));
List.Add(TNewObject.Create('Two'));
{ Add a new item, but keep the reference. }
Obj := TNewObject.Create('Three');
List.Add(Obj);
The example code should give you an idea of what the TObjectList can do but If I've understood the question correctly it seems that you would like to be able to add more than one class type to a single instance of the TObjectList? A TObjectList can only be initiated with a single type so it might be better if you initiated the TObjectList with a Interface or Abstract class that is shared by all of the classes you wish to add to it.
One important difference when using a TObjectList compared to creating your own is the existance of the OwnsObjects property which tells the TObjectList whether it owns the objects you add to it and therefore consequently whether it should manage freeing them itself.
Something like this?
TSomeClass = class
private
FList1: TArray<TList1>;
FList2: TArray<TList2>;
public
procedure FillArray<T>(var List: TArray<T>; Source: TSource);
end;
procedure TSomeClass.FillArray<T>(var List: TArray<T>; Source: TSource);
begin
for i := 0 to Source.List1.Count - 1 do begin
SetLength(List, Length(List) + 1);
List[i] := T.Create(Source.List1[i].Key, Source.List1[i].Value);
end;
end;
This, or something like it should do what you want, afaict.

Resources