从另一个线程更新标签的最简单方法是什么?

我在thread1上运行了一个Form,然后我开始另一个线程(thread2)。当thread2处理一些文件时,我想用thread2工作的当前状态更新表单上的标签。

我怎么能这样做?


当前回答

关于这个主题的另一个示例:我创建了一个抽象类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
});

其他回答

我无法理解这一丑陋实现背后的微软逻辑,但你必须有两个功能:

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 */ }));

为了在WPF中实现这一点,我采用以下方式。

 new Thread(() => 
 {
     while (...)
     {
         SomeLabel.Dispatcher.BeginInvoke((Action)(() => SomeLabel.Text = ...));
     }
 }).Start();

最简单的方法是将匿名方法传递给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会阻止执行直到它完成——这是同步代码。这个问题不涉及异步代码,但在StackOverflow上有很多关于在您想了解异步代码时编写异步代码的内容。

由于场景的平凡性,我实际上会让UI线程轮询状态。我想你会发现它可以很优雅。

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和ISynchroniizeInvoke.BeginInvoke方法时所需的封送处理操作。使用封送处理技术没有什么问题,但有几点需要注意。

请确保不要过于频繁地调用BeginInvoke,否则它可能会超出消息泵。在工作线程上调用Invoke是一个阻塞调用。它将暂时停止该线程中正在执行的工作。

我在这个答案中提出的策略颠倒了线程的通信角色。UI线程轮询数据,而不是工作线程推送数据。这是许多场景中使用的常见模式。由于您所要做的只是显示工作线程的进度信息,所以我认为您会发现此解决方案是编组解决方案的一个很好的替代方案。它具有以下优点。

UI和工作线程保持松散耦合,而不是Control.Invoke或Control.BeginVoke方法紧密耦合它们。UI线程不会阻碍工作线程的进度。工作线程不能支配UI线程更新的时间。UI和工作线程执行操作的间隔可以保持独立。工作线程不能超出UI线程的消息泵。UI线程可以决定何时以及多久更新一次UI。

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

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