如何从其他线程更新GUI?

Cru*_*lIO 1331 .net c# user-interface multithreading winforms

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

我有一Formthread1,并从我开始另一个线程(thread2).虽然thread2在处理一些文件,我想更新LabelForm用的当前状态thread2的工作.

我怎样才能做到这一点?

Mar*_*ell 1046

最简单的方法是匿名方法传递到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
Run Code Online (Sandbox Code Playgroud)

请注意,Invoke阻止执行直到完成 - 这是同步代码.这个问题并不是关于异步代码的问题,但是当你想要了解异步代码时,Stack Overflow上有很多关于编写异步代码的内容.

  • 不要忘记"this"关键字引用了"Control"类. (38认同)
  • 看到OP没有提到除了*表单之外的任何类/实例*,这不是一个错误的默认值...... (7认同)
  • @codecompleting它是安全的,我们已经知道我们是一个工人,所以为什么要检查一下我们知道的东西? (7认同)
  • @Dragouf并不是真的 - 使用这种方法的一个原因是你已经知道哪些部分在worker上运行,哪些部分在UI线程上运行.无需检查. (4认同)
  • @ Joan.bdm我没有足够的背景来评论这个问题 (3认同)
  • 但是,那么您的处理功能必须是GUI表单的成员方法吗? (2认同)
  • @John因为这就是Control.Invoke对任何委托所做的事情 - 而不仅仅是anon方法 (2认同)
  • @MarcGravell与使用"new MethodInvoker(()=> {someLabel.Text = newText;})相比,在执行此操作时是否存在任何(性能)​​差异 (2认同)

Ian*_*emp 757

对于.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 });
  }
}
Run Code Online (Sandbox Code Playgroud)

像这样称呼它:

// thread-safe equivalent of
// myLabel.Text = status;
SetControlPropertyThreadSafe(myLabel, "Text", status);
Run Code Online (Sandbox Code Playgroud)

如果您使用的是.NET 3.0或更高版本,则可以将上述方法重写为类的扩展方法Control,这样可以简化对以下内容的调用:

myLabel.SetPropertyThreadSafe("Text", status);
Run Code Online (Sandbox Code Playgroud)

更新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 });
  }
}
Run Code Online (Sandbox Code Playgroud)

它使用LINQ和lambda表达式来允许更清晰,更简单和更安全的语法:

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

现在不仅在编译时检查属性名称,属性的类型也是如此,因此不可能(例如)将字符串值赋给布尔属性,从而导致运行时异常.

不幸的是,这并没有阻止任何人做愚蠢的事情,比如传递另一个Control人的财产和价值,所以以下内容将很乐意编译:

myLabel.SetPropertyThreadSafe(() => aForm.ShowIcon, false);
Run Code Online (Sandbox Code Playgroud)

因此,我添加了运行时检查,以确保传入的属性确实属于Control被调用方法的属性.不完美,但仍然比.NET 2.0版本好很多.

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

  • 提供的解决方案不必要地复杂.如果您重视简洁,请参阅Marc Gravell的解决方案或Zaid Masud的解决方案. (66认同)
  • @lan可以从另一个模块或类或形式调用`SetControlPropertyThreadSafe(myLabel,"Text",status)` (8认同)
  • 如果您更新多个属性,则此解决方案会浪费大量资源,因为每个Invoke都需要花费大量资源.我不认为这是线程安全的特性无论如何都是如此.对您的UI更新操作进行封装并将其调用ONCE(而不是每个属性) (7认同)
  • 为什么你会在BackgroundWorker组件上使用这个代码呢? (4认同)
  • 有些情况下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) (3认同)

Rys*_*gan 387

处理长期工作

.NET 4.5和C#5.0开始,您应该使用基于任务的异步模式(TAP)异步 - 等待所有区域(包括GUI)中的关键字:

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";
    }
    
    Run Code Online (Sandbox Code Playgroud)
  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());
            }
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

请注意以下事项:

  1. 以顺序方式编写的简短而干净的代码,没有回调和显式线程.
  2. 任务而不是线程.
  3. async关键字,允许使用await反过来阻止事件处理程序达到完成状态,直到任务完成,同时不阻止UI线程.
  4. 进度类(参见IProgress接口),支持Separation of Concerns(SoC)设计原则,不需要显式调度和调用.它使用来自其创建位置的当前SynchronizationContext(此处为UI线程).
  5. TaskCreationOptions.LongRunning,提示不将任务排入ThreadPool.

