Athena

Run-Time Type Information In Delphi - Can It Do Anything For You?

Brian Long (www.blong.com)

If you find this article useful then please consider making a donation. It will be appreciated however big or small it might be and will encourage Brian to continue researching and writing about interesting subjects in the future.


Introduction

All versions of Delphi have supported the generation of run-time type information, or RTTI. Support for this has been added first and foremost to allow the design-time environment to do its job, but developers can also take advantage of it to achieve certain code simplifications. Unfortunately, it has not been made entirely obvious how to use this information as the RTTI has for the most part remained undocumented. This paper attempts to lift the mystique from RTTI and move it from the realms of the propeller head into the reach of the average Delphi user.

What Is RTTI?

RTTI is implemented as data structures generated by the compiler whilst compiling a program. The general idea is to allow information describing various categories of types to be available at run-time for examination and indeed more productive purposes. Normally, types are considered to only be available at compile-time, used by the compiler to identify storage requirements, to ensure appropriate values are used with various variables and so on. RTTI means we have extra information available to query for our own purposes.

Why Do We Have RTTI?

To run the risk of stating the obvious, the Delphi design-time environment allows you to visually design forms that are used in your programs at run-time. It stores the form description in a binary .DFM file, including the type names of all the components placed on the form and also the names and (non-default) values of all form and component properties. At run-time your form file (which was turned into a resource in your EXE during compilation) is read and your EXE magically creates all the objects giving them all the required property values.

In order to get from a string, signifying a class name, to a created object of the required type requires RTTI (or a humungous case statement, which would be a nightmare to maintain). Also, in order to set the values of the named properties in the created objects, bearing in mind they can be of many types, also requires RTTI.

Additionally, the is and as keywords rely on the presence of some form of RTTI to identify if a given object reference is defined in terms of a supplied class type, or one of its descendants. If no information on types was present at run-time then identifying ancestors of any types would be rather tricky.

How Do We Get RTTI?

When designing forms, references to all the objects placed on the form are inserted at the top of the form class definition. This top section has no visibility specifier on show, but is considered to be published. Entries in the published and public sections are equally accessible at run-time. The principal difference between published and public is that published items of a component appear in the Object Inspector at design-time. This happens because RTTI is automatically generated for the types used to define data fields in the published section. The Object Inspector picks this up and uses it to identify what to add to its list of properties and events.

To access the RTTI for a class, you can access its ClassInfo method. This returns a pointer to an RTTI block as generated by the compiler. For any other arbitrary type (that the compiler supports generating it for) you can pass the type to the TypeInfo function.

TypeInfo is one of those special functions that gets evaluated by the compiler at compile time. If you pass a type identifier to TypeInfo (as opposed to a class reference variable that would need to be evaluated at run-time) then, presuming it is an appropriate type, at run-time you will have some RTTI for that type to play with. If the type is a class, then the RTTI will exist in your executable anyway, but for other types such as enumerated types and sets, it won’t necessarily exist.

Types not appropriate for passing to TypeInfo include any pointer types (e.g. PChar, PWideChar, PString or Pointer), types defined locally in a subroutine, array types and record types. Delphi 3 enhances the situation by supporting arrays, records and interfaces, although array or record properties are still not supported in component published sections. Delphi 4 goes forward with 64-bit integers and dynamic arrays. Type information will exist for a given type if a call to TypeInfo is made for it, or if the type is referred to in the published section of a class that is referenced in the program source. If type information exists, it will not be duplicated. Calling TypeInfo a second time on one type will return a pointer to the same RTTI table.

Calling an object’s or class’s ClassInfo method performs the same job as passing it to the TypeInfo function. It returns a pointer to an object’s RTTI. The only real difference is that TypeInfo is evaluated at compile time. The implication of this fact is that you must pass TypeInfo an actual type, rather than a class reference variable. The net result is that TypeInfo will be more efficient if you know the type.

How Do We Talk To RTTI?

