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


1392

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

  • Form正在上运行thread1,然后从中启动另一个线程(thread2)。

  • thread2处理某些文件时,我想用的工作状态更新Label上的。Formthread2

我该怎么办?


25
.net 2.0+并没有为此提供BackgroundWorker类。它对UI线程的感知。1.创建一个BackgroundWorker 2.添加两个代表(一个代表进行处理,一个代表完成)
Preet Sangha,2009年


4
参见.NET 4.5和C#5.0的答案:stackoverflow.com/a/18033198/2042090
RyszardDżegan2013年

5
此问题不适用于Gtk#GUI。为GTK#看看这个这个答案。
hlovdal 2014年

当心:这个问题的答案现在是混乱的OT(“这是我为WPF应用所做的事情”)和历史悠久的.NET 2.0工件。
Marc L.

Answers:


768

对于.NET 2.0,这是我编写的大量代码,完全可以实现您想要的功能,并且可以在a上的任何属性下使用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版本好很多。

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


3
在某些情况下,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)
Corvin

9
可以SetControlPropertyThreadSafe(myLabel, "Text", status)从另一个模块或类或表单中调用@lan
Smith Smith

71
提供的解决方案不必要地复杂。如果您重视简单性,请参阅Marc Gravell的解决方案或Zaid Masud的解决方案。
Frank Hileman 2014年

8
如果您更新多个属性,则此解决方案会浪费大量资源,因为每次Invoke都会消耗大量资源。我认为这不是线程安全性的原意。是否封装您的UI更新操作并一次调用它(而不是按属性调用)
控制台

4
到底为什么要在BackgroundWorker组件上使用此代码?
安迪

1079

最简单的方法是匿名方法传递到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直到执行完成为止,它都会阻塞执行-这是同步代码。这个问题并没有询问异步代码,但是您想了解异步代码时,Stack Overflow上有很多内容涉及编写异步代码。


8
看起来OP并没有提到表单以外的任何类/实例,这并不是一个糟糕的默认值...
Marc Gravell

39
不要忘记“ this”关键字引用了“ Control”类。
AZ。

8
@codecompleting这两种方法都是安全的,而且我们已经知道我们在工作,所以为什么要检查我们知道的东西?
马克·格雷韦尔

4
@Dragouf不是真的-使用此方法的要点之一是,您已经知道哪些部分在工作程序上运行,哪些部分在UI线程上运行。无需检查。
Marc Gravell

3
@ Joan.bdm没有足够的上下文让我对此发表评论
Marc Gravell

399

处理长时间的工作

.NET 4.5和C#5.0开始,您应该在所有区域(包括GUI)中使用基于任务的异步模式(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. 通知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. 任务而不是Thread
  3. async关键字,它允许使用await,从而防止事件处理程序到达完成状态,直到任务完成为止,同时不会阻塞UI线程。
  4. Progress类(请参阅IProgress接口),它支持关注点分离(SoC)设计原理,并且不需要显式的调度程序和调用。它从创建位置(此处为UI线程)使用当前的SynchronizationContext
  5. TaskCreationOptions.LongRunning提示不要将任务排队到ThreadPool中

有关更详细的示例,请参见:C#的未来:约瑟夫·阿尔巴哈里Joseph Albahari )“等待”人们会感到高兴

另请参阅关于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...");
    }
}

2
如果SecondThreadConcern.LongWork()抛出异常,UI线程是否可以捕获它?顺便说一句,这是一个很棒的帖子。
kdbanman

2
我在答案中添加了另一部分,以满足您的要求。问候。
RyszardDżegan2015年

3
ExceptionDispatchInfo类是负责异步的await图案UI线程重新抛出异常背景的那奇迹。
2015年

1
只是我在想这种方式比调用Invoke / Begin更冗长吗?
MeTitus

2
Task.Delay(500).Wait()?创建仅阻止当前线程的任务有什么意义?您永远不应阻塞线程池线程!
Yarik

