Delphi: Correct way to store objects fetched from TObjectList - delphi

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.

Related

Delphi - Can you close all of the database tables?

Can you close all database tables except some? Can you then reopen them? I use an absolute database that is similar to BDE. If this is possible, how can I do so many?
Yes, of course you can. You could iterate the Components property of your form/datamodule, use the is operator to check whether each is an instance of your table type and use a cast to call Open or Close on it.
The following closes all TABSDataSet tables on your form except one called Table1.
procedure TForm1.ProcessTables;
var
ATable : TABSDataSet; // used to access a particular TABSDataSet found on the form
i : Integer;
begin
for i := 0 to ComponentCount - 1 do begin
if Components[i] is TABSDataSet then begin
ATable := TABSDataSet(Components[i]);
// Now that you have a reference to a dataset in ATable, you can
// do whatever you like with it. For example
if ATable.Active and (ATable <> Table1) then
ATable.Close;
end;
end;
end;
I've seen from the code you've posted in comments and your answer that you
are obviously having trouble applying my code example to your situation. You
may find the following code easier to use:
procedure ProcessTables(AContainer : TComponent);
var
ATable : TABSTable;
i : Integer;
begin
for i := 0 to AContainer.ComponentCount - 1 do begin
if AContainer.Components[i] is TABSTable then begin
ATable := TABSTable(AContainer.Components[i]);
// Now that you have a reference to a dataset in ACDS, you can
// do whatever you like with it. For example
if ATable.Active then
ATable.Close;
end;
end;
end;
Note that this is a stand-alone procedure, not a procedure of a particular
form or datamodule. Instead, when you use this procedure, you call it passing
whatever form or datamodule contains the TABSTables you want to work with as the
AContainer parameter, like so
if Assigned(DataModule1) then
ProcessTables(DataModule1);
or
if Assigned(Form1) then
ProcessTables(Form1);
However, the downside of doing it this was is that it is trickier to specify which tables, if any, to leave open, because AContainer, being a TComponent, will not have any member tables.
Btw, your task would probably be easier if you could iterate through the tables in a TABSDatabase. However I've looked at its online documentation but can't see an obvious way to do this; I've asked the publishers, ComponentAce, about this but haven't had a reply yet.

How to replace TListbox Items property with my own published object list based type in a TCustomListBox control?

