Gestion des événements Exit et Enter de textbox créés dynamiquement

Septembre 2016

Malheureusement, les modules de classe ne supportent pas les événements Exit, Enter, AfterUpdate et BeforeUpdate des textbox.



Solutions de contournement

Il en existe plusieurs, en voici trois.

Routine de validation

La première consiste à créer une routine de validation, routine appelée par autant de Sub événementielle que vous avez de textbox.
source
Par exemple, pour trois textbox :
Private Sub TextBox1_Exit(ByVal Cancel As MSForms.ReturnBoolean)
  Validation Cancel
End Sub
Private Sub TextBox2_Exit(ByVal Cancel As MSForms.ReturnBoolean)
  Validation Cancel
End Sub
Private Sub TextBox3_Exit(ByVal Cancel As MSForms.ReturnBoolean)
  Validation Cancel
End Sub

' Sub commune de validation
Private Sub Validation(Optional ByVal Cancel As MSForms.ReturnBoolean)
  Dim monTextBox As MSForms.TextBox
  Set monTextBox = Me.ActiveControl
  If Not IsNumeric(monTextBox) Then
    MsgBox "Merci de saisir une valeur numérique", vbExclamation, monTextBox.Name
    Cancel = True
  End If
End Sub


Le problème de ce code, c'est qu'il n'a rien de dynamique.
Vous êtes obligés de connaitre, à l'avance, le nombre de textbox.

Surveillance de l'UserForm

Source
Je ne donne pas ici le code de Silkyroad, mais il est disponible sur le Net.
Le principe est qu'une procédure CibleFocus tourne en boucle dès le lancement de l'UserForm et déclenche les événements lors d'un changement d'ActiveControl.
Cette procédure rend la main régulièrement à l'utilisateur grâce à un DoEvents en boucle.
C'est là que le bâs blesse. Un DoEvents en boucle affole le CPU, voir ici : vba doevents problemes et solutions

Création dynamique des procédures événementielles

Ici, le principe est d'écrire les codes des procédures événementielles pour chaque création de textbox.
On reprend donc les deux premières solutions pour les associer.
Dim Ct As Control, j As Long, i As Integer
For i = 1 To 10
  Set Ct = Usf.Controls.Add("forms.TextBox.1", "TextBox" & i, True)
  With Usf.CodeModule
    j = .CountOfLines
    .InsertLines j + 1, "Sub TextBox" & i & "_Click()"
    .InsertLines j + 2, "Validation Cancel"
    .InsertLines j + 3, "End Sub"
  End With
Next i

Le problème tient essentiellement en trois points :
1- la création de l'userform devient alors également dynamique
2- la suppression de ces lignes de code à la fermeture de l'Userform est faisable, mais pas évidente.
Il nous faut, en effet, rechercher la première ligne à supprimer par son contenu.
3- Risque de bug si la limite de lignes est atteinte dans le module de l'UserForm [rare]

Gestion des événements

Il existe une fonction de l'API windows permettant d'établir une connection entre un contrôle et une instance de classe.
Cette fonction, ConnectToConnectionPoint, sert à créer un lien entre un Objet VBA (source de l'événement) et une Classe (gestionnaire de l'événement).
Les événements ne sont alors que des méthodes (Sub) de la Classe auquelles on attribue un Attribute VB_UserMemId.
C'est la présence de cet attribut, dans la méthode (Sub) de la Classe, qui en fait un gestionnaire d'événement.

Function ConnectToConnectionPoint

La déclaration, à placer en entête du module de Classe :
Private Declare Function ConnectToConnectionPoint Lib "shlwapi" Alias "#168" _
                 (ByVal punk As stdole.IUnknown, _
                 ByRef riidEvent As GUID, _
                 ByVal fConnect As Long, _
                 ByVal punkTarget As stdole.IUnknown, _
                 ByRef pdwCookie As Long, _
                 Optional ByVal ppcpOut As Long) As Long