236

Marc Gravell针对.NET 4 最简单解决方案的变体:

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

或改用Action委托:

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

参见此处,对两者进行比较:MethodInvoker与Action for Control.BeginInvoke


1
在此示例中,“控制”是什么?我的UI控件?试图在WPF上的标签控件上实现此功能,并且Invoke不是我的标签成员。
Dbloom

像@styxriver stackoverflow.com/a/3588137/206730这样的扩展方法是什么 ?
Kiquenet

声明“动作y”;在类或方法内部更改text属性,并使用这段代码'yourcontrol.Invoke(y =()=> yourcontrol.Text =“ new text”);'更新文本。
Antonio Leite

4
@Dbloom不是会员,因为它仅适用于WinForms。对于WPF,请使用Dispatcher.Invoke
sLw

1
我正在遵循此解决方案,但有时我的用户界面未得到更新。我发现我需要this.refresh()强制无效并重新绘制GUI ..如果有帮助的话..
Rakibul Haq '18

137

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

5
@this用法有什么意义?“控制”不等同吗?@this有什么好处吗?
argyle,2013年

14
@jeromeyers- @this仅是变量名,在这种情况下,是对调用扩展名的当前控件的引用。您可以将其重命名为源,也可以重命名。我使用@this,因为它是指正在调用扩展的“ this Control”,并且与在正常(非扩展)代码中使用“ this”关键字一致(至少在我看来)。
StyxRiver

1
这很棒,简单,对我来说是最好的解决方案。您可以在ui线程中包括所有要做的工作。示例:this.UIThread(()=> {txtMessage.Text = message; listBox1.Items.Add(message);});
自动

1
我真的很喜欢这个解决方案。次要:我会将此方法命名为,OnUIThread而不是UIThread
ToolmakerSteve

2
这就是为什么我将此扩展名命名的原因RunOnUiThread。但这仅仅是个人品味。
Grisgram '19

66

这是您应该执行的经典方式:

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线程从另一个线程开始执行工作,并连接该工作程序事件,以便您可以显示工作程序线程的状态。

然后,在用户界面中,您需要跨线程来更改实际控件……例如标签或进度条。


62

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

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

void updateGUI() {
    // update gui here
}

做得好简单!不仅简单,而且效果很好!我真的不明白为什么Microsoft不能像它本来那样简化它!为了在主线程上调用1行,我们应该编写几个函数!
MBH 2015年

1
@MBH同意。顺便说一句,您是否注意到上面的stackoverflow.com/a/3588137/199364答案,其中定义了扩展方法?在自定义实用程序类中执行一次此操作,然后不必再担心Microsoft不会为我们执行此操作:)
ToolmakerSteve

@ToolmakerSteve这就是它的真正含义!没错,我们可以找到一种方法,但是我的意思是从DRY(不要重复自己)的角度来看,具有共同解决方案的问题可以由Microsoft以最小的努力由他们解决,这将为您节省大量时间程序员:)
MBH

47

线程代码通常有漏洞,并且总是很难测试。您无需编写线程代码即可从后台任务更新用户界面。只需使用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方法将被完全忽略。


2
在处理结束时,还可以通过来更新用户界面backgroundWorker1_RunWorkerCompleted
DavidRR

41

绝大多数答案都在使用Control.Invoke,这是等待发生竞赛条件。例如,考虑接受的答案:

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

如果用户在this.Invoke调用之前关闭表单(记住,thisForm对象),ObjectDisposedException则很可能会触发。

解决方案是使用SynchronizationContext,具体SynchronizationContext.Currenthamilton.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的答案,以了解基于任务的等效方法(使用TaskScheduler.FromCurrentSynchronizationContext)。

最后,在Ryszard D onegan的演示中,在.NET 4.5及更高版本上,您还可以使用Progress<T>(基本上会SynchronizationContext.Current在创建时捕获)在长时间运行的操作仍需要运行UI代码的情况下。


