如何从另一个线程更新GUI?

1581

如何用最简单的方式从另一个线程更新Label

  • 我有一个在thread1上运行的Form,从那里我启动了另一个线程(thread2)。

  • thread2处理一些文件时,我想通过FormLabel的当前状态更新为thread2的工作状态。

我该怎么做呢?


28
.NET 2.0+有专门用于此的BackgroundWorker类,它可以注意UI线程。1.创建一个BackgroundWorker;2.添加两个委托(一个用于处理,另一个用于完成)。 - Preet Sangha
15
或许有点晚了:http://www.codeproject.com/KB/cs/Threadsafe_formupdating.aspx(请注意,为了保持翻译的准确性和简洁性,我省略了原文中的所有标点符号和格式) - MichaelD
4
请参考.NET 4.5和C# 5.0的答案:https://dev59.com/wnRB5IYBdhLWcg3wUVtd#18033198。 - Ryszard Dżegan
5
此问题不适用于Gtk# GUI。有关Gtk#的信息,请参见答案。 - hlovdal
3
警告:这个问题的回答现在混杂着 OT(“这是我为我的 WPF 应用所做的”)和历史遗留的 .NET 2.0 工件。请注意。 - Marc L.
相关帖子 - 从UI线程强制更新GUI - RBT
47个回答

9
例如,在当前线程之外访问控件:
Speed_Threshold = 30;
textOutput.Invoke(new EventHandler(delegate
{
    lblThreshold.Text = Speed_Threshold.ToString();
}));

这里的lblThreshold是一个标签,而Speed_Threshold是一个全局变量。


8

只需使用UI的同步上下文

using System.Threading;

// ...

public partial class MyForm : Form
{
    private readonly SynchronizationContext uiContext;

    public MyForm()
    {
        InitializeComponent();
        uiContext = SynchronizationContext.Current; // get ui thread context
    }

    private void button1_Click(object sender, EventArgs e)
    {
        Thread t = new Thread(() =>
            {// set ui thread context to new thread context                            
             // for operations with ui elements to be performed in proper thread
             SynchronizationContext
                 .SetSynchronizationContext(uiContext);
             label1.Text = "some text";
            });
        t.Start();
    }
}

当然可以。我已经添加了注释以达到这个目的。 - user53373
这是我认为最直接和易读的方法。 - Sidhin S Thomas

8

我刚刚阅读了答案,发现这是一个非常热门的话题。我目前正在使用.NET 3.5 SP1和Windows Forms。

在之前的答案中描述得非常好的公式使用了InvokeRequired属性,它涵盖了大部分情况,但并不是全部。

如果Handle尚未创建怎么办?

正如此处(Control.InvokeRequired Property reference to MSDN)所述的,InvokeRequired属性会返回true,如果调用是从不是GUI线程的线程进行的,则返回false,如果调用是从GUI线程或Handle尚未创建的线程进行的,则返回false。

如果要显示并由另一个线程更新模态形式,则可能遇到异常。因为您想要以模态方式显示该表单,所以可以执行以下操作:

private MyForm _gui;

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

并且委托可以更新GUI上的标签:

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线程创建FormHandle所需的时间。这发生在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; });
}

我很确定,无论哪个线程调用,我的GUI都会更新,并可选择等待一段明确定义的时间(超时)。

1
我来这里找这个,因为我也检查了IsHandleCreated。另一个要检查的属性是IsDisposed。如果您的窗体已被处理,您无法在其上调用Invoke()。如果用户在后台线程完成之前关闭了窗体,则不希望它在窗体被处理时尝试回调到UI。 - Jon
我认为从...开始是个坏主意。通常,你会立即显示子窗体,并在后台处理时提供进度条或其他反馈。或者你会先进行所有处理,然后在创建时将结果传递给新窗体。同时执行这两个操作通常会带来较小的好处,但代码可维护性要差得多。 - Phil1970
所描述的场景考虑了一个模态表单,用作后台线程作业的进度视图。因为它必须是模态的,所以必须通过调用Form.ShowDialog()方法来显示。通过这样做,您可以防止在调用之后执行代码,直到该表单关闭。因此,除非您可以从给定示例中以不同的方式启动后台线程(当然,您可以),否则必须在后台线程启动后模态显示此表单。在这种情况下,您需要检查Handle是否已创建。如果您不需要模态表单,则另当别论。 - Sume

