web-dev-qa-db-fra.com

Comment appeler une méthode d'interface utilisateur à partir d'un autre thread

Jouer avec Timers. Contexte: un winforms avec deux étiquettes.

Je voudrais voir comment System.Timers.Timer fonctionne donc je n'ai pas utilisé le minuteur Forms. Je comprends que le formulaire et myTimer s'exécuteront désormais dans différents threads. Existe-t-il un moyen simple de représenter le temps écoulé sur lblValue sous la forme suivante?

J'ai regardé ici sur MSDN mais y a-t-il un moyen plus simple!

Voici le code winforms:

using System.Timers;

namespace Ariport_Parking
{
  public partial class AirportParking : Form
  {
    //instance variables of the form
    System.Timers.Timer myTimer;
    int ElapsedCounter = 0;

    int MaxTime = 5000;
    int elapsedTime = 0;
    static int tickLength = 100;

    public AirportParking()
    {
        InitializeComponent();
        keepingTime();
        lblValue.Text = "hello";
    }

    //method for keeping time
    public void keepingTime() {

        myTimer = new System.Timers.Timer(tickLength); 
        myTimer.Elapsed += new ElapsedEventHandler(myTimer_Elapsed);

        myTimer.AutoReset = true;
        myTimer.Enabled = true;

        myTimer.Start();
    }


    void myTimer_Elapsed(Object myObject,EventArgs myEventArgs){

        myTimer.Stop();
        ElapsedCounter += 1;
        elapsedTime += tickLength; 

        if (elapsedTime < MaxTime)
        {
            this.lblElapsedTime.Text = elapsedTime.ToString();

            if (ElapsedCounter % 2 == 0)
                this.lblValue.Text = "hello world";
            else
                this.lblValue.Text = "hello";

            myTimer.Start(); 

        }
        else
        { myTimer.Start(); }

    }
  }
}
21
whytheq

Je suppose que votre code n'est qu'un test, donc je ne discuterai pas de ce que vous faites avec votre minuterie. Le problème ici est de savoir comment faire quelque chose avec un contrôle d'interface utilisateur dans votre rappel de minuterie.

La plupart des méthodes et propriétés de Control sont accessibles uniquement à partir du thread d'interface utilisateur (en réalité, elles ne sont accessibles qu'à partir du thread où vous les avez créées, mais c'est une autre histoire). C'est parce que chaque thread doit avoir sa propre boucle de message (GetMessage() filtre les messages par thread), puis pour faire quelque chose avec un Control, vous devez envoyer un message de votre thread vers le thread principal . Dans .NET, c'est facile car chaque Control hérite de quelques méthodes à cet effet: Invoke/BeginInvoke/EndInvoke. Pour savoir si le thread d'exécution doit appeler ces méthodes, vous avez la propriété InvokeRequired. Modifiez simplement votre code avec ceci pour le faire fonctionner:

if (elapsedTime < MaxTime)
{
    this.BeginInvoke(new MethodInvoker(delegate 
    {
        this.lblElapsedTime.Text = elapsedTime.ToString();

        if (ElapsedCounter % 2 == 0)
            this.lblValue.Text = "hello world";
        else
            this.lblValue.Text = "hello";
    }));
}

Veuillez consulter MSDN pour la liste des méthodes que vous pouvez appeler à partir de n'importe quel thread, tout comme référence, vous pouvez toujours appeler les méthodes Invalidate, BeginInvoke, EndInvoke, Invoke et pour lire la propriété InvokeRequired. En général, il s'agit d'un modèle d'utilisation courant (en supposant que this est un objet dérivé de Control):

void DoStuff() {
    // Has been called from a "wrong" thread?
    if (InvokeRequired) {
        // Dispatch to correct thread, use BeginInvoke if you don't need
        // caller thread until operation completes
        Invoke(new MethodInvoker(DoStuff));
    } else {
        // Do things
    }
}

Notez que le thread actuel se bloquera jusqu'à ce que le thread d'interface utilisateur ait terminé l'exécution de la méthode. Cela peut être un problème si le timing du thread est important (n'oubliez pas que le thread de l'interface utilisateur peut être occupé ou suspendu un peu). Si vous n'avez pas besoin de la valeur de retour de la méthode, vous pouvez simplement remplacer Invoke par BeginInvoke, pour WinForms, vous n'avez même pas besoin d'un appel ultérieur à EndInvoke:

void DoStuff() {
    if (InvokeRequired) {
        BeginInvoke(new MethodInvoker(DoStuff));
    } else {
        // Do things
    }
}

Si vous avez besoin d'une valeur de retour, vous devez faire face à l'interface habituelle IAsyncResult.

Comment ça fonctionne?

Une application Windows GUI est basée sur la procédure de fenêtre avec ses boucles de messages. Si vous écrivez une application en C simple, vous avez quelque chose comme ceci:

MSG message;
while (GetMessage(&message, NULL, 0, 0))
{
    TranslateMessage(&message);
    DispatchMessage(&message);
}

Avec ces quelques lignes de code, votre application attend un message, puis remet le message à la procédure de fenêtre. La procédure de fenêtre est une grande instruction switch/case où vous vérifiez les messages (WM_) vous savez et vous les traitez en quelque sorte (vous peignez la fenêtre pour WM_Paint, vous quittez votre application pour WM_QUIT etc).