37

您必须确保更新发生在正确的线程上。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控件未实现该ISynchronizeInvoke接口,因此上面的代码不适用于WPF项目。

为了确保以上Windows窗体和WPF,和所有其他平台作品的代码,你可以看看AsyncOperationAsyncOperationManagerSynchronizationContext类。

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

MyEvent.Raise(this, EventArgs.Empty);

当然,您也可以使用BackGroundWorker类,该类将为您抽象此问题。


确实,但是我不喜欢在这件事上“弄乱”我的GUI代码。我的GUI不在乎是否需要调用。换句话说:我不认为执行上下文切换是GUI的责任。
Frederik Gheysels,2009年

1
将委托人分开等等似乎是多余的-为什么不仅仅是:SynchronizationContext.Current.Send(delegate {MyEvent(...);},null);
马克·格雷夫

您是否始终可以访问SynchronizationContext?即使您的班级在类库中?
Frederik Gheysels,2009年

29

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

例如:

delegate void UpdateLabelDelegate (string message);

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

    MyLabelControl.Text = message;
}

1
调用行给了我一个编译器错误。最佳重载方法匹配'System.Windows.Forms.Control.Invoke(System.Delegate,object [])'有一些无效的参数
CruelIO 2009年

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

该方法避免了使用ISynchronizeInvoke.InvokeISynchronizeInvoke.BeginInvoke方法时所需的编组操作。使用封送处理技术没有错,但是您需要注意一些警告。

  • 确保您不要打得BeginInvoke太频繁,否则可能会超出消息泵。
  • 调用Invoke工作线程是阻塞调用。它将暂时中止该线程中正在进行的工作。

我在这个答案中提出的策略颠倒了线程的通信角色。UI线程会轮询它,而不是辅助线程推送数据。这是在许多情况下使用的常见模式。因为您要做的只是显示工作线程中的进度信息,所以我认为您会发现该解决方案是封送处理解决方案的绝佳替代方案。具有以下优点。

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

3
好主意。您唯一没有提到的是,一旦WorkerThread完成,如何正确配置计时器。请注意,这可能在应用程序结束时(即用户关闭应用程序)引起麻烦。您有解决方法的想法吗?
马特

@Matt Elapsed使用成员方法,而不是使用匿名的事件处理程序,因此可以在处理表单时删除计时器...
Phil1970年

@ Phil1970-好点。您是说想System.Timers.ElapsedEventHandler handler = (s, a) => { MyProgressLabel.Text = m_Text; };通过分配它m_Timer.Elapsed += handler;,稍后在配置上下文中做m_Timer.Elapsed -= handler;对吗?并按照此处讨论的建议进行处置/关闭。
马特

27

先前答案中的“调用”东西都不是必需的。

您需要查看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
}

4
您认为Post方法在后台使用什么?:)
令人难以置信的是

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

使用方法:

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

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

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

23

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

我有一个对话框窗体form_Diagnostics,,其中有一个RTF框,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

21

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

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

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

您这样称呼它:

myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
  1. 它将空检查添加到“作为MemberExpression”的结果中。
  2. 它提高了静态类型安全性。

否则,原始版本是一个非常好的解决方案。


21
Label lblText; //initialized elsewhere

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

   lblText.Text = text;           
}

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

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

这实际上导致我们发布的某些软件挂起。这是很容易不足以解决通过更换Invoke()BeginInvoke()。除非需要同步操作(如果需要返回值),请使用BeginInvoke()


20

当我遇到相同的问题时,我寻求Google的帮助,但没有给我简单的解决方案,反而给我更多的例子,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
}

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

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

不要与混淆Thread(() => .....)。在线程上工作时,我使用匿名函数或lambda表达式。为了减少代码行,您也可以使用在ThreadStart(..)此不做解释的方法。


17

只需使用如下代码:

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

如果您有e.ProgressPercentage,您是否已经从调用此方法的方法中进入UI线程?
LarsTech

