Concevoir une liste qui filtre les enregistrements selon le contenu d'une zone de texte

Jean-Philippe AMBROSINO

Ce document a pour but de vous montrer comment concevoir une liste qui voit son contenu filtré en fonction de ce qui est inscrit dans une zone de texte.
Vous devez être relativement à l'aise avec Microsoft Access et connaître la conception de formulaires, mais également avoir une bonne approche du langage Visual Basic for Application afin mettre en pratique cet exemple.

Commentez ♪

- Titre : Concevoir une liste qui filtre les enregistrements selon le contenu d'une zone de texte

- Auteur : Jean-Philippe AMBROSINO

- Parution : 20 septembre 2011

- Mise à jour : 8 avril 2014

- Licence : Ce document est issu de https://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.

Sommaire

- 1. Avant-propos

- 1-1. Niveau

- 1-2. Contact

- 2. Présentation du projet

- 2-1. Création de la table

- 2-1-1. Vue de la table en mode création

- 2-2. Remplissage des tables

- 2-2-1. Vue de la table en mode feuille de données

- 3. Création du formulaire

- 3-1. Affectation des propriétés aux contrôles

- 3-1-1. Propriétés de la liste des communes

- 3-1-2. Les propriétés des autres contrôles

- 3-1-3. Disposition des contrôles

- 3-1-4. Mise en place du code événement

- 3-1-4-1. Description des événements du projet

- 3-1-5. Rédaction du code du formulaire

- 4. Mise en exécution

- 4-1. Affichage en mode formulaire

- 4-2. Ajout d'une ville...

- 4-3. Modification d'une ville

- 5. Conclusion

- 6. Sujets corrélatifs

- 7. Remerciements

1 - Avant-propos

Ce document a pour but de vous montrer comment concevoir une liste qui voit son contenu filtré en fonction de ce qui est inscrit dans une zone de texte.

Vous devez être relativement à l'aise avec Microsoft Access et connaître la conception de formulaires, mais également la manipulation des requêtes avec Visual Basic en ayant une bonne approche de ce langage afin de mettre en pratique cet exemple

1-1 - Niveau

Ce tutoriel s'adresse plus particulièrement aux personnes qui possèdent déjà des notions concernant la gestion des événements avec Visual Basic sur Microsoft Access incluant la manipulation des données à l'aide DAO.

1-2 - Contact

Pour tous renseignements complémentaires, veuillez me contacter directement (Argyronet) par MP.
Si votre question est disons publique, merci de poster un message dans le forum afin d'en faire partager le contenu...

2 - Présentation du projet

Dans la série des listes, il y a eu le tutoriel vous permettant de concevoir une liste qui en alimente une autre...

Au regard du succès de ce dernier, par les commentaires que j'ai pu lire d'une part et par les mails que j'ai reçus d'autre part, je me suis laissé tenter de vous faire partager cette nouvelle aventure au sein d'une... autre liste.
Surprenant, non ?

Cette fois-ci, le petit projet permet de comprendre comment mettre en œuvre la possibilité d'obtenir les enregistrements filtrés dynamiquement au fur et à mesure que des caractères sont saisis dans une zone de texte.

Pourquoi un tel projet ? Eh bien parce que dans une zone de liste (non déroulante), il n'y a pas possibilité de taper des caractères pour localiser l'occurrence cherchée...
Il existe cependant une possibilité alternative qui vise à employer l'API SendMessage() accompagnée de la constante LB_FINDSTRING mais cette méthode elle aussi nécessite un contrôle Texbox et utilise le Handle du contrôle, chose qui reste indisponible dans les contrôles zone de liste d'Access...
Mais nous allons faire en sorte d'obtenir la même chose ici.

Dans le projet présenté, j'ai volontairement décortiqué le module et son formulaire en l'extirpant de son projet initial pour vous en faire bénéficier.

Je précise qu'un peu de peaufinage a été mis en application dans la conception du formulaire afin que cela vous ouvre des idées similaires au sujet même si l'objectif se limite à la compréhension du phénomène de pseudocommunication entre deux contrôles : - une Zone de liste déroulante et une Zone de texte via une procédure VBA traduite à travers différents événements.

Pour mettre en œuvre ce projet, j'ai conservé la table d'origine qui contient, selon ce que j'avais pu récupérer à l'époque, l'ensemble des villes et codes postaux correspondants.
Je n'ai plus la source en tête, mais une chose est sûre, c'est la toile qui me l'a fournie.

Note:

Ce tutoriel peut, dans son ensemble, être mis en application pour toutes les versions d'Access. Il est présenté ici sous Access 2010 (14)
Toutefois, certaines méthodes seront inexistantes dans les versions antérieures à la version 97.

2-1 - Création de la table

Le projet ne comporte qu'une seule table. Celle-ci contient l'ensemble des villes et leur code postal, le tout associé à un champ compteur (Numéro Auto). En effet, des mêmes codes postaux peuvent être affiliés à plusieurs villes et inversement.

La table est composée de trois champs :

- le 1er champ définit l'identifiant de l'association Code postal + Ville et sera la clé primaire ; pour plus de commodité, nous appellerons ce champ IDPostalCode.

Le préfixe ID définit le fait que ce champ est l'identifiant de l'enregistrement ;

- Le 2e champ PostalCode définit le numéro du code postal sur 5 caractères (France) ;

- Le 3e champ Town définit le nom de la commune sur 100 caractères (suffisant pour le nom).

Pour les champs PostalCode et Town, affectez-leur un index avec doublon.... Ce point est primordial
N'attribuez pas d'autres propriétés à la table que celles affectées par défaut pour ces champs.
En effet, le code régi dans le formulaire n'impacte pas la table directement, en tout cas pour ce qui concerne le sujet de ce tutoriel.

Par principe, ne mettez jamais d'accents ni d'espaces dans le nom des champs. C'est pourquoi je nomme toujours mes champs en anglais.
Pour plus d'information sur la convention de nommage des objets, vous pouvez lire ce tutoriel.

