Trois nouveaux composants orientés données

Introduction

Un composant Delphi est automatiquement une classe et constitue à ce titre une extension de la VCL (Visual Component Library) de Delphi, cet ensemble de composants qui constitue le fondement de l'environnement de programmation Delphi. Le sujet est traité dans tous les livres qui traitent de Delphi et je présente une courte introduction du sujet dans la page intitulée Composants visuels de Delphi.

Dans ce qui suit, nous allons parler d'un type particulier de composants, les composants orientés données ("data aware" en anglais) et plus particulièrement de trois nouveaux composants orientés données que j'ai développés parce que j'en avais besoin pour effectuer d'autres travaux.

Architecture d'accès aux bases de données

Sur un ordinateur, les données permanentes, incluant les bases de données sont toujours "stockées" dans des fichiers. Pour les bases de données, le stockage des données se fait selon deux tendances: dans ce qui apparaît comme un seul fichier ou dans plusieurs fichiers localisés dans un répertoire unique. Delphi supporte les deux démarches sans toutefois accorder aux applications un accès direct aux bases de données. Il faut utiliser un interface logiciel comme la BDE (Borland Database Engine) ou l'ADO (ActiveX Data Object) de Microsoft.

Modèles d'accès aux bases de données dans Delphi 6

La BDE offre un accès direct à un certain nombre de bases de données incluant dBase, Paradox, ASCII, FoxPro et même Access. La BDE peut aussi rejoindre des serveurs de données comme Oracle, Sybase, Informix, Interbase et DB2 au moyen de pilotes disponibles dans la version Entreprise de Delphi. La BDE peut aussi rejoindre d'autres bases de données avec les pilotes ODBC () mais pour ces pilotes, il vaut mieux utiliser ADO. Notons finalement, que l'usage de la BDE oblige le développeur d'application à distribuer la BDE avec son apllication. On peut se connecter à une base de données avec la BDE en utilisant les composants du volet BDE de la palette de composants de Delphi.

L'ADO est une interface de haut niveau proposée par Microsoft pour l'accès aux bases de données. Elle est basée sur la technologie OLE DB d'accès aux données qui elle, donne accès aux bases de données relationnelles et non-relationnelles aussi bien qu'aux courriels, systèmes de fichiers et autres objets d'affaire. On peut se connecter à une base de données avec ADO en utilisant les composants du volet ADO de la palette de composants de Delphi.

Delphi offre aussi les volets dbExpress et Interbase pour se connecter aux bases de données. Il s'agit d'une connexion rapide rapide et légère dans le premier cas et d'une connection aux bases de données Interbase dans le second.

Lien vers la base de données

Quelle que soit la façon dont Delphi est connecté à une base de données, il existe des contrôles qui permettent l'accès aux bases de données en permettant la connexion du contrôle à une source de données (un composant TDataSource). Dans le volet de la palette des composants intitulée "Contrôles de données", Delphi en propose plusieurs: DBGrid, DBNavigator, DBText, DBEdit, DBMemo, DBImage, DBListBox, DBComboBox, etc. Tous ces composants ont une propriété DataSource qui leur établit une connexion avec une table ou une requête d'une base de données ainsi que une ou des propriétés pointant sur des champs spécifiques de la table ou de la requête sous-jaçente. On dit de ces composants qu'ils sont orientés données.

Descendants de TDataLink

Les composants connectables aux données ont la particulatité d'avoir une propriété DataSource qui leur permet de se connecter aux données. Lorsqu'on conçoit un tel composant, on doit utiliser un descendant de la classe TDatalink afin d'y arriver. J'explique: tout descendant de TDatalink a une propriété DataSource. Si on en fait une variable membre du nouveau composant, il est alors assez facile de coder une propriété du nouveau composant qui rend visible cette propriété et donne l'impression que c'est une propriété du nouveau composant.

