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 :
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.
Cliquez sur OK. Ouvrez l'inspecteur de projet, par le biais du menu Projet, Inspecteur de projet :
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 :
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 :
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 :
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 :
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 :
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 :
Un dialogue permettra de gérer la liste des voitures disponibles :
Un autre, similaire, sera consacré à la liste des clients :
Dans un dialogue, on pourra créer une nouvelle location :
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 « + » :
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.
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 :
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) :
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 » :
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 :
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 :
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 :
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 :
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 :
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 :
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) :
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 :
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.
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 :
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 ».
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 :
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 :
Le petit message nous informe que la connexion à la base de données est couronnée de succès :
La fenêtre principale est bien vide pour l'instant (nous allons rapidement la remplir), juste le menu principal :
Dans le menu Fichier, vous choisissez l'item Gérer les voitures. La fiche que nous voulons tester apparaît :
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 :
type
ISQLSyntax = interface
end
;
À l'aide de la combinaison de touches Shift-Ctrl-G, créez automatiquement un GUID :
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 :
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 :
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 :
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é :
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 :
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) :
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 :
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 |
|
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) :
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 :
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 :
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 :
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 :
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 :
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 :
procedure
ChargementLocations (const
Requete : String
);
Voici son implémentation :
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 :
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 :
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 !
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 :
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 :
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 :
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 :
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 :
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) :
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 :
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 :
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 :
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 :
procedure
ChargementClients (const
Requete : String
);
procedure
ChargementVoitures (const
Requete : String
);
Et leur implémentation :
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) :
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 :
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 :
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 :
function
RequeteSelectionVoituresFiltre : String
;
Voici son implémentation :
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 :
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 :
function
SelectionVoituresFiltre (const
ACylindreeMin, ACylindreeMax : Integer
;
const
ATransmission : String
) : String
;
Un petit Shift-Ctrl-C puis complétez :
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 :
strict
private
FIndexClient : Integer
;
FIndexVoiture : Integer
;
Dans la section public, créez les deux propriétés suivantes :
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 :
private
procedure
SetIndexClient (AValue : Integer
);
procedure
SetIndexVoiture (AValue : Integer
);
Voici le code source des setters :
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 :
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 :
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 :
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 :
procedure
EstimationPrix;
Voici son implémentation :
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.
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 :
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 :
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 :
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) :
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 :
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 ») :
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 :
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 :
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 :
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 :
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 :
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
);
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 :
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 :
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 :
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 :
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 :
function
SauvegardeLocations (const
Requete : String
) : Boolean
;
Voici son implémentation :
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 :
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 :
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 :
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 » :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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) :
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 :
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 :
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 :
function
VoitureDejaLouee (const
AIdLocationAExclure : Integer
;
const
APlaque : String
;
const
ADateDebut, ADateFin : TDateTime) : Boolean
;
Dont voici le code source :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 :
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 » :
Quand vous avez cliqué sur OK, une (horrible) zone hachurée s'est placée en haut de la page :
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 :
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 :
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.
À 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 :
[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.
Créez un second titre « Véhicule loué : », puis un cadre sur toute la largeur, contenant :
[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 :
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 :
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 » :
Cliquez deux fois sur OK.
Double-cliquez sur le tout dernier rectangle que vous avez créé sur la largeur de la page, et inscrivez :
[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 :
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 :
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 :
Date : [DATE]
Facile, non ?
Voici donc la maquette de facture complète :
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 :
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 :
procedure
CreerFacture;
La combinaison de touches Shift-Ctrl-C, comme d'habitude, crée son implémentation :
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 :
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.