Vous enregistrez la table sous le nom TBLPostalCodes.

2-1-1 - Vue de la table en mode création

La table en mode création une fois les champs créés et la table enregistrée.

Structure de la table

2-2 - Remplissage des tables

Pour pouvoir tester le bon fonctionnement de ce projet, vous devez entrer un jeu de codes postaux et les communes qui leur correspondent. Vous pourrez en récupérer la liste à la fin de ce tutoriel.

Le champ de clé primaire s'incrémente de lui-même par sa propriété Numéro Auto.

Pour la petite histoire, j'ai eu à poser ce type de champ pour la communication entre tables, parce qu'il il était plus commode de chercher une association Code postal + Ville par cet identifiant que par l'association sous forme de chaîne de caractères telle que l'imposent les contenus respectifs des deux champs de données.
En effet, un code postal autant qu'une ville peut être affecté à plusieurs correspondances identiques ce qui contraint d'établir une condition de recherche (WHERE) avec les deux occurrences :

Town = "????" AND PostalCode = "?????"

2-2-1 - Vue de la table en mode feuille de données

La table en mode feuille de données une fois les enregistrements saisis ou collés.

La table en mode feuille de données

Note : Le numéro de la clé primaire n'a rien de commun avec les codes postaux...

3 - Création du formulaire

Vous créez donc un nouveau formulaire vide (Mode création) où vous y posez quelques contrôles entre autres zones de liste modifiable (ComboBox), étiquettes (Label) et boutons de commande (CommandButton)...

Le formulaire en mode création (fini)

De haut en bas :

1/ vous dessinez un jeu de deux étiquettes superposées décalées d'un pixel que vous nommez lblTitle1 et lblTitle2 et contenant le titre du formulaire en Times New Roman 20 Gras italique avec une couleur claire pour l'étiquette de premier plan et une couleur foncée pour celle de l'arrière-plan.

Le mieux pour créer ce type de titre est d'en dessiner un seul, de lui affecter les propriétés voulues et enfin de le dupliquer. Une fois dupliqué, vous le superposez après l'avoir nommé et attribué sa couleur.

2°) vous dessinez une autre étiquette qui stipule succinctement le mode opératoire et que vous nommez lblInfo ;

3°) vous dessinez au-dessous une zone de texte et que vous nommez txtLookup ;

4°) juxtaposée à cette zone de texte, vous dessinez un bouton (Berci Saint-Exupéry) que vous nommez cmdEraseEntry.
Vous lui affectez une image idoine. Celle qui est illustrée a été piochée dans la liste des images au niveau de la propriété de même nom pour ce contrôle ;

5°) de façon facultative, vous dessinez les deux séparateurs rouges : l'un horizontal et l'autre vertical ;

6°) vous dessinez au-dessous dans des proportions adaptées la zone de liste que vous nommez lboPCTowns ;
C'est ce contrôle qui va recevoir la liste des villes et codes postaux associés. Vous définissez sa taille en cm selon la police que vous lui affecterez ; ainsi, pour du Tahoma 12, une taille de 9,2 cm x 5 cm est bien proportionnée.
Vous pouvez la « relooker » en lui attribuant un effet 3D...

D'un point de vue ergonomique, si vous attribuez un effet 3D enfoncé à un contrôle, arrangez-vous pour que les autres qui sont prédominants subissent le même sort.

7°) vous dessinez au-dessous et de la même longueur, une étiquette que vous nommez lblCountTowns.
Celle-ci est chargée d'afficher le nombre d'occurrences trouvées qu'il y ait recherche filtrée ou non.
À l'intérieur de ce contrôle qui se voit attribuer un effet 3D enfoncé, vous dessinez deux miniboutons qui serviront respectivement à modifier une ville et son code postal ou à ajouter ces deux éléments dans la base...

En effet, il n'est pas obligatoire d'avoir toutes les villes listées dans la table. Au départ, ce projet ne ciblait que les personnes habitant dans le 92 donc l'utilisateur m'avait demandé de ne mettre que les communes qui jouxtent celle de son lieu d'utilisation de l'application et c'est pourquoi j'avais pris l'initiative d'ajouter ces contrôles.
Dans la mesure où vous incorporez toutes les villes de France, il est inutile de dessiner ces boutons.

8°) vous dessinez plus bas un rectangle en 3D relevé avec une faible hauteur que vous nommez shpSeparator ;
Il sert juste de séparateur entre le haut et le bas. Euh..., c'est juste pour faire joli, mais d'après moi, bien entendu.

9°) vous dessinez alors au-dessous de ce trait un autre rectangle en 3D enfoncé que vous nommez shpButtons ;
C'est dans ce rectangle que nous allons dessiner trois boutons de commande...

10°) vous dessinez les trois boutons que vous nommez et intitulez de gauche à droite :

- cmdClose : &Fermer
- cmdInvert : Ville / C&ode postal
- cmdValid : &Valider

Notez le symbole & jouxtant un caractère de la légende (Caption).
Ce caractère est utilisé pour définir la touche d'accès rapide lorsque celle-ci est combinée à la touche ALT.

Ces trois boutons ont pour rôle respectif :

- cmdClose : fermer la fenêtre sans autre action - code qui équivaut à l'action Annuler
- cmdInvert : inverser l'ordre des colonnes de la liste Ville / Code postal et Code postal / Ville
- cmdValid : valider la sélection : initialement, ce formulaire était voué à appliquer au formulaire appelant la valeur sélectionnée dans la liste pour renseigner la ville et le code postal du candidat.

Rappelez-vous la clé primaire... IDPostalCode
Il est évident que transporter les informations souhaitées d'un enregistrement via sa clé d'identité est somme toute plus simple.
S'il n'y en avait pas eu, il aurait fallu alors détecter l'ordre des colonnes choisies d'une part et transporter les deux éléments d'autre part dans des variables distinctes ou sous forme de tableau pour que leur valeur puisse s'insérer dans les zones voulues.

Embellissement du formulaire