De plus, lorsque le nouveau composant n'utilise qu'un champ de la source de données, Delphi fournit ce composant. Il s'agit de TFieldDataLink. Si on veut utiliser plusieurs champs, il faut concevoir un descendant approprié de TDatalink. C'est ce qu'on fait en annexe pour le composant GtroDBTreeView.

Nouvaux composants orientés données

Les composants que nous présenterons dans ce qui suit sont orientés-données et se connectent grâce à leur propriété DataSource. Il s'agit de GtroDBDateTimePicker, GtroDBPushButtonCalendar et GtroDBTreeView. Les deux premiers se servent du composant TFieldDataLink de Delphi pour se connecter alors que le dernier a requi la conception et le développement d'un descendant original de TDataLink.

Composant # 1 - Sélecteur de date

Le composant sélecteur de date TGtroDbDateTimePicker est une version connectable aux de données du composant TDateTimePicker de la palette de composants de Delphi. Le nouveau composant n'opère qu'avec les dates et il ne permet pas de choisir des heures. D'autre part, il ne fait rien de plus que TDateTimePicker sinon qu'il permet de modifier un champ de type Date d'un jeu de données. Il faut toutefois noter que la modification faite au champ du jeu de données n'est pas permanente: il faut que l'utilisateur sauvegarde explicitement la modification (post).

Lien avec les données

Le composant devient orienté données par l'intermédiaire de la classe TFieldDataLink de Delphi puisque le composant n'affecte la valeur que d'un seul champ du jeu de données. Une fois cette classe déclarée comme variable membre du nouveau composant, il a fallu rendre les propriétés DataSource, FieldName et ReadOnly de cette classe visibles de l'extérieur comme si elles étaient des propriétés du nouveau composant. C'est un processus assez simple que j'illustre dans la méthode de lecture de la propriété DataSource du nouveau composant:


procedure TGtroDBDateTimePicker.SetDataSource(const Value: TDataSource);
begin
  if not (FDataLink.DataSourceFixed and (csLoading in ComponentState)) then
    FDataLink.DataSource:= Value;
  if Value <> nil then
    Value.FreeNotification(self);
end;

Actions sur lesquels le composant réagit

On définit deux types d'actions du composant sur lesquels le jeu de données sous-jaçent à la source de données peut être modifié. Elles sont définies par la déclaration de type suivant:

type
  TCloseActions = (dtnChange, dtnCloseUp);

Au moment de la conception, on peut utiliser la propriété CloseActions définie dans ce qui suit

type
...
  published
    property CloseAction: TCloseActions read FCloseAction write FCloseAction;

pour définir l'action du sélecteur de date qui va tenter de modifier le champ du jeu de donnée. J'ai bien dit, tenter de modifier, car le code exécuté est le suivant:

procedure TGtroDBDateTimePicker.CNNotify(var Message: TWMNotify);
begin
  inherited; // fait ce que TDateTimePicker faisait ...
  with Message, NMHdr^ do
    begin
      case code of
        DTN_DATETIMECHANGE:
          if CloseAction = dtnChange then UpdateData(self);
        DTN_CLOSEUP:
          if CloseAction = dtnCloseUp then UpdateData(Self);
    end; //...case
  end; //...with
end;

Lorsqu'il doit y avoir un changement apporté à la valeur du champ du jeu de données, cette méthode appelle la méthode UpdateData dont le code suit:

procedure TGtroDBDateTimePicker.UpdateData(Sender: TObject);
begin
 try
  FDataLink.OnDataChange:= nil;
  FDataLink.DataSet.Edit;
  FDataLink.Field.AsDateTime:= Date;
 finally
  FDataLink.OnDataChange:= DataChange;
 end;
end;

L'exécution de cette méthode laisse le jeu de données en mode édition car la sauvegarde (post) doit être faite explicitement par l'usager du composant.

Composant # 2 - Calendrier à boutons poussoirs