Les paramètres obligatoires, à passer à cette fonction, sont :
punk As stdole.Iunknown ==> Il s'agit du gestionnaire d'événement, votre instance de classe
riidEvent As GUID ==> Un identificateur global unique (cf : ICI)
fConnect As Long ==> Définit le type de connection : VRAI (-1) = établit la connection, FAUX (0) = déconnection
punkTarget As stdole.Iunknown ==> Votre objet VBA, dans notre cas, votre TextBox
pdwCookie ==> un numéro unique identifiant cette connection (valeur de retour de la fonction)
exemple d'utilisation :
Option Explicit
 
    Private Type GUID
        Data1 As Long
        Data2 As Integer
        Data3 As Integer
        Data4(0 To 7) As Byte
    End Type
     
    #If VBA7 And Win64 Then
        Private Declare PtrSafe Function ConnectToConnectionPoint Lib "shlwapi" Alias "#168" _
                 (ByVal punk As stdole.IUnknown, _
                 ByRef riidEvent As GUID, _
                 ByVal fConnect As Long, _
                 ByVal punkTarget As stdole.IUnknown, _
                 ByRef pdwCookie As Long, _
                 Optional ByVal ppcpOut As LongPtr) As Long
    #Else
        Private Declare Function ConnectToConnectionPoint Lib "shlwapi" Alias "#168" _
                 (ByVal punk As stdole.IUnknown, _
                 ByRef riidEvent As GUID, _
                 ByVal fConnect As Long, _
                 ByVal punkTarget As stdole.IUnknown, _
                 ByRef pdwCookie As Long, _
                 Optional ByVal ppcpOut As Long) As Long
    #End If
Sub Connection(Obj As Object)
Dim cGuid As GUID, C As Boolean, Cook As Long
        With cGuid
            .Data1 = &H20400
            .Data4(0) = &HC0
            .Data4(7) = &H46
        End With
        C = True
        ConnectToConnectionPoint Me, cGuid, C, Obj, Cook, 0&
End Sub

On connecte, ici, l'instance de classe Me à l'objet Obj. Il est retourné une valeur de type Long dans la variable Cook.
A noter :
> La déclaration du Private Type GUID, obligatoire pour créer l'identifiant global unique
> La double déclaration au sein d'un test IF de la fonction ConnectToConnectionPoint afin de rendre ce code valide sur les systèmes 32 et/ou 64 bits.

Ce que fait la fonction :
  • Tout d'abord, recevoir un message d'événement de la source d'événements.
  • Trouver le gestionnaire d'événements pour ce message d'événement.
  • Réaliser le traitement relatif au gestionnaire d'événement trouvé.

Les Méthodes événementielles

Vous pouvez les nommer comme bon vous semble. Attention toutefois à conserver un nom parlant pour la maintenance de votre code.
Personnellement, je l'ai appellé : Entree, Sortie, AvantMiseAjour et ApresMiseAjour.
Chacune de ces procédures doit comporter son propre Attribute VB_UserMemId.
Voici la liste des 4 nous concernant :
  • Enter : &H80018202 = -2147384830
  • Exit : &H80018203 = -2147384829
  • BeforeUpdate : &H80018201 = -2147384831
  • AfterUpdate : &H80018200 = -2147384832

Pour chacune des procédures, vous devez placer, sous la ligne de déclaration, le UserMemId correspondant. C'est cela qui va permettre à VBA de savoir quel événement doit être déclenché.
Petite manipulation à faire :
1- Coller le code suivant dans votre module de classe
 Public Sub Entree()
'Attribute Entree.VB_UserMemId = -2147384830
        
    End Sub
    
    Public Sub Sortie(ByVal Cancel As MSForms.ReturnBoolean)
'Attribute Sortie.VB_UserMemId = -2147384829
        
    End Sub
    
    Public Sub AvantMiseAjour(ByVal Cancel As MSForms.ReturnBoolean)