ProgressChanged事件在UI线程上运行。这是使用BackgroundWorker的便利之一。Completed事件也在gui上运行。在非UI线程中运行的唯一东西是DoWork方法。
LarsTech

15

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

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

14

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

没有参数:

    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}您只知道“可以从另一个线程安全地调用此函数”。


13

尝试使用此刷新标签

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

    public static void Refresh(this UIElement uiElement)
    {
        uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate);
    }
}

Windows窗体吗?
Kiquenet

13

创建一个类变量:

SynchronizationContext _context;

在创建您的UI的构造函数中进行设置:

var _context = SynchronizationContext.Current;

当您要更新标签时:

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

12

您必须使用invoke和委托

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

12

在这个问题上,大多数其他答案对我来说有点复杂(我是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。
Marc L.

谢谢。上面添加了WinForms解决方案。
Manohar Reddy Poreddy

...这只是该问题的其他答案的副本,但是可以。为什么不加入解决方案而只删除答案呢?
Marc L.

嗯,正确的是,如果是的话,您会仔细阅读我的答案,开始部分(我写答案的原因),并希望再多一点注意,您会发现有些人今天遇到了完全相同的问题,并且对此表示反对我的简单回答,如果您能预见到所有这一切为何发生的真实故事,即使我搜索wpf,谷歌也将我发送到这里,甚至更多。当然,由于您错过了这些或多或少显而易见的3个原因,我可以理解为什么您不会删除您的弃权票。与其清理还可以的东西,不如创建新东西,这要困难得多。
Manohar Reddy Poreddy '18年


8

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

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

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


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

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


1
打电话Task.Start通常不是一个好习惯blogs.msdn.com/b/pfxteam/archive/2012/01/14/10256832.aspx
Ohad Schneider

8

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

在前面的答案中大大描述了一个众所周知的公式,该公式利用了InvokeRequired属性,它涵盖了大多数情况,但没有涵盖整个情况。

如果尚未创建句柄怎么办?

如果调用是从不是GUI线程的线程进行的,则此处描述的InvokeRequired属性(对MSDN的Control.InvokeRequired属性的引用)将返回true;如果调用是从GUI线程进行的,或者如果Handle是尚未创建。

如果您想让模式窗体显示并由另一个线程更新,则可能会遇到异常。因为您希望该表格以模态显示,所以您可以执行以下操作:

private MyForm _gui;

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

委托可以在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!";
}

如果标签更新之前的操作“花费更少的时间”(读取并解释为简化)比GUI线程创建FormHandle花费的时间少,则可能导致InvalidOperationException。这发生在ShowDialog()方法中。

您还应该像这样检查句柄

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(在我的Form中):

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

通过这种方式,我非常确定我将使GUI进行更新,无论发出呼叫的线程是什么,可以选择等待明确定义的时间(超时)。


1
来这里找到这个,因为我也检查IsHandleCreated。要检查的另一个属性是IsDisposed。如果您处理了表单,则无法在其上调用Invoke()。如果用户在后台线程完成之前关闭了表单,则您不希望它在处理表单时尝试回调UI。
2016年

我会说从一开始是一个坏主意。通常,您将立即显示子窗体,并在进行后台处理时显示进度条或其他一些反馈。或者,您将先进行所有处理,然后在创建时将结果传递给新表单。同时执行这两个操作通常会带来边际收益,但可维护性却大大降低。
Phil1970

所描述的场景考虑了用作后台线程作业进度视图的模式形式。因为它必须是模式的,所以必须通过调用Form.ShowDialog()方法来显示它。这样,您可以阻止执行调用之后的代码,直到关闭表单为止。因此,除非您可以以不同于给定示例的方式启动后台线程(当然可以),否则必须在后台线程启动后以模态方式显示此表单。在这种情况下,您需要检查要创建的句柄。如果您不需要模态形式,那就另当别论了。
假设
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.