Il y a quelques années, j'ai utilisé un logiciel appelé "Calendar Creator Plus" qui affichait un calendrier où chaque date était représentée par un bouton poussoir. Ici, j'utilise un calendrier dont chaque date peut être liée à un record d'une table de base de données suite à un click ou . un double-click. C'est le composant TGtroPushButtonCalendar dont vous voyez une réalisation (le calendrier) dans l'image qui suit.

Que fait ce composant? L'activation d'une date crée un nouveau record avec la date activée dans le champ connecté du jeu de données et change la couleur (propriété SelCellBkgCol à clRed ici) de la date activée dans l'affichage. Sa désactivation supprime le record correspondant et tout ce qu'il contient. La propriété MainActions du composant permet de choisir l'action qui déclenche l'activation ou la désactivation d'une date tant à la conception qu'à l'exécution. Ça peut être un clic ou un double-clic.

La signification de l'image ci-haut est simple: le calendrier de mai 2005 apparaît et le 1er, le 17 et le 19 du mois ont été activés par l'utilisateur comme étant des dates où un événement quelconque s'est produit. Dans le cas présent, il est possible pour l'utilisateur d'inscrire une courte note pour le 17 du mois mais, dans une autre application, le reste du record pourrait contenir n'importe quoi.

Au début, je pensais illuster le calendrier avec de vrais boutons mais, pour des raisons de simplicité, j'ai utilisé un calendrier semblable à TCalendar quui apparaît au volet "Samples" de Delphi. J'ai donc dérivé le nouveau composant de la classe TCustomGrid.

Comme je devais rendre le composant orienté données et que ce composant n'utilise qu'un seul champ du jeu de données, j'ai instancié un objet TFieldDataLink en tant que variable dans le composant. Ça été un détail de rendre les propriété DataSource et FieldName visibles de l'extérieur en tant que propriétés du nouveau composant.

Composant # 3 - Arborescence hiérarchique

Je me demandais comment générer une arborescence hiérarchique emmagasinée dans une table de base de données Paradox et j'ai conçu un composant orienté données qui aurait la caractéristique de se stocker dans une table d'une base de données. Comme une telle table n'est pas quelconque, je définis donc dans ce qui suit les conditions qui lui sont imposées.

Conditions exigées de la table de base de données

La table de base de données qui contient les éléments constitutifs de la vue hiérarchique produite par le composant TGtroDbTreeView n'est pas quelconque. Il faut qu'elle possède trois champs qui définissent les relations entre les éléments de la vue hiérarchique: un champ clé (Key field), un champ parent (Parent field) et un champ booléen (IsFolder) indiquant si le noeud est ou n'est pas un répertoire.

On doit en tout premier lieu connecter le composant TGtroDbTreeView avec la source de donnée; on connecte ensuite kle composant avec la table qui contient les éléments de la vue hiérarchique. Finalement, le composant TGtroDbTreeView a trois propriétés qui établissent la connexion du composant avec ces trois champs:

Ces trois propriétés sont le résultat de l'existance d'un lien de données qui est un descendant du composant TDatalink, une classe utilisée par les objets qui accèdent aux données (data aware en anglais) et qui veulent utiliser les événements générés par la base de données. Lorsqu'on accède à un seul champ de la table, on peut utiliser TFieldDatalink, un descendant de TDataLink qui fait partie des classes livrées avec Delphi. Puisque ici, on doit se lier à trois champs de la table, il a fallu en déveloper un qu'on a appelé TTreeViewDataLink.

Chargement de la vue hiérarchique

Le chargement de la vue hiérarchique à partir de la table de base de données ne peut se faire directement comme c'est le cas lorsqu'un composant TTreeView est chargé à partir d'un fichier texte. Le programme qui charge la vue hiérarchique est le suivant:

procedure TGtroDBTreeView.LoadFromTable;
var
  R, P, i: Integer;
  T: string;
  F: Boolean;
  Ptr: TItem;
  Item: TItem;