'Attribute AvantMiseAjour.VB_UserMemId = -2147384831
        
    End Sub
     
    Public Sub ApresMiseAjour()
'Attribute ApresMiseAjour.VB_UserMemId = -2147384832
        
    End Sub

2- Exportez votre module de classe (Fichier/Exporter)
3- depuis votre éditeur de texte préféré (bloc-note), ouvrez le
4- décommentez les lignes contenant Attribute VB_UserMemId
5- enregistrez
6- Importez dans votre classeur (en ayant pris soin de supprimer le précédent)
Vous disposez maintenant des 4 procédures événementielles pour votre classe de textbox.

La classe - Création

Pour réaliser ce que nous voulons faire simplement, nous avons besoin d'une variable contenant tous nos textbox et étant "lisible" aussi bien depuis le module de classe que depuis l'userform.

Variable tableau publique

Nous allons donc déclarer une variable tableau, typée comme notre classe, en Public dans un module standard.
Pour cela :
> insérez un module standard (Insertion/Module)
> Collez ce code :
Option Explicit

    Public mesTB(18) As New cTextbox
    'constantes permettant de changer la couleur de fond des textbox
    Public Const COULEUR_INITIALE As Long = &H80000005
    Public Const COULEUR_VISITEE As Long = &H80000003

Rien de bien spécial non plus, nous déclarons une variable tableau amenée à contenir un maximum de 19 contrôles, typée As cTextbox.
Ce type n'existe pas encore. Pour cela, il nous faut créer notre module de classe.

La classe - début du code

Insérez un nouveau module de classe, nommez le cTextbox, et placez y le type GUID ainsi que la déclaration de ConnectToConnectionPoint :
Option Explicit
 
    Private Type GUID
        Data1 As Long
        Data2 As Integer
        Data3 As Integer
        Data4(0 To 7) As Byte
    End Type
     
    #If VBA7 And Win64 Then
        Private Declare PtrSafe Function ConnectToConnectionPoint Lib "shlwapi" Alias "#168" _
                 (ByVal punk As stdole.IUnknown, _
                 ByRef riidEvent As GUID, _
                 ByVal fConnect As Long, _
                 ByVal punkTarget As stdole.IUnknown, _
                 ByRef pdwCookie As Long, _
                 Optional ByVal ppcpOut As LongPtr) As Long
    #Else
        Private Declare Function ConnectToConnectionPoint Lib "shlwapi" Alias "#168" _
                 (ByVal punk As stdole.IUnknown, _
                 ByRef riidEvent As GUID, _
                 ByVal fConnect As Long, _
                 ByVal punkTarget As stdole.IUnknown, _
                 ByRef pdwCookie As Long, _
                 Optional ByVal ppcpOut As Long) As Long
    #End If

Voilà. Nous avons créé notre classe qui nous a permis de typer notre variable tableau.

L'UserForm - Code appelant

Vous le verrez ici, rien de spécial.
Nous faisons appel à la procédure événementielle des UserForm :
Private Sub UserForm_Initialize()
.
Dans cette procédure, il va nous falloir alimenter notre variable tableau Publique de l'ensemble des contrôles que nous souhaitons voir réagir aux événements Entree, Sortie, etc…
Attention, vous allez voir qu'il nous faut également passer les contrôles que j'appelle "containers". En effet, la sortie d'un textbox placé à l'intérieur d'un Frame est différente de la sortie d'un textbox directement placé sur l'UserForm.
La sortie dans ce cas est contrôlée par l'événement Exit du container (Multipage, Page ou Frame) et non plus par l'Exit du TextBox.
Il nous faut donc les passer à la classe.

De plus, pour nos essais, nous allons construire, dynamiquement, des contrôles dans un UserForm.
Afin de tester plusieurs cas de figure, nous allons créer :
- des textbox directement dans l'UserForm
- des textbox dans un frame
- des textbox dans les pages d'un multipage
- des textbox dans des frame eux-mêmes situés dans d'autres frame.

