Delphi 3 Memory Allocation Problem - delphi

I must be missing something rather easy? I’m trying to create a linked list in a Delphi 3 application.
This is implemented via two classes ItemList and Item. The ItemList is instantiated at form creation. It persists for the life of the form. Each Item object is instantiated as needed. The form has a function called AddAcc. AddAcc is called via the on-change event of one of the form's controls.
What happens during this on-change event:
AddAcc is called
AddAcc create a new Item object
AccAdd calls ItemList.AddItem and passes the Item by reference
AddItem places Item object at the tail of the list
I’ve tested AddItem and it works well. My problem is that each time *AddAcc*is called it obtains the same memory location. I’ve tried different means of creating a new Item object. I’ve used New, GetMem (w/ FillChar), and instantiating a local variable of type Item. All calls to AddAcc results in the same memory location being obtained.
I’ve passed the Item object directly (by reference) to AddItem and alternatively passed a pointer to the Item object.
I thought that a reference (pointer) to an instance of the Item object within the linked list would ensure the Item's memory location would be maintained. It appears, however, that it is being collected once the AddAcc class is exited.
FUNCTION AddAcc;
Var
accItem : ptrItem;
BEGIN
GetMem(accItem, sizeOf(Item));
FillChar(accItem^, sizeof(Item), 0);
ItemList.AddItem(accItem^);
End;
Procedure TItemList.AddItem(Var newItem : TAccessoryItem);
begin
Inc(_count);
// add first item to the list
If (_count = 1) Then
begin
_fifoHead := #newItem;
_tail := #newItem;
newItem.Next := #_tail;
newItem.Previous := #_fifoHead;
exit;
end;
_tail^.Next := #newItem;
newItem.Previous := _tail^;
mewItem.Next := #_tail;
_tail := #newItem;
end;
Any help is greatly appreciated.

Here's how I'd write a linked list:
type
PItem = ^TItem;
TItem = record
Next: PItem;
Data: Integer;
end;
procedure Add(var First: PItem; Data: Integer);
var
NewItem: PItem;
begin
New(NewItem);
NewItem.Next := First;
NewItem.Data := Data;
First := NewItem;
end;
...
var
First: PItem;
begin
First := nil;
Add(First, 42);
//etc.
end;
When you need to deallocate your list you do this:
var
Item: PItem;
begin
while Assigned(First) do begin
Item := First;
First := Item.Next;
Dispose(Item);
end;
end;
It is my belief that this is the canonical way of writing linked list type code in Pascal.
I've intentionally written this code for this simplest linked list imaginable. That allows you to focus on the allocation, use of pointers etc. You appear to already know how to maintain the references in your more complex list and so I believe you will have no trouble adapting this style of code to your needs.

Related

Accessing TListItem.Data leads to an error when using records

I have a program which uses TListView to visualize and store some data. TListitem's data property is filled with a pointer to a record like so:
type
TWatch = record
name : string;
path : string;
//...
end;
procedure TfrmProcessWatcherMain.AddWatchToListView(AWatch: TWatch);
var
ANewWatch : TListItem;
begin
ANewWatch := lvWatches.Items.Add; //lvWatches is TListview
//...
ANewWatch.Data:= #AWatch;
end;
When I'm trying to retrieve this data somehow I'm getting access violation error, which is a total surprise for me because all seems legit, here is code of retrieval:
if(lvWatches.Selected <> nil) then begin
tempWatch := TWatch(lvWatches.Selected.Data^); // AV here
ShowMessage(tempWatch.name);
Also AWatch which is passed to a first function is stored in a
WatchList : TList<TWatch>;
so it is accessible using other methods
The problem is that #AWatch is the address of a local variable. As soon as AddWatchToListView returns AWatch goes out of scope and that address is no longer valid.
Instead of taking the address of a local variable you need to allocate a record on the heap using New.
procedure TfrmProcessWatcherMain.AddWatchToListView(AWatch: TWatch);
var
ANewWatch : TListItem;
P : ^TWatch;
begin
ANewWatch := lvWatches.Items.Add;
New(P);
P^ := AWatch;
ANewWatch.Data:= P;
end;
You will need to deallocate the memory with Dispose whenever a list item is destroyed. Do that using the list view's OnDeletion event.
Alternatively, you could store the index of the item in WatchList. Or the address of the record in WatchList, which you get like this: #WatchList.List[Index]. Both of these options rely on WatchList not being modified after references to items are taken, which may be too constraining for you.

