jeudi 18 janvier 2007

Comment fonctionne les dfm ?

Les dfm sont les fichiers descriptifs des fiches, datamodule et frame. Le but de cet article est de décrire comment fonctionne la construction des fiches dans un programme Delphi.

La sérialisation des composants dans Delphi passe par la classe TStream. Cette classe est abstraite et ne propose pas d'implémentation des méthodes d'accès au support de stockage. De cette manière on a dans la VCL une implémentation de cette classe pour des fichiers disques (TFileStream), pour la RAM (TMemoryStream), pour un blob binaire dans une base de données (TBlobStream), sur un réseau (TSocketStream) ou dans les ressources d'un fichier au format PE, type .exe ou dll (TResourceStream). C'est cette dernière classe qui va nous intérresser ici.

En fait les points communs entre TDatamodule, TForm et TFrame est que tout d'abord ce sont des composants et donc qu'ils peuvent être agrégés et sérialisés. De plus les constructeurs de ces trois classes sont conçus pour aller chercher un fichier dfm et le déssérialiser.

Dans l'éditeur de Delphi, ces classes se dérivent toujours. A chaque fois que l'on souhaite créer une nouvelle fiche, Delphi va construire une nouvelle classe de fiche dérivée de TForm. De plus le source de cette classe possède une directive particulière qui lui permet de convertir le fichier dfm associé en ressource du fichier compilé ({$R *.dfm}). Dans les sources des constructeurs de ces classes on peut constater que l'on fait appel à un traitement particulier si l'on n'est pas dans l'IDE et si la classe courante n'est pas la classe de base.

Ce traitement particulier se trouve dans la procédure InitInheritedComponent. Cette procédure va faire appel à la fonction InternalReadComponentRes(const ResName: string; HInst: THandle; var Instance: TComponent) qui va reconstruire la fiche à partir de la ressource ResName, dans le module HInst et sur le composant Instance. Le nom de la ressource est celui de la classe finale.

Voilà.

Comment appeler une méthode par son nom ?

L'objectif est de pouvoir appeler une méthode sur un objet, descendant de TObject, dont la classe est inconnu. Cette méthode sera simple et sans paramètre. La difficulté ici est que l'on ne peut pas passer par le transtypage.

Donc il va falloir ruser pour pouvoir faire l'appel à une méthode de cette classe.

Conditions préalables :

1 - La classe doit être compilé avec la directive $M+ (par exemple TPersistent).

2 - La méthode que l'on souhaite appeler doit être publiée.

Pour récupérer la méthode save d'un objet inconnu il faut passer par la fonction de classe MethodAddress introduite dans TObject. Tant qu'une classe descendante de TObject n'est pas compilée avec la directive $M+, les instances n'auront pas de RTTI, on donc pas de possibilité de retrouver l'adresse d'une méthode.

Pour plus de détails à ce sujet vous pouvez voir le code de MethodAddress dans System.pas.

Le code de cet appel est le suivant :


var
zmSave: procedure of object; // la méthode save
zpSave: pointer; // pointeur vers le code de la méthode save
begin
// si il existe un éditeur courant et que le panneau de cet éditeur existe et que le panneau a une méthode
// save publié alors....
if assigned(CurrentEditor) and assigned(CurrentEditor.ctrl) and (CurrentEditor.ctrl.MethodAddress('Save') <> nil) then
begin
// on récupère le pointeur sur le code de la méthode save
zpSave := CurrentEditor.ctrl.MethodAddress('Save');
// puis on initialise la variable zmSave avec le code et les données associés
TMethod(zmSave).data := CurrentEditor.ctrl;
TMethod(zmSave).code := zpSave;
// et on fait l'appel
zmSave;