Pour cela, insérez, dans un nouveau classeur, un UserForm, et placez les code suivants dans son module :
====UserForm Initialize===
'code qui se déclenche lors de l'initialisation de l'UserForm
    Private Sub UserForm_Initialize()
Appel de la procédure de création dynamique des contrôles
        Call Creation_Dynamique_Des_Controles
'AJOUT TEXTBOX
        mesTB(0).Add Me.Controls("TextBox1")
        'Pour le TextBox2 : aucune particularité
        mesTB(1).Add Me.Controls("Textbox3")
        mesTB(2).Add Me.Controls("TextBox4")
        mesTB(3).Add Me.Controls("TextBox5")
        mesTB(4).Add Me.Controls("TextBox6")
        mesTB(5).Add Me.Controls("TextBox7")
        'Pour le TextBox8 : aucune particularité
        mesTB(6).Add Me.Controls("TextBox9")
        mesTB(7).Add Me.Controls("TextBox10")
        mesTB(8).Add Me.Controls("TextBox11")
        mesTB(9).Add Me.Controls("TextBox12")
        mesTB(10).Add Me.Controls("TextBox13")
        mesTB(11).Add Me.Controls("TextBox14")
'AJOUT "CONTAINERS"
        mesTB(12).Add Me.Controls("Frame1")
        mesTB(13).Add Me.Controls("Frame2")
        mesTB(14).Add Me.Controls("Frame3")
        mesTB(15).Add Me.Controls("Frame4")
        mesTB(16).Add Me.Controls("MultiPage1").Page1
        mesTB(17).Add Me.Controls("MultiPage1").Page2
        mesTB(18).Add Me.Controls("MultiPage1")
    End Sub