8
我认为最简单的方法是:
   void Update()
   {
       BeginInvoke((Action)delegate()
       {
           //do your update
       });
   }

7
我不理解微软这个丑陋实现背后的逻辑,但你需要有两个函数:

void setEnableLoginButton()
{
  if (InvokeRequired)
  {
    // btn_login can be any conroller, (label, button textbox ..etc.)

    btn_login.Invoke(new MethodInvoker(setEnable));

    // OR
    //Invoke(new MethodInvoker(setEnable));
  }
  else {
    setEnable();
  }
}

void setEnable()
{
  btn_login.Enabled = isLoginBtnEnabled;
}

这些片段适用于我,因此我可以在另一个线程上执行某些操作,然后更新GUI界面:

Task.Factory.StartNew(()=>
{
    // THIS IS NOT GUI
    Thread.Sleep(5000);
    // HERE IS INVOKING GUI
    btn_login.Invoke(new Action(() => DoSomethingOnGUI()));
});

private void DoSomethingOnGUI()
{
   // GUI
   MessageBox.Show("message", "title", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}

更简单的方法是:
btn_login.Invoke(new Action(()=>{ /* HERE YOU ARE ON GUI */ }));

6

无论框架版本或GUI底层库类型如何,解决此问题的基本方法是保存创建线程的控件的同步上下文,以便将控件相关交互从工作线程调度到GUI线程消息队列。

例如:

SynchronizationContext ctx = SynchronizationContext.Current; // From control
ctx.Send\Post... // From worker thread

6

这是一种更加功能化的方法来看待一个古老的问题。如果您在所有项目中都使用TaskXM类,那么您只需要一行代码就可以永远不再担心跨线程更新。

public class Example
{
    /// <summary>
    /// No more delegates, background workers, etc. Just one line of code as shown below.
    /// Note it is dependent on the Task Extension method shown next.
    /// </summary>
    public async void Method1()
    {
        // Still on the GUI thread here if the method was called from the GUI thread
        // This code below calls the extension method which spins up a new task and calls back.
        await TaskXM.RunCodeAsync(() =>
        {
            // Running an asynchronous task here
            // Cannot update the GUI thread here, but can do lots of work
        });
        // Can update GUI on this line
    }
}


/// <summary>
/// A class containing extension methods for the Task class
/// </summary>
public static class TaskXM
{
    /// <summary>
    /// RunCodeAsyc is an extension method that encapsulates the Task.run using a callback
    /// </summary>
    /// <param name="Code">The caller is called back on the new Task (on a different thread)</param>
    /// <returns></returns>
    public async static Task RunCodeAsync(Action Code)
    {
        await Task.Run(() =>
        {
            Code();
        });
        return;
    }
}

真正的问题是:像这样包装Task.Run和直接调用await Task.Run(() => {...});有什么区别?这里间接性的优势是什么? - Marc L.
表面上看起来没有区别。但是深入了解后,它展示了函数式编程的强大之处。特别是它包装了一个静态方法,有助于实现单一职责原则。如果你现在想要实现ConfigureAwait(false)或添加日志记录语句,你只需要做一次即可。 - JWP

6
也许有点过度,但这是我通常解决这个问题的方式:
由于同步,这里不需要调用invokes。 BasicClassThreadExample只是我为自己设置的一种布局,因此请根据您的实际需求进行更改。
这很简单,因为您不需要处理UI线程中的内容!
public partial class Form1 : Form
{
    BasicClassThreadExample _example;

    public Form1()
    {
        InitializeComponent();
        _example = new BasicClassThreadExample();
        _example.MessageReceivedEvent += _example_MessageReceivedEvent;
    }

    void _example_MessageReceivedEvent(string command)
    {
        listBox1.Items.Add(command);
    }

    private void button1_Click(object sender, EventArgs e)
    {
        listBox1.Items.Clear();
        _example.Start();
    }
}

public class BasicClassThreadExample : IDisposable
{
    public delegate void MessageReceivedHandler(string msg);

    public event MessageReceivedHandler MessageReceivedEvent;

    protected virtual void OnMessageReceivedEvent(string msg)
    {
        MessageReceivedHandler handler = MessageReceivedEvent;
        if (handler != null)
        {
            handler(msg);
        }
    }

    private System.Threading.SynchronizationContext _SynchronizationContext;
    private System.Threading.Thread _doWorkThread;
    private bool disposed = false;

    public BasicClassThreadExample()
    {
        _SynchronizationContext = System.ComponentModel.AsyncOperationManager.SynchronizationContext;
    }

    public void Start()
    {
        _doWorkThread = _doWorkThread ?? new System.Threading.Thread(dowork);

        if (!(_doWorkThread.IsAlive))
        {
            _doWorkThread = new System.Threading.Thread(dowork);
            _doWorkThread.IsBackground = true;
            _doWorkThread.Start();
        }
    }

    public void dowork()
    {
        string[] retval = System.IO.Directory.GetFiles(@"C:\Windows\System32", "*.*", System.IO.SearchOption.TopDirectoryOnly);
        foreach (var item in retval)
        {
            System.Threading.Thread.Sleep(25);
            _SynchronizationContext.Post(new System.Threading.SendOrPostCallback(delegate(object obj)
            {
                OnMessageReceivedEvent(item);
            }), null);
        }
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                _doWorkThread.Abort();
            }
            disposed = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~BasicClassThreadExample() { Dispose(false); }

}

6

我希望添加一个警告,因为我注意到有一些简单的解决方案省略了InvokeRequired检查。

我注意到如果你的代码在控件的窗口句柄创建之前执行(例如,在表单显示之前),Invoke会抛出异常。因此,我建议在调用InvokeBeginInvoke之前始终检查InvokeRequired


6

另一个关于这个主题的例子:我创建了一个抽象类UiSynchronizeModel,其中包含一个通用方法的实现:

public abstract class UiSynchronizeModel
{
    private readonly TaskScheduler uiSyncContext;
    private readonly SynchronizationContext winformsOrDefaultContext;

    protected UiSynchronizeModel()
    {
        this.winformsOrDefaultContext = SynchronizationContext.Current ?? new SynchronizationContext();
        this.uiSyncContext = TaskScheduler.FromCurrentSynchronizationContext();
    }

    protected void RunOnGuiThread(Action action)
    {
        this.winformsOrDefaultContext.Post(o => action(), null);
    }

    protected void CompleteTask(Task task, TaskContinuationOptions options, Action<Task> action)
    {
        task.ContinueWith(delegate
        {
            action(task);
            task.Dispose();
        }, CancellationToken.None, options, this.uiSyncContext);
    }
}

您的模型或控制器类应该派生自这个抽象类。您可以使用任何模式(任务或手动管理的后台线程),并像这样使用这些方法:

public void MethodThatCalledFromBackroundThread()
{
   this.RunOnGuiThread(() => {
       // Do something over UI controls
   });
}

任务示例:

var task = Task.Factory.StartNew(delegate
{
    // Background code
    this.RunOnGuiThread(() => {
        // Do something over UI controls
    });
});

this.CompleteTask(task, TaskContinuationOptions.OnlyOnRanToCompletion, delegate
{
    // Code that can safely use UI controls
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接