Vous pouvez remarquer le dégradé en arrière-plan de formulaire exemple. Il s'agit simplement d'une image appliquée en mosaïque.
Pour le reste, arrangez un peu la disposition des contrôles pour que tout soit aligné et proportionnel.

C'en est fini de la conception du formulaire ; maintenant, nous allons attaquer la partie propriétés pour les contrôles.

Vérifiez une dernière fois que vous avez bien nommé les contrôles. Cela est important... Euh... Oui, je sais, je l'écris à chaque fois dans mes tutoriels, mais c'est juste pour que ça devienne un réflexe, surtout pour les nouveaux.

3-1 - Affectation des propriétés aux contrôles

Seule la zone de liste reçoit des propriétés initiales... Bien que celles-ci pourraient être définies par code, je vous montre ici l'alternative de manière à ce que vous distinguiez bien la différence entre un contrôle nanti de ses propriétés définies par l'utilisateur et celles définies dynamiquement par le code VBA en fonction d'une situation donnée.

Dans le contexte, la liste est censée afficher des codes postaux et des villes...
Comme il semble qu'il soit plus aisé de retenir un nom de commune plutôt qu'un code postal, dans le sens abstrait du terme, j'ai délibérément choisi d'afficher par défaut et donc au chargement du formulaire, la liste avec les communes à gauche et le code postal en face, à droite, donc.

Cela se traduit par une requête que vous allez créer et nommer qry_LBOListTownsCP avec les paramètres suivants :
ID | Ville | Code postal avec ordre de tri sur la ville

Requête qry_LBOListTownsCP

SELECT IDPostalCode, Town, PostalCode
FROM TBLPostalCodes
ORDER BY Town;

En parallèle à cela et dans le même élan, puisque vous y êtes, créez également la requête inverse, cette fois-ci nommée qry_LBOListCPTowns, avec les paramètres suivants :
ID | Code postal | Ville avec ordre de tri sur le code postal

Requête qry_LBOListCPTowns

SELECT IDPostalCode, PostalCode, Town
FROM TBLPostalCodes
ORDER BY PostalCode;

Ainsi, et selon vos préférences, vous pourrez indifféremment affecter la requête souhaitée à votre liste sans oublier de changer la légende du bouton cmdInvert selon le cas.

En effet, vous allez affecter à la zone de liste la requête qui liste les communes et codes postaux (dans cet ordre).

3-1-1 - Propriétés de la liste des communes

Vous attribuerez aux propriétés :

- Origine source : Table/Requête
- Contenu : qry_LBOListTownsCP
- Nbre de colonnes : 3
- Largeurs colonnes : 0cm;7cm;1,6cm;
- Colonne liée : 3

Explications :

- 0cm pour la 1re colonne qui est la clé primaire et que nous ne voulons pas voir apparaître
- 1,6cm pour la 2e colonne réservé au code postal est suffisant pour les cinq caractères ; apparaître dans la liste déroulante ;
- 7cm pour la 3e colonne réservés au nom de la commune (certaines sont vraiment longues en matière de nombre de caractères).

Remarque : l'ordre 1,6 cm et 7 cm est inversé lors de l'inversion provoquée par le clic du bouton...

Lorsque vous prenez l'Assistant Création Contrôle et que vous lui demandez de vous aider dans la conception d'une zone de liste déroulante
sur telle table ou telle requête, il vous propose par défaut de masquer la colonne numéro 1 (si elle est définie comme primaire) puisqu'il est sous-entendu (normalisation oblige),
que vous avez effectivement mis un identifiant UNIQUE pour représenter chacun des enregistrements de la table source.

Bien entendu, cela est le cas si votre table possède au moins deux champs tels que cela est conçu dans la table de ce projet.

Il en est de même si vous construisez le contrôle manuellement…
Il vous appartiendra alors d'ajuster la taille des colonnes en conséquence...

Affectation des propriétés au contrôle

3-1-2 - Les propriétés des autres contrôles

Toutes les propriétés des autres contrôles restent celles par défaut à l'exception des boutons cmdUpdate et cmdValid qui ont leur propriété Activé (Enabled) définie à Non (False).
Leur état sera modifié dynamiquement dans le code en fonction de ce qui se passe dans le formulaire.

3-1-3 - Disposition des contrôles

Nous avons vu en début de paragraphe un aperçu d'une disposition qui reste avant tout une suggestion.
Ayant quelques (bonnes) années de pratique sur le sujet, je me suis permis de vous proposer ce look mais rien ne vous empêche d'agencer ce formulaire comme bon vous semble.

Ordre de tabulation Dans ce formulaire, vous devez affecter à la propriété Arrêt tabulation (TabStop) la valeur Oui pour les contrôles de zone de texte (Textbox), zone de liste (Listbox) et pour les trois boutons du bas (CommandButton).
L'ordre sera alors 0 pour la zone de texte, 1 pour la zone de liste et de 2 à 4 pour les trois boutons en partant de la droite vers la gauche...
Il n'est en effet pas ou peu utile de donner les focus par la touche TAB aux autres contrôles du fait qu'il n'y a pas lieu d'intervenir dessus en utilisation disons standard.

Exécution du formulaire en l'état

Vous pouvez passer en mode formulaire (ou appuyer sur F5) pour voir si tout concorde et si la liste est bien alimentée par l'ensemble des villes.

Cela doit ressembler à :

Mode formulaire (premier jet)

Vu qu'aucun code n'a été affecté au formulaire et ses contrôles, il est évident que rien ne se passe lorsque vous écrivez dans la zone de texte...

3-1-4 - Mise en place du code événement

Pour affecter des événements aux différents contrôles, différentes solutions sont possibles...

Tout d'abord...
Pour mettre en place le code événement, il se peut que vos options ne soient pas définies pour attaquer à chaque fois l'éditeur Visual Basic (VBE)...
Aussi, pour rendre plus confortable la rédaction du code, je vous invite à forcer l'usage de procédures événementielles pour chaque événement et éviter l'apparition systématique de cette boîte de dialogue :

