这也是一个网友提出这个问题,细想来还是可以优化一下,算是再熟悉明确一下这个吧。在 WinForms 开发中,跨线程更新 UI 是一个常见的场景。通常我们会使用 Control.Invoke
或 Control.BeginInvoke
来确保 UI 更新在正确的线程上执行。但是,如果使用不当,这些调用可能会带来性能问题。让我们深入探讨这个话题。
问题描述
让我们先看一个典型的场景 - 进度条更新:
public partial class Form1 : Form
{
private void btnStart_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
for (int i = 0; i <= 100; i++)
{
Thread.Sleep(50); // 模拟耗时操作
UpdateProgressBar(i);
}
});
}
private void UpdateProgressBar(int value)
{
if (progressBar1.InvokeRequired)
{
progressBar1.Invoke(new Action<int>(UpdateProgressBar), value);
}
else
{
progressBar1.Value = value;
}
}
}
这段代码存在以下问题:
每次调用都创建新的 Action<int>
委托对象
频繁的跨线程调用可能导致UI响应迟钝
同步调用 Invoke
会阻塞工作线程
优化方案
1. 缓存委托对象
第一个简单的优化是缓存委托对象:
public partial class Form1 : Form
{
private readonly Action<int> _updateProgressBarAction;
public Form1()
{
InitializeComponent();
_updateProgressBarAction = new Action<int>(UpdateProgressBar);
}
private void btnStart_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
for (int i = 0; i <= 100; i++)
{
Thread.Sleep(50);
UpdateProgressBar(i);
}
});
}
private void UpdateProgressBar(int value)
{
if (progressBar1.InvokeRequired)
{
progressBar1.Invoke(_updateProgressBarAction, value);
}
else
{
progressBar1.Value = value;
}
}
}
2. 使用 Progress<T>
更现代的方式是使用 Progress<T>
类:
public partial class Form1 : Form
{
private readonly IProgress<int> _progress;
public Form1()
{
InitializeComponent();
_progress = new Progress<int>(value => progressBar1.Value = value);
}
private async void btnStart_Click(object sender, EventArgs e)
{
await Task.Run(() =>
{
for (int i = 0; i <= 100; i++)
{
Thread.Sleep(50);
_progress.Report(i);
}
});
}
}
3. 批量更新策略
如果更新频率过高,可以采用批量更新策略:
public partial class Form1 : Form
{
private const int UpdateThreshold = 5; // 每5%更新一次
private async void btnStart_Click(object sender, EventArgs e)
{
var progress = new Progress<int>(value => progressBar1.Value = value);
await Task.Run(() =>
{
for (int i = 0; i <= 100; i++)
{
Thread.Sleep(50);
if (i % UpdateThreshold == 0)
{
((IProgress<int>)progress).Report(i);
}
}
});
}
}
4. 使用 BeginInvoke 异步调用
如果不需要等待UI更新完成,可以使用 BeginInvoke
:
public partial class Form1 : Form
{
private readonly Action<int> _updateProgressBarAction;
public Form1()
{
InitializeComponent();
_updateProgressBarAction = new Action<int>(UpdateProgressBarAsync);
}
private void btnStart_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
for (int i = 0; i <= 100; i++)
{
Thread.Sleep(50);
UpdateProgressBarAsync(i);
}
});
}
private void UpdateProgressBarAsync(int value)
{
if (progressBar1.InvokeRequired)
{
progressBar1.BeginInvoke(_updateProgressBarAction, value);
}
else
{
progressBar1.Value = value;
}
}
}
5. 综合示例:带取消和异常处理的进度更新
下面是一个更完整的示例,包含了错误处理、取消操作和进度更新:
// 进度信息类
public class ProgressInfo
{
public int Percentage { get; set; }
public string Message { get; set; }
}
public partial class Form1 : Form
{
private CancellationTokenSource _cts;
private readonly IProgress<ProgressInfo> _progress;
private bool _isRunning;
public Form1()
{
InitializeComponent();
// 初始化进度报告器
_progress = new Progress<ProgressInfo>(OnProgressChanged);
InitializeControls();
}
private void InitializeControls()
{
// 初始状态设置
btnCancel.Enabled = false;
progressBar1.Minimum = 0;
progressBar1.Maximum = 100;
progressBar1.Value = 0;
}
private void OnProgressChanged(ProgressInfo info)
{
progressBar1.Value = info.Percentage;
lblStatus.Text = info.Message;
}
private async void btnStart_Click(object sender, EventArgs e)
{
if (_isRunning)
return;
try
{
_isRunning = true;
UpdateUIState(true);
// 创建新的取消令牌源
_cts = new CancellationTokenSource();
// 执行长时间运行的任务
await ProcessLongRunningTaskAsync(_cts.Token);
MessageBox.Show("处理完成!", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (OperationCanceledException)
{
MessageBox.Show("操作已被用户取消", "已取消", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show($"处理过程中发生错误:{ex.Message}", "错误",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
_isRunning = false;
UpdateUIState(false);
_cts?.Dispose();
_cts = null;
}
}
private void UpdateUIState(bool isProcessing)
{
btnStart.Enabled = !isProcessing;
btnCancel.Enabled = isProcessing;
}
private async Task ProcessLongRunningTaskAsync(CancellationToken token)
{
// 模拟一个需要处理100个项目的长时间运行任务
const int totalItems = 100;
await Task.Run(async () =>
{
try
{
for (int i = 0; i <= totalItems; i++)
{
// 检查是否请求取消
token.ThrowIfCancellationRequested();
// 模拟处理工作
await Task.Delay(50, token);
// 每处理一个项目报告进度
if (i % 5 == 0)
{
_progress.Report(new ProgressInfo
{
Percentage = i,
Message = $"正在处理... {i}%"
});
}
}
// 报告完成
_progress.Report(new ProgressInfo
{
Percentage = 100,
Message = "处理完成"
});
}
catch (Exception)
{
// 确保在发生异常时更新UI显示
_progress.Report(new ProgressInfo
{
Percentage = 0,
Message = "操作已取消"
});
throw; // 重新抛出异常,让外层处理
}
}, token);
}
private void btnCancel_Click(object sender, EventArgs e)
{
if (_cts?.IsCancellationRequested == false)
{
// 显示确认对话框
if (MessageBox.Show("确定要取消当前操作吗?", "确认取消",
MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
{
_cts?.Cancel();
lblStatus.Text = "正在取消...";
btnCancel.Enabled = false;
}
}
}
// 防止内存泄漏
protected override void OnFormClosing(FormClosingEventArgs e)
{
if (_isRunning)
{
e.Cancel = true;
MessageBox.Show("请等待当前操作完成或取消后再关闭窗口", "提示",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
_cts?.Dispose();
base.OnFormClosing(e);
}
}
总结
在 WinForms 应用程序中,正确处理跨线程UI更新是很重要的。通过采用适当的模式和实践,我们可以:
减少不必要的对象创建
提高应用程序的响应性
使代码更加清晰和易维护
避免潜在的内存问题
提供更好的用户体验
选择哪种方式取决于具体的应用场景,但总的来说,使用 API(如 Progress<T>
和 async/await)通常是更好的选择。对于需要精细控制的场景,可以考虑使用缓存的委托对象和自定义的更新策略。
该文章在 2024/11/7 10:30:53 编辑过