Pour aider à célébrer le 25e anniversaire de C++Builder, je republie un très vieil article sur les extensions de langage C++ introduites par Borland à l’époque.
Comme vous l’avez probablement entendu, peu de temps après le 27e anniversaire de Delphi, C++ Builder célèbre son 25e anniversaire. Vous pouvez en savoir plus sur l’historique du produit dans cet excellent article de blog de C++Builder PM David Millington sur « Celebrating 25 Years of C++Builder ! » Il y a un autre article de blog très intéressant de David I sur » Le 25e anniversaire de C++Builder : le développement visuel, la puissance du langage C++ et 2,5 décennies d’excellence continue « . Même si vous êtes surtout intéressé par Delphi, les deux articles aident à mettre deux produits «jumeaux» dans la bonne perspective.
Au moment de la sortie de Delphi, j’étais principalement concentré sur C++ (et en particulier la bibliothèque Borland C++ OWL pour la programmation Windows) et j’avais écrit 3 livres sur le langage , l’IDE Borland et la bibliothèque. Alors que je suis passé à Delphi, C++ était toujours une partie active de mon objectif lorsque C++Builder est arrivé. J’ai donné une session à la conférence SD West sur les extensions de langage C++ Builder et j’ai beaucoup écrit sur ce sujet, même si je n’ai jamais écrit un livre complet dessus, étant assez occupé avec le côté Delphi.
Pour célébrer le 25e anniversaire de C++Builder, j’ai décidé de publier un de mes articles sur le langage exactement tel qu’il est. Ceci est tiré du matériel de la conférence SD 97 West. Tout n’est pas encore exact après 25 ans, mais la plupart de la description générale reste valable. Mais plutôt que de l’éditer dans les langues d’aujourd’hui, j’ai pensé qu’il était agréable de le garder exactement tel qu’il avait été écrit à l’origine. J’espère que vous apprécierez sa lecture.
Table of Contents
Extensions de langage C++ Builder (ou : programmation Delphi en C++)
de Marco Cantu’
Lorsque Delphi a été publié pour la première fois, de nombreux programmeurs se sont plaints : « Pourquoi n’est-il pas basé sur le langage C++ ? ». Certains d’entre eux (dont moi-même) ont répondu : « Tout simplement parce que ce n’est pas possible, en raison de nombreuses fonctionnalités manquantes du langage C++ ». Maintenant que Borland a publié C++Builder, ou Delphi pour C++, vous pourriez penser que je vais commencer à trouver des excuses pour ma mauvaise déclaration. Je ne suis pas! Je pense toujours que ma déclaration est correcte. En fait, Borland C++ Builder n’est pas basé sur le langage ANSI C++, mais sur une version fortement étendue de ce langage, qui inclut presque toutes les principales fonctionnalités du langage Object Pascal que l’on trouve dans Delphi.
Dans cet article, je vais discuter de toutes ces extensions de langage en détail, en fournissant des informations de base aux programmeurs sans expérience avec Delphi. Je ne vais pas couvrir toutes les nouvelles fonctionnalités de C++Builder, telles que les macros utilisées pour implémenter les paramètres de tableau ouvert ou le modèle utilisé pour cloner les ensembles Pascal. Je vais me concentrer uniquement sur les fonctionnalités de base.
Qu’est-ce qu’une propriété ?
L’un des éléments clés de la programmation visuelle est l’idée des propriétés d’un objet. Mais qu’est-ce qu’une propriété ? Une propriété est un nom de type , lié à certaines données ou fonctions membres d’accès aux données d’une classe.
L’une des idées de base de la POO est que les membres de données doivent toujours être privés. Ensuite, vous écrivez souvent des fonctions membres publiques pour obtenir et définir cette valeur privée. Si vous modifiez ultérieurement l’implémentation, vous pourrez facilement changer le code de ces fonctions d’accès, et éviter tout changement dans le code qui utilise la classe.
Voici un exemple de base (écrit selon les conventions de dénomination standard pour les propriétés, les champs et les fonctions d’accès) :
1 2 3 4 5 |
private: int fTotal; protected: void __fastcall SetTotal (int Value); int __fastcall GetTotal (); |
Le code de ces deux fonctions membres (non illustrées) définit ou renvoie simplement la valeur de la donnée membre privée fTotal .. Vous pouvez les utiliser de la manière suivante :
1 2 |
int x = Form1->GetTotal(); Form1->SetTotal (x + 5); |
Il s’agit de l’approche standard du C++. Dans C++Builder, nous pouvons définir à l’intérieur d’une classe une propriété enveloppant ces fonctions d’accès :
1 2 3 4 |
public: __property int Total = { read = GetTotal, write = SetTotal }; |
Cela signifie que nous pouvons maintenant accéder à cette valeur de manière plus uniforme, puisque nous utilisons la même notation à la fois pour lire ou écrire la propriété :
1 2 |
int x = Form1->Total; Form1->Total = x + 5; |
Selon son rôle dans une instruction, l’expression Form1->Total est traduite par le compilateur dans un appel à la fonction read ou write. En fait, le compilateur peut également traduire une expression similaire en un accès direct aux données. Regardez la déclaration de propriété suivante :
1 2 3 4 5 6 |
private: int fValue; public: __property int Value = { read = fValue, write = fValue }; |
Bien que ce code semble étrange au premier abord, il démontre pleinement le rôle des propriétés en tant que mécanisme d’encapsulation. En fait, nous pouvons modifier ultérieurement la déclaration de cette propriété en introduisant l’une des deux fonctions au lieu de l’accès direct aux données. Dans ce cas, nous devrons recompiler le code des classes qui utilisent cette propriété, mais nous n’aurons pas besoin de le modifier.
Évidemment, les fonctions d’accès ne se limitent pas à lire et écrire des données privées, mais peuvent faire n’importe quoi. L’un des effets typiques des fonctions d’écriture est de mettre à jour l’interface utilisateur. Voici une nouvelle version de la propriété Value , suivie du code de sa fonction d’écriture (écrite selon le style standard) :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
__property int Value = { read = fValue, write = SetValue }; void __fastcall TForm1::SetValue (int newValue) { if (fValue != newValue) { fValue = newValue; Label2->Caption = "Value = " + IntToStr (fValue); }; }; |
Maintenant, si nous écrivons les déclarations suivantes :
1 2 3 |
Value = Value + 2; Value += 2; Value ++; |
l’effet de la modification des données se reflétera automatiquement dans l’interface utilisateur. Notez que cela fonctionne aussi avec les opérateurs. Lorsque vous appliquez l’opérateur d’incrémentation (++) à la propriété, sa valeur est lue et la méthode Set est appelée. Je pense vraiment que les propriétés sont un bon mécanisme d’encapsulation POO !
Plus de règles linguistiques pour les propriétés
Outre la structure de base que nous avons vue, les déclarations de propriété vous permettent de nombreuses alternatives. Une idée fondamentale est que les propriétés ont un type de données, mais sont limitées à un ensemble donné de types de données (y compris la plupart des types, chaînes et classes prédéfinis).
Les propriétés peuvent être en lecture-écriture comme dans les exemples ci-dessus, mais elles peuvent également être en lecture seule ou en écriture seule. Ce qui est courant, c’est de voir des propriétés en lecture seule, c’est-à-dire des valeurs que vous pouvez lire mais pas modifier. Un exemple évident de propriété en lecture seule dans la VCL (la bibliothèque de composants visuels, la hiérarchie de classes de Delphi également utilisée par C++Builder) est la propriété Handle des contrôles basés sur une fenêtre. Vous voudrez peut-être demander à un contrôle son handle Windows, lorsque vous souhaitez appeler directement les fonctions de l’API Windows, mais vous n’êtes pas censé changer le handle d’une fenêtre. Pour définir une propriété en lecture seule, vous omettez simplement la déclaration d’ écriture :
1 2 |
__property int ReadValue = { read = GetReadValue }; |
Il est possible de déclarer des propriétés de tableau, c’est-à-dire des propriétés avec un index. Dans ce cas, les fonctions de lecture et d’ écriture requises ont un paramètre supplémentaire, l’index lui-même. Cet index doit également être utilisé pour accéder aux valeurs, car vous ne pouvez pas accéder au tableau dans son ensemble. Le tableau peut ne pas exister en tant que champ réel de la classe : lorsque vous lisez les éléments d’une zone de liste, vous demandez en fait à Windows la valeur de l’élément (qui n’est pas dupliqué à l’intérieur de l’ objet TListBox ). Il est également possible de déclarer des propriétés de tableau avec plusieurs index (voir par exemple la propriété Pixels de la classe TCanvas de la VCL).
La visibilité d’accès d’une propriété
Les propriétés peuvent être déclarées à l’aide de n’importe lequel des spécificateurs d’accès, y compris private (bien que cela n’ait pas de sens), protected et public, ainsi que les deux nouveaux spécificateurs : __published et __automated. Les propriétés en lecture seule ne peuvent pas être publiées, soit dit en passant.
Un champ ou une méthode publié n’est pas seulement disponible au moment de l’exécution (en tant qu’élément public), mais également au moment de la conception. Le compilateur Borland C++Builder génère une identification de type d’exécution (RTTI) de style Delphi pour les propriétés publiées de la classe dérivée de TPersistent . Ces informations de type sont utilisées par l’environnement de conception, à commencer par l’inspecteur d’objets, mais elles sont également disponibles pour les programmeurs qui souhaitent se plonger dans l’unité TypInfo non documentée.
Le mot-clé publié est généralement utilisé pour les propriétés ou les événements, mais les formulaires ont généralement une interface publiée comprenant également des sous-composants et des méthodes de gestion d’événements. Ces informations sont automatiquement analysées par l’environnement de développement et rendues disponibles dans l’inspecteur d’objets avant même que vous ne compiliez le programme.
Par exemple, alors que l’interface publiée d’un composant est utilisée par l’inspecteur d’objets pour afficher et modifier les valeurs de propriété au moment de la conception, l’interface publiée d’un formulaire est utilisée par l’inspecteur d’objets pour rechercher des composants compatibles avec un type de données et des fonctions membres donnés. compatible avec un événement donné.
Il existe un cinquième spécificateur d’accès, automatic , qui est utilisé pour définir une interface publique avec les informations de type OLE Automation correspondantes, permettant de créer des serveurs OLE Automation. Le mot-clé __automated est utilisé dans les sous-classes de TAutoObject .
Gardez également à l’esprit que la visibilité d’une propriété peut être étendue dans les classes dérivées. Une propriété protégée, par exemple, peut être redéclarée en tant que propriété publiée. Pour ce faire, vous n’avez pas besoin de redéfinir la propriété, et seulement de la re-déclarer (Borland utilise le terme « propriétés hissées » pour indiquer ce comportement). Lors de la re-déclaration d’une propriété, vous pouvez également la modifier, par exemple en modifiant sa valeur par défaut.
Fermetures et événements
Lorsque les propriétés sont du type de données « pointeur vers une fonction membre » (également appelée fermeture ), elles sont appelées événements. Mais qu’est-ce qu’une fermeture ? Un autre ajout au langage C++ standard. Une fermeture est une sorte de pointeur de fonction membre. En fait, il associe un pointeur vers une fonction membre à un pointeur vers une instance de classe, un objet. Le pointeur vers l’instance de classe est utilisé comme pointeur this lors de l’appel de la fonction membre associée. Voici la définition d’une fermeture :
1 |
typedef void __fastcall (__closure *TNotifyEvent)(TObject* Sender); |
Étant donné qu’en C++, les pointeurs de fonction membre sont également disponibles, mais rarement utilisés, vous pourriez vous demander pourquoi avons-nous besoin de ce truc gênant ? Les fermetures comptent vraiment dans le modèle Delphi. En fait, les événements sont des fermetures, contenant la valeur d’une fonction membre du formulaire hébergeant le composant associé. Par exemple, un bouton a une fermeture, nommée OnClick , et vous pouvez lui affecter une fonction membre du formulaire. Lorsqu’un utilisateur clique sur le bouton, cette fonction membre est exécutée, même si vous l’avez définie dans une autre classe (généralement, dans le formulaire). Voici ce que vous pouvez écrire (et C++ Builder écrit généralement pour vous) :
1 2 |
BtnBeep->OnClick = BtnHelloClick; BtnHello->OnClick = BtnBeepClick; |
Le code ci-dessus échange deux gestionnaires d’événements lors de l’exécution. Vous pouvez également déclarer explicitement une variable d’un type de fermeture, en tant que type TNotifyEvent commun , et l’utiliser pour échanger les gestionnaires de deux événements :
1 2 3 |
TNotifyEvent event = BtnBeep->OnClick; BtnBeep->OnClick = BtnHello->OnClick; BtnHello->OnClick = event; |
Cela signifie que vous pouvez affecter une fonction membre (comme BtnHelloClick ) à une fermeture ou à un événement à la fois au moment de la conception et au moment de l’exécution.
Propriétés et objets de diffusion en continu
Les classes dérivées de TPersistent ont d’autres caractéristiques importantes. Vous pouvez enregistrer des objets de ces classes dans un flux. La VCL n’enregistre pas les données internes d’un objet, mais enregistre simplement les valeurs de toutes ses propriétés (et événements) publiées. Lorsque l’objet est chargé, la VCL crée d’abord un nouvel objet, puis réaffecte chacune de ses propriétés (et événements). L’exemple le plus évident de flux d’objets est l’utilisation de fichiers DFM. Ces fichiers binaires stockent les propriétés et les composants d’un formulaire, et je vous propose de les charger dans l’éditeur C++Builder pour étudier leur version textuelle (vous pouvez aussi utiliser le programme en ligne de commande CONVERT.EXE pour transformer un fichier DFM en un fichier TXT ou vice versa)
Le processus de diffusion en continu peut être personnalisé de différentes manières : en ajoutant une clause par défaut ou stockée à la déclaration des propriétés, ou en remplaçant la méthode DefineProperties . Lorsque vous ajoutez une propriété à un composant, vous ajoutez généralement à la définition une clause par défaut . Si la valeur d’une propriété correspond à sa valeur par défaut, la propriété n’est pas enregistrée dans un flux avec les autres propriétés d’un objet. Le constructeur de la classe doit initialiser la propriété à la même valeur par défaut, sinon cette technique ne fonctionnera pas. La directive stockée , à la place, indique si une propriété doit être enregistrée dans un fichier avec l’objet ou non. Le stocké La directive peut être suivie d’une valeur booléenne ou d’une fonction membre renvoyant un résultat booléen (vous pouvez donc choisir d’enregistrer ou non la valeur en fonction de l’état actuel de l’objet). Enfin, la méthode DefineProperties vous permet de créer des pseudo-propriétés, d’ajouter des valeurs supplémentaires à l’objet diffusé et de les recharger correctement.
Plus de RTTI ( dynamic_cast et plus)
Outre les informations RTTI générées par le mot-clé __published , chaque objet dispose de plusieurs méthodes que vous pouvez utiliser pour s’interroger lui-même. Ces méthodes font partie de la classe TObject , la classe de base de chaque objet basé sur la VCL. Vous pouvez voir la liste des méthodes de la classe TObject (y compris les fonctions membres statiques) dans le tableau 1.
Tableau 1 : Fonctions membres de TObject
// fonctions membres publiques
TObject
Gratuit
Type de classe
CleanupInstance
Adresse de champ
Après la construction
AvantDestruction
Envoi
Gestionnaire par défaut
FreeInstance
~TObject
// fonction des membres publics statiques
InitInstance
Nom du cours
NomClasseEst
ClasseParent
InfoClasse
InstanceSize
Hérite de
MethodAddress
NomMéthode
Pour vérifier si une classe est d’un type donné, vous pouvez utiliser la méthode ClassType . Ceci est assez courant avec le paramètre Sender d’un gestionnaire d’événements. Ce paramètre fait référence à l’objet qui a généré l’événement, mais est du type TObject générique . Lorsque vous associez la même méthode à des événements d’objets différents, vous pouvez vérifier le type de l’objet qui a provoqué l’événement :
1 2 |
if (Sender->ClassType() == __classid(TButton)) Beep(); |
Le __classid est un autre mot clé ajouté à C++Builder. Elle renvoie la métaclasse de l’objet (voir la section suivante). L’alternative (pas toujours valide) consiste à utiliser le paramètre Sender d’un gestionnaire d’événements et à le convertir en un type de données donné, à l’aide de la technique C++ standard dynamic_cast . Cela a du sens si nous connaissons le type de données ou celui d’une classe ancêtre commune, comme dans ce cas :
1 2 3 |
TWinControl* wc = dynamic_cast<TWinControl*> (Sender); if (wc != 0) wc->Left = wc->Left + 2; |
Comme je l’ai mentionné, à côté de ces capacités RTTI, Delphi et C++ Builder partagent des informations de type étendues, disponibles au moment de l’exécution pour chaque objet d’une classe dérivée de TObject et ayant des champs, des propriétés ou des méthodes publiés. Par exemple, vous pouvez accéder à une propriété dynamiquement, par son nom ; vous pouvez obtenir la liste des noms des propriétés d’une classe ; vous pouvez obtenir la liste des paramètres d’une fermeture. Cela permet aux programmeurs de créer des outils complémentaires très puissants pour l’environnement et est utilisé par le système lui-même comme base des outils de développement visuels, à commencer par l’inspecteur d’objets (j’ai en fait construit un clone d’exécution de l’inspecteur d’objets) .
Métaclasses et constructeurs virtuels
Delphi a introduit le concept de référence de classe, un pointeur vers les informations de type d’une classe. Dans C++ Builder, cela a été mappé dans l’idée d’une classe TMetaclass, et le type TClass n’est rien d’autre qu’un pointeur vers cette métaclasse. Dans Delphi , TClass a un rôle similaire mais une définition différente (bien que les deux implémentations soient totalement compatibles).
Les méthodes disponibles pour une métaclasse correspondent exactement aux méthodes statiques de la classe TObject (en fait, il n’y a pas de classe TMetaclass dans Delphi, mais une référence de classe, qui peut utiliser directement les méthodes statiques de TObject ). Une fois que vous avez une variable TClass , vous pouvez lui affecter une classe, extraite de l’objet (avec la fonction ClassType ) ou obtenue de la classe elle-même en utilisant le mot-clé __classid . Voici un exemple:
1 |
TClass myclass = __classid (TEdit); |
Vous pouvez utiliser une métaclasse presque comme vous utilisez une référence de classe dans Delphi. Ce qui n’est pas possible dans C++Builder, c’est de créer un nouvel objet basé sur une métaclasse. C’est étrange : C++Builder vous permet de définir des constructeurs virtuels, mais ne fournit aucun moyen de les appeler, sauf si vous appelez une méthode Delphi. C’est ce qui se passe lorsque vous définissez un nouveau composant dans C++Builder et que vous laissez ensuite la VCL le gérer (par exemple, lorsque vous chargez un composant à partir d’un flux, son constructeur virtuel est appelé).
Pour créer un objet à partir d’une référence de classe, nous devons ajouter une simple unité Pascal à notre application C++Builder. Voici le code de la fonction Pascal que nous pouvons utiliser :
1 2 3 4 |
function DynCreate (myclass: TComponentClass; Owner: TComponent): TComponent; begin Result := myclass.Create (Owner); end; |
Nous pouvons utiliser cette fonction dans une application C++Builder. Il s’agit du code brut que vous pouvez utiliser pour créer un composant d’une classe donnée lors de l’exécution et le placer dans un contrôle ScrollBox :
1 2 3 4 5 6 7 8 |
void __fastcall TForm1::BtnNewEditClick(TObject *Sender) { TEdit* edit = new TEdit (this); edit->Parent = ScrollBox1; edit->Left = random (ScrollBox1->Width); edit->Top = random (ScrollBox1->Height); edit->Text = "Edit" + IntToStr (++Counter); } |
Voici le code que vous pouvez utiliser pour créer un composant du même type que l’ objet Sender :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void __fastcall TForm1::OnDynNew(TObject *Sender) { TClass myclass = Sender->ClassType(); // illegal: // TComponent* Comp = new myclass (this); TComponent* Comp = DynCreate (myclass, this); TControl* Control1 = dynamic_cast<TControl*> (Comp); if (Control1 != 0) { Control1->Parent = ScrollBox1; Control1->Left = random (ScrollBox1->Width); Control1->Top = random (ScrollBox1->Height); Control1->Name = "Comp" + IntToStr (++Counter); } }; |
Conclusion
Comme nous l’avons vu, Borland a ajouté de nombreuses nouvelles fonctionnalités au langage C++ dans C++ Builder pour rendre ce langage compatible avec Object Pascal et adapté à la programmation basée sur les composants. Étendre C++ de cette façon peut sembler un peu gênant, puisque nous essayons de mapper les constructions d’un langage dans un autre langage. Cependant, la facilité d’utilisation de l’environnement et le développement visuel valent probablement cette complexité supplémentaire, qui reste la plupart du temps dans les coulisses.
Dans le même temps, C++ Builder conserve une compatibilité totale avec la norme ANSI C++, y compris les modèles et la STL (pour nommer deux fonctionnalités non disponibles pour les programmeurs Delphi), et vous permet de mélanger du code basé sur VCL avec du code MFC ou OWL, bien que cela rend n’a de sens que si vous avez des applications existantes. La bibliothèque VCL, en fait, vous permet de tirer parti de la programmation visuelle et de travailler à une abstraction plus élevée que les bibliothèques de classes Windows C++ typiques.
Il existe de nombreuses autres différences entre Delphi et C++Builder, mais l’implémentation des propriétés est la fonctionnalité qui a le plus d’impact en termes d’extensions du langage C++. La plupart des autres problèmes (cartes de messages, ensembles et paramètres de tableau ouvert, pour n’en citer que quelques-uns) ont été résolus à l’aide de mots-clés et de fonctionnalités de langage C++ existants.
Marco Cantu’; est un écrivain et consultant indépendant, basé en Italie. Il a écrit des livres de programmation sur C++ et Delphi, traduits en 10 langues dans le monde. Il contribue à plusieurs magazines, aime prendre la parole lors de conférences et enseigne des séminaires avancés Delphi et C++Builder dans le monde entier. Vous pouvez le joindre au [email protected] ou http://ourworld. informatique. com/homepages/marcocantu.
Inutile de dire que les informations de contact et le site Web de Compuserve ci-dessus ne sont plus valides, mais j’ai décidé de laisser l’article tel qu’il a été initialement publié, y compris ma biographie.
Design. Code. Compile. Deploy.
Start Free Trial Upgrade Today
Free Delphi Community Edition Free C++Builder Community Edition