Veuillez activer les options d'Access et cocher la case appropriée dans la rubrique concernée : la rubrique est intitulée :
"Toujours utiliser les procédures événementielles"...

Option des événements

Passer dans l'éditeur Visual Basic

À l'extérieur du formulaire tout en restant dans la fenêtre de celui-ci, effectuez un clic droit et choisissez la commande : "Créer code événement…"

Accéder à VBE

Dans l'éditeur Visual Basic, une fenêtre blanche va vous proposer d'écrire un bout de code correspondant à l'événement attaché au chargement du formulaire puisque le fait d'effectuer un clic droit sur la fenêtre correspond à ce dernier.

Option Explicit
Option Compare Database

Private Sub Form_Load()

End Sub

Cet événement s'appelle Form_Load qui de par sa traduction signifie ce qu'il fait...

Pour plus d'informations sur les événements des contrôles et autres objets, veuillez vous référer à l'aide en ligne en mettant en surbrillance l'événement souhaité et appuyez sur la touche F1.

Rappelez-vous qu'un événement est quelque chose qui se passe lorsque l'utilisateur effectue une certaine opération "avant", "pendant", "après", etc. sur tel ou tel contrôle.

Il faut que vous sachiez qu'un contrôle de type zone de liste ne possède pas les mêmes événements d'un contrôle bouton de commande par exemple et pour autant, ils ont des événements communs.
Il vous appartiendra alors de choisir l'événement le plus approprié lorsque tel ou tel contrôle est utilisé dans votre formulaire.

3-1-4-1 - Description des événements du projet

Pour ne rien vous cacher, la page de code Visual Basic pour ce formulaire contient grosso modo 300 lignes de code. Mais je n'ai volontairement pas laissé toutes les fonctions telles qu'elles existaient, car certaines d'entre elles ne pouvaient pas s'appliquer à vos développements existants.
Bref, le code est assez simple en soi à comprendre, mais il faut tout de même avoir déjà abordé ce langage car en plus d'être dommage, il est plutôt frustrant de copier-coller du code sans comprendre à quoi il sert et ce qu'il fait.

Liste des procédures et fonctions de la classe de formulaire

- cmdAdd_Click() Appelle AddNewTown pour ajouter une ville

- cmdClose_Click() Ferme le formulaire

- cmdEraseEntry_Click() Permet d'effacer la zone de texte AddNewTown

- cmdInvert_Click() Permet d'inverser la sélection Ville/Code postal - Code postal/Ville

- cmdUpdate_Click() Permet de modifier une ville ou un code postal

- cmdValid_Click() Permet de valider la commune sélectionnée pour la déposer dans un formulaire appelant

- CountTownFound() Fonction qui renseigne le nombre de communes trouvées (selon le critère)

- Form_Close() Événement Close (Sur fermeture) du formulaire

- Form_KeyPress() Événement KeyPress (Sur touche activée) du formulaire

- Form_Load() Événement Load (Sur chargement) du formulaire

- lboPCTowns_Click() Événement Click (Clic) de la zone de liste

- txtLookup_KeyPress() Événement KeyPress (Sur touche activée) de la zone de texte

- AddNewTown() : Procédure permettant d'ajouter une ville dans la base

- SetListOfCityProperties() Procédure qui définit les propriétés de la zone de liste

- ThisTownExists() Fonction qui vérifie l'existence d'une commune

- RemoveCharAccents() Fonction qui substitue les accents d'un chaîne de caractères

- UpdateTownList() Procédure qui rafraîchit la liste des communes en fonction d'un critère

La liste ci-dessus représente l'ensemble des procédures (événementielles ou non) qui sont appliquées au formulaire...

3-1-5 - Rédaction du code du formulaire

Voici l'ensemble des blocs de code de la classe du formulaire :

Code complet du module

'**********************************************************************
' Module            : Form_frmTowns
' Type              : Document VBA
' DateTime          : 20/09/2011
' Author            : Jean-Philippe AMBROSINO
' Review            : Jean-Philippe AMBROSINO
' Purpose           : Module de sélection d'une ville selon un critère
'
''**********************************************************************

Option Explicit
Option Compare Database

'Variable objet d'instance de la base de données
Private m_oDB                                               As DAO.Database
'Variable récupérant la sélection de la liste
Private m_strPostalCode                                     As String
Private m_strTown                                           As String
Private m_lngIDPostalCode                                   As Long
'Variable de la frappe en cours dans la zone de texte
Private m_strCurrentChars                                   As String
'Constantes des requêtes
Private Const TOWNCP_QUERY                                  As String = "qry_LBOListTownsCP"
Private Const CPTOWN_QUERY                                  As String = "qry_LBOListCPTowns"
'Constantes de légende du bouton d'inversion
Private Const CPTOWN_CAPTION                                As String = "Code Postal / Ville"
Private Const TOWNCP_CAPTION                                As String = "Ville / Code Postal"
'Constantes de largeurs de colonnes
Private Const CPT_TOWN_COLWIDTH                             As String = "0cm;1,6cm;7cm"
Private Const TOWN_CPT_COLWIDTH                             As String = "0cm;7cm;1,6cm"
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Sub cmdAdd_Click()
'Appelle la fonction AddNewTown pour ajouter une ville dans la base
If _
MsgBox("Normalement, vous n'avez pas à créer de nouvelles villes car elles ont toutes été implémentées dans la base..." _
& vbCrLf & "Toutefois, il est probable qu'un oubli ait été commis." & _
vbCrLf & vbCrLf & "Voulez-vous ajouter une autre ville ?", vbExclamation + vbYesNo + vbDefaultButton2, _
"Ajouter une ville") = vbYes Then
AddNewTown
End If
End Sub
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Sub cmdClose_Click()
'Ferme le formulaire sans autre action (on peut y greffer un message de confirmation)
DoCmd.Close acForm, Me.Name
End Sub
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Sub cmdEraseEntry_Click()
'Efface la zone de texte, remet les contrôles à l'état initial et réinitialise la variable
m_strCurrentChars = vbNullString
cmdInvert.Caption = TOWNCP_CAPTION
SetListOfCityProperties TOWNCP_QUERY, TOWN_CPT_COLWIDTH
With Me.txtLookup
.Value = vbNullString
.SetFocus
End With
End Sub
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Sub cmdInvert_Click()
'Inverse la sélection et adapte la source de la liste en conséquence
m_strCurrentChars = vbNullString
Select Case cmdInvert.Caption
Case CPTOWN_CAPTION
SetListOfCityProperties TOWNCP_QUERY, TOWN_CPT_COLWIDTH
Me.cmdInvert.Caption = TOWNCP_CAPTION
Case TOWNCP_CAPTION
SetListOfCityProperties CPTOWN_QUERY, CPT_TOWN_COLWIDTH
Me.cmdInvert.Caption = CPTOWN_CAPTION
End Select
'Désactive les boutons
Me.cmdUpdate.Enabled = False
Me.cmdValid.Enabled = False
With Me.txtLookup
.Value = vbNullString
.SetFocus
End With
End Sub
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Sub cmdUpdate_Click()
'Permet de mettre à jour une ville ou un code postal : le formulaire est à construire
Dim strCurrentSelection                                     As String
strCurrentSelection = IIf(cmdInvert.Caption = CPTOWN_CAPTION, _
Me!lboPCTowns.Column(1) & " " & Me!lboPCTowns.Column(2), _
Me!lboPCTowns.Column(2) & " " & Me!lboPCTowns.Column(1))

