Как мне обновить графический интерфейс из другого потока? - PullRequest
1279 голосов
/ 19 марта 2009

Какой самый простой способ обновить Label из другого потока?

У меня Form на thread1, и с этого я запускаю другой поток (thread2). Пока thread2 обрабатывает некоторые файлы, я бы хотел обновить Label на Form с текущим статусом thread2.

Как я могу это сделать?

Ответы [ 47 ]

1013 голосов
/ 19 марта 2009

Самый простой способ - это анонимный метод, переданный в Label.Invoke:

// Running on the worker thread
string newText = "abc";
form.Label.Invoke((MethodInvoker)delegate {
    // Running on the UI thread
    form.Label.Text = newText;
});
// Back on the worker thread

Обратите внимание, что Invoke блокирует выполнение до его завершения - это синхронный код. Вопрос не касается асинхронного кода, но в переполнении стека содержится множество контента о написании асинхронного кода, когда вы хотите узнать о нем.

741 голосов
/ 19 марта 2009

Для .NET 2.0, вот хороший фрагмент кода, который я написал, который делает именно то, что вы хотите, и работает для любого свойства в Control:

private delegate void SetControlPropertyThreadSafeDelegate(
    Control control, 
    string propertyName, 
    object propertyValue);

public static void SetControlPropertyThreadSafe(
    Control control, 
    string propertyName, 
    object propertyValue)
{
  if (control.InvokeRequired)
  {
    control.Invoke(new SetControlPropertyThreadSafeDelegate               
    (SetControlPropertyThreadSafe), 
    new object[] { control, propertyName, propertyValue });
  }
  else
  {
    control.GetType().InvokeMember(
        propertyName, 
        BindingFlags.SetProperty, 
        null, 
        control, 
        new object[] { propertyValue });
  }
}

Назовите это так:

// thread-safe equivalent of
// myLabel.Text = status;
SetControlPropertyThreadSafe(myLabel, "Text", status);

Если вы используете .NET 3.0 или выше, вы можете переписать указанный выше метод как метод расширения класса Control, что упростит вызов:

myLabel.SetPropertyThreadSafe("Text", status);

ОБНОВЛЕНИЕ 05/10/2010:

Для .NET 3.0 вы должны использовать этот код:

private delegate void SetPropertyThreadSafeDelegate<TResult>(
    Control @this, 
    Expression<Func<TResult>> property, 
    TResult value);

public static void SetPropertyThreadSafe<TResult>(
    this Control @this, 
    Expression<Func<TResult>> property, 
    TResult value)
{
  var propertyInfo = (property.Body as MemberExpression).Member 
      as PropertyInfo;

  if (propertyInfo == null ||
      !@this.GetType().IsSubclassOf(propertyInfo.ReflectedType) ||
      @this.GetType().GetProperty(
          propertyInfo.Name, 
          propertyInfo.PropertyType) == null)
  {
    throw new ArgumentException("The lambda expression 'property' must reference a valid property on this Control.");
  }

  if (@this.InvokeRequired)
  {
      @this.Invoke(new SetPropertyThreadSafeDelegate<TResult> 
      (SetPropertyThreadSafe), 
      new object[] { @this, property, value });
  }
  else
  {
      @this.GetType().InvokeMember(
          propertyInfo.Name, 
          BindingFlags.SetProperty, 
          null, 
          @this, 
          new object[] { value });
  }
}

, который использует LINQ и лямбда-выражения для обеспечения более чистого, простого и безопасного синтаксиса:

myLabel.SetPropertyThreadSafe(() => myLabel.Text, status); // status has to be a string or this will fail to compile

Теперь проверяется не только имя свойства во время компиляции, но и тип свойства, поэтому невозможно (например) присвоить строковое значение логическому свойству и, следовательно, вызвать исключение времени выполнения.

К сожалению, это не мешает кому-либо делать глупости, такие как передача другого свойства и значения Control, поэтому с радостью скомпилируется следующее:

myLabel.SetPropertyThreadSafe(() => aForm.ShowIcon, false);

Поэтому я добавил проверки во время выполнения, чтобы убедиться, что переданное свойство действительно принадлежит Control, к которому вызывается метод. Не идеально, но все же намного лучше, чем версия .NET 2.0.

