Key/Value pairs in ComboBox using Delphi Firemonkey - delphi

I would like to use an enumerator to populate a Combobox with Key/Value pairs. Its important that I hide the key from the user and display the value only. On selecting I would like to capture the key associated with the selected value.
The code looks something similar to this.
var
currentObj: ISuperObject;
enum: TSuperEnumerator<IJSONAncestor>;
while enum.MoveNext do
begin
currentObj := enum.Current.AsObject;
cboUserList.Items.Add(currentObj.S['key'],currentObj.S['value']);
end;
The key currentObj.S['key'] should be capture on user select of the value
currentObj.S['value'] which is visible to the user on the cboUserList dropdownlist.
Any ideas?

A simple cross-platform solution would be to use a separate TStringList to hold the keys, then display the values in the ComboBox and use its item indices to access the TStringList items.
var
currentObj: ISuperObject;
enum: TSuperEnumerator<IJSONAncestor>;
while enum.MoveNext do
begin
currentObj := enum.Current.AsObject;
userSL.Add(currentObj.S['key']);
cboUserList.Items.Add(currentObj.S['value']);
end;
var
index: Integer;
key: string;
begin
index := cboUserList.ItemIndex;
key := userSL[index];
...
end;

You can wrap your key in class, e.g.
type
TKey = class
S: string;
constructor Create(const AStr: string);
end;
constructor TKey.Create(const AStr: string);
begin
S := AStr;
end;
procedure TForm2.Button2Click(Sender: TObject);
begin
ComboBox1.Items.AddObject('value', TKey.Create('key'));
end;
And then access it as
procedure TForm2.ComboBox1Change(Sender: TObject);
begin
Caption := (ComboBox1.Items.Objects[ComboBox1.ItemIndex] as TKey).S;
end;
just make sure to destroy these objects later

Related

how can i put a tdatetime into a stringlist object?

I'm trying to add a TDateTime value into a TStringList object using Delphi 10.4 Sydney.
I managed to do it like this:
TDateTimeObj = class(TObject)
strict private
DT: TDateTime;
protected
public
constructor Create(FDateTime: TDateTime);
property DateTime: TDateTime read DT write DT;
end;
constructor TDateTimeObj.Create(FDateTime: TDateTime);
begin
Self.DT := FDateTime;
end;
Then I add it to the TStringList like this:
procedure TForm1.Button1Click(Sender: TObject);
var
b: TStringList;
begin
b := TStringList.Create;
b.AddObject('a', TDateTimeObj.Create(now));
b.AddObject('b', TDateTimeObj.Create(now));
FreeAndNil(b);
end;
It works, but when I close the program I have a memory leak as I did not free the TDateTimeObj objects.
Is there a way to free the objects automatically, or a better way to achieve the same result?
You have to make the string list own the added objects. Owned objects are destroyed when the string list is destroyed.
procedure TForm1.Button1Click(Sender: TObject);
var b: TStringList;
begin
b := TStringList.Create(TRUE); // TRUE means OwnObjects
try
b.AddObject('a', TDateTimeObj.Create(now));
b.AddObject('b', TDateTimeObj.Create(now));
finally
FreeAndNil(b); // Owned objects will be destroyed as well
end;
end;

How to register a TCollectionItem property editor as a list of objects in the object inspector?

I have a collection published in a component and I would like to be able to choose a collection item in the object inspector without using the method of saving the item's index.
I have already published the property of type to item (TCollectionItem), but in the object inspector it appears as a subcomponent with no option to choose another one. I registered an editor so that it could be possible to display the list of items, but it gives an error when clicking on the item dropbox. Below is an illustrative excerpt of the problem:
TMyCollection = class(TCollection)
end;
TMyCollectionItem = class(TCollectionItem)
end;
TMycomponent = class(TComponent)
published
property MyColection: TMyCollection;
property MyChosedItem: TCollectionItem; //< this need to be a list of TCollectionItem
end;
Bellow the Property Editor
type
TMyItemProperty = class(TClassProperty)
public
function GetAttributes: TPropertyAttributes; override;
procedure GetValueList(List: TStrings); virtual;
procedure GetValues(Proc: TGetStrProc); override;
procedure SetValue(const Value: string); override;
end;
function TMyItemProperty.GetAttributes: TPropertyAttributes;
begin
Result := [paValueList, paSortList, paMultiSelect];
end;
procedure TMyItemProperty.GetValueList(List: TStrings);
var
Item: TMyCollectionItem;
Items: TMyCollection;
I: Integer;
begin
Items := (GetComponent(0) as TMyComponent).MyColection;
if Items <> nil then
for I := 0 to Items.Count-1 do
List.Add(Items.Items[I].GetNamePath); // Is this the problem ?
end;
procedure TMyItemProperty.GetValues(Proc: TGetStrProc);
var
I: Integer;
Values: TStringList;
begin
Values := TStringList.Create;
try
GetValueList(Values);
for I := 0 to Values.Count - 1 do
Proc(Values[I]);
finally
Values.Free;
end;
end;
procedure TMyItemProperty.SetValue(const Value: string);
begin
inherited SetValue(Value);
end;
when i open the property dropbox appear a error:"Invalid TypeCast".
How do I correctly implement the property editor for this property?

