Introduction aux RTTI sous DelphiDate de publication : 13/03/2006 , Date de mise à jour : 15/08/2006
Par
Laurent Dardenne (Contributions)
RTTI est l'acronyme de RunTime Type Information.
Delphi met en oeuvre ce mécanisme pour retrouver dynamiquement des informations sur les données d'une instance mais surtout sur le type de ces membres.
On utilise aussi le terme d'introspection ou de réflexion pour définir ce mécanisme d'interrogation sur des instances de classes.
Je tiens à remercier Olivier Lance( Bestiol) pour ses corrections orthographiques et relectures attentives.
1. Public concerné
1-1. Les sources
2. RTTI, qu'est-ce que c'est et à quoi ça sert ?
3. Comment ça marche ?
4. Membres publiés
5. Adaptation de la classe pour la publication
6. Afficher les détails d'une propriété
6-1. Propriété de type méthode
7. Manipuler la valeur d'une propriété
7-1. Propriété de type objet
7-2. Accèder aux méthodes des Getter et Setter
9. Quelques opérations particulières
8-1. Modifier une propriété commune à des contrôles
8-2. Supprimer un objet et ces propriétés de type objet
8-3. Cloner les propriétés d'objet de même classe
8-4. Dupliquer un objet
8-5. Opérations sur le type ensemble et énumération
8-6. Interfaces invocable
10. Liens
1. Public concerné
Testé sous Delphi 2005 update3.
Version 1.0
1-1. Les sources
2. RTTI, qu'est-ce que c'est et à quoi ça sert ?
L'objectif premier des informations de types à l'exécution (alias RTTI) est de permettre à l'IDE de reconnaître et de manipuler des classes, plus précisement des composants, qui ont été définis bien après la commercialisation de Delphi.
Le mode conception de fiche ne serait pas possible sans le mécanisme de RTTI qui est également lié à la sérialisation, c'est à dire la persistance sur disque des fiches (forms) conçues et contenant des composants (fichiers .DFM). Je n'aborderai pas ici la sérialisation, le sujet nécessitant à lui seul un tutoriel.
RTTI est donc la clef de voûte entre les composants Delphi et leur incorporation et manipulation dans L'EDI. Il permet de retrouver des informations sur le type d'une instance de classe lors de l'exécution d'un programme, ce type pouvant être inconnu lors de la compilation.
Le premier niveau d'information que l'on peut récupérer passe par les méthodes de la classe ancêtre primaire : TObject.
On peut retrouver par exemple le nom de la classe via la méthode ClassName ou encore le type de la classe via la méthode ClassType. Cette dernière renvoyant une référence de classe, et pas un pointeur, de l'instance de classe intérrogée ce qui permettrait par exemple de créer une seconde instance de même classe sans pour autant connaître le nom de la classe lors de la compilation.
Il existe également la méthode TObject.InheritsFrom qui permet de déterminer, au travers des opérateurs IS et AS, si une instance est d'une classe particulière ou non. Je vous renvoie à l'aide en ligne pour l'usage de ces deux opérateurs.
En revanche le second niveau d'informations plus détaillées est accessible uniquement via des traitements de bas niveau qui ne sont pas implémentés dans la classe TObject.
Pour accéder à ces informations générées par le compilateur, l'appel de la méthode TObject.ClassInfo renvoie un pointeur sur un enregistrement de type TTypeInfo.
class function TObject.ClassInfo: Pointer;
begin
Result := PPointer(Integer(Self) + vmtTypeInfo)^;
end; |
On s'aperçoit que ces autres informations sont liées à la VMT ou Virtual Methods Table (Table des Méthodes Virtuelles).
Aux offsets positifs, une VMT se compose d'une liste de pointeurs de méthodes, un par méthode virtuelle définie par l'utilisateur, dans l'ordre de déclaration au sein de la classe.
Aux offsets négatifs, une VMT contient un certain nombre de champs utilisés en interne. Par exemple, la valeur de la constante vmtTypeInfo est -60.
L'inconvénient majeur est que ces informations RTTI sont peu documentées par Borland, les types et procédures permettant de les manipuler se trouvent dans l'unité TypInfo.pas.
Avant d'aller plus loin, il faut savoir que l'usage de RTTI au sein d'un programme apporte un temp de traitement supplémentaire lors de son exécution.
3. Comment ça marche ?
Projet : ..\RTTI\RTTI1
Déclarons une classe simple :
type
TMonObjet=Class(TObject)
ChampUn : Integer;
ChampDeux : String;
Function SiExiste:Boolean;
Procedure Traite(S:String);
end; |
On peut d'ores et déjà appeler les méthodes renvoyant des informations de type de premier niveau sur l'instance créée :
Var UnObjet : TMonObjet;
begin
UnObjet:=TMonObjet.Create;
UnObjet.ChampUn:=3;
Writeln('UnObjet est de la classe : ',UnObjet.ClassName);
If UnObjet.InheritsFrom(TComponent) then
Writeln('UnObjet hérite de TComponent.');
Writeln('Le parent de UnObjet est de la classe : ',UnObjet.ClassParent.ClassName);
Writeln('Taille de UnObjet : ',UnObjet.InstanceSize);
Readln;
UnObjet.Free;
end. |
En revanche pour retrouver des informations de type de second niveau sur l'instance créée, on doit utiliser un peu plus de code.
Ajoutons une variable pour récupérer les informations de type :
Var UnObjet : TMonObjet;
InfoRTTI : Pointer; |
Puis en fin de programme l'appel de la méthode ClassInfo :
InfoRTTI:=UnObjet.ClassInfo;
If Not Assigned(InfoRTTI)
then Writeln('UnObjet n''a pas d''information de type.')
else AfficheInfoRTTI(InfoRTTI);
Readln;
UnObjet.Free;
end. |
Nous verrons plus avant le détail de la méthode AfficheInfoRTTI.
Le code précédent nous affichera que la variable UnObjet n'a pas d'information de type. Ceci est dû au fait que le code compilé n'en contient tout simplement pas ! La directive de compilation {$M}, de portée locale, doit être activée par {$M+} pour que la génération de ces informations soit effective.
Je vous laisse consulter l'aide en ligne de Delphi concernant cette directive au lieu de la recopier ici dans son intégralité. Sachez qu'une classe descendant d'une classe compilée avec cette directive bénéficie des informations RTTI, dans ce cas la directive {$M} devient inutile (voir par exemple la classe TPersistent).
Ajoutons cette directive en début de programme et recompilons.
Malheureusement nous obtenons l'erreur suivante :
E2217: Le champ publié 'ChampUn' n'est pas un type classe ou interface. |
L'aide en ligne nous indique qu'une tentative a été effectuée pour publier un champ dans une classe qui n'est pas une classe ni un type interface.
Sans précision dans la déclaration de la classe, les membres sont public par défaut. Ajoutons la directive private pour compiler correctement ce programme :
TMonObjet=Class(TObject)
private
ChampUn : Integer; |
Recompilons et exécutons de nouveau ce programme.
Cette fois-ci le résultat est différent :
Nom du type d'information : TMonObjet
Type de donnée : tkClass
Définie dans le fichier : RTTI11.pas
Nombre de propriétés : 0 |
On retrouve le nom de la classe de notre instance, son type, le fichier dans laquelle elle est déclarée et le nombre de propriétés qu'elle possède.
Revenons au code de la procédure d'affichage :
procedure AfficheInfoRTTI(Informations : PTypeInfo); |
Voici la déclaration de l'enregistrement PTypeInfo :
PPTypeInfo = ^PTypeInfo;
PTypeInfo = ^TTypeInfo;
TTypeInfo = record
Kind: TTypeKind;
Name: ShortString;
end; |
On voit que l'on pourra être amené à manipuler des pointeurs de pointeur de TTypeInfo et que le champ Kind renvoie vers l'énumération suivante:
TTypeKind = (tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat,
tkString, tkSet, tkClass, tkMethod, tkWChar, tkLString, tkWString,
tkVariant, tkArray, tkRecord, tkInterface, tkInt64, tkDynArray); |
L'énumération TTypeKind contient les types de données accessibles via les informations RTTI.
Continuons avec le code de la procédure d'affichage :
procedure AfficheInfoRTTI(Informations : PTypeInfo);
var DonneeDeType : PTypeData;
NomEnumeration : String;
begin
Writeln('Nom du type d''information : ',Informations.Name);
DonneeDeType := GetTypeData(Informations);
NomEnumeration := GetEnumName(TypeInfo(TTypeKind), Integer(Informations.Kind));
Writeln('Type de donnee : ', NomEnumeration);
Writeln('Definie dans le fichier : ', DonneeDeType.UnitName,'.pas');
Writeln('Nombre de proprietes : ',DonneeDeType.PropCount);
end; |
La fonction GetTypeData renvoie un pointeur vers un enregistrement de type PTypeData qui est un record avec une partie variable (voir ce tutoriel sur le langage Pascal):
PTypeData = ^TTypeData;
TTypeData = packed record
case TTypeKind of
tkUnknown, tkLString, tkWString, tkVariant: ();
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: PPTypeInfo;
NameList: ShortStringBase;
EnumUnitName: ShortStringBase));
tkSet: (
CompType: PPTypeInfo));
tkFloat: (
FloatType: TFloatType);
tkString: (
MaxLength: Byte);
tkClass: (
ClassType: TClass;
ParentInfo: PPTypeInfo;
PropCount: SmallInt;
UnitName: ShortStringBase;
);
tkMethod: (
MethodKind: TMethodKind;
ParamCount: Byte;
ParamList: array[0..1023] of Char
);
tkInterface: (
IntfParent : PPTypeInfo;
IntfFlags : TIntfFlagsBase;
Guid : TGUID;
IntfUnit : ShortStringBase;
);
tkInt64: (
MinInt64Value, MaxInt64Value: Int64);
tkDynArray: (
elSize: Longint;
elType: PPTypeInfo;
varType: Integer;
elType2: PPTypeInfo;
DynUnitName: ShortStringBase);
end; |
Cet enregistrement utilise de nombreuses énumérations qui permettent de détailler le type de donnée interrogé :
TOrdType = (otSByte, otUByte, otSWord, otUWord, otSLong, otULong);
TFloatType = (ftSingle, ftDouble, ftExtended, ftComp, ftCurr);
TMethodKind = (mkProcedure, mkFunction, mkConstructor, mkDestructor,
mkClassProcedure, mkClassFunction, mkClassConstructor, mkOperatorOverload,
mkSafeProcedure, mkSafeFunction); |
Vous trouverez le projet modifié dans
Projet : ..\RTTI\RTTI11
La fonction GetEnumName renvoie une chaîne de caractères contenant le nom du type porté par le champ Kind. On doit transtyper l'énumération TTypeKind en TypeInfo pour pouvoir l'interroger. Pour la classe de notre exemple nous récupérons la valeur tkClass.
Les autres procédures de l'unité TypInfo.pas sont dédiées à la manipulation des interfaces, des méthodes et surtout des propriétés.
 |