Если у кого-либо есть какие-либо дополнительные предложения по улучшению этого кода для обеспечения безопасности во время компиляции, пожалуйста, оставьте комментарий!

374 голосов
/ 03 августа 2013

Обработка длинных работ

Начиная с .NET 4.5 и C # 5.0 вы должны использовать Асинхронный шаблон на основе задач (TAP) вместе с асинхронным - ждут ключевые слова во всех областях (включая GUI):

TAP - рекомендуемый шаблон асинхронного проектирования для новой разработки

вместо Модель асинхронного программирования (APM) и Асинхронный шаблон на основе событий (EAP) (последний включает BackgroundWorker Class ).

Тогда рекомендуемое решение для новой разработки:

  1. Асинхронная реализация обработчика событий (да, вот и все):

    private async void Button_Clicked(object sender, EventArgs e)
    {
        var progress = new Progress<string>(s => label.Text = s);
        await Task.Factory.StartNew(() => SecondThreadConcern.LongWork(progress),
                                    TaskCreationOptions.LongRunning);
        label.Text = "completed";
    }
    
  2. Реализация второго потока, который уведомляет поток пользовательского интерфейса:

    class SecondThreadConcern
    {
        public static void LongWork(IProgress<string> progress)
        {
            // Perform a long running work...
            for (var i = 0; i < 10; i++)
            {
                Task.Delay(500).Wait();
                progress.Report(i.ToString());
            }
        }
    }
    

Обратите внимание на следующее:

  1. Короткий и чистый код, написанный последовательным образом без обратных вызовов и явных потоков.
  2. Задание вместо Тема .
  3. async ключевое слово, которое позволяет использовать await , что, в свою очередь, не позволяет обработчику событий достигать состояния завершения до завершения задачи, а тем временем не блокирует поток пользовательского интерфейса .
  4. Класс прогресса (см. Интерфейс IProgress ), поддерживающий принцип проектирования Разделение проблем (SoC) и не требующий явного диспетчера и вызова. Он использует текущий SynchronizationContext из своего места создания (здесь поток пользовательского интерфейса).
  5. TaskCreationOptions.LongRunning , который намекает не ставить задачу в очередь в ThreadPool .

Более подробные примеры см .: Будущее C #: хорошие вещи приходят к тем, кто «ждет» от Джозеф Албахари .

См. Также о Модель интерфейса пользователя concept.

Обработка исключений

Приведенный ниже фрагмент является примером того, как обрабатывать исключения и свойство Enabled кнопки переключения, чтобы предотвращать множественные щелчки во время фонового выполнения.

private async void Button_Click(object sender, EventArgs e)
{
    button.Enabled = false;

    try
    {
        var progress = new Progress<string>(s => button.Text = s);
        await Task.Run(() => SecondThreadConcern.FailingWork(progress));
        button.Text = "Completed";
    }
    catch(Exception exception)
    {
        button.Text = "Failed: " + exception.Message;
    }

    button.Enabled = true;
}

class SecondThreadConcern
{
    public static void FailingWork(IProgress<string> progress)
    {
        progress.Report("I will fail in...");
        Task.Delay(500).Wait();

        for (var i = 0; i < 3; i++)
        {
            progress.Report((3 - i).ToString());
            Task.Delay(500).Wait();
        }

        throw new Exception("Oops...");
    }
}
219 голосов
/ 29 мая 2012

Вариация * * * * * * * * * * * * * * * * * * * * * * * * * * для *.

control.Invoke((MethodInvoker) (() => control.Text = "new text"));

Или используйте вместо этого делегат Action:

control.Invoke(new Action(() => control.Text = "new text"));

Смотрите здесь для сравнения двух: MethodInvoker против Action для Control.BeginInvoke

131 голосов
/ 28 августа 2010

Метод запуска и запуска расширений для .NET 3.5 +

using System;
using System.Windows.Forms;