有关更详细的例子,请参阅:C#的未来:约瑟夫·阿尔巴哈里"等待"的人会遇到好事.

另请参阅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...");
    }
}
Run Code Online (Sandbox Code Playgroud)

  • **[ExceptionDispatchInfo类](https://msdn.microsoft.com/en-us/library/system.runtime.exceptionservices.exceptiondispatchinfo(v = vs.110).aspx)**负责重新抛出的奇迹async-await模式中UI线程上的后台异常. (3认同)
  • 如果`SecondThreadConcern.LongWork()`抛出异常,是否可以被UI线程捕获?这是一个很好的帖子,顺便说一句。 (2认同)
  • 我在答案中添加了一个附加部分以满足您的要求。问候。 (2认同)
  • `Task.Delay(500).Wait()`?创建一个只阻止当前线程的Task有什么意义?你永远不应该阻塞一个线程池线程! (2认同)

Zai*_*sud 231

Marc Gravell 最简单的.NET 4 解决方案的变化:

control.Invoke((MethodInvoker) (() => control.Text = "new text"));
Run Code Online (Sandbox Code Playgroud)

或者使用Action委托代替:

control.Invoke(new Action(() => control.Text = "new text"));
Run Code Online (Sandbox Code Playgroud)

请参阅此处以比较两者:MethodInvoker与Control.BeginInvoke的Action

  • @Dbloom它不是会员,因为它只适用于WinForms.对于WPF,您可以使用Dispatcher.Invoke (3认同)
  • 我正在遵循此解决方案,但有时我的用户界面没有更新。我发现我需要 `this.refresh()` 来强制使 GUI 无效并重新绘制 GUI .. 如果有帮助的话.. (3认同)

Sty*_*ver 135

.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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这可以使用以下代码行调用:

this.UIThread(() => this.myLabel.Text = "Text Goes Here");
Run Code Online (Sandbox Code Playgroud)

  • @jeromeyers - `@ this`只是变量名,在这种情况下是对调用扩展名的当前控件的引用.您可以将其重命名为source,或者将您的船浮起来.我使用`@ this`,因为它指的是调用扩展名的'this Control'并且在普通(非扩展)代码中使用'this'关键字是一致的(至少在我脑海中). (14认同)
  • 这就是我将此扩展命名为“RunOnUiThread”的原因。但这只是个人口味。 (4认同)
  • @this用法有什么意义?"控制"不等同吗?@this有什么好处吗? (3认同)
  • 这很棒,很简单,对我来说是最好的解决方案。您可以将所有必须做的工作包含在 ui 线程中。示例: this.UIThread(() =&gt; { txtMessage.Text = message; listBox1.Items.Add(message); }); (2认同)

Hat*_*ath 65

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

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您的工作线程有一个事件.您的UI线程从另一个线程开始执行工作并挂接该工作事件,以便您可以显示工作线程的状态.

然后在UI中,您需要跨线程来更改实际控件...如标签或进度条.


Ore*_*ost 61

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

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

void updateGUI() {
    // update gui here
}
Run Code Online (Sandbox Code Playgroud)


Don*_*kby 46

线程代码经常出错并且总是难以测试.您无需编写线程代码来从后台任务更新用户界面.只需使用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();
    }
Run Code Online (Sandbox Code Playgroud)

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

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

  • 在处理结束时,还可以通过`backgroundWorker1_RunWorkerCompleted`更新用户界面。 (2认同)

Oha*_*der 41

绝大多数答案Control.Invoke都是等待发生竞争条件.例如,考虑接受的答案:

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

如果用户在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);
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,在.NET 4.0及更高版本中,您应该使用异步操作的任务.请参阅n-san的答案,了解基于任务的等效方法(使用TaskScheduler.FromCurrentSynchronizationContext).

最后,在.NET 4.5及更高版本中,您也可以使用Progress<T>(基本上可以捕获SynchronizationContext.Current它的创建),如RyszardDżegan所展示的那样,长时间运行的操作需要在仍然工作时运行UI代码.


Fre*_*els 35

您必须确保更新发生在正确的线程上; 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 ( ... );
      }
   }
}
Run Code Online (Sandbox Code Playgroud)

