1592 Stimmen

Wie aktualisiere ich die GUI von einem anderen Thread aus?

Welches ist der einfachste Weg, eine Label von einem anderen Thread ?

  • Tengo un Form weiterlaufend thread1 und davon ausgehend eröffne ich ein weiteres Thema ( thread2 ).

  • Während thread2 einige Dateien verarbeitet, würde ich gerne eine Label sur le Form mit dem aktuellen Status von thread2 Arbeit.

Wie könnte ich das tun?

28 Stimmen

Hat .net 2.0+ nicht die BackgroundWorker-Klasse genau für diesen Zweck. Es UI Thread bewusst. 1. Erstellen Sie einen BackgroundWorker 2. Fügen Sie zwei Delegierte hinzu (einen für die Verarbeitung und einen für den Abschluss)

15 Stimmen

4 Stimmen

Siehe die Antwort für .NET 4.5 und C# 5.0: stackoverflow.com/a/18033198/2042090

72voto

Hath Punkte 12288

Dies ist der klassische Weg, den Sie einschlagen sollten:

using System;
using System.Windows.Forms;
using System.Threading;

namespace Test
{
    public partial class UIThread : Form
    {
        Worker worker;

        Thread workerThread;

        public UIThread()
        {
            InitializeComponent();

            worker = new Worker();
            worker.ProgressChanged += new EventHandler<ProgressChangedArgs>(OnWorkerProgressChanged);
            workerThread = new Thread(new ThreadStart(worker.StartWork));
            workerThread.Start();
        }

        private void OnWorkerProgressChanged(object sender, ProgressChangedArgs e)
        {
            // Cross thread - so you don't get the cross-threading exception
            if (this.InvokeRequired)
            {
                this.BeginInvoke((MethodInvoker)delegate
                {
                    OnWorkerProgressChanged(sender, e);
                });
                return;
            }

            // Change control
            this.label1.Text = e.Progress;
        }
    }

    public class Worker
    {
        public event EventHandler<ProgressChangedArgs> ProgressChanged;

        protected void OnProgressChanged(ProgressChangedArgs e)
        {
            if(ProgressChanged!=null)
            {
                ProgressChanged(this,e);
            }
        }

        public void StartWork()
        {
            Thread.Sleep(100);
            OnProgressChanged(new ProgressChangedArgs("Progress Changed"));
            Thread.Sleep(100);
        }
    }

    public class ProgressChangedArgs : EventArgs
    {
        public string Progress {get;private set;}
        public ProgressChangedArgs(string progress)
        {
            Progress = progress;
        }
    }
}

Ihr Worker-Thread hat ein Ereignis. Ihr UI-Thread startet einen anderen Thread, um die Arbeit zu erledigen, und hakt das Worker-Ereignis ein, damit Sie den Status des Worker-Threads anzeigen können.

In der Benutzeroberfläche müssen Sie dann die Threads überqueren, um das eigentliche Steuerelement zu ändern, z. B. ein Etikett oder einen Fortschrittsbalken.

69voto

OregonGhost Punkte 22841

Die einfache Lösung ist die Verwendung von Control.Invoke .

void DoSomething()
{
    if (InvokeRequired) {
        Invoke(new MethodInvoker(updateGUI));
    } else {
        // Do Something
        updateGUI();
    }
}

void updateGUI() {
    // update gui here
}

0 Stimmen

Gut gemacht für die Einfachheit! nicht nur einfach, sondern funktioniert auch gut! Ich habe wirklich nicht verstanden, warum microsoft konnte es nicht einfacher machen, wie es sein soll! für den Aufruf 1 Zeile auf dem Haupt-Thread, sollten wir ein paar Funktionen schreiben!

1 Stimmen

@MBH Zustimmen. Übrigens, haben Sie bemerkt stackoverflow.com/a/3588137/199364 Antwort oben, die eine Erweiterungsmethode definiert? Machen Sie das einmal in einer benutzerdefinierten Dienstprogrammklasse, dann müssen Sie sich nicht mehr darum kümmern, dass Microsoft es nicht für uns getan hat :)

0 Stimmen

@ToolmakerSteve Das ist genau das, was es sein sollte! Sie haben Recht, wir können einen Weg finden, aber ich meine von DRY (wiederholen Sie sich nicht) Sicht, das Problem, das gemeinsame Lösung hat, kann von ihnen mit minimalem Aufwand von Microsoft gelöst werden, die eine Menge Zeit für Programmierer sparen wird :)

52voto

Ohad Schneider Punkte 34748

Die überwiegende Mehrheit der Antworten verwendet Control.Invoke die eine Wettlaufbedingung, die nur darauf wartet, zu passieren . Nehmen wir zum Beispiel die akzeptierte Antwort:

string newText = "abc"; // running on worker thread
this.Invoke((MethodInvoker)delegate { 
    someLabel.Text = newText; // runs on UI thread
});

Wenn der Benutzer das Formular schließt, kurz bevor this.Invoke aufgerufen wird (zur Erinnerung, this ist die Form Objekt), ein ObjectDisposedException wird wahrscheinlich entlassen werden.

Die Lösung ist die Verwendung von SynchronizationContext und zwar SynchronizationContext.Current als hamilton.danielb suggeriert (andere Antworten beziehen sich auf spezifische SynchronizationContext Implementierungen, was völlig unnötig ist). Ich würde seinen Code leicht modifizieren und Folgendes verwenden SynchronizationContext.Post statt SynchronizationContext.Send (da der Worker-Thread normalerweise nicht warten muss):

public partial class MyForm : Form
{
    private readonly SynchronizationContext _context;
    public MyForm()
    {
        _context = SynchronizationContext.Current
        ...
    }

    private MethodOnOtherThread()
    {
         ...
         _context.Post(status => someLabel.Text = newText,null);
    }
}