public static class ControlExtensions
{
    /// <summary>
    /// Executes the Action asynchronously on the UI thread, does not block execution on the calling thread.
    /// </summary>
    /// <param name="control"></param>
    /// <param name="code"></param>
    public static void UIThread(this Control @this, Action code)
    {
        if (@this.InvokeRequired)
        {
            @this.BeginInvoke(code);
        }
        else
        {
            code.Invoke();
        }
    }
}

Это можно вызвать с помощью следующей строки кода:

this.UIThread(() => this.myLabel.Text = "Text Goes Here");
64 голосов
/ 19 марта 2009

Это классический способ сделать это:

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;
        }
    }
}

В вашем рабочем потоке есть событие. Ваш поток пользовательского интерфейса запускает другой поток для выполнения работы и подключает это рабочее событие, чтобы вы могли отобразить состояние рабочего потока.

Затем в пользовательском интерфейсе вам нужно пересечь потоки, чтобы изменить фактический элемент управления ... как метку или индикатор выполнения.

59 голосов
/ 19 марта 2009

Простое решение - использовать Control.Invoke.

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

void updateGUI() {
    // update gui here
}
45 голосов
/ 28 августа 2010

Код многопоточности часто содержит ошибки и его всегда сложно протестировать. Вам не нужно писать многопоточный код для обновления пользовательского интерфейса из фоновой задачи. Просто используйте класс BackgroundWorker для запуска задачи и его метод ReportProgress для обновления пользовательского интерфейса. Обычно вы просто сообщаете, что процент выполнения завершен, но есть еще одна перегрузка, которая включает объект состояния. Вот пример, который просто сообщает о строковом объекте:

    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();
    }

Хорошо, если вы всегда хотите обновить одно и то же поле. Если вам нужно сделать более сложные обновления, вы можете определить класс для представления состояния пользовательского интерфейса и передать его методу ReportProgress.

И последнее: обязательно установите флаг WorkerReportsProgress, иначе метод ReportProgress будет полностью проигнорирован.

39 голосов
/ 24 мая 2014

В подавляющем большинстве ответов используется Control.Invoke, то есть состояние гонки, ожидающее наступления . Например, рассмотрим принятый ответ:

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

Если пользователь закрывает форму непосредственно перед вызовом this.Invoke (помните, this - это объект Form), вероятно, будет запущен ObjectDisposedException.

Решение состоит в том, чтобы использовать SynchronizationContext, в частности SynchronizationContext.Current, как предлагает hamilton.danielb (другие ответы зависят от конкретных реализаций SynchronizationContext, что совершенно не нужно). Я бы немного изменил его код, чтобы использовать SynchronizationContext.Post вместо SynchronizationContext.Send (поскольку обычно рабочему потоку не нужно ждать):

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

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

Обратите внимание, что в .NET 4.0 и выше вы действительно должны использовать задачи для асинхронных операций. См. ответ n-san для эквивалентного подхода на основе задач (используя TaskScheduler.FromCurrentSynchronizationContext).

Наконец, в .NET 4.5 и более поздних версиях вы также можете использовать Progress<T> (который в основном захватывает SynchronizationContext.Current при его создании), как продемонстрировано Риззарда Дегана для случаев, когда длительная операция должна запустить код пользовательского интерфейса, все еще работая.

34 голосов
/ 19 марта 2009

Вы должны убедиться, что обновление происходит в правильном потоке; поток пользовательского интерфейса.

Для этого вам придется вызывать обработчик событий, а не вызывать его напрямую.

Вы можете сделать это, подняв событие так:

(Код набран здесь из моей головы, поэтому я не проверил правильность синтаксиса и т. Д., Но он должен помочь вам.)

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 ( ... );
      }
   }
}

Обратите внимание, что приведенный выше код не будет работать в проектах WPF, поскольку элементы управления WPF не реализуют интерфейс ISynchronizeInvoke.

Чтобы убедиться, что приведенный выше код работает с Windows Forms и WPF и всеми другими платформами, можно взглянуть на классы AsyncOperation, AsyncOperationManager и SynchronizationContext.

Чтобы легко вызывать события таким образом, я создал метод расширения, который позволяет мне упростить вызов события, просто вызвав:

MyEvent.Raise(this, EventArgs.Empty);

Конечно, вы также можете использовать класс BackGroundWorker, который абстрагирует вас от этого.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...