【问题标题】:How do I update the GUI from another thread?如何从另一个线程更新 GUI?
【发布时间】:2020-07-13 22:33:40
【问题描述】:

从另一个Thread 更新Label 的最简单方法是什么?

  • 我有一个在 thread1 上运行的 Form,并且我正在启动另一个线程 (thread2)。

  • thread2 正在处理一些文件时,我想将Form 上的Label 更新为thread2 工作的当前状态。

我该怎么做?

【问题讨论】:

  • .net 2.0+ 没有专门用于此的 BackgroundWorker 类。它的 UI 线程感知。 1.创建一个BackgroundWorker 2.添加两个delegate(一个用于处理,一个用于完成)
  • 查看 .NET 4.5 和 C# 5.0 的答案:stackoverflow.com/a/18033198/2042090
  • 这个问题不适用于 Gtk# GUI。对于 Gtk#,请参阅 thisthis 答案。
  • 当心:这个问题的答案现在是一堆杂乱无章的 OT(“这就是我为我的 WPF 应用程序所做的”)和历史 .NET 2.0 工件。

标签: c# .net multithreading winforms user-interface


【解决方案1】:

最简单的方式是匿名方法传入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 会阻塞执行直到它完成——这是同步代码。这个问题没有问异步代码,但是当你想了解异步代码时,有很多content on Stack Overflow关于编写异步代码。

【讨论】:

  • 看到 OP 没有提到任何类/实例除了表单,这不是一个糟糕的默认值......
  • 不要忘记“this”关键字引用的是“Control”类。
  • @codecompleting 无论哪种方式都是安全的,而且我们已经知道我们在工作,所以为什么要检查我们知道的东西呢?
  • @Dragouf 不是真的 - 使用此方法的重点之一是您已经知道哪些部分在工作线程上运行,哪些在 UI 线程上运行。无需检查。
  • @John 因为这是 Control.Invoke 对任何委托所做的 - 不仅仅是匿名方法
【解决方案2】:

对于 .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);

2010 年 5 月 10 日更新:

对于 .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 和 lambda 表达式来实现更简洁、更简单和更安全的语法:

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 版本好很多。

如果有人对如何改进此代码以提高编译时安全性有任何进一步的建议,请发表评论!

【讨论】:

  • 在某些情况下 this.GetType() 的计算结果与 propertyInfo.ReflectedType 相同(例如 WinForms 上的 LinkLabel)。我没有丰富的C#经验,但我认为异常的条件应该是: if (propertyInfo == null || (!@this.GetType().IsSubclassOf(propertyInfo.ReflectedType) && @this.GetType( ) != propertyInfo.ReflectedType) || @this.GetType().GetProperty(propertyInfo.Name, propertyInfo.PropertyType) == null)
  • @lan 这个SetControlPropertyThreadSafe(myLabel, "Text", status) 可以从另一个模块或类或表单中调用吗
  • 所提供的解决方案过于复杂。如果您重视简单性,请参阅 Marc Gravell 的解决方案或 Zaid Masud 的解决方案。
  • 如果您更新多个属性,此解决方案确实会浪费大量资源,因为每次调用都会消耗大量资源。我不认为这就是线程安全功能的意图。封装您的 UI 更新操作并调用一次(而不是每个属性)
  • 你到底为什么要在 BackgroundWorker 组件上使用这段代码?
【解决方案3】:

处理长时间的工作

由于.NET 4.5 and C# 5.0,您应该使用Task-based Asynchronous Pattern (TAP)async-await 关键字in all areas(包括GUI):

TAP 是推荐用于新开发的异步设计模式

而不是Asynchronous Programming Model (APM)Event-based Asynchronous Pattern (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. 通知UI线程的第二个线程的实现:

    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 反过来阻止事件处理程序在任务完成之前达到完成状态,同时不会阻塞 UI 线程。
  4. 进度类(参见IProgress Interface),支持Separation of Concerns (SoC) 设计原则,不需要显式调度程序和调用。它使用来自其创建位置(此处为 UI 线程)的当前 SynchronizationContext
  5. TaskCreationOptions.LongRunning 暗示不要将任务排入ThreadPool

有关更详细的示例,请参阅:The Future of C#: Good things come to those who 'await' by Joseph Albahari

另请参阅UI Threading Model 概念。

处理异常

下面的 sn-p 是如何处理异常并切换按钮的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...");
    }
}

