Simple FireMonkey Object Inspector - Felix John COLIBRI. |
- abstract : building a FireMonkey Object Inspector which presents the components of the Form and displays their property names an values and allows the user to modify them at runtime. Comments about our first FireMonkey project.
- key words : FireMonkey - tFmxObject - Children - RemoveObject - Rtti - tRectangle - tText - FireMonkey controls Class Diagram
- software used : Windows XP Home, Delphi XE2, update 1
- hardware used : Pentium 2.800Mhz, 512 M memory, 140 G hard disc
- scope : Delphi XE2 FireMonkey
- level : Delphi developer
- plan :
1 - Simple FireMonkey Object Inspector This Delphi XE2 FireMonkey project displays the property names and property
values in a VERY SIMPLE Object Inspector, allowing us, at runtime, to display the Form's component properties and change their values. This simplified "Object Inspector" only displays the properties (not the
events), and its presentation is quite primitive.
2 - Reading and Writing Properties 2.1 - Object Inspector The standard way to present an Object Inspector is to use
- a drawing surface with the Property / Events tabs, each with 2 columns, one for the name, the other for the value
- an in-place edit for updating the property / event values
The mouse is then monitored to let us
- select a property / event
- change the size of the name or value column
2.2 - FireMonkey Name / Value pair display The first step is to be able to display some Name / Value pair on the screen.
The simplest way is to dynamically create tLabel / tEdit pairs. We originally wanted to use a tLabel for the value and replace it with the in-place edit, but the tLabel does not react to the clic. So we decided to
replace it with a tText, wich does react. But we cannot easily display the surface: the tText.Fill.Color changes the font color, not the background. So
we decided to use tRectangles and place the tText on the tRectangle. So the organization is the following:
The coding is then simple : | create a FireMonkey HD application | |
drop a tPanel on Form1 | | type the code which adds our tLabel / tRectangle / tText elements to the tPanel:
Const k_property_value_x= 70+ 2;
k_value_width= 100; Var g_y: Single= 5;
Procedure add_property(p_property_name, p_property_value: String);
Var l_c_property_name_label: tLabel;
l_c_value_rectangle: tRectangle;
l_c_property_value_text: tText;
l_height: Single; Begin
With Form1 Do Begin
l_height:= property_in_place_edit_.Height+ 2;
l_c_property_name_label:= tLabel.Create(Form1);
With l_c_property_name_label Do
Begin Parent:= property_panel_;
Position.X:= 5;
Position.Y:= g_y+ 2;
Text:= p_property_name;
End; // with l_c_property_name_label
l_c_value_rectangle:= tRectangle.Create(Form1);
With l_c_value_rectangle Do
Begin Parent:= property_panel_;
Position.X:= k_property_value_x;
Position.Y:= g_y;
Width:= k_value_width;
Stroke.Color:= claRed;
Height:= l_height;
End; // with l_c_value_rectangle
l_c_property_value_text:= tText.Create(Form1);
With l_c_property_value_text Do
Begin Parent:= l_c_value_rectangle;
Position.X:= 4;
Position.Y:= 1;
Width:= k_value_width;
Height:= l_height- 2;
HorzTextAlign:= TTextAlign.taLeading;
Text:= p_property_value;
// -- the font color
Fill.Color:= claDarkTurquoise;
OnMouseDown:= handle_property_value_mousedown;
End; // with l_c_property_value_text
End; // with Form1
g_y:= g_y+ l_height+ 2;
End; // add_property | The handle_property_value_mousedown is in charge to display the in-place
tEdit. This procedure is defined in the Private part of Form1, and the implementation is:
Procedure tForm1.handle_property_value_mousedown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Single);
Var l_c_sender_text: tText;
l_c_property_value_rectangle: tRectangle; Begin
l_c_sender_text:= Sender As tText;
l_c_property_value_rectangle:= l_c_sender_text.Parent As tRectangle;
With Form1, property_in_place_edit_ Do
Begin Parent:= l_c_property_value_rectangle;
// -- position relative to the PARENT
Position.X:= 4; Position.Y:= 1;
// -- attach the tText to the in place edit Tag
Tag:= NativeInt(l_c_sender_text);
Text:= l_c_sender_text.Text;
Visible:= True;
End; // with Form1, property_in_place_edit_
End; // handle_property_value_mousedown |
where we save in the tEdit.Tag a reference to the tText, to be able to update the tText when the user enters a value on the tEdit |
| drop two tEdits for entering example values of the Name and Value of some property, and a tButton to dynamically add the Name / Value pairs on the tPanel:
Procedure TForm1.add_name_value_Click(Sender: TObject);
Begin
add_property(property_name_edit_.Text, property_value_edit_.Text);
End; // add_name_value_Click | | |
compile, run and enter "Height", "77" and click "add_name_value_", and any other example | | here is the display
| | click on "888" |
| the in place tEdit is positioned, made visible and the value from the tText is transfered into the tEdit :
|
To transfer the value types in the in-place tEdit, we create a tEdit and use
the tEdit.OnChange, or tEdit.OnKeyDown, like this:
Procedure TForm1.property_in_place_edit_KeyDown(Sender: TObject; Var Key: Word;
Var KeyChar: Char; Shift: TShiftState);
Var l_c_property_text: tText; Begin
If Key= 13 Then Begin
property_in_place_edit_.Visible:= False;
l_c_property_text:= tText(property_in_place_edit_.Tag);
l_c_property_text.Text:= property_in_place_edit_.Text;
End; End; // property_in_place_edit_KeyDown |
where we use the tEdit.Tag initialized when the tEdit was positioned to reach the tText control and set the value entered by the user.
Please note
- we used the new NativeInt integer type to cast the Tag property
- to set the tText alignment property, we used the QUALIFIED enumerated notation, as required by FireMonkey:
my_ttext.HorzTextAlign:= TTextAlign.taLeading; |
2.3 - Reading and Writing a Control properties To simulate an Object Inspector, we have to fill a "Selector", which is a combobox containing all component names. Here is the procedure used to fill the combobox:
Procedure fill_property_selector_combobox;
Var l_component_index: Integer; Begin
With Form1 Do Begin
property_selector_combobox_.Items.Clear;
For l_component_index:= 0 To ComponentCount- 1 Do
property_selector_combobox_.Items.Add(Components[l_component_index].Name);
property_selector_combobox_.ItemIndex:= 0;
End; // with Form1 End; // fill_property_selector_combobox
|
Once a name is selected, we have to fill the property names and values. This can be done - by locating the component from its name
- by using RTTI to extract the component property names and values
Procedure fill_object_inspector(p_component_name: String);
Var l_c_component: tComponent;
l_pt_property_list: PPropList;
l_property_count : Integer;
l_property_index : Integer;
l_property_name, l_property_value: String; Begin
l_c_component:= Form1.FindComponent(p_component_name);
If l_c_component Is tFmxObject
Then Begin
GetMem(l_pt_property_list, SizeOf(Pointer)* GetTypeData(l_c_component.ClassInfo)^.PropCount);
l_property_count := GetPropList(l_c_component.ClassInfo, tkProperties,
l_pt_property_list, True);
For l_property_index := 0 To l_property_count- 1 Do
Begin
l_property_name:= l_pt_property_list^[l_property_index].Name;
l_property_value:= GetPropValue(l_c_component, l_property_name, True);
add_property(l_property_name, l_property_value);
End;
FreeMem(l_pt_property_list); End;
End; // fill_object_inspector |
And before filling the property names and values, we must clear any previous content:
Procedure clear_object_inspector;
Var l_child_index: Integer;
l_c_fmx_object: tFmxObject; Begin
With Form1.property_panel_ Do
Begin l_child_index:= ChildrenCount- 1;
While l_child_index>= 0 Do
Begin
l_c_fmx_object:= Children[l_child_index];
If (l_c_fmx_object Is tLabel)
Or (l_c_fmx_object Is tRectangle)
Then Begin
RemoveObject(l_c_fmx_object);
l_c_fmx_object.Free
End; Dec(l_child_index);
End; // while l_child_index End;
End; // clear_object_inspector | Note that - to extract a component's property names and values, we could also use the
new Delphi 2010 RTTI unit:
Procedure fill_object_inspector(p_component_name: String);
Var l_c_component: tComponent;
l_rtti_context: tRttiContext;
l_rtti_type: tRttiType;
l_c_rtti_property: tRttiProperty;
l_property_name, l_property_value: String; Begin
l_c_component:= Form1.FindComponent(p_component_name);
If l_c_component Is tFmxObject
Then Begin
l_rtti_type:= l_rtti_context.GetType(p_c_component.ClassType);
For l_c_rtti_property In l_rtti_type.GetProperties Do
Begin
l_property_name:= l_c_rtti_property.Name;
If l_c_rtti_property.PropertyType.TypeKind In
[tkInteger, tkChar
, tkFloat
, tkString
, tkWChar, tkLString, tkWString
, tkEnumeration
]
Then l_property_value:=
l_c_rtti_property.GetValue(p_c_component).ToString
Else l_property_value:= '?';
add_property(l_property_name, l_property_value);
End; End;
End; // fill_object_inspector | However the items are not sorted by property name
- removing from the end of the list simply saves the forward shifting of the pointers if we remove from the start
- we must also avoid to remove the tLayout Style rectangle, which is the first
child of the property_panel_. If we remove it, the border of the display_panel_ vanishes. The
- if a property value has been selected, the tEdit became a child of the
tRectangle containing the value. Therefore, before freeing all the values, we must remove the in place edit from any tRectangle's children list (the in-place tEdit cannot have a Parent which has been freeed). This can be
done by the following code :
Procedure detach_in_place_edit;
// -- The tEdit becomes invisible Begin
With Form1.property_in_place_edit_ Do
Begin Parent:= Form1;
Tag:= 0; Visible:= False;
End; // with Form1.property_in_place_edit_
End; // detach_in_place_edit | If we do not reassign the Parent, the children are destroyed when their
Parent is destroyed. This was a surprise for us, since we believed that Parent only managed the visibility / clipping, and the destruction was managed ONLY by the Owner. This is not the case in FireMonkey, and not in the VCL.
2.4 - The Final version Finally, we include the tPanel in a tScrollBox to have scrolling functionality. To do so - we simply place the property_panel_ on a tScrollBox
- after the creation of all the properties, we set the property_panel_.Height to the last g_y value, wich automatically triggers the scrolling if this value is greater than the tScrollBox.Height
And here is a snapshot after selecting the top-left "exit_" button in the "Selector" combobox, and changing its Align property to alLeft:
2.5 - The Component UML Class Diagram The components involved in the presentation can be described by the following UML Class Diagram:
You may also have a look at the more complete Firemonkey Architecture Class Diagram, which also contains short component category descriptions
2.6 - What we learned This showed us - that programming standard IDE stuff is just the same as it used to be with the VCL
- a couple of minor points were raised, mainly
- the tFmxObject.Children organization
- tFmxObject.AddObject and tFmxObject.RemoveObject are used to manage those parent / child relationship.
- RemoveObject does not Free the removed object. It simply removes it from the list
- when freeing an object, we have to make sure that it no longer contains
children. This can be done by setting a new my_child.Parent value
- the tStyledObjects all have a child containing the style hierarchy. Removing this child removes the display of the styles of this control (no
border, effects, animation etc)
- the Parent is in charge of the display : the children's Positions, for instance, are relative to the Parent. And there are lots of conversions
methods for absolute values. But the traditional Vcl clipping now requires the ClipChildren True value (the default is False)
- the change in some notations
- tLabel.Text (instead of Caption)
- tCheckBox.IsChecked (instead of Checked). Same for IsVisible etc.
- the Single type for all measures (Position, Scale etc)
- the fully qualified notation for all enumerated.
- notice that this fully qualification does NOT apply to tAlphaColor. The reason is that colors are not enumerated, but "enumerated looking" names which are in fact CONSTs.
- in addition those names have the "A" prefix: clAred, clAblue. I read it somewhere but do not remember what the "A" stands for
- more disturbing is the absence of some simple properties, like
tLabel.Color. As we presented in the FireMonkey Styles article, those changes are performed using the styled controls style elements
As for the result, it certainly is no candidate for the "coolest Object Inspector" in the world. But for a 2 hour job, it seems that a 300 line Object Inspector is not that bad a result after all.
3 - Download the Sources Here are the source code files: The .ZIP file(s) contain:
- the main program (.DPR, .DOF, .RES), the main form (.PAS, .DFM), and any other auxiliary form
- any .TXT for parameters, samples, test data
- all units (.PAS) for units
Those .ZIP
- are self-contained: you will not need any other product (unless expressly mentioned).
- for Delphi 6 projects, can be used from any folder (the pathes are RELATIVE)
- will not modify your PC in any way beyond the path where you placed the .ZIP (no registry changes, no path creation etc).
To use the .ZIP: - create or select any folder of your choice
- unzip the downloaded file
- using Delphi, compile and execute
To remove the .ZIP simply delete the folder. The Pascal code uses the Alsacian notation, which prefixes identifier by
program area: K_onstant, T_ype, G_lobal, L_ocal, P_arametre, F_unction, C_lass etc. This notation is presented in the Alsacian Notation paper. The .ZIP file(s) contain:
- the main program (.DPROJ, .DPR, .RES), the main form (.PAS, .ASPX), and any other auxiliary form or files
- any .TXT for parameters, samples, test data
- all units (.PAS .ASPX and other) for units
Those .ZIP
- are self-contained: you will not need any other product (unless expressly mentioned).
- will not modify your PC in any way beyond the path where you placed the .ZIP
(no registry changes, no path outside from the container path creation etc).
To use the .ZIP: - create or select any folder of your choice.
- unzip the downloaded file
- using Delphi, compile and execute
To remove the .ZIP simply delete the folder. The Pascal code uses the Alsacian notation, which prefixes identifier by program area: K_onstant, T_ype, G_lobal, L_ocal, P_arametre,
F_unction, C_lass etc. This notation is presented in the Alsacian Notation paper.
As usual:
- please tell us at fcolibri@felix-colibri.com if you found some errors, mistakes, bugs, broken links or had some problem downloading the file. Resulting corrections will
be helpful for other readers
- we welcome any comment, criticism, enhancement, other sources or reference suggestion. Just send an e-mail to fcolibri@felix-colibri.com.
- or more simply, enter your (anonymous or with your e-mail if you want an answer) comments below and clic the "send" button
- and if you liked this article, talk about this site to your fellow developpers, add a link to your links page ou mention our articles in
your blog or newsgroup posts when relevant. That's the way we operate: the more traffic and Google references we get, the more articles we will write.
4 - References For RTTI
- Delphi 2010 - RTTI & Attributes
Robert LOVE - September 2009
Robert is the "Delphi Rtti Guru". This link will present you a comprehensive introduction for RTTI. And he will also present several videos about RTTI at the 2011 CodeRage
- Rtti.TRttiType Example
example for displaying methods, properties etc from the Wiki Help
- The Delphi XE2 demos at SourceForge also contain a RttiBrowser
We have a couple of articles about FireMonkey or Delphi XE2 :
- FireMonkey Architecture : the basic tComponent <- tFmxObject <- Fmx.tControl <- tStyledControl hierarchy.
Firemonkey UML Class diagram, and short feature description.
- FireMonkey Styles changing styles for all or for
some components, the Style Designer, content of a .STYLE file, setting then StyleLookup property, predefined styles.
- FireMonkey Animations tutorial : selecting the Property to animate, the start and end
values, the interpolation law, the speed and repetition. 3d animations. Vcl or FireMonkey ? (in French)
- Delphi XE2
LiveBindings Tutorial : how to setup the SourceComponent and the ControlComponent and expression, tBindingsList, the bindings Editor, using several sources with tBindingScope, building bindings by code,
LiveBindings and databases. Far more flexible than the Vcl db_xxx, but with the risks of late binding (in French)
- Delphi LiveBindings Spelunking : analysis of the architecture of the Delphi LiveBindings : how the
tBindingExpression compiles a String expression to build an environment referencing objects which can be evaluated to fill component properties. Dump of the pseudo code and UML Class Diagram of the LiveBinding architecture
- FireMonkey Style Explorer : create tFmxObjects from their class name, create their default style, display their
child style herarchy in a tTreeView, present each style element in an Object Inspector which can be used to change the property values.
5 - The author
Felix John COLIBRI works at the Pascal Institute. Starting with Pascal in 1979, he then became involved with Object Oriented Programming, Delphi, Sql, Tcp/Ip, Html, UML. Currently, he is mainly
active in the area of custom software development (new projects, maintenance, audits, BDE migration, Delphi
Xe_n migrations, refactoring), Delphi Consulting and Delph
training. His web site features tutorials, technical papers about programming with full downloadable source code, and the description and calendar of forthcoming Delphi, FireBird, Tcp/IP, Web Services, OOP / UML, Design Patterns, Unit Testing training sessions. |