It has been mentioned that both TypeInfo and TObject.ClassInfo return a pointer to a compiler-generated RTTI block. In order to make use of RTTI we need to know the structure of this table. Fortunately, Borland have supplied a unit which defines it and also has various routines to give us access to it. The TypInfo unit had the interface section supplied in Delphi 1, but Delphi 2 onwards supply the whole unit (if you have the VCL source). However Inprise has offered no other documentation on this unit, apart from the calls to its routines that are scattered through the VCL. The interface file in Delphi 1 also had a stern message alerting you to the dangers of its use: Warning: The interface section of this file will change in future versions of Delphi.

The unit defines a number of types, many of which have fields that have been commented out. This is because the types are there to try and provide a Pascal framework around the RTTI that the compiler generates in a space-efficient format.

Let’s start at the beginning. Both the TypeInfo pseudo-function and the ClassInfo class method are defined to return a simple Pointer, but they really return a PTypeInfo pointer. This is a pointer to the most important type in the unit, the TTypeInfo record, which looks like this:

TTypeInfo = record
  Kind: TTypeKind;
  Name: string;
  {TypeData: TTypeData}
end;

Kind is a value from the TTypeKind enumerated type, which gets four additional values in Delphi 2, another three in Delphi 3 and a pair more in Delphi 4. TTypeKind rather dictates the sorts of general types that can have RTTI:

TTypeKind value

Types that yield this value

tkUnknown

A place holder value. Never used

tkInteger

Used for any ordinal type and sub-range types

tkChar

Char and AnsiChar types (where Char and AnsiChar are synonyms)

tkEnumeration

All enumerated types. This includes Boolean, ByteBool, WordBool, LongBool and Bool

tkFloat

Any floating point type except Real, which explains why Real properties are not fully supported

tkString

Old-style string types, e.g. String[12] and ShortString

tkSet

Set types

tkClass

Class types

tkMethod

Procedure and function method types

tkWChar

WideChar type, new in Delphi 2

tkLString

Delphi 2+ long strings (made of AnsiChars)

tkLWString

Delphi 2 constant added in preparation of long wide strings for Unicode, which were planned for Delphi 3

tkWString

The Delphi 3 constant which replaces tkLWString

tkVariant

Variant type, new in Delphi 2

tkArray

Array types, new in Delphi 3

tkRecord

Record types, new in Delphi 3

tkInterface

Interface types, new in Delphi 3

tkInt64

64-bit integers, new in Delphi 4

tkDynArray

Dynamic array types, new in Delphi 4

To get the type name of an appropriate type at run-time we can use something like:

TTypeInfo(TypeInfo(Integer)^).Name

The last TTypeInfo field, TypeData, is commented out because the Name field (a string holding the type name) is not what it seems. It is implemented as an old-style Pascal short string, but for space efficiency only takes up as many bytes as are required for the type name, rather than a fixed number (which would leave several unused characters). Therefore the position of the TypeData field is variable.

If we want to get access to the TypeData field we have to fiddle around somewhat, setting up a pointer to the relevant position in the record after using the length of Name to identify where it is. The TypInfo unit provides a routine to do this as we will see later.

The TTypeData record is where things get interesting/messy (delete as appropriate). It is a large variant record with different fields being used depending on the value of the TTypeInfo.Kind field. Let’s look at a simplified view of it.

TTypeData = packed record
  case TTypeKind of
    tkUnknown, tkLString, tkWString, tkVariant: ();
    tkInteger, tkChar, tkEnumeration, tkSet, tkWChar (...):
    tkFloat: (FloatType: TFloatType);
    tkString: (MaxLength: Byte);
    tkClass: (...);
    tkMethod: (...);
    tkInterface: (...);
    tkInt64: (...);
  end;

In the case of a long or wide string or variant there is no additional type information recorded. For a floating point type, a value from the TFloatType appears that specifies what variety it is.

TFloatType = (ftSingle, ftDouble, ftExtended, ftComp, ftCurr);

There are two thing to notice here. Firstly, the ftCurr symbol was new in Delphi 2 to denote the Currency type.