请注意,上面的代码不适用于WPF项目,因为WPF控件不实现该ISynchronizeInvoke接口.

为了确保上面的代码与Windows窗体和WPF,和所有其他平台的作品,你可以看看AsyncOperation,AsyncOperationManagerSynchronizationContext类.

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

MyEvent.Raise(this, EventArgs.Empty);
Run Code Online (Sandbox Code Playgroud)

当然,您也可以使用BackGroundWorker类,它将为您抽象出这个问题.


Kie*_*ron 29

您需要在GUI线程上调用该方法.您可以通过调用Control.Invoke来实现.

例如:

delegate void UpdateLabelDelegate (string message);

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

    MyLabelControl.Text = message;
}
Run Code Online (Sandbox Code Playgroud)


Bri*_*eon 28

由于场景的微不足道,我实际上会对状态进行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...";
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

该方法避免了使用ISynchronizeInvoke.InvokeISynchronizeInvoke.BeginInvoke方法时所需的编组操作.使用编组技术没有任何问题,但您需要注意几个警告.

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

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

  • 而不是在用户界面和工作线程保持松耦合Control.InvokeControl.BeginInvoke的做法,紧密结合他们.
  • UI线程不会妨碍工作线程的进度.
  • 工作线程不能支配UI线程花费更新的时间.
  • UI和工作线程执行操作的时间间隔可以保持独立.
  • 工作线程不能超出UI线程的消息泵.
  • UI线程可以决定UI何时以及多久更新一次.

  • 好主意.你没有提到的唯一一件事是你在WorkerThread完成后如何正确处理计时器.请注意,这可能会在应用程序结束时(即用户关闭应用程序)导致问题.你知道如何解决这个问题吗? (3认同)

小智 27

之前的答案中没有必要的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
}
Run Code Online (Sandbox Code Playgroud)

  • 您认为Post方法在引擎盖下使用了什么?:) (4认同)

小智 23

这个类似于上面使用.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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

使用:

this.lblTimeDisplay.SetPropertyValue(a => a.Text, "some string");
this.lblTimeDisplay.SetPropertyValue(a => a.Visible, false);
Run Code Online (Sandbox Code Playgroud)

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

this.lblTimeDisplay.SetPropertyValue(a => a.Visible, "sometext");
Run Code Online (Sandbox Code Playgroud)


bgm*_*der 23

Salvete!在搜索了这个问题之后,我发现FrankGOregon Ghost的答案对我来说是最简单的.现在,我在Visual Basic中编码并通过转换器运行此代码段; 所以我不确定结果如何.

我有一个名为的对话框form_Diagnostics,,其中有一个richtext框,updateDiagWindow,我将其用作一种日志记录显示.我需要能够从所有线程更新其文本.额外的线条允许窗口自动滚动到最新的线条.

所以,我现在可以用你认为它可以在没有任何线程的情况下工作的方式从整个程序的任何地方用一行更新显示:

  form_Diagnostics.updateDiagWindow(whatmessage);
Run Code Online (Sandbox Code Playgroud)

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

#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
Run Code Online (Sandbox Code Playgroud)


Fra*_*nkg 21

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

public delegate void serviceGUIDelegate();
private void updateGUI()
{
  this.Invoke(new serviceGUIDelegate(serviceGUI));
}
Run Code Online (Sandbox Code Playgroud)

"serviceGUI()"是表单(this)中的GUI级方法,可以根据需要更改任意数量的控件.从另一个线程调用"updateGUI()".可以添加参数来传递值,或者(可能更快)使用带有锁定的类范围变量,如果有可能在访问它们的线程之间发生冲突而导致不稳定.如果非GUI线程是时间关键的,请使用BeginInvoke而不是Invoke(记住Brian Gideon的警告).


小智 21

这是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);
}
Run Code Online (Sandbox Code Playgroud)

你这样称呼它:

myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
Run Code Online (Sandbox Code Playgroud)
  1. 它将"检查"添加到"as MemberExpression"的结果中.
  2. 它提高了静态类型的安全性.

否则,原来是一个非常好的解决方案.


ILo*_*ran 21

Label lblText; //initialized elsewhere

void AssignLabel(string text)
{
   if (InvokeRequired)
   {
      BeginInvoke((Action<string>)AssignLabel, text);
      return;
   }

   lblText.Text = text;           
}
Run Code Online (Sandbox Code Playgroud)

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

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