If MsgBox("Souhaitez-vous modifier le code postal ou la ville '" & _
strCurrentSelection & "' ?", vbQuestion + vbYesNo, "Modifier") = vbYes Then
m_lngIDPostalCode = Nz(Me.lboPCTowns.Column(0), 0)
DoCmd.Close acForm, Me.Name
'***************************************************************
MsgBox "Formulaire cible à définir...", vbInformation, _
"Mise en commentaire"
'DoCmd.OpenForm "Formulaire_De_Mise_A_Jour", , , "IDPostalCode=" & m_lngIDPostalCode, _
acFormEdit, acDialog, strCurrentSelection
'***************************************************************
End If
End Sub
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Sub cmdValid_Click()
'Récupère la sélection effectuée sur la liste et la dépose dans les zones appropriées du formulaire appelant
'On entend par formulaire appelant, le formulaire qui a ouvert celui-ci
Dim m_strPostalCode                                         As Long
Dim m_strTown                                               As String
Dim m_lngIDPostalCode                                       As Long

m_lngIDPostalCode = Nz(Me.lboPCTowns.Column(0), 0)
If m_lngIDPostalCode Then
'Affectation des valeurs aux variables selon la sélection
m_strPostalCode = IIf(cmdInvert.Caption = CPTOWN_CAPTION, Me!lboPCTowns.Column(1), Me!lboPCTowns.Column(2))
m_strTown = IIf(cmdInvert.Caption = CPTOWN_CAPTION, Me!lboPCTowns.Column(2), Me!lboPCTowns.Column(1))
If MsgBox("Cette action va déposer la sélection :" & vbCrLf & m_strPostalCode & " " & m_strTown & " dans le formulaire..." & _
vbCrLf & vbCrLf & "Est-ce correct ?", vbQuestion + vbYesNo, "Confirmation") = vbYes Then
DoCmd.Close acForm, Me.Name
'On passe la valeur de 'ID à l'argument OpenArgs pour le récupérer...
'On aurait pu procéder à l'aide d'une variable ou d'une propriété publique
DoCmd.OpenForm "frmAppelant", , , , , acDialog, m_lngIDPostalCode
End If
End If
End Sub
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Sub Form_Close()
'Fermer l'objet Database et distinguer la variable objet de l'objet réel qu'elle représente
If Not m_oDB Is Nothing Then m_oDB.Close
Set m_oDB = Nothing
End Sub
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Sub Form_KeyPress(KeyAscii As Integer)
'Si l'utilsateur appuie sur Echap (27) le formulaire est fermé sans confirmation
'Utilisable seulement si la propriété KeyPreview = True
If KeyAscii = 27 Then
DoCmd.Close acForm, Me.Name
End If
End Sub
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Sub Form_Load()
'Active le sablier
DoCmd.Hourglass True
'Propriété du formulaire
Me.KeyPreview = True
'Initialise la base de données dans la variable
Set m_oDB = CurrentDb
'Prépare les contrôles
Me.cmdInvert.Caption = TOWNCP_CAPTION
Me.cmdAdd.Enabled = True
Me.cmdUpdate.Enabled = False
Me.lblCountTowns.Caption = CountTownFound(vbNullString)
'Initialise la liste des communes
SetListOfCityProperties TOWNCP_QUERY, TOWN_CPT_COLWIDTH
'Désactive le sablier
DoCmd.Hourglass False
End Sub
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Sub lboPCTowns_Click()
Dim strCurrentSelection                                     As String
'Affecte la sélection CP+Commune dans une variable quel que soit l'ordre des colonnes
strCurrentSelection = IIf(cmdInvert.Caption = CPTOWN_CAPTION, _
Me!lboPCTowns.Column(1) & " " & Me!lboPCTowns.Column(2), _
Me!lboPCTowns.Column(2) & " " & Me!lboPCTowns.Column(1))

'Libère le bouton de validation
Me.cmdValid.Enabled = True
'et celui de la mise à jour
Me.cmdUpdate.Enabled = True
Me.labelTownSelected1.Caption = strCurrentSelection
Me.labelTownSelected2.Caption = labelTownSelected1.Caption
End Sub
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Sub txtLookup_KeyPress(KeyAscii As Integer)
UpdateTownList KeyAscii
End Sub
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Sub AddNewTown()
'Permet d'ajouter une ville et son CP dans la base de données
Dim oRS                                                     As DAO.Recordset
Dim lngNewID                                                As Long
Dim strCurrentSelection                                     As String

