How to create a record at runtime in delphi - delphi

I want to create a record that I call TSprite at runtime.
TSprite is a image and 8 selection points that im using in a level editor I'm building.
type
TSprite = record
Image: TImage;
Selection: TSelection;
SelectionPointTL: TSelectionPoint; // top-left
SelectionPointTM: TSelectionPoint; // top-middle
SelectionPointTR: TSelectionPoint; // top-right
SelectionPointML: TSelectionPoint; // middle-left
SelectionPointMR: TSelectionPoint; // middle-right
SelectionPointBL: TSelectionPoint; // bottom-left
SelectionPointBM: TSelectionPoint; // bottom-middle
SelectionPointBR: TSelectionPoint; // bottom-right
end;
Now i want to store it in a array.
arrSprites: array[0..1000] of TSprite;
And now the creation (the part im struggling with)
This is what I have so far:
arrSprites[i].Image.Position.X := frmMainUI.CurrentMouseX;
arrSprites[i].Image.Position.Y := frmMainUI.CurrentMouseY;
arrSprites[i].Image.Bitmap.LoadFromFile('1.png');
arrSprites[i].Image.Visible := True;
arrSprites[i].Image.WrapMode := TImageWrapMode.iwStretch;
So what this code is supposed to do is to create an image with a selection around it inside a scrollbox i names : fsbcanvas.
Just to be clear I'm asking for the code that creates an instance of TSprite.
Thanks

Records don't need to be created. They are value types and you should think of them the same way that you think about other value types, e.g. Integer. Declare a local variable or a class field that is a value type and that's all you need to do. Similarly, a constant sized array is a value type.
So, the answer to your question, is that arrSprites does not need any special allocation. What does need allocation and initialization are the contents of your record. So if any of the fields in your record are class instances, then they need instantiating. So, consider this record:
type
TMyRecord = record
i: Integer;
obj: TObject;
end;
You can declare one like this:
var
rec: TMyRecord;
and the record itself is allocated. But you need to initialise its members:
rec.i := 42; // or some other initial value
rec.obj := TObject.Create; // instantiate the object
And when you are done with the record, you'd need to destroy the object.
rec.obj.Free;
This is easy to get wrong and so in general your records should contain only value types, or managed types (e.g. strings, interfaces, dynamic arrays etc.)
Now, I can't tell what your code is all about from what's in the question, but I suspect that your record has some class instances. Which immediately makes record a dubious choice of data structure. I'd hold these in a class which has constructor and destructor that manage the lifetime of the objects within.
And I'd also avoid using a constant length array. They are very inflexible. Instead I suggest you hold your sprite objects in a generic list, TList<T>. Or, perhaps even better, TObjectList<T> and that way you can let the list look after the life time of its members.

