VBA DoEvents, problèmes et solutions

Ce tutoriel est une partie de : celui-ci.

Qu'est-ce que DoEvents ?

La fonction DoEvents suspend l'exécution de la macro afin de rendre la main au système d'exploitation. Celui-ci peut alors traiter les messages en attente dans ses files d'attentes.
DoEvents retourne un Integer représentant le nombre de feuilles ouvertes dans les versions autonomes de Visual Basic (Visual Basic édition professionnelle, par exemple). Elle renvoie un zéro dans toutes les autres applications.

Quand utilise-t-on DoEvents ?

A chaque fois que l'on doit rendre la main au système pour qu'il exécute une éventuelle action de l'utilisateur (clic de souris, frappes clavier etc.).

Cas des Entrées/Sorties (I/O ou E/S) :

Le DoEvents peut améliorer la vitesse d'exécution dans le cas d'échange d'informations entre le processeur et des périphériques qui lui sont associés. Si votre code attend une entrée d'un tel dispositif, DoEvents accélère l'application en « multitâches ». En conséquence, l'ordinateur ne semble pas suspendre ou cesser de répondre (se bloquer), tandis que le code s'exécute.

Cas d'une boucle d'attente :

DoEvents, dans ce cas, permet à votre système de traiter d'autres informations, d'autres messages en attente. C'est le cas, par exemple, si vous devez attendre le rafraichissement d'une page Internet.

Do Until IE.ReadyState = READYSTATE_COMPLETE
   DoEvents
Loop

Cas d'un objet généré par appel au système d'exploitation :

Lorsque votre code génère un objet géré par un appel à votre système d'exploitation, il faut que cet objet soit totalement disponible pour pouvoir s'exécuter correctement. L'exemple sur le site de Microsoft démarre l'application Word grâce à la commande Shell. Si Word n'est pas encore ouvert, tout effort visant à établir un lien vers elle arrêtera le code. En utilisant DoEvents, votre procédure permet de s'assurer qu'une opération, telles que Shell, est complètement exécutée avant l'instruction de macro suivante est traitée.

z% = Shell("WinWord Source.Doc",1)
DoEvents

Cas de l'appel d'une application qui a besoin d'une autre :

On imagine ici l'appel d'une application qui nécessite une autre application pour obtenir des données. Si votre code ne laisse pas la main pour que se réalise cet échange entre les deux applications, le résultat sera erroné. DoEvents lors de la communication entre les deux applications permet d'éviter ce type de blocage.

Cas du rafraichissement de l'affichage :

Lorsque votre code affiche, dans une Form, par exemple, des informations les unes après les autres, si vous ne souhaitez pas voir le résultat qu'en fin de macro, il vous faudra y intégrer des DoEvents.
Voyez ce test avec un UserForm en VBA :

Private Sub CommandButton1_Click()
  TextBox1 = "Texte 1"
  DoEvents
  For i = 1 To 50000000: Next
  TextBox2 = "Texte 2"
  DoEvents
  For i = 1 To 50000000: Next
  TextBox3 = "Texte 3"
End Sub

Faites ce test avec et sans les 2 DoEvents.

Problèmes liés à l'utilisation de DoEvents

Imbrications de DoEvents

Utiliser trop d'instructions DoEvents imbriquées peut épuiser l'espace de pile et donc générer un message d'erreur « espace de pile insuffisant ». Cette erreur fait référence à l'espace de pile d'application attribué à la demande de l'application.
De la même manière, utiliser DoEvents en boucle ne peut que solliciter abusivement le processeur et en provoquer la surchauffe. Il ne s'agit d'augmentation de mémoire utilisée, mais de sollicitation abusive.

Exécution d'une procédure événementielle

Lorsque vous rendez la main de manière temporaire à votre processeur dans une procédure d'événement, veillez à ce que la procédure ne soit pas exécutée à nouveau par une autre portion de votre programme, avant que le résultat du premier appel ne soit renvoyé. Cela peut provoquer des résultats imprévisibles.

Reprise du contrôle

Une fois que DoEvents cède le contrôle du système d'exploitation, il n'est pas possible de déterminer quand votre application va reprendre le contrôle. Après que le système d'exploitation prend le contrôle du processeur, il traite tous les événements en attente qui sont actuellement dans la file d'attente de messages (tels que les clics de souris et les frappes de clavier). Cela peut ne pas convenir pour certaines applications d'acquisition de données en temps réel.