Creation_Dynamique_Des_Controles

    Private Sub Creation_Dynamique_Des_Controles()
        Dim Ct As Control, Frm As Frame, Frm2 As Frame, Frm3 As Frame, Multi As MultiPage
 
        Me.Move Me.Left, Me.Top, 470, 540
        Set Ct = Me.Controls.Add("forms.Label.1", "Lab1", True)
        Ct.Move 10, 10, 40, 20
        Ct.Caption = "NOM"
        Set Ct = Me.Controls.Add("forms.TextBox.1", "TextBox1", True)
        Ct.Move 50, 10, 100, 20
        Set Ct = Me.Controls.Add("forms.Label.1", "Lab2", True)
        Ct.Move 10, 30, 40, 20
        Ct.Caption = "Prénom"
        Set Ct = Me.Controls.Add("forms.TextBox.1", "TextBox2", True)
        Ct.Move 50, 30, 100, 20
        Set Ct = Me.Controls.Add("forms.Label.1", "Lab3", True)
        Ct.Move 10, 50, 40, 20
        Ct.Caption = "Sécurité Soc"
        Set Ct = Me.Controls.Add("forms.TextBox.1", "TextBox3", True)
        Ct.Move 50, 50, 100, 20
        Set Frm = Me.Controls.Add("forms.Frame.1", "Frame1", True)
        With Frm
            .Move 160, 10, 200, 100
            .Caption = "Etat civil"
            Set Ct = .Controls.Add("forms.Label.1", "Lab4", True)
            Ct.Move 10, 10, 40, 20
            Ct.Caption = "Date Naiss"
            Set Ct = .Controls.Add("forms.TextBox.1", "TextBox4", True)
            Ct.Move 50, 10, 70, 20
            Set Ct = .Controls.Add("forms.Label.1", "Lab5", True)
            Ct.Move 10, 30, 40, 20
            Ct.Caption = "Heure"
            Set Ct = .Controls.Add("forms.TextBox.1", "TextBox5", True)
            Ct.Move 50, 30, 70, 20
            Set Ct = .Controls.Add("forms.Label.1", "Lab6", True)
            Ct.Move 10, 50, 40, 20
            Ct.Caption = "VILLE"
            Set Ct = .Controls.Add("forms.TextBox.1", "TextBox6", True)
            Ct.Move 50, 50, 100, 20
        End With
        Set Ct = Me.Controls.Add("forms.CommandButton.1", "Bouton1", True)
        Ct.Move 370, 10, 80, 20
        Ct.Caption = "BOUTONS POUR"
        Set Ct = Me.Controls.Add("forms.CommandButton.1", "Bouton2", True)
        Ct.Move 370, 35, 80, 20
        Ct.Caption = "TESTER SORTIE"
        Set Ct = Me.Controls.Add("forms.CommandButton.1", "Bouton3", True)
        Ct.Move 370, 60, 80, 20
        Ct.Caption = "PAR CLIC"
        Set Multi = Me.Controls.Add("forms.Multipage.1", "Multipage1", True)
        With Multi
            .Move 10, 120, 400, 150
            With .Pages(0)
                .Caption = "ADRESSE 1"
                Set Ct = .Controls.Add("forms.Label.1", "Lab7", True)
                Ct.Move 10, 10, 40, 20
                Ct.Caption = "NUMERO"
                Set Ct = .Controls.Add("forms.TextBox.1", "TextBox7", True)
                Ct.Move 50, 10, 30, 20
                Set Ct = .Controls.Add("forms.Label.1", "Lab8", True)
                Ct.Move 10, 30, 40, 20
                Ct.Caption = "RUE"
                Set Ct = .Controls.Add("forms.TextBox.1", "TextBox8", True)
                Ct.Move 50, 30, 300, 60
            End With
            With .Pages(1)
                .Caption = "ADRESSE 2"
                Set Ct = .Controls.Add("forms.Label.1", "Lab9", True)
                Ct.Move 10, 10, 40, 20
                Ct.Caption = "CODE POSTAL"
                Set Ct = .Controls.Add("forms.TextBox.1", "TextBox9", True)
                Ct.Move 50, 10, 50, 20
                Set Ct = .Controls.Add("forms.Label.1", "Lab10", True)
                Ct.Move 10, 30, 40, 20
                Ct.Caption = "VILLE"
                Set Ct = .Controls.Add("forms.TextBox.1", "TextBox10", True)
                Ct.Move 50, 30, 150, 20
            End With
        End With
        Set Frm = Me.Controls.Add("forms.Frame.1", "Frame2", True)
        With Frm
            .Caption = "Tout numérique"
            .Move 35, 300, 400, 200
            Set Ct = .Controls.Add("forms.TextBox.1", "TextBox11", True)
            Ct.Move 10, 10, 240, 20
            Set Frm2 = .Controls.Add("forms.Frame.1", "Frame3", True)
        End With
        With Frm2
            .Move 10, 35, 370, 140
            Set Ct = .Controls.Add("forms.TextBox.1", "TextBox12", True)
            Ct.Move 10, 10, 240, 20
            Set Frm3 = .Controls.Add("forms.Frame.1", "Frame4", True)
        End With
        With Frm3
            .Move 10, 35, 350, 100
            Set Ct = .Controls.Add("forms.TextBox.1", "TextBox13", True)
            Ct.Move 10, 10, 100, 20
            Set Ct = .Controls.Add("forms.TextBox.1", "TextBox14", True)
            Ct.Move 10, 30, 100, 20
        End With
        Set Frm = Nothing
        Set Frm2 = Nothing
        Set Frm3 = Nothing
        Set Ct = Nothing
        Set Multi = Nothing
    End Sub

Destruction des instances de classe

Comme nous avons créés, dans l'initialize, 19 instances de classe, il nous faut les détruire.
Pour cela, nous allons attendre la fin d'utilisation de notre UserForm, soit son événement Terminate.
Ce qui nous donne le code :
Private Sub UserForm_Terminate()
        Dim i As Long
        For i = LBound(mesTB) To UBound(mesTB)
            mesTB(i).Clear
        Next i
        Erase mesTB
    End Sub


