Jump to content


Check out our Community Blogs

Register and join over 40,000 other developers!


Recent Status Updates

View All Updates

Photo
- - - - -

Unleash the Power of Delphi RTTI

runtime

  • Please log in to reply
No replies to this topic

#1 Luthfi

Luthfi

    CC Leader

  • Expert Member
  • PipPipPipPipPipPipPip
  • 1320 posts
  • Programming Language:PHP, Delphi/Object Pascal, Pascal, Transact-SQL
  • Learning:C, Java, PHP

Posted 17 April 2011 - 01:23 PM

RTTI stands for RunTime Type Information. Simply put, it provides information that is only available in run time, not in coding/design time. For example, using RTTI you can examine whether an object/class has property with specific name and if it has, you can examine whether the property is read only, read-writable, or write only. With this information at hand, you can retrieve the property current value and even modify it (if it's writable).

For Delphi itself RTTI is playing very important role, since gui designing in Delphi relies heavily on RTTI. Values you can inspect and modify in the object inspector is provided through RTTI. However since RTTI initially developed for this purpose (supporting GUI design time), complete information about it is not documented "properly" (except in a few last versions of Delphi since in them RTTI has been developed further and coders are encouraged to take advantage of it).

RTTI is not only limited with manipulating properties. It can provide information of methods and parameters of the methods. The only requirements for RTTI to work is that the properties or methods must be published (i.e. declared in published section of the class), and the class or any of its parent was declared with {$M+} compiler directive, i.e. something like this:

type
  {$M+}
  TRTTIEnabledClass=class
  end;
  {$M-}

Beside specifically use {$M} directive, we can also derive our class from TPersistent which was declared with {$M+} on.

In this tutorial, I am going to show you how to use RTTI to retrieve and modify property value of a class using a custom edit control derived from TEdit.

Base Class

Here we will write a base class that enable RTTI access to it and also has methods supporting RTTI based command. Let's call it TRttiObj. It must has methods that:

  • indicates if it has property of a specific name
  • indicates if we can read the property value (to anticipate the possibility of write-only properties which will generate error if we read from them)
  • indicates if a property (specified by name) is read-only or not
  • returns the kind of the property value
  • returns the property value in string format
  • modifies the property value using string format

The declaration of the class would be something like:
uses
  ...
  , TypInfo    // RTTI library was provided in this unit
  , ..
  ;
  
type
  {$M+}
  TRttiObj=class
  protected
    // checks if published property specified by APropName exists
    function GetIsPropExists(APropName: string): Boolean;
    // checks if we can read the property's value
    function GetIsPropReadable(APropName: string): Boolean;
    // checks if the property specified by APropName writable or not
    function GetIsPropReadOnly(APropName: string): Boolean;
    // returns the kind of the property specified by APropName. TTypeKind was
    // declared in TypInfo
    function GetPropKind(APropName: string): TTypeKind;
    // returns the property value in string
    function GetPropAsString(APropName: string): string;
    // modify property value using string value
    procedure SetPropAsString(APropName: string; const Value: string);
  public
    property IsPropExists[APropName: string]: Boolean read GetIsPropExists;
    property IsPropReadable[APropName: string]: Boolean read GetIsPropReadable;
    property IsPropReadOnly[APropName: string]: Boolean read GetIsPropReadOnly;
    property PropKind[APropName: string]: TTypeKind read GetPropKind;
    property PropAsString[APropName: string]: string read GetPropAsString 
	         write SetPropAsString; default;
  end;
  {$M-}

Note that the real methods were declared in protected section, but access to them are provided publicly by declaring "virtual" properties of array type, i.e.:
  • property IsPropExists is actually GetIsPropExists
  • property IsPropReadable is actually GetIsPropReadable
  • property IsPropReadOnly is actually GetIsPropReadOnly
  • property PropKind is actually GetPropKind
  • property PropAsString is actually GetPropAsString (when reading) and SetPropAsString (when writing)

This approach will make our code easier to read (self commenting code), e.g. we can write something like this:
  if Person.IsPropReadable['Name'] and (Person.PropKind['Name']=tkLString) then
  begin
    WriteLn(Person.PropAsString['Name'];
    Person.PropAsString['Note'] := 'Displayed';
  end;
which I believe is easier to comprehend than this:
  if Person.GetIsPropReadable('Name') and (Person.GetPropKind('Name')=tkLString) then
  begin
    WriteLn(Person.GetPropAsString('Name');
    Person.SetPropAsString('Note', 'Displayed');
  end;

Furthermore, since we have we set PropAsString as default property, we can rewrite the above sample code into:
  if Person.IsPropReadable['Name'] and (Person.PropKind['Name']=tkLString) then
  begin
    WriteLn(Person['Name'];
    Person['Note'] := 'Displayed';
  end;
Neater, I believe.

However that is not the only reason I choose to use them through properties. With this approach their values are immediately available when tracing your codes without having to run into their actual codes.

Implementation of GetIsPropExists function:
function TRttiObj.GetIsPropExists(APropName: string): Boolean;
var
  // local var to store pointer to data on a property
  vPropInfo: PPropInfo;
begin
  // get the property information
  vPropInfo := GetPropInfo(Self, APropName);
  // if no property information returned then the property is not a published one
  Result := vPropInfo <> nil;
end;

Basically this function only checks if rtti information on property of the given name is available. When not available, then this property does not exist or at least not published.

Implementation of GetIsPropReadable function:
function TRttiObj.GetIsPropReadable(APropName: string): Boolean;
var
  // local var to store pointer to data on a property
  vPropInfo: PPropInfo;
begin
  Result := False;
  // get the property information
  vPropInfo := GetPropInfo(Self, APropName);
  // if no property information returned then the property is not a published one
  // in that case do not continue
  if vPropInfo = nil then Exit;

  // the property is readable only if it has GetProc specified
  Result := vPropInfo^.GetProc <> nil;
end;

This function checks if rtti information is available and there is a "getter" for the property. If rtti info is not available or the "getter" does not exist then we cannot read the property.

Implementation of GetIsPropReadOnly function:
function TRttiObj.GetIsPropReadOnly(APropName: string): Boolean;
var
  // local var to store pointer to data on a property
  vPropInfo: PPropInfo;
begin
  Result := False;
  // get the property information
  vPropInfo := GetPropInfo(Self, APropName);
  // if no property information returned then the property is not a published one
  // in that case do not continue
  if vPropInfo = nil then Exit;

  // the property is read-only if it has no SetProc specified
  Result := vPropInfo^.SetProc = nil;
end;

This one is very similar with GetIsPropReadable. But instead of checking "getter", it checks for "setter". If no setter is found then the property is not writable.

Implementation of GetPropAsString function:
function TRttiObj.GetPropAsString(APropName: string): string;
var
  T: Variant;
begin
  // get the variant version of the property value
  T := GetPropValue(Self, APropName, True);

  // make sure we return the value in "string" format
  Result := VarAsType(T, varString);
end;

Note that this is a quick implementation. We only read the property value as variant and cast that variant into string. Better implementation is when we detect the type of value and then format the returned string according to the value type (and our taste).

Implementation of SetPropAsString procedure:
procedure TRttiObj.SetPropAsString(APropName: string; const Value: string);
begin
  TypInfo.SetPropValue(Self, APropName, Value);
end;

This one is also a quick implementation, for we don't check if the string really fit into the property (according to the actual type). For example, this would cause exception if we write date time value like 'January 1st, 2011' (which real type is float) into a property of tkFloat type.

RTTI Edit Control

Here we will develop a special TEdit which is "rtti-aware". Using this edit control we only have to give it the property name of the object and it will automatically read and write correct value from our rtti object assigned to it. Furthermore, the assignment of property name can be done in design time (no need from code). Since we want that this control available in design time, we will put it in our own package and install this package as design time package.

So create a new package and name it RttiComps.dpk. Add to it a new unit where we will define our edit control. Let's name this unit RttiEdit.pas.

Here are things we need for our edit control:

  • Reference to the rtti object so we can read and write its property value. We will store this reference in a private variable, but allow access to it through a public property and use a setter method when writing so we can update the edit's text when the rtti object is changed.
  • The name of the property of which this edit control must work with. This will be saved into a private variable, and access to it will be allowed through published property with a setter method when writing. By publishing the property, we can access it in design time.
  • Method to read property value from the rtti object. It will be called each time we change the rtti object or change the property name. Usually I would put this method in protected section, but for this tutorial I think it's better to public it.
  • Method to write value to the rtti object. It will be called whenever text of the edit control is changed. I also prefer to declare it in protected section, but for this tutorial we'll public it.
  • A way to automatically write the edit's text into the rtti property.

In RttiEdit.pas put the following codes.

unit RttiEdit;

interface
uses
  Classes
  , Controls
  , StdCtrls
  , RttiBase
  ;

type
  TRttiEdit=class(TEdit)
  private
    FPropName: string;
    FRttiObj: TRttiObj;
    procedure SetPropName(const Value: string);
    procedure SetRttiObj(const Value: TRttiObj);
  protected
    procedure Change; override;
  public
    procedure WriteRttiObj;
    procedure ReadRttiObj;
    
    property RttiObj: TRttiObj read FRttiObj write SetRttiObj;
  published
    property PropName: string read FPropName write SetPropName;
  end;

  procedure Register;

implementation

procedure Register;
begin
  RegisterComponentsProc('CodeCall', [TRttiEdit]);
end;

{ TRttiEdit }

procedure TRttiEdit.Change;
begin
  inherited;
  WriteRttiObj;
end;

procedure TRttiEdit.ReadRttiObj;
begin
  if (FRttiObj<>nil)
     and (FPropName<>'')
     and FRttiObj.IsPropExists[FPropName]
     and FRttiObj.IsPropReadable[FPropName] then
    Self.Text := FRttiObj[FPropName]
  else
    Self.Text := '';
end;

procedure TRttiEdit.SetPropName(const Value: string);
begin
  if FPropName <> Value then
  begin
    FPropName := Value;
    ReadRttiObj;
  end;
end;

procedure TRttiEdit.SetRttiObj(const Value: TRttiObj);
begin
  if FRttiObj <> Value then
  begin
    FRttiObj := Value;
    ReadRttiObj;
  end;
end;

procedure TRttiEdit.WriteRttiObj;
begin
  if (FRttiObj<>nil)
     and (FPropName<>'')
     and FRttiObj.IsPropExists[FPropName]
     and not FRttiObj.IsPropReadOnly[FPropName] then
    FRttiObj[FPropName] := Self.Text;
end;

end.

Since the codes are pretty simple and self explanatory, I won't give much explanation here. The only thing I want to point out is the purpose of global function Register in that unit. Its purpose is to register our TRttiEdit into Delphi Editor, so TRttiEdit is available in components pallete and also viewable in design time.

When it's ready, compile RttiComps packaged and install it. Upon successful installation you will get a new tab in the component pallette named CodeCall and inside you will find RttiEdit component.

[ATTACH]3829[/ATTACH]


Demo/Test Project

  • Create new delphi application.
  • Define a descendant of TRttiObj that has two published properties Name (of type string) and Age (of type integer). Name this class TPerson. The structure of this class would be like:
    type
      TPerson=class(TRttiObj)
      private
        FAge: Integer;
        FName: string;
      public
        constructor Create(const AName: string; const AAge: Integer);
      published
        property Name: string read FName write FName;
        property Age : Integer read FAge write FAge; 
      end;
    

    and the implementation of constructor Create will be:
    constructor TPerson.Create(const AName: string; const AAge: Integer);
    begin
      FName := AName;
      FAge  := AAge;
    end;
    
  • In the main form drop two TGroupBoxes, one is for editing and the other one is for inspecting only.
  • In the editing group box add two TRttiEdits along with two TLabels. Give the first TRttiEdit name of edtName and set its PropName property into Name. We will use this edit box to show and modify the Name of a TPerson. Now name the second TRttiEdit edtAge and set its PropName into Age. We will be using this edit box to edit Age of a TPerson.
  • In the "inspect-only" group box add two TRttiEdits, two TLabels, a TCheckBox, and a TBitBtn.
    Name the first TRttiEdit edtName2, and set its PropName into Name. Give the second TRttiEdit name of edtAge2 and set its PropName into Age. Set the ReadOnly property of both TRttiEdit into True to make them unavailable to be changed directly by user.

    Rename the TCheckBox into ckbAutoRefresh and give Autorefresh for its Caption. We will use this control to indicate whether information shown by the two TRttiEdits must be updated automatically or manually (by clicking the TBitBtn).
    Give the TBitBtn Refresh Info as its caption, and use the following codes for its OnClick event handler.
    procedure TForm1.BitBtn1Click(Sender: TObject);
    begin
      edtName2.ReadRttiObj;
      edtAge2.ReadRttiObj;
    end;
    

    The above codes simply tells edtName2 and edtAge2 to refresh their information.

  • Drop a TApplicationEvents onto the mainform, and use the following codes for its OnIdle event handler.
    procedure TForm1.ApplicationEvents1Idle(Sender: TObject;
      var Done: Boolean);
    begin
      if ckbAutorefresh.Checked then
        BitBtn1.Click;
    end;
    

    The above codes simply checks if ckbAutoRefresh is checked or not. When checked it will simulates click event of our TBitBtn.

  • In private section of the main form, add a variable to hold our instance of TPerson. In the OnCreate event of the form we will create an instance of TPerson and store the reference to this variable, and in OnDestroy event we will free that instance of TPerson.

    In OnCreate event we also have to assign our TPerson to the TRttiEdit. Like below.

    procedure TForm1.FormCreate(Sender: TObject);
    begin
      FPerson := TPerson.Create('John Doe', 20);
      edtName.RttiObj := FPerson;
      edtAge.RttiObj := FPerson;
      edtName2.RttiObj := FPerson;
      edtAge2.RttiObj := FPerson;
    end;
    
  • Now the main form will look similar like this.

    [ATTACH]3830[/ATTACH]

Run The Demo Project
Now since we are set, let's run the demo project. Note that prior to run time, our TRttiEdits never know that TPerson has property named [i]Name
and [i]Age
. And none of our code that directly assign the content of TPerson.Name or TPerson.Age into the TRttiEdit.

After playing with the demo project for some time, you will notice that changes done by edtName and edtAge can be detected automatically by readonly edtName2 and edtAge2. If you set AutoRefresh to true, then the readonly TRttiEdits can show the changes in real time as you typed in.

RttiDemo_RunningTime.png

Full source code of this tutorial is attached below, feel free to use or improve it.

[ATTACH]3831[/ATTACH]

Attached Thumbnails

  • RttiDemo_MainForm_.png
  • CodeCall_CompPallette.png

Attached Files


Edited by LuthfiHakim, 17 April 2011 - 02:00 PM.

  • 0





Also tagged with one or more of these keywords: runtime

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download