begin
  FListOfItems:= TListOfItems.Create; // crée la liste pour contenir les TItems
  OnChange:= nil;  // élimine la réaction à l'événement OnChange
  Items.BeginUpdate;
  try
    Items.Clear;
    AddRootNode;
    FDatalink.DataSource.DataSet.Open; // ouvre la table
    FDatalink.DataSource.DataSet.First; // met le pointeur sur le 1er record
    while not FDatalink.DataSource.DataSet.Eof do
    begin
      R:= FDatalink.Fields[0].AsInteger; // Key field
      P:= FDatalink.Fields[1].AsInteger; // Parent field
      F:= FDatalink.Fields[2].AsBoolean; // IsFolder field
      T:= FDatalink.Fields[3].AsString;  // Title field
      Item:= TItem.Create(F, R, P, T); // Crée un TItem initialisé
      FListOfItems.Add(Item); // Ajoute le TItem dans la liste
      FDatalink.DataSource.DataSet.Next; // Passe au record suivant
    end; // ...for
    for i:= 0 to FListOfItems.Count - 1 do
    begin // Mise à jour des pointeurs parents
      P:= TItem(FListOfItems.Items[i]).ParentID;
      if P <> 0 then
      begin
        Ptr:= FListOfItems.FindItem(P);
        TItem(FListOfItems.Items[i]).ParentItem:= Ptr;
      end; // ...if
    end; // ...for
    // Construction de la vue hiérarchique TreeView
    for i:= 0 to FListOfItems.Count - 1 do
      AddANode(TItem(FListOfItems.Items[i]));
    CustomSort(@MyCustomSortProc, 0); // ordonne les noeuds
    FullCollapse;
    Items.Item[0].Expand(False);
  finally
    OnChange:= TreeViewChange; // réassigne le gestionnaire d'événement de OnChange
    FListOfItems.Free; // libère la liste de TItems
    Items.EndUpdate;
  end; // try...finally
end;

Classes intermédiaires requises

Pour charger la vue hiérarchique à partir de la table de base de données, la méthode LoadFromTable doit utiliser les classes intermédiaires TItem et TListOfItems.

La classe TItem est un descendant de TObject déclarée comme suit:

TItem = class
  private
   RecordID, ParentID: Integer;
    Text: string;
    FParentItem: TItem;
    Flag: Boolean;
  public
    constructor Create(F: Boolean; R, P: Integer; T: string);
    property ParentItem: TItem read FParentItem write FParentItem;
  end;

D'autre part, la classe TListOfItems est un descendant de TList dont chaque item pointe sur un objet TItem.

TListOfItems = class(TList)
   destructor Destroy; override;
    function FindItem(X: Integer): TItem;
  end;

Chargement de la vue hiérarchique

La lecture de la table de base de base de données et le transfert des informations requises pour la vue hiérarchique se font en trois étapes:

  1. Tous les records de la table sont lus séquentiellement et le contenu des champs KeyField, ParentField, Title et IsFolder sont transférés dans les objets TItems créés pour chaque ligne, chacun de ces objets étant placés dans une liste de type TList;
  2. Cette liste est ensuite lue TItem par TItem et l'objet TItem dont le champ RecordID correspond au champ ParentID de l'objet lu est trouvé et un pointeur placé dessus dans le champ ParentItem. Si aucun TItem n'est trouvé (lorsque ParentID est nul et le pointeur PatentItem est nil), l'objet n'a pas de parent et le noeud correspondant sera réputé avoir la racine de la vue hiérarchique comme parent.
  3. Il ne reste plus qu'à transformer les TItems en noeuds pour la vue hiérarchique (TTreeView) et à ordonner le tout.

Question importante
Quelle est la condition sur la table pour que le chargement de la vue hiérarchique se fasse en une seule passe -> il faut que les parents précèdent leurs enfants. Quel est l'index qui maintiendrait cet ordre?

