本文共 21201 字,大约阅读时间需要 70 分钟。
从另一个线程更新Label
的最简单方法是什么?
我在thread1
上有一个Form
, thread1
开始,我开始了另一个线程( thread2
)。 当thread2
处理某些文件时,我想使用thread2
的当前状态更新Form
上的Label
。
我怎样才能做到这一点?
Label lblText; //initialized elsewherevoid AssignLabel(string text){ if (InvokeRequired) { BeginInvoke((Action)AssignLabel, text); return; } lblText.Text = text; }
请注意, BeginInvoke()
比Invoke()
BeginInvoke()
更可取,因为它不太可能导致死锁(但是,将文本分配给标签时,这并不是问题):
使用Invoke()
您正在等待方法返回。 现在,可能是您在被调用的代码中执行了一些操作,该操作需要等待线程,如果该线程被埋在您正在调用的某些函数中,则该事件可能不会立即显现出来,而这本身可以通过事件处理程序间接发生。 因此,您将等待线程,线程将等待您,并且您陷入僵局。
这实际上导致我们发布的某些软件挂起。 通过用BeginInvoke()
替换Invoke()
来修复它很容易。 除非需要同步操作(如果需要返回值),请使用BeginInvoke()
。
针对 4 变体:
control.Invoke((MethodInvoker) (() => control.Text = "new text"));
或改用Action委托:
control.Invoke(new Action(() => control.Text = "new text"));
参见此处,对两者进行比较:
您可以使用已经存在的委托Action
:
private void UpdateMethod(){ if (InvokeRequired) { Invoke(new Action(UpdateMethod)); }}
当我遇到相同的问题时,我寻求Google的帮助,但没有给我简单的解决方案,而是举了MethodInvoker
和blah blah之类的例子,这使我更加困惑。 所以我决定自己解决。 这是我的解决方案:
像这样委托:
Public delegate void LabelDelegate(string s);void Updatelabel(string text){ if (label.InvokeRequired) { LabelDelegate LDEL = new LabelDelegate(Updatelabel); label.Invoke(LDEL, text); } else label.Text = text}
您可以像这样在新线程中调用此函数
Thread th = new Thread(() => Updatelabel("Hello World"));th.start();
不要与Thread(() => .....)
混淆。 在线程上工作时,我使用匿名函数或lambda表达式。 为了减少代码行,您也可以使用ThreadStart(..)
方法,在此不做解释。
先前答案中的“调用”东西都不是必需的。
您需要查看WindowsFormsSynchronizationContext:
// In the main threadWindowsFormsSynchronizationContext 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}
尝试使用此刷新标签
public static class ExtensionMethods{ private static Action EmptyDelegate = delegate() { }; public static void Refresh(this UIElement uiElement) { uiElement.Dispatcher.Invoke(DispatcherPriority.Render, EmptyDelegate); }}
从您应该 (包括GUI)中使用以及 - 关键字:
TAP是新开发的推荐异步设计模式
而不是和 (后者包括 )。
然后,针对新开发的推荐解决方案是:
事件处理程序的异步实现(是的,仅此而已):
private async void Button_Clicked(object sender, EventArgs e) { var progress = new Progress(s => label.Text = s); await Task.Factory.StartNew(() => SecondThreadConcern.LongWork(progress), TaskCreationOptions.LongRunning); label.Text = "completed"; }
通知UI线程的第二个线程的实现:
class SecondThreadConcern { public static void LongWork(IProgressprogress) { // Perform a long running work... for (var i = 0; i < 10; i++) { Task.Delay(500).Wait(); progress.Report(i.ToString()); } } }
请注意以下几点:
有关更详细的示例,请参见: ( 的 。
另请参阅关于概念。
下面的代码段示例说明了如何处理异常以及切换按钮的Enabled
属性以防止在后台执行过程中多次单击。
private async void Button_Click(object sender, EventArgs e){ button.Enabled = false; try { var progress = new Progress(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 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..."); }}
您必须使用invoke和委托
private delegate void MyLabelDelegate();label1.Invoke( new MyLabelDelegate(){ label1.Text += 1; });
我认为最简单的方法是:
void Update() { BeginInvoke((Action)delegate() { //do your update }); }
创建一个类变量:
SynchronizationContext _context;
在创建您的UI的构造函数中进行设置:
var _context = SynchronizationContext.Current;
当您要更新标签时:
_context.Send(status =>{ // UPDATE LABEL}, null);
例如,访问当前线程以外的控件:
Speed_Threshold = 30;textOutput.Invoke(new EventHandler(delegate{ lblThreshold.Text = Speed_Threshold.ToString();}));
那里lblThreshold
是一个Label,而Speed_Threshold
是一个全局变量。
当您进入UI线程时,可以要求其提供同步上下文任务计划程序。 这将为您提供一个 ,以调度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(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; }}
这适用于任务(不是线程),这是的 。
绝大多数答案使用Control.Invoke
,这是一种的 。 例如,考虑接受的答案:
string newText = "abc"; // running on worker threadthis.Invoke((MethodInvoker)delegate { someLabel.Text = newText; // runs on UI thread});
如果用户在此之前关闭窗体。调用this.Invoke
(请记住, this
是Form
对象),则可能会引发ObjectDisposedException
。
解决方案是使用SynchronizationContext
,特别是使用建议的SynchronizationContext.Current
(其他答案依赖于完全不需要的特定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及更高版本上,您实际上应该将任务用于异步操作。 请参阅答案以获取基于任务的等效方法(使用TaskScheduler.FromCurrentSynchronizationContext
)。
最终,在.NET 4.5及更高版本上,您还可以使用Progress<T>
(基本上捕获SynchronizationContext.Current
在创建时),这由演示,用于长时间运行的操作需要在运行时仍运行UI代码的情况。
我刚刚阅读了答案,这似乎是一个非常热门的话题。 我目前正在使用.NET 3.5 SP1和Windows窗体。
在前面的答案中大大描述了一个众所周知的公式,该公式利用了InvokeRequired属性,它涵盖了大多数情况,但没有涵盖整个情况。
如果尚未创建句柄怎么办?
如果调用是从不是GUI线程的线程进行的,则描述的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线程创建Form的Handle花费的时间少,则可能导致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进行更新,无论发出呼叫的线程是什么,可以选择等待明确定义的时间(超时)。
只需使用如下代码:
this.Invoke((MethodInvoker)delegate { progressBar1.Value = e.ProgressPercentage; // runs on UI thread });
出于许多目的,它就像这样简单:
public delegate void serviceGUIDelegate();private void updateGUI(){ this.Invoke(new serviceGUIDelegate(serviceGUI));}
“ serviceGUI()”是表单(this)中的GUI级别方法,可以根据需要更改任意数量的控件。 从另一个线程调用“ updateGUI()”。 可以添加参数以传递值,或者(如果更快)在访问它们的线程之间可能发生冲突而导致不稳定的情况下,根据需要使用带有锁的类范围变量。 如果非GUI线程时间紧迫,请使用BeginInvoke而不是Invoke(牢记Brian Gideon的警告)。
由于场景的琐碎性,我实际上将获得状态的UI线程轮询。 我认为您会发现它可能非常优雅。
public class MyForm : Form{ private volatile string m_Text = ""; private System.Timers.Timer m_Timer; private MyForm() { m_Timer = new System.Timers.Timer(); m_Timer.SynchronizingObject = this; m_Timer.Interval = 1000; m_Timer.Elapsed += (s, a) => { MyProgressLabel.Text = m_Text; }; m_Timer.Start(); var thread = new Thread(WorkerThread); thread.Start(); } private void WorkerThread() { while (...) { // Periodically publish progress information. m_Text = "Still working..."; } }}
该方法避免了使用ISynchronizeInvoke.Invoke
和ISynchronizeInvoke.BeginInvoke
方法时所需的编组操作。 使用封送处理技术没有错,但是您需要注意一些警告。
BeginInvoke
否则它可能会超出消息泵。 Invoke
是阻塞调用。 它将暂时中止该线程中正在进行的工作。 我在这个答案中提出的策略颠倒了线程的通信角色。 UI线程会轮询它,而不是辅助线程推送数据。 这是在许多情况下使用的常见模式。 因为您要做的只是显示工作线程中的进度信息,所以我认为您会发现该解决方案是封送处理解决方案的绝佳替代方案。 具有以下优点。
Control.Invoke
或Control.BeginInvoke
方法。 线程代码通常有漏洞,并且总是很难测试。 您无需编写线程代码即可从后台任务更新用户界面。 只需使用类来运行任务及其方法即可更新用户界面。 通常,您只报告完成百分比,但是还有一个包含状态对象的重载。 这是一个仅报告字符串对象的示例:
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
方法将被完全忽略。
.NET 3.5及以上版本的一劳永逸扩展方法
using System;using System.Windows.Forms;public static class ControlExtensions{ ////// Executes the Action asynchronously on the UI thread, does not block execution on the calling thread. /// /// /// 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");
在这个问题上,大多数其他答案对我来说有点复杂(我是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"));}
我还没有找出上面这行的意思,但是它有效。
对于 :
解:
txtLog.Invoke((MethodInvoker)delegate{ txtLog.Text = "my message";});
这个类似于上面使用.NET Framework 3.0的解决方案,但是它解决了编译时安全支持的问题 。
public static class ControlExtension{ delegate void SetPropertyValueHandler(Control souce, Expression > selector, TResult value); public static void SetPropertyValue (this Control source, Expression > selector, TResult value) { if (source.InvokeRequired) { var del = new SetPropertyValueHandler (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");
您必须确保更新发生在正确的线程上。 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 Forms和WPF以及所有其他平台一起使用,可以查看AsyncOperation
, AsyncOperationManager
和SynchronizationContext
类。
为了以这种方式轻松引发事件,我创建了一个扩展方法,该方法允许我通过调用以下方法简化引发事件:
MyEvent.Raise(this, EventArgs.Empty);
当然,您也可以使用BackGroundWorker类,该类将为您抽象此问题。
简单的解决方案是使用Control.Invoke
。
void DoSomething(){ if (InvokeRequired) { Invoke(new MethodInvoker(updateGUI)); } else { // Do Something updateGUI(); }}void updateGUI() { // update gui here}
您需要在GUI线程上调用该方法。 您可以通过调用Control.Invoke来实现。
例如:
delegate void UpdateLabelDelegate (string message);void UpdateLabel (string message){ if (InvokeRequired) { Invoke (new UpdateLabelDelegate (UpdateLabel), message); return; } MyLabelControl.Text = message;}
最简单的方法是将匿名方法传递给 :
// Running on the worker threadstring newText = "abc";form.Label.Invoke((MethodInvoker)delegate { // Running on the UI thread form.Label.Text = newText;});// Back on the worker thread
请注意, Invoke
阻止执行直到完成为止-这是同步代码。 这个问题并没有询问异步代码,但是您想了解异步代码时有很多涉及编写异步代码。
这是您应该执行的经典方式:
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(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 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线程从另一个线程开始进行工作,并连接该工作程序事件,以便您可以显示工作程序线程的状态。
然后,在用户界面中,您需要跨线程来更改实际控件……例如标签或进度条。
对于.NET 2.0,这是我编写的大量代码,完全可以满足您的要求,并且可以用于Control
上的任何属性:
private delegate void SetControlPropertyThreadSafeDelegate( Control control, string propertyName, object propertyValue);public static void SetControlPropertyThreadSafe( Control control, string propertyName, object propertyValue){ if (control.InvokeRequired) { control.Invoke(new SetControlPropertyThreadSafeDelegate (SetControlPropertyThreadSafe), new object[] { control, propertyName, propertyValue }); } else { control.GetType().InvokeMember( propertyName, BindingFlags.SetProperty, null, control, new object[] { propertyValue }); }}
这样称呼它:
// thread-safe equivalent of// myLabel.Text = status;SetControlPropertyThreadSafe(myLabel, "Text", status);
如果您使用的是.NET 3.0或更高版本,则可以将上述方法重写为Control
类的扩展方法,从而将调用简化为:
myLabel.SetPropertyThreadSafe("Text", status);
2010年5月10日更新:
对于.NET 3.0,您应该使用以下代码:
private delegate void SetPropertyThreadSafeDelegate( Control @this, Expression > property, TResult value);public static void SetPropertyThreadSafe ( this Control @this, Expression > 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 (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版本好很多。
如果有人对如何提高此代码的编译时安全性有任何进一步的建议,请发表评论!
这是我Ian Kemp解决方案的C#3.0变体:
public static void SetPropertyInGuiThread(this C control, Expression > 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 >, V>)SetPropertyInGuiThread, new object[] { control, property, value } ); else propertyInfo.SetValue(control, value, null);}
您这样称呼它:
myButton.SetPropertyInGuiThread(b => b.Text, "Click Me!")
否则,原始版本是一个非常好的解决方案。
Salvete! 搜索了这个问题之后,我发现FrankG和Oregon Ghost的答案对我来说是最简单,最有用的。 现在,我用Visual Basic编写代码,并通过转换器运行此代码段。 所以我不确定结果如何。
我有一个名为form_Diagnostics,
的对话框form_Diagnostics,
其中有一个form_Diagnostics,
框,称为updateDiagWindow,
我将其用作日志记录显示。 我需要能够从所有线程更新其文本。 多余的行允许窗口自动滚动到最新的行。
因此,我现在可以在整个程序中的任何位置以一行的方式更新显示,以您认为无需任何线程即可工作的方式进行:
form_Diagnostics.updateDiagWindow(whatmessage);
主代码(将其放入表单的类代码中):
#region "---------Update Diag Window Text------------------------------------"// This sub allows the diag window to be updated by all threadspublic 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 threadsprivate delegate void UpdateDiagDelegate(string whatmessage);private void UpdateDiag(string whatmessage){ var _with2 = diagwindow; _with2.appendtext(whatmessage); _with2.SelectionStart = _with2.Text.Length; _with2.ScrollToCaret();}#endregion
我的版本是插入一行递归“咒语”:
没有参数:
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(Bbb), new[] { x, text }); return; } // Your code! }
那就是它 。
一些争论 :通常,将代码{}放在if ()
语句后一行不利于代码的可读性。 但是在这种情况下,这是常规的“咒语”。 如果此方法在项目中一致,则不会破坏代码的可读性。 而且可以节省乱码(一行而不是五行)。
正如您看到if(InvokeRequired) {something long}
您只知道“从另一个线程调用该函数很安全”。
转载地址:http://bddnb.baihongyu.com/