Secondly, there is no symbol to represent the Real type, since it’s use was meant to be dwindling. In Delphi 1 to 3 we were advised not to use type Real, as it was a proprietary data format and nothing other than Delphi understood it. The fact that no type information float type symbol exists for Real explains why published properties of type Real are not supported - RTTI cannot be generated for them. In Delphi 4, Real is now defined to be the same as Double. So Real can be used for published properties and will have an associated float type symbol of ftDouble. However if Delphi 4 programmers really want to use the old 6-byte Real format they can either use type Real48, or use the $RealCompatibility compiler directive.

In the case of a short string, the maximum length is recorded, and for ordinal types, class types, method types and interface types, more information is stored.

...
tkInteger, tkChar, tkEnumeration, tkSet, tkWChar: (
  OrdType: TOrdType;
  case TTypeKind of
    tkInteger, tkChar, tkEnumeration, tkWChar: (
      MinValue: Longint;
      MaxValue: Longint;
      case TTypeKind of
        tkInteger, tkChar, tkWChar: ();
        tkEnumeration: (
          BaseType: PTypeInfo;
          NameList: ShortString));
        tkSet: (
          CompType: PPTypeInfo));
...

All ordinal types have a field OrdType of type TOrdType which says how many bytes are required and whether signed values are supported.

TOrdType = (otSByte, otUByte, otSWord, otUWord, otSLong);

Note that Delphi 3’s implementation of Cardinal (ostensibly an unsigned four byte number) is implemented as a Longint without the negative values (i.e. a 31-bit unsigned number). It uses otSLong. Delphi 4 extends Cardinal to be a true 32-bit unsigned number, however the OrdType value is still otSLong.

If the type is not a set, the minimum and maximum values are available in terms of Longints and if it is an enumerated type you can gain access to all the enumerated type value names. There is also a field called BaseType which presumably is meant to point to the RTTI table for the enumerated type used as a basis for this one, but I have been unable to get it to point to anything other than the same type’s RTTI.

If the type is a set, you can get to the RTTI table for the set’s base type - the ordinal type whose values can be in a set of this type. Note that the BaseType field for an enumerated type mentioned just above and the CompType field for a set are PTypeInfo’s in Delphi 1 and 2 (pointers to TTypeInfo records) but are defined as PPTypeInfo’s in Delphi 3 onwards (pointers to PTypeInfo record pointers). This should, in theory require conditional compilation but in fact you can forget about the extra level of indirection in Delphi 3+. It has been the case since Delphi 2 came out that if a de-reference operation was required (i.e. a caret, ^, sign was needed) you could dispense with it if there would be no ambiguity as to what was intended.

...
tkClass: (
  ClassType: TClass;
  ParentInfo: PTypeInfo;
  PropCount: Smallint;
  UnitName: ShortString
  {PropData: TPropData});
...

RTTI for a class type includes a class reference, a pointer to the ancestor class’s RTTI table (again defined as PTypeInfo or PPTypeInfo depending on which version), the number of published properties and the name of the defining unit. Again, this name is only as long as it needs to be, hence the commented field that follows. In addition, there is a TPropData record that follows containing data about all the published properties.

TPropData = packed record
  PropCount: Word;
  PropList: record end;
  {PropList: array[1..PropCount] of TPropInfo}
end;

This time the commented out field at the end can be accessed because it is not preceded by an indeterminately long string. However because this PropList field is supposed to be a variably sized array, its declaration is commented and substituted with an empty record. PropCount dictates how many TPropInfo records there are, and each TPropInfo record contains all the characteristics of a published property.

TPropInfo = packed record
  PropType: PTypeInfo;
  GetProc: Pointer;
  SetProc: Pointer;
  StoredProc: Pointer;
  Index: Integer;
  Default: Longint;
  NameIndex: Smallint;
  Name: ShortString;
end;

We have a pointer to the RTTI table for the property’s type, a pointer to the property reader and property writer methods. Additionally there is a reference to the method, field or value that is used to determine whether to store the property value in the form file (specified with the stored directive), the property index (specified with the index directive), a default value (specified by the default directive, which is also used to dictate whether the property gets stored in the form file) and its name.

