Setting dynamicly a property of an object containing datetime - delphi

I have a bunch of different methods for creating, calling and setting properties of different objects using Delphi's RTTI. But now I have come to an error where setting a TDateTime triggers an error like: "Cannot convert variant into double". Google doesn't help when searching for this error.
So far, I'm defining an object of any type, for example:
TExample = class
private
FDateField : TDateTime;
published
property DateField : TDateTime read FDateField write FDateField;
end;
I'm then putting this object in a TObjectList, and then looping some internal logic that's not really relevant to the problem. But when I come to the DateField property, it triggers the error. I'm trying to set it like this:
objPropValue := '12/02/2018 12:25:00';
objPropName := 'DateField';
if IsPublishedProp(parameterObject, objPropName) then
begin
SetPropValue(parameterObject, objPropName, objPropValue); <- doesn't work on DateField
end;
This is only a hardcoded example, the objPropValue and Name are set in a loop and can be of any other type. I tried different formatting as well, but I can't seem to find the correct way to do this.

Despite the error message, what you are trying to do here is assign a string to a date, which you just can't do. If instead you did this
objPropValue := '12/02/2018 12:25:00';
objPropName := 'DateField';
if IsPublishedProp(parameterObject, objPropName) then
begin
SetPropValue(parameterObject, objPropName, StrToDateTime(objPropValue));
end;
it would work fine. That is just for illustration, of course. If objPropValue is a variant (which you don't show) you could use
objPropValue := StrToDateTime('12/02/2018 12:25:00');
objPropName := 'DateField';
if IsPublishedProp(parameterObject, objPropName) then
begin
SetPropValue(parameterObject, objPropName, objPropValue);
end;
instead.

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;

Failing to update a table

I have a boolean field in cxGrid (represented as a checkbox inside the grid).
I am trying to do an update of the very same record when user sets the checkbox as checked.
I tried this:
procedure TDataModule3.ABSTable2BeforePost(DataSet: TDataSet);
begin
if DataModule3.ABSTable2.FieldByName('DONE').AsBoolean = True then
with Datamodule3.ABSQuery4 do begin
Datamodule3.ABSQuery4.Close;
Datamodule3.ABSQuery4.SQL.Clear;
Datamodule3.ABSQuery4.SQL.Text :='UPDATE MYTABLE SET USER=:a1 where TW_ID =:a2';
Datamodule3.ABSQuery4.Params.ParamByName('a1').AsString := MainForm.AdvOfficeStatusBar1.Panels[3].Text ;
Datamodule3.ABSQuery4.Params.ParamByName('a2').AsInteger := Datamodule3.ABSTable2.FieldByName('TW_ID').AsInteger;
Datamodule3.ABSQuery4.ExecSQL;
end;
end;
I get :
First chance exception at $00B6E33F. Exception class $C0000005 with message 'access violation at 0x00b6e33f: read of address 0x000003a0'. Process Project1.exe (4916)
What am I doing wrong ?
Edit: I tried the suggestion.
/
if ABSTable2.FieldByName('Done').AsBoolean = True then
begin
ABSQuery4.Close;
ABSQuery4.SQL.Clear;
ABSQuery4.SQL.Text :='UPDATE mytable SET user=:a1 where TW_ID=:a2 ';
ABSQuery4.Params.ParamByName('a1').AsString := ABSQuery1.FieldByName('USER').asString ;
ABSQuery4.Params.ParamByName('a2').AsInteger := ABSTable2.FieldByName('TW_ID').AsInteger;
ABSQuery4.ExecSQL;
end;
I have removed the reference to the main form and added the query syntax as the AdvOfficeStatusBar1.Panels[3].Text gets its data from it.
Now I get: Absolute Engine error : The table is locked.
Remove the DataModule3 from the beginning of all of the lines of code.
You're using an instance variable inside the methods of a class. If the instance is named anything other than DataModule3 or DataModule3 hasn't been created, your code will fail.
The proper way to write the code would be (see the NOTE below):
procedure TDataModule3.ABSTable2BeforePost(DataSet: TDataSet);
begin
if Self.ABSTable2.FieldByName('DONE').AsBoolean = True then
begin
Self.ABSQuery4.Close;
Self.ABSQuery4.SQL.Clear;
Self.ABSQuery4.SQL.Text :='UPDATE MYTABLE SET USER=:a1 where TW_ID =:a2';
// See NOTE below
Self.ABSQuery4.Params.ParamByName('a1').AsString := MainForm.AdvOfficeStatusBar1.Panels[3].Text ;
Self.ABSQuery4.Params.ParamByName('a2').AsInteger := Self.ABSTable2.FieldByName('TW_ID').AsInteger;
ABSQuery4.ExecSQL;
end;
end;
NOTES
The Self is optional. You could write each without the Self. and it would work equally as well in this case. Self refers to the current instance of the object, rather than a specific named instance like DataModule3 does.
You should find a way to remove the reference to MainForm and a visual component by adding an instance variable or property to the datamodule class instead that you can refer to in your code. Hard-coding in the MainForm can cause the same kind of problems that hard-coding in DataModule3 - it's a specific instance name rather than just being the current instance of something. If you rename your MainForm to something else, your code won't compile. If you replace MainForm with a different form, but there's another MainForm in scope that hasn't been created yet, your code will also crash.

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.

TStrings vs TStringList in TCheckListBox

In Delphi 7, I'm using a TCheckListBox. I want it to use a TStringList rather than a TStrings, so I can set Duplicates to dupIgnore, and Sorted to TRUE.
Can I just do this:
Form1 = class(TObject
CheckListBox1: TCheckListBox; // created by the IDE
end;
procedure TForm1.FormCreate
begin
CheckListBox1.Items.Free;
CheckListBox1.Items := TStringList.Create;
CheckListBox1.Items.Sorted := TRUE;
CheckListBox1.Items.Duplicates := dupIgnore;
end;
Is this safe? Any caveats or suggestions?
EDIT: Removed declaration for MyStringList and added .Items to the last two assignment lines.
EDIT 2: Trying to compile the above, it looks like I'd have to cast the two final lines like this:
TStringList(CheckListBox1.Items).Sorted := TRUE;
TStringList(CheckListBox1.Items).Duplicates := dupIgnore;
Although I might be able to get this to run, I'm asking the question because just getting it to run doesn't mean it will always run or is safe.
You don't control what class TCheckListBox uses to store its items. Assigning the Items property a value only assigns its items to the internal storage.
Also, you shouldn't call Items.Free;. TCheckListBox depends on its internal instance of TListBoxStrings.
To answer your edits in your question: Don't hard-cast the Items property to TStringList, either. The typecast is wrong (the instance exposed by Items is not a TStringList) and will only cause problems.
Edit, to suggest a workaround for what you seem to try to achieve: To keep the checklistbox sorted, you can set its Sorted property to True. To avoid duplicates, you can check the list before adding an item in code.

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