Firemonkey TTreeView - Storing object references in TTreeViewItems, TValue

Was trying the same way as for the good old VCL TTreeNode. Ok, there is no TTreeNode and there is no method to add treenodes to the tree, instead i have to manually create TTreeViewItem instances and set it's parent property to a TTreeView instance. Now, TTreeViewItem has a data property but it's type is TValue.
How to handle this type?
I tried the following:
type
TMaster = class(TDevice)
...
end;
...
mstitem := TTreeViewItem.create(self);
mstitem.parent := TreeView1;
mstitem.data := TMaster.Create(i, 'master'+ inttostr(i));
...
procedure TForm1.TreeView1Click(Sender: TObject);
var
obj: TObject;
begin
selectednode := TTreeView1.Selected;
obj := TDevice(selectednode.Data.AsObject); //Invalid typecast
if obj is TDevice then
showmessage( TDevice(obj).DevName );
end;
TFmxObject.SetData method is empty virtual stub that has to be overriden in descendant classes. You cannot use TreeViewItem.Data the way you use it, because Data actually contains TTreeViewItem.Name property.
You would have to create your own descendant TTreeViewItem class and use it instead of default one
TMyTreeViewItem = class(TTreeViewItem)
protected
fData: TValue;
function GetData: TValue; override;
procedure SetData(const Value: TValue); override;
end;
function TMyTreeViewItem.GetData: TValue;
begin
Result := fData;
end;
procedure TMyTreeViewItem.SetData(const Value: TValue);
begin
fData := Value;
end;

TComboBox 'Control has no parent window' in destructor

I'm using Delphi XE2. I build a custom TComboBox so that I can easily add key/string pairs and handle the cleanup in the component's destructor.
All if not (csDesigning in ComponentState) code is omitted for brevity.
interface
type
TKeyRec = class(TObject)
Key: string;
Value: string;
end;
TMyComboBox = class(TComboBox)
public
destructor Destroy; override;
procedure AddItemPair(const Key, Value: string);
end;
implementation
destructor TMyComboBox.Destroy;
var i: Integer;
begin
for i := 0 to Self.Items.Count - 1 do
Self.Items.Objects[i].Free;
Self.Clear;
inherited;
end;
procedure TMyComboBox.AddItemPair(const Key, Value: string);
var rec: TKeyRec;
begin
rec := TKeyRec.Create;
rec.Key := Key;
rec.Value := Value;
Self.Items.AddObject(Value, rec);
end;
When the application closes, the destructor is called, but the Items.Count property is inaccessible because the TComboBox must have a parent control to access this property. By the time the destructor is called, it no longer has a parent control.
I saw this problem once before and had to store the objects in a separate TList and free them separately. But that only worked because the order that I added them to the TList was always the same as the strings added to the combo box. When the user selected a string, I could use the combo box index to find the correlating object in the TList. If the combo box is sorted, then the indexes won't match, so I can't always use that solution.
Has anyone else seen this? How did you workaround the issue? It would be really nice to be able to free the objects in the component's destructor!
You can override function GetItemsClass:
function GetItemsClass: TCustomComboBoxStringsClass; override;
It is called by Combo to create Items (by default it is TComboBoxStrings probably).
Then you can create your own TComboBoxStrings descendant, for example TComboBoxStringObjects, where
you can free object linked with item (when item deleted).
After reading the link from Sertac (David Heffernan's comment and NGLN's answer), I believe a solution that stores the objects in a managed list and not in a GUI control is the best. To that end, I have create a combo box that descends from TCustomComboBox. This lets me promote all the properties except for Sorted to published. This keeps the internal FList in sync with the strings in the combo boxes Items property. I just make sure they are sorted the way I want before adding them...
The following shows what I did. I only included the essential code (less range checking) for brevity, but included some conditional logic that allows the combo box to be used without objects as well.
FList is properly destroyed in the destructor, freeing all objects without any run-time exceptions and the object list is managed within the component itself instead of having to manage it elsewhere -- making it very portable. It works when the control is added to a form at design-time, or when it is created at run-time. I hope this is useful to someone else!
interface
type
TMyComboBox = class(TCustomComboBox)
private
FList: TList;
FUsesObjects: Boolean;
function GetKey: string;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure AddItemPair(const Key, Value: string);
procedure ClearAllItems;
procedure DeleteItem(const Index: Integer);
property Key: string read GetKey;
published
// all published properties (except Sorted) from TComboBox
end;
implementation
type
TKeyRec = class(TObject)
Key: string;
Value: string;
end;
function TMyComboBox.GetKey: string;
begin
if not FUsesObjects then
raise Exception.Create('Objects are not used.');
Result := TKeyRec(FList.Items[ItemIndex]).Key;
end;
constructor TMyComboBox.Create(AOwner: TComponent);
begin
inherited;
if not (csDesigning in ComponentState) then
begin
FUsesObjects := False;
FList := TList.Create;
end;
end;
destructor TMyComboBox.Destroy;
begin
if not (csDesigning in ComponentState) then
begin
ClearAllItems;
FreeAndNil(FList);
end;
inherited;
end;
procedure TMyComboBox.AddItemPair(const Key, Value: string);
var rec: TKeyRec;
begin
FUsesObjects := True;
rec := TKeyRec.Create;
rec.Key := Key;
rec.Value := Value;
FList.Add(rec);
Items.Add(Value);
end;
procedure TMyComboBox.ClearAllItems;
var i: Integer;
begin
if not (csDesigning in ComponentState) then
begin
if FUsesObjects then
begin
for i := 0 to FList.Count - 1 do
TKeyRec(FList.Items[i]).Free;
FList.Clear;
end;
if not (csDestroying in ComponentState) then
Clear; // can't clear if the component is being destroyed or there is an exception, 'no parent window'
end;
end;
procedure TMyComboBox.DeleteItem(const Index: Integer);
begin
if FUsesObjects then
begin
TKeyRec(FList.Items[Index]).Free;
FList.Delete(Index);
end;
Items.Delete(Index);
end;
end.
There's a way that avoid the need of rewrite the component to use another list to save the objects. The solution is to use the WM_DESTROY message along with the ComponentState property.
When the component is about to be destroyed, its state change to csDestroying, so the next time it receives a WM_DESTROY message it will be not part of the window recreation process.
We use this method in our component library sucessfully.
TMyCombo = class(TCombobox)
...
procedure WMDestroy(var message: TMessage); message WM_DESTROY;
...
procedure TMyCombo.WMDestroy(var message: TMessage);
var
i: integer;
begin
if (csDestroying in ComponentState) then
for i:=0 to Items.Count - 1 do
Items.Objects[i].Free;
inherited;
end;