I was typing an answer as David Heffernan just beat me to it.
Still while at it i would like to add;
Realize that in your first code example you are using a record (TSprite) to hold an object (TImage). TImage is a visual component actually, but ultimately it descends from TObject.
A record is like most variables (such as Integer) in that it does not have to be instantiated and can be copied at will.
An object however is a pointer to an instance, and must be created/destroyed by following these steps
rec.Image := TImage.Create(nil);
// do other things..
rec.Image.Free;
And thus may result in memory leaks or Access Violation errors if not done properly. (for example while copying a TSprite..)
There is a lot of things that can go wrong in this setup, so therefore i say;
Using objects in records can get tricky. Consider keeping objects in objects:
A simple solution (if you indeed want to keep an instance or pointer reference of TImage inside a TSprite) would be to make TSprite also an object. It could keep track of of creating/destroying automatically by using its constructor and destructor:
TSpriteObject = class(TObject)
public
Image : TImage;
constructor Create;
destructor Destroy; override;
end;
and its implemenation:
constructor TSpriteObject.Create;
begin
Image := TImage.Create(nil);
// ^ TImage is a component and expects an Owner component that would also
// destroy it, so we use a nil value to disable that behavior.
end;
destructor TSpriteObject.Destroy;
begin
Image.Free;
end;
You can then have a TObjectList keep track of many instances of TSprite, and it'll destroy any TSprite (which destroys its TImage) when the list is cleared or destroyed.
(this is my first ever stackoverflow post attempt, please bear with me while i find out what i'm doing wrong here)

As David Heffernan said a class may be a better data structure for this situation. But if you decided to use records instead, you can declare methods in your record to refactor the instantiating and destroying parts:
type
TSprite = record
Image: TImage;
Selection: TSelection;
SelectionPointTL: TSelectionPoint; // top-left
SelectionPointTM: TSelectionPoint; // top-middle
SelectionPointTR: TSelectionPoint; // top-right
SelectionPointML: TSelectionPoint; // middle-left
SelectionPointMR: TSelectionPoint; // middle-right
SelectionPointBL: TSelectionPoint; // bottom-left
SelectionPointBM: TSelectionPoint; // bottom-middle
SelectionPointBR: TSelectionPoint; // bottom-right
procedure Create(aImageOwner: TComponent);
procedure Destroy;
end;
{ TSprite }
procedure TSprite.Create(aImageOwner: TComponent);
begin
Image := TImage.Create(aImageOwner);
end;
procedure TSprite.Destroy;
begin
Image.Free;
end;
// ...
var
Rec: TSprite;
begin
Rec.Create(Form1);
// ...
Rec.Destroy;
end;
Notice that it's not a real class instantiation so Rec := TSprite.Create(Form1); whould be meaningless, unless you want to define and implement TSprite.Create in that way.

Related

Delphi - Getting property values with GetPropValue()

I'm using Delphi's GetPropValue() function to get values ​​of certain properties of some objects of type TControl. Everything works correctly when I get simple property values ​​such as Value, Opacity, etc, but as I'm using firemonkey there are some extended properties, such as RotationCenter, it has RotationCenter.X and RotationCenter.Y, or even properties of text within TextSettings, in these properties with sub-types I can not get the values.
In this example I get the values ​​correctly:
If IsPublishedProp (Component_cc, 'Value') then
EditValue.Text: = GetPropValue (Component_cc, 'Value', true);
Where Component_cc:TControl; And is created dynamically, it can also be any type of Firemonkey component (so far everything is okay, everything works).
When I need to use the form below, it does not work.
If IsPublishedProp (Component_cc, 'RotationCenter.X') then
EditRotationCenterX.Text: = GetPropValue (CC_component, 'RotationCenter.X', true);
Does anyone know a way to get these properties extended by this function?
First, the CC_component's RotationCenter property is actually an instance of TPosition class which decends from TPersistent.
Second, you cannot use dotted notation when calling IsPublishedProp.
You can use GetObjectProp to first retrieve the internal TPosition instance and then access the X property from there:
(Assume a simple FMX application with one form that contains a TButton called Button1 and a TEdit called EditRotationCenterX.)
procedure TForm1.Button1Click(Sender: TObject);
var
CC_component : TComponent;
CC_component_RotationCenter : TPosition;
begin
CC_component := Button1;
if IsPublishedProp(CC_component, 'RotationCenter') then
begin
CC_component_RotationCenter := TPosition(GetObjectProp(CC_component, 'RotationCenter'));
EditRotationCenterX.Text := CC_component_RotationCenter.X.ToString;
end
end;
Update, for a property of type Set:
For a Set type property, you will need to retrieve its ordinal value using GetOrdProp. This will be the array of bits that represent which elements are included in the current value. Then, you simply test if the appropriate bit is set. This is the method I would prefer.
Alternatively you can use GetSetProp which will return a text representation of the elements in the Set's current value. For example, if the Set's value is [TCorner.BottonLeft, TCorner.TopRight] the you will get back the string value "TopRight,BottonLeft". You then check to see if the name of your target element appears anywhere in the returned string. This method is susceptible to failure if the Delphi RTL or FMX libraries are ever changed in the future.
(This example adds a TRectangle shape called Rectangle1 and a TCheckBox called cbCornerBottonRight to the simple FMX App from above:)
procedure TForm1.Button1Click(Sender: TObject);
var
CC_component : TComponent;
CC_component_Corners : nativeint;
CC_component_CornersAsString : string;
begin
CC_component := Rectangle1;
if IsPublishedProp(CC_component, 'Corners') then
begin
// Using this method will make your code less sensitive to
// changes in the ordinal values of the Set's members or
// changes to names of the enumeration elements.
//
CC_component_Corners := GetOrdProp(CC_component,'Corners');
cbCornerBottonRight.IsChecked := ((1 shl ord(TCorner.BottomRight)) and CC_component_Corners) <> 0;
// This approach may break if the names of the elements of
// the TCorner enumeration are ever changed. (BTW, they have
// been in the past: "cvTopLeft", "cvTopRight", "cvBottomLeft",
// and "cvBottomRight" are now deprecated)
//
CC_component_CornersAsString := GetSetProp(CC_component,'Corners');
cbCornerBottonRight.IsChecked := CC_component_CornersAsString.IndexOf('BottomRight') >= 0;
end;
end;
When speaking about the old RTTI, you can do this. You need to go deeper in the structure. Ask for the X property the TPosition object:
var
O: TObject;
X: Integer;
begin
if PropIsType(Component_cc, 'RotationCenter', tkClass) then
begin
O := GetObjectProp(Component_cc, 'RotationCenter');
if Assigned(O) and PropIsType(O, 'X', tkInteger) then
X := GetOrdProp(O, 'X');
end;
end;

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.

Method pointer and regular procedure incompatible

I have an app, which has multiple forms. All these forms have a PopupMenu. I build the menu items programatically, all under a common root menu item. I want ALL the menu items to call the same procedure, and the menu item itself is basically acting as an argument....
I had this working when I just had one form doing this functionality. I now have multiple forms needing to do this. I am moving all my code to a common unit.
Example.
Form A has PopupMenu 1. When clicked, call code in Unit CommonUnit.
Form B has PopupMenu 2. When clicked, call code in unit CommonUnit.
When I need to call my popup from each form, I call my top level procedure (which is in unit CommonUnit), passing the name of the top menu item from each form to the top level procedure in the common unit.
I am adding items to my PopupMenu with with code.
M1 := TMenuItem.Create(TopMenuItem);
M1.Caption := FieldByName('NAME').AsString;
M1.Tag := FieldByName('ID').AsInteger;
M1.OnClick := BrowseCategories1Click;
TopMenuItem.Add(M1);
I am getting an error message when I compile. Specifically, the OnClick line is complaining about
Incompatible types: 'method pointer and regular procedure'.
I have defined BrowseCategories1Click exactly like it was before when I was doing this on a single form. The only difference is that it is now defined in a common unit, rather than as part of a form.
It is defined as
procedure BrowseCategories1Click(Sender: TObject);
begin
//
end;
What is the easiest way to get around this?
Thanks
GS
A little background...
Delphi has 3 procedural types:
Standalone or unit-scoped function/procedure pointers declared like so:
var Func: function(arg1:string):string;
var Proc: procedure(arg1:string);
Method pointers declared like so:
var Func: function(arg1:string):string of object;
var Proc: procedure(arg1:string) of object;
And, since Delphi 2009, anonymous(see below) function/method pointers declared like so:
var Func: reference to function(arg1:string):string;
var Proc: reference to procedure(arg1:string);
Standalone pointers and method pointers are not interchangeable. The reason for this is the implicit Self parameter that is accessible in methods. Delphi's event model relies on method pointers, which is why you can't assign a standalone function to an object's event property.
So your event handlers will have to be defined as part of some class definition, any class definition to appease the compiler.
As TOndrej suggested you can hack around the compiler but if these event handlers are in the same unit then they should already be related anyway so you may as well go ahead and wrap them into a class.
One additional suggestion I have not seen yet is to backtrack a little. Let each form implement its own event handler but have that handler delegate responsibility to a function declared in your new unit.
TForm1.BrowseCategoriesClick(Sender:TObject)
begin
BrowseCategories;
end;
TForm2.BrowseCategoriesClick(Sender:TObject)
begin
BrowseCategories;
end;
unit CommonUnit
interface
procedure BrowseCategories;
begin
//
end;
This has the added benefit of separating the response to the user's action from the control that triggered the action. You could easily have the event handlers for a toolbar button and a popup menu item delegate to the same function.
Which direction you choose is ultimately up to you but I'd caution you to focus on which option will make maintainability easier in the future rather than which is the most expedient in the present.
Anonymous methods
Anonymous methods are a different beast all together. An anonymous method pointer can point to a standalone function, a method or a unnamed function declared inline. This last function type is where they get the name anonymous from. Anonymous functions/methods have the unique ability to capture variables declared outside of their scope
function DoFunc(Func:TFunc<string>):string
begin
Result := Func('Foo');
end;
// elsewhere
procedure CallDoFunc;
var
MyString: string;
begin
MyString := 'Bar';
DoFunc(function(Arg1:string):string
begin
Result := Arg1 + MyString;
end);
end;
This makes them the most flexible of the procedural pointer types but they also have potentially more overhead. Variable capture consumes additional resources as does inline declarations. The compiler uses a hidden reference counted interface for inline declarations which adds some minor overhead.
You can wrap your procedures into a class. This class might look like this in a separate unit:
unit CommonUnit;
interface
uses
Dialogs;
type
TMenuActions = class
public
class procedure BrowseCategoriesClick(Sender: TObject);
end;
implementation
{ TMenuActions }
class procedure TMenuActions.BrowseCategoriesClick(Sender: TObject);
begin
ShowMessage('BrowseCategoriesClick');
end;
end.
And to assign the action to a menu item in a different unit is enough to use this:
uses
CommonUnit;
procedure TForm1.FormCreate(Sender: TObject);
begin
PopupMenuItem1.OnClick := TMenuActions.BrowseCategoriesClick;
end;
Update:
Updated to use class procedures (instead of object methods) by David's suggestion. For those who want to use the object methods with the need of object instance, follow this version of the post.
This is the difference between a "procedure" and a "procedure of object"
The OnClick is defined as a TNotifyEvent:
type TNotifyEvent = procedure(Sender: TObject) of object;
You cannot assign a procedure to the OnClick as it is the wrong type. It needs to be a procedure of object.
You could choose one of these:
Derive your forms from a common ancestor and declare the method in it so it's available to descendants
Use a global instance of a class (e.g. data module) shared by all forms
Use a procedure as a fake method like this:
procedure MyClick(Self, Sender: TObject);
begin
//...
end;
var
M: TMethod;
begin
M.Data := nil;
M.Code := #MyClick;
MyMenuItem.OnClick := TNotifyEvent(M);
end;
One solution is to place the OnClick method into a TDatamodule.

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.

problem subclassing TTreeNode in delphi

i'm writing a delphi 2009 app that uses a TTreeView on a docking panel.
i saw i could make big simplifications in my app if i subclassed the TTreeNode. the tree view it's on is placed on a docking panel.
TInfoTreeNode=class(TTreeNode)
private
// remember some stuff
public
end;
procedure TfraInfoTree.tvInfoCreateNodeClass(Sender: TCustomTreeView;
var NodeClass: TTreeNodeClass);
begin
NodeClass:=TInfoTreeNode;
end;
i think i've hit a wall though...each "TInfoTreeNode" instance needs to remember things about itself. since the handles are freed when the panel containing the TTreeView auto-hides, the classes are destroyed.
that's a problem because then everything the classes knew is then forgotten.
is there a way around this (other than reloading every TInfoTreeNode from the database again)?
thank you!
IIRC, the Tag Data property on each TTreeNode instance is preserved through the handle rebuild.
You could either use this as an index into a List containing objects with additional information, or use type-casting to store an object reference and access the objects directly.
the problem is caused by the wrong implementation of your custom TreeNode - it doesn't preserve its information when the TreeView's parent window gets recreated after it has been hodden. As a solution, create a TTreeView descendant and override its DestroyWnd method, to preserve your custom values. For example, take a look at how the TCustomTreeView.DestroyWnd method is implemented.
Having looked at TCustomTreeView.DestroyWnd like Joe Meyer proposes, I would suggest you revert to using the TreeNode.Data property, and store a reference to objects of a new class inheriting from TObject directly. The OnDeletion event of the TreeView offers a good spot to put the destruction code: "TMyObject(Node.Data).Free;"
Usage is pretty similar except you'll need to use "TMyObject(Node.Data)" instead of "TMyNode(Node)". A warning though: experience has taught me to pay close attention not to forget the ".Data" part, since "TMyObject(Node)" will not throw a compile error and raise access violations at run-time.
thank you all for your replies!
i have for 10 years been using the tree view using TTreeNode's data property. i wanted to be free of:
setting the Data property
creating/destroying the "data" object in a manner so there are no memory leaks
i have used the Data property for an ID number in the past as well.
today, my nodes have GUIDs to find their data in the database so they don't "fit" into the Data property anymore.
using a descendant of TTreeNode seems to have addressed my wishes nicely but in order to make that work nicely i had to do a few things:
handle TTreeView.OnCreateNodeClass event
handle TTreeView.OnDeletion event to retrieve latest data from the nodes before they are destroyed
handle TTreeView.OnAddition event to: 1) maintain a simple list of the nodes 2) set the node's Data property so we can use it to find the place in the list allocated for storing it's data.
here's the code:
TInfoTreeNodeMemory=record
...
end;
TInfoTreeNode=class(TTreeNode)
private
m_rInfoTreeNodeMemory:TInfoTreeNodeMemory;
public
property InfoTreeNodeMemory:TInfoTreeNodeMemory read m_rInfoTreeNodeMemory write m_rInfoTreeNodeMemory;
end;
TInfoTreeNodeMemoryItemList=class
private
m_List:TList<TInfoTreeNodeMemory>;
public
constructor Create;
destructor Destroy; override;
procedure HandleOnDeletion(Node: TInfoTreeNode);
procedure HandleOnAddition(Node: TInfoTreeNode);
end;
TfraInfoTree = class(TFrame)
tvInfo: TTreeView;
procedure tvInfoCreateNodeClass(Sender: TCustomTreeView;
var NodeClass: TTreeNodeClass);
procedure tvInfoDeletion(Sender: TObject; Node: TTreeNode);
procedure tvInfoAddition(Sender: TObject; Node: TTreeNode);
private
m_NodeMemory:TInfoTreeNodeMemoryItemList;
...
procedure TfraInfoTree.tvInfoCreateNodeClass(Sender: TCustomTreeView;
var NodeClass: TTreeNodeClass);
begin
// THIS IS VITAL!
NodeClass:=TInfoTreeNode;
end;
procedure TfraInfoTree.tvInfoDeletion(Sender: TObject; Node: TTreeNode);
begin
m_NodeMemory.HandleOnDeletion(TInfoTreeNode(Node));
end;
procedure TfraInfoTree.tvInfoAddition(Sender: TObject; Node: TTreeNode);
begin
m_NodeMemory.HandleOnAddition(TInfoTreeNode(Node));
end;
g_icTreeNodeNotInList=MAXINT;
procedure TInfoTreeNodeMemoryItemList.HandleOnDeletion(Node: TInfoTreeNode);
var
iPosition:integer;
begin
iPosition:=integer(Node.Data);
if iPosition=g_icTreeNodeNotInList then
raise Exception.Create('Node memory not found!')
else
// we recognize this node; store his data so we can give it back to him later
m_List[iPosition]:=Node.InfoTreeNodeMemory;
end;
procedure TInfoTreeNodeMemoryItemList.HandleOnAddition(Node: TInfoTreeNode);
var
iPosition:integer;
begin
// "coat check" for getting back node data later
iPosition:=integer(Node.Data);
if iPosition=g_icTreeNodeNotInList then
begin
// Node.Data = index of it's data
// can't set Node.Data in OnDeletion so we must assign it in OnAddition instead
Node.Data:=pointer(m_List.Count);
// this data may very well be blank; it mostly occupies space; we harvest the real data in OnDeletion
m_List.Add(Node.InfoTreeNodeMemory);
end
else
// we recognize this node; give him his data back
Node.InfoTreeNodeMemory:=m_List[iPosition];
end;
very cool...it meets all my objectives!
to add a node to the tree, all i need to do is:
// g_icTreeNodeNotInList important so the "coat check" (TInfoTreeNodeMemoryItemList)
// can recognize this as something that's not in it's list yet.
MyInfoTreeNode:=TInfoTreeNode(tvInfo.Items.AddChildObject(nParent, sText, pointer(g_icTreeNodeNotInList))));

Resources