如何从另一个线程调用 UI 方法
本文关键字:调用 UI 方法 线程 另一个 | 更新日期: 2023-09-27 18:31:10
使用计时器进行回合。上下文:具有两个标签的Winforms。
我想看看System.Timers.Timer
是如何工作的,所以我没有使用表单计时器。我知道表单和 myTimer 现在将在不同的线程中运行。有没有一种简单的方法可以用以下形式表示lblValue
上经过的时间?
我已经在MSDN上看过这里,但是有没有更简单的方法!
这是 winforms 代码:
using System.Timers;
namespace Ariport_Parking
{
public partial class AirportParking : Form
{
//instance variables of the form
System.Timers.Timer myTimer;
int ElapsedCounter = 0;
int MaxTime = 5000;
int elapsedTime = 0;
static int tickLength = 100;
public AirportParking()
{
InitializeComponent();
keepingTime();
lblValue.Text = "hello";
}
//method for keeping time
public void keepingTime() {
myTimer = new System.Timers.Timer(tickLength);
myTimer.Elapsed += new ElapsedEventHandler(myTimer_Elapsed);
myTimer.AutoReset = true;
myTimer.Enabled = true;
myTimer.Start();
}
void myTimer_Elapsed(Object myObject,EventArgs myEventArgs){
myTimer.Stop();
ElapsedCounter += 1;
elapsedTime += tickLength;
if (elapsedTime < MaxTime)
{
this.lblElapsedTime.Text = elapsedTime.ToString();
if (ElapsedCounter % 2 == 0)
this.lblValue.Text = "hello world";
else
this.lblValue.Text = "hello";
myTimer.Start();
}
else
{ myTimer.Start(); }
}
}
}
我想你的代码只是一个测试,所以我不会讨论你用计时器做什么。这里的问题是如何在计时器回调中使用用户界面控件执行某些操作。
Control
的大多数方法和属性只能从 UI 线程访问(实际上它们只能从创建它们的线程访问,但这是另一回事)。这是因为每个线程都必须有自己的消息循环(GetMessage()
按线程过滤掉消息),然后要对Control
执行某些操作,您必须将消息从线程调度到主线程。在 .NET 中,这很容易,因为每个Control
都为此目的继承了几个方法:Invoke/BeginInvoke/EndInvoke
。要知道执行线程是否必须调用这些方法,您的属性InvokeRequired
.只需用这个更改您的代码即可使其工作:
if (elapsedTime < MaxTime)
{
this.BeginInvoke(new MethodInvoker(delegate
{
this.lblElapsedTime.Text = elapsedTime.ToString();
if (ElapsedCounter % 2 == 0)
this.lblValue.Text = "hello world";
else
this.lblValue.Text = "hello";
}));
}
请检查 MSDN 以获取可以从任何线程调用的方法列表,就像您始终可以调用Invalidate
、BeginInvoke
、EndInvoke
、Invoke
方法并读取InvokeRequired
属性的参考一样。通常,这是一种常见的使用模式(假设this
是从Control
派生的对象):
void DoStuff() {
// Has been called from a "wrong" thread?
if (InvokeRequired) {
// Dispatch to correct thread, use BeginInvoke if you don't need
// caller thread until operation completes
Invoke(new MethodInvoker(DoStuff));
} else {
// Do things
}
}
请注意,当前线程将阻塞,直到 UI 线程完成方法执行。如果线程的计时很重要,这可能是一个问题(不要忘记 UI 线程可能很忙或挂起了一点)。如果你不需要方法的返回值,你可以简单地用BeginInvoke
替换Invoke
,对于WinForms,你甚至不需要后续调用EndInvoke
:
void DoStuff() {
if (InvokeRequired) {
BeginInvoke(new MethodInvoker(DoStuff));
} else {
// Do things
}
}
如果你需要返回值,那么你必须处理通常的IAsyncResult
接口。
它是如何工作的?
GUI Windows 应用程序基于窗口过程及其消息循环。如果你用普通的C编写一个应用程序,你会得到这样的东西:
MSG message;
while (GetMessage(&message, NULL, 0, 0))
{
TranslateMessage(&message);
DispatchMessage(&message);
}
使用这几行代码,应用程序等待消息,然后将消息传递到窗口过程。窗口过程是一个大的开关/案例语句,您可以在其中检查您知道的消息(WM_
)并以某种方式处理它们(您为WM_PAINT
绘制窗口,退出应用程序以进行WM_QUIT
等等)。
现在想象一下你有一个工作线程,你怎么能调用你的主线程?最简单的方法是使用这个底层结构来解决问题。我过度简化了任务,但这些是步骤:
- 创建一个要调用的函数的(线程安全)队列(SO 上的一些示例)。
- 将自定义消息发布到窗口过程。如果将此队列设置为优先级队列,则甚至可以确定这些调用的优先级(例如,来自工作线程的进度通知的优先级可能低于警报通知)。 在
- 窗口过程中(在 switch/case 语句内),您了解该消息,然后您可以查看要从队列中调用并调用它的函数。
WPF 和 WinForms 都使用此方法将消息从线程传递(调度)到 UI 线程。有关多线程和用户界面的更多详细信息,请查看MSDN上的这篇文章,WinForms隐藏了很多这些细节,您不必照顾它们,但您可以查看以了解它在后台的工作方式。
就我个人而言,当我在一个与 UI 线程一起使用的应用程序时,我通常会写这个小片段:
private void InvokeUI(Action a)
{
this.BeginInvoke(new MethodInvoker(a));
}
当我在不同的线程中进行异步调用时,我始终可以使用以下内容进行回调:
InvokeUI(() => {
Label1.Text = "Super Cool";
});
简单干净。
正如所问的,这是我的答案,它检查跨线程调用,同步变量更新,不停止和启动计时器,也不使用计时器来计算经过的时间。
编辑固定BeginInvoke
调用。我已经使用泛型Action
完成了跨线程调用,这允许传递发送方和事件参数。如果这些未使用(就像这里一样),使用MethodInvoker
更有效,但我怀疑需要将处理转移到无参数方法中。
public partial class AirportParking : Form
{
private Timer myTimer = new Timer(100);
private int elapsedCounter = 0;
private readonly DateTime startTime = DateTime.Now;
private const string EvenText = "hello";
private const string OddText = "hello world";
public AirportParking()
{
lblValue.Text = EvenText;
myTimer.Elapsed += MyTimerElapsed;
myTimer.AutoReset = true;
myTimer.Enabled = true;
myTimer.Start();
}
private void MyTimerElapsed(object sender,EventArgs myEventArgs)
{
If (lblValue.InvokeRequired)
{
var self = new Action<object, EventArgs>(MyTimerElapsed);
this.BeginInvoke(self, new [] {sender, myEventArgs});
return;
}
lock (this)
{
lblElapsedTime.Text = DateTime.Now.SubTract(startTime).ToString();
elapesedCounter++;
if(elapsedCounter % 2 == 0)
{
lblValue.Text = EvenText;
}
else
{
lblValue.Text = OddText;
}
}
}
}
首先,在 Windows 窗体(和大多数框架)中,控件只能由 UI 线程访问(除非记录为"线程安全")。
因此,回调中的this.lblElapsedTime.Text = ...
是完全错误的。看看 Control.BeginInvoke。
其次,您应该使用 System.DateTime 和 System.TimeSpan 进行时间计算。
未经测试:
DateTime startTime = DateTime.Now;
void myTimer_Elapsed(...) {
TimeSpan elapsed = DateTime.Now - startTime;
this.lblElapsedTime.BeginInvoke(delegate() {
this.lblElapsedTime.Text = elapsed.ToString();
});
}
最终使用了以下内容。这是给出的建议的组合:
using System.Timers;
namespace Ariport_Parking
{
public partial class AirportParking : Form
{
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
//instance variables of the form
System.Timers.Timer myTimer;
private const string EvenText = "hello";
private const string OddText = "hello world";
static int tickLength = 100;
static int elapsedCounter;
private int MaxTime = 5000;
private TimeSpan elapsedTime;
private readonly DateTime startTime = DateTime.Now;
//<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
public AirportParking()
{
InitializeComponent();
lblValue.Text = EvenText;
keepingTime();
}
//method for keeping time
public void keepingTime() {
using (System.Timers.Timer myTimer = new System.Timers.Timer(tickLength))
{
myTimer.Elapsed += new ElapsedEventHandler(myTimer_Elapsed);
myTimer.AutoReset = true;
myTimer.Enabled = true;
myTimer.Start();
}
}
private void myTimer_Elapsed(Object myObject,EventArgs myEventArgs){
elapsedCounter++;
elapsedTime = DateTime.Now.Subtract(startTime);
if (elapsedTime.TotalMilliseconds < MaxTime)
{
this.BeginInvoke(new MethodInvoker(delegate
{
this.lblElapsedTime.Text = elapsedTime.ToString();
if (elapsedCounter % 2 == 0)
this.lblValue.Text = EvenText;
else
this.lblValue.Text = OddText;
}));
}
else {myTimer.Stop();}
}
}
}