Quantité de traitements

DoEvents a tendance à ralentir le système, car il augmente la quantité de traitement que le système doit effectuer pour chaque message. Vous devez n'utiliser DoEvents que lorsque cela est nécessaire, et « l'arrêter » dès que possible.

Illustrations / Exemples (à ne pas reproduire dans vos codes !) :

Double lancement d'une procédure événementielle

L'exemple du clic sur un bouton de commande est le plus fréquent. Que se passe t'il lorsque l'utilisateur clic deux fois (par impatience) sur un bouton dont le code contient un DoEvents ?

Dan Tohatan :
Imaginez ceci : Vous avez un bouton sur votre formulaire qui, une fois cliqué, fait un traitement complexe. Pendant ce traitement complexe, il appelle également de façon intermittente DoEvents afin de garder l'interface utilisateur de l'application "réactive" - pas la meilleure méthode, mais nous parlons d'un programmeur médiocre ici. Quoi qu'il en soit, l'utilisateur voit l'application toujours en action mais sans aucune indication qu'il y a un processus en cours. Ainsi, l'utilisateur clique à nouveau sur ce bouton pendant que le traitement se passe! A cause du DoEvents, La touche répond à l'événement et commence un autre thread de traitement. Donc, comme je l'ai dit plus tôt, DoEvents « fout en l'air » le flux de l'application trop facilement.

Private Sub CommandButton1_Click()
  If TextBox1 = "" Then 
TextBox1 = "Texte 1" 
  Else 
TextBox1 = "Second lancement"
  End If
  For i = 1 To 500000000
    DoEvents
  Next
End Sub

Constatations : Une fois que vous aurez cliqué deux fois sur votre bouton, vous pourrez fermer la Form grâce à la petite croix de fermeture. Cependant, votre code sera toujours en train de tourner. Vous ne pouvez pas repasser en mode création, à moins de ne « breaker » votre macro en frappant sur les touches Ctrl+Pause.

DoEvents en boucle et surchauffe du processeur

Comme nous l'avons dit précédemment : utiliser DoEvents en boucle ne peut que solliciter abusivement le processeur et en provoquer la surchauffe. Il ne s'agit d'augmentation de mémoire utilisée, mais de sollicitation abusive.

ucfoutu :
Insérez une TextBox (TextBox1) et un bouton de commande (Commandbutton1) et ce code :

Private Sub CommandButton1_Click()
  Chrono 300, Now
End Sub
Sub Chrono(duree As Double, depart As Double)
Dim p As Double
Do While depart - duree > CDbl(TimeValue(Now))
  If ArretJeu = True Then Exit Sub
  TextBox1.Value = Right(Format(duree - (CDbl(TimeValue(Now)) - depart), "hh:mm:ss"), 5)
  DoEvents
Loop
End Sub

Mettez en route le gestionnaire de tâches, onglet performances et, parallèlement lancez en cliquant sur le bouton de commande. Observez ce qui se passe alors (dans la petite "fenêtre" la plus à droite relative à l'utilisation de l'UC). L'outil constate à ce moment-là une augmentation (croissante ensuite) de la température. Sur ma vieille machine de test (utilisée exprès car moins bien réfrigérée) on entend même le ventilateur de refroidissement du processeur commencer à s'affoler.
Faites un break (CTRL + PAUSE) de la macro, tout se "calme".
Il ne faut donc pas utiliser DoEvents en boucle sur une si longue durée. Va bene sur des petites "attentes", mais pas sur de longues périodes.

DoEvents a tendance à ralentir le système

En effet, il augmente la quantité de traitement que le système doit effectuer pour chaque message.