...
tkMethod: (
  MethodKind: TMethodKind;
  ParamCount: Byte;
  ParamList: array[0..1023] of Char
  {ParamList: array[1..ParamCount] of
    record
      Flags: TParamFlags;
      ParamName: ShortString;
      TypeName: ShortString;
    end;
  ResultType: ShortString});
...
 
TMethodKind = (mkProcedure, mkFunction);
TParamFlags = set of (pfVar, pfConst, pfArray);

Information about published method is also quite full. We can find out if the method is a procedure or function, and how many parameters it has. Delphi 3 added new values to the TMethodKind type (mkSafeProcedure and mkSafeFunction) to represent routines compiled with the new safecall directive as used in dual interface declarations and implementations. Delphi 4 added several more: mkConstructor, mkDestructor, mkClassProcedure, and mkClassFunction. ParamCount specifies also how many parameter description records (the commented out record below) exist, and each one of those has an indication of whether the parameter has a var or const prefix, if it is an array, and also what the parameter’s name and type name are. Again, Delphi 3 extended the TParamFlags type with pfAddress, pfReference and pfOut for the new parameter types supported in interfaces.

Once you see the extent of the information recorded in RTTI (automatically for types used in published component sections), you can begin to get an idea of how the Object Inspector does its job. It knows how to let you edit values, because it can find out all the details about the type. The Events page can manufacture event handlers because event properties have a tkMethod type identifier, and Delphi can find out what sort of event handler to manufacture.

The TTypeData record has a lot of useful stuff in, but of course with all these non-standard strings dotted here and there, accessing the various constituent records proves a bit tricky. There are a number of routines in the TypInfo unit to help us out, as explained below.

What Can We Do With RTTI?

The routines available in the TypInfo unit are much the same in the three current versions of Delphi, although in some cases where strings are returned they differ. In Delphi 1, pointers to strings are sometimes returned, but in Delphi 2 and 3 the strings themselves are always returned.

The routines tend to take either a PTypeInfo or PPropInfo pointer - a pointer to an RTTI table or to a property information record as described above.

Remember that a PTypeInfo pointer can be obtained both from the TypeInfo function for most types and from the ClassInfo class method for object references and class references. It’s interesting that to make things simple, the TTypeData record for a class type also has ClassType field that points back to the class reference.

We can gain easy access to the TTypeData record using GetTypeData which returns a pointer to it:

function GetTypeData(TypeInfo: PTypeInfo): PTypeData;

so to show the unit in which the class of the main form was defined in (by default, this would be Unit1), we can use:

GetTypeData(TypeInfo(TForm1))^.UnitName

To be more generic, bearing in mind the form's class name may have changed, we could use:

GetTypeData(Application.MainForm.ClassInfo)^.UnitName

Since all of the TypInfo routines that operate on properties want a PPropInfo, there are several ways of getting one. GetPropInfo gets a TPropInfo record for a named property. GetPropInfos gets TPropInfos for all published properties in the class and its ancestors, and puts them in a TPropList array. Lastly, GetPropList will get all the properties whose types match the values in the set parameter TypeKinds and return how many it found. If you pass tkProperties as this parameter, it acts much the same as GetPropList (although GetPropList does not return how many properties were found - you have to use TTypeData.PropCount instead).

const
  tkAny = [Low(TTypeKind)..High(TTypeKind)];
  tkMethods = [tkMethod];
  tkProperties = tkAny - tkMethods - [tkUnknown];
 
PPropList = ^TPropList;
TPropList = array[0..16379] of PPropInfo;
 
function GetPropInfo(TypeInfo: PTypeInfo;
  const PropName: string): PPropInfo;
procedure GetPropInfos(TypeInfo: PTypeInfo; PropList: PPropList);
function GetPropList(TypeInfo: PTypeInfo; TypeKinds: TTypeKinds;
  PropList: PPropList): Integer;

For example, to get the names of all form properties in a listbox called ListBox1, you could write:

procedure TForm1.FormCreate(Sender: TObject);
var
  Count, Loop: Integer;
  List: TPropList;
