Delphi - Getting property values with GetPropValue() - delphi

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;

Related

Delphi 10.4 - Sort Memtable by clicking Stringgrid Header

I feel like an idiot because my question seams so simple but I don't get it done :D
My Settings is that:
One Dataset (Memtable), One Stringgrid. The Grid is bind via live Bindungs.
I would like to sort my Columns by clicking on the GridHeader. In the OnHeaderClick Event I get an tColumn Object. I only can read the Column.Header String, but I changed the Text from the Header to a more speakable Text. When I put Column.header into Memtable.Indexfieldsname Memtable says that field does not exist, what is right, but I don't know how to get the right Fieldname from the column.
What you want is quite straightforward to do. In the example below, which uses the demo data from
the Biolife demo, I've linked the StringgRid to the FDMemTable entirely by binding objects
created in code so that there is no doubt about any of the binding steps or binding properties,
nor the method used to establish the bindings.
procedure TForm2.FormCreate(Sender: TObject);
var
BindSourceDB1 : TBindSourceDB;
LinkGridToDataSourceBindSourceDB1 : TLinkGridToDataSource;
begin
// Note : You need to load FDMemTable1 at design time from the sample Biolife.Fds datafile
// The following code creates a TBindSourceDB which Live-Binds FDMemTable1
// to StringGrid1
//
// As a result, the column header texts will be the fieldnames of FDMemTable1's fields
// However, the code that determines the column on which to sort the StringGrid does not depend
// on this
BindSourceDB1 := TBindSourceDB.Create(Self);
BindSourceDB1.DataSet := FDMemTable1;
LinkGridToDataSourceBindSourceDB1 := TLinkGridToDataSource.Create(Self);
LinkGridToDataSourceBindSourceDB1.DataSource := BindSourceDB1;
LinkGridToDataSourceBindSourceDB1.GridControl := StringGrid1;
end;
procedure TForm2.StringGrid1HeaderClick(Column: TColumn);
// Sorts the STringGrid on the column whose header has been clicked
var
ColIndex,
FieldIndex : Integer;
AFieldName : String;
begin
ColIndex := Column.Index;
FieldIndex := ColIndex;
AFieldName := FDMemTable1.Fields[FieldIndex].FieldName;
Caption := AFieldName;
// Should check here that the the field is a sortable one and not a blob like a graphic field
FDMemTable1.IndexFieldNames := AFieldName;
end;
Note that this answer assumes that there is a one-for-one correspondence between grid columns and fields of the bound dataset, which will usually be the case for bindings created using the default methods in the IDE. However, Live Binding is sophisticated enough to support situations where this correspondence does not exist, and in those circumstances it should not be assumed that the method in this answer will work.

Creating property to store two values

