How do I update the GUI from another thread?
从另一个线程更新
我在
我该怎么做?
最简单的方法是将匿名方法传入
1 2 3 4 5 6 7 | // 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 |
注意,
对于.NET 2.0,我编写了一段很好的代码,它完全满足您的需要,并且适用于
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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 }); } } |
这样称呼它:
1 2 3 | // thread-safe equivalent of // myLabel.Text = status; SetControlPropertyThreadSafe(myLabel,"Text", status); |
如果您使用的是.NET 3.0或更高版本,则可以将上述方法重写为
1 | myLabel.SetPropertyThreadSafe("Text", status); |
2010年10月5日更新:
对于.NET 3.0,应使用以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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表达式来实现更清晰、更简单和更安全的语法:
1 | myLabel.SetPropertyThreadSafe(() => myLabel.Text, status); // status has to be a string or this will fail to compile |
不仅在编译时检查了属性名,而且该属性的类型也是,因此(例如)无法将字符串值赋给布尔属性,从而导致运行时异常。
不幸的是,这并不能阻止任何人做一些愚蠢的事情,比如传入另一个
1 | myLabel.SetPropertyThreadSafe(() => aForm.ShowIcon, false); |
因此,我添加了运行时检查,以确保传入的属性实际上属于正在调用该方法的
如果有人对如何改进此代码以提高编译时安全性有任何进一步的建议,请发表评论!
处理长时间工作
由于.NET 4.5和C 5.0,您应该在所有区域(包括GUI)中使用基于任务的异步模式(TAP)和异步等待关键字:
TAP is the recommended asynchronous design pattern for new development
而不是异步编程模型(APM)和基于事件的异步模式(EAP)(后者包括BackgroundWorker类)。
那么,新开发的建议解决方案是:
事件处理程序的异步实现(是,仅此而已):
1 2 3 4 5 6 7 | 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"; } |
通知UI线程的第二个线程的实现:
1 2 3 4 5 6 7 8 9 10 11 12 | 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()); } } } |
注意以下事项:
关于更详细的例子,请看:C的未来:好的事情发生在那些"等待"约瑟夫·阿尔巴哈里的人身上。
另请参见关于UI线程模型概念。
异常处理下面的代码片段是一个如何处理异常和切换按钮的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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..."); } } |
Marc Gravell对.NET 4最简单解的变化:
1 | control.Invoke((MethodInvoker) (() => control.Text ="new text")); |
或者使用动作委托:
1 |
请参阅此处以比较这两种方法:MethodInvoker与Action for Control.BeginInvoke
.NET 3.5的Fire and Forget扩展方法+
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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(); } } } |
这可以使用以下代码行调用:
1 | this.UIThread(() => this.myLabel.Text ="Text Goes Here"); |
这是您应该采用的经典方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | 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中,您需要跨线程来更改实际控件…像标签或进度条。
简单的解决方案是使用
1 2 3 4 5 6 7 8 9 10 11 12 13 | void DoSomething() { if (InvokeRequired) { Invoke(new MethodInvoker(updateGUI)); } else { // Do Something updateGUI(); } } void updateGUI() { // update gui here } |
线程代码常常有问题,而且总是很难测试。您不需要编写线程代码来从后台任务更新用户界面。只需使用BackgroundWorker类来运行任务及其ReportProgress方法来更新用户界面。通常,您只报告一个完成百分比,但还有另一个重载,其中包含一个状态对象。下面是一个只报告字符串对象的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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方法。
最后一点,一定要设置
绝大多数答案使用
1 2 3 4 | string newText ="abc"; // running on worker thread this.Invoke((MethodInvoker)delegate { someLabel.Text = newText; // runs on UI thread }); |
如果用户在调用
解决方案是使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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及更高版本上,您应该使用任务进行异步操作。关于等效的基于任务的方法(使用
最后,在.NET 4.5及更高版本上,您还可以使用Ryszard d演示的
您必须确保更新发生在正确的线程上,即UI线程上。
为了做到这一点,您必须调用事件处理程序,而不是直接调用它。
您可以通过如下方式引发事件:
(代码是在我脑子里打出来的,所以我没有检查正确的语法,等等,但它应该能让你继续。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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控件不实现
为了确保上述代码适用于Windows窗体和WPF以及所有其他平台,您可以查看
为了以这种方式轻松引发事件,我创建了一个扩展方法,它允许我通过调用以下命令来简化引发事件的过程:
1 | MyEvent.Raise(this, EventArgs.Empty); |
当然,您也可以使用BackgroundWorker类,它将为您抽象这个问题。
您需要在GUI线程上调用该方法。您可以通过调用control.invoke来实现这一点。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 | delegate void UpdateLabelDelegate (string message); void UpdateLabel (string message) { if (InvokeRequired) { Invoke (new UpdateLabelDelegate (UpdateLabel), message); return; } MyLabelControl.Text = message; } |
前面的答案中的调用内容都不是必需的。
您需要查看WindowsFormsSynchronizationContext:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 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 } |
由于场景的琐碎性,我实际上需要对状态进行UI线程轮询。我想你会发现它非常优雅。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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..."; } } } |
该方法避免了使用
- 确保不要过于频繁地给
BeginInvoke 打电话,否则会超出消息泵的工作范围。 - 在工作线程上调用
Invoke 是一个阻塞调用。它将暂时停止该线程中正在执行的工作。
我在这个答案中提出的策略将反转线程的通信角色。而不是工作线程推送数据,而是UI线程对其进行轮询。这是许多场景中使用的常见模式。因为您所要做的只是显示来自工作线程的进度信息,所以我认为您将发现此解决方案是封送解决方案的一个很好的替代方案。它有以下优点。
- UI和工作线程保持松散耦合,而不是与它们紧密耦合的
Control.Invoke 或Control.BeginInvoke 方法。 - UI线程不会妨碍工作线程的进度。
- 工作线程不能支配UI线程花费的更新时间。
- UI和工作线程执行操作的间隔可以保持独立。
- 工作线程不能超过UI线程的消息泵。
- 用户界面线程决定用户界面的更新时间和频率。
萨维特!在搜索了这个问题之后,我发现弗兰克和俄勒冈幽灵的答案对我来说是最简单最有用的。现在,我用VisualBasic编写代码,并通过转换器运行这段代码;所以我不太确定它是如何产生的。
我有一个名为
因此,我现在可以用一行来更新整个程序中的任何地方的显示,以您认为它不需要任何线程就可以工作的方式:
1 | form_Diagnostics.updateDiagWindow(whatmessage); |
主代码(将其放入表单的类代码中):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #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 |
这一解决方案类似于上述使用.NET Framework 3.0的解决方案,但它解决了编译时安全支持的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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); } } } |
使用:
1 2 | this.lblTimeDisplay.SetPropertyValue(a => a.Text,"some string"); this.lblTimeDisplay.SetPropertyValue(a => a.Visible, false); |
如果用户传递错误的数据类型,编译器将失败。
1 | this.lblTimeDisplay.SetPropertyValue(a => a.Visible,"sometext"); |
在很多方面,它都很简单:
1 2 3 4 5 | public delegate void serviceGUIDelegate(); private void updateGUI() { this.Invoke(new serviceGUIDelegate(serviceGUI)); } |
"servicegui()"是表单(this)中的一个GUI级别的方法,可以根据需要更改任意多个控件。从另一个线程调用"updateGUI()"。可以添加参数来传递值,或者(可能更快)根据需要使用具有锁的类作用域变量,如果访问它们的线程之间存在任何可能导致不稳定的冲突。如果非GUI线程是时间关键的,请使用begininvoke而不是invoke(记住brian gideon的警告)。
在我的C 3.0版本中,这是Ian Kemp解决方案的变化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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); } |
你这样称呼它:
1 | myButton.SetPropertyInGuiThread(b => b.Text,"Click Me!") |
否则,原来是一个非常好的解决方案。
1 2 3 4 5 6 7 8 9 10 11 12 | Label lblText; //initialized elsewhere void AssignLabel(string text) { if (InvokeRequired) { BeginInvoke((Action<string>)AssignLabel, text); return; } lblText.Text = text; } |
请注意,与
当使用
这实际上导致我们发布的一些软件挂起。用
当我遇到同样的问题时,我寻求谷歌的帮助,但并没有给出一个简单的解决方案,而是给出了
这样做代表:
1 2 3 4 5 6 7 8 9 10 11 12 | 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 } |
您可以在这样的新线程中调用此函数
1 2 |
不要与
简单地使用如下内容:
1 2 3 4 | this.Invoke((MethodInvoker)delegate { progressBar1.Value = e.ProgressPercentage; // runs on UI thread }); |
您可以使用现有的代表
1 2 3 4 5 6 7 |
我的版本是插入一行递归的"咒语":
对于无参数:
1 2 3 4 5 6 | void Aaaaaaa() { if (InvokeRequired) { Invoke(new Action(Aaaaaaa)); return; } //1 line of mantra // Your code! } |
对于具有参数的函数:
1 2 3 4 5 |
就是这样。
一些论证:通常在一行中的
正如您看到的
创建类变量:
1 | SynchronizationContext _context; |
在创建用户界面的构造函数中设置它:
1 | var _context = SynchronizationContext.Current; |
要更新标签时:
1 2 3 | _context.Send(status =>{ // UPDATE LABEL }, null); |
尝试使用此刷新标签
1 2 3 4 5 6 7 8 9 | public static class ExtensionMethods { private static Action EmptyDelegate = delegate() { }; public static void Refresh(this UIElement uiElement) { uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate); } } |
必须使用Invoke和委托
1 2 | private delegate void MyLabelDelegate(); label1.Invoke( new MyLabelDelegate(){ label1.Text += 1; }); |
在这个问题上,大多数其他的答案对我来说都有点复杂(我对C不熟悉),所以我在写我的:
我有一个WPF应用程序,并定义了一个工人,如下所示:
问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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" } |
解决方案:
1 2 3 4 5 | workerAllocator.DoWork += delegate (object sender1, DoWorkEventArgs e1) { // The below single line works txtLog.Dispatcher.BeginInvoke((Action)(() => txtLog.Text ="my message")); } |
我还没有弄清楚上面这条线是什么意思,但它是有效的。
对于WiFrase:
解决方案:
1 2 3 4 | txtLog.Invoke((MethodInvoker)delegate { txtLog.Text ="my message"; }); |
例如,访问当前线程以外的控件:
1 2 3 4 5 | Speed_Threshold = 30; textOutput.Invoke(new EventHandler(delegate { lblThreshold.Text = Speed_Threshold.ToString(); })); |
其中,
当您在UI线程中时,可以向它请求同步上下文任务调度程序。它将为您提供一个任务调度程序,用于调度UI线程上的所有内容。
然后,您可以链接您的任务,以便当结果准备就绪时,另一个任务(在UI线程上调度)选择它并将其分配给一个标签。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 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; } } |
这适用于任务(而不是线程),这是目前编写并发代码的首选方式。
我刚刚读了答案,这似乎是一个非常热门的话题。我目前正在使用.NET 3.5 SP1和Windows窗体。
在前面的答案中大量描述的使用invokeRequired属性的著名公式涵盖了大多数情况,但并不是整个池。
如果还没有创建句柄呢?
这里描述的invokeRequired属性(control.invokeRequired属性引用到msdn)如果调用的线程不是gui线程,则返回true;如果调用的线程不是gui线程,则返回false;或者如果尚未创建句柄,则返回false。
如果您想让另一个线程显示和更新模式表单,则可以遇到异常。因为您希望以模式显示该表单,所以可以执行以下操作:
1 2 3 4 5 6 7 8 9 |
学员可以更新GUI上的标签:
1 2 3 4 5 6 7 8 9 | 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线程创建表单句柄所需的时间"花费更少的时间"(读取它并将其解释为简化),则这可能会导致InvalidOperationException。这在showDialog()方法中发生。
您还应该像这样检查手柄:
1 2 3 4 5 6 7 8 9 10 | 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!"; } |
如果尚未创建句柄,则可以处理要执行的操作:您可以忽略GUI更新(如上面的代码所示),也可以等待(风险更大)。这应该可以回答这个问题。
可选材料:我个人想出了以下编码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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(); } } |
我用这个threadsafegui命令的一个实例来提供由另一个线程更新的表单,我定义了更新GUI(在我的表单中)的方法,如下所示:
1 2 3 4 | public void SetLabeTextTo(string value) { _threadSafeGuiCommand.Execute(this, delegate { Label1.Text = value; }); } |
通过这种方式,我确信无论线程调用什么,我都会更新我的GUI,可以选择等待一段定义良好的时间(超时)。
我认为最简单的方法是:
1 2 3 4 5 6 7 | void Update() { BeginInvoke((Action)delegate() { //do your update }); } |
WPF应用程序中最简单的方法是:
1 2 3 4 5 | this.Dispatcher.Invoke((Action)(() => { // This refers to a form in a WPF application val1 = textBox.Text; // Access the UI })); |
我无法得到这个丑陋的实现背后的微软逻辑,但你必须有两个功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | void setEnableLoginButton() { if (InvokeRequired) { // btn_login can be any conroller, (label, button textbox ..etc.) btn_login.Invoke(new MethodInvoker(setEnable)); // OR //Invoke(new MethodInvoker(setEnable)); } else { setEnable(); } } void setEnable() { btn_login.Enabled = isLoginBtnEnabled; } |
这些代码片段对我很有用,因此我可以在另一个线程上执行一些操作,然后更新GUI:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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); } |
更简单:
1 |
我想添加一个警告,因为我注意到一些简单的解决方案省略了
我注意到,如果您的代码在创建控件的窗口句柄之前执行(例如,在显示窗体之前),那么
即使操作很耗时(在我的示例中是thread.sleep),此代码也不会锁定您的UI:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
关于主题的另一个示例:我创建了一个抽象类uisynchronizeModel,它包含一个公共方法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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); } } |
模型或控制器类应从此抽象类派生。您可以使用任何模式(任务或手动管理的后台线程),并使用以下方法:
1 2 3 4 5 6 | public void MethodThatCalledFromBackroundThread() { this.RunOnGuiThread(() => { // Do something over UI controls }); } |
任务实例:
1 2 3 4 5 6 7 8 9 10 11 12 | 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 }); |
这是一个使用更实用风格的老问题的新外观。如果将taskxm类保留在所有项目中,那么您只有一行代码可以不再担心跨线程更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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; } } |
也许有点过量,但这正是我通常解决这个问题的方法:
由于同步,此处不需要调用。basicClasssThreadExample对我来说只是一种布局,因此请根据您的实际需要更改它。
这很简单,因为您不需要处理UI线程中的内容!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | 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); } } |
基本上,无论框架版本或GUI基础库类型如何,解决此问题的方法都是保存控件,为工作线程创建线程的同步上下文,该线程将把控件的相关交互从工作线程封送到GUI的线程消息队列。
例子:
1 2 | SynchronizationContext ctx = SynchronizationContext.Current; // From control ctx.Send\Post... // From worker thread |
我喜欢这个:
1 2 3 4 5 6 7 8 9 10 11 12 | private void UpdateNowProcessing(string nowProcessing) { if (this.InvokeRequired) { Action<string> d = UpdateNowProcessing; Invoke(d, nowProcessing); } else { this.progressDialog.Next(nowProcessing); } } |
将一些公共变量放在单独的类中以保存该值。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public class data_holder_for_controls { // It will hold the value for your label public string status = string.Empty; } class Demo { public static data_holder_for_controls d1 = new data_holder_for_controls(); static void Main(string[] args) { ThreadStart ts = new ThreadStart(perform_logic); Thread t1 = new Thread(ts); t1.Start(); t1.Join(); //your_label.Text=d1.status; --- can access it from any thread } public static void perform_logic() { // Put some code here in this function for (int i = 0; i < 10; i++) { // Statements here } // Set the result in the status variable d1.status ="Task done"; } } |
还有另一个通用的控制扩展。
首先为控件类型的对象添加扩展方法
1 2 3 4 5 6 7 8 9 10 11 | 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的控件:
1 2 | object1.InvokeIfRequired(c => { c.Visible = true; }); object1.InvokeIfRequired(c => { c.Text ="ABC"; }); |
…或者像这样
1 2 3 4 5 6 | object1.InvokeIfRequired(c => { c.Text ="ABC"; c.Visible = true; } ); |
首先获取表单的实例(在本例中是MainForm),然后在另一个线程中使用此代码。
1 2 3 4 5 | mainForm.Invoke(new MethodInvoker(delegate () { // Update things in my mainForm here mainForm.UpdateView(); })); |
在我的例子(WPF)中,解决方案很简单,如下所示:
1 2 3 4 5 6 7 8 9 10 | private void updateUI() { if (!Dispatcher.CheckAccess()) { Dispatcher.BeginInvoke(updateUI); return; } // Update any number of controls here } |
一般方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | using System; using System.Threading; using System.Windows.Forms; namespace WindowsFormsApp1 { public partial class Form1 : Form { int clickCount = 0; public Form1() { InitializeComponent(); label1.SetText("0"); } private void button1_Click(object sender, EventArgs e) { new Thread(() => label1.SetText((++clickCount).ToString())).Start(); } } public static class ControlExtensions { public static void SetText(this Control control, string text) { if (control.InvokeRequired) control.Invoke(setText, control, text); else control.Text = text; } private static Action<Control, string> setText = (control, text) => control.Text = text; } } |
只使用UI的同步上下文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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(); } } |
最简单的方法是如下调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
为了在WPF中实现这一点,我采用以下方法。
1 2 3 4 5 6 7 | new Thread(() => { while (...) { SomeLabel.Dispatcher.BeginInvoke((Action)(() => SomeLabel.Text = ...)); } }).Start(); |