Beachten Sie, dass Sie unter .NET 4.0 und höher eigentlich Tasks für asynchrone Operationen verwenden sollten. Siehe n-san's Antwort für den gleichwertigen aufgabenbasierten Ansatz (mit TaskScheduler.FromCurrentSynchronizationContext ).

Schließlich können Sie unter .NET 4.5 und höher auch Progress<T> (die im Wesentlichen Folgendes erfasst SynchronizationContext.Current bei seiner Gründung), wie die folgenden Beispiele zeigen Ryszard Dzegans für Fälle, in denen die lang laufende Operation UI-Code ausführen muss, während sie noch arbeitet.

0 Stimmen

Da der _Kontext eine Variable in Myform Wenn sie entsorgt wird, würde dann nicht auch Ihre Lösung scheitern?

0 Stimmen

@AaA nein, weil entsorgend MyForm wird nichts tun, um _context wird es immer noch SynchronizationContext.Current (die immer gültig wäre, solange die WinForms-Anwendung läuft)

0 Stimmen

Das ist richtig, aber wenn MyForm entsorgt wird, wird dann nicht auch das someLabel entsorgt?

51voto

Don Kirkby Punkte 46803

Threading-Code ist oft fehlerhaft und immer schwer zu testen. Sie brauchen keinen Threading-Code zu schreiben, um die Benutzeroberfläche über eine Hintergrundaufgabe zu aktualisieren. Verwenden Sie einfach die BackgroundWorker Klasse zur Ausführung der Aufgabe und ihrer BerichtFortschritt Methode, um die Benutzeroberfläche zu aktualisieren. Normalerweise melden Sie nur einen Prozentsatz der Fertigstellung, aber es gibt eine weitere Überladung, die ein Statusobjekt enthält. Hier ist ein Beispiel, das nur ein String-Objekt meldet:

    private void button1_Click(object sender, EventArgs e)
    {
        backgroundWorker1.WorkerReportsProgress = true;
        backgroundWorker1.RunWorkerAsync();
    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        Thread.Sleep(5000);
        backgroundWorker1.ReportProgress(0, "A");
        Thread.Sleep(5000);
        backgroundWorker1.ReportProgress(0, "B");
        Thread.Sleep(5000);
        backgroundWorker1.ReportProgress(0, "C");
    }

    private void backgroundWorker1_ProgressChanged(
        object sender, 
        ProgressChangedEventArgs e)
    {
        label1.Text = e.UserState.ToString();
    }

Das ist in Ordnung, wenn Sie immer das gleiche Feld aktualisieren wollen. Wenn Sie kompliziertere Aktualisierungen vornehmen müssen, können Sie eine Klasse definieren, die den UI-Status darstellt, und diese an die ReportProgress-Methode übergeben.

Ein letzter Punkt: Stellen Sie sicher, dass Sie die WorkerReportsProgress Flagge, oder die ReportProgress Methode wird vollständig ignoriert.

2 Stimmen

Am Ende der Verarbeitung ist es auch möglich, die Benutzeroberfläche über backgroundWorker1_RunWorkerCompleted .

43voto

Frederik Gheysels Punkte 54908

Sie müssen sicherstellen, dass die Aktualisierung im richtigen Thread erfolgt, nämlich dem UI-Thread.

Um dies zu tun, müssen Sie den Event-Handler aufrufen, anstatt ihn direkt aufzurufen.

Sie können dies tun, indem Sie Ihr Ereignis wie folgt anheben:

(Der Code ist hier aus dem Kopf getippt, ich habe ihn also nicht auf korrekte Syntax usw. geprüft, aber er sollte Ihnen weiterhelfen).

if( MyEvent != null )
{
   Delegate[] eventHandlers = MyEvent.GetInvocationList();

   foreach( Delegate d in eventHandlers )
   {
      // Check whether the target of the delegate implements 
      // ISynchronizeInvoke (Winforms controls do), and see
      // if a context-switch is required.
      ISynchronizeInvoke target = d.Target as ISynchronizeInvoke;

      if( target != null && target.InvokeRequired )
      {
         target.Invoke (d, ... );
      }
      else
      {
          d.DynamicInvoke ( ... );
      }
   }
}

Beachten Sie, dass der obige Code in WPF-Projekten nicht funktioniert, da WPF-Steuerelemente nicht die ISynchronizeInvoke Schnittstelle.

Um sicherzustellen, dass der obige Code mit Windows Forms und WPF und allen anderen Plattformen funktioniert, können Sie einen Blick auf die AsyncOperation , AsyncOperationManager y SynchronizationContext Klassen.

Um Ereignisse auf diese Weise einfach auslösen zu können, habe ich eine Erweiterungsmethode erstellt, die es mir ermöglicht, das Auslösen eines Ereignisses durch einen einfachen Aufruf zu vereinfachen:

MyEvent.Raise(this, EventArgs.Empty);

Natürlich können Sie auch die Klasse BackGroundWorker verwenden, die diese Angelegenheit für Sie abstrahiert.

0 Stimmen

In der Tat, aber ich mag es nicht, meinen GUI-Code mit diesem Thema zu überladen. Mein GUI sollte sich nicht darum kümmern, ob es Invoke benötigt oder nicht. Mit anderen Worten: Ich denke nicht, dass es die Aufgabe der GUI ist, den Kontext mit c zu erstellen.

1 Stimmen

Den Delegierten zu zerlegen usw. scheint übertrieben - warum nicht einfach: SynchronizationContext.Current.Send(delegate { MyEvent(...); }, null);

0 Stimmen

Haben Sie immer Zugriff auf den SynchronizationContext? Auch wenn Ihre Klasse in einer Klassenbibliothek enthalten ist?

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X