'Mise en place d'une gestion d'erreurs (DAO)
On Error GoTo L_ErrAddNewTown
'Demande le nom de la ville...
m_strTown = UCase$(InputBox("Quel est le nom de la commune à ajouter ?", "Nouvelle commune"))
'Vérifie si une entrée est saisie
If Len(m_strTown) = 0 Then
MsgBox "Le nom inscrit est incorrect ou bien vous avez annulé!", vbExclamation, "Erreur"
Exit Sub
End If
'Demande le N° du code postal...
m_strPostalCode = InputBox("Quel est le N° du code postal de cette ville '" & m_strTown & "' ?", "Code postal de la ville")
'Vérifie si une entrée est saisie et comporte 5 caractères
If Len(m_strPostalCode) <> 5 Then
MsgBox "Le N° inscrit est incorrect ou bien vous avez annulé !", vbExclamation, "Erreur"
Exit Sub
End If
'Confirmation de l'entrée (en MAJUSCULES non accentuées)...
strCurrentSelection = m_strPostalCode & " " & RemoveCharAccents(m_strTown, True)
If MsgBox("Veuillez confirmer que doit être ajouté '" & strCurrentSelection & "' dans la base ?", _
vbQuestion + vbYesNo, "Ajout") = vbYes Then

'Contrôle de l'existence de la ville
If ThisTownExists(m_strTown, m_strPostalCode) Then
MsgBox "La commune '" & m_strTown & _
"' existe déjà dans la base : elle a pour code postal " & m_strPostalCode & "...", vbExclamation, "Doublon"
Exit Sub
Else
'Ouvre le Recordset sur la table pour ajouter les valeurs
Set oRS = CurrentDb.OpenRecordset("TBLPostalCodes", dbOpenDynaset)
With oRS
.AddNew
!PostalCode = m_strPostalCode
!Town = m_strTown
lngNewID = !IDPostalCode
.update
.Close
End With
'Met à jour la liste en conséquence
With Me.lboPCTowns
.Requery
End With
'Ajoute la nouvelle entrée dans la zone de texte selon l'inversion en cours
If cmdInvert.Caption = CPTOWN_CAPTION Then
Me.txtLookup = m_strPostalCode
Else
Me.txtLookup = m_strTown
End If
'Affecte l'ID à la variable comme si on l'avait sélectionnée
m_lngIDPostalCode = lngNewID
'Et au label au-dessous de la liste
Me.labelTownSelected1.Caption = strCurrentSelection
Me.labelTownSelected2.Caption = labelTownSelected1.Caption
'Et on compte le nombre de villes trouvées
Me.lblCountTowns.Caption = CountTownFound(vbNullString)
End If
End If

On Error GoTo 0
L_ExAddNewTown:
'Libère l'objet de la mémoire
Set oRS = Nothing
Exit Sub

L_ErrAddNewTown:
'Message si erreur
MsgBox Err.Description, vbExclamation, Err.Source
'Reprendre la suite...
Resume L_ExAddNewTown
End Sub
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Function ThisTownExists(ByVal Town As String, ByVal PostalCode As _
String) As Long
'Vérifie si la paire une commune et son CP existe déjà dans la base
Dim oRS                                                     As DAO.Recordset
Dim SQL                                                     As String
Dim blnExists                                               As Boolean
Dim SQLCheckTown                                            As String
Dim m_strPostalCode                                         As Long

On Error GoTo L_ErrThisTownExists

SQL = "SELECT IDPostalCode FROM TBLPostalCodes WHERE Town = '" & Town & "' AND PostalCode = '" & PostalCode & "';"
Set oRS = CurrentDb.OpenRecordset(SQL, dbOpenSnapshot)
With oRS
'Si un enregistrement est trouvé...
If Not .EOF Then ThisTownExists = .Fields(0).Value
.Close
End With

On Error GoTo 0
L_ExThisTownExists:
'Libère l'objet de la mémoire
Set oRS = Nothing
Exit Function

L_ErrThisTownExists:
MsgBox Err.Description, vbExclamation, Err.Source
Resume L_ExThisTownExists
End Function
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Sub UpdateTownList(KeyAscii As Integer)
'Met à jour la liste des communes en fonction des caractères tapés dans la zone de texte
Dim SQL                                                     As String
Dim SQLWhere                                                As String
Dim strCityCount                                            As String

'Selon le sens de la recherche
Select Case cmdInvert.Caption
Case CPTOWN_CAPTION
'Sens Code postal => Ville
Select Case KeyAscii
Case 8, 48 To 57
'Backspace ou un nombre : acceptés
Case Else
'Sinon rien
KeyAscii = 0
MsgBox "Un caractère numérique est requis ici !", vbExclamation, "Erreur de frappe"
m_strCurrentChars = vbNullString
Exit Sub
End Select
If KeyAscii = 8 Then
'Si on appuie sur Backspace alors on retire un caractère
On Error Resume Next
m_strCurrentChars = Left$(m_strCurrentChars, Len(m_strCurrentChars) - 1)
On Error GoTo 0
Else
'Sinon la variable s'incrémente et prend le caractère tapé
m_strCurrentChars = m_strCurrentChars & Chr(KeyAscii)
End If
'On applique alors le critère à la chaîne SQL
SQL = "SELECT IDPostalCode, PostalCode, Town "
SQL = SQL & "FROM TBLPostalCodes "
SQLWhere = "WHERE PostalCode Like '" & m_strCurrentChars & "*' "
SQL = SQL & SQLWhere & "ORDER BY PostalCode;"
'Et on rafraîchit la liste selon cette clause SQL
SetListOfCityProperties SQL, CPT_TOWN_COLWIDTH