La figure suivante présente une illustration des TItems à l'intérieur de la liste (TList). Les lignes fléchées indiquent de quelle façon les TItems sont liés par pointeurs avec leur TItem parent.

Ajoût/suppression/déplacement de noeuds

L'ajoût et la suppression de noeuds doivent être synchronisés avec les modifications correspondantes de la table de base de données. Ce sont des opérations qui sont gérés à l'intérieur du composant mais qui sont déclanchées au moyen d'un navigateur extérieur, lui même contrôlé de l'intérieur.

Navigateur interne

Le contrôle des ajoûts et suppressions de noeuds doit se faire au moyen d'un composant TDBNavigator dont le comportement est géré par le code du composant TGtroDbTreeView sans toutefois que ce composant soit créé à l'intérieur du composant: la propriété Navigator permet ainsi de connecter ce composant interne à un TDBNavigator existant. Comme la propriété Navigator est publiée, l'association avec le TDBNavigator extérieur peut donc se faire soir à la conception, soit à l'exécution.

La propriété Navigator de notre composant est lue au moyen de la méthode dont le code annoté suit:

procedure TGtroDBTreeView.SetNavigator(Value: TDBNavigator);
begin
  if Assigned(Value) then // nouvelle valeur <> nil => Initialisation de FNavigator
  begin
    // Mémorise le gestionnaire d'événement associé au navigateur externe
    // afin de l'appeler lors du déclanchement de OnClick
    FOldOnClick:= Value.OnClick;
    FNavigator:= Value;
    FNavigator.VisibleButtons:= [nbInsert, nbDelete]; // deux boutons
    FNavigator.DataSource:= DataSource; // assigne la source de données
    FNavigator.OnClick:= NavigatorClick; // assigne le gestionnaire d'événement interne
  end
  else // nouvelle valeur = nil => supprime tout lien avec FNavigaror
  begin
    if Assigned(FNavigator) then  // à moins que FNavigator ne soit déjà nil.
    begin
      FNavigator.OnClick:= nil;
      FNavigator.DataSource:= nil;
      FNavigator:= nil;
    end;
  end;
end;

Après sa connexion, le TDBNavigator externe est complètement contrôlé par notre composant de sorte que le gestionnaire d'événement qui était antérieurement associé avec le navigareur externe ne sera plus exécuté.

Le navigateur a deux boutons: un pour ajouter des noeuds, l'autre pour les supprimer. Le gestionnaire d'événement OnClick est le suivant:

procedure TGtroDBTreeView.NavigatorClick(Sender: TObject;
  Button: TNavigateBtn);
begin
  // Exécute le gestionnaire d'événement extérieur associé au navigateur
  if Assigned(FOldOnClick) then FOldOnClick(Self, Button);
  // Supprime le noeud sélectionné de la vue hiérarchique
  if Button = nbDelete then DeleteSelectedNode;
  // Ajoute un noeuf enfant au noeud sélectionné
  if Button = nbInsert then AddNewNode(NodeType);
  // Le type de ce noeud est spécifié par la propriété NodeType
end;