Arrivé à ce stade, si vous essayez d'ouvrir l'UserForm, vous allez vous retrouver avec une belle erreur : la Méthode n'existe pas.
En effet, pour ajouter nos contrôles à la Classe, nous avons utilisé la méthode Add. Pour supprimer les instances de classe, nous utilisons la méthode Clear.
Or, aucune Sub ni Function, dans notre module de classe, ne porte ces noms.

La classe - code appelé

Les variables

En dessous des déclaration de type GUID et de la Function ConnectToConnectionPoint, nous allons déclarer les 4 variables suivantes :
 Private iCook As Long 'valeur retournée par la connection
   Private iObjet As Object 'Notre contrôle
    Private iNom As String 'le nom de notre contrôle
   Private iFocus As Boolean 'une variable permettant de savoir s'il a le focus ou non

Pour alimenter la variable iFocus depuis le code de la classe et pouvoir lire les propriétés iNom et iFocus, il nous faut créer des propriétés en Lecture et/ou Lecture - écriture.
Les codes :
    Public Property Let Focus(booF As Boolean)
        iFocus = booF
    End Property
    
     Public  Property Get Focus() As Boolean
        Focus = iFocus
    End Property
    
     Public Property Get Nom() As String
        Nom = iNom
    End Property

Je ne m'étendrais pas sur ce sujet, il existe de nombreux tutos sur Internet.

La connection

Pour créer (et détruire) nos connections, nous allons créer, en Private, une fonction de connection :

Private Sub ConnectEvent(ByVal Connect As Boolean)
    Dim cGuid As GUID
        With cGuid
            .Data1 = &H20400
            .Data4(0) = &HC0
            .Data4(7) = &H46
        End With
        ConnectToConnectionPoint Me, cGuid, Connect, iObjet, iCook, 0&
    End Sub

Il nous faut donc maintenant définir les deux méthodes permettant l'ajout et la suppression d'instances à notre classe.

Les méthodes d'ajout et suppression

La méthode Add permettra la connection de la classe à l'objet :
Public Sub Add(ByVal Obj As Object)
        Set iObjet = Obj 'variable Objet alimentée de notre contrôle (côté appelant : .Add Me.Controls("TextBox1"))
        iNom = Obj.Name 'le nom de notre contrôle stocké dans son instance de classe
        Call ConnectEvent(True) 'connection : True = connection
    End Sub

La méthode Clear va déconnecter l'objet :
    Public Sub Clear()
        If (iCook <> 0) Then Call ConnectEvent(False)
        Set iObjet = Nothing
    End Sub

Les procédures événementielles

Leurs codes ont été donnés plus haut. N'oubliez pas les Attribute xxxxxx.VB_UserMemId
Le but de ces codes va être :
> de colorer un textbox lors de l'entrée
> d'en empêcher la sortie s'il est vide.
A la base, nous disposons de ces 4 Sub (qui doivent rester Public !!!)

    Public Sub Entree()
'Attribute Entree.VB_UserMemId = -2147384830
        
    End Sub
    
    Public Sub Sortie(ByVal Cancel As MSForms.ReturnBoolean)
'Attribute Sortie.VB_UserMemId = -2147384829
        
    End Sub
    
    Public Sub AvantMiseAjour(ByVal Cancel As MSForms.ReturnBoolean)
'Attribute AvantMiseAjour.VB_UserMemId = -2147384831
        
    End Sub
     
    Public Sub ApresMiseAjour()
'Attribute ApresMiseAjour.VB_UserMemId = -2147384832
        
    End Sub

Les deux dernières, Avant et Apres MiseAjour ne nous intéressent pas pour l'exercice. Nous allons donc les laisser de côté.
Dans la procédure d'entrée, il nous faut :
> lui "attribuer" le focus (régler sa variable Focus = True)
> s'il s'agit d'un textbox, en colorer le fond.
Le code devient tout simplement :
    Public Sub Entree()