ucfoutu :
Sur un userform :
- deux boutons (un pour lancer et l'autre pour arrêter)
- un label

Private Declare Function GetInputState Lib "user32" () As Long
Private toto As Boolean

Private Sub Commandbutton1_Click()
 Dim nb_iterations As Long
 nb_iterations = 3000000
 'le Label1 n'est là que pour choisir le moment de l'interruption souhaitée
 ' par click sur le bouton command2
 ' tester d'abord à fond, sans interrompre
 ' Vérifier ensuite que vous pouvez interrompre, dans les deux cas.
 ' Le Me.Caption vous permettra de savoir quelle boucle (avec ou sans) vous interrompez
 Label1.Move 0, 0, Me.Width, 20
 Label1.FontSize = 14
 Label1.Caption = "traitement avec Doevents seul en cours"
 Me.Caption = "tout"
 temps_sans = test(nb_iterations, False) ' false pour ne pas subordonner à la seule appli en cours
 Label1.Caption = "traitement avec Doevents limités aux événements de l'appli en cours"
 DoEvents 'ce doevents n'est là que pour permettre la mise à jour du Label
 Me.Caption = "avec contrôle"
 temps_avec = test(nb_iterations, True) ' true pour subordonner à la seule appli en cours
 If Not toto Then
   MsgBox "il a fallu :" & vbCrLf & _
   temps_sans & " secondes en utilisant uniquement Doevents" & vbCrLf & "et seulement " & _
   temps_avec & " secondes en utilisant un Doevents ne concernant que l'appli en cours" & vbCrLf & _
   "pour faire les mêmes " & nb_iterations & " itérations."
 End If
End Sub
Private Sub Commandbutton2_Click()
 toto = True ' pour interrompre la boucle
End Sub
Private Function test(nb_iterations As Long, comment As Boolean) As Long
 depart = Timer
 toto = False
 For i = 1 To nb_iterations
  If comment = True Then
    If GetInputState <> 0 Then DoEvents ' un doevents "subordonné"
  Else
    DoEvents ' un Doevents non "subordonné"
  End If
  If toto Then Exit For
 Next
 test = Timer - depart
End Function

Constatation : Il faut être patient pour 300 000 itérations. Vous pouvez réduire ce nombre si la patience n'est pas votre qualité principale. Le temps d'exécution entre les deux méthodes utilisés est saisissant.

Pourquoi avoir conservé cette fonction ?

Extrait d'une entrevue disponible ici : http://blog.codinghorror.com/is-doevents-evil/

Question: Pour qu'elle raison avoir maintenu DoEvents dans le .NET framework? Pourquoi ne pas l'avoir limité à une association dans l'espace de noms Microsoft.VisualBasic? Qu'elle est l'utilité de DoEvents lorsqu'il existe un support approprié pour les applications Multi-Thread?

Réponse: Jason (Microsoft): DoEvents est un vestige de VB 5.x, mais il est toujours utile dans le monde de .NET Framework. Si vous avez une boucle qui fonctionne pendant une longue période, il est souvent plus facile d'appeler DoEvents que de remanier votre boucle pour utiliser un véritable multitâche (threading) .NET.

Q: Pourquoi DoEvents est-il dans l'espace de noms BCL?

R: Glenn (Microsoft): Le multitâche est difficile, et devrait être évité s'il y a un moyen plus simple. Si tout ce que vous avez besoin dans votre thread d'interface utilisateur c'est du rendement, DoEvents est parfait.

Q: DoEvents est-il le diable?

A: Glenn (Microsoft) : La souplesse dans votre thread d'interface utilisateur est une pratique légitime dans la programmation Windows. Cela a toujours été. DoEvents rend les choses faciles, parce que les situations dans lesquelles vous avez besoin de l'utiliser sont simples.

Méthodes de substitution :

Que substituer à ce "mécanisme" alors ?
Plusieurs pistes s'offrent à nous.

Application.OnTime

Vous pouvez avoir besoin d'exécuter du code périodiquement et automatiquement. Pour cela, vous pouvez faire appel à la méthode OnTime de l'application afin d'exécuter automatiquement une procédure. Cette méthode permet de planifier l'exécution d'une action spécifique lorsqu'un délai donné s'est écoulé ou de planifier l'exécution d'une action à une heure définie. Elle met donc en attente l'exécution d'une action. Contrairement à d'autres méthodes (Application.Wait...), avec Application.Ontime, l'utilisateur ne perd jamais la main.
L'idée ici est de créer un appel, à intervalle régulier, de notre fonction, à partir d'elle-même. En s'auto-appelant toutes les x secondes, elle laisse le temps, à l'utilisateur, de réaliser des actions.
Chip Pearson :
http://www.cpearson.com/excel/ontime.aspx
Comme paramètres, la méthode OnTime prend l'heure précise à laquelle elle doit exécuter la procédure et le nom de la procédure à exécuter. Si vous devez annuler un OnTime, vous devez fournir l'heure exacte à laquelle l'événement a été calendrier avoir lieu. Il n'y a pas moyen de dire à Excel d'annuler le prochain événement OnTime ou d'annuler tous les événements OnTime attente. Par conséquent, vous avez besoin de stocker l'heure à laquelle la procédure est à exécuter dans une variable publique et utiliser la valeur de cette variable dans les appels à OnTime.

Illustrations :

ucfoutu :

Exemple 1
Regardez ce que fait cet exemple, sans aucun DoEvents :
Dans un module :

Public laps As Integer
Public Sub mon_timer()
  If laps <= 0 Then msgbox "c'est fini": Exit Sub
  MsgBox "coucou"
  Application.OnTime Now + TimeValue("00:00:02"), "mon_timer"
  laps = laps - 2
End Sub

Ajoutez deux boutons de commandes : CommandButton1 et CommandButton2.
Leurs codes respectifs seront :

Private Sub CommandButton1_Click()
'code d'appel de la fonction montimer avec OnTime
laps = 300
Application.OnTime Now + TimeValue("00:00:01"), "mon_timer"
End Sub
Private Sub CommandButton2_Click() 'Arret Ontime
laps = 0
End Sub

Toutes les deux secondes, la procédure mon_timer est appelée par OnTime. L'utilisateur peut continuer de travailler entre ces appels.

Exemple 2
Dans cet exemple, il s'agit d'alimenter une boucle (pour une fausse progress_barre par exemple) tout en maintenant le bouton appuyé. La boucle stoppe lorsque l'utilisateur « relâche » le bouton.
Code du bouton de commande :

Private Sub CommandButton1_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
 actif = True
 Application.OnTime Now, "BarreDeProgression" ' on "lance" sans attendre
End Sub
Private Sub CommandButton1_MouseUp(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
  actif = False
End Sub

Dans un Module :

Private der As Double, cpt As Long
Public actif As Boolean
Public Sub BarreDeProgression()
  If Timer > der + 0.05 Or der = 0 Then
    cpt = cpt + 1 '                   | ou toutes autres
    UserForm3.Label1.Width = cpt '  | actions de ton choix
    DoEvents ' un doevents qui n'interviendra que si condition remplie
             ' (si + de 0,05 sec écoulées)
    der = Timer
  End If
  If actif Then
    Application.OnTime Now, "BarreDeProgression" ' on "relance" aussitôt"
  Else
    cpt = 0
    der = 0
  End If
End Sub

Function GetInputState - Un DoEvents maîtrisé

Cette fonction ne se substitue pas à DoEvents, mais va nous permettre d'en contrôler et donc de maitriser son utilisation.
Cette API Windows, fonction de type Boolean, intercepte le clic de la souris ou la frappe des touches de votre clavier. Tant qu'aucune de ces actions n'est faite par l'utilisateur, elle retourne 0. Si un clic ou une touche a été pressée, elle retourne une valeur non-nulle.
L'intérêt ici est de n'utiliser DoEvents qu'à bon escient, uniquement lorsque l'utilisateur réalise une action dans le cadre de votre application. En utilisant GetInputState, vous limitez le nombre de DoEvents aux seuls événements de l'application en cours.
En guise d'exemple, vous pouvez vous reportez au chapitre Illustrations / Exemples, paragraphe DoEvents a tendance à ralentir le système

Sans DoEvent - Timer Event

Cette méthode de substitution utilise les API SetTimer et KillTimer pour créer une procédure événementielle. Celle-ci va donc "lancer" à intervalles réguliers une fonction, que vous désignerez dans le code.
Cet exemple est basé sur celui disponible sur le site de microsoft à cette adresse : http://support.microsoft.com/kb/180736/fr
Il va s'agir d'alimenter progressivement une progressBar (ici un label dont la propriété Width sera incrémentée de 1 en 1) pendant que l'utilisateur saisit des données dans un TextBox.
Dans cet exemple, l'intervalle d'exécution est de 200 millisecondes (0,2 sec) et la fonction a pour nom : TimerProc

Dans un module :

          Option Explicit
'Les API
      Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long, ByVal uElapse As Long, _
            ByVal lpTimerFunc As Long) As Long
      Declare Function KillTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
'variables globales
      Public iCounter As Integer
      Public lngTimerID As Long
      Public BlnTimer As Boolean
'Votre procédure à déclencher à intervalles réguliers
      Sub TimerProc(ByVal hwnd As Long, ByVal uMsg As Long, ByVal idEvent As Long, ByVal dwTime As Long)
          iCounter = iCounter + 1
       'Pour stopper la relance de cette procédure depuis elle-même :
          'If iCounter = 50 Then Call Arret_Timer: Exit Sub
          UserForm1.Label1.Width = UserForm1.Label1.Width + 1
          UserForm1.Label1.Caption = CStr(iCounter)
      End Sub
'La procédure de commencement du Timer
      Public Sub Lance_Timer()
        '200 = intervalle en millisecondes
        'TimerProc = nom de la procédure à relancer toutes les 200ms
        lngTimerID = SetTimer(0, 0, 200, AddressOf TimerProc)
        If lngTimerID = 0 Then
            MsgBox "Timer non créé. Fin du programme."
            Exit Sub
        End If
        BlnTimer = True
      End Sub
'La procédure d'arrêt du Timer
      Public Sub Arret_Timer()
        lngTimerID = KillTimer(0, lngTimerID)
        If lngTimerID = 0 Then
            MsgBox "Ne peut pas arrêter le Timer."
        Else
            UserForm1.CommandButton1.Caption = "En cliquant vous lancerez" & Chr(13) & _
               "la ""progress-barre"" et " & Chr(13) & _
               "pourrez toujours saisir dans le textbox"
            UserForm1.Label1.Width = 0
        End If
        BlnTimer = False
      End Sub

Dans un UserForm (UserForm1), placez : un CommandButton (CommandButton1), un Label (Label1), un TextBox (TextBox1) et un second bouton (CommandButton2).
Le code :

        Option Explicit
Private Sub CommandButton1_Click()
   If BlnTimer = False Then
      Call Lance_Timer
      Label1.Width = 0
      CommandButton1.Caption = "Stop"
   Else
      Call Arret_Timer
   End If
End Sub
Private Sub CommandButton2_Click()
   [G33] = TextBox1
   TextBox1 = ""
End Sub
Private Sub UserForm_Initialize()
  BlnTimer = False
  CommandButton1.Caption = "En cliquant vous lancerez" & Chr(13) & _
      "la ""progress-barre"" et " & Chr(13) & _
      "pourrez toujours saisir dans le textbox"
  CommandButton2.Caption = "Insérer TextBox1.Text en cellule G33"
End Sub

Nota : Si le code exécuté par le Timer change une valeur de cellule, et que vous êtes en mode d'édition dans Excel, pour, par exemple, la saisie des données dans une cellule, Excel va probablement planter complètement et vous perdrez tout travail non enregistré. Utilisez Windows Timer avec prudence.

Conseils :

  • Si vous démarrez un Timer, assurez-vous de l'arrêter. A la différence avec une boucle sans fin, vous ne vous apercevrez pas nécessairement du non-arrêt de votre Timer et ainsi consommerez de la mémoire inutilement. En cas de souci, fermez Excel.
  • N'utilisez qu'un Timer à la fois. Ne débutez jamais un nouveau Timer sans avoir préalablement arrêté le premier.
  • Une procédure utilisant ce type d'API n'est pas nécessairement portable d'une machine à l'autre. A tester par conséquent sur toutes les machines devant utiliser votre code.

Conclusion :

DoEvents devrait n'être utilisé que pour des choses simples comme permettre à l'utilisateur d'annuler un processus après qu'il ait commencé ou le rafraichissement d'un UserForm. Pour les opérations nécessitant une exécution plus longue, l'opérateur pourra plus aisément rendre la main au processeur s'il a recours à un contrôle Timer ou à un composant EXE ActiveX. Dans ce cas, la tâche s'effectue de manière complètement indépendante, hors de votre application, le système d'exploitation gérant à la fois le multitâche et le partage du temps.

Un très grand merci à ucfoutu. Rendons à César...

Sources et saines lectures :

http://www.excel-downloads.com/forum/33720-doevents-explication.html

http://blog.codinghorror.com/is-doevents-evil/

http://support.microsoft.com/kb/118468/fr

http://support.microsoft.com/kb/180736/fr

http://www.cpearson.com/excel/ontime.aspx

Ce tutoriel est un complément de : celui-ci. Vous pourrez y trouver un exemple d'utilisation.

A voir également
Ce document intitulé « VBA DoEvents, problèmes et solutions » issu de CodeS SourceS (codes-sources.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.
Rejoignez-nous