begin
  Count := GetPropList(TypeInfo(TForm1), tkAny, @List);
  Listbox1.Items.BeginUpdate;
  for Loop := 0 to Pred(Count) do
    Listbox1.Items.Add(List[Loop]^.Name);
  Listbox1.Items.EndUpdate;
end;

or perhaps like the following which would only take as much memory as is required:

procedure TForm1.FormCreate(Sender: TObject);
var
  Count, Loop: Integer;
  List: PPropList;
begin
  Count := GetPropList(TypeInfo(TForm1), tkAny, nil);
  GetMem(List, Count * SizeOf(PPropInfo));
  try
    GetPropList(TypeInfo(TForm1), tkAny, List);
    Listbox1.Items.BeginUpdate;
    for Loop := 0 to Pred(Count) do
      Listbox1.Items.Add(List^[Loop]^.Name);
    Listbox1.Items.EndUpdate;
  finally
    FreeMem(List, Count * SizeOf(PPropInfo))
  end;
end;

Reading and writing arbitrary properties

There are five pairs of routines (four in Delphi 1) for reading and writing property values of a given name (a sixth was added in Delphi 4):

TypInfo routine

Properties to use them with

GetOrdProp,SetOrdProp

ordinal or class properties

GetStrProp, SetStrProp

long or short string properties

GetFloatProp, SetFloatProp

floating point properties

GetVariantProp, SetVariantProp

variant properties (not present in Delphi 1)

GetMethodProp, SetMethodProp

method properties (events)

GetInt64Prop, SetInt64Prop

64-bit Integer properties (added in Delphi 4)

The Get/SetOrdProp routines both take an object reference and a pointer to a property information record and they either take or return a Longint. A four-byte Longint is big enough to represent any Pascal ordinal value and also any object reference and so you use a typecast to translate between your value of interest and the Longint.

function GetOrdProp(Instance: TObject; PropInfo: PPropInfo): Longint;
procedure SetOrdProp(Instance: TObject;
  PropInfo: PPropInfo; Value: Longint);

The other routines are laid out in a similar way, but take or return a value of an appropriate type.

These routines are used by the VCL streaming mechanics to read and write form files. Once a property value is read from a form file, information is looked up (a TPropInfo record) and its type category is identified (TTypeKind). Then the relevant property writing routine is invoked to set the property value with the data read in from the form file.

Common properties with no common ancestor

You can often come up against a requirement to set a property in many components where there is no common ancestor that defines this property. Let’s look at an example or two to emphasise the problem and see how RTTI can offer us a solution.

Example 1 - the Enabled property

Consider this scenario. You have a form with a number of controls on that all need to be disabled by having their Enabled property set to False. The controls might be a button, an edit control and a check box, but then again may be of some other types. You could implement a routine called DisableThem as shown below that disables any controls.

procedure DisableThem(Comps: array of TControl);
var
  Loop: Integer;
begin
  for Loop := Low(Comps) to High(Comps) do
    Comps[Loop].Enabled := False;
end;
...
DisableThem([Button1, Edit1, Checkbox1]);

Now consider that you also need to disable some menu items, which also have an Enabled property. The problem here is that the TMenuItem class adds Enabled as a new property of its own - TMenuItem is derived from TComponent that does not have an Enabled property. On the other hand, the TButton, TEdit and TCheckBox class all inherit the Enabled property from their common ancestor type TControl.

So the Enabled property of the TMenuItem and that of all the other classes are actually different properties - they just have the same names. This means we have to use a different coding approach.

procedure DisableThem(Comps: array of TComponent);
var
  Loop: Integer;
begin
  for Loop := Low(Comps) to High(Comps) do
    if Comps[Loop] is TMenuItem then
      TMenuItem(Comps[Loop]).Enabled := False
    else
      TControl(Comps[Loop]).Enabled := False;
end;
...
DisableThem([Button1, Edit1, Checkbox1, Menu1]);

If we need to address other components with unique Enabled properties, this code will lengthen. A possibly better approach that takes advantage of the SetOrdProp routine from the TypInfo unit follows.