Imaginez maintenant que vous avez un thread de travail, comment pouvez-vous appeler votre thread principal? Le moyen le plus simple consiste à utiliser cette structure sous-jacente pour faire l'affaire. Je simplifie trop la tâche, mais voici les étapes:

  • Créez une file d'attente (thread-safe) de fonctions à invoquer (quelques exemples ici sur SO ).
  • Publiez un message personnalisé dans la procédure de fenêtre. Si vous faites de cette file d'attente une file d'attente prioritaire, vous pouvez même décider de la priorité de ces appels (par exemple, une notification de progression à partir d'un thread de travail peut avoir une priorité inférieure à une notification d'alarme).
  • Dans la procédure de fenêtre (à l'intérieur de votre instruction switch/case) vous comprenez ce message, vous pouvez alors jeter un œil à la fonction à appeler à partir de la file d'attente et à l'invoquer.

WPF et WinForms utilisent cette méthode pour délivrer (envoyer) un message d'un thread au thread d'interface utilisateur. Jetez un oeil à cet article sur MSDN pour plus de détails sur plusieurs threads et interface utilisateur, WinForms cache beaucoup de ces détails et vous n'avez pas à vous en occuper mais vous pouvez jeter un oeil pour comprendre comment cela fonctionne sous le capot.

35
Adriano Repetti

Personnellement, lorsque je travaille dans une application qui fonctionne avec des threads hors de l'interface utilisateur, j'écris généralement ce petit extrait.

private void InvokeUI(Action a)
{
    this.BeginInvoke(new MethodInvoker(a));
}

Lorsque je fais un appel asynchrone dans un thread différent, je peux toujours rappeler en utilisant.

InvokeUI(() => { 
   Label1.Text = "Super Cool";
});

Simple et propre.

7
Theodor Solbjørg

Comme demandé, voici ma réponse qui vérifie les appels croisés, synchronise les mises à jour variables, n'arrête pas et ne démarre pas le minuteur et n'utilise pas le minuteur pour compter le temps écoulé.

MODIFIER appel BeginInvoke fixe. J'ai fait l'invocation croisée en utilisant un Action générique, cela permet de transmettre l'expéditeur et les arguments d'événement. Si ceux-ci ne sont pas utilisés (comme ils sont ici), il est plus efficace d'utiliser MethodInvoker mais je soupçonne que la gestion devrait être déplacée dans une méthode sans paramètre.

public partial class AirportParking : Form
{
    private Timer myTimer = new Timer(100);
    private int elapsedCounter = 0;
    private readonly DateTime startTime = DateTime.Now;

    private const string EvenText = "hello";
    private const string OddText = "hello world";

    public AirportParking()
    {
        lblValue.Text = EvenText;
        myTimer.Elapsed += MyTimerElapsed;
        myTimer.AutoReset = true;
        myTimer.Enabled = true;
        myTimer.Start();
    }

    private void MyTimerElapsed(object sender,EventArgs myEventArgs)
    {
        If (lblValue.InvokeRequired)
        {
            var self = new Action<object, EventArgs>(MyTimerElapsed);
            this.BeginInvoke(self, new [] {sender, myEventArgs});
            return;   
        }

        lock (this)
        {
            lblElapsedTime.Text = DateTime.Now.SubTract(startTime).ToString();
            elapesedCounter++;
            if(elapsedCounter % 2 == 0)
            {
                lblValue.Text = EvenText;
            }
            else
            {
                lblValue.Text = OddText;
            }
        }
    }
}
2
Jodrell

Tout d'abord, dans Windows Forms (et la plupart des frameworks), un contrôle n'est accessible (sauf si documenté comme "thread-safe") que par le thread d'interface utilisateur.

Donc this.lblElapsedTime.Text = ... dans votre rappel est tout simplement faux. Jetez un oeil à Control.BeginInvoke .

Deuxièmement, vous devez utiliser System.DateTime et System.TimeSpan pour vos calculs de temps.

Non testé:

DateTime startTime = DateTime.Now;

void myTimer_Elapsed(...) {
  TimeSpan elapsed = DateTime.Now - startTime;
  this.lblElapsedTime.BeginInvoke(delegate() {
    this.lblElapsedTime.Text = elapsed.ToString();
  });
}
1
Nicolas Repiquet

J'ai fini par utiliser ce qui suit. C'est une combinaison des suggestions données:

using System.Timers;

namespace Ariport_Parking
{
  public partial class AirportParking : Form
  {

    //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    //instance variables of the form
    System.Timers.Timer myTimer;

    private const string EvenText = "hello";
    private const string OddText = "hello world";

    static int tickLength = 100; 
    static int elapsedCounter;
    private int MaxTime = 5000;
    private TimeSpan elapsedTime; 
    private readonly DateTime startTime = DateTime.Now; 
    //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<


    public AirportParking()
    {
        InitializeComponent();
        lblValue.Text = EvenText;
        keepingTime();
    }

    //method for keeping time
    public void keepingTime() {

    using (System.Timers.Timer myTimer = new System.Timers.Timer(tickLength))
    {  
           myTimer.Elapsed += new ElapsedEventHandler(myTimer_Elapsed);
           myTimer.AutoReset = true;
           myTimer.Enabled = true;
           myTimer.Start(); 
    }  

    }

    private void myTimer_Elapsed(Object myObject,EventArgs myEventArgs){

        elapsedCounter++;
        elapsedTime = DateTime.Now.Subtract(startTime);

        if (elapsedTime.TotalMilliseconds < MaxTime) 
        {
            this.BeginInvoke(new MethodInvoker(delegate
            {
                this.lblElapsedTime.Text = elapsedTime.ToString();

                if (elapsedCounter % 2 == 0)
                    this.lblValue.Text = EvenText;
                else
                    this.lblValue.Text = OddText;
            })); 
        } 
        else {myTimer.Stop();}
      }
  }
}
0
whytheq