Delphi select object by unknown class type

I have a few different classes which origin is a another class. I have one property that is extended to all other classes.
But different classes handle this property differently.
So i want to do this:
TClass(ObjectPointer).Property:=Value;
But TClass is unknown class type
Can i do something like that:
ObjectPointer.ClassType(ObjectPointer).Property:=Value
or this
var
ClassRef: TClass;
begin
ClassRef := Sender.ClassType;
ClassRef(ObjectPointer).DoStuff
end;
Is there way to do this in delphi without using if statement
Please note, the code from this post will work only for published properties!
To answer your question if there's a way to set a property value without using if statement, check the following overloaded functions.
The first one is for char, string, variant, integer, 64-bit integer, float, enumeration, set and dynamic array type of properties (phew). The second one is just for class type properties. Both will return True if given property exists and the value or object instance is successfuly assigned, False otherwise:
uses
TypInfo;
function TrySetPropValue(AInstance: TObject; const APropName: string;
const AValue: Variant): Boolean; overload;
begin
Result := True;
try
SetPropValue(AInstance, APropName, AValue);
except
Result := False;
end;
end;
function TrySetPropValue(AInstance: TObject; const APropName: string;
AValue: TObject): Boolean; overload;
begin
Result := True;
try
SetObjectProp(AInstance, APropName, AValue);
except
Result := False;
end;
end;
And the usage; when the Memo1.Lines is set the, the second version of TrySetPropValue is called:
procedure TForm1.Button1Click(Sender: TObject);
var
Strings: TStringList;
begin
TrySetPropValue(Memo1, 'Width', 250);
TrySetPropValue(Memo1, 'Height', 100);
TrySetPropValue(Memo1, 'ScrollBars', ssBoth);
Strings := TStringList.Create;
try
Strings.Add('First line');
Strings.Add('Second line');
TrySetPropValue(Memo1, 'Lines', Strings);
finally
Strings.Free;
end;
if not TrySetPropValue(Memo1, 'Height', 'String') then
ShowMessage('Property doesn''t exist or the value is invalid...');
if not TrySetPropValue(Memo1, 'Nonsense', 123456) then
ShowMessage('Property doesn''t exist or the value is invalid...');
end;

Resources