Storing interface pointer inside tree view nodes

I'm attempting to store interface pointers in a tree view under TTreeNode.Data properties. While I am able to store an interface pointer (Node.Data := Pointer(MyInterface);) it does not seem to work the other way around (MyInterface := ISomeInterface(Node.Data);). It always comes out nil.
I've also attempted to use manual reference counting, as I've seen required in another question. However, it is still coming out nil and now giving memory leaks.
//Clears tree view and adds drive letters
procedure TfrmMain.cmdRefreshBrowseClick(Sender: TObject);
var
Arr, O: ISuperObject;
X: Integer;
N, C: TTreeNode;
begin
//First clear all items and release their interface refs
for N in tvBrowse.Items do begin
O:= ISuperObject(N.Data);
O._Release;
end;
tvBrowse.Items.Clear;
Arr:= ListDirectory(''); //Returns ISuperObject array listing drives
for X := 0 to Arr.AsArray.Length-1 do begin
O:= Arr.AsArray.O[X];
N:= tvBrowse.Items.Add(nil, O.S['drive']+':\ ['+O.S['type']+']'); //Add root node
N.Data:= Pointer(O); // Assign interface pointer to node data
O._AddRef; //Manually increment interface reference count
C:= tvBrowse.Items.AddChild(N, ''); //Add a fake child node
end;
end;
procedure TfrmMain.tvBrowseExpanding(Sender: TObject; Node: TTreeNode;
var AllowExpansion: Boolean);
var
N, C: TTreeNode;
P, A, O: ISuperObject;
X: Integer;
begin
//Check first node if it's a fake node
N:= Node.getFirstChild;
if N.Text = '' then begin //if first node is a fake node...
P:= ISuperObject(Node.Data); // <-- P always comes out nil here???
N.Delete; //Delete first "fake" node
//Get child files/folders
if Node.Parent = nil then //If root (drive) node...
A:= ListDirectory(P.S['drive']+':\') //Returns ISuperObject array listing files/folders
else
A:= ListDirectory(P.S['name']); //Returns ISuperObject array listing files/folders
for X := 0 to A.AsArray.Length-1 do begin
O:= A.AsArray.O[X];
C:= tvBrowse.Items.AddChild(N, O.S['name']); //Add child node
C.Data:= Pointer(O); //Assign interface pointer to node data
O._AddRef; //Manually increment reference count
end;
end;
end;
What's the appropriate way to do this?
Essentially you are doing this correctly. Your casts are reasonable, and you understand the need to perform manual reference counting since you are holding the reference in a field of type Pointer which does not perform reference counting.
P := ISuperObject(Node.Data);
If P is assigned the value nil that means that Node.Data is equal to nil. There's nothing more to say. Presumably there is some rather mundane reason for Data being nil, but it's nothing to do with the way you are casting.
Looking at your code, I would criticise it for mixing all the different concerns up together. You will find this task much easier if you can maintain a degree of isolation between the various different aspects.
One way to make life much simpler is to avoid using the untyped pointer Data. Instead use a custom node type that can perform proper reference counting:
type
TMyTreeNode = class(TTreeNode)
private
FIntf: IInterface;
property
Intf: IInterface read FIntf write FIntf;
end;
You'll need to handle the OnCreateNodeClass event of the tree view to get the control to create your node class.
procedure TForm1.TreeView1CreateNodeClass(Sender: TCustomTreeView;
var NodeClass: TTreeNodeClass);
begin
NodeClass := TMyTreeNode;
end;
Now whenever the tree view control creates a node instance it creates one of type TMyTreeNode. Which happens to have a field to contain your interface. I've typed it as IInterface here, but you'd use the more specific interface that fits your needs. And of course you can add whatever capability you please to your custom node type.
The mild bind to this is that you need to cast node references from TTreeNode (as returned by the underlying tree view control) to TMyTreeNode in order to gain access to the interface property. However, this bind is well worth it in my view because you can rely on the compiler to managed lifetime correctly, and so forget all about that aspect of the code. This will allow you to concentrate on your program rather than tedious boilerplate. Continuing down the path you are currently on looks like a recipe for memory leaks and access violations. Get the compiler to manage things and you can be sure to avoid any such pitfalls.

Disposing pointers to complex records

I have list of pointers to some complex records. Sometimes when I try disposing them I get invalid pointer operation error. I'm not really sure if I'm creating and disposing them properly.
The record looks like this:
type
PFILEDATA = ^TFILEDATA;
TFILEDATA = record
Description80: TFileType80; // that's array[0..80] of WideChar
pFullPath: PVeryLongPath; // this is pointer to array of WideChar
pNext: PFILEDATA; // this is pointer to the next TFILEDATA record
end;
As I understand when I want a pointer to such record I need to initialize the pointer and the dynamic arrays like this:
function GimmeNewData(): PFILEDATA;
begin
New(Result);
New(Result^.pFullPath);
end;
Now to dispose of series of these records I wrote this:
procedure DisposeData(var pData: PFILEDATA);
var pNextData: PFILEDATA;
begin
while pData^.pNext <> nil do begin
pNextData := pData^.pNext; // Save pointer to the next record
Finalize(pData^.pFullPath); // Free dynamic array
Dispose(pData); // Free the record
pData := pNextData;
end;
Finalize(pData^.pFullPath);
Dispose(pData);
pData := nil;
end;
When I run my program in the debug mode (F9) in the Delphi 2010 IDE something weird happens. When I step trough DisposeData code with F8 it appears that program skips Finalize(pData^.pFullPath) line and jumps to Dispose(pData). Is this normal? Also when Dispose(pData) is executed the Local variables window that displays contents of the pointers does not change. Does this mean that dispose fails?
Edit:
PVeryLongPath is:
type
TVeryLongPath = array of WideChar;
PVeryLongPath = ^TVeryLongPath;
Edit2
So I create 2 TFILEDATA records then I dispose them. Then I create the same 2 records again. For some reason this time pNext in the second record is not nil. It points to the 1st record. Disposing this weird thing gets invalid pointer operation error.
Randomly I have inserted pData^.pNext := nil in the DisposeData procedure.
Now the code looks like this:
procedure DisposeData(var pData: PFILEDATA);
var pNextData: PFILEDATA;
begin
while pData <> nil do begin
pNextData := pData^.pNext;
pData^.pNext := nil; // <----
Dispose(pData^.pFullPath);
Dispose(pData);
pData := pNextData;
end;
end;
The error is gone.
I'll try to change PVeryLongPath into TVeryLongPath.
First, if you free something, the contents of pointers to it do not change. That is why you don't see a change in the local variables display.
EDIT: declare pFullPath as TVeryLongPath. This is a reference type already, and you should not use a pointer to such a type. New() doesn't do what you think it does, in such a case.
It would probably be better if you declared it as UnicodeString, or if your Delphi doesn't have that, WideString.
If pFullPath is declared as a dynamic "array of WideChar", then you should not use New() on it. For dynamic arrays, use SetLength() and nothing else. Dispose() will properly dispose of all items in your record, so just do:
New(Result);
SetLength(Result^.pFullPath, size_you_need);
and later:
Dispose(pData);
In normal code, you should never have to call Finalize(). This is all taken care of by Dispose, as long as you pass a pointer of the correct type to Dispose().
FWIW, I would recommend this and this article of mine.
The fact that you accepted Serg's answer indicates that there is something wrong with your node creation code. Your comment to that answer confirms that.
I'm adding this as a new answer because the edits to the question significantly change it.
Linked list code should look like this:
var
Head: PNode=nil;
//this may be a global variable, or better, a field in a class,
//in which case it would be initialized to nil on creation
function AddNode(var Head: PNode): PNode;
begin
New(Result);
Result.Next := Head;
Head := Result;
end;
Notice that we are adding the node to the head of the list. We don't need to initialize Next to nil anywhere because we always assign another node pointer to Next. That rule is important.
I've written this as a function which returns the new node. Since the new node is always added at the head this is somewhat redundant. Because you can ignore function return values it doesn't really do any harm.
Sometimes you may want to initialize the contents of the node when you add new nodes. For example:
function AddNode(var Head: PNode; const Caption: string): PNode;
begin
New(Result);
Result.Caption := Caption;
Result.Next := Head;
Head := Result;
end;
I much prefer this approach. Always make sure that your fields are initialized. If zero initialization is fine for you then you can use AllocMem to create your node.
Here's a more concrete example of using such a method:
type
PNode = ^TNode;
TNode = record
Caption: string;
Next: PNode;
end;
procedure PopulateList(Items: TStrings);
var
Item: string;
begin
for Item in Items do
AddNode(Head, Item);
end;
To destroy the list the code runs like this:
procedure DestroyList(var Head: PNode);
var
Next: PNode;
begin
while Assigned(Head) do begin
Next := Head.Next;
Dispose(Head);
Head := Next;
end;
end;
You can clearly see that this method can only return when Head is nil.
If you encapsulate your linked list in a class then you can make the head pointer a member of the class and avoid the need to pass it around.
The main point I would like to make is that manual memory allocation code is delicate. It is easy to make little mistakes in the details. In situations like that it pays to put the delicate code in helper functions or methods so you only need to write it once. Linked lists are a great example of a problem that loves to be solved with generics. You can write the memory management code once and re-use it for all sorts of different node types.
I recommend that you avoid using a dynamic array of WideChar which is not at all convenient to work with. Instead use string if you have Delphi 2009 or later, or WideString for earlier Delphi versions. Both of these are dynamic string types with WideChar elements. You can assign to them and Delphi deals with all the allocation.
So, assuming that you now have the following record:
TFILEDATA = record
Description80: TFileType80;
pFullPath: WideString;
pNext: PFILEDATA;
end;
you can simplify things considerably.
function GimmeNewData(): PFILEDATA;
begin
New(Result);
end;
procedure DisposeData(var pData: PFILEDATA);
var pNextData: PFILEDATA;
begin
while pData <> nil do begin
pNextData := pData^.pNext;
Dispose(pData);
pData := pNextData;
end;
end;
You should also initialize pNext field to nil - without it you will finally get access violation. Taking into account what was already said in the previous answers, you can change your code as
type
TFileType80 = array[0..80] of WideChar;
PFILEDATA = ^TFILEDATA;
TFILEDATA = record
Description80: TFileType80;
FullPath: WideString;
pNext: PFILEDATA;
end;
function GimmeNewData: PFILEDATA;
begin
New(Result);
Result^.pNext:= nil;
end;
I think most of your problems are caused by the assumption that New() gives you memory that is zeroed out. I'm pretty sure (and I'm also sure someone will correct me if I'm wrong), but Delphi does not guarantee that that is the case. This can be rectified by changing your code to this:
function GimmeNewData(): PFILEDATA;
begin
New(Result);
ZeroMemory(Result, SizeOf(TFILEDATA));
end;
You should always either zero the memory you get allocated for a record, or at least fill all the fields with something else relevant. This behavior is different to objects, which are guaranteed to be zeroed on allocation.
Hope this helps.

How to store dynamic arrays in a TList?

I need to store an unknown number of groups. Each group has an unknown number of elements/items.
This is my 'group':
TGroup= array of Integer; <------ dynamic array (as you can see) :)
I want to use a TList to hold my groups. The idea is that I may want to access the groups later and add more items to them.
I have this code, but I can't make it work:
TYPE
TGroup= array of Integer; // Each group has x items (x can be from 1 to 10000)
procedure TForm1.FormCreate(Sender: TObject);
VAR CurGroup: TGroup;
grp, item: Integer;
Groups: TList; // can contain up to 1 million groups
begin
Groups:= TList.Create;
{ Init }
for grp:= 1 to 4 DO // Put a dummy item in TList
begin
SetLength(CurGroup, 1); // Create new group
Groups.Add(#CurGroup); // Store it
end;
CurGroup:= NIL; // Prepare for next use
for grp:= 1 to 4 DO // We create 4 groups. Each group has 3 items
begin
CurGroup:= Groups[Groups.Count-1]; // We retrieve the current group from list in order to add more items to it
{ We add few items }
for item:= 0 to 2 DO
begin
SetLength(CurGroup, Length(CurGroup)+1); // reserve space for each new item added
CurGroup[item]:= Item;
end;
Groups[Groups.Count-1]:= #CurGroup; // We put the group back into the list
end;
{ Verify }
CurGroup:= NIL;
CurGroup:= Groups[0];
Assert(Length(CurGroup)> 0); // FAIL
if (CurGroup[0]= 0)
AND (CurGroup[1]= 1)
AND (CurGroup[2]= 2)
then Application.ProcessMessages;
FreeAndNil(Groups);
end;
Note: The code is complete. You can paste it in your Delphi (7) to try it.
Oh, this would be sooooo much nicer in newer versions of Delphi... You'd use the generic, TList<T>. var Groups: TList<TGroup>;
You're best bet is to use another dynamic array: Groups: array of TGroup;
The reason is that dynamic arrays are compiler managed and reference counted. TList only operates on Pointers. There is no straight forward way to keep the reference counts sane when trying to put the dynarrays into the TList.
Another issue you're having is that you're adding the stack address of the dynamic array variable to the TList, and not the actual array. The expression #CurGroup is the "address of the CurGroup variable" which being a local variable, is on the stack.
I've created a wrapper around dynamic array RTTI.
It's just a first draft, but it was inspired by your question, and the fact that the TList methods are missing.
type
TGroup: array of integer;
var
Group: TGroup;
GroupA: TDynArray;
i, v: integer;
begin
GroupA.Init(TypeInfo(TGroup),Group); // associate GroupA with Group
for i := 0 to 1000 do begin
v := i+1000; // need argument passed as a const variable
GroupA.Add(v);
end;
v := 1500;
if GroupA.IndexOf(v)<0 then // search by content
ShowMessage('Error: 1500 not found!');
for i := GroupA.Count-1 downto 0 do
if i and 3=0 then
GroupA.Delete(i); // delete integer at index i
end;
This TDynArray wrapper will work also with array of string or array of records... Records need only to be packed and have only not reference counted fields (byte, integer, double...) or string reference-counted fields (no Variant nor Interface within).
The IndexOf() method will search by content. That is e.g. for an array of record, all record fields (including strings) must match.
See TDynArray in the SynCommons.pas unit from our Source Code repository. Works from Delphi 6 up to XE, and handle Unicode strings.
And the TTestLowLevelCommon._TDynArray method is the automated unitary tests associated with this wrapper. You'll find out here samples of array of records and more advanced features.
I'm currently implementing SaveToStream and LoadToStream methods...
Perhaps a new way of using generic-like features in all Delphi versions.
Edit:
I've added some new methods to the TDynArray record/object:
now you can save and load a dynamic array content to or from a string (using LoadFromStream/SaveToStream or LoadFrom/SaveTo methods) - it will use a proprietary but very fast binary stream layout;
and you can sort the dynamic array content by two means: either in-place (i.e. the array elements content is exchanged) or via an external integer index look-up array (using the CreateOrderedIndex method - in this case, you can have several orders to the same data);
you can specify any custom comparison function, and there is a new Find method will can use fast binary search if available.
Here is how those new methods work:
var
Test: RawByteString;
...
Test := GroupA.SaveTo;
GroupA.Clear;
GroupA.LoadFrom(Test);
GroupA.Compare := SortDynArrayInteger;
GroupA.Sort;
for i := 1 to GroupA.Count-1 do
if Group[i]<Group[i-1] then
ShowMessage('Error: unsorted!');
v := 1500;
if GroupA.Find(v)<0 then // fast binary search
ShowMessage('Error: 1500 not found!');
Still closer to the generic paradigm, faster, and for Delphi 6 up to XE...
I don't have D7 on a machine here, but you might try something like this instead (console test app - it compiles in XE with no hints or warnings, but not sure how D7 will handle it):
program Project1;
{$APPTYPE CONSOLE}
uses
SysUtils, Classes;
type
TGroup = array of Integer;
THolder=class(TObject)
Group: TGroup;
end;
var
GroupList: TList;
i: Integer;
begin
GroupList := TList.Create;
for i := 0 to 2 do
begin
GroupList.Add(THolder.Create);
with THolder(GroupList[GroupList.Count - 1]) do
begin
SetLength(Group, 3);
Group[0] := i;
Group[1] := i + 10;
Group[2] := i + 20;
end;
end;
for i := 0 to GroupList.Count - 1 do
begin
Writeln(i, ':0 ', THolder(GroupList[i]).Group[0]);
Writeln(i, ':1 ', THolder(GroupList[i]).Group[1]);
Writeln(i, ':2 ', THolder(GroupList[i]).Group[2]);
end;
// Not cleaning up list content, because we're exiting the app.
// In a real app, of course, you'd want to free each THolder in
// the list, or use a TObjectList and let it own the objects.
// You'd also want a try..finally to make sure the list gets freed.
GroupList.Free;
Readln;
end.
Another thing you may want to try is using a TObjectList. TObjectList is able to store a list of Objects, so you can create a new class descendant of TObject and then manage the TObjectList using these objects.
Something like (untested):
type TGroupArray : array of Integer;
type TGroup = class(Tobject)
GroupArray : TGroupArray;
end;
GroupList : TobjectList;
procedure TForm1.FormCreate(Sender: TObject);
var CurGroup : TGroup;
begin
GroupList:= TObjectList.Create;
CurGroup := TGroup.Create;
SetLength(CurGroup.GroupArray,1);
CurGroup.GroupArray[0] := 10;
GroupList.Add(CurGroup);
RetreiveGroup := GroupList.Items[0];
FreeandNil(GroupList);
end;
etc...
When you code:
for grp:= 1 to 4 DO // Put a dummy item in TList
begin
SetLength(CurGroup, 1); // Create new group
Groups.Add(#CurGroup); // Store it
end;
in fact, SetLength(CurGroup) WON'T create a new group.
It will resize the only one existing CurGroup.
So #CurGroup won't change: it will always be some address on the stack, where CurGroup is added. You add the same address to the list several times.
So You'll have to create a dynamic array of TGroup instances, like that:
var GroupArray: array of TGroup;
SetLength(GroupArray,4);
for grp := 0 to high(GroupArray) do
begin
SetLength(GroupArray[grp],1);
Groups.Add(#GroupArray[grp]);
end;
But of course, the GroupArray must remain allocated during all the time Groups will need it. So you'll have perhaps to put this GroupArray as a property in the class, because if you create this GroupArray on the stack, all GroupArray[] items will be released when the method exits and its stack is freed.
But of course, the GroupArray[] will be a more direct access to the TGroup items... Groups[i] will be equal to GroupArray[]... With no reference counting problem... Because e.g. if you resize for instance a TGroup item from its pointer in Groups[], I'm not sure that you won't have any memory leak...
So, basically everybody suggests to create an array of array instead of using a TList. Well, I already have done that. I just wanted to "upgrade" from 'array of array' to a TList since it has Add(). It looks like I will return to my original code.
Thank you all and +1 for each answer.

Delphi: Correct way to store objects fetched from TObjectList

This example is of course simplified, but basically I have a main form that triggers another form (frmSettings) with
function Execute(var aSettings: TSettings):Boolean
TSettings is my own object created in main form for keeping track of the settings.
In this newly opened form (frmSettings) I fetch a TMyObjectList that is a descendant from TObjectList.
It's filled with TMyObj.
I then fill a TListBox with values from that TMyObjectList.
the code:
...
FMyObjectList : TMyObjectList;
property MyObjectList: TMyObjectList read getMyObjectList;
...
function TfrmSettings.getMyObjectList: TMyObjectList ;
begin
If not Assigned(FMyObjectList) then FMyObjectList := TMyObjectList.Create(True)
Result := FMyObjectList;
end;
function TfrmSettings.Execute(var aSettings: TSettings): Boolean;
begin
//Fill myObjectList
FetchObjs(myObjectList);
//Show list to user
FillList(ListBox1, myObjectList);
//Show form
ShowModal;
Result := self.ModalResult = mrOk;
if Result then
begin
// Save the selected object, but how??
// Store only pointer? Lost if list is destroyed.. no good
//Settings.selectedObj := myObjectList.Items[ListBox1.ItemIndex];
// Or store a new object? Have to check if exist already?
If not Assigned(Settings.selectedObj) then Settings.selectedObj := TMyObj.Create;
Settings.selectedObj.Assign(myObjectList.Items[ListBox1.ItemIndex];);
end;
end;
procedure TfrmSettings.FillList(listBox: TListBox; myObjectList: TMyObjectList);
var
i: Integer;
begin
listBox.Clear;
With myObjectList do
begin
for i := 0 to Count - 1 do
begin
//list names to user
listBox.Items.Add(Items[i].Name);
end;
end;
end;
procedure TfrmSettings.FormDestroy(Sender: TObject);
begin
FreeAndNil(FMyObjectList);
end;
Storing just the pointer doesn't seem as a good idea, as triggering the settings form again, recreates the list, and the original object would be lost even if user hits "cancel"
So storing a copy seems better, using assign to get all the properties correct. And first checking if I already have an object.
If not Assigned(Settings.selectedObj) then Settings.selectedObj := TMyObj.Create;
Settings.selectedObj.Assign(myObjectList.Items[ListBox1.ItemIndex];);
Should I move those two lines to a method instead like Settings.AssignSelectedObj(aMyObj:TMyObj)
Does this look correct or am I implementing this the wrong way?
Something more/less needed?
I need some guidelines so I feel more secure that I don't open up for memory leaks and other trouble.
Other than that reviewing the code a bit, the real question is: Is this the correct way to store my SelectedObject in the settings class?
Is this the correct way to store the selected object in the settings?
Probably not. Your settings class should not depend on the form in any way. What if you decide to create and destroy your form dynamically each time the user opens the settings? In this case your settings would hold an invalid object reference.
IMHO it is better to store the object list in the settings together with the index of the selected object. The form should just access the settings, fill the list box and modify the selected object index after the user confirmed with OK.
You are producing a memory leak in your code. You create a TObjectList as a local variable but you never free it. And if you free the local variable, the object references in the listbox will be invalid. You have two options:
Store the object list as a member variable of your form, create in the FromCreate event handler and destroy it in the FormDestroy event handler. You can then safely use object references in your list box.
Store the object list somewhere outside and pass it into the form as a parameter of the Execute method. In this scenario, you can also safely use object references.
I would rename myObjectList to GlobalObjectList, and move it out of the class. It can be declared in the form, but create/free in the initialization/finalization sections. During initialization, after you create the list, populate it from the ini file (or wherever you store it). Now you can access it from anywhere that has your unit in the Uses.
What about the serialization of TSettings? Put your settings in some published properties, then let the RTTI save its content:
type
TSettings = class(TPersistent)
public
function SaveAsText: UTF8String;
end;
function TSettings.SaveAsText: UTF8String;
begin
var
Stream1, Stream2: TMemoryStream;
begin
Stream1 := TMemoryStream.Create;
Stream2 := TMemoryStream.Create;
try
Stream1.WriteComponent(MyComponent);
ObjectBinaryToText(Stream1, Stream2);
SetString(result,PAnsiChar(Stream2.Memory),Stream2.Size);
finally
Stream1.Free;
Stream2.Free;
end;
end;
Then your settings can be stored in a text file or text string.
It's just one solution. But storing settings as text is very handy. We use such an approach in our framework, to store settings via a code-generated user interface. A settings tree is created, from a tree of TPersistent instances.

Resources