Il appartient à l'application cliente de déterminer le type de noeud à ajouter en assignant soit la valeur ntFolder (création d'un répertoire) ou la valeur ntFile (création d'un fichier) à la propriété publiée NodeType du composant. La valeur ntRoot est possible mais n'a aucun sens. Elle ne produit rien.

Ajoût de noeuds

procedure TGtroDBTreeView.AddNewNode(NodeType: eNodeType);
var
  RecordID, ParentID: Integer;
  NewNode: TTreeNode;
begin
  if NodeType = ntRoot then Exit; // do nothing if NodeType is ntRoot
  if Selected = nil then Selected:= Items.GetFirstNode;
  if not IsNodeAllowed(Selected) then Exit;
  RecordID:= NextRecordID;
  ParentID:= Integer(Selected.Data); // RefNo du noeud parent
  NewNode:= Items.AddChildObjectFirst(Selected, '     ', Pointer(RecordID));
    with NewNode do
    begin
     case NodeType of
        ntFolder:
        begin
          ImageIndex:= IMG_FOLDER_CLOSED;
          SelectedIndex:= IMG_FOLDER_OPEN;
        end;
        ntFile:
        begin
          ImageIndex:= IMG_FILE_CLOSED;
           SelectedIndex:= IMG_FILE_OPEN;
        end;
      end; // ...case
      MakeVisible;
    end; // ...with
    Selected:= NewNode;
    NewNode.Focused:= true;
    NewNode.EditText;
  // Inscription dans la table
  with FDataLink.DataSource.DataSet do
  begin
    Append;
    FDatalink.Fields[0].Value:= RecordID;
    FDatalink.Fields[1].Value:= ParentID;
    FDatalink.Fields[2].Value:= NodeType = ntFolder;
    FDatalink.Fields[3].Value:= NewNode.Text;
    Post;
    GetNextRecordID;
  end; // ...with
end;

Suppression de noeuds

La suppression des noeufs de la vue hiérarchique et la mise à jour de la table de données sous-jaçente a été l'objet de nombreux problèmes.

Modification et déplacement des noeuds

Appendices

Liens avec les données

Lorsqu'on développe une application qui se connecte à une base de données, on utilise des composant orientés données qu'on connecte à une source de données de type TDataSource qui doit elle-même être connectée à un ensemble de données (descendants de TDataSet). La connection entre le contrôle orienté données et la source de données estalors appelée lien de données (datalink en anglais) qui est représenté par un objet de classe descendante de TDatalink. En d'autres termes, pour rendre un composant orienté données, on doit lui adjoindre un lien de données et rendre visible de l'extérieur ses propriétés (DataSource et FieldName) de même que ses événements.

Classes dérivées de TDataLink

La classe TDataLink est déclarée dans l'unité db.pas. Techniquement, il ne s'agit pas d'une classe abstraite mais elle est rarement utilisée directement. Lorsqu'on crée un composant orienté données, on utilisera plutôt un de ses descendant ou on en dérivera un nous-même.

La classe la plus importante dérivée de TDataLink est la classe TFieldDataLink qui est utilisée par les contrôles data-aware qui ne se lient qu'à un seul champ du jeu de données. Comme la majorité des contrôles data-aware tombent dans cette catégorie, la classe TFieldataLink est la solutions pour la majorité des contrôles mais il y a des exceptions comme TGridDataLink qui permet la connexion d'une grille.

Dans le cas présent, on doit se lier à troix champs très particuliers du jeu de données et il a fallu développer la classe TTreeViewDataLink.

Classe TTreeViewDataLink

La classe ... (pas terminé!)

Éphémérides

Le composant TGtroDBTreeView a été développé en novembre 2000 comme composant devant être intégré dans une application de type gestionnaire d'information personnelles appelé "Assistant GTRO". Il a fonctionné de façon convenable jusqu'en avril 2004 alors que j'ai décidé de corriger quelques "bugs". Le suivant était cependant le plus important (et le plus ennuyeux).

Problèmes avec la suppression de noeuds (avril-mai 2005)

La suppression d'un noeuf de l'affichage hiérarchique brise la structure même de la vue hiérarchique et de sa table de base de donnes sous-jacente. Ce problème a été le sujet d'une investigation effectuée entre le 27 avril et le 1er mai 2005 et il a été résolu mais m'a obligé à restructurer le composant en y contrôlant un navigateur externe à partir de l'intérieur du composant.

Warning!
This code was developed for the pleasure of it. Anyone who decides to use it does so at its own risk and agrees not to hold the author responsible for its failure.


Pour toute question ou commentaire
E-Mail
page modifiée le 4 novembre 2014 à 18:55:06. []