这实际上导致我们发布的一些软件挂起.这是很容易通过更换来解决Invoke()BeginInvoke().除非您需要同步操作(如果需要返回值,可能就是这种情况),请使用BeginInvoke().


小智 20

当我遇到同样的问题时,我向谷歌寻求帮助,但不是给我一个简单的解决方案,而是通过举例MethodInvoker和等等等等方式让我更加困惑.所以我决定自己解决它.这是我的解决方案:

像这样做一个代表:

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
}
Run Code Online (Sandbox Code Playgroud)

您可以在这样的新线程中调用此函数

Thread th = new Thread(() => Updatelabel("Hello World"));
th.start();
Run Code Online (Sandbox Code Playgroud)

不要混淆Thread(() => .....).我在线程上工作时使用匿名函数或lambda表达式.为了减少代码行,你也可以使用这个ThreadStart(..)方法,我不应该在这里解释.


Has*_*man 17

只需使用以下内容:

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


Emb*_*rja 15

您可以使用已存在的委托Action:

private void UpdateMethod()
{
    if (InvokeRequired)
    {
        Invoke(new Action(UpdateMethod));
    }
}
Run Code Online (Sandbox Code Playgroud)


Maj*_*cRa 14

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

没有参数:

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

        // Your code!
    }
Run Code Online (Sandbox Code Playgroud)

对于具有参数的函数:

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

那就是IT.


一些论证:通常,if ()在一行中的语句之后放置{}对于代码可读性是不利的.但在这种情况下,它是常规的"咒语".如果此方法在项目中保持一致,则不会破坏代码可读性.它可以避免您的代码乱扔垃圾(一行代码而不是五行).

如您所见,if(InvokeRequired) {something long}您只知道"此功能可以安全地从另一个线程调用".


小智 13

尝试使用此刷新标签

public static class ExtensionMethods
{
    private static Action EmptyDelegate = delegate() { };

    public static void Refresh(this UIElement uiElement)
    {
        uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
    }
}
Run Code Online (Sandbox Code Playgroud)


bla*_*ind 13

创建一个类变量:

SynchronizationContext _context;
Run Code Online (Sandbox Code Playgroud)

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

var _context = SynchronizationContext.Current;
Run Code Online (Sandbox Code Playgroud)

如果要更新标签:

_context.Send(status =>{
    // UPDATE LABEL
}, null);
Run Code Online (Sandbox Code Playgroud)


A. *_*nis 12

您必须使用invoke和delegate

private delegate void MyLabelDelegate();
label1.Invoke( new MyLabelDelegate(){ label1.Text += 1; });
Run Code Online (Sandbox Code Playgroud)


Man*_*ddy 9

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

我有一个WPF应用程序,并定义了一个worker如下:

问题:

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"
}
Run Code Online (Sandbox Code Playgroud)

解:

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

我还没有找出上述内容的含义,但它确实有效.

对于WinForms:

解:

txtLog.Invoke((MethodInvoker)delegate
{
    txtLog.Text = "my message";
});
Run Code Online (Sandbox Code Playgroud)


flo*_*dis 9

还有另一种通用的控制扩展方法..

首先为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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

object1.InvokeIfRequired(c => { c.Visible = true; });
object1.InvokeIfRequired(c => { c.Text = "ABC"; });
Run Code Online (Sandbox Code Playgroud)

..或者像这样

object1.InvokeIfRequired(c => 
  { 
      c.Text = "ABC";
      c.Visible = true; 
  }
);
Run Code Online (Sandbox Code Playgroud)


小智 8

我认为最简单的方法:

   void Update()
   {
       BeginInvoke((Action)delegate()
       {
           //do your update
       });
   }
Run Code Online (Sandbox Code Playgroud)


小智 8

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

Speed_Threshold = 30;
textOutput.Invoke(new EventHandler(delegate
{
    lblThreshold.Text = Speed_Threshold.ToString();
}));
Run Code Online (Sandbox Code Playgroud)

lblThreshold一个Label,Speed_Threshold是一个全局变量.


nos*_*lan 8

当您在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;
  }
}
Run Code Online (Sandbox Code Playgroud)

这适用于任务(而不是线程),这是现在编写并发代码首选方法.


Sum*_*ume 8

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