procedure DisableThem(Comps: array of TComponent);
var
  Loop: Integer;
  PropInfo: PPropInfo;
begin
  for Loop := Low(Comps) to High(Comps) do
  begin
    { Get info record for Enabled property }
    PropInfo := GetPropInfo(Comps[Loop].ClassInfo, 'Enabled');
    { If property exists, set value to False }
    if Assigned(PropInfo) then
      SetOrdProp(Comps[Loop], PropInfo, Longint(False));
  end;
end;

The projects Generic.Dpr and Genric2.Dpr that accompany this paper shows this same idea in a couple of ways, one more generic than the other.

Example 2 - DataSource and DataField properties

The same sort of problem arises when trying to set up data-aware controls to refer to a particular datasource component. All data-aware controls have a DataSource property, and most of them have a DataField property. However each control adds these properties individually - none of them come from a common ancestor. We can take the same approach as above to set these properties en masse. This routine deals with the DataSource property.

uses
  TypInfo;
...
procedure SetDataSource(Comps: array of TControl; DS: TDataSource);
var
  Loop: Integer;
  PropInfo: PPropInfo;
begin
  for Loop := Low(Comps) to High(Comps) do
  begin
    { Get info record for DataSource property }
    PropInfo := GetPropInfo(Comps[Loop].ClassInfo, 'DataSource');
    { If property exists, set value to DS }
    if Assigned(PropInfo) then
      SetOrdProp(Comps[Loop], PropInfo, Longint(DS));
  end;
end;
...
Table1.Open;
SetDataSource([DBEdit1, DBText1, DBNavigator1], DataSource1);

Converting between enumerated type values and strings

Enumerated types are programmatic devices to make programs easier to read and write by substituting recognisable terms for numeric values (which enumerated types are implemented as). It is normally the case that at compile time, all information about these textual terms is removed from the executable, but RTTI can change that.

Given a type:

TDayOfWeek = (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);

It is possible to start with a string ‘Tuesday’ and get a TDayOfWeek value Tuesday out. It is also possible to do the reverse. The two routines GetEnumName and GetEnumValue do this and to work with them requires a typecast between any enumerated value and an Integer.

function GetEnumName(TypeInfo: PTypeInfo; Value: Integer): string;
function GetEnumValue(TypeInfo: PTypeInfo; const Name: string): Integer;

Given the enumerated type TFormBorderStyle, used for a form’s BorderStyle property and defined as:

TFormBorderStyle = (bsNone, bsSingle, bsSizeable, bsDialog);

then we can say:

uses
  TypInfo;
...
{ Write current border style on form caption }
Caption := GetEnumName(TypeInfo(TFormBorderStyle),
Ord(BorderStyle)){$ifdef Windows}^{$endif};
...
{ Change current border style to single-line }
BorderStyle := TFormBorderStyle(
GetEnumValue(TypeInfo(TFormBorderStyle), 'bsSingle'));

Notice that in Delphi 2 and 3 you do not use the ^ symbol in conjunction with GetEnumName.

You can find code like this in the projects EnumEg.Dpr and EnumEg2.Dpr that accompany this paper.

Setting a component’s default properties

When writing components you are able to specify default property values in the class declaration. This does not actually set the properties of any component instances to those defaults - that must still be done in the component constructor. Instead, it simply sets up one of the fields in the component’s RTTI block. The Delphi environment uses this information to reduce the volume of information that must go in the .DFM form file. Only properties whose values differ from any specified default go in the file.

As mentioned just above, it is still down to the developer to manually set the properties to their default values in the constructor. This can be a bit of a chore to do, particularly if there are several of them. To simplify the task we can use the RTTI information, looping through all the published properties that might have a default value and apply it. The code shown here (and available to test out in the PropEg.Pas component unit and RttiUnit.Pas utility unit) is based on code from Secrets Of Delphi 2 by Ray Lischner, published by The Waite Group.