Case TOWNCP_CAPTION
Select Case KeyAscii
Case 8, 32
'Backspace, un espace : acceptés
Case 39, 45
'apostrophe ou tiret : refusés
KeyAscii = 0
MsgBox "Les apostrophes et les tirets ne sont pas normalisés pour les noms des villes !" & _
vbCrLf & vbCrLf & "Rendez-vous ici pour plus d'infos :" & _
vbCrLf & "http://www.laposte.fr/sna/rubrique.php3?id_rubrique=87", vbExclamation, "Erreur de frappe"
m_strCurrentChars = vbNullString

'Voir "http://www.laposte.fr/sna/rubrique.php3?id_rubrique=87" pour plus d'infos
Case 65 To 90
'MAJUSCULE : acceptés
Case 97 To 122
'minuscule : acceptés donc converti en majuscule
KeyAscii = KeyAscii - 32
'MAJUSCULE accentuées
Case 192, 193, 194, 195, 196, 197: KeyAscii = 65    'A
Case 199: KeyAscii = 67                    'C
Case 200, 201, 202, 203: KeyAscii = 69     'E
Case 204, 205, 206, 207: KeyAscii = 73     'I
Case 209: KeyAscii = 78                    'N
Case 210, 211, 212, 213, 214: KeyAscii = 79    'O
Case 217, 218, 219, 220: KeyAscii = 85     'U
'minuscules accentuées
Case 224, 225, 226, 227, 228, 229: KeyAscii = 65    '97:a
Case 231: KeyAscii = 67                    '99:c
Case 232, 233, 234, 235: KeyAscii = 69     '101:e
Case 236, 237, 238, 239: KeyAscii = 73     '105:i
Case 241: KeyAscii = 78                    '110:n
Case 242, 243, 244, 245, 246: KeyAscii = 79    '111:o
Case 249, 250, 251, 252: KeyAscii = 85     '117:u
Case Else
'Sinon rien
KeyAscii = 0
MsgBox "Un caractère alphabétique est requis ici !", vbExclamation, "Erreur de frappe"
m_strCurrentChars = vbNullString
Exit Sub
End Select