在前面的答案中大量描述的众所周知的公式使用了InvokeRequired属性,涵盖了大多数情况,但不包括整个池.

如果还没有创建Handle怎么办?

InvokeRequired属性,描述在这里(Control.InvokeRequired物业参考MSDN)如果呼叫从一个线程不是GUI线程,假做返回true或者如果呼叫是从GUI线程做,或者如果手柄是尚未创建.

如果您希望另一个线程显示和更新模式表单,则可能会遇到异常.因为您希望以模态方式显示该表单,所以您可以执行以下操作:

private MyForm _gui;

public void StartToDoThings()
{
    _gui = new MyForm();
    Thread thread = new Thread(SomeDelegate);
    thread.Start();
    _gui.ShowDialog();
}
Run Code Online (Sandbox Code Playgroud)

代理可以在GUI上更新Label:

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!";
}
Run Code Online (Sandbox Code Playgroud)

如果标签更新之前的操作"花费更少的时间"(读取并将其解释为简化),则会导致InvalidOperationException,而不是GUI线程创建Form句柄所花费的时间.这发生在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!";
}
Run Code Online (Sandbox Code Playgroud)

如果还没有创建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();
  }
}
Run Code Online (Sandbox Code Playgroud)

我提供的表单由另一个具有此ThreadSafeGuiCommand实例的线程更新,我定义了更新GUI(在我的表单中)的方法,如下所示:

public void SetLabeTextTo(string value)
{
  _threadSafeGuiCommand.Execute(this, delegate { Label1.Text = value; });
}
Run Code Online (Sandbox Code Playgroud)

通过这种方式,我非常确定我会更新任何线程进行调用的GUI,可选择等待明确定义的时间(超时).


Yul*_*mok 7

即使操作很耗时(在我的示例中为thread.sleep) - 此代码不会锁定您的UI:

 private void button1_Click(object sender, EventArgs e)
 {

      Thread t = new Thread(new ThreadStart(ThreadJob));
      t.IsBackground = true;
      t.Start();         
 }

 private void ThreadJob()
 {
     string newValue= "Hi";
     Thread.Sleep(2000); 

     this.Invoke((MethodInvoker)delegate
     {
         label1.Text = newValue; 
     });
 }
Run Code Online (Sandbox Code Playgroud)


Bas*_*ANI 7

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

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

  • 如果您使用的是WPF应用程序,这是正确的.但他正在使用Windows Forms. (3认同)

use*_*373 7

只使用 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();
    }
}
Run Code Online (Sandbox Code Playgroud)


小智 6

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

我注意到如果你的代码在创建控件的窗口句柄之前执行(例如在显示表单之前),则Invoke抛出异常.所以我建议InvokeRequired在打电话之前一直检查InvokeBeginInvoke.


小智 6

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

例子:

SynchronizationContext ctx = SynchronizationContext.Current; // From control
ctx.Send\Post... // From worker thread
Run Code Online (Sandbox Code Playgroud)


MBH*_*MBH 6

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

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;
}
Run Code Online (Sandbox Code Playgroud)

这些片段对我有用,所以我可以在另一个线程上做一些事情,然后我更新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);
}
Run Code Online (Sandbox Code Playgroud)

更简单:

btn_login.Invoke(new Action(()=>{ /* HERE YOU ARE ON GUI */ }));
Run Code Online (Sandbox Code Playgroud)


小智 5

也许有些过量,但这是我通常解决此问题的一种方式:

由于同步,此处不需要调用。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); }

}
Run Code Online (Sandbox Code Playgroud)


Joh*_*ers 5

这是使用更实用的样式的古老问题的新外观。如果将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;
    }
}
Run Code Online (Sandbox Code Playgroud)


Mus*_*laa 5

首先获取表单的实例(在本例中为 mainForm),然后在另一个线程中使用此代码。

mainForm.Invoke(new MethodInvoker(delegate () 
{
    // Update things in my mainForm here
    mainForm.UpdateView();
}));
Run Code Online (Sandbox Code Playgroud)


Ale*_*rov 5

关于该主题的另一个示例:我制作了一个抽象类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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

public void MethodThatCalledFromBackroundThread()
{
   this.RunOnGuiThread(() => {
       // Do something over UI controls
   });
}
Run Code Online (Sandbox Code Playgroud)

任务示例:

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
});
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

631209 次

最近记录:

6 年,4 月 前