今天来给大家讲解一下如何解决C#的Winform/WPF应用卡界面的问题。
1.问题复现
我们在用C#编写桌面软件的时候经常会用到实时更新界面信息的功能,比如这样
private void button1_Click(object sender, EventArgs e)
{
for (int i = 0; i < 200; i++)
{
Thread.Sleep(10);
label1.Text = $"{DateTime.Now.ToString("yyyy-MM-dd:mm:HH:ss.fff")} i的值为 {i}";
}
}
此时,界面表现是这样的:
可以看到,代码一旦开始执行,不仅数据不会更新,界面还会拖拽不动,这是因为在循环期间,UI线程被阻塞,无法更新用户界面,并且程序将在等待循环完成时变得不可响应。
2.解决方案
按照平常的想法,既然是UI线程被阻塞,那么利用C#异步编程技术,使用另一个线程去操作UI不就解决了吗?说干就干,接下来修改代码:
private async void button1_Click(object sender, EventArgs e)
{
await Task.Run(() =>
{
for (int i = 0; i < 200; i++)
{
Thread.Sleep(10);
label1.Text = $"{DateTime.Now.ToString("yyyy-MM-dd:mm:HH:ss.fff")} i的值为 {i}";
}
});
}
再次尝试运行看看,不好,抛出异常了:
提示不是从创建控件的线程访问控件,这是为什么呢?原来我们要赋值的控件label1是UI线程创建的,而我们用Task.Run()相当于新建了一个线程,用新线程去调用UI线程创建的控件,就会提示这个错误,那么现在该怎么办呢?别慌,我们可以用委托的方法,再次修改代码点击运行:
private void button1_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
for (int i = 0; i < 200; i++)
{
Thread.Sleep(10);
this.BeginInvoke(new Action(() => label1.Text = $"{DateTime.Now.ToString("yyyy-MM-dd:mm:HH:ss.fff")} i的值为 {i}"));
}
});
}
我们可以看到,不仅数据在实时更新,且随意拖动界面都不会影响数据更新。
3.原理说明
接下来,我们详细阐述一下原理:
BeginInvoke 方法是一个异步方法,它允许通过委托在控件的 UI 线程上异步执行代码,new Action() 是一个简单的匿名方法,将会在UI线程上执行,当UI线程空闲时,该方法将会被异步调用。
使用 BeginInvoke 的好处是,即使在执行长时间运行的操作时,也不会阻塞 UI 线程。这意味着用户仍然可以与应用程序进行交互,并且应用程序的响应性不会受到影响。
当调用 BeginInvoke 时,系统会将任务添加到 UI 线程的消息队列中。一旦消息队列中没有正在执行的任务,UI 线程就会从消息队列中获取下一个任务并执行它。这样,UI 线程就可以在需要执行的任务之间快速切换,从而保持应用程序的响应性和流畅性。
好了,今天的分享就到这里,祝大家生活愉快。