On a pu voir que pour une classe on obtient les informations de RTTI via l'appel de ClassInfo, pour les autres types on utilisera la fonction TypeInfo.
Elle ne supporte pas les types pointeur (par exemple PChar, PWideChar, PString ou indicateur) et les types définis localement dans un méthode.
L'information de type existera pour un type donné si un appel à TypeInfo est fait pour lui, ou si le type est mentionné dans la section published d'une classe qui est référencéé dans le code du programme.
La seule différence réside dans le fait que l'appel est effectué à la compilation et que l'on doit lui passer un nom de type et non pas un nom d'instance.
Pour une classe l'appel de GetTypeData(TMonObjet.ClassInfo) est identique à GetTypeData(TypeInfo(TMonObjet)).
|
Essayons maintenant de récupérer des informations sur la méthode Traite de notre classe :
Var UnObjet : TMonObjet;
InfoRTTI : Pointer;
M : TMethod;
begin
...
M:=GetMethodProp(UnObjet,'Traite');
readln;
UnObjet.Free;
end. |
Ce code déclenche une exception EPropertyError : 'La propriété traite n'existe pas'.
On pourrait croire que le simple fait que notre classe hérite de TPersistent permette d'accéder aux RTTI mais il n'en est rien.
La méthode TObject.ClassInfo renvoie des informations sur le type des propriétés publiées ce qui fait que notre classe telle qu'elle est construite ne permet pas de récupérer ce type d'informations.
On doit donc modifier la déclaration de la classe afin de publier des propriétés.
4. Membres publiés
Texte issu de l'aide en ligne :
Les membres publiés ont la même visibilité que les membres publics. La différence est que des informations de type à l'exécution (RTTI) sont générées pour les membres publiés. Ces informations permettent à une application d'interroger dynamiquement les champs et les propriétés d'un objet et de localiser ses méthodes. Les RTTI sont utilisées pour accéder aux valeurs des propriétés lors de la lecture ou de l'enregistrement des fichiers fiche, pour afficher les propriétés dans l'inspecteur de propriétés et pour associer des méthodes spécifiques (appelées gestionnaires d'événements) à des propriétés spécifiques (appelées événements).
La directive Published n'est pas lié au concept d'héritage mais permet la redéfinition de propriété en passant sa visibilité de protégée à publiée.
La génération RTTI s'applique seulement pour :
- Les classes qui descendent de TPersistent.
- Les classes compilées avec la directive {$M+}.
- Les interfaces qui descendent d'IInvokeable.
Note :
Consultez les nombreuses informations disponibles dans l'aide en ligne traitant des membres publiés et des propriétés.
Sachez qu'il existe la méthode TPersistent.DefineProperties qui permet de sauvegarder les propriétés non publiées d'une classe lors d'une opération de sérialisation, mais dans ce cas c'est au développeur de gérer cette opération.
 | Texte issu de l'aide en ligne :