If KeyAscii = 8 Then
'Si on appuie sur Backspace alors on retire un caractère
On Error Resume Next
m_strCurrentChars = Left(m_strCurrentChars, Len(m_strCurrentChars) - 1)
On Error GoTo 0
Else
'Sinon la variable s'incrémente et prend le caractère tapé
m_strCurrentChars = m_strCurrentChars & Chr(KeyAscii)
End If
'Si aucun caractère tapé alors on pose un astérisque de manière à ne pas provoquer une erreur
If Len(m_strCurrentChars) = 0 Then m_strCurrentChars = "*"
'On applique alors le critère à la chaîne SQL
SQL = "SELECT IDPostalCode, Town, PostalCode FROM TBLPostalCodes "
SQLWhere = "WHERE Town Like '" & m_strCurrentChars & "*' "
SQL = SQL & SQLWhere & "ORDER BY Town;"
'Et on rafraîchit la liste selon cette clause SQL
SetListOfCityProperties SQL, TOWN_CPT_COLWIDTH
End Select
'Et on compte le nombre de villes trouvées
Me.lblCountTowns.Caption = CountTownFound(SQLWhere)
'Si la zone ne contient que * on efface la variable (cas de KeyAscii = 8)
If Len(m_strCurrentChars) Then
If (Asc(Left$(m_strCurrentChars, 1)) = 42) And (Len(m_strCurrentChars) = 1) Then
m_strCurrentChars = vbNullString
End If
End If
End Sub
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Sub SetListOfCityProperties(ByVal Source As String, ColWidth As String)
'Rafraîchit la liste des villes
With lboPCTowns
.ColumnCount = 3
.BoundColumn = 3
.RowSource = Source
.ColumnWidths = ColWidth
End With
End Sub
''' ---------------------------------------------------------------------------------------------------------------------------------------
Private Function CountTownFound(ByVal SQLWhere As String) As String
'Compte le nombre de villes correspondant au critère
Dim oRS                                                     As DAO.Recordset
Dim SQL                                                     As String
Dim lngRowCount                                             As Long

On Error GoTo L_ErrCountTownFound
'On ouvre le RecordSet sur le critère en cours...
SQL = "SELECT COUNT(IDPostalCode) AS NBRows "
SQL = SQL & "FROM TBLPostalCodes " & SQLWhere
Set oRS = m_oDB.OpenRecordset(SQL, dbOpenSnapshot)
With oRS
lngRowCount = Nz(.Fields(0).Value, 0)
'Si la variable est > 0
If lngRowCount Then
CountTownFound = lngRowCount & " commune" & IIf(lngRowCount = 1, " ", "s ") & "trouvée" & _
IIf(lngRowCount = 1, " ", "s ")
Else
CountTownFound = "Aucune commune trouvée..."
End If
.Close
End With

On Error GoTo 0
L_ExCountTownFound:
'Libère l'objet de la mémoire
Set oRS = Nothing
Exit Function

L_ErrCountTownFound:
MsgBox Err.Description, vbExclamation, Err.Source
Resume 'L_ExCountTownFound
End Function

Function RemoveCharAccents(ByVal TextToChange As String, Optional ByVal ToUpperCase As Boolean = False)
'Permet de substituer les accents d'un chaîne avec option MAJUSCULE
Const CHARS_WITH_ACCENTS                                    As String = "ÀÁÂÃÄÅÈÉÊËÌÍÎÏÒÓÔÕÖ&#216;ÙÚÛÜàáâãäåèéêëìíîïòóôõö&#248;ùúûü&#255;&#209;Ç&#241;ç"
Const CHARS_WITHOUT_ACCENTS                                 As String = "AAAAAAEEEEIIIIOOOOOOUUUUaaaaaaeeeeiiiioooooouuuuyNCnc"
Dim C                                                       As Integer
Dim strChar                                                 As String
Dim strNoAccentChars                                        As String
Dim strFinalString                                          As String

strFinalString = TextToChange
strNoAccentChars = CHARS_WITHOUT_ACCENTS
If ToUpperCase Then
strNoAccentChars = UCase$(CHARS_WITHOUT_ACCENTS)
End If
For C = 1 To Len(CHARS_WITH_ACCENTS)
strChar = Mid$(CHARS_WITH_ACCENTS, C, 1)
If InStr(1, strFinalString, strChar, vbBinaryCompare) Then
strFinalString = Replace(strFinalString, strChar, Mid$(strNoAccentChars, C, 1))
End If
Next C
RemoveCharAccents = IIf(ToUpperCase, UCase(strFinalString), strFinalString)
End Function

Explications du déroulement

Le code est commenté ligne par ligne pour une meilleure compréhension...

Dans la fonction qui vérifie le caractère inscrit par sa valeur ASCII via l'argument KeyPress, j'ai intentionnellement laissé le :

Case 39, 45
'apostrophe ou tiret : refusés
KeyAscii = 0
MsgBox "Les apostrophes et les tirets ne sont pas normalisés pour les noms des villes !" & _
vbCrLf & vbCrLf & "Rendez-vous ici pour plus d'infos :" & _
vbCrLf & "http://www.laposte.fr/sna/rubrique.php3?id_rubrique=87", vbExclamation, "Erreur de frappe"
m_strCurrentChars = vbNullString

tout simplement pour vous préciser que le tiret comme l'apostrophe sont interdits pour le nom des villes.
Tous les détails à ce sujet sont précisés ici : Service National de l'Adresse

Effectivement, dans la pratique et pour ce cas précis, j'aurais dû enlever ce "Case", l'interprétation aurait alors été dans le "Case Else" et la valeur tapée aurait été ignorée (KeyAscii = 0).
Dans les gros traits, le formulaire et ses contrôles s'initialisent avec ce qui se déroule dans l'événement Sur chargement (Load)...

À partir de l'instant où l'utilisateur inscrit un caractère dans la zone liste, l'événement Sur touche activée (KeyPress) s'enclenche et appelle la procédure UpdateTownList() avec le paramètre KeyAscii du même événement passé en argument à la procédure.

La liste se rafraîchit alors en conséquence de la condition WHERE qui s'exécute toujours avec l'opérateur LIKE.

Le paramètre KeyPress est analysé à chaque frappe et la procédure adapte la valeur de la variable de module m_strCurrentChars selon le sens (inversion) de la sélection.

Selon le bouton sur lequel l'utilisateur clique, il se passe un certain nombre de choses, mais à chaque fois, la liste se rafraîchit et des contrôles se réinitialisent.

4 - Mise en exécution

Une fois cela terminé, vous basculez de nouveau sur votre formulaire (Alt+Tab) ou bien vous quittez l'éditeur Visual Basic depuis le menu Fichier.

4-1 - Affichage en mode formulaire

Vous enregistrez de manière à ne pas perdre votre travail et vous passez en Mode Formulaire une seconde fois.

Pour ce faire, appuyez sur F5 ou bien sur le bouton Mode formulaire...
À cet instant, lorsque vous écrivez dans la zone de texte, le rafraîchissement de la liste est déclenché.

La liste avec recherche par le nom de la commune... (remarquez l'intitulé du bouton cmdInvert)

La liste avec recherche par le numéro du code postal... (remarquez l'intitulé du bouton cmdInvert)

Dès que vous cliquez sur l'élément souhaité dans la liste, le bouton de validation se libère.

4-2 - Ajout d'une ville...

Ainsi que je vous l'ai évoqué en début de ce tutoriel, le projet gère l'ensemble des communes de France.
Il est donc inutile d'en ajouter, mais j'ai prévu ce module parce le club qui utilisait l'application où était implémenté ce module ne gérait que quelques villes, mais n'excluait pas l'ajout d'autres qui étaient hors de sa circonscription.

Le clic sur le bouton ajout, la procédure AddNewTown est appelée...

Remarque : Cette procédure contrôlera l'existence de la nouvelle entrée avant de la stocker...

Le message de confirmation est affiché...

La boîte de dialogue permet d'inscrire la ville...

Puis le code postal...

Une confirmation de l'entrée par un dernier message.
Si la ville est ajoutée, la liste se rafraîchira en conséquence.

Sinon, c'est qu'elle existe déjà, alors un message d'erreur informatif est affiché ;

4-3 - Modification d'une ville

Il n'est pas prévu dans ce tutoriel de module de modification de la ville, en tout cas au niveau du formulaire.

Le mode opératoire est inscrit dans l'événement Click du bouton cmdUpdate où vous êtes tenu de créer le formulaire approprié, celui-ci étant ouvert en mode Dialog avec la condition WHERE basée sur le critère qui prend l'ID du code postal via la variable m_lngIDPostalCode...

5 - Conclusion

Ce petit tutoriel permettra aux personnes exigeantes à l'égard des applications de gestion et nécessitant des options de sélection évolutives et dynamiques de mettre en place ce type de formulaire...
Si le besoin tourne autour du même thème, tant mieux, vous n'aurez pas de grosses modifications à apporter, mais dans le cas contraire, l'adaptation ne devrait pas poser trop de problèmes.

Je précise que j'ai volontairement modifié et simplifié quelques instructions pour que le code soit plus compréhensible pour tous...

De ce fait, les utilisateurs disposant d'un niveau avancé s'en apercevront et pourront adapter ces blocs selon leur propre méthode de développement. Initialement, le projet exploitait une classe et des événements déclencheurs... Je me suis dit qu'il était préférable que tout le monde puisse y accéder et j'ai adopté une rédaction plus simplifiée.

6 - Sujets corrélatifs

Dans la FAQ du forum Access, vous pouvez trouver de nombreux sujets sur les zones de liste en cliquant ici.

7 - Remerciements

Je tiens à remercier toutes celles et ceux qui ont participé à la relecture de ce document en y incluant leurs remarques et en particulier :

Claude Leloup pour la correction,
dourouc05 pour le côté administratif,
Philippe Jochmans pour ses remarques relatives aux normes postales,
et arkham46 pour le reste...

sans oublier Nono40 pour son outil permettant la rédaction des articles.