Using Delphi 10 I have two values work_start and work_finish of type TTime that I need to read and write from database table so I though to create a property for each one like that
private
fWorkStart: TTime;
function GetWS: TTime;
procedure SetWS(const Value: TTime);
Public
property WorkStart: TTime read GetWS write SetWS;
....
procedure MyClass.SetWS(const Value: TTime);
begin
fWorkStart := value;
mydataset.Edit;
mydataset.FieldByName('work_start').AsDateTime := fWorkStart;
mydataset.Post;
end;
function MyClass.GetWS: TTime;
begin
if mydataset.FieldByName('work_start').IsNull then
fWorkStart := encodetime(6,0,0,0)
else
fWorkStart := mydataset.FieldByName('work_start').AsDateTime;
result := fWorkStart;
end;
WorkFinish property is the same. So is there a way to create one property for both times or my code is fine ?
Craig's answer demonstrates record properties, which means you have a single property that gets set as a unit; you can't set the start and finish times independently. Dawood's answer demonstrates an array property, which allows independent accesses, but imposes cumbersome bracket notation on the consumer. Kobik's comment improves the semantics, but we can do even better using index specifiers.
First, define an enum to represent the two kinds of times:
type
TWorkTime = (wtStart, wtFinish);
Use those values in your property declarations, and provide an extra parameter to your property accessors to represent the index:
private
FWorkTime: :array[TWorkTime] of TTime;
function GetWT(Index: TWorkTime): TTime;
procedure SetWT(Index: TWorkTime; const Value: TTime);
public
property WorkStart: TTime index wsStart read GetWT write SetWT;
property WorkFinish: TTime index wsFinish read GetWT write SetWT;
To reduce the bloat Craig warns about in your accessors, you can define another array with the corresponding fields names, which lets you avoid duplicating code for your different fields:
const
FieldNames: array[TWorkTime] of string = (
'work_start',
'work_finish'
);
function MyClass.GetWT(Index: TWorkTime): TTime;
begin
if mydataset.FieldByName(FieldName[Index]).IsNull then
FWorkTime[Index] := EncodeTime(6, 0, 0, 0)
else
FWorkTime[Index] := mydataset.FieldByName(FieldNames[Index]).AsDateTime;
Result := FWorkTime[Index];
end;
It is possible:
//Define a record to hold both
type
TTimeRange = record
StartTime: TTime;
EndTime: TTime;
end;
//And have your property use the record
property WorkHours: TTimeRange read GetWorkHours write SetWorkHours;
However, this would force clients of your class to interact using the record structure. Basically the complications you'd encounter outweigh the small benefit you'd gain.
So I don't recommend it.
(Although it's worth remembering the technique because in other scenarios it may prove more useful.)
As for your code:
Handling of properties is fine. Although in the code you've presented fWorkStart is redundant.
I'd caution against Edit and Post within your property writer. Apart from the fact that updating 1 field at a time in the Db would be highly inefficient, your method has unexpected side-effects. (And can you always assume edit is the right choice and not insert?)
In your property reader, assuming NULL == 6:00 is not a good idea. NULL has very specific meaning that the value is unknown/unassigned. Defaulting it in the wrong place leads to being unable to tell the difference between 6:00 and NULL. (I'm not saying never default a null; just understand the implications.)
yes you can use indexed properties
property WorkTime[IsStart: Boolean]: TDataTime read GetWorkTime write SetWorkTime;
procedure MyClass.SetWorkTime(IsStart: Boolean;const value: TDataTime);
begin
mydataset.Edit;
if IsStart then
mydataset.FieldByName('work_start').AsDateTime := value else
mydataset.FieldByName('work_Finish').AsDateTime := value;
mydataset.Post;
end;
function MyClass.GetWorkTime(IsStart: Boolean): TTime;
begin
if IsStart then
Begin
if mydataset.FieldByName('work_start').IsNull then
fWorkStart := encodetime(6,0,0,0)
else
fWorkStart := mydataset.FieldByName('work_start').AsDateTime;
result := fWorkStart;
end else
begin
if mydataset.FieldByName('work_finish').IsNull then
fWorkfinish := encodetime(6,0,0,0)
else
fWorkfinish := mydataset.FieldByName('work_finish').AsDateTime;
result := fWorkfinish;
end
end;

Assigning the selected items in a TxpComboBox to a variable

I'm attempting to assign all the selected items in a TxpListBox to a TStringList.
My initial thought was to do something like
Function AssignListBoxToList(ComponentName : TxpListBox) : Boolean;
var
slComponentValue : TStringList;
begin
slComponentValue := TStringList.Create;
slComponentValue.Add(ComponentName.Items);
end;
But it throws the following exception Incompatible types: 'String' and 'TString'.
Is there a way to either create a TStringList of TStrings, or is it safe to use String instead of TString in my TxpListBox, and/or am I missing something.
TxpListBox is a TListBox with a modified look to fit in with the Windows XP design aesthetic.
It looks like TxpComboBox.Items might be a TStrings descendent (like the standard TComboBox.Items). If that's the case, something like this should work:
slComponentValue := TStringList.Create;
slComponentValue.Add(ComponentName.Items[ComponentName.ItemIndex]);
Your function won't work as is, though, because it doesn't return slComponentValue.
It's generally not a good idea (without a specific reason to do so) to return an object from a function, because it's not clear where the responsibility lies to free it. I prefer to make that more clear by having a procedure accept an already-created instance of an object instead:
procedure AssignComboBoxToList(ComponentName : TxpComboBox;
ListToFill: TStrings) : Boolean;
begin
Assert(Assigned(ListToFill));
ListToFill.Add(ComponentName.Items[ComponentName.ItemIndex);
end;
You can then use it like this:
slComponentValue := TStringList.Create;
try
AssignComboBoxToList(YourComboBox, slComponentValue);
if slComponentValue.Count > 0 then
// Do whatever with the slComponentValue list
finally
slComponentValue.Free;
end;
However, as you're only dealing with a single string, it might be easier to just use a single string; there's not really a TStringList neededhere:
strResult := YourComboBox.Items[YourComboBox.ItemIndex];
With that being said, TComboBox doesn't support multiple selections; TListBox does, but TComboBox displays a drop down list and allows selecting of a single item, making your question somewhat unclear.

How to pass StringGrid Row Number to another form?

I have a StrinGrid component and a procedure:
procedure TForm3.StringGrid1Click(Sender: TObject);
begin
SelectedElement := stringgrid1.Cells[0,stringgrid1.Row];
end
SelectedElement is declared in public section:
public
SelectedElement : String;
end;
When I use it in this unit, for example Label1.Caption := SelectedElement, it works fine. But in another unit, where I specified uses unit1 in implementation, and I try to use this variable like this Label1.Caption := Form1.SelectedElement it sets label to empty string. But when I set variable manually for example on first form create, then this value shows up in second form, even if variable is later changed to value from stringgrid.
Given the small amount of code you have shown so far, it is difficult to diagnose your problem for sure, but based on your comments so far, it sounds to me like you are probably dynamically creating your TForm3 object at run-time using TForm3.Create() and not assigning the object to the global Form3 pointer, but are trying to use the global Form3 pointer to access the SelectedElement value. Is that correct?
Also, you show TForm3.StringGrid1Click() is setting TForm3.SelectedElement, but you are accessing Form1.SelectedElement instead of Form3.SelectedElement. Does TForm1 have its own SelectedElement member? Or are you not showing your real code copy/pasted from your real project?
You should add a property to the form that returns the desired value:
....
private
function GetSelectedElement: string;
public
property SelectedElement: string read GetSelectedElement;
....
And implement it like this:
function TForm3.GetSelectedElement: string;
begin
Result := StringGrid1.Cells[0, StringGrid1.Row];
end;
This will always return the current state which I believe is what you want.

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