目录
1. 使用 Control.Invoke 或 Control.BeginInvoke(Windows Forms)
2. 使用 Dispatcher.Invoke 或 Dispatcher.BeginInvoke(WPF)
桌面应用程序(如 Windows Forms 或 WPF)中,UI 操作必须由主线程(也称 UI 线程)执行。如果尝试从非 UI 线程直接更新 UI 元素,通常会引发异常或导致不可预测的行为。
Thread 类本身无法直接更新 UI,但可以通过以下方法将操作委托给 UI 线程来实现安全的 UI 更新。
1. 使用 Control.Invoke 或 Control.BeginInvoke(Windows Forms)
在 Windows Forms 应用程序中,可以使用 Control.Invoke 或 Control.BeginInvoke 方法将代码调度到 UI 线程。
示例代码:
using System;
using System.Reflection.Emit;
using System.Threading;
using System.Windows.Forms;
using static System.Net.Mime.MediaTypeNames;
class Program : Form
{
private Button button;
private Label label;
public Program()
{
button = new Button { Text = "Start Thread", Dock = DockStyle.Top };
label = new Label { Text = "Waiting...", Dock = DockStyle.Fill };
button.Click += Button_Click;
Controls.Add(label);
Controls.Add(button);
}
private void Button_Click(object sender, EventArgs e)
{
Thread thread = new Thread(UpdateLabel);
thread.Start();
}
private void UpdateLabel()
{
for (int i = 0; i < 10; i++)
{
// 检查是否需要调用 Invoke
if (label.InvokeRequired)
{
// 使用 Invoke 将操作调度到 UI 线程
label.Invoke(new Action(() => label.Text = $"Count: {i}"));
}
else
{
// 如果当前线程是 UI 线程,则直接更新
label.Text = $"Count: {i}";
}
Thread.Sleep(500); // 模拟工作
}
}
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.Run(new Program());
}
}
解释:
- InvokeRequired:检查当前线程是否是创建控件的线程(即 UI 线程)。如果不是,则需要通过 Invoke 或 BeginInvoke 调度到 UI 线程。
- Invoke:同步执行指定的操作,等待操作完成后再继续。
- BeginInvoke:异步执行指定的操作,不阻塞当前线程。
输出效果:
点击按钮后,label 的文本会每 500 毫秒更新一次,显示当前计数值。
2. 使用 Dispatcher.Invoke 或 Dispatcher.BeginInvoke(WPF)
在 WPF 应用程序中,可以使用 Dispatcher 对象将操作调度到 UI 线程。
示例代码:
using System;
using System.Reflection.Metadata;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using static System.Net.Mime.MediaTypeNames;
class MainWindow : Window
{
private Button button;
private TextBlock textBlock;
public MainWindow()
{
button = new Button { Content = "Start Thread" };
textBlock = new TextBlock { Text = "Waiting..." };
button.Click += Button_Click;
var stackPanel = new StackPanel();
stackPanel.Children.Add(button);
stackPanel.Children.Add(textBlock);
Content = stackPanel;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(UpdateTextBlock);
thread.Start();
}
private void UpdateTextBlock()
{
for (int i = 0; i < 10; i++)
{
// 使用 Dispatcher 将操作调度到 UI 线程
textBlock.Dispatcher.Invoke(() => textBlock.Text = $"Count: {i}");
Thread.Sleep(500); // 模拟工作
}
}
}
class Program
{
[STAThread]
static void Main()
{
var app = new Application();
app.Run(new MainWindow());
}
}
解释:
- Dispatcher.Invoke:同步执行指定的操作,确保操作在 UI 线程上运行。
- Dispatcher.BeginInvoke:异步执行指定的操作,不阻塞当前线程。
输出效果:
点击按钮后,TextBlock 的文本会每 500 毫秒更新一次,显示当前计数值。
3. 使用 SynchronizationContext
SynchronizationContext 是一种更通用的方式,适用于 Windows Forms 和 WPF,甚至其他框架(如 ASP.NET)。它允许你捕获当前线程的上下文,并在需要时将其用于调度操作。
示例代码:
using System;
using System.Reflection.Emit;
using System.Threading;
using System.Windows.Forms;
using static System.Net.Mime.MediaTypeNames;
class Program : Form
{
private Button button;
private Label label;
private SynchronizationContext _uiContext;
public Program()
{
button = new Button { Text = "Start Thread", Dock = DockStyle.Top };
label = new Label { Text = "Waiting...", Dock = DockStyle.Fill };
button.Click += Button_Click;
Controls.Add(label);
Controls.Add(button);
// 捕获 UI 线程的上下文
_uiContext = SynchronizationContext.Current;
}
private void Button_Click(object sender, EventArgs e)
{
Thread thread = new Thread(UpdateLabel);
thread.Start();
}
private void UpdateLabel()
{
for (int i = 0; i < 10; i++)
{
// 使用 SynchronizationContext 将操作调度到 UI 线程
_uiContext.Post(_ => label.Text = $"Count: {i}", null);
Thread.Sleep(500); // 模拟工作
}
}
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.Run(new Program());
}
}
解释:
- SynchronizationContext.Current:捕获当前线程的上下文(通常是 UI 线程的上下文)。
- Post:异步执行指定的操作。
- Send:同步执行指定的操作。
输出效果:
点击按钮后,label 的文本会每 500 毫秒更新一次,显示当前计数值。