|
Vous ne pouvez pas transmettre des informations de type à l'exécution (RTTI) Delphi entre DLL ou d'une DLL à un exécutable. Si vous transmettez un objet d'une DLL vers une autre ou vers un exécutable, vous ne pouvez pas utiliser les opérateurs is ou as avec l'objet transmis. En effet, les opérateurs is et as doivent comparer les informations RTTI. Si vous devez transmettre des objets à partir d'une bibliothèque, utilisez plutôt des packages, car ceux-ci peuvent partager les informations RTTI. De même, vous devez utiliser des packages au lieu de DLL dans les services Web car ils reposent sur des informations RTTI Delphi.
5. Adaptation de la classe pour la publication
Projet : ..\RTTI\RTTI12
La publication ne peut se faire que sur des membres, déclarées avec le mot-clé property et placées dans la section published d'une déclaration de classe.
La publication de méthodes d'une classe nécessite de définir un type pointeur de méthode se terminant par le mot-clé Of Object.
type
TTraitement = Procedure (S:String) of Object; |
Appliquons ensuite les conventions de nommage Borland qui sont pour
- les champs, de préfixer le nom du champ par F pour field,
- les événements, de préfixer le nom de la propriété par On.
TMonObjet=Class(TObject)
private
FChampUn : Integer;
FChampDeux : String;
FOnTraite :TTraitement;
public
Function SiExiste:Boolean;
Property Champ1:Integer read FChampUn Write FChampUn;
published
Property Champ2:String read FChampDeux Write FChampDeux;
Property OnTraitement:TTraitement read FOnTraite Write FOnTraite;
end; |
Ajoutons le code pour visualiser le nom des propriétés de notre classe :
procedure AffichePropriete;
var
Nombre, I: Integer;
ListeProprietes: TPropList;
begin
Writeln;
Nombre := GetPropList(TMonObjet.ClassInfo, tkAny, @ListeProprietes);
for I := 0 to Nombre-1 do
Writeln('Propriete ',I+1,' = ',ListeProprietes[I]^.Name);
end; |
On utilise ici une liste d'enregistrements de type TPropInfo qui permet de retrouver certaines informations sur une propriété :
PPropInfo = ^TPropInfo;
TPropInfo = packed record
PropType: PPTypeInfo;
GetProc: Pointer;
SetProc: Pointer;
StoredProc: Pointer;
Index: Integer;
Default: Longint;
NameIndex: SmallInt;
Name: ShortString;
end; |
Une des fonctions GetPropList surchargée renvoie le nombre de propriétés trouvées :
- Le premier paramètre attendu est l'enregistrement contenant les informations de type de la classe,
- le second paramètre est le type de donnée recherché,
- le troisième paramètre est la liste de record hébergeant le résultat de la recherche,
- et enfin le quatrième paramètre permet de retrouver les propriétés triées ou non.
function GetPropList(TypeInfo: PTypeInfo; TypeKinds: TTypeKinds;
PropList: PPropList; SortList: Boolean): Integer; |
Il est possible d'utiliser la procédure GetPropInfos mais dans ce cas il faut récupérer le nombre de propriétes par l'appel de :
GetTypeData(TMonObjet.ClassInfo).PropCount |
L'exécution du programme ainsi modifié nous permet bien de récupérer des informations supplémentaires :
UnObjet est de la classe : TMonObjet
Le parent de UnObjet est de la classe : TObject
Taille de UnObjet : 24
Nom du type d'information : TMonObjet
Type de donnee : tkClass
Definie dans le fichier : RTTI12.pas
Nombre de proprietes : 2
Propriete 1 = Champ2
Propriete 2 = OnTraitement |
Le nombre de propriétés indiquées est 2 et non pas 3 car nous n'avons publié que deux propriétés. Celles déclarées hors de la section published ne sont donc pas accessibles. On peut savoir savoir si une propriété est publiée par l'appel suivant :
If Not IsPublishedProp(UnObjet,'Champ1')
then Writeln('La propriété Champ1 n''est pas publiée'); |
Remarquez aussi que l'appel de la méthode GetMethodProp(UnObjet,'OnTraitement'), adaptée aux nouvelles déclarations, ne provoque plus d'exception. Le contenu de la variable M est égal à Nil étant donné que la propriété OnTraitement n'est pas renseignée.
6. Afficher les détails d'une propriété
Projet : ..\RTTI\RTTI13
Une fois obtenus la liste d'enregistrements de type TPropInfo, il est possible d'afficher le détail d'une propriété publiée.
Créons une méthode dédiée à l'affichage à laquelle nous passerons un élément du tableau ListeProprietes, affichons dans un premier temps les champs d'enregistrements du type TPropInfo. Nous aborderons plus tard le traitement du champ TPropInfo.PropType, traitement qui sera bien évidement différent selon le type de donnée de la propriété.
procedure AfficheDetails(Informations : TPropInfo);
var
TypePropriete : String;
DonneePropriete: PTypeData;
begin
With Informations do
begin
Writeln('Propriete de Type ',PropType^.Name);
if not Assigned(GetProc)
then Writeln('Nil')
else Writeln(Format('Getter : %p', [GetProc]));
if not Assigned(SetProc)
then Writeln('Nil')
else Writeln(Format('Setter : %p', [SetProc]));
if not Assigned(StoredProc)
then Writeln('Nil')
else Writeln(Format('StoredProc : %p', [StoredProc]));
Writeln(Format('Index : %x', [Index]));
Writeln(Format('Default : %x', [Default]));
Writeln('NameIndex :',NameIndex);
case PropType^.Kind of
tkInteger : writeln('Pas de donnees supplementaires');
tkLString : writeln('Pas de donnees supplementaires');
tkMethod : writeln('Type methode');
end;
end;
end; |
L'appel de cette méthode se faisant dans la boucle utilisée précédement :
for I := 0 to Nombre-1 do
begin
Writeln('Propriete ',I+1,' = ',ListeProprietes[I]^.Name);
AfficheDetails(ListeProprietes[I]^);
Writeln;
end; |
Voici l'affichage des informations détaillée :
Propriété 1 = Champ2
Propriété de Type string
Getter : FF000008
Setter : FF000008
StoredProc : 00000001
Index : 80000000
Default : 80000000
NameIndex :0
Pas de données supplémentaires
Propriété 2 = OnTraitement
Propriété de Type TTraitement
Getter : FF000010
Setter : FF000010
StoredProc : 00000001
Index : 80000000
Default : 80000000
NameIndex :1
Type méthode |
Pour les champs Getter et Setter on pouvait s'attendre à ce qu'ils contiennent Nil mais la valeur affichée contient 2 informations, propre à la déclaration de notre classe, qui sont :
- le mode d'accès pour Read et Write, ici il s'agit d'un accès direct sur le champ mais cela pourrait être un accés in direct via une méthode
- et l'index du champ adressé FOnTraite.
Une traduction automatique de cet article russe associée à cet exemple trouvé sur CodeCentral m'a permis de faire ces déductions :
- si le premier octet est égal à $FF il s'agit d'un accès direct au champ (les derniers octets indique l'offset du champ),
- si le premier octet est égal à $FE il s'agit d'un accès par une méthode virtuelle, ici les derniers octets indique l'index dans la VMT et non son adresse réelle),
- si le premier octet est inférieur à $FE il s'agit de l'adresse d'une méthode statique. Dans ce cas la valeur peut être différente selon les compilations,
- si la valeur est égale à $80000000 la méthode est indéterminée, il s'agit d'une propriété en Read Only par exemple,
- Toute autre valeur indique une méthode statique. Bien qu'il reste une autre possibilité que je n'ai pas encore éclaircie :
if (Integer(Value) and $FFFFFF00) = 0 then
Result := ptConstant |
Pour le champ StoredProc la valeur 1 indique que le champ est stocké et la valeur 0 qu'il ne l'est pas.
Rappel : Si la propriété n'a pas de directive stored, elle est traitée comme si stored True était spécifié.
Pour les champs Index et Default, la valeur $80000000 semble indiquer que c'est la valeur par défaut ou que l'information de ces directives n'est pas précisée.
 |
Les directives facultatives stored, default, et nodefault sont appelées des spécificateurs de stockage. Elles n'ont aucun effet sur le comportement du programme, mais commandent la manière dont Delphi maintient les informations RTTI. Plus particulièrement, les spécificateurs de stockage déterminent si Delphi sauve les valeurs des propriétés éditées dans les fichiers .DFM .
|
// Code issu de http://codecentral.borland.com/Item.aspx?id=18460
function GetProcType(Value: Pointer): TStoredType;
begin
if (Integer(Value) and $FFFFFF00) = 0 then
Result := ptConstant
else if (Integer(Value) and $FF000000) = $FE000000 then
Result := ptVirtualMethod
else if (Integer(Value) and $FF000000) = $FF000000 then
Result := ptField
else
Result := ptStaticMethod
end;
function GetProcValue(Value: Pointer): Integer;
begin
case GetProcType(Value) of
ptVirtualMethod, ptField:
Result := Integer(Value) and $00FFFFFF;
else
Result := Integer(Value);
end;
end;
procedure WritePropertyProc(const Str: string; Proc: Pointer);
const
BooleanStrings: array[0..1] of string = ('False', 'True');
begin
case GetProcType(Proc) of
ptConstant : Write(Format(' %s %s', [Str, BooleanStrings[GetProcValue(Proc)]]));
ptField : Write(Format(' %s (Champ %d)', [Str, GetProcValue(Proc)]));
ptVirtualMethod : Write(Format(' %s (methode virtuelle %d)', [Str, GetProcValue(Proc)]));
ptStaticMethod : Write(Format(' %s (methode statique %p)', [Str, Proc]));
end;
end;
procedure AfficheDetails2(Informations : TPropInfo);
begin
With Informations do
begin
Write(Format('Propriete %s : %s', [Name,PropType^.Name]));
if Index <> Low(Integer)
then Write(Format(' index %d', [Index]));
if GetProc <> nil
then WritePropertyProc('read', GetProc);
if SetProc <> nil
then WritePropertyProc('write', SetProc);
if Default = Low(Integer)
then Write(' nodefault')
else Write(Format(' default %d', [Default]));
if StoredProc = nil then
Write(' stored False')
else
WritePropertyProc('stored', StoredProc);
Writeln(Format('; / / index %d', [NameIndex]));
end;
end; |
6-1. Propriété de type méthode
Projet : ..\RTTI\RTTI131
Toujours dans la même procédure d'affichage, abordons maintenant le traitement du champ TPropInfo.PropType.
Le champ PropType^.Kind permet de retrouver le type de donnée de la propriété, pour une méthode son contenu est égale à tkMethod :
case PropType^.Kind of
tkInteger : writeln('Pas de données supplémentaires');
tkLString : writeln('Pas de données supplémentaires');
tkMethod : Begin
writeln('Type methode');
DonneePropriete:= GetTypeData(PTypeInfo(PropType^));
Case DonneePropriete.MethodKind of
mkProcedure: TypePropriete := 'procedure';
mkFunction: TypePropriete := 'function';
mkConstructor: TypePropriete := 'constructor';
mkDestructor: TypePropriete := 'destructor';
mkClassProcedure: TypePropriete := 'class procedure';
mkClassFunction: TypePropriete := 'class function';
end; |
Si on souhaite afficher les informations du type de la propriété à partir du champ PropType on doit récupérer un pointeur sur enregistrement de type TTypeData qui porte ces informations supplémentaires :
TTypeData = packed record
case TTypeKind of
...
tkMethod: (
MethodKind: TMethodKind;
ParamCount: Byte;
ParamList: array[0..1023] of Char
);
tkInterface: ... |
Pour ce faire on effectue donc l'appel suivant :
DonneePropriete:= GetTypeData(PTypeInfo(PropType^)); |
voyons le détail de cet appel :
- le champ TPropInfo.PropType est du type PPTypeInfo,
- la méthode GetTypeData(TypeInfo: PTypeInfo) renvoie un pointeur de type PTypeData,
- pointeur qui permet de retrouver les informations de la méthode, notamment le champ MethodKind.
Affichons ensuite les informations recherchées :
Writeln('Nombre de paramètres ',DonneePropriete^.ParamCount);
Writeln('Liste des paramètres ',DonneePropriete^.ParamList); |
Malheureusement ici l'affichage du champ ParamList n'est pas correcte. Pour traiter correctement les informations contenues dans ce tableau on doit redéclarer, dans notre programme, les définitions mises en commentaire dans la déclaration du type TTypeData:
PShortString=^ShortString;
PParametre= ^Parametre;
Parametre=Record
Flags: TParamFlags;
ParamName: ShortString;
TypeName: ShortString;
end;
ParametresDeMethode=Record
Tableau : Array[1..20] of Parametre;
ResultType: ShortString;
end; |
La procédure SetArrayParameter, du projet RTTI131, permet de construire un tableau de type ParametresDeMethode à partir du champ TTypeData.ParamList de type array[0..1023] of Char. Elle utilise l'arithmétique de pointeur pour récupérer les différentes informations.
Le champ Flags contient des informations sur le type du passage de paramètre tel que :
TParamFlag = (pfVar, pfConst, pfArray, pfAddress, pfReference, pfOut); |
Le champ ParamName contient le nom du Paramètre.
Le champ TypeName contient le nom du type du paramètre.
Pour terminer, l'appel suivant reconstruit, à l'aide du tableau créé précédemment, la déclaration de type méthode de la propriété en cours de traitement :
Writeln(TypePropriete+' '+BuildMethodDefinition(ListeParametres,DonneePropriete^.ParamCount)); |
c'est à dire :
procedure (var S:String) of Object; |
Revenons à la méthode GetMethodProp, de type fonction, que nous avons vu précédemment. Elle renvoie un pointeur de méthode qui nous permet d'exécuter le code associé à la propriété en cours d'analyse.
Method := GetMethodProp(UnObjet, Informations.Name);
if Method.Code <> NIL then
begin
Resultat:='';
TTraitement(Method)(Resultat);
Writeln(Resultat);
end; |
Vous devez bien évidemment associé une méthode à votre propriété soit par l'éditeur de propriété de l'EDI soit par le code suivant :
UnObjet.OnTraitement:=UnObjet.Traite1; |
7. Manipuler la valeur d'une propriété
La lecture de la valeur d'une propriéte via RTTI se fait via les méthodes GetXXXProp, où XXX représente le type de donnée de la propriété.
Nous venons de voir, au travers de la méthode GetMethodProp comment lire la valeur d'une propriété.
function GetMethodProp(Instance: TObject; const PropName: string): TMethod; overload;
function GetMethodProp(Instance: TObject; PropInfo: PPropInfo): TMethod; overload; |
La première permet la lecture d'un propriété en indiquant son nom et appel en interne la seconde procédure surchargée en lui passant un pointeur sur un enregistrement de type TPropInfo:
Result := GetMethodProp(Instance, FindPropInfo(Instance, PropName)); |
L'assignation d'une valeur à une propriéte via RTTI se fait via les méthodes SetXXXProp.
Certaines de ces méthodes regroupent la manipulation de plusieurs type, par exemple SetOrdProp autorise la manipulation des types ordinaux suivants : tkInteger, tkChar, tkEnumeration et tkSet. Elle renvoie la valeur de la propriété dans un LongInt.
A noter que dans le code source de l'unité Typinfo.pas, Borland a couplé les déclarations Getxxx et Setxxx.
...
function GetDynArrayProp(Instance: TObject; PropInfo: PPropInfo): Pointer; overload;
procedure SetDynArrayProp(Instance: TObject; PropInfo: PPropInfo;
const Value: Pointer); overload;
... |
 |
On ne retrouvent pas toutes ces procédures dans les versions inférieures à Delphi 5. Et chaque nouvelle version peut ajouter des traitements supplémentaires sur des types particuliers.
|
7-1. Propriété de type objet
La méthode GetObjectProp permet de récupérer une propriéte de type objet, le résultat devant être transtypé dans la classe attendue :
var Fonte: TFont;
begin
...
Fonte := TFont(GetObjectProp(MonControl, 'font', TFont));
Fonte.Name := 'Symbol';
...
end; |
7-2. Accèder aux méthodes des Getter et Setter
Projet : ..\RTTI\RTTI2
Pour illustrer cette opération utilisons la déclaration de classe suivante :
TMonObjet=Class(TObject)
private
FChampUn : Integer;
FChampDeux : String;
FChampTrois : String;
FChampQuatre: String;
FOnTraite :TTraitement;
Procedure Traite1(Var S:String);
function GetChampTrois: String;
procedure SetChampTrois(const Value: String);
function GetChampQuatre: String;Virtual;
procedure SetChampQuatre(const Value: String);Virtual;
public
Function SiExiste:Boolean;
Property Champ1:Integer read FChampUn Write FChampUn;
published
Property Champ2:String read FChampDeux Write FChampDeux;
Property Champ3:String read GetChampTrois Write SetChampTrois;
Property Champ4:String read GetChampQuatre Write SetChampQuatre;
Property OnTraitement:TTraitement read FOnTraite Write FOnTraite;
end; |
Comme nous l'avons précédemment le premier octet de l'adresse des méthodes accesseurs ( getproc et setproc) a une signification particuliére.
Pour ceux qui possédent une version de Delphi incluant les sources vous pouvez consulter le code la procédure TypInfo.SetFloatProp. Elle permet de récupèrer la valeur d'une propriétés de type
Double ou Extended ou Comp ou encore Currency. Ces procédures Getxxx manipulent l'adresse de l'accesseur Getproc.
Déclarons une procédure nous renvoyant le type d'accés du champs :
type
TTypeMethode = (tmStaticMethod, tmVirtualMethod, tmField, tmConstant);
TTypeMethodeDePropriete = tmStaticMethod..tmField;
...
function GetMethodeType(Value: Pointer): TTypeMethode;
begin
if (Integer(Value) and $FFFFFF00) = 0 then
Result := tmConstant
else if (Integer(Value) and $FF000000) = $FE000000 then
Result := tmVirtualMethod
else if (Integer(Value) and $FF000000) = $FF000000 then
Result := tmField
else
Result := tmStaticMethod
end; |
Déclarons ensuite un record afin de mémoriser les informations nécessaires aux traitements :
TTypeAccesseur=(taGetter,taSetter);
PTMethodeDePropriete=^TMethodeDePropriete;
TMethodeDePropriete =Record
Methode : TMethod;
TypeDePropriete : TTypeKind;
TypeDeMethod : TTypeMethodeDePropriete
AccesseurRTTI : Pointer;
TypeAccesseur : TTypeAccesseur;
end;
TAccesseurs=Record
Getter : TMethodeDePropriete;
Setter : TMethodeDePropriete;
end; |
Venons en à la procédure qui nous intéresse :
Procedure ConstruitLaMethodeDuGettterSetter(Instance: TObject; Var AAccesseur : TMethodeDePropriete);
begin
With AAccesseur do
begin
Methode.Data := Instance;
case TypeDeMethod of
tmField : begin
Writeln('Appel sur un champ');
if TypeDePropriete<>tkMethod then
Methode.Code := Pointer(Integer(Instance) + (Longint(AccesseurRTTI) and $00FFFFFF));
end;
tmVirtualMethod : begin
Writeln('Appel sur une methode virtuelle');
Methode.Code := Pointer(PInteger(PInteger(Instance)^ + SmallInt(AccesseurRTTI))^)
end;
tmStaticMethod : begin
Writeln('Appel sur une methode statique');
Methode.Code := Pointer(AccesseurRTTI);
end;
end;
end;
end; |
Elle ne fait que calculer un pointeur selon le type d'accés de l'accesseur de la propriété concernée.
- Pour un champ ce pointeur adresse directement le champ privé (pour accéder à la valeur dans ce cas on utilisera un pointeur typé et surtout pas le containeur TMethod),
- pour une méthode statique il pointe directement sur la méthode de l'instance et
- pour une méthode virtuelle il pointe sur la méthode dans la VMT ( le code SmallInt(AccesseurRTTI) renvoi l'index de la méthode).
On utilisera, pour les propriétés de type événement, la procédure GetMethoProp qui nous renvoi un objet TMethod.
Voici le code d'appel des TMethod récupérés :
type
TTraitement = Procedure (Var S:String) of Object;
TGetter=Function:String of Object;
TSetter=Procedure (Var S:String) of Object;
PStr=^String;
...
Procedure CallGettter(AAccesseur : TMethodeDePropriete);
Const S='Valeur renvoyée par l''accesseur : ';
var Resultat:String;
begin
Write('Resultat renvoye par l''accesseur=');
With AAccesseur do
begin
case TypeDeMethod of
tmField : begin
if TypeDePropriete=tkMethod then
if Methode.Code <> NIL then
begin
Resultat:='';
TTraitement(Methode)(Resultat);
Writeln(Resultat);
end
else
else Writeln(Pstr(Methode.Code)^);
end;
tmVirtualMethod : Writeln(S,TGetter(Methode)());
tmStaticMethod : Writeln(S,TGetter(Methode)());
end;
end;
end;
... |
Tout comptes fait cette appproche n'est pas en soi utile, autant utiliser les procédures dédiées Getxxx. En revanche la connaissance de la construction du pointeur d'un accesseur permet une meilleure compréhension de certaines section de code de la JVCL.
La recherche de Getproc dans les sources de la JVCL renvoie, entre autre, ce code issu de l'unité ...\jvcl\run\JvListbox.pas :
PI := GetPropInfo(TJvListBox, 'Items');
PStringsAddr := Pointer(Integer(PI.GetProc) and $00FFFFFF + Integer(Self));
Items.Free;
PStringsAddr^ := GetItemsClass.Create; |
Ce code modifie le champ FItem de la propriété CustomListBox.Items de type TListBoxStrings par un objet de type TJvListBoxStrings.
Sur le sujet vous pouvez consulter les articles de ce blog :
Un article concernant les accés en écriture sur des propriétés en lecture seule.
9. Quelques opérations particulières
8-1. Modifier une propriété commune à des contrôles
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]); |
Ce code fonctionne très bien, maintenant ajoutons des entrées de menus, qui possédent aussi une propriété Enabled.
DisableThem([Button1, Edit1, Checkbox1, Menu1]); |
Le problème est que les objets manipulés ne partagent pas un ancêtre commun. Un TMenuItem est dérivé de TComponent qui n'a pas de propriété Enabled. Les contrôles Button1, Edit1 et Checkbox1 héritent de TControl qui lui a bien une propriété Enabled.
Dans un premier temps amélioront le code en modifiant le type du tableau et en testant les 2 cas de figure :
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]);
End; |
Mais on voit que l'ajout de nombreuses classes implique de modifer à chque fois ce code. Voici la solution RTTI qui s'affranchie des nombreux tests sur les noms de classes :
procedure DisableThem(Comps: array of TComponent);
var Loop: Integer;
PropInfo: PPropInfo;
begin
for Loop := Low(Comps) to High(Comps) do
begin
PropInfo := GetPropInfo(Comps[Loop].ClassInfo, 'Enabled');
if Assigned(PropInfo)
then SetOrdProp(Comps[Loop], PropInfo, Longint(False));
end; |
On aborde le problème en testant la présence ou non d'une propriété au lieu de tester un nom de classe.
Un autre exemple sur le même principe. Ici on modifie la propriété DataSource d'un ensemble de composants :
// Issue du site www.Delphi.about.com
procedure ApplyDataSource(dbCtrls: array of TControl; DS: TDataSource) ;
var
cnt: Integer;
PropInfo: PPropInfo;
begin
for cnt := Low(dbCtrls) to High(dbCtrls) do
begin
PropInfo := GetPropInfo(dbCtrls[cnt].ClassInfo, 'DataSource') ;
if Assigned(PropInfo) then
SetOrdProp(dbCtrls[cnt], PropInfo, LongInt(DS)) ;
end;
end;
...
ApplyDataSource([DBNavigator1, DBText1, DBButton1], DataSource1) ; |
8-2. Supprimer un objet et ces propriétés de type objet
La procédure FreeAndNilProperties permet de supprimer et de libérer, sur n'importe quel objet possédant des RTTI, chacune de ses propriétés de type objet.
Notez que tous les objets de cet objet peuvent avoir des propriétés, dans ce cas la libération récursive n'est pas prise en compte.
8-3. Cloner les propriétés d'objet de même classe
// Exemple issu de Torry's Delphi Pages
uses
TypInfo;
function CloneProperty(SourceComp, TargetComp: TObject;
Properties: array of string): Boolean;
var
i: Integer;
begin
Result := True;
try
for i := Low(Properties) to High(Properties) do
begin
if not IsPublishedProp(SourceComp, Properties[I]) then Continue;
if not IsPublishedProp(TargetComp, Properties[I]) then Continue;
if PropType(SourceComp, Properties[I]) <> PropType(TargetComp, Properties[I]) then
Continue;
case PropType(SourceComp, Properties[i]) of
tkClass:
SetObjectProp(TargetComp, Properties[i],
GetObjectProp(SourceComp, Properties[i]));
tkMethod:
SetMethodProp(TargetComp, Properties[I], GetMethodProp(SourceComp,
Properties[I]));
else
SetPropValue(TargetComp, Properties[i], GetPropValue(SourceComp, Properties[i]));
end;
end;
except
Result := False;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
if CloneProperty(Button1, Button2, ['Left', 'Font', 'PopupMenu', 'OnClick']) then
ShowMessage('OK');
end; |
8-4. Dupliquer un objet
8-5. Opérations sur le type ensemble et énumération
Projet : ..\RTTI\Enum_Ensemble
Il est possible d'énumérer les valeurs d'un ensemble de la manière suivante :
program Enum_Ensemble;
{$APPTYPE CONSOLE}
uses
SysUtils,TypInfo;
type
Valeurs=(un,deux,trois,quatre,cinq);
EnsembleDeValeurs=set of Valeurs;
Const
cstNombreDeValeurs=5-1;
var
MonEnsemble : EnsembleDeValeurs;
InformationDeType : PTypeInfo;
UneValeur : Valeurs;
Nom :String;
Contenu : Integer;
procedure TestValeurEnsemble;
Var J : Integer;
begin
Writeln('Affiche les noms des elements de l''ensemble MonEnsemble');
Writeln;
For J:=0 to cstNombreDeValeurs do
begin
if Valeurs(J) in MonEnsemble
then Writeln('La valeur [',GetSetElementName(InformationDeType, J),'] est dans la variable MonEnsemble.');
end;
readln;
end;
begin
MonEnsemble:=[Un,trois,cinq];
InformationDeType:=TypeInfo(Valeurs);
TestValeurEnsemble;
Writeln('Affiche les nom et les valeurs de l''enumeration Valeurs');
Writeln;
For UneValeur:=Low(Valeurs) to High(Valeurs) do
begin
Nom:=GetSetElementName(InformationDeType, Ord(UneValeur));
Contenu:=GetSetElementValue(InformationDeType, Nom);
Writeln('La valeur enumere nommee ''',Nom,''' = ',Contenu);
end;
readln;
Writeln('Modifie les valeurs de l''enumeration Valeurs en manipulant des chaines de caracteres');
Writeln; |
On peut ajouter ou retirer des valeurs d'un ensemble en manipulant directement des chaînes de caractères :
Writeln('Retire l''élément cinq');
Exclude (MonEnsemble, Valeurs(GetEnumValue(InformationDeType,'CINQ')));
Writeln('Ajoute l''élément deux');
Include(MonEnsemble, Valeurs(GetEnumValue(InformationDeType,'deux')));
TestValeurEnsemble;
readln;
end. |
8-6. Interfaces invocable
"Une interface invocable est une interface dérivée de IInvokable qui assure l'ajout des informations de type à l'exécution, et sert à définir une " Interface serveur SOAP " lors de la définition d'un nouveau service web."
Vous pouvez consultez le chapitre suivant de l'aide en ligne de Delphi 2005 Présentation des interfaces invocables :
ms-help://borland.bds3/bds3win32devguide/html/soapunderstandinginvokableinterfaces.htm
10. Liens
La JCL propose de nombreuses routines d'accés aux RTTI, voir l'exemple : ...\jcl\examples\vcl\rtti\RTTIExample.bdsproj
Ou encore l'unité JclSysUtils qui contient de nombreuses méthodes d'accés à la VMT :
//--------------------------------------------------------------------------------------------------
function GetVirtualMethodCount(AClass: TClass): Integer;
function GetVirtualMethod(AClass: TClass; const Index: Integer): Pointer;
procedure SetVirtualMethod(AClass: TClass; const Index: Integer; const Method: Pointer);
function GetDynamicMethodCount(AClass: TClass): Integer;
function GetDynamicIndexList(AClass: TClass): PDynamicIndexList;
function GetDynamicAddressList(AClass: TClass): PDynamicAddressList;
function HasDynamicMethod(AClass: TClass; Index: Integer): Boolean;
function GetDynamicMethod(AClass: TClass; Index: Integer): Pointer;
... |
 
|