...
constructor TDefaultPropertyExample.Create(AOwner: TComponent);
begin
  inherited;
  { This is the traditional way of setting }
  { default property values - assign the }
  { value required to each of the underlying }
  { data fields one at a time, e.g. }
  { FProperty1 := 10; }
  { FProperty1 := 20; }
  { FProperty1 := 30; }
 
  { But here is a neater way as shown in Secrets of Delphi 2 }
  { use the RTTI to do it all automatically. The code }
  { is then the same for each component constructor }
  SetDefaults(Self)
end;
 
const
  NoDefault = $80000000;
  tkPropsWithDefault = [tkInteger, tkChar, tkSet, tkEnumeration];
 
procedure SetDefaults(Obj: TObject);
var
  PropInfos: PPropList;
  Count, Loop: Integer;
begin
  { Find out how many properties we'll be considering }
  Count := GetPropList(Obj.ClassInfo, tkPropsWithDefault, nil);
  { Allocate memory to hold their RTTI data }
  GetMem(PropInfos, Count * SizeOf(PPropInfo));
  try
    { Get hold of the property list in our new buffer }
    GetPropList(Obj.ClassInfo, tkPropsWithDefault, PropInfos);
    { Loop through all the selected properties }
    for Loop := 0 to Count - 1 do
      with PropInfos^[Loop]^ do
        { If there is supposed to be a default value... }
        if Default <> NoDefault then
          { ...then jolly well set it }
          SetOrdProp(Obj, PropInfos^[Loop], Default)
  finally
    FreeMem(PropInfos, Count * SizeOf(PPropInfo));
  end;
end;

Copying properties from one component to another

Another possible use for RTTI is for helping to copy one object to another. Delphi doesn’t provide much in the way of support for copying objects apart from the Assign method, which must be coded specifically for each class in question. If you want to copy the values of common properties from one component to another then RTTI can help. Additionally, if you make up new classes, publishing properties that represent the important data will facilitate easy object copying if it ever becomes necessary.

The RTTIUnit.pas unit that accompanies this paper has a routine called CopyObject defined in it. CopyObject takes two object references - a source object and a target object. It iterates through all the published properties in the source object and attempts to set the value of the same property in the target. If the property doesn’t exist in the target then it skips onto the next one.

Given a bitmap button, a normal button and an edit control on a form, you could use the following statements to ensure that all common properties of these objects are exact copies of each other using the following statements. Of course, given that the component co-ordinates are properties themselves, then the last two statements ensures that they don’t all end up sitting on top of each other.

CopyObject(BitBtn1, Button1);
CopyObject(BitBtn1, Edit1);
Button1.Left := BitBtn1.Left + BitBtn1.Width + 2;
Edit1.Left := Button1.Left + Button1.Width + 2

The code is a bit too lengthy to list here but isn’t really that far removed from the default property setting code above. Refer to the unit for the source. Also, the sample project CopyEg.Dpr supplied with this paper shows an example of the function in use.

Summary

This paper has discussed the subject of Run-Time Type Information. After looking at why it exists we then looked at how we can access it and a number of applications of how it can be used. You can use it in many different ways to those suggested here, and hopefully you have been given a taster of how this remarkably under-used feature of Delphi can be employed.

About Brian Long

Brian Long used to work at Borland UK, performing a number of duties including Technical Support on all the programming tools. Since leaving in 1995, Brian has spent the intervening years as a trainer, trouble-shooter and mentor focusing on the use of the C#, Delphi and C++ languages, and of the Win32 and .NET platforms. In his spare time Brian actively researches and employs strategies for the convenient identification, isolation and removal of malware. If you need training in these areas or need solutions to problems you have with them, please get in touch or visit Brian's Web site.

Brian authored a Borland Pascal problem-solving book in 1994 and occasionally acts as a Technical Editor for Wiley (previously Sybex); he was the Technical Editor for Mastering Delphi 7 and Mastering Delphi 2005 and also contributed a chapter to Delphi for .NET Developer Guide. Brian is a regular columnist in The Delphi Magazine and has had numerous articles published in Developer's Review, Computing, Delphi Developer's Journal and EXE Magazine. He was nominated for the Spirit of Delphi award in 2000.