форму - доступ к элементам формы из другого потока c#




Как обновить графический интерфейс из другого потока? (20)

Обработка длительной работы

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

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

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

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

  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. Task вместо Thread .
  3. async , что позволяет использовать await что, в свою очередь, не позволяет обработчику события достичь состояния завершения до завершения задачи и тем временем не блокирует поток пользовательского интерфейса.
  4. Прогресс (см. Интерфейс IProgress ), который поддерживает принцип рассоединения проблем (SoC) и не требует явного диспетчера и вызова. Он использует текущий SynchronizationContext из своего места создания (здесь поток пользовательского интерфейса).
  5. TaskCreationOptions.LongRunning который намекает, чтобы не ставить очередь задачи в ThreadPool .

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

См. Также о концепции модели Threading UI .

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

Нижеприведенный фрагмент представляет собой пример того, как обрабатывать исключения и включить свойство 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...");
    }
}

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

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

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


Fire и забыть метод расширения для .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");

Вам нужно вызвать метод в потоке графического интерфейса. Вы можете сделать это, вызвав Control.Invoke.

Например:

delegate void UpdateLabelDelegate (string message);

void UpdateLabel (string message)
{
    if (InvokeRequired)
    {
         Invoke (new UpdateLabelDelegate (UpdateLabel), message);
         return;
    }

    MyLabelControl.Text = message;
}

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

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

Вы можете сделать это, подняв ваше событие следующим образом:

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

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, который абстрагирует этот вопрос для вас.


Для многих целей это так просто:

public delegate void serviceGUIDelegate();
private void updateGUI()
{
  this.Invoke(new serviceGUIDelegate(serviceGUI));
}

«serviceGUI ()» - это метод уровня GUI в форме (это), который может изменять как можно большее количество элементов управления. Вызовите «updateGUI ()» из другого потока. Параметры могут быть добавлены для передачи значений или (возможно, быстрее) использования переменных класса класса с блокировками на них по мере необходимости, если есть какая-либо вероятность столкновения между потоками, обращающимися к ним, что может вызвать нестабильность. Используйте BeginInvoke вместо Invoke, если поток без GUI является критическим по времени (учитывая предупреждение Брайана Гидеона).


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

public class MyForm : Form
{
  private volatile string m_Text = "";
  private System.Timers.Timer m_Timer;

  private MyForm()
  {
    m_Timer = new System.Timers.Timer();
    m_Timer.SynchronizingObject = this;
    m_Timer.Interval = 1000;
    m_Timer.Elapsed += (s, a) => { MyProgressLabel.Text = m_Text; };
    m_Timer.Start();
    var thread = new Thread(WorkerThread);
    thread.Start();
  }

  private void WorkerThread()
  {
    while (...)
    {
      // Periodically publish progress information.
      m_Text = "Still working...";
    }
  }
}

Этот подход позволяет избежать операции маршалинга, требуемой при использовании методов ISynchronizeInvoke.Invoke и ISynchronizeInvoke.BeginInvoke . Нет ничего плохого в использовании техники маршалинга, но есть пара предостережений, о которых вам нужно знать.

  • Убедитесь, что вы не вызываете BeginInvoke слишком часто, или он может переполнять насос сообщений.
  • Вызов Invoke в рабочем потоке является блокирующим вызовом. Он временно остановит работу, выполняемую в этом потоке.

Стратегия, которую я предлагаю в этом ответе, отменяет роли связи потоков. Вместо того, чтобы рабочий поток толкал данные, опрос пользовательского интерфейса. Это общий шаблон, используемый во многих сценариях. Поскольку все, что вы хотите сделать, это отобразить информацию о ходе работы из рабочего потока, тогда я думаю, вы обнаружите, что это решение является отличной альтернативой решению маршалинга. Он имеет следующие преимущества.

  • Пользовательский интерфейс и рабочие потоки остаются слабо связанными, в отличие от подхода Control.Invoke или Control.BeginInvoke который плотно соединяет их.
  • Нить пользовательского интерфейса не будет препятствовать прогрессу рабочего потока.
  • Рабочий поток не может доминировать во время обновления потока пользовательского интерфейса.
  • Интервалы, на которых выполняются операции пользовательского интерфейса и рабочего потока, могут оставаться независимыми.
  • Рабочий поток не может переполнять поток сообщений потока пользовательского интерфейса.
  • Поток пользовательского интерфейса определяет, когда и как часто обновляется пользовательский интерфейс.