Overview
This question is a second attempt based on this one I recently asked: How can I make a TList property from my custom control streamable?
Although I accepted the answer in that question and it worked, I soon realized that TCollection is not the solution or requirement I was looking for.
Requirements
To keep my requirements as simple and clear to understand as possible, this is what I am attempting to:
Derive a new custom control based on TCustomListBox
Replace the Items property with my own Items type, eg a TList.
The TList (Items property) will hold objects, each containing a caption and a image index property etc.
Ownerdraw my listbox and draw its icons and text etc.
Create a property editor to edit the Items at design-time.
With that in mind, I know how to create the custom control, I know how to work with TList or even TObjectList for example, I know how to ownerdraw the control and I also know how to create the property editor.
Problem
What I don't know is how to replace the standard listbox Items type with my own? well I kind of do (publishing my own property that shares the same name), only I need to make sure it is fully streamable with the dfm.
I have searched extensively on this subject and have tried studying code where TListView and TTreeView etc publishes its Items type but I have found myself more confused than ever.
In fact I came across this very old question asked by someone else on a different website which asks very much what I want to do: Streaming a TList property of a component to a dfm. I have quoted it below in the event the link is lost:
I recently wrote a component that publishes a TList property. I then created a property editor for the TList to enable design-time editing. The problem is that the TList doesn't stream to the dfm file, so all changes are lost when the project is closed. I assume this is because TList inherits from TObject and not from TPersistant. I was hoping there was an easy work around for this situation (or that I have misunderstood the problem to begin with). Right now all I can come up with is to switch to a TCollection or override the DefineProperties method. Is there any other way to get the information in the TList streamed to and from the dfm?
I came across that whilst searching keywords such as DefineProperties() given that this was an alternative option Remy Lebeau briefly touched upon in the previous question linked at the top, it also seemed to be the answer to that question.
Question
I need to know how to replace the Items (TStrings) property of a TCustomListBox derived control with my own Items (TList) or Items (TObjectList) etc type but make it fully streamable with the dfm. I know from previous comments TList is not streamable but I cannot use TStrings like the standard TListBox control does, I need to use my own object based list that is streamable.
I don't want to use TCollection, DefineProperties sounds promising but I don't know how exactly I would implement this?
I would greatly appreciate some help with this please.
Thank you.
Override DefineProperties procedure in your TCustomListBox (let's name it TMyListBox here). In there it's possible to "register" as many fields as you wish, they will be stored in dfm in the same way as other fields, but you won't see them in object inspector. To be honest, I've never encountered having more then one property defined this way, called 'data' or 'strings'.
You can define 'normal' property or binary one. 'Normal' properties are quite handy for strings, integers, enumerations and so on. Here is how items with caption and ImageIndex can be implemented:
TMyListBox = class(TCustomListBox)
private
//other stuff
procedure ReadData(reader: TReader);
procedure WriteData(writer: TWriter);
protected
procedure DefineProperties(filer: TFiler); override;
//other stuff
public
//other stuff
property Items: TList read fItems; //not used for streaming, not shown in object inspector. Strictly for use in code itself. We can make it read-only to avoid memory leak.
published
//some properties
end;
that's DefineProperties implementation:
procedure TMyListBox.DefineProperties(filer: TFiler);
begin
filer.DefineProperty('data', ReadData, WriteData, items.Count>0);
end;
fourth argument, hasData is Boolean. When your component is saved to dfm, DefineProperties is called and it's possible to decide at that moment is there any data worth saving. If not, 'data' property is omitted. In this example, we won't have this property if there is no items present.
If we expect to ever use visual inheritance of this control (for example, create a frame with this listBox with predefined values and then eventually change them when put to form), there is a possibility to check, is value of this property any different than on our ancestor. Filer.Ancestor property is used for it. You can watch how it's done in TStrings:
procedure TStrings.DefineProperties(Filer: TFiler);
function DoWrite: Boolean;
begin
if Filer.Ancestor <> nil then
begin
Result := True;
if Filer.Ancestor is TStrings then
Result := not Equals(TStrings(Filer.Ancestor))
end
else Result := Count > 0;
end;
begin
Filer.DefineProperty('Strings', ReadData, WriteData, DoWrite);
end;
This would save a little bit of space (or lots of space if image is stored within) and sure is elegant, but in first implementation it can well be omitted.
Now the code for WriteData and ReadData. Writing is much easier usually and we may begin with it:
procedure TMyListBox.WriteData(writer: TWriter);
var i: Integer;
begin
writer.WriteListBegin; //in text dfm it will be '(' and new line
for i:=0 to items.Count-1 do begin
writer.WriteString(TListBoxItem(items[I]).caption);
writer.WriteInteger(TListBoxItem(items[I]).ImageIndex);
end;
writer.WriteListEnd;
end;
In dfm it will look like this:
object MyListBox1: TMyListBox
data = (
'item1'
-1
'item2'
-1
'item3'
0
'item4'
1)
end
Output from TCollection seems more elegant to me (triangular brackets and then items, one after another), but what we have here would suffice.
Now reading it:
procedure TMyListBox.ReadData(reader: TReader);
var item: TListBoxItem;
begin
reader.ReadListBegin;
while not reader.EndOfList do begin
item:=TListBoxItem.Create;
item.Caption:=reader.ReadString;
item.ImageIndex:=reader.ReadInteger;
items.Add(item); //maybe some other registering needed
end;
reader.ReadListEnd;
end;
That's it. In such a way rather complex structures can be streamed with ease, for example, two-dimensional arrays, we WriteListBegin when writing new row and then when writing new element.
Beware of WriteStr / ReadStr - these are some archaic procedures which exist for backward compatibility, ALWAYS use WriteString / ReadString instead!
Other way to do is to define binary property. That's used mostly for saving images into dfm. Let's say, for example, that listBox has hundreds of items and we'd like to compress data in it to reduce size of executable. Then:
TMyListBox = class(TCustomListBox)
private
//other stuff
procedure LoadFromStream(stream: TStream);
procedure SaveToStream(stream: TStream);
protected
procedure DefineProperties(filer: TFiler); override;
//etc
end;
procedure TMyListBox.DefineProperties(filer: TFiler);
filer.DefineBinaryProperty('data',LoadFromStream,SaveToStream,items.Count>0);
end;
procedure TMyListBox.SaveToStream(stream: TStream);
var gz: TCompressionStream;
i: Integer;
value: Integer;
item: TListBoxItem;
begin
gz:=TCompressionStream.Create(stream);
try
value:=items.Count;
//write number of items at first
gz.Write(value, SizeOf(value));
//properties can't be passed here, only variables
for i:=0 to items.Count-1 do begin
item:=TListBoxItem(items[I]);
value:=Length(item.Caption);
//almost as in good ol' Pascal: length of string and then string itself
gz.Write(value,SizeOf(value));
gz.Write(item.Caption[1], SizeOf(Char)*value); //will work in old Delphi and new (Unicode) ones
value:=item.ImageIndex;
gz.Write(value,SizeOf(value));
end;
finally
gz.free;
end;
end;
procedure TMyListBox.LoadFromStream(stream: TStream);
var gz: TDecompressionStream;
i: Integer;
count: Integer;
value: Integer;
item: TListBoxItem;
begin
gz:=TDecompressionStream.Create(stream);
try
gz.Read(count,SizeOf(count)); //number of items
for i:=0 to count-1 do begin
item:=TListBoxItem.Create;
gz.Read(value, SizeOf(value)); //length of string
SetLength(item.caption,value);
gz.Read(item.caption[1],SizeOf(char)*value); //we got our string
gz.Read(value, SizeOf(value)); //imageIndex
item.ImageIndex:=value;
items.Add(item); //some other initialization may be needed
end;
finally
gz.free;
end;
end;
In dfm it would look like this:
object MyListBox1: TMyListBox1
data = {
789C636260606005E24C86128654865C064386FF40802C62C40002009C5607CA}
end
78 is sort of signature of ZLib, 9C means default compression, so it works (there are only 2 items actually, not hundreds). Of course, this is just one example, with BinaryProperties any possible format may be used, for example saving to JSON and putting it into stream, or XML or something custom. But I'd not recommend to use binary unless it's absolutely inevitable, because it's difficult to see from dfm, what happens in component.
It seems like good idea to me to actively use streaming when implementing component: we can have no designer at all and set all values by manually editing dfm and see if component behaves correctly. Reading/loading itself can be tested easily: if component is loaded, then saved and text is just the same, it's all right. It's so 'transparent' when streaming format is 'human-readable', self-explaining that it often overweighs drawbacks (like file size) if there are any.

How can i know if the user define component is created?

I create a memo inside a procedure, using this code:
Global_MemoIni := TMemo.Create(Conf);
Global_MemoIni.Parent := Conf;
Global_MemoIni.Visible := False;
Global_MemoIni.Align := alClient;
Global_MemoIni.WordWrap := False;
When I call the procedure again it creates the global_memoini again.
How can I know if the component is created so I don't need to call it again?
Update : Can I use the Global_MemoIni.Free above the creation code so the next time create
the Global_memoini once... But i want to know if this is created...
Thank you
You can check if Global_MemoIni is Nil and create the TMemo if it is. Otherwise it already exists, you can then free it using Free or FreeAndNil. If you use free be careful that you assign Nil to Global_MemoIni. If you don't, you can't use the Global_MemoIni <> Nil check.
I honestly don't understand the point of using a memo in stead of a TStringList which is more lightweight. just do
unit UnitName;
interface
uses SysUtils, Windows, Classes, ...;
var Global_INI: TStringList; // <-- it's defined in the interface section, therefore
// it can be accessed by any unit which uses this unit
implementation
initialization
Global_INI := TStringList.Create;
Global_INI.LoadFromFile( 'C:\config.ini' ); // <-- replace the file name with the
// one you want
finalization
FreeAndNil( Global_INI );
end;
Don't do this is an arbitrary function. Either create the component in the FormCreate or even the constructor of the form, or make it a read-only property of the form, and use lazy instantiation, i.e.
if not Assigned(Global_MemoIni) then
begin
Global_MemoIni := TMemo.Create(Self);
// rest of your code
end;
Result := Global_MemoIni;
But why is it global? If you make it a field and corresponding read-only property of the form, it is easily accessible and you can protect it in the way shown above.
FWIW, instead of Free-ing the component, let the Owner (the form) do that. That way, it is available as long as the form exists, and no nasty invalid pointer issues can take place.
If you do not know the creation state of object use:
if not Assigned(Global_MemoIni) then
begin
Global_MemoIni := TMemo.Create(Conf);
...
end
And don't forget to use FreeAndNil(Global_MemoIni) when destroying the object.

Delphi 3 Memory Allocation Problem

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.

Delphi getting value of form components properties

I am implementing a Boilerplate feature - allow users to Change descriptions of some components - like TLabels - at run time.
e.g.
TFooClass = Class ( TBaseClass)
Label : Tlabel;
...
End;
Var FooClass : TFooClass;
...
At design time, the value Label's caption property is say - 'First Name', when the
application is run, there is a feature that allows the user to change the caption
value to say 'Other Name'. Once this is changed, the caption for the label for
the class instance of FooClass is updated immediately.
The problem now is if the user for whatever reason wants to revert back to the design
time value of say 'First Name' , it seems impossible.
I can use the RTTIContext methods and all that but I at the end of the day, it seems
to require the instance of the class for me to change the value and since this
has already being changed - I seem to to have hit a brick wall getting around it.
My question is this - is there a way using the old RTTI methods or the new RTTIContext
stuff to the property of a class' member without instantiating the class - i.e. getting
the property from the ClassType definition.
This is code snippet of my attempt at doing that :
c : TRttiContext;
z : TRttiInstanceType;
w : TRttiProperty;
Aform : Tform;
....
Begin
.....
Aform := Tform(FooClass);
for vCount := 0 to AForm.ComponentCount-1 do begin
vDummyComponent := AForm.Components[vCount];
if IsPublishedProp(vDummyComponent,'Caption') then begin
c := TRttiContext.Create;
try
z := (c.GetType(vDummyComponent.ClassInfo) as TRttiInstanceType);
w := z.GetProperty('Caption');
if w <> nil then
Values[vOffset, 1] := w.GetValue(vDummyComponent.ClassType).AsString
.....
.....
....
....
I am getting all sorts of errors and any help will be greatly appreciated.
The RTTI System does not provide what you are after. Type information is currently only determined at compile time. Initial form values are set at Runtime using the DFM resource. You can change values in a DFM Resource in a compiled application because it's evaluated at runtime.
Parse and use the DFM Resource where it is stored, or make a copy of the original value at runtime. Possibly at the point of initial change to reduce your memory footprint.
Masons Suggestion of using TDictionary<string, TValue> is what I would use. I would be careful of storing this information in a database as keeping it in sync could become a real maintenance nightmare.
Sounds like what you're trying to do is get the value of a certain property as defined in the DFM. This can't be done using RTTI, since RTTI is based on inspecting the structure of an object as specified by its class definition. The DFM isn't part of the class definition; it's a property list that gets applied to the objects after they've been created from the class definitions.
If you want to get the values of the properties of a form's controls, you'll probably have to cache them somewhere. Try putting something in the form's OnCreate that runs through all the controls and uses RTTI to populate a TDictionary<string, TValue> with the values of all the properties. Then you can look them up later on when you need them.
If what you are trying to achieve it to restore the value that was set at design-time (i.e. that one that is saved in the DFM), I'd use InitInheritedComponent as a starting point.
It's possible to get the content of the DFM at runtime. Could be a pain to parse though.
Check also InternalReadComponentRes.
Both routine can be found in the classes unit.
Well - I solved the problem. The trick is basically instantiating another instance of the form like so :
procedure ShowBoilerPlate(AForm : TForm; ASaveAllowed : Boolean);
var
vCount : Integer;
vDesignTimeForm : TForm;
vDesignTimeComp : TComponent;
vDesignTimeValue : String;
vCurrentValue : String;
begin
....
....
vDesignTimeForm := TFormClass(FindClass(AForm.ClassName)).Create(AForm.Owner);
try
// Now I have two instances of the form - I also need to have at least one
// overloaded constructor defined for the base class of the forms that will allow for
// boilerplating. If you call the default Constructor - no boilerplating
// is done. If you call the overloaded constructor, then, boilerplating is done.
// Bottom line, I can have two instances AForm - with boilerplated values and
// vDesignForm without boilerplated values.
for vCount := 0 to AForm.ComponentCount-1 do begin
vDummyComponent := AForm.Components[vCount];
if Supports (vDummyComponent,IdoGUIMetaData,iGetGUICaption) then begin
RecordCount := RecordCount + 1;
Values[vOffset, 0] := vDummyComponent.Name;
if IsPublishedProp(vDummyComponent,'Caption') then begin
vDesignTimeComp := vDesignTimeForm.FindComponent(vDummyComponent.Name);
if vDesignTimeComp <> nil then begin
// get Design time values here
vDesignTimeValue := GetPropValue(vDesignTimeComp,'Caption');
end;
// get current boilerplated value here
vCurrentValue := GetPropValue(vDummyComponent,'Caption');
end;
vOffset := RecordCount;;
end;
end;
finally
FreeAndNil(vDesignTimeForm);
end;
end;
Anyway - thank you all for all your advice.

Resources