'Attribute Entree.VB_UserMemId = -2147384830
        Focus = True
        If TypeOf iObjet Is MSForms.TextBox Then iObjet.BackColor = COULEUR_VISITEE
End Sub


Dans la procédure de Sortie, il nous faut :
  • différencier s'il s'agit d'un textbox ou d'un container,
  • S'il s'agit d'un textbox :
    • vérifier s'il est vide
    • S'il l'est, empêcher la sortie
    • sinon, changer sa couleur et sa variable Focus
  • s'il s'agit d'un container, il faut :
    • vérifier si son activeControl est un textbox
    • si oui, lancer la procédure événementielle de sortie de ce textbox.


Le cas du container :
Nous allons ici avoir besoin d'une procédure qui vérifie l'ActiveControl et lance la procédure événementielle.
    Private Sub Conteneur_Sortie(ByVal Cancel As MSForms.ReturnBoolean)
        'le code déclenché dans "Frame_Exit" ou "MultiPage_Exit"
        Dim Conteneur As Object
        If TypeOf iObjet Is MSForms.MultiPage Then
            Set Conteneur = iObjet.SelectedItem
        Else
            Set Conteneur = iObjet
        End If
        If Conteneur.ActiveControl Is Nothing Then Exit Sub
        If TypeOf Conteneur.ActiveControl Is MSForms.TextBox Then
   'lance la procédure événementielle de sortie de l'activecontrol
            CallByName ItemByName(Conteneur.ActiveControl.Name), "Sortie", VbMethod, Cancel
        End If
    End Sub

Pour lancer la procédure événementielle du textbox ayant le focus, nous utilisons ici CallByName.
CallByName utilise comme paramètres :
> l'instance de classe devant être appelée
> le nom de la procédure à déclencher
> la méthode
> les éventuels paramètres à passer à la procédure
Il nous faut donc ici retrouver la bonne instance de classe.
Pour cela, nous allons utiliser une fonction qui boucle sur toutes les instances de classe.
Nous avons bien fait donc de déclarer notre variable tableau de contrôles en Public.
La fonction est donc la suivante :
  Private Function ItemByName(Nom As String) As cTextbox
    Dim i As Integer
        For i = LBound(mesTB) To UBound(mesTB)
            If mesTB(i).Nom = Nom Then
                Set ItemByName = mesTB(i): Exit Function
            End If
        Next i
    End Function


Et voici donc, fort de ces deux méthodes, la procédure de Sortie :
    Public Sub Sortie(ByVal Cancel As MSForms.ReturnBoolean)
'Attribute Sortie.VB_UserMemId = -2147384829
        If TypeOf iObjet Is MSForms.MultiPage Or TypeOf iObjet Is MSForms.Page Or TypeOf iObjet Is MSForms.Frame Then
            Call Conteneur_Sortie(Cancel)
        Else
            If iObjet.Text = "" Then Cancel = True
        End If
        Focus = CBool(Cancel)
        If Not Focus Then
            If TypeOf iObjet Is MSForms.TextBox Then iObjet.BackColor = COULEUR_INITIALE
        End If
    End Sub

Conclusion

Mes sources :
Implementation of the event handling by API: ConnectToConnectionPoint :
http://www.h3.dion.ne.jp/~sakatsu/Breakthrough_P-Ctrl_Arrays_Eng_ref.htm#C2CP
TextBox Exit & Enter :
http://codes-sources.commentcamarche.net/forum/affich-10062379-classe-de-textbox-et-evenements-exit-et-enter

Un classeur exemple est disponible au téléchargement ici : http://www.cjoint.com/c/FEtmwqoV1CE
Un exemple d'utilisation ici :
http://codes-sources.commentcamarche.net/source/101489-interface-en-vba-implements

N'hésitez pas à me contacter pour de plus amples informations.

A voir également :

Ce document intitulé «  Gestion des événements Exit et Enter de textbox créés dynamiquement  » issu de CommentCaMarche (www.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.