【讨论】:

  • 如果SecondThreadConcern.LongWork()抛出异常,是否可以被UI线程捕获?这是一篇很棒的帖子,顺便说一句。
  • 我在答案中添加了一个附加部分以满足您的要求。问候。
  • ExceptionDispatchInfo class 负责以异步等待模式在 UI 线程上重新抛出后台异常的奇迹。
  • 只有我认为这样做比调用 Invoke/Begin 更冗长吗?!
  • Task.Delay(500).Wait()?创建一个任务来阻塞当前线程有什么意义?你永远不应该阻塞线程池线程!
【解决方案4】:

.NET 4 的 Marc Gravell's simplest solution 的变体:

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

或者改用 Action 委托:

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

两者的比较见这里:MethodInvoker vs Action for Control.BeginInvoke

【讨论】:

  • 本例中的“控制”是什么?我的 UI 控件?尝试在 WPF 中的标签控件上实现此功能,而 Invoke 不是我的标签的成员。
  • 像@styxriver stackoverflow.com/a/3588137/206730这样的扩展方法是什么?
  • 声明 'Action y;'在类或方法内部更改文本属性并使用这段代码更新文本'yourcontrol.Invoke(y=() => yourcontrol.Text = "new text");'
  • @Dbloom 它不是成员,因为它仅适用于 WinForms。对于 WPF,您使用 Dispatcher.Invoke
  • 我一直在关注这个解决方案,但有时我的 UI 没有得到更新。我发现我需要this.refresh() 强制使 GUI 无效并重新绘制 GUI .. 如果它有帮助..
【解决方案5】:

.NET 3.5+ 的 Fire and forget 扩展方法

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");

【讨论】:

  • @this 用法的意义何在? “控制”不是等价的吗? @this 有什么好处吗?
  • @jeromeyers - @this 只是变量名,在这种情况下是对调用扩展的当前控件的引用。您可以将其重命名为源,或任何浮动您的船。我使用@this,因为它指的是调用扩展的“此控件”,并且与在普通(非扩展)代码中使用“this”关键字一致(至少在我的脑海中)。
  • 这很好,很简单,对我来说是最好的解决方案。您可以在 ui 线程中包含您必须做的所有工作。示例:this.UIThread(() => { txtMessage.Text = message; listBox1.Items.Add(message); });
  • 我真的很喜欢这个解决方案。次要问题:我会将此方法命名为 OnUIThread 而不是 UIThread
  • 这就是为什么我将此扩展命名为RunOnUiThread。但这只是个人口味。
【解决方案6】:

这是你应该这样做的经典方式:

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

您的工作线程有一个事件。您的 UI 线程启动另一个线程来完成工作并连接该工作线程事件,以便您可以显示工作线程的状态。

然后在 UI 中你需要跨线程来改变实际的控制...比如标签或进度条。