Когда я столкнулся с той же проблемой, я обратился за помощью к Google, но вместо того, чтобы дать мне простое решение, я больше смутил меня, представив примеры MethodInvoker и blah blah blah. Поэтому я решил решить это самостоятельно. Вот мое решение:

Сделайте делегат следующим образом:

Public delegate void LabelDelegate(string s);

void Updatelabel(string text)
{
   if (label.InvokeRequired)
   {
       LabelDelegate LDEL = new LabelDelegate(Updatelabel);
       label.Invoke(LDEL, text);
   }
   else
       label.Text = text
}

Вы можете вызвать эту функцию в новом потоке, подобном этому

Thread th = new Thread(() => Updatelabel("Hello World"));
th.start();

Не путайте Thread(() => .....). Я использую анонимную функцию или лямбда-выражение при работе над потоком. Чтобы уменьшить строки кода, вы можете использовать ThreadStart(..)метод, который я не должен здесь объяснять.


Код Threading часто глючит и всегда трудно тестировать. Вам не нужно писать код потока для обновления пользовательского интерфейса из фоновой задачи. Просто используйте класс 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 , обязательно установите флаг WorkerReportsProgress , ReportProgress метод ReportProgress будет полностью проигнорирован.


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

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

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

Решение заключается в использовании 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's Ответ n-san's для эквивалентного подхода на основе задач (с использованием TaskScheduler.FromCurrentSynchronizationContext ).

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


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

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

void updateGUI() {
    // update gui here
}

Это в моей версии C # 3.0 решения Яна Кемпа:

public static void SetPropertyInGuiThread<C,V>(this C control, Expression<Func<C, V>> property, V value) where C : Control
{
    var memberExpression = property.Body as MemberExpression;
    if (memberExpression == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    var propertyInfo = memberExpression.Member as PropertyInfo;
    if (propertyInfo == null)
        throw new ArgumentException("The 'property' expression must specify a property on the control.");

    if (control.InvokeRequired)
        control.Invoke(
            (Action<C, Expression<Func<C, V>>, V>)SetPropertyInGuiThread,
            new object[] { control, property, value }
        );
    else
        propertyInfo.SetValue(control, value, null);
}

Вы называете это следующим образом:

myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
  1. Он добавляет нулевую проверку к результату «как выражение MemberExpression».
  2. Это улучшает статическую безопасность.

В противном случае оригинал - очень приятное решение.


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

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

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

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


Вы должны использовать invoke и делегировать

private delegate void MyLabelDelegate();
label1.Invoke( new MyLabelDelegate(){ label1.Text += 1; });

Например, доступ к элементу управления, отличному от текущего потока:

Speed_Threshold = 30;
textOutput.Invoke(new EventHandler(delegate
{
    lblThreshold.Text = Speed_Threshold.ToString();
}));

Там lblThresholdесть метка и Speed_Thresholdявляется глобальной переменной.


Просто используйте что-то вроде этого:

 this.Invoke((MethodInvoker)delegate
            {
                progressBar1.Value = e.ProgressPercentage; // runs on UI thread
            });

Самый простой способ, я думаю:

   void Update()
   {
       BeginInvoke((Action)delegate()
       {
           //do your update
       });
   }

Вы можете использовать уже существующий делегат Action:

private void UpdateMethod()
{
    if (InvokeRequired)
    {
        Invoke(new Action(UpdateMethod));
    }
}

Когда вы находитесь в потоке пользовательского интерфейса, вы можете запросить его для планировщика задач контекста синхронизации. Это даст вам TaskScheduler который планирует все в потоке пользовательского интерфейса.

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

public partial class MyForm : Form
{
  private readonly TaskScheduler _uiTaskScheduler;
  public MyForm()
  {
    InitializeComponent();
    _uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
  }

  private void buttonRunAsyncOperation_Click(object sender, EventArgs e)
  {
    RunAsyncOperation();
  }

  private void RunAsyncOperation()
  {
    var task = new Task<string>(LengthyComputation);
    task.ContinueWith(antecedent =>
                         UpdateResultLabel(antecedent.Result), _uiTaskScheduler);
    task.Start();
  }

  private string LengthyComputation()
  {
    Thread.Sleep(3000);
    return "47";
  }

  private void UpdateResultLabel(string text)
  {
    labelResult.Text = text;
  }
}

Это работает для задач (а не потоков), которые являются предпочтительным способом написания параллельного кода .


Я просто прочитал ответы, и это, кажется, очень горячая тема. В настоящее время я использую .NET 3.5 SP1 и Windows Forms.

Хорошо известная формула, описанная в предыдущих ответах, использующая свойство InvokeRequired, охватывает большинство случаев, но не весь пул.

Что делать, если ручка еще не создана?

Свойство InvokeRequired , как описано здесь (ссылка на свойство Control.InvokeRequired для MSDN), возвращает true, если вызов был выполнен из потока, который не является потоком графического интерфейса пользователя, ложным, либо если вызов был выполнен из потока графического интерфейса пользователя, либо если Handle был еще не создан.

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

private MyForm _gui;

public void StartToDoThings()
{
    _gui = new MyForm();
    Thread thread = new Thread(SomeDelegate);
    thread.Start();
    _gui.ShowDialog();
}

И делегат может обновить ярлык в графическом интерфейсе:

private void SomeDelegate()
{
    // Operations that can take a variable amount of time, even no time
    //... then you update the GUI
    if(_gui.InvokeRequired)
        _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
    else
        _gui.Label1.Text = "Done!";
}

Это может привести к InvalidOperationException , если операции перед обновлением лейбла «занимает меньше времени» (читать и интерпретировать его как упрощением) , чем время, необходимое для GUI потока , чтобы создать форму «s Handle . Это происходит в методе ShowDialog () .

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

private void SomeDelegate()
{
    // Operations that can take a variable amount of time, even no time
    //... then you update the GUI
    if(_gui.IsHandleCreated)  //  <---- ADDED
        if(_gui.InvokeRequired)
            _gui.Invoke((Action)delegate { _gui.Label1.Text = "Done!"; });
        else
            _gui.Label1.Text = "Done!";
}

Вы можете обработать операцию для выполнения, если Handle еще не была создана: вы можете просто игнорировать обновление GUI (как показано в приведенном выше коде), или вы можете ждать (более рискованно). Это должно ответить на вопрос.

Необязательный материал: Лично я придумал следующее:

public class ThreadSafeGuiCommand
{
  private const int SLEEPING_STEP = 100;
  private readonly int _totalTimeout;
  private int _timeout;

  public ThreadSafeGuiCommand(int totalTimeout)
  {
    _totalTimeout = totalTimeout;
  }

  public void Execute(Form form, Action guiCommand)
  {
    _timeout = _totalTimeout;
    while (!form.IsHandleCreated)
    {
      if (_timeout <= 0) return;

      Thread.Sleep(SLEEPING_STEP);
      _timeout -= SLEEPING_STEP;
    }

    if (form.InvokeRequired)
      form.Invoke(guiCommand);
    else
      guiCommand();
  }
}

Я передаю свои формы, которые обновляются другим потоком с экземпляром этого ThreadSafeGuiCommand , и я определяю методы, которые обновляют GUI (в моей форме) следующим образом:

public void SetLabeTextTo(string value)
{
  _threadSafeGuiCommand.Execute(this, delegate { Label1.Text = value; });
}

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


Я хотел добавить предупреждение, потому что заметил, что некоторые простые решения опускают InvokeRequiredчек.

Я заметил, что если ваш код выполняется до того, как был создан дескриптор окна элемента управления (например, перед тем, как форма будет показана), Invokeвыбрасывается исключение. Поэтому я рекомендую всегда проверять InvokeRequiredперед вызовом Invokeили BeginInvoke.





user-interface