Gestion d'une base de données MySQL

Avec les composants natifs de Lazarus


précédentsommairesuivant

V. Exemple 3 : une application complète

Nous pourrions déjà arrêter là ce tutoriel : vous avez découvert les composants natifs de Lazarus permettant de créer des applications utilisant une base de données et vous pourrez sans trop de difficultés appliquer les principes vus pour MySQL à d'autres systèmes de bases de données. Mais nous allons essayer d'aller un peu plus loin, en découvrant d'autres contrôles spécialisés, en voyant comment manipuler les données dans des contrôles classiques (non spécialisés dans les bases de données), et comment regrouper le traitement des données dans une unité d'un type un peu particulier : un DataModule. Nous parlerons également de l'intérêt de centraliser la génération des requêtes SQL dans une interface.

V-A. Création du projet

Commençons par le commencement :

  • créez un nouveau projet de type Application ;
  • renommez l'unité Unit1 en Main ;
  • renommez la fiche principale en MainForm (qui devient automatiquement de type TMainForm) ;
  • changez sa propriété Caption (son titre) en, par exemple, « Location de voitures » ;
  • enregistrez le projet sous le nom mysql03, dans un nouveau répertoire du même nom.

V-B. Unité de type DataModule

Les deux premiers exemples étaient de minuscules applications, qui ne sont pas très difficiles à comprendre ni à déboguer, pour un développeur qui les découvre. Lorsque l'on crée des applications de plus grande ampleur, ces aspects (comprendre et déboguer) prennent toute leur importance et il faut que vous aussi vous vous y retrouviez facilement si vous devez en assurer la maintenance dans le futur.

Nous allons faire en sorte que tout le traitement en rapport avec la base de données soit regroupé à part, et Lazarus possède un type particulier d'unité adapté à cela : le DataModule. Ce type d'unité est l'endroit idéal pour déposer des composants invisibles, comme ceux que nous avons utilisés dans ce tutoriel, mais ce n'est pas limitatif.

Cette manière de faire pourrait aussi faciliter la migration d'une application vers un autre système de gestion de bases de données.

Allez dans le menu Fichier, Nouveau, Module de données et cliquez sur OK pour ajouter une unité DataModule :

Image non disponible

La nouvelle unité créée présente une fiche similaire à une fiche normale de type Tform. Renommez tout de suite l'unité en DataAccess.

Depuis l'onglet de composants SQLdb, déposez sur la fiche un composant TMySQLxxConnection correspondant à votre version, et un composant TSQLTransaction. Renommez-les respectivement SQLConnection et SQLTransaction, et assignez SQLConnection à la propriété Database de SQLTransaction. N'oubliez pas d'inscrire UTF8 dans la propriété CharSet de SQLConnection.

Allez dans le menu Projet, Options du projet et cliquez sur l'entrée Fiches. Vous constatez que la fiche principale et le datamodule sont automatiquement créés au démarrage de l'application. C'est très bien ainsi, hormis qu'ils ne sont pas créés dans le bon ordre : vous comprendrez très vite pourquoi le datamodule doit être créé avant la fiche principale. Mettez DataModule1 en surbrillance et faites-le monter en tête de liste à l'aide de la petite flèche verte à gauche. Tant que vous y êtes, décochez la case Créer automatiquement les nouvelles fiches au bas du dialogue : nous n'aurons pas besoin que toutes les autres fiches de l'application soient créées au démarrage.

Image non disponible

Cliquez sur OK. Ouvrez l'inspecteur de projet, par le biais du menu Projet, Inspecteur de projet :

Image non disponible

Le projet est pour l'instant composé d'un programme principal, mysql03.lpr, et de deux unités, main.pas et dataaccess.pas. Double-cliquez sur le programme principal : dans son code source, vous voyez que le datamodule est bien créé avant la fiche principale :

 
Sélectionnez
program mysql03;

{$mode objfpc}{$H+}

uses
  {$IFDEF UNIX}{$IFDEF UseCThreads}
  cthreads,
  {$ENDIF}{$ENDIF}
  Interfaces, // this includes the LCL widgetset
  Forms,
  DataAccess,
  Main
  { you can add units after this };
{$R *.res}
begin
  RequireDerivedFormResource:=True;
  Application.Initialize;
  Application.CreateForm(TDataModule1, DataModule1);
  Application.CreateForm(TMainForm, MainForm);
  Application.Run;
end.

Nous avons mentionné, au début de ce chapitre, que l'utilisation du datamodule pourrait faciliter la migration de notre application vers un autre système de gestion de bases de données, comme SQLite ou PostgreSQL. Alors nous allons jouer le jeu et y regrouper tout ce qui est propre à MySQL.

Nous allons d'abord y inclure les méthodes de connexion (avec la demande de mot de passe) et de déconnexion que nous avions développées dans l'exemple 2.

Cliquez sur l'onglet du code source de l'unité DataAccess et ajoutez, dans la section public de la classe TDataModule1, ces deux méthodes :

 
Sélectionnez
function Login : Boolean;
procedure Logoff;

Pressez la combinaison de touches Shift-Ctrl-C, afin que Lazarus crée les deux méthodes dans la section implementation.

Recopiez dans la méthode Login le code de connexion contenu dans la méthode FormActivate de l'exemple 2, et dans la méthode Logoff le contenu de la méthode FormClose :

 
Sélectionnez
function TDataModule1.Login: Boolean;
(* Demande du mot de passe et connexion à la base de données *)
var
  LPassword : String;
begin
  Result := True;
  SQLConnection.HostName := '192.168.0.1';
  SQLConnection.DatabaseName := 'location';
  SQLConnection.UserName := 'mysqldvp';
  if InputQuery('Connexion à la base de données', 'Tapez votre mot de passe :', True, LPassword)
     then
       begin
         SQLConnection.Password := LPassword;
         try
           SQLConnection.Connected := True;
           SQLTransaction.Active := True;
         except
           on e : ESQLDatabaseError do
             begin   (* Erreur renvoyée par MySQL : fin de programme *)
               MessageDlg('Erreur de connexion à la base de données :'#10#10#13 +
                          IntToStr(e.ErrorCode) + ' : ' + e.Message +
                          #10#10#13'Fin de programme.', mtError, [mbOk], 0);
               Result := False;;
             end;
           on e : EDatabaseError do
             begin   (* Erreur de connexion : fin de programme *)
               MessageDlg('Erreur de connexion à la base de données.'#10#10#13'Fin de programme.', mtError, [mbOk], 0);
               Result := False;;
             end;
         end;
       end
     else
       Result := False;
end;
procedure TDataModule1.Logoff;
(* Déconnexion de la base de données *)
begin
  if SQLTransaction.Active
     then
       SQLTransaction.Active := False;
  if SQLConnection.Connected
     then
       SQLConnection.Connected := False;
end;

Pour que le compilateur trouve la fonction InputQuery, ajoutez l'unité Dialogs à la clause uses du datamodule, et ajoutez l'unité db pour le type EDatabaseError.

Retournez dans l'unité Main et pressez F12 pour afficher la fiche principale. Dans l'inspecteur d'objets, dans l'onglet Événements, descendez sur l'événement OnShow et cliquez sur les trois points correspondants. Cela va créer la méthode TMainForm.FormShow dans la section implémentation. C'est à cet endroit que nous allons appeler la méthode de login que nous avons implémentée dans le datamodule :

 
Sélectionnez
procedure TMainForm.FormShow(Sender: TObject);
(* Demande de mot de passe *)
begin
if DataModule1.Login
   then
     begin
       ShowMessage('Login couronné de succès !');
     end
   else
     Close;
end;

Faites la même chose avec l'événement OnClose :

 
Sélectionnez
procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
(* Fermeture propre de la connexion à la base de données *)
begin
  DataModule1.Logoff;
end;

Il ne faut pas oublier d'ajouter l'unité DataAccess à la clause uses de l'unité Main, sinon le compilateur dira qu'il ne connaît ni Login ni Logoff.

Vous pouvez compiler et exécuter votre application à ce stade, pour vérifier que la connexion est bien couronnée de succès.

Bon, il est temps de définir à quoi va ressembler et ce que va faire notre application de gestion de location de voitures.

Nous aurons une fenêtre principale, qui va contenir la liste des locations :

Image non disponible

Un dialogue permettra de gérer la liste des voitures disponibles :

Image non disponible

Un autre, similaire, sera consacré à la liste des clients :

Image non disponible

Dans un dialogue, on pourra créer une nouvelle location :

Image non disponible

Ce dialogue servira également à modifier une location existante.

Pour terminer, nous sortirons une facture à l'aide d'un générateur de rapports.

Nous avons du pain sur la planche ! Créons tout de suite le dialogue de gestion des voitures.

V-C. Utilisation des composants spécialisés

Dans les deux premiers exemples de ce tutoriel, vous avez déjà découvert les composants TDBGrid et TDBNavigator. Nous allons encore nous en servir, mais nous allons aussi utiliser d'autres composants spécialisés comme TDBEdit et TDBRadioGroup. Si vous parcourez l'onglet Data Controls de la palette, vous trouvez toute une panoplie de composants : mémo, liste déroulante, etc. Avec ceux que nous allons voir ici, vous devriez être à même de les utiliser tous par la suite.

L'utilisation de tous ces composants spécialisés impose de mettre en service, comme dans les deux premiers exemples, un TSQLQuery et un TDataSource. Et à quel endroit allons-nous les placer ? Dans le datamodule, bien sûr !

Nous assignerons un TSQLQuery et, éventuellement, un TDataSource à chaque table de la base de données.

Cliquez sur l'onglet du code source de l'unité DataAccess puis pressez F12. Sur la fiche DataModule1, déposez donc un exemplaire de chacun de ces deux composants (le TSQLQuery depuis l'onglet SQLdb et le TDataSource depuis l'onglet Data Access). Renommez-les respectivement SQLQueryVoitures et DataSourceVoitures. La propriété Database du premier doit être initialisée à SQLConnection et la propriété DataSet du second à SQLQueryVoitures.

Tant que nous sommes dans l'inspecteur d'objets, nous allons définir les différents champs de la table Voitures dans les propriétés de SQLQueryVoitures. Repérez la propriété FieldDefs et cliquez sur les trois points en regard. Un dialogue va s'ouvrir, dans lequel vous allez ajouter successivement les champs en cliquant sur le bouton « + » :

Image non disponible

Pour chaque champ, vous allez définir dans l'inspecteur d'objets les propriétés Name, DataType et, éventuellement, Size. Voici la liste de ces propriétés :

Name

DataType

Size

Plaque

ftString

12

Marque

ftString

20

Modele

ftString

20

Cylindree

ftInteger

0

Transmission

ftFixedChar

1

Prix

ftFloat

0

Créons à présent une nouvelle fiche, à l'aide du second bouton de la barre d'outils. Concomitamment, une nouvelle unité est créée : renommez-la Voitures et enregistrez-la sous le nom voitures.pas. Dans l'explorateur d'objets, changez la propriété Name de la fiche en CarForm et indiquez son titre (par exemple, « Liste des voitures ») dans la propriété Caption.

Cliquez sur l'onglet de composants Data Controls de la palette. Depuis cet onglet, déposez sur la fiche les composants énumérés ci-après.

Ne faites pas trop vite le lien entre les composants que vous allez déposer et le TDataSource : si vous le faites, Lazarus vous bloquera lorsque vous voudrez assigner aux contrôles les champs de la table Voitures.

Donc définissez d'abord toutes les autres propriétés de vos contrôles et finissez par leur propriété DataSource.

V-C-1. TDBGrid

Nommez-le dbgVoitures dans sa propriété Name et dimensionnez-le à 424 pixels de largeur (Width) sur 240 pixels de hauteur (Height). La propriété Scrollbars peut être fixée à ssAutoVertical.

Cliquez sur les trois points en regard de la propriété Columns : un assistant va vous aider à créer les différentes colonnes du DBGrid. Cliquez chaque fois sur le bouton « + » pour ajouter une colonne et éditez les propriétés de celle-ci dans l'inspecteur d'objets.

Image non disponible

Tous les titres (propriété Title/Alignment) étant centrés (taCenter), voici les propriétés des différentes colonnes à ajouter :

FieldName

Caption

Width + MaxSize

Alignment

Plaque

Plaque

79

taCenter

Marque

Marque

80

taLeftJustify

Modele

Modèle

110

TaLeftJustify

Cylindree

Cyl.

45

taCenter

Transmission

Trans.

45

taCenter

Prix

€/jour

45

taCenter

Pour terminer les réglages, il faut modifier les propriétés suivantes :

Propriété

Valeur

Options/dgIndicator

False

FixedCols

0

Comme nous l'avons mentionné juste avant de commencer le dépôt des composants sur la fiche, nous terminons par la propriété DataSource du DBGrid, que nous assignons à DataModule1.DataSourceVoitures.

Nous pourrions penser que le fait de faire un lien entre un composant de la fiche et un datasource qui se trouve dans un datamodule entraînerait de la part de Lazarus la déclaration de ce datamodule dans la clause uses de l'unité de la fiche. Il n'en est rien : c'est à nous de le faire. Ajoutez donc l'unité DataAccess à la clause uses de l'unité Voitures.

V-C-2. TDBNavigator

Nous restons pour l'instant en terrain connu, car nous allons déposer sur la fiche un composant TDBNavigator. C'est lui qui s'occupera de la navigation dans la table et de toutes les modifications de données.

Déposez-le à droite du DBGrid, nommez-le dbnVoitures (propriété Name), changez sa propriété Direction en nbdVertical (pour qu'il s'affiche verticalement), réglez sa largeur et sa hauteur pour qu'il vienne se coller le long de la bordure de droite du DBGrid :

Image non disponible

Fixez enfin sa propriété DataSource à DataModule1.DataSourceVoitures.

V-C-3. TDBEdit

Nous allons à présent (« enfin », direz-vous peut-être) découvrir un nouveau composant spécialisé : le TDBEdit. Il s'agit, comme son nom l'indique, d'un champ d'édition qui sera lié à un champ d'une table de base de données. Nous allons en déposer plusieurs, en dessous du DBGrid, accompagnés de classiques TLabels (de l'onglet Standard) :

Image non disponible

Leurs propriétés respectives Name et Caption (pour les TLabel), et Name et DataField (pour les TDBEdit) seront fixées comme suit :

Name (TLabel)

Caption (TLabel)

Name (TDBEdit)

DataField (TDBEdit)

lblPlaque

&Plaque

dbePlaque

Plaque

lblMarque

Mar&que

dbeMarque

Marque

lblModele

Mo&dèle

dbeModele

Modele

lblCylindree

&Cylindrée

dbeCylindree

Cylindree

lblPrix

€/&jour

dbePrix

Prix

Élargissez les deux TDBEdit correspondant à la marque et au modèle de voiture, pour laisser suffisamment de place au texte qui s'y placera.

Sélectionnez les cinq TDBEdit, en pressant la touche Shift tout en cliquant sur les composants, et assignez d'un seul coup DataModule1.DataSourceVoitures à leur propriété DataSource.

V-C-4. TDBRadioGroup

Le champ Transmission de la table Voitures indique si la voiture est équipée d'une transmission automatique ou manuelle ; ce champ est destiné à recevoir la valeur « A » ou « M ». Très naturellement, nous allons confier cette alternative à deux boutons radio, au sein d'un composant TDBRadioGroup.

Depuis l'onglet Data Controls (toujours le même), déposez à droite, en dessous du DBGrid, un composant TDBRadioGroup, que vous renommez directement dbrgTransmission. Sa propriété Caption devient « Transmission » et sa taille 90 (Width) par 73 (Height). Fort logiquement, vous assignerez à la propriété DataField le nom de champ Transmission.

Dans l'inspecteur d'objets, cliquez sur les trois points qui correspondent à la propriété Items du composant : un assistant va vous permettre d'énumérer les options qui seront proposées dans le groupe de boutons radio. Ce ne sera pas très long, puisque les valeurs possibles sont « A » et « M » :

Image non disponible

Nous terminons, comme chaque fois, par la propriété DataSource du composant, qui est initialisée elle aussi à DataModule1.DataSourceVoitures.

V-C-5. Finalisation de la fiche

Nous avons terminé le dépôt de tous les composants spécialisés sur la fiche. Nous allons la finaliser et la tester.

Il reste de la place, en bas et à droite de la fiche, pour ajouter deux boutons classiques : le premier, que nous renommerons en btnEnregistrer et dont nous initialiserons la propriété Caption à « &Enregistrer », et le second, que nous appellerons btnAnnuler et dont la propriété Caption sera « A&nnuler ». Réglez la taille de la fiche pour obtenir quelque chose de joli :

Image non disponible

Le but de notre fiche sera de permettre à l'utilisateur de modifier le contenu de la table Voitures de la base de données. Dès que notre dialogue s'affichera, toutes les voitures devront être chargées dans le DBGrid. Vous connaissez à présent la requête SQL qui permet de le faire :

 
Sélectionnez
SELECT * FROM Voitures;

Mais ! Rappelez-vous, nous avons pris le parti, dans cette application, de regrouper toute l'interface avec la base de données dans le datamodule. Nous n'allons donc pas implémenter notre requête dans le code de notre fiche, mais bien dans celui du datamodule.

Dans l'inspecteur d'objets, sélectionnez la fiche CarForm elle-même, cliquez sur l'onglet Événements, puis sur les trois points en regard de l'événement OnShow. Complétez la nouvelle méthode créée par Lazarus comme ceci :

 
Sélectionnez
procedure TCarForm.FormShow(Sender: TObject);
(* Chargement de la liste des voitures *)
begin
  DataModule1.ChargementVoitures;
end;

Nous confions donc le chargement de la table à une méthode ChargementVoitures, que nous n'avons pas encore écrite dans le datamodule. Nous le ferons juste après.

Les deux boutons que nous avons ajoutés en dernier vont permettre à l'utilisateur d'enregistrer les modifications, ou bien de quitter le dialogue sans les enregistrer. Nous allons créer une propriété Enregistre (nous aimerions écrire « Enregistré » mais les caractères accentués ne sont pas - pas encore ? - autorisés dans la syntaxe du Pascal) pour notre fiche, de type booléen, qui va permettre de savoir si les données ont bien été enregistrées au moment de fermer le dialogue.

Créez un champ FEnregistre dans la section strict private de la fiche, ainsi que la propriété dont nous venons de parler et son setter :

 
Sélectionnez
type
  { TCarForm }
  TCarForm = class(TForm)
    btnEnregistrer: TButton;
    btnAnnuler: TButton;
    dbePlaque: TDBEdit;
    dbeMarque: TDBEdit;
    dbeModele: TDBEdit;
    dbeCylindree: TDBEdit;
    dbePrix: TDBEdit;
    dbgVoitures: TDBGrid;
    DBNavigator1: TDBNavigator;
    dbrgTransmission: TDBRadioGroup;
    lblPlaque: TLabel;
    lblMarque: TLabel;
    lblModele: TLabel;
    lblCylindree: TLabel;
    lblPrix: TLabel;
    procedure FormShow(Sender: TObject);
    // DÉBUT DE L'AJOUT
  strict private
    FEnregistre : Boolean;
  private
    procedure SetEnregistre (AValue : Boolean);
  public
    property Enregistre : Boolean read FEnregistre write SetEnregistre;
    // FIN DE L'AJOUT
  end;

N'oublions pas d'initialiser cette propriété à False dès l'affichage du dialogue :

 
Sélectionnez
procedure TCarForm.FormShow(Sender: TObject);
(* Chargement de la liste des voitures *)
begin
  // AJOUT :
  Enregistre := False;
  // FIN AJOUT
  DataModule1.ChargementVoitures;
end;

Voici le code du setter :

 
Sélectionnez
procedure TCarForm.SetEnregistre (AValue : Boolean);
(* Setter de la propriété Enregistre *)
begin
  if FEnregistre = AValue
     then
       Exit;
  FEnregistre := AValue;
end;

Faisons en sorte que l'utilisateur reçoive un message de confirmation, s'il veut fermer le dialogue sans que les données soient enregistrées. Cela se fera en réponse à l'événement OnCloseQuery (cliquez sur les trois points en regard de cet événement, dans l'inspecteur d'objets) :

 
Sélectionnez
procedure TCarForm.FormCloseQuery (Sender : TObject; var CanClose : Boolean);
(* Demande éventuelle de confirmation de fermeture sans enregistrer *)
begin
  if Enregistre
     then
       CanClose := True
     else
       CanClose := (MessageDlg('Voulez-vous fermer sans enregistrer ?', mtConfirmation, [mbYes, mbNo], 0) = mrYes);
end;

Il nous reste juste à implémenter les méthodes qui vont réagir à un clic sur les boutons « Enregistrer » et « Annuler ». Cliquez successivement sur les trois points qui correspondent à l'événement OnClick des deux boutons, et complétez les méthodes comme ceci :

 
Sélectionnez
procedure TCarForm.btnEnregistrerClick(Sender: TObject);
(* Enregistre les modifications dans la base de données *)
begin
  Enregistre := DataModule1.SauvegardeVoitures;
  Close;
end;
procedure TCarForm.btnAnnulerClick(Sender: TObject);
(* Ferme la fenêtre sans enregistrer *)
begin
  Close;
end;

Vous le voyez, nous allons également tout de suite devoir écrire une méthode SauvegardeVoitures dans le datamodule.

V-C-6. Méthodes de chargement et de sauvegarde des données dans le datamodule

Allons-y, dans notre datamodule, et créons-y les deux méthodes publiques dont nous avons besoin. Il nous faudra une troisième méthode privée, que nous appellerons Commit, qui sera chargée d'enregistrer toutes les modifications définitivement dans la base de données.

 
Sélectionnez
type
  { TDataModule1 }
  TDataModule1 = class(TDataModule)
    DataSourceVoitures: TDataSource;
    SQLConnection: TMySQL56Connection;
    SQLQueryVoitures: TSQLQuery;
    SQLTransaction: TSQLTransaction;
  private
    // AJOUT :
    function Commit : Boolean;
    // FIN AJOUT
  public
    function Login : Boolean;
    procedure Logoff;
    // AJOUT :
    procedure ChargementVoitures;
    function SauvegardeVoitures : Boolean;
    // FIN AJOUT
  end;

Après un Shift-Ctrl-C, voici le code à implémenter :

 
Sélectionnez
function TDataModule1.Commit: Boolean;
(* Sauvegarde des changements dans la base de données *)
begin
  Result := True;
  if SQLTransaction.Active
     then
       try
         SQLTransaction.Commit;
       except
         on e: ESQLDatabaseError do
             begin   (* Erreur renvoyée par MySQL *)
               MessageDlg('Erreur n° ' +
                          IntToStr(e.ErrorCode) + ' : ' + e.Message,
                          mtError, [mbOk], 0);
               Result := False;
             end;
         on e: EDatabaseError do
           begin   (* Erreur générale *)
             MessageDlg('Erreur de sauvegarde des données', mtError, [mbOk], 0);
             Result := False;
           end;
       end
     else
       Result := False;
end;
procedure TDataModule1.ChargementVoitures;
(* Chargement des voitures *)
begin
  with SQLQueryVoitures do
    begin
      Close;
      SQL.Text := 'SELECT * FROM Voitures;';
      Open;
    end;
end;
function TDataModule1.SauvegardeVoitures: Boolean;
(* Sauvegarde de la table Voitures *)
begin
  SQLQueryVoitures.ApplyUpdates;
  Result := Commit;
end;

Il n'y a rien de nouveau, à ce niveau, par rapport aux deux premiers exemples du tutoriel.

V-C-7. Test de la fiche

Vous êtes sans doute tout excité(e) à l'idée de tester le dialogue de modification des voitures que vous avez créé.

Retournez dans l'unité de la fiche principale. On pourrait trouver mille et un événements pour afficher le dialogue (un bouton sur la fiche principale, une réponse à un clic de souris sur la fenêtre, etc.). J'ai opté pour un menu.

Dans l'onglet Standard, choisissez un TMainMenu et déposez-le sur la fiche. Renommez-le éventuellement MainMenu (propriété Name). Cliquez sur les trois points à droite de sa propriété Items, pour ouvrir l'assistant de conception.

Un seul item est présent dans l'éditeur de menu. Dans l'inspecteur d'objets, changez sa propriété Name en mnuFichier et sa propriété Caption en « &Fichier ». Faites un clic droit sur l'item dans l'assistant, puis choisissez Créer un sous-menu. Cliquez sur le nouvel item créé et, dans l'inspecteur d'objets renommez-le mnuFichierVoitures, avec comme caption « Gérer les &voitures ».

Image non disponible

Une fois que c'est fait, fermez l'assistant. Dans l'inspecteur d'objets, sélectionnez le tout dernier item qui vient d'être créé ; dans l'onglet Événements, cliquez sur les trois points à droite de l'événement OnClick et complétez la nouvelle méthode créée :

 
Sélectionnez
procedure TMainForm.mnuFichierVoituresClick(Sender: TObject);
(* Gestion de la liste des voitures *)
var
  LCarForm : TCarForm;   (* Dialogue de gestion des voitures *)
begin
  LCarForm := TCarForm.Create(Self);
  try
    LCarForm.ShowModal;
  finally
    FreeAndNil(LCarForm);
  end;
end;

N'oubliez pas d'ajouter l'unité Voitures à la clause uses de l'unité Main.

Cette fois, nous sommes prêts : compilez et exécutez l'application.

Entrez le mot de passe dans le premier dialogue :

Image non disponible

Le petit message nous informe que la connexion à la base de données est couronnée de succès :

Image non disponible

La fenêtre principale est bien vide pour l'instant (nous allons rapidement la remplir), juste le menu principal :

Image non disponible

Dans le menu Fichier, vous choisissez l'item Gérer les voitures. La fiche que nous voulons tester apparaît :

Image non disponible

Parcourez le DBGrid et voyez comme tous les contrôles que nous avons déposés sur la fiche se mettent à jour automatiquement. Faites l'expérience d'un modifier un, cliquez sur la petite coche (« Post ») du DBNavigator et voyez comment le champ se met à jour dans le DBGrid. Faites ce que vous voulez ; pensez simplement que lorsque vous aurez cliqué sur Enregistrer, vos modifications seront injectées dans la base de données.

V-C-8. Exercice : réaliser le dialogue de gestion des clients

Je vous propose un exercice, à ce stade : avec ce que vous venez de voir, vous devriez être capable de réaliser seul(e) le dialogue de gestion des clients, avec des composants spécialisés. Le principe est identique à celui des voitures.

Petites précisions :

  • appelez votre fiche CustomerForm et l'unité Clients ;
  • n'affichez pas toutes les colonnes dans le DBGrid, sous peine d'avoir une fiche très, très large. Vous pouvez vous contenter des noms et prénoms.

La solution (du moins, une solution possible) se trouvera dans le code complet du projet, que vous trouverez tout à la fin de ce tutoriel.

V-D. Une interface pour définir les requêtes SQL

Dans le souci de bien structurer notre application, nous avons centralisé l'interfaçage avec la base de données dans un datamodule. Nous allons encore aller un cran plus loin, en regroupant tout ce qui a trait à la syntaxe SQL dans une unité séparée.

SQL (acronyme de Structured Query Language), est un langage normalisé, devenu pratiquement universel, permettant d'exploiter un très grand nombre de bases de données. Chaque système de gestion de bases de données, malheureusement, ajoute çà et là de petites variantes, ou développe des extensions propres qui complètent le langage SQL de base.

Si nous voulons que notre application puisse aisément être transposée à un autre SGBD (par exemple, de MySQL à SQLite), nous n'avons qu'à modifier la syntaxe de nos requêtes, et le reste de l'application, hormis le connecteur, pourra rester pratiquement inchangé.

Nous allons déclarer une interface, qui va contenir toutes les requêtes utiles pour notre application. Pour chaque nouvelle syntaxe, il faudra déclarer une classe particulière qui sera obligée d'implémenter toutes les requêtes définies dans l'interface.

Voyez, au sujet des interfaces, le tutoriel de Laurent Dardenne et celui de Robin Valtot.

À l'aide du tout premier bouton de la barre d'outils de Lazarus, créez une nouvelle unité, que vous enregistrez immédiatement sous le nom sql.pas.

Dans la section type, créez une interface ISQLSyntax :

 
Sélectionnez
type
  ISQLSyntax = interface
  end;

À l'aide de la combinaison de touches Shift-Ctrl-G, créez automatiquement un GUID :

 
Sélectionnez
type

  ISQLSyntax = interface
    ['{4AF51BFD-D53D-43F7-9A36-17E859D467CE}']
  end;

La seule requête SQL que nous ayons utilisée jusqu'à présent est celle qui sélectionne toutes les voitures dans la table Voitures. Créez une première fonction SelectionVoituresToutes :

 
Sélectionnez
type

  { ISQLSyntax }

  ISQLSyntax = interface

    ['{238542C2-ADA2-46BC-9138-4D270BEB85D0}']
    function SelectionVoituresToutes : String;
      (* Requête de sélection de toutes les voitures *)
  end;

Pressez la combinaison de touches Shift-Ctrl-C : il ne se passe… rien. En effet, les méthodes déclarées dans l'interface sont uniquement implémentées dans une classe descendante.

Toute classe descendante de cette interface sera donc obligée d'implémenter la fonction SelectionVoituresToutes. Nous allons créer une classe pour la syntaxe MySQL :

 
Sélectionnez
  TMySQLSyntax = class(TInterfacedObject, ISQLSyntax)
    function SelectionVoituresToutes : String;
  end;

Cette fois, pressez Shift-Ctrl-C et complétez la méthode dans la section implémentation :

 
Sélectionnez
function TMySQLSyntax.SelectionVoituresToutes : String;
(* Requête de sélection de toutes les voitures *)
begin
  Result := 'SELECT * FROM Voitures;';
end;

Déclarez une variable de type TMySQLSyntax dans la partie interface de l'unité :

 
Sélectionnez
var
  SQLSyntax : TMySQLSyntax;   (* Syntaxe propre à MySQL *)

Pour bien illustrer que toute classe descendante de l'interface sera obligée d'implémenter toutes ses méthodes, faites l'expérience de créer une méthode bidon dans l'interface et de compiler : l'erreur renvoyée sera « No matching implementation for interface method 'bidon' found ».

Retournez dans l'unité DataAccess (le datamodule) et modifiez la méthode ChargementVoitures :

 
Sélectionnez
procedure TDataModule1.ChargementVoitures;
(* Chargement des voitures *)
begin
  with SQLQueryVoitures do
    begin
      Close;
      // DÉBUT DES MODIFICATIONS
      SQL.Text := MySQLSyntax.SelectionVoituresToutes;
      // FIN DES MODIFICATIONS
      Open;
    end;
end;

N'oubliez pas d'ajouter l'unité SQL à la clause uses.

À présent, réfléchissons à l'endroit où nous allons instancier la classe TMySQLSyntax. À quel moment en aurons-nous besoin ? À chaque fois qu'une commande SQL devra être générée, c'est-à-dire à peu près partout dans l'application. Donc le meilleur endroit est de la créer au moment de l'affichage de la fenêtre principale, après le login, puis de la libérer à la fermeture de l'application. Il faut donc modifier les méthodes FormShow et FormClose de la fiche principale (dans l'unité Main) :

 
Sélectionnez
procedure TMainForm.FormShow(Sender: TObject);
(* Demande de mot de passe *)
begin
if DataModule1.Login
   then
     begin
       // DÉBUT DE L'AJOUT
       (* Initialisation de la classe de syntaxe SQL *)
       SQLSyntax := TMySQLSyntax.Create;
       // FIN DE L'AJOUT

       ShowMessage('Login couronné de succès !');
     end
   else
     Close;
end;
procedure TMainForm.FormClose (Sender : TObject; var CloseAction : TCloseAction);
(* Fermeture propre de la connexion à la base de données *)
begin
  // DÉBUT DE L'AJOUT
  (* Libération des commandes SQL *)
  SQLSyntax.Free;
  // FIN DE L'AJOUT
  (* Déconnexion *)
  DataModule1.Logoff;
end;

Encore une fois, ajoutez l'unité SQL à la clause uses de l'unité Main.

V-E. Utilisation d'un TDBGrid sans TDBNavigator

Nous allons nous occuper de notre fenêtre principale, qui est bien vide pour l'instant. Elle contiendra la liste des locations, mais aussi différents filtres permettant de n'afficher que les locations répondant à différents critères.

V-E-1. TDBGrid

La liste des locations sera contenue dans un DBGrid, dont les colonnes seront les suivantes :

  • les nom et prénom du client ;
  • la plaque de la voiture ;
  • la marque de la voiture ;
  • son modèle ;
  • la date de début de location ;
  • la date de fin prévue ;
  • la date de rentrée de la voiture à l'issue de la location.

J'ai décidé d'afficher le nom et le prénom du client dans une seule colonne. Pour ce faire, une solution est d'ajouter dans la table Clients un champ supplémentaire. En réalité, nous n'avons pas besoin de créer une colonne qui contiendra en permanence des données : nous allons créer une colonne virtuelle, dont le contenu sera calculé à partir des colonnes Nom et Prénom (qui ne peuvent être nulles, ainsi que nous les avons conçues).

Direction le navigateur web et phpMyAdmin : dans l'onglet SQL, collez la commande suivante :

 
Sélectionnez
ALTER TABLE Clients ADD NomPrenom VARCHAR(81) AS ( CONCAT(Nom,' ',Prenom) ) VIRTUAL;

Cette nouvelle colonne NomPrenom ne prendra aucune place dans la base de données, et nous pourrons nous en servir pour afficher ensemble le nom et le prénom de chaque client.

Direction le datamodule : cliquez sur l'onglet de l'unité DataAccess et pressez F12.

Déposez sur la fiche un nouveau composant TSQLQuery et un nouveau TDataSource, que vous renommez tout de suite respectivement SQLQueryMain et DataSourceMain. Vous connaissez la musique : assignez SQLConnection à la propriété Database du SQLQuery et SQLQueryMain à la propriété DataSet du DataSource.

En cliquant sur les trois points en regard de la propriété FieldDefs du SQLQuery, définissez les différents champs à l'aide de l'assistant :

Name

DataType

Size

IdLocation

ftWord

0

IdClient

ftWord

0

Plaque

ftString

12

DateDebut

ftDateTime

0

DateFin

ftDateTime

0

DateRentree

ftDateTime

0

Assurance

ftSmallInt

0

Plaque_1

ftString

12

Marque

ftString

20

Modele

ftString

20

Cylindree

ftInteger

0

Transmission

ftFixedChar

1

Prix

ftFloat

0

IdClient_1

ftWord

0

Nom

ftString

40

Prenom

ftString

40

CodePostal

ftString

10

Localite

ftString

50

Rue

ftString

80

Numero

ftString

10

Telephone

ftString

40

Email

ftString

50

NomPrenom

ftString

81

Êtes-vous surpris(e) par le nombre de champs ? C'est normal : la table Locations fait référence aux tables Voitures et Clients, par ses clés étrangères, et dans toute requête de sélection nous ferons ce que l'on appelle une jointure. Laissez-moi le bénéfice du doute pendant quelques minutes encore.

Revenez à la fiche principale, en cliquant sur l'onglet Main et en pressant F12.

Élargissez franchement la fiche et déposez-y un composant TDBGrid, que vous renommez dbgMain et dont vous fixez la taille à 784 x 312.

À l'aide de l'assistant de création de colonnes (que vous exécutez en cliquant sur les trois points en regard de la propriété Columns, dans l'inspecteur d'objets), créez les colonnes suivantes :

Titre (Title)

Taille (Width)

Champ (FieldName)

Client

240

NomPrenom

Plaque

80

Plaque

Marque

80

Marque

Modèle

110

Modele

Début

80

DateDebut

Fin

80

DateFin

Rentrée

80

DateRentree

Pour obtenir un affichage correct dans les trois colonnes de dates, assignez à leur propriété DisplayFormat la valeur suivante : « dd"/"mm"/"yyyy ».

Pour terminer, affectez DataModule1.DataSourceMain à la propriété DataSource du DBGrid.

V-E-2. Filtres

Est-il intéressant d'afficher l'entièreté des locations, c'est-à-dire passées, présentes et futures ? Pas tellement, alors nous allons ajouter des filtres sur la fenêtre principale.

À droite du DBGrid, déposez deux TLabel (onglet Standard) et deux TDateEdit (onglet Misc) :

Image non disponible

Renommez-les respectivement lblFiltreDateDebut et lblFiltreDateFin, pour les TLabel, et deFiltreDateDebut et deFiltreDateFin pour les TDateEdit. Fixez les propriétés Caption des labels à « Date de début : » et « Date de fin : ».

Il nous faut fixer le format des deux composants d'édition de date : dans l'inspecteur d'objets, leur propriété DateOrder doit être assignée à doDMY. Vous voyez que le contrôle d'édition lié est automatiquement configuré par Lazarus en « __/__/____ », pour permettre d'afficher la date au format « jj/mm/aaaa » auquel nous sommes habitués.

Nous allons ajouter un autre filtre, en dessous des deux que nous venons de créer, permettant de n'afficher que les locations pour lesquelles une assurance complémentaire a été contractée par le client. Il s'agira d'une simple case à cocher (composant TCheckBox), que l'on trouve dans l'onglet Standard.

Assignez cbFiltreAssurance à sa propriété Name et « Assurance complémentaire » à sa propriété Caption.

Nous allons faire en sorte qu'au démarrage de l'application, les filtres sur la date de début et la date de fin soient fixés à un mois dans le passé et un mois dans le futur. Dans le code source de l'unité Main, ajoutez ce code à la méthode FormShow :

 
Sélectionnez
procedure TMainForm.FormShow(Sender: TObject);
(* Demande de mot de passe *)
begin
if DataModule1.Login
   then
     begin
       (* Initialisation de la classe de syntaxe SQL *)
       SQLSyntax := TMySQLSyntax.Create;
       (* Initialisation des filtres de dates *)
       // DÉBUT DE L'AJOUT
       deFiltreDateDebut.Date := IncDay(Today, -30);
       deFiltreDateFin.Date := IncDay(Today, 30);
       // FIN DE L'AJOUT
     end
   else
     Close;
end;

Ajoutez également l'unité DateUtils à la clause uses de l'unité.

V-E-3. Requête SQL de sélection

Penchons-nous à présent sur la requête SQL qui va nous permettre de charger le DBGrid en tenant compte des filtres.

Dans l'unité SQL, nous ajoutons une fonction à l'interface ISQLSyntax :

 
Sélectionnez
    function SelectionLocationsFiltre (
      (* Requête de sélection de locations avec critères *)
      const ADateDebut, ADateFin : TDateTime;   (* Dates de début et de fin *)
      const AAssurance : Boolean                (* Avec assurance complémentaire *)
      ) : String;

Comme prévu, nous passons comme paramètres à la fonction les dates de début et de fin, ainsi que l'option d'affichage de l'assurance complémentaire.

Ajoutez identiquement cette fonction à la classe TMySQLSyntax qui dérive de l'interface. À l'aide de la combinaison de touches habituelle Shift-Control-C, créez l'implémentation de cette fonction et complétez-la comme suit :

 
Sélectionnez
function TMySQLSyntax.SelectionLocationsFiltre (const ADateDebut, ADateFin : TDateTime;
                                                const AAssurance : Boolean) : String;
(* Requête de sélection de locations avec critères *)
begin
  Result := 'SELECT * FROM Locations' +
            ' INNER JOIN Voitures ON Locations.Plaque = Voitures.Plaque' +
            ' INNER JOIN Clients ON Locations.IdClient = Clients.IdClient' +
            ' WHERE DateDebut >= ''' + DateToStr(ADateDebut, FormatDate) + '''' +
            ' AND DateFin <= ''' + DateToStr(ADateFin, FormatDate) + ''' AND Assurance = ';
  if AAssurance
     then
       Result := Result + '''1'';'
     else
       Result := Result + '''0'';';
end;

Nous avons beaucoup de choses à dire à propos de cette requête.

Tout d'abord, je vous ai annoncé un peu plus haut que nous allions recourir à une jointure. Cette technique permet d'inclure à la requête différentes tables auxquelles il est fait référence, par le biais des clés étrangères, dans la table sur laquelle s'effectue la requête. C'est ainsi que la table Locations fait référence à la table Clients par son champ IdClient, et à la table Voitures par son champ Plaque. La requête de sélection joint donc la table Voitures par la commande SQL INNER JOIN Voitures ON Locations.Plaque = Voitures.Plaque et la table Clients par la commande INNER JOIN Clients ON Locations.IdClient = Clients.IdClient.

Ensuite, c'est dans la clause WHERE que nous effectuons les tests sur les différents filtres.

Une chose importante : il faut que les dates de début et de fin soient incluses dans la requête sous forme de texte, mais dans un format reconnu par MySQL.

Ce format de date n'a rien à voir avec le format doDMY que nous avons fixé pour les deux TDateEdit.

Nous allons donc ajouter à l'interface ISQLSyntax une fonction FormatDate qui va retourner une structure de type TFormatSettings utilisable par la fonction de conversion DateToStr :

 
Sélectionnez
    function FormatDate : TFormatSettings;
      (* Format de date et le séparateur compatibles avec le SGBD *)

Si vous avez la curiosité de regarder la déclaration du type TFormatSettings, vous verrez qu'il contient une vingtaine de champs. Seuls deux d'entre eux nous intéressent pour assurer la compatibilité du format de date avec MySQL :

  • DateSeparator ;
  • ShortDateFormat.

Voici l'implémentation de la fonction FormatDate dans la classe TMySQLSyntax :

 
Sélectionnez
function TMySQLSyntax.FormatDate: TFormatSettings;
(* Formats de date et de séparateur compatibles avec le SGBD *)
begin
  Result.DateSeparator := '-';
  Result.ShortDateFormat := 'yyyy-mm-dd';
end;

Vous voyez que la base de données est configurée pour travailler avec un format de date anglo-saxon.

Il reste un petit détail à mentionner, à propos du filtre sur l'assurance complémentaire : le booléen passé en paramètre doit être transformé en valeur 0 ou 1.

V-E-4. Chargement du DBGrid

Nous y sommes presque : le DBGrid est prêt, la requête SQL de sélection est écrite, il ne nous reste plus qu'à créer une méthode qui va charger le DBGrid.

Pour respecter notre logique, cette méthode se trouvera dans le datamodule. Hop, un clic sur l'onglet DataAccess dans l'éditeur de source !

Dans la section public du datamodule, créez la procédure suivante :

 
Sélectionnez
procedure ChargementLocations (const Requete : String);

Voici son implémentation :

 
Sélectionnez
procedure TDataModule1.ChargementLocations (const Requete: String);
(* Charge la table des locations *)
begin
  SQLQueryMain.Close;
  SQLQueryMain.SQL.Text := Requete;
  SQLQueryMain.Open;
end;

La requête passée comme paramètre sera celle que nous venons de créer.

Retournez dans l'unité Main et ajoutez à la méthode FormShow :

 
Sélectionnez
procedure TMainForm.FormShow(Sender: TObject);
(* Demande de mot de passe *)
begin
if DataModule1.Login
   then
     begin
       (* Initialisation de la classe de syntaxe SQL *)
       SQLSyntax := TMySQLSyntax.Create;
       (* Initialisation des filtres de dates *)
       deFiltreDateDebut.Date := IncDay(Today, -30);
       deFiltreDateFin.Date := IncDay(Today, 30);
       // DÉBUT DE L'AJOUT
       (* Chargement de la liste des locations *)
       DataModule1.ChargementLocations(SQLSyntax.SelectionLocationsFiltre(deFiltreDateDebut.Date, deFiltreDateFin.Date, cbFiltreAssurance.Checked));
       // FIN DE L'AJOUT
     end
   else
     Close;
end;

De cette manière, dès l'apparition de la fenêtre principale de l'application, s'affichera la liste des locations répondant aux filtres par défaut.

Je suis sûr que vous avez envie de tester votre programme. Oui, mais ! Il n'y a encore aucune location dans la base de données, donc le DBGrid sera vide. Faisons donc l'exercice de créer quelques locations directement dans la base, à l'aide de phpMyAdmin. Ce faisant, nous visualiserons, d'ailleurs, la jointure avec les voitures et les clients.

Rendez-vous dans votre navigateur et connectez-vous à la base de données. Sélectionnez, dans la colonne de gauche de phpMyAdmin, la table Locations, et cliquez à droite, sur l'onglet Insérer.

Laissez vide la première colonne, IdLocation : il s'agit d'un index qui sera automatiquement incrémenté. Dans la liste déroulante du champ IdClient, choisissez l'identificateur d'un client ; dans celle du champ Plaque, choisissez l'un des véhicules. Ces deux valeurs font le lien avec les deux autres tables, c'est à ce niveau que s'effectue la jointure. Définissez également une date de début et de fin (choisissez des dates proches d'aujourd'hui, car rappelez-vous, par défaut les filtres sont réglés à un mois dans le passé et un mois dans le futur), et inscrivez 0 ou 1 comme valeur pour le champ Assurance :

Image non disponible

Cliquez sur le bouton Exécuter pour créer la location.

Vous pouvez répéter l'opération une ou deux fois, histoire d'avoir quelque chose à afficher dans le DBGrid de l'application.

À présent, vous êtes prêt(e) à exécuter votre programme. Allez-y !

Image non disponible

Yes !

Réfléchissons encore un peu avant d'aller fêter cela. À chaque fois qu'une modification va être apportée à l'un des filtres, il faudra actualiser le contenu du DBGrid. Par conséquent, nous devons exécuter la méthode DataModule1.SelectionLocationsFiltre en réponse à tout événement OnChange d'un filtre.

Ce sera vite fait. Sélectionnez successivement les deux TDateEdit et le TCheckBox ; dans l'inspecteur d'objets, cliquez sur l'onglet Événements puis sur les trois points correspondant à l'événement OnChange. Complétez les trois méthodes événementielles créées par le même code :

 
Sélectionnez
procedure TMainForm.deFiltreDateDebutChange(Sender: Tobject);
(* Modification au filtre : mise à jour du contenu du DBGrid *)
begin
  DataModule1.ChargementLocations(SQLSyntax.SelectionLocationsFiltre(deFiltreDateDebut.Date, deFiltreDateFin.Date, cbFiltreAssurance.Checked));
end;
procedure TMainForm.cbFiltreAssuranceChange(Sender: Tobject);
(* Modification au filtre : mise à jour du contenu du DBGrid *)
begin
  DataModule1.ChargementLocations(SQLSyntax.SelectionLocationsFiltre(deFiltreDateDebut.Date, deFiltreDateFin.Date, cbFiltreAssurance.Checked));
end;
procedure TMainForm.deFiltreDateFinChange(Sender: Tobject);
(* Modification au filtre : mise à jour du contenu du DBGrid *)
begin
  DataModule1.ChargementLocations(SQLSyntax.SelectionLocationsFiltre(deFiltreDateDebut.Date, deFiltreDateFin.Date, cbFiltreAssurance.Checked));
end;

V-E-5. Exercice : ajouter un filtre pour n'afficher que les locations en cours

Je vous propose comme exercice d'ajouter un filtre pour que seules les locations en cours (dont la date de rentrée de la voiture est vide) s'affichent dans le DBGrid. Utilisez une case à cocher et réfléchissez bien à ce qu'il faut modifier dans la requête SQL.

Une valeur NULL ne se compare pas comme n'importe quelle valeur. Pour la tester, on peut utiliser la syntaxe Champ IS NULL ou Champ IS NOT NULL.

Une proposition de solution se trouve dans le code source complet de l'application.

V-E-6. Des boutons classiques pour l'ajout, la modification et la suppression

Nous avons pris le parti de ne pas utiliser de TDBNavigator, et donc d'utiliser le DBGrid comme une listbox classique. Nous allons par conséquent substituer des boutons normaux à ceux du DBNavigator.

En dessous des filtres, ajoutez trois composants de type TButton (onglet Standard) :

Nom (Name)

Libellé (Caption)

btnAjouter

&Nouvelle location

btnModifier

&Modifier la location

btnSupprimer

&Supprimer la location

Afin que le DBGrid se comporte comme une simple listbox, nous devons y désactiver les possibilités d'édition des données et faire en sorte que la sélection porte sur une ligne entière (et non plus sur une seule cellule). Sélectionnez-le et réglez les propriétés suivantes dans l'inspecteur d'objets :

Propriété

Valeur

ReadOnly

True

Options / dgDisableDelete

True

Options / dgDisableInsert

True

Options / dgEditing

False

Options / dgRowHighlight

True

Options / dgRowSelect

True

Maintenant, vous pouvez aller faire une pause bien méritée. Rechargez bien vos batteries et ne buvez pas trop, car ce qui va suivre va nécessiter toute votre attention.

V-F. Utilisation de composants non spécialisés

Créez une nouvelle fiche à l'aide du second bouton de la barre d'outils de Lazarus. Remplacez son nom Form1 par NewLeasingForm, ce qui transformera automatiquement son type en TNewLeasingForm. Assignez « Location » à sa propriété Caption. Tant que vous y êtes, donnez-lui comme dimensions 508 pixels (Width) sur 181 pixels (Height).

Cette fiche constituera le dialogue de création d'une nouvelle location. Comme nous sommes prévoyants, nous créerons la fiche de manière à ce qu'elle puisse facilement être utilisée pour modifier une location existante, dans une classe descendante.

Enregistrez-la et donnez à la nouvelle unité le nom Locations.

Nous allons déposer sur la fiche une série de contrôles classiques, c'est-à-dire non spécialisés dans les bases de données.

À partir du coin supérieur gauche, déposez quatre TLabel l'un en dessous de l'autre :

Name

Caption

lblClient

Client :

lblVoiture

Voiture :

lblDateDebut

Date de début :

lblDateFin

Date de fin :

En regard de ces quatre labels, déposez les quatre composants suivants :

  • deux TComboBox (onglet Standard), respectivement nommés (propriété Name) cbClients et cbVoitures, de largeur 288 ;
  • deux TDateEdit (onglet Misc), respectivement nommés deDateDebut et deDateFin.

Les deux TComboBox contiendront la liste des clients et des voitures. Les deux TDateEdit, eux, permettront de définir les dates de début et de fin de location. Fixez leur propriété DateOrder à doDMY et cochez leur propriété DefaultToday : ainsi, par défaut ils contiendront la date du jour.

À droite des TDateEdit, déposez un TCheckBox (onglet Standard), que vous renommez cbAssurance et dont vous initialisez la propriété Caption à « Assurance + ».

En dessous de cette case à cocher, déposez un autre TLabel nommé lblEstimationPrix, contenant « Estimation du prix (€) : » dans sa propriété Caption, et enfin un TStaticText (de l'onglet Additional), nommé stEstimationPrix, dont vous effacez la propriété Caption et vous fixez la propriété Alignment à taCenter.

La fiche devrait ressembler à ceci :

Image non disponible

Dans l'espace libre à droite, nous allons déposer divers contrôles permettant de filtrer les voitures qui se trouveront dans la liste.

D'abord, au milieu de l'espace vide, un TLabel, nommé lblCylindree, qui affiche « Cylindrée », et dont la propriété Font.Style est initialisée à [fsUnderline] (pour souligner le texte).

Ensuite, deux TLabel l'un à côté de l'autre, nommés lblCylindreeMin et lblCylindreeMax, dont les propriétés Caption sont respectivement « Min » et « Max ». En dessous de ces labels, deux TSpinEdit (onglet Misc), respectivement nommés seCylindreeMin et seCylindreeMax. Ces deux composants permettront de fixer les limites inférieure et supérieure de cylindrée des voitures de la liste. Il faut définir leurs bornes minimale et maximale (propriétés MinValue et MaxValue : 500 et 9900 pour le premier, 600 et 10000 pour le second), ainsi que la valeur qui sera incrémentée ou décrémentée à chaque pas, la propriété Increment, que nous fixons à 100. Par défaut, nous leur assignons comme valeur de départ 1000 et 2000, dans leur propriété Value. La largeur des deux TSpinEdit sera fixée à 64.

Nous allons remplir l'espace restant à droite avec un filtre sur la transmission. Dans la palette Standard, sélectionnez un TRadioGroup et déposez-en un sous les TSpinEdit, en adaptant sa largeur et sa hauteur pour occuper l'espace libre tout en restant aligné avec les autres contrôles (autant que ça soit joli, n'est-ce pas !). Renommez-le rgTransmission.

Nous déposons dans le TRadioGroup trois boutons radio (TRadioButton, de l'onglet Standard), dont Lazarus fera automatiquement un groupe. Les trois boutons sont nommés rbTransmissionX, rbTransmissionM et rbTransmissionA, et leurs propriétés Caption sont assignées à « Les deux », « Manuelle » et « Automatique ». Vous devinez aisément que le premier bouton radio ne filtrera pas les voitures sur le critère de la transmission, et que les deux autres filtreront les boîtes manuelles ou automatiques. Si Lazarus ne l'a pas fait automatiquement, cochez la propriété Checked du premier bouton radio, afin qu'il soit sélectionné par défaut.

Il ne reste plus que deux boutons à placer, en bas et à gauche : deux TButton respectivement nommés btnEnregistrer et btnAnnuler, avec comme propriété Caption « &Enregistrer » et « A&nnuler ».

Nous avons terminé la conception de notre fiche :

Image non disponible

Vous avez sûrement hâte d'afficher le nouveau dialogue, même vide. Retournez dans la fiche principale (unité Main), pressez F12, sélectionnez le bouton btnAjouter dans l'inspecteur d'objets, allez dans l'onglet Événements et cliquez sur les trois points pour créer une méthode qui répondra à l'événement OnClick. Ajoutez l'unité Locations à la clause uses de l'unité Main.

Sans surprise, voici le code d'affichage du dialogue :

 
Sélectionnez
procedure TMainForm.btnAjouterClick (Sender : TObject);
(* Ajout d'une nouvelle location *)
var
  LNewLeasingForm : TNewLeasingForm;   (* Dialogue de collecte des données *)
begin
  LNewLeasingForm := TNewLeasingForm.Create(Self);
  try
    LNewLeasingForm.ShowModal;
  finally
    FreeAndNil(LNewLeasingForm);
  end;
  (* Mise à jour de la liste des locations affichée *)
  DataModule1.ChargementLocations(SQLSyntax.SelectionLocationsFiltre(deFiltreDateDebut.Date, deFiltreDateFin.Date,
                                                                     cbFiltreAssurance.Checked, cbFiltreEnCours.Checked));

Le dialogue ne contient aucun composant orienté bases de données, c'était le but que nous nous étions fixé. Comment alors faire le lien avec la base de données ?

V-F-1. Chargement des clients et des voitures dans des TComboBox

Pour la liste des clients et des voitures, nous allons utiliser une fonctionnalité bien pratique des comboboxes : à chaque élément de la liste peut être attaché un objet quelconque. Considérons la déclaration de la méthode AddItem, qui est héritée de la classe TCustomComboBox :

 
Sélectionnez
procedure TCustomComboBox.AddItem (
  const Item: String;
  AnObject: TObject
  ); virtual;

Le paramètre Item contient la chaîne de caractères à afficher et AnObject sera un objet lié à l'élément, qui pourra contenir des données supplémentaires.

Réfléchissons : en plus de son nom, qui va être affiché dans la combobox des clients, de quelles données supplémentaires avons-nous besoin pour un client ? De pas grand-chose, en fait, juste son identificateur dans la table Clients de la base de données (la clé primaire de la table). Et pour une voiture ? Là, c'est plus compliqué, car il y a différentes caractéristiques d'une voiture qui sont concernées par les filtres de notre boîte de dialogue : la cylindrée et la transmission. Le prix par jour est également requis pour le calcul de l'estimation du coût de la location. Et nous aurons également besoin de l'identificateur de la voiture dans la table Voitures, qui est sa plaque d'immatriculation.

Dans l'unité DataAccess, nous créons deux classes TCBClient et TCBVoiture (« CB » faisant référence aux comboboxes auxquelles elles sont destinées) :

 
Sélectionnez
type

  { TCBClient }

  TCBClient = class
    (* Données invisibles d'un élément de combobox contenant la liste des clients *)
    strict private
      FIdClient : Integer;
    public
      property IdClient : Integer read FIdClient;
      constructor Create (const AIdClient : Integer);
  end;
  { TCBVoiture }
  TCBVoiture = class
    (* Données invisibles d'un élément de combobox contenant la liste des voitures *)
    strict private
      FPlaque : String;
      FCylindree : Integer;
      FTransmission : Char;
      FPrix : Real;
    public
      property Plaque : String read FPlaque;
      property Cylindree : Integer read FCylindree;
      property Transmission : Char read FTransmission;
      property Prix : Real read FPrix;
      constructor Create (const APlaque : String;
                          const ACylindree : Integer;
                          const ATransmission : Char;
                          const APrix : Real);
  end;

Voici le code des deux méthodes Create :

 
Sélectionnez
constructor TCBClient.Create (const AIdClient : Integer);
(* Initialisation des champs *)
begin
  FIdClient := AIdClient;
end;
constructor TCBVoiture.Create (const APlaque : String;
                               const ACylindree : Integer;
                               const ATransmission : Char;
                               const APrix : Real);
(* Initialisation des champs *)
begin
  FPlaque := APlaque;
  FCylindree := ACylindree;
  FTransmission := ATransmission;
  FPrix := APrix;
end;

Nous confions au datamodule le soin de charger les clients et les voitures dans les comboboxes correspondantes. Dans sa section public, créez les deux méthodes suivantes :

 
Sélectionnez
    procedure ChargementCBClients (var ComboBox : TComboBox);
    procedure ChargementCBVoitures (var ComboBox : TComboBox;
                                    const Requete : String);

Grâce à la combinaison de touches Shift-Ctrl-C, créez leur implémentation et complétez le code comme suit :

 
Sélectionnez
procedure TDataModule1.ChargementCBClients (var ComboBox : TComboBox);
(* Remplit une combobox avec les noms et prénoms des clients.
   Chaque élément est constitué d'un texte visible et de données invisibles *)
var
  LNomPrenom : String;   (* Texte visible d'un élément *)
begin
  ChargementClients(SQLSyntax.SelectionClientsTous);
  with SQLQueryClients do
    while not EOF do
      begin
        LNomPrenom := FieldByName('Nom').AsString + ' ' + FieldByName('Prenom').AsString;
        ComboBox.AddItem(LNomPrenom, TCBClient.Create(FieldByName('IdClient').AsInteger));
        Next;
      end;
end;
procedure TDataModule1.ChargementCBVoitures (var ComboBox : TComboBox;
                                             const Requete : String);
(* Charge une combobox avec les voitures correspondant à la requête.
   Chaque élément est constitué d'un texte visible et de données invisibles *)
var
  LVoiture : String;   (* Texte visible d'un élément *)
begin
  ComboBox.Clear;
  ChargementVoitures(Requete);
  with SQLQueryVoitures do
    while not EOF do
      begin
        LVoiture := '[' + FieldByName('Plaque').AsString + '] ' +
                    FieldByName('Marque').AsString + ' ' +
                    FieldByName('Modele').AsString;
        ComboBox.AddItem(LVoiture, TCBVoiture.Create(FieldByName('Plaque').AsString,
                         FieldByName('Cylindree').AsInteger,
                         (FieldByName('Transmission').AsString)[1],
                         FieldByName('Prix').AsFloat));
        Next;
      end;
end;

V-F-2. Requêtes de sélection des voitures et des clients

Ne compilez pas le projet à ce stade, sous peine d'obtenir des erreurs de compilation. Il faut d'abord ajouter l'unité StdCtrls à la clause uses de l'unité (pour que le compilateur connaisse le type TComboBox). Ensuite, les méthodes ChargementClients et ChargementVoitures, qui avaient été définies plus tôt, doivent être légèrement modifiées pour accepter comme paramètre une autre requête que SelectionClientsTous et SelectionVoituresToutes.

Nous partons du principe que tous les clients seront chargés dans la liste, et que donc une requête SELECT * toute simple sera suffisante pour remplir le TSQLQuery des clients. Par contre, la requête de sélection des voitures devra répondre aux différents filtres de la boîte de dialogue.

Dans le datamodule, modifiez la déclaration des méthodes ChargementClients et ChargementVoitures :

 
Sélectionnez
    procedure ChargementClients (const Requete : String);
    procedure ChargementVoitures (const Requete : String);

Et leur implémentation :

 
Sélectionnez
procedure TDataModule1.ChargementClients (const Requete : String);
(* Chargement des clients *)
begin
  with SQLQueryClients do
    begin
      Close;
      SQL.Text := Requete;
      Open;
    end;
end;
procedure TDataModule1.ChargementVoitures (const Requete : String);
(* Chargement des voitures *)
begin
  with SQLQueryVoitures do
    begin
      Close;
      SQL.Text := Requete;
      Open;
    end;
end;

La modification consiste à passer la requête à exécuter comme paramètre, ce qui rend d'usage beaucoup plus général les deux méthodes.

Cela signifie que nous devons également modifier les appels antérieurs à ces méthodes :

  • la méthode TCarForm.FormShow (de l'unité Voitures) :
 
Sélectionnez
procedure TCarForm.FormShow(Sender: TObject);
(* Chargement de la liste des voitures *)
begin
  Enregistre := False;
  DataModule1.ChargementVoitures(SQLSyntax.SelectionVoituresToutes);
end;
  • la méthode TCustomerForm.FormShow (si, bien sûr, vous avez fait l'exercice proposé au chapitre V-C-8).

Dans ces deux unités, il faudra rajouter l'unité SQL dans la clause uses.

Allons maintenant implémenter le chargement des deux comboxes dans notre dialogue TNewLeasingForm. Dans l'éditeur de source, sélectionnez l'onglet Locations et pressez F12. Dans l'inspecteur d'objets, allez dans l'onglet Événements et cliquez sur les trois points correspondant à l'événement OnShow. Lazarus crée une nouvelle méthode FormShow, que vous complétez comme suit :

 
Sélectionnez
procedure TNewLeasingForm.FormShow (Sender: TObject);
(* Chargement des contrôles *)
begin
  (* Listes des clients et des voitures *)
  DataModule1.ChargementCBClients(cbClients);
  DataModule1.ChargementCBVoitures(cbVoitures, RequeteSelectionVoituresFiltre);
end;

On voit tout de suite qu'il faudra créer la méthode RequeteSelectionVoituresFiltre ; gardons cela deux minutes dans un coin de notre esprit. Il faut tout d'abord avoir le réflexe de libérer les objets invisibles qui sont liés aux éléments des comboboxes, à la fermeture du dialogue.

Cliquez sur les trois points de l'événement OnClose, puis complétez le code :

 
Sélectionnez
procedure TNewLeasingForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
(* Détruit les objets passés comme paramètres aux comboboxes *)
var
  Li : Integer;
begin
  for Li := 0 to (cbClients.Items.Count - 1) do
    TCBClient(cbClients.Items.Objects[Li]).Free;
  for Li := 0 to (cbVoitures.Items.Count - 1) do
    TCBVoiture(cbVoitures.Items.Objects[Li]).Free;
end;

Pour compiler, l'unité DataAccess doit être ajoutée à la clause uses.

Occupons-nous de cette méthode RequeteSelectionVoituresFiltre dont nous avons postposé la création. Ajoutez cette déclaration dans la section private de la classe TNewLeasingForm :

 
Sélectionnez
    function RequeteSelectionVoituresFiltre : String;

Voici son implémentation :

 
Sélectionnez
function TNewLeasingForm.RequeteSelectionVoituresFiltre: String;
(* Construit la requête de sélection des voitures pour la combobox *)
var
  LTransmission : String;
begin
  if rbTransmissionM.Checked
     then
       LTransmission := 'M'
     else
       if rbTransmissionA.Checked
          then
            LTransmission := 'A'
          else
            LTransmission := '';
  Result := SQLSyntax.SelectionVoituresFiltre(seCylindreeMin.Value, seCylindreeMax.Value, LTransmission);
end;

Cette méthode ne construit pas directement la requête SQL, elle définit les paramètres à passer à une méthode de la classe TMySQLSyntax, que nous allons tout de suite créer.

Direction l'unité SQL et notre interface ISQLSyntax. Ajoutez-y la déclaration suivante :

 
Sélectionnez
    function SelectionVoituresFiltre (
      (* Requête de sélection de voitures avec critères *)
      const ACylindreeMin, ACylindreeMax : Integer;   (* Cylindrées minimale et maximale *)
      const ATransmission : String                    (* Transmission : "A" ou "M" *)
      ) : String;

Dans la déclaration du type TMySQLSyntax, ajoutez-la aussi :

 
Sélectionnez
    function SelectionVoituresFiltre (const ACylindreeMin, ACylindreeMax : Integer;
                                      const ATransmission : String) : String;

Un petit Shift-Ctrl-C puis complétez :

 
Sélectionnez
function TMySQLSyntax.SelectionVoituresFiltre (const ACylindreeMin, ACylindreeMax : Integer;
                                               const ATransmission : String) : String;
(* Requête de sélection de voitures avec critères *)
begin
  Result := 'SELECT * FROM Voitures WHERE Cylindree >= ''' + IntToStr(ACylindreeMin) +
            ''' AND Cylindree <= ''' + IntToStr(ACylindreeMax) + '''';
  if ATransmission <> ''
     then
       Result := Result + ' AND Transmission = ''' + ATransmission + '''';
  Result := Result + ';';
end;

La requête SELECT construite teste les bornes inférieure et supérieure de la cylindrée et introduit un test sur la transmission uniquement si le paramètre ATransmission n'est pas une chaîne vide.

V-F-3. Indices des client et voiture courants

Comme il s'agit d'une nouvelle location, par défaut ce sont les premiers éléments (d'indice 0) des deux comboboxes qui sont sélectionnés lorsque s'affiche le dialogue. Cependant, comme la liste des voitures va être rechargée à chaque fois que les filtres seront modifiés, il faudra garder en mémoire l'indice de la voiture sélectionnée pour la resélectionner après (si elle correspond, bien sûr, toujours aux critères).

Ensuite, rappelez-vous que nous avions prévu de concevoir notre fiche de manière à pouvoir servir de parent à une fiche descendante qui permettrait de modifier une location. Par conséquent, à l'affichage de la fiche, le client et la voiture de la location à modifier devront être sélectionnés. Alors autant prévoir tout de suite de gérer, également, l'indice de la personne.

La conservation des deux indices se fera dans deux propriétés.

Dans la déclaration du type TNewLeasingForm, avant la section private, créez une section strict private et déclarez-y les deux champs suivants :

 
Sélectionnez
  strict private
    FIndexClient : Integer;
    FIndexVoiture : Integer;

Dans la section public, créez les deux propriétés suivantes :

 
Sélectionnez
  public
    property IndexClient : Integer read FIndexClient write SetIndexClient;
      (* Index du client actuellement sélectionné *)
    property IndexVoiture : Integer read FIndexVoiture write SetIndexVoiture;
      (* Index de la voiture actuellement sélectionnée *)

Créez leurs setters dans la section private :

 
Sélectionnez
  private
    procedure SetIndexClient (AValue : Integer);
    procedure SetIndexVoiture (AValue : Integer);

Voici le code source des setters :

 
Sélectionnez
procedure TNewLeasingForm.SetIndexClient (AValue : Integer);
(* Setter de la propriété IndexClient *)
begin
  if FIndexClient = AValue
     then
       Exit;
  FIndexClient := AValue;
end;
procedure TNewLeasingForm.SetIndexVoiture (AValue : Integer);
(* Setter de la propriété IndexVoiture *)
begin
  if FIndexVoiture = AValue
     then
       Exit;
  FIndexVoiture := AValue;
end;

Initialisons les deux propriétés dans la méthode FormShow :

 
Sélectionnez
procedure TNewLeasingForm.FormShow(Sender: TObject);
(* Chargement des contrôles *)
begin
  (* Listes des clients et des voitures *)
  DataModule1.ChargementCBClients(cbClients);
  DataModule1.ChargementCBVoitures(cbVoitures, RequeteSelectionVoituresFiltre);
  // DÉBUT DE L'AJOUT
  (* Éléments sélectionnés dans les deux comboboxes *)
  IndexClient := 0;
  IndexVoiture := 0;
  cbClients.ItemIndex := IndexClient;
  cbVoitures.ItemIndex := IndexVoiture;
  // FIN DE L'AJOUT
end;

Nous savons déjà que, dans la fiche descendante qui permettra de modifier une location, ces deux indices seront initialisés non pas à 0 mais à ceux correspondant au client et à la voiture de la location.

Nous devons aussi prévoir de mettre à jour les deux propriétés à chaque changement de client ou de voiture. Allez dans l'inspecteur d'objets, sélectionnez la combobox cbClients, allez dans l'onglet Événements et cliquez sur les trois points correspondant à l'événement OnChange. Complétez la méthode événementielle créée :

 
Sélectionnez
procedure TNewLeasingForm.cbClientsChange(Sender: TObject);
(* Mise à jour de l'index de l'élément actuellement sélectionné *)
begin
  IndexClient := cbClients.ItemIndex;
end;

Faites de même pour la combobox cbVoitures :

 
Sélectionnez
procedure TNewLeasingForm.cbVoituresChange(Sender: TObject);
(* Changement de voiture : mise à jour de l'index *)
begin
  IndexVoiture := cbVoitures.ItemIndex;
end;

V-F-4. Estimation du prix de la location

Souvenez-vous, nous avons déposé un composant TStaticText, nommé stEstimationPrix. Nous y afficherons le prix de la location. De quoi dépend ce prix :

  • du coût de location journalier (une donnée liée à la voiture) ;
  • de la durée (qui dépend des dates de début et de fin, sur la fiche) ;
  • de l'assurance (case à cocher sur la fiche).

Créons une méthode EstimationPrix, dans la section private de la déclaration de type de la fiche :

 
Sélectionnez
    procedure EstimationPrix;

Voici son implémentation :

 
Sélectionnez
procedure TNewLeasingForm.EstimationPrix;
(* Affichage du prix de location à chaque changement de date ou de voiture *)
var
  LPrix : Real;
begin
  if (Length(deDateDebut.Text) > 0) and (Length(deDateFin.Text) > 0) and (cbVoitures.ItemIndex >= 0)
     then
       begin
         LPrix := (DaysBetween(deDateDebut.Date, deDateFin.Date) + 1) *
                  TCBVoiture(cbVoitures.Items.Objects[cbVoitures.ItemIndex]).Prix;
         if cbAssurance.Checked
            then
              LPrix := LPrix + 10.00;
         stEstimationPrix.Caption := FloatToStr(LPrix);
       end;
end;

La fonction DaysBetween compte le nombre de jours entre deux dates ; pour pouvoir l'utiliser, vous devez ajouter l'unité DateUtils à la clause uses. Nous incrémentons son résultat, car par exemple, le client va payer deux jours de location, et non un seul, s'il rend la voiture le lendemain. Notre entreprise de location n'est pas une société philanthropique, non mais !

Regardez où nous allons rechercher le coût journalier dans les données de la voiture : dans l'objet lié à l'élément.

 
Sélectionnez
TCBVoiture(cbVoitures.Items.Objects[cbVoitures.ItemIndex]).Prix

Réfléchissez : à quel moment le prix de location doit-il être calculé ? À l'affichage de la fiche, vous avez raison. Mais encore ? À chaque fois qu'une autre voiture sera sélectionnée, que la durée de la location aura changé et que l'assurance complémentaire sera cochée ou non. Bigre !

Commençons par compléter la méthode FormShow, pour que le prix soit visible dès l'affichage de la fiche :

 
Sélectionnez
procedure TNewLeasingForm.FormShow(Sender: TObject);
(* Chargement des contrôles *)
begin
  (* Listes des clients et des voitures *)
  DataModule1.ChargementCBClients(cbClients);
  DataModule1.ChargementCBVoitures(cbVoitures, RequeteSelectionVoituresFiltre);
  (* Éléments sélectionnés dans les deux comboboxes *)
  IndexClient := 0;
  IndexVoiture := 0;
  cbClients.ItemIndex := IndexClient;
  cbVoitures.ItemIndex := IndexVoiture;
  // DÉBUT DE L'AJOUT
  (* Estimation du prix *)
  EstimationPrix;
  // FIN DE L'AJOUT
end;

Ensuite, complétons la méthode cbVoituresChange :

 
Sélectionnez
procedure TNewLeasingForm.cbVoituresChange(Sender: TObject);
(* Changement de voiture : mise à jour de l'index et réestimation du prix de location *)
begin
  IndexVoiture := cbVoitures.ItemIndex;
  // DÉBUT DE L'AJOUT
  EstimationPrix;
  // FIN DE L'AJOUT
end;

Réagissons au cochage ou au décochage de la case à cocher de l'assurance complémentaire, en cliquant sur les trois points qui correspondent à l'événement OnChange du composant cbAssurance, dans l'inspecteur d'objets :

 
Sélectionnez
procedure TNewLeasingForm.cbAssuranceChange(Sender: TObject);
(* Changement d'option d'assurance : recalcul du prix de location *)
begin
  EstimationPrix;
end;

Il reste à réagir au changement de date de début ou de fin de location. Nous ne pouvons nous contenter de recalculer le prix de location, nous devons aussi faire en sorte que la date de fin reste au moins égale à la date de début. Si ce n'est pas le cas, il faudra corriger les dates ; nous ne nous casserons pas la tête : la date de début sera simplement recopiée dans la date de fin.

Rappelons-nous qu'à l'affichage du dialogue, les deux dates sont initialisées à la date du jour.

Dans l'inspecteur d'objets, créez une méthode événementielle pour l'événement OnChange des deux composants deDateDebut et deDateFin (voici une des deux, l'autre étant identique) :

 
Sélectionnez
procedure TNewLeasingForm.deDateDebutChange(Sender: TObject);
(* Changement de la durée : test des dates et réestimation du prix de la location *)
begin
  (* Teste préalablement si la date de fin est postérieure à la date de début *)
  if (Length(deDateDebut.Text) > 0) and (CompareDate(deDateDebut.Date,deDateFin.Date) > 0)
     then   (* Erreur : la date de fin devient la date de début *)
       deDateFin.Date := deDateDebut.Date;
  EstimationPrix;
end;

V-F-5. Réaction aux modifications des filtres

À présent, nous devons prévoir de recharger la liste des voitures à chaque fois que l'utilisateur modifie un des filtres.

Les contrôles dont le changement entraîne la mise à jour de la liste des voitures sont :

  • les TSpinEdit seCylindreeMin et seCylindreeMax ;
  • les cases à cocher contenues dans le groupe de boutons radio rgTransmission.

De façon identique pour les deux premiers contrôles cités, dans l'inspecteur d'objets, allez dans l'onglet Événements et cliquez sur les trois points à côté de l'événement OnChange, puis complétez la méthode événementielle comme suit :

 
Sélectionnez
procedure TNewLeasingForm.seCylindreeMinChange(Sender: TObject);
(* Modification du filtre sur la cylindrée : rechargement des voitures *)
begin
  DataModule1.ChargementCBVoitures(cbVoitures, RequeteSelectionVoituresFiltre);
  cbVoitures.ItemIndex := IndexVoiture;
end;

Pour le groupe de boutons radio, c'est en regard de l'événement OnChange de chaque bouton qu'il faut cliquer. Le code de la méthode événementielle est le suivant (par exemple, pour le choix de la transmission automatique (« A ») :

 
Sélectionnez
procedure TNewLeasingForm.rbTransmissionAChange(Sender: TObject);
(* Modification du filtre sur la transmission : rechargement des voitures *)
begin
  DataModule1.ChargementCBVoitures(cbVoitures, RequeteSelectionVoituresFiltre);
  cbVoitures.ItemIndex := IndexVoiture;
end;

V-F-6. Test de disponibilité de la voiture

Avant d'enregistrer une nouvelle location, il faut évidemment s'assurer que la voiture est bien disponible pendant toute la durée de la période souhaitée ! Comment faire ? En créant une requête qui compte toutes les locations de ladite voiture entre les dates de début et de fin. Direction l'unité SQL et l'interface ISQLSyntax, dans laquelle vous ajoutez la méthode suivante :

 
Sélectionnez
    function SelectionVoitureLouee (
      (* Requête de sélection de location correspondant aux critères *)
      const APlaque : String;                  (* Plaque de la voiture *)
      const ADateDebut, ADateFin : TDateTime   (* Dates de début et de fin de location *)
      ) : String;

Cette méthode doit obligatoirement figurer dans la déclaration de la classe TMySQLSyntax, et voici son implémentation :

 
Sélectionnez
function TMySQLSyntax.SelectionVoitureLouee (const APlaque : String;
                                             const ADateDebut, ADateFin : TDateTime) : String;
(* Requête de sélection de location correspondant aux critères *)
begin
  Result := 'SELECT * FROM Locations' +
            ' INNER JOIN Voitures ON Locations.Plaque = Voitures.Plaque' +
            ' INNER JOIN Clients ON Locations.IdClient = Clients.IdClient' +
            ' WHERE Locations.Plaque = ''' + APlaque + '''' +
            ' AND (DateDebut <= ''' + DateToStr(ADateDebut, FormatDate) + ''')' +
            ' AND (DateFin >= ''' + DateToStr(ADateFin, FormatDate) + ''');';
end;

Rien de très spécial dans cette requête, dans laquelle vous remarquez la jointure des tables Voitures et Clients et l'utilisation de la structure FormatDate, qui est, rappelons-le, également déclarée dans l'interface pour définir le format de date propre au système de bases de données.

La nouvelle requête va être utilisée par une fonction qui va compter le nombre d'enregistrements correspondant à la voiture et aux dates de location. Cette fonction est logiquement définie dans le datamodule, et donc prenez la direction de l'unité DataAccess.

Dans la déclaration du datamodule TDataModule1, ajoutez cette fonction :

 
Sélectionnez
    function VoitureDejaLouee (const APlaque : String;
                               const ADateDebut, ADateFin : TDateTime) : Boolean;

Prenons le temps de réfléchir : la requête peut-elle être exécutée par le TSQLQuery correspondant à la table Locations, TSQLQueryMain ? Si nous faisons cela, le TDBGrid de la fenêtre principale, qui contient la liste des locations et qui est lié au SQLQueryMain, va automatiquement être rechargé avec les locations qui répondent à la requête de comptage que nous exécutons. Ce n'est pas ce que nous voulons, ce TDBGrid doit rester inchangé. Voici une autre solution : ajouter un nouveau TSQLQuery au datamodule, que nous appellerons SQLQueryTemp (pour « temporaire »). Je vous laisse faire cela tout(e) seule, vous le faites les yeux fermés à présent !

Il n'y a pas besoin de composant TDataSource, puisque nous ne travaillons qu'avec des contrôles classiques (seuls les composants spécialisés bases de données le requièrent).

Vous avez terminé ? Voici l'implémentation de la fonction VoitureDejaLouee :

 
Sélectionnez
function TDataModule1.VoitureDejaLouee (const APlaque : String;
                                        const ADateDebut, ADateFin : TDateTime) : Boolean;
(* Effectue une requête pour voir si une voiture est louée entre deux dates.
   Le principe est de rechercher les locations de ladite voiture pendant ce laps de temps *)
begin
  SQLQueryTemp.Close;
  SQLQueryTemp.SQL.Text := SQLSyntax.SelectionVoitureLouee(APlaque, ADateFin, ADateDebut);
  SQLQueryTemp.Open;
  Result := SQLQueryTemp.RecordCount > 0;
end;

Le principe est d'appliquer la requête de sélection et de tester le nombre d'enregistrements correspondants (donc, de compter combien de fois la voiture est louée, totalement ou partiellement, entre les dates de début et de fin).

V-F-7. Enregistrement de la location

Exactement comme nous l'avions fait dans la fiche TCarForm (qui permettait de modifier la table des voitures) - et dans la fiche TCustomerForm si vous aviez fait l'exercice proposé - nous créons un champ FEnregistre dans la section strict private, une propriété Enregistre dans la section public et un setter dans la section private :

 
Sélectionnez
  strict private
    // DÉBUT DE L'AJOUT
    FEnregistre : Boolean;
    // FIN DE L'AJOUT
    FIndexClient : Integer;
    FIndexVoiture : Integer;
  private
    // DÉBUT DE L'AJOUT
    procedure SetEnregistre (AValue : Boolean);
    // FIN DE L'AJOUT
    procedure SetIndexClient (AValue : Integer);
    procedure SetIndexVoiture (AValue : Integer);
 
Sélectionnez
  public
    // DÉBUT DE L'AJOUT
    property Enregistre : Boolean read FEnregistre write SetEnregistre;
      (* Indique si les données ont été enregistrées *)
    // FIN DE L'AJOUT
    property IndexClient : Integer read FIndexClient write FIndexClient;
      (* Index du client actuellement sélectionné *)
    property IndexVoiture : Integer read FIndexVoiture write FIndexVoiture;
      (* Index de la voiture actuellement sélectionnée *)

Cette propriété doit être initialisée dès l'affichage de la fiche, dans la méthode FormShow :

 
Sélectionnez
procedure TNewLeasingForm.FormShow(Sender: TObject);
(* Chargement des contrôles *)
begin
  // DÉBUT DE L'AJOUT
  Enregistre := False;
  // FIN DE L'AJOUT
  (* Listes des clients et des voitures *)
  // Etc.
end;

N'oublions pas le code du setter :

 
Sélectionnez
procedure TNewLeasingForm.SetEnregistre (AValue : Boolean);
(* Setter de la propriété Enregistre *)
begin
  if FEnregistre = AValue
     then
       Exit;
  FEnregistre := AValue;
end;

Je vous laisse vous occuper de la création des méthodes correspondant aux deux boutons « Enregistrer » et « Annuler ». Juste leur création, car nous nous nous pencherons attentivement sur leur contenu.

Créez également une méthode FormCloseQuery, qui sera de la même forme que précédemment :

 
Sélectionnez
procedure TNewLeasingForm.FormCloseQuery(Sender: TObject; var CanClose: boolean);
(* Message d'avertissement en cas de fermeture sans sauvegarde *)
begin
  if Enregistre
     then
       CanClose := True
     else
       CanClose := (MessageDlg('Voulez-vous fermer sans enregistrer ?', mtConfirmation, [mbYes, mbNo], 0) = mrYes);
end;

Répondons à présent au clic sur le bouton « Enregistrer ».

Premièrement, avant d'enregistrer nous devons vérifier que la location est possible, grâce à la méthode VoitureDejaLouee que nous avons créée dans le datamodule il y a quelques minutes :

 
Sélectionnez
procedure TUpdateLeasingForm.btnEnregistrerClick(Sender: TObject);
(* Sauvegarde de la location *)
begin
  (* Requête vérifiant que la voiture n'est pas déjà louée aux dates sélectionnées *)
  if DataModule1.VoitureDejaLouee(LocationAModifier.IdLocation, TCBVoiture(cbVoitures.Items.Objects[cbVoitures.ItemIndex]).Plaque, deDateDebut.Date, deDateFin.Date)
     then
       MessageDlg('La voiture est déjà louée pendant cette période', mtError, [mbOk], 0)
     else
       begin
         // Enregistrement
       end;
end;

L'enregistrement proprement dit de la location va se faire dans le datamodule. Ajoutez-y la méthode suivante, en dessous de celles destinées à la sauvegarde des voitures et des clients :

 
Sélectionnez
function SauvegardeLocations (const Requete : String) : Boolean;

Voici son implémentation :

 
Sélectionnez
function TDataModule1.SauvegardeLocations (const Requete : String) : Boolean;
(* Sauvegarde de la table Locations *)
begin
  SQLQueryMain.Close;
  SQLQueryMain.SQL.Clear;
  SQLQueryMain.SQL.Add(Requete);
  SQLQueryMain.ExecSQL;
  Result := Commit;
end;

Il n'y a plus qu'à construire la requête qui est passée comme paramètre, dans l'unité SQL. Il s'agit d'une requête d'insertion :

 
Sélectionnez
    function InsertionLocation (
      (* Requête d'insertion d'une nouvelle location *)
      const AIdClient : Integer;                (* Identificateur du client *)
      const APlaque : String;                   (* Plaque de la voiture *)
      const ADateDebut, ADateFin : TDateTime;   (* Dates de début et de fin *)
      const AAssurance: Boolean                 (* Option d'assurance complémentaire *)
      ) : String;

Dont voici l'implémentation :

 
Sélectionnez
function TMySQLSyntax.InsertionLocation (const AIdClient : Integer;
                                         const APlaque : String;
                                         const ADateDebut, ADateFin : TDateTime;
                                         const AAssurance : Boolean) : String;
(* Requête d'insertion d'une nouvelle location *)
begin
  Result := 'INSERT INTO Locations VALUES (NULL, ''' + IntToStr(AIdClient) + ''', ''' + APlaque + ''', ''' +
            DateToStr(ADateDebut, FormatDate) + ''', ''' +
            DateToStr(ADateFin, FormatDate) + ''', NULL, ';
  if AAssurance
     then
       Result := Result + '1);'
     else
       Result := Result + '0);';
end;

Vous constatez que le client et la voiture sont ajoutés sous la forme des deux clés étrangères IdClient et Plaque.

Nous devons encore compléter la méthode TNewLeasingForm.btnEnregistrerClick :

 
Sélectionnez
procedure TNewLeasingForm.btnEnregistrerClick(Sender: TObject);
(* Sauvegarde de la location *)
begin
  (* Requête vérifiant que la voiture n'est pas déjà louée aux dates sélectionnées *)
  if DataModule1.VoitureDejaLouee(TCBVoiture(cbVoitures.Items.Objects[cbVoitures.ItemIndex]).Plaque, deDateDebut.Date, deDateFin.Date)
     then
       MessageDlg('La voiture est déjà louée pendant cette période', mtError, [mbOk], 0)
     else
       begin
         // DÉBUT DE L'AJOUT
         Enregistre := DataModule1.SauvegardeLocations(SQLSyntax.InsertionLocation(
                       TCBClient(cbClients.Items.Objects[cbClients.ItemIndex]).IdClient,
                       TCBVoiture(cbVoitures.Items.Objects[cbVoitures.ItemIndex]).Plaque,
                       deDateDebut.Date, deDateFin.Date, cbAssurance.Checked));
         Close;
         // FIN DE L'AJOUT
       end;
end;

Une dernière formalité, la réponse au clic sur le bouton « Annuler » :

 
Sélectionnez
procedure TNewLeasingForm.btnAnnulerClick(Sender: TObject);
(* Fermeture du dialogue *)
begin
  Close;
end;

Testez la création de nouvelle locations, mais n'oubliez pas qu'elles seront réellement insérées dans la base de données !

V-F-8. Une classe descendante pour le dialogue de modification de location

En fait, pour modifier une location existante, il n'y a pas besoin de créer de toutes pièces un nouveau dialogue : quelques modifications au dialogue de création de location suffiront. Nous allons donc créer une classe descendante et redéfinir ou ajouter l'une ou l'autre méthode ou propriété.

Faisons le point sur ce que ce dialogue descendant aura de différent par rapport à celui d'origine :

  • au démarrage, il faudra sélectionner le bon client et la bonne voiture, initialiser les dates de la location et l'option d'assurance ;
  • à l'enregistrement, lors de la recherche des locations en cours, il faudra évidemment exclure la location que nous sommes en train de modifier (sinon jamais nous ne pourrons l'enregistrer).

Nous devrons donc redéfinir les méthodes FormShow et btnEnregistrerClick.

Allez-y : dans la section type de l'interface de l'unité Locations, créez la classe descendante TUpdateLeasingForm :

 
Sélectionnez
  TUpdateLeasingForm = class(TNewLeasingForm)
    procedure FormShow(Sender: TObject);
    procedure btnEnregistrerClick(Sender: TObject);
  end;

V-F-8-a. Initialisation des champs

Comment allons-nous transmettre au dialogue les données de la location à modifier ? Un moyen parmi d'autres est sous forme d'un objet similaire à ceux qui servent à stocker les données des voitures et des clients dans les comboboxes, TCBVoiture et TCBClient.

Direction l'unité DataAccess, créez une nouvelle classe TLocation en dessous des deux classes qui viennent d'être citées :

 
Sélectionnez
  TLocation = class
    (* Données initiales d'une location à modifier *)
    strict private
      FIdLocation : Integer;
      FPlaque : String;
      FIdClient : Integer;
      FDateDebut : TDateTime;
      FDateFin : TDateTime;
      FAssurance : Boolean;
    public
      property IdLocation : Integer read FIdLocation;
      property Plaque : String read FPlaque;
      property IdClient : Integer read FIdClient;
      property DateDebut : TDateTime read FDateDebut;
      property DateFin : TDateTime read FDateFin;
      property Assurance : Boolean read FAssurance;
      constructor Create (const AIdLocation : Integer;
                          const APlaque : String;
                          const AIdClient : Integer;
                          const ADateDebut, ADateFin : TDateTime;
                          const AAssurance : Boolean);
  end;

Voici le contenu du constructeur Create :

 
Sélectionnez
constructor TLocation.Create (const AIdLocation : Integer;
                              const APlaque : String;
                              const AIdClient : Integer;
                              const ADateDebut, ADateFin : TDateTime;
                              const AAssurance : Boolean);
(* Initialisation des champs *)
begin
  FIdLocation := AIdLocation;
  FPlaque := APlaque;
  FIdClient := AIdClient;
  FDateDebut := ADateDebut;
  FDateFin := ADateFin;
  FAssurance := AAssurance;
end;

Lors de l'appel du dialogue de modification, une structure de type TLocation, contenant les données de l'application à modifier, sera transmise au dialogue, dans une propriété que nous allons tout de suite créer.

Dans la déclaration du type TUpdateLeasingForm, ajoutez ce qui suit dans les sections strict private et public :

 
Sélectionnez
  TUpdateLeasingForm = class(TNewLeasingForm)
    procedure FormShow(Sender: TObject);
    procedure btnEnregistrerClick(Sender: TObject);
  // DÉBUT DE L'AJOUT
  strict private
    FLocationAModifier : TLocation;
  private
    procedure SetLocationAModifier (AValue : TLocation);
  public
    property LocationAModifier : TLocation read FLocationAModifier write SetLocationAModifier;
      (* Données pour initialiser les contrôles *)
  // FIN DE L'AJOUT
  end;

Pressez Shift-Ctrl-C pour créer le setter et les deux méthodes dans la section implementation. Commençons par le classique setter :

 
Sélectionnez
procedure TUpdateLeasingForm.SetLocationAModifier (AValue : TLocation);
(* Setter de la location à modifier *)
begin
  if FLocationAModifier = AValue
     then
       Exit;
  FLocationAModifier := AValue;
end;

Penchons-nous ensuite sur FormShow.

Tout d'abord, le chargement des comboboxes peut être hérité de la classe parent. La méthode FormShow commencera ainsi :

 
Sélectionnez
  inherited FormShow(Sender);

C'est après que les comboboxes auront été chargées que nous allons y sélectionner le bon client et la bonne voiture, initialiser les dates de location et cocher l'assurance complémentaire s'il y a lieu. Tout cela se trouve, rappelons-le, dans la propriété LocationAModifier, qui aura été initialisée par la fenêtre parent. Voici le code de la méthode complète :

 
Sélectionnez
procedure TUpdateLeasingForm.FormShow(Sender: TObject);
(* Initialisation des contrôles *)
var
  Li : Integer;        (* Indice dans une combobox *)
  LTrouve : Boolean;   (* Voiture ou client à modifier trouvé dans sa combobox *)
begin
  (* Initialisation par défaut des contrôles *)
  inherited FormShow(Sender);
  (* Adaptation des contrôles aux données à modifier *)
  deDateDebut.Date := LocationAModifier.DateDebut;
  deDateFin.Date := LocationAModifier.DateFin;
  if LocationAModifier.Assurance
     then
       cbAssurance.Checked := True;
  LTrouve := False;
  Li := 0;
  while (Li < cbClients.Items.Count) and not LTrouve do
    if TCBClient(cbClients.Items.Objects[Li]).IdClient = LocationAModifier.IdClient
       then
         begin
           cbClients.ItemIndex := Li;
           LTrouve := True;
         end
       else
         Inc(Li);
  LTrouve := False;
  Li := 0;
  while (Li < cbVoitures.Items.Count) and not LTrouve do
    if TCBVoiture(cbVoitures.Items.Objects[Li]).Plaque = LocationAModifier.Plaque
       then
         begin
           cbVoitures.ItemIndex := Li;
           LTrouve := True;
         end
       else
         Inc(Li);
end;

Dans l'unité Main, dans la fiche principale de l'application, voyons tout de suite le code d'exécution du dialogue TUpdateLeasingForm, et spécialement comment l'objet de type TLocation est initialisé et transmis. Cela se fera dans la méthode qui répondra à l'événement OnClick du bouton btnModifier :

 
Sélectionnez
procedure TMainForm.btnModifierClick (Sender : TObject);
(* Modification de la location actuellement sélectionnée *)
var
  LUpdateLeasingForm : TUpdateLeasingForm;   (* Dialogue de modification *)
  LLocationAModifier : TLocation;            (* Données à modifier *)
begin
  LUpdateLeasingForm := TUpdateLeasingForm.Create(Self);
  try
    (* Récolte des données de l'élément à modifier *)
    with DataModule1.SQLQueryMain do
      LLocationAModifier := TLocation.Create(FieldByName('IdLocation').AsInteger,
                                             FieldByName('Plaque').AsString,
                                             FieldByName('IdClient').AsInteger,
                                             FieldByName('DateDebut').AsDateTime,
                                             FieldByName('DateFin').AsDateTime,
                                             FieldByName('Assurance').AsBoolean);
    LUpdateLeasingForm.LocationAModifier := LLocationAModifier;
    (* Exécution du dialogue *)
    LUpdateLeasingForm.ShowModal;
  finally
    FreeAndNil(LUpdateLeasingForm);
    LLocationAModifier.Free;
  end;
  (* Mise à jour de la liste des locations affichée *)
  DataModule1.ChargementLocations(SQLSyntax.SelectionLocationsFiltre(deFiltreDateDebut.Date, deFiltreDateFin.Date,
                                                                     cbFiltreAssurance.Checked, cbFiltreEnCours.Checked));
end;

Vous voyez que l'initialisation des données de la location à modifier est réalisée avant l'affichage du dialogue (avant ShowModal). Les données sont récupérées dans l'enregistrement actuellement sélectionné dans le SQLQuery principal ; les champs sont identifiés à l'aide de la fonction FieldByName et de leur identificateur dans la base de données.

L'objet créé est assigné à la propriété LocationAModifier du dialogue, comme annoncé. Après la fermeture du dialogue, nous n'oublions pas de le détruire.

Notez également que nous n'avons pas oublié de mettre à jour la liste des locations en cours dans la fiche principale, après l'exécution du dialogue.

V-F-8-b. Enregistrement de la location modifiée

Il ne nous reste qu'à apporter des changements à l'enregistrement de la location. Comme nous l'avons dit auparavant, si nous n'excluons pas la location courante de la recherche des locations en cours, jamais nous ne pourrons enregistrer les changements : le message disant que la voiture est déjà louée apparaîtra systématiquement. Pas convaincu(e) ? Faites l'expérience de ne pas redéfinir la méthode btnEnregistrerClick et exécutez le programme. Impossible d'enregistrer !

Il nous faut donc compléter la requête SQL qui sélectionne toutes les locations de la voiture entre les dates de début et de fin. Pour l'instant, cette requête est construite ainsi (dans l'unité SQL) :

 
Sélectionnez
function TMySQLSyntax.SelectionVoitureLouee (const APlaque : String;
                                             const ADateDebut, ADateFin : TDateTime) : String;
(* Requête de sélection de location correspondant aux critères *)
begin
  Result := 'SELECT * FROM Locations' +
            ' INNER JOIN Voitures ON Locations.Plaque = Voitures.Plaque' +
            ' INNER JOIN Clients ON Locations.IdClient = Clients.IdClient' +
            ' WHERE Locations.Plaque = ''' + APlaque + '''' +
            ' AND (DateDebut <= ''' + DateToStr(ADateDebut,FormatDate) + ''')' +
            ' AND (DateFin >= ''' + DateToStr(ADateFin,FormatDate) + ''');';
end;

Il faut ajouter une condition qui teste si le champ IdLocation est différent de celui de la location qui est en train d'être modifiée.

Dans l'interface ISQLSyntax et dans la classe TMySQLSyntax, créez une nouvelle version de la méthode SelectionVoitureLouee :

 
Sélectionnez
    function SelectionVoitureLouee (
      (* Requête de sélection de location correspondant aux critères, excluant la location courante *)
      const AIdLocationAExclure : Integer;     (* Identificateur de la location à exclure *)
      const APlaque : String;                  (* Plaque de la voiture *)
      const ADateDebut, ADateFin : TDateTime   (* Dates de début et de fin de location *)
      ) : String;

Son implémentation est presque identique à l'ancienne version, on y ajoute la nouvelle condition :

 
Sélectionnez
function TMySQLSyntax.SelectionVoitureLouee (const AIdLocationAExclure : Integer;
                                             const APlaque : String;
                                             const ADateDebut, ADateFin : TDateTime) : String;
(* Requête de sélection de location correspondant aux critères, excluant la location courante *)
begin
  Result := 'SELECT * FROM Locations' +
            ' INNER JOIN Voitures ON Locations.Plaque = Voitures.Plaque' +
            ' INNER JOIN Clients ON Locations.IdClient = Clients.IdClient' +
            // DÉBUT DE LA NOUVELLE CONDITION
            ' WHERE Locations.IdLocation != ''' + IntToStr(AIdLocationAExclure) + '''' +
            // FIN DE LA NOUVELLE CONDITION
            ' AND Locations.Plaque = ''' + APlaque + '''' +
            ' AND (DateDebut <= ''' + DateToStr(ADateDebut, FormatDate) + ''')' +
            ' AND (DateFin >= ''' + DateToStr(ADateFin, FormatDate) + ''');';
end;

On utilise l'opérateur « != » issu du C, qui est plus standard que l'opérateur « <> ».

Grâce au nombre différent de paramètres, le compilateur ne risque pas de se tromper entre les deux versions de la méthode, même si elles ont le même nom.

Même chose dans le datamodule, puisque nous allons créer une nouvelle version de la méthode VoitureDejaLouee :

 
Sélectionnez
    function VoitureDejaLouee (const AIdLocationAExclure : Integer;
                               const APlaque : String;
                               const ADateDebut, ADateFin : TDateTime) : Boolean;

Dont voici le code source :

 
Sélectionnez
function TDataModule1.VoitureDejaLouee (const AIdLocationAExclure : Integer;
                                        const APlaque : String;
                                        const ADateDebut, ADateFin : TDateTime) : Boolean;
(* Effectue une requête pour voir si une voiture est louée entre deux dates.
   Le principe est de rechercher les locations de ladite voiture pendant ce laps de temps.
   Version excluant la location en cours de modification *)
begin
  SQLQueryTemp.Close;
  SQLQueryTemp.SQL.Text := SQLSyntax.SelectionVoitureLouee(AIdLocationAExclure, APlaque, ADateFin, ADateDebut);
  SQLQueryTemp.Open;
  Result := SQLQueryTemp.RecordCount > 0;
end;

Il nous reste une ultime requête SQL à créer. Nous avons déjà utilisé les commandes SELECT (pour sélectionner des enregistrements) et INSERT (pour en ajouter de nouveaux) ; pour la mise à jour d'un enregistrement, nous avons besoin de la requête UPDATE.

Dans l'interface ISQLSyntax et dans la classe TMySQLSyntax de l'unité SQL, ajoutez cette méthode :

 
Sélectionnez
    function ModificationLocation (
      (* Requête de modification d'une location *)
      const AIdLocation : Integer;              (* Identificateur de la location modifiée *)
      const AIdClient : Integer;                (* Identificateur du client *)
      const APlaque : String;                   (* Plaque de la voiture *)
      const ADateDebut, ADateFin : TDateTime;   (* Dates de début et de fin *)
      const AAssurance: Boolean                 (* Option d'assurance complémentaire *)
      ) : String;

Voici son implémentation :

 
Sélectionnez
function TMySQLSyntax.ModificationLocation (const AIdLocation, AIdClient : Integer;
                                            const APlaque : String;
                                            const ADateDebut, ADateFin : TDateTime;
                                            const AAssurance : Boolean) : String;
(* Requête de modification d'une location *)
begin
  Result := 'UPDATE Locations SET Plaque = ''' + APlaque + ''', IdClient = ''' + IntToStr(AIdClient) +
            ''', DateDebut = ''' + DateToStr(ADateDebut, FormatDate) +
            ''', DateFin = ''' + DateToStr(ADateFin, FormatDate) + ''', Assurance = ''';
  if AAssurance
     then
       Result := Result + '1'''
     else
       Result := Result + '0''';
  Result := Result + ' WHERE IdLocation = ''' + IntToStr(AIdLocation) + ''';';
end;

Nous avons presque terminé. Voici notre méthode TUpdateLeasingForm.btnEnregistrerClick :

 
Sélectionnez
procedure TUpdateLeasingForm.btnEnregistrerClick(Sender: TObject);
(* Sauvegarde de la location *)
begin
  (* Requête vérifiant que la voiture n'est pas déjà louée aux dates sélectionnées *)
  if DataModule1.VoitureDejaLouee(LocationAModifier.IdLocation, TCBVoiture(cbVoitures.Items.Objects[cbVoitures.ItemIndex]).Plaque, deDateDebut.Date, deDateFin.Date)
     then
       MessageDlg('La voiture est déjà louée pendant cette période', mtError, [mbOk], 0)
     else
       begin
         Enregistre := DataModule1.SauvegardeLocations(SQLSyntax.ModificationLocation(
                       LocationAModifier.IdLocation, TCBClient(cbClients.Items.Objects[cbClients.ItemIndex]).IdClient,
                       TCBVoiture(cbVoitures.Items.Objects[cbVoitures.ItemIndex]).Plaque,
                       deDateDebut.Date, deDateFin.Date, cbAssurance.Checked));
         Close;
       end;
end;

L'identificateur IdLocation est issu de la structure LocationAModifier.

V-F-9. Retour d'une voiture louée

Lorsqu'une voiture louée est restituée, il faut mettre à jour la base de données en ajoutant une date de rentrée à la location.

Sur la fiche principale, ajoutez un bouton « &Rentrée de la voiture », nommé btnRentreeVoiture, en dessous du bouton « Supprimer la location ». Dans l'inspecteur d'objets, cliquez en regard de l'événement OnClick de ce nouveau bouton, pour créer une procédure événementielle que nous compléterons dans quelques minutes.

Pour ne pas compliquer l'application, nous considérons que la date de rentrée de la voiture est la date du jour. Mais vous pouvez, comme exercice, utiliser un dialogue tel que TCalendarDialog pour permettre à l'utilisateur de choisir une date.

Comme il s'agit de la mise à jour d'un enregistrement, la commande SQL que nous devons utiliser est UPDATE. Dans l'unité SQL, créez cette nouvelle méthode :

 
Sélectionnez
    function ModificationLocation (
      (* Requête de modification de la date de rentrée d'une voiture *)
      const AIdLocation : Integer;     (* Identificateur de la location modifiée *)
      const ADateRentree : TDateTime   (* Dates de début et de fin *)
      ) : String;

Elle se différencie de la méthode de même nom existante par ses paramètres. Voici son contenu :

 
Sélectionnez
function TMySQLSyntax.ModificationLocation (const AIdLocation : Integer;
                                            const ADateRentree : TDateTime) : String;
(* Requête de modification de la date de rentrée d'une voiture *)
begin
  Result := 'UPDATE Locations SET DateRentree = ''' + DateToStr(ADateRentree, FormatDate) +
            ''' WHERE IdLocation = ''' + IntToStr(AIdLocation) + ''';';
end;

Complétons la méthode événementielle TMainForm.btnRentreeVoitureClick, dans l'unité Main :

 
Sélectionnez
procedure TMainForm.btnRentreeVoitureClick (Sender : TObject);
(* Ajout de la date de rentrée de la voiture *)
begin
  DataModule1.SauvegardeLocations(SQLSyntax.ModificationLocation(DataModule1.SQLQueryMain.FieldByName('IdLocation').AsInteger,
                                                                 Today));
  (* Mise à jour de la liste des locations affichée *)
  DataModule1.ChargementLocations(SQLSyntax.SelectionLocationsFiltre(deFiltreDateDebut.Date, deFiltreDateFin.Date,
                                                                     cbFiltreAssurance.Checked, cbFiltreEnCours.Checked));
end;

La date du jour est déterminée par la fonction Today, de l'unité DateUtils.

V-F-10. Exercice : supprimer une location

Encore un exercice ?!? Oui, vous devriez être capable de gérer la suppression de la location actuellement sélectionnée dans le DBGrid principal (en réponse à un clic sur le bouton btnSupprimer). Une proposition de solution se trouve dans le code source du projet.

Je vous explique quand même quelle commande SQL vous devez utiliser : DELETE. Votre requête devra ressembler à ceci :

 
Sélectionnez
DELETE FROM Locations WHERE IdLocation = '1234';

Passez votre requête comme paramètre à la méthode TDataModule1.SauvegardeLocations.

Si vous ne savez pas comment retrouver l'identificateur de la location sélectionnée, voici un indice :

 
Cacher/Afficher le codeSélectionnez

V-G. Toujours plus loin : une facture avec LazReport

Attention, n'attendez pas de ce chapitre beaucoup d'explications sur la création d'états avec LazReport. Le but du jeu est de montrer l'interaction des composants avec la base de données. Si vous avez besoin d'explications pour démarrer avec LazReport, je vous conseille ce tutoriel très clair sur la création d'un état simple, écrit par Jean-Paul Humbert.

Avant de commencer, prenons le temps de réfléchir. Le but est de créer une facture ; il s'agira d'une facture sur une seule location à la fois. Si nous lions les composants de LazReport au SQLQuery de la fiche principale, cela va créer des factures pour toutes les locations présentes dans le DBGrid ; la solution est de sélectionner une seule facture dans le SQLQuery temporaire, et donc de lier les composants de LazReport au SQLQueryTemp.

V-G-1. Requête de sélection

Zou, première étape : créer une requête de sélection de la location pour laquelle nous désirons une facture. Dans l'unité SQL, ajoutez la méthode suivante à l'interface ISQLSyntax et à la classe TMySQLSyntax :

 
Sélectionnez
    function SelectionLocationsFiltre (
      (* Requête de sélection d'une location à partir de son identificateur *)
      const AIdLocation : Integer   (* Identificateur de la location *)
      ) : String;

Son implémentation est relativement simple :

 
Sélectionnez
function TMySQLSyntax.SelectionLocationsFiltre (const AIdLocation : Integer) : String;
(* Requête de sélection d'une location à partir de son identificateur *)
begin
  Result := 'SELECT * FROM Locations' +
            ' INNER JOIN Voitures ON Locations.Plaque = Voitures.Plaque' +
            ' INNER JOIN Clients ON Locations.IdClient = Clients.IdClient' +
            ' WHERE IdLocation = ''' + IntToStr(AIdLocation) + ''';';
end;

V-G-2. Composants LazReport

Seconde étape : ajouter les composants LazReport. À quel endroit, d'après vous ? Oui, dans le datamodule, avec les autres composants de connexion à la base de données. Direction l'unité DataAccess, pressez F12 pour afficher le concepteur.

Depuis l'onglet Data Access, déposez un composant TDataSource, que vous renommez DataSourceTemp et que vous liez à SQLQueryTemp. Déposez ensuite, depuis l'onglet LazReport :

  • un composant TfrDBDataSet (le second sur l'onglet de la palette), dont vous liez la propriété DataSet au SQLQueryTemp ;
  • un composant TfrReport (le tout premier), dont vous liez la propriété DataSet au frDBDataSet1 que vous venez de déposer.

V-G-3. Conception du rapport

Il s'agit à présent de faire la maquette de la facture. Faites un double-clic sur le composant frReport1, une nouvelle fenêtre s'affiche :

Image non disponible

C'est le concepteur de rapport de LazReport.

Créons une en-tête pour la facture, en cliquant sur le tout petit rectangle entouré de pointillés dans la colonne de gauche. Cliquez ensuite en haut et à gauche de la page vide : un rectangle pointillé se dépose et LazReport vous demande de quel type de bande il s'agit. Comme proposé automatiquement, il s'agit du « Titre du rapport » :

Image non disponible

Quand vous avez cliqué sur OK, une (horrible) zone hachurée s'est placée en haut de la page :

Image non disponible

Cliquez ensuite sur la première icône (rectangulaire) de la colonne de gauche, pour placer une zone de texte dans la zone hachurée. Tout de suite, un dialogue vous permet d'écrire du texte :

Image non disponible

Inscrivez « FACTURE ». À l'aide des poignées vertes de la zone de texte, centrez le rectangle. Centrez aussi le texte et augmentez la taille de la police, par exemple 22 :

Image non disponible

De la même manière, créez une zone hachurée en-dessous de l'en-tête, en choisissant « Données principales » comme type de bande. Un petit dialogue vous demande de sélectionner la source de données : choisissez frDBDataSet1.

Image non disponible

À l'aide de la poignée verte inférieure, augmentez la hauteur de la zone hachurée.

V-G-3-a. Données statiques

Ajoutez une zone de texte et mettez-y comme titre (taille 14) « Identité du client : ». Pour voir le texte, vous devrez peut-être augmenter la taille du rectangle. En dessous, ajoutez encore une zone dans laquelle vous inscrivez :

 
Sélectionnez
[SQLQueryTemp."NomPrenom"]
[SQLQueryTemp."Rue"] [SQLQueryTemp."Numero"]
[SQLQueryTemp."CodePostal"] [SQLQueryTemp."Localite"]

La taille du texte peut être fixée à 10 et l'alignement à gauche.

Les champs du SQLQuery sont mis entre crochets ; c'est leur valeur qui sera affichée. Vous allez toute de suite voir que l'on peut mélanger du texte simple avec les champs.

Image non disponible

Créez un second titre « Véhicule loué : », puis un cadre sur toute la largeur, contenant :

 
Sélectionnez
[SQLQueryTemp."Marque"] [SQLQueryTemp."Modele"]
Prix par jour : [SQLQueryTemp."Prix"] €

Puis créez de nouveau un titre « Location : », et encore un autre cadre sur toute la largeur :

 
Sélectionnez
Date de début : [SQLQueryTemp."DateDebut"]
Date de fin prévue : [SQLQueryTemp."DateFin"]
Rentrée du véhicule : [SQLQueryTemp."DateRentree"]
Assurance complémentaire : [SQLQueryTemp."Assurance"]

V-G-3-b. Données calculées

Passons maintenant au plus important pour notre société de location : le pognon ! Ajoutez un quatrième titre « À payer : », puis un rectangle sur toute la largeur.

LazReport possède des fonctions qui permettent de faire des opérations sur des champs de base de données et/ou sur des variables. Malheureusement, il n'existe pas de fonction permettant de calculer le nombre de jours entre les dates de début et de fin de location ; nous allons devoir fournir ce nombre.

Allez dans le menu Fichier du concepteur et choisissez Liste des variables :

Image non disponible

Cliquez sur le bouton Variables. Dans le dialogue qui suit, inscrivez comme titre « Facture variables » et, à la ligne suivante, commençant par une espace, « Duree » :

Image non disponible

Cliquez deux fois sur OK.

Double-cliquez sur le tout dernier rectangle que vous avez créé sur la largeur de la page, et inscrivez :

 
Sélectionnez
[Duree] jours * [SQLQueryTemp.Prix] € = [[Duree]*[SQLQueryTemp.Prix]] €

C'est à notre application de fournir la valeur de la variable Duree, sur demande de LazReport. Fermez le concepteur de rapport et enregistrez la maquette dans le répertoire source de notre application, sous le nom Facture1.lrf. Dans l'inspecteur d'objets de l'unité DataAccess, sur le composant frReport1, cherchez l'événement OnGetValue. Cliquez sur les trois points qui l'accompagnent, pour créer une méthode événementielle frReport1GetValue.

Voici son contenu :

 
Sélectionnez
procedure TDataModule1.frReport1GetValue (const ParName : String; var ParValue : Variant);
(* Calcul de la variable Duree sur demande de LazReport *)
begin
  if UpperCase(ParName) = 'DUREE'
     then
       if SQLQueryTemp.FieldByName('DateRentree').AsString <> ''
          then   (* Calcul sur la date de rentrée *)
            ParValue := DaysBetween(SQLQueryTemp.FieldByName('DateDebut').AsDateTime,
                                    SQLQueryTemp.FieldByName('DateRentree').AsDateTime) + 1
          else   (* Pas de date de rentrée : calcul sur la date de fin prévue *)
            ParValue := DaysBetween(SQLQueryTemp.FieldByName('DateDebut').AsDateTime,
                                    SQLQueryTemp.FieldByName('DateFin').AsDateTime) + 1;
end;

Si la date de rentrée de la voiture est vide, le calcul est fait sur la date de fin de location prévue.

Ce n'est pas tout : si le client a opté pour l'assurance complémentaire, la somme de 10 € est ajoutée au total. Cette donnée étant un booléen, elle est stockée comme une valeur entière 0 ou 1 dans la base de données. C'est parfait pour nous : la somme à éventuellement ajouter au total sera de 10 multiplié par 0 ou 1. Nous complétons ainsi le contenu de notre rectangle :

 
Sélectionnez
Base : [Duree] jours * [SQLQueryTemp.Prix] € = [[Duree]*[SQLQueryTemp.Prix]] €
Assurance : [10*[SQLQueryTemp."Assurance"]] €
Total : [[Duree]*[SQLQueryTemp.Prix]+10*[SQLQueryTemp."Assurance"]] €

V-G-3-c. Données automatiques

Pour terminer, nous allons vite voir comment rajouter la date du jour dans le rapport. Augmentez la hauteur de la zone hachurée de titre (descendez au besoin la zone hachurée en dessous) et ajoutez un rectangle de texte centré en dessous du titre « FACTURE ». Inscrivez dans ce rectangle :

 
Sélectionnez
Date : [DATE]

Facile, non ?

Voici donc la maquette de facture complète :

Image non disponible

V-G-4. Code de création de la facture

Nous allons voir si nous avons bien travaillé. Sur la fiche principale, en dessous du bouton « rentrée de la voiture », ajoutez un dernier bouton « &Facture », nommé btnFacture. Le code de la méthode événementielle qui correspond à OnClick est le suivant :

 
Sélectionnez
procedure TMainForm.btnFactureClick (Sender : TObject);
(* Création d'une facture *)
begin
  DataModule1.CreerFacture;
end;

C'est en effet logiquement dans le datamodule que va figurer le code de création de la facture. Rendez-vous dans l'unité DataAccess.

Dans la section public de la déclaration du type TDataModule1, ajoutez cette méthode :

 
Sélectionnez
    procedure CreerFacture;

La combinaison de touches Shift-Ctrl-C, comme d'habitude, crée son implémentation :

 
Sélectionnez
procedure TDataModule1.CreerFacture;
(* Remplit l'état Facture1 avec les champs de la location sélectionnée *)
begin
  (* Sélection de la location à facturer dans le SQLQueryTemp *)
  SQLQueryTemp.Close;
  SQLQueryTemp.SQL.Text := SQLSyntax.SelectionLocationsFiltre(SQLQueryMain.FieldByName('IdLocation').AsInteger);
  SQLQueryTemp.Open;
  (* Création du rapport *)
  frReport1.LoadFromFile('Facture1.lrf');
  frReport1.PrepareReport;
  frReport1.ShowReport;
end;

La requête de sélection initialise le SQLQueryTemp avec les données de la location à facturer, et ce SQLQuery sert de source de données au rapport.

Et voici enfin une facture générée par notre application :

Image non disponible

Notre application est terminée ! C'est le moment de verser une petite larme, car nous avons bien travaillé. Vous pouvez être fier(e) de vous !

V-H. Code complet de l'exemple 3

Téléchargez le code source complet de l'application ici.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2017 Alcatîz. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.