【讨论】:

    【解决方案7】:

    简单的解决方案是使用Control.Invoke

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

    【讨论】:

    • 简洁干得好!不仅简单,而且效果很好!我真的不明白为什么微软不能让它变得更简单!为了在主线程上调用 1 行,我们应该编写几个函数!
    • @MBH 同意。顺便说一句,您是否注意到上面的stackoverflow.com/a/3588137/199364 答案,它定义了扩展方法?在自定义实用程序类中执行一次,然后不必再关心 Microsoft 没有为我们执行此操作 :)
    • @ToolmakerSteve 这正是它的意思!你是对的,我们可以找到一种方法,但我的意思是从 DRY(不要重复自己)的角度来看,有共同解决方案的问题,可以由他们以最小的努力由微软解决,这将节省大量时间程序员:)
    【解决方案8】:

    线程代码通常是错误的并且总是难以测试。您无需编写线程代码即可从后台任务更新用户界面。只需使用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();
        }
    

    如果您总是想更新同一个字段,那很好。如果您要进行更复杂的更新,您可以定义一个类来表示 UI 状态并将其传递给 ReportProgress 方法。

    最后一件事,一定要设置WorkerReportsProgress 标志,否则ReportProgress 方法将被完全忽略。

    【讨论】:

    • 处理结束时,也可以通过backgroundWorker1_RunWorkerCompleted更新用户界面。
    【解决方案9】:

    绝大多数答案使用Control.Invoke,即race condition waiting to happen。例如,考虑接受的答案:

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

    如果用户在调用this.Invoke 之前关闭表单(请记住,thisForm 对象),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's 答案以获得等效的基于任务的方法(使用TaskScheduler.FromCurrentSynchronizationContext)。

    最后,在 .NET 4.5 及更高版本上,您还可以使用 Progress&lt;T&gt;(它基本上在创建时捕获 SynchronizationContext.Current),如 Ryszard Dżegan's 所示,用于长时间运行的操作需要运行 UI 代码同时仍然工作。

    【讨论】:

      【解决方案10】:

      您必须确保更新发生在正确的线程上; UI 线程。

      为此,您必须调用事件处理程序而不是直接调用它。

      您可以通过像这样发起您的活动来做到这一点:

      (代码是在这里打出来的,所以我没有检查正确的语法等,但它应该能让你继续。)

      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 窗体和 WPF 以及所有其他平台,您可以查看 AsyncOperationAsyncOperationManagerSynchronizationContext 类。

      为了以这种方式轻松引发事件,我创建了一个扩展方法,它允许我通过调用来简化引发事件:

      MyEvent.Raise(this, EventArgs.Empty);
      

      当然,你也可以使用BackGroundWorker类,它会为你抽象出这件事。

      【讨论】:

      • 确实如此,但我不喜欢在这件事上“弄乱”我的 GUI 代码。我的 GUI 不应该关心它是否需要调用。换句话说:我认为执行上下文切换不是 GUI 的责任。
      • 将委托分开等似乎有点过头了——为什么不只是:SynchronizationContext.Current.Send(delegate { MyEvent(...); }, null);
      • 你总是可以访问 SynchronizationContext 吗?即使您的课程在类库中?
      【解决方案11】:

      由于场景的琐碎性,我实际上会让 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.InvokeISynchronizeInvoke.BeginInvoke 方法时所需的编组操作。使用编组技术没有任何问题,但您需要注意一些注意事项。

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

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

      • UI 和工作线程保持松散耦合,而 Control.InvokeControl.BeginInvoke 方法将它们紧密耦合。
      • UI 线程不会阻碍工作线程的进程。
      • 工作线程无法控制 UI 线程花费在更新上的时间。
      • UI 和工作线程执行操作的时间间隔可以保持独立。
      • 工作线程不能超出 UI 线程的消息泵。
      • UI 线程可以决定 UI 更新的时间和频率。

      【讨论】:

      • 好主意。您唯一没有提到的是在 WorkerThread 完成后如何正确处理计时器。请注意,当应用程序结束时(即用户关闭应用程序),这可能会导致麻烦。你知道如何解决这个问题吗?
      • @Matt 不是为Elapsed 事件使用匿名处理程序,而是使用成员方法,以便在处理表单时删除计时器...
      • @Phil1970 - 好点。您的意思是像System.Timers.ElapsedEventHandler handler = (s, a) =&gt; { MyProgressLabel.Text = m_Text; }; 并通过m_Timer.Elapsed += handler; 分配它,稍后在处置上下文中执行m_Timer.Elapsed -= handler; 对吗?并按照here 讨论的建议进行处置/关闭。
      【解决方案12】:

      您需要在 GUI 线程上调用该方法。您可以通过调用 Control.Invoke 来做到这一点。

      例如:

      delegate void UpdateLabelDelegate (string message);
      
      void UpdateLabel (string message)
      {
          if (InvokeRequired)
          {
               Invoke (new UpdateLabelDelegate (UpdateLabel), message);
               return;
          }
      
          MyLabelControl.Text = message;
      }
      

      【讨论】:

      • 调用行给了我一个编译器错误。 'System.Windows.Forms.Control.Invoke(System.Delegate, object[])' 的最佳重载方法匹配有一些无效参数
      【解决方案13】:

      前面的答案中的 Invoke 内容都不是必需的。

      你需要看看WindowsFormsSynchronizationContext:

      // In the main thread
      WindowsFormsSynchronizationContext mUiContext = new WindowsFormsSynchronizationContext();
      
      ...
      
      // In some non-UI Thread
      
      // Causes an update in the GUI thread.
      mUiContext.Post(UpdateGUI, userData);
      
      ...
      
      void UpdateGUI(object userData)
      {
          // Update your GUI controls here
      }
      

      【讨论】:

      • 您认为 Post 方法在底层使用了什么? :)
      【解决方案14】:

      这个和上面使用 .NET Framework 3.0 的解决方案类似,但是解决了编译时安全支持的问题。

      public  static class ControlExtension
      {
          delegate void SetPropertyValueHandler<TResult>(Control souce, Expression<Func<Control, TResult>> selector, TResult value);
      
          public static void SetPropertyValue<TResult>(this Control source, Expression<Func<Control, TResult>> selector, TResult value)
          {
              if (source.InvokeRequired)
              {
                  var del = new SetPropertyValueHandler<TResult>(SetPropertyValue);
                  source.Invoke(del, new object[]{ source, selector, value});
              }
              else
              {
                  var propInfo = ((MemberExpression)selector.Body).Member as PropertyInfo;
                  propInfo.SetValue(source, value, null);
              }
          }
      }
      

      使用方法:

      this.lblTimeDisplay.SetPropertyValue(a => a.Text, "some string");
      this.lblTimeDisplay.SetPropertyValue(a => a.Visible, false);
      

      如果用户传递了错误的数据类型,编译器将失败。

      this.lblTimeDisplay.SetPropertyValue(a => a.Visible, "sometext");
      

      【讨论】:

        【解决方案15】:

        加油!搜索了这个问题后,我发现 FrankGOregon Ghost 的答案对我来说是最简单最有用的。现在,我在 Visual Basic 中编写代码并通过转换器运行这个 sn-p;所以我不太确定结果如何。

        我有一个名为form_Diagnostics, 的对话框表单,它有一个名为updateDiagWindow, 的富文本框,我将其用作一种日志显示。我需要能够从所有线程更新其文本。额外的行允许窗口自动滚动到最新的行。

        因此,我现在可以在整个程序中的任何位置以您认为无需任何线程即可工作的方式用一行更新显示:

          form_Diagnostics.updateDiagWindow(whatmessage);
        

        主代码(将其放在表单的类代码中):

        #region "---------Update Diag Window Text------------------------------------"
        // This sub allows the diag window to be updated by all threads
        public void updateDiagWindow(string whatmessage)
        {
            var _with1 = diagwindow;
            if (_with1.InvokeRequired) {
                _with1.Invoke(new UpdateDiagDelegate(UpdateDiag), whatmessage);
            } else {
                UpdateDiag(whatmessage);
            }
        }
        // This next line makes the private UpdateDiagWindow available to all threads
        private delegate void UpdateDiagDelegate(string whatmessage);
        private void UpdateDiag(string whatmessage)
        {
            var _with2 = diagwindow;
            _with2.appendtext(whatmessage);
            _with2.SelectionStart = _with2.Text.Length;
            _with2.ScrollToCaret();
        }
        #endregion
        

        【讨论】:

          【解决方案16】:
          Label lblText; //initialized elsewhere
          
          void AssignLabel(string text)
          {
             if (InvokeRequired)
             {
                BeginInvoke((Action<string>)AssignLabel, text);
                return;
             }
          
             lblText.Text = text;           
          }
          

          请注意,BeginInvoke()Invoke() 更受欢迎,因为它不太可能导致死锁(但是,仅将文本分配给标签时,这不是问题):

          当使用Invoke() 时,您正在等待方法返回。现在,可能是您在调用的代码中做了一些需要等待线程的事情,如果它隐藏在您正在调用的某些函数中,这可能不会立即显而易见,这本身可能通过事件处理程序间接发生。因此,您将等待线程,线程将等待您,而您陷入僵局。

          这实际上导致我们发布的一些软件挂起。用BeginInvoke() 替换Invoke() 很容易修复。除非你有同步操作的需要,如果你需要返回值可能就是这种情况,使用BeginInvoke()

          【讨论】:

            【解决方案17】:

            出于许多目的,它就像这样简单:

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

            “serviceGUI()”是表单 (this) 中的一种 GUI 级方法,可以根据需要更改任意数量的控件。从另一个线程调用“updateGUI()”。可以添加参数来传递值,或者(可能更快)使用类范围变量并根据需要对它们进行锁定,如果访问它们的线程之间存在任何可能导致不稳定的冲突的可能性。如果非 GUI 线程时间紧迫(记住 Brian Gideon 的警告),请使用 BeginInvoke 而不是 Invoke。

            【讨论】:

              【解决方案18】:

              当我遇到同样的问题时,我向 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(() =&gt; .....) 混淆。我在处理线程时使用匿名函数或 lambda 表达式。为了减少代码行数,您也可以使用ThreadStart(..) 方法,我不应该在这里解释。

              【讨论】:

                【解决方案19】:

                这在我的 Ian Kemp 解决方案的 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. 它为“as MemberExpression”的结果添加了空检查。
                2. 它提高了静态类型安全性。

                否则,原版是一个很好的解决方案。

                【讨论】:

                  【解决方案20】:

                  简单地使用这样的东西:

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

                  【讨论】:

                  • 如果你有e.ProgressPercentage,你不是已经在你调用这个方法的UI线程中了吗?
                  • ProgressChanged 事件在 UI 线程上运行。这是使用 BackgroundWorker 的便利之一。 Completed 事件也在 gui 上运行。唯一在非 UI 线程中运行的是 DoWork 方法。
                  【解决方案21】:

                  在这个问题上,大多数其他答案对我来说有点复杂(我是 C# 新手),所以我正在写我的:

                  我有一个 WPF 应用程序并定义了一个工作器,如下所示:

                  问题:

                  BackgroundWorker workerAllocator;
                  workerAllocator.DoWork += delegate (object sender1, DoWorkEventArgs e1) {
                      // This is my DoWork function.
                      // It is given as an anonymous function, instead of a separate DoWork function
                  
                      // I need to update a message to textbox (txtLog) from this thread function
                  
                      // Want to write below line, to update UI
                      txt.Text = "my message"
                  
                      // But it fails with:
                      //  'System.InvalidOperationException':
                      //  "The calling thread cannot access this object because a different thread owns it"
                  }
                  

                  解决方案:

                  workerAllocator.DoWork += delegate (object sender1, DoWorkEventArgs e1)
                  {
                      // The below single line works
                      txtLog.Dispatcher.BeginInvoke((Action)(() => txtLog.Text = "my message"));
                  }
                  

                  我还没有弄清楚上面那行是什么意思,但它有效。

                  对于WinForms

                  解决方案:

                  txtLog.Invoke((MethodInvoker)delegate
                  {
                      txtLog.Text = "my message";
                  });
                  

                  【讨论】:

                  • 问题是关于 Winforms,而不是 WPF。
                  • 谢谢。上面添加了 WinForms 解决方案。
                  • ...这只是同一问题上许多其他答案的副本,但没关系。为什么不成为解决方案的一部分而只是删除您的答案?
                  • 嗯,对了,如果您仔细阅读了我的答案,开始部分(我写答案的原因),希望您多注意一下,您会看到有人今天遇到了完全相同的问题并为我的简单答案投了赞成票,如果你能预见到为什么这一切发生的真实故事,即使我搜索 wpf,谷歌也会把我送到这里。当然,既然你错过了这些或多或少明显的 3 个原因,我可以理解为什么你不会删除你的反对票。与其清理没问题的东西,不如创造一些更困难的新东西。
                  【解决方案22】:

                  我的版本是插入一行递归“咒语”:

                  如果没有参数:

                      void Aaaaaaa()
                      {
                          if (InvokeRequired) { Invoke(new Action(Aaaaaaa)); return; } //1 line of mantra
                  
                          // Your code!
                      }
                  

                  对于有参数的函数:

                      void Bbb(int x, string text)
                      {
                          if (InvokeRequired) { Invoke(new Action<int, string>(Bbb), new[] { x, text }); return; }
                          // Your code!
                      }
                  

                  就是这样


                  一些争论:通常将 {} 放在一行中的 if () 语句之后会降低代码的可读性。但在这种情况下,它是例行公事的“口头禅”。如果此方法在项目中保持一致,则不会破坏代码的可读性。并且它可以避免您的代码乱扔垃圾(一行代码而不是五行代码)。

                  如您所见if(InvokeRequired) {something long},您只知道“从另一个线程调用此函数是安全的”。

                  【讨论】:

                    【解决方案23】:

                    你可以使用已经存在的委托Action

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

                    【讨论】:

                      【解决方案24】:

                      创建一个类变量:

                      SynchronizationContext _context;
                      

                      在创建 UI 的构造函数中设置它:

                      var _context = SynchronizationContext.Current;
                      

                      当你想更新标签时:

                      _context.Send(status =>{
                          // UPDATE LABEL
                      }, null);
                      

                      【讨论】:

                        【解决方案25】:

                        你必须使用调用和委托

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

                        【讨论】:

                          【解决方案26】:

                          尝试使用此刷新标签

                          public static class ExtensionMethods
                          {
                              private static Action EmptyDelegate = delegate() { };
                          
                              public static void Refresh(this UIElement uiElement)
                              {
                                  uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
                              }
                          }
                          

                          【讨论】:

                          • 它适用于 Windows 窗体 吗?
                          【解决方案27】:

                          还有另一个通用的 Control 扩展方法..

                          首先为Control类型的对象添加一个扩展方法

                          public static void InvokeIfRequired<T>(this T c, Action<T> action) where T : Control
                          {
                              if (c.InvokeRequired)
                              {
                                  c.Invoke(new Action(() => action(c)));
                              }
                              else
                              {
                                  action(c);
                              }
                          }
                          

                          并像这样从另一个线程调用以访问 UI 线程中名为 object1 的控件:

                          object1.InvokeIfRequired(c => { c.Visible = true; });
                          object1.InvokeIfRequired(c => { c.Text = "ABC"; });
                          

                          ..或者像这样

                          object1.InvokeIfRequired(c => 
                            { 
                                c.Text = "ABC";
                                c.Visible = true; 
                            }
                          );
                          

                          【讨论】:

                          • 很优雅,很好看!
                          • 我已经开始使用 c.BeginInvoke 进行异步更新。如果在级联中调用,则不太可能导致死锁。
                          【解决方案28】:

                          WPF 应用程序中最简单的方法是:

                          this.Dispatcher.Invoke((Action)(() =>
                          {
                              // This refers to a form in a WPF application 
                              val1 = textBox.Text; // Access the UI 
                          }));
                          

                          【讨论】:

                          • 这是正确的,如果您使用的是 WPF 应用程序。但他使用的是 Windows 窗体。
                          • 您甚至可以在 Winforms 应用程序中使用 Dispatcher。 stackoverflow.com/questions/303116/…
                          【解决方案29】:

                          当您在 UI 线程中时,您可以询问它的同步上下文任务调度程序。它会给你一个 TaskScheduler 来安排 UI 线程上的一切。

                          然后您可以链接您的任务,以便当结果准备好时,另一个任务(在 UI 线程上安排)选择它并将其分配给标签。

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

                          这适用于preferred way of writing concurrent code now 的任务(不是线程)。

                          【讨论】:

                          【解决方案30】:

                          例如,访问当前线程以外的控件:

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

                          lblThreshold 是一个标签,Speed_Threshold 是一个全局变量。

                          【讨论】:

                            猜你喜欢
                            相关资源
                            最近更新 更多