1、前言
WinForm
中的UI
假死其实是个老生常谈的问题了,但最近还是很多同志问该如何解决,今天就来说明一下。测试程序界面如下图所示:一个Button
和一个ProgressBar
。
2、主线程的阻塞和UI更新问题
先看一个简单的例子:
using System;
using System.Threading;
using System.Windows.Forms;
namespace App
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private void btn_Click(object sender, EventArgs e)
{
progressBar1.Minimum = 0;
progressBar1.Maximum = 100;
progressBar1.Style = ProgressBarStyle.Blocks;
progressBar1.Step = 1;
progressBar1.Value = 0;
int max = progressBar1.Maximum;
for (int i = 1; i <= max; i++)
{
progressBar1.Value = i;
Thread.Sleep(100);
}
}
}
}
运行上面的代码,你会发现在程序运行期间界面无法拖动,UI
处于假死状态。原因其实很简单:循环中的Thread.Sleep(100)
阻塞了线程。既然主线程被阻塞了,那我们自然会想到创建一个子线程去更新控件,现在修改一下代码:
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace App
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private void btn_Click(object sender, EventArgs e)
{
progressBar1.Minimum = 0;
progressBar1.Maximum = 100;
progressBar1.Style = ProgressBarStyle.Blocks;
progressBar1.Step = 1;
progressBar1.Value = 0;
Task task = Task.Run(() =>
{
int max = progressBar1.Maximum;
for (int i = 1; i <= max; i++)
{
progressBar1.Value = i;
Thread.Sleep(100);
}
});
}
}
}
运行上面的代码,发现报错:线程间操作无效: 从不是创建控件“progressBar1”的线程访问它
。
3、使用Delagate解决界面卡死问题
首先明确一点:UI
线程位于主线程,如果想要在子线程里更新UI
状态,则必须要将其切换到主线程,然后进行更新操作。UI
控件一般会提供Invoke、InvokeRequired
,其中InvokeRequired
用于判断是否有子线程在更新UI
控件,如果有则返回true
,Invoke
用于将控制权切换到UI
线程,代码如下:
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace App
{
public delegate void UpdateProgressBarDelegate(int num);
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private void btn_Click(object sender, EventArgs e)
{
progressBar1.Minimum = 0;
progressBar1.Maximum = 100;
progressBar1.Style = ProgressBarStyle.Blocks;
progressBar1.Step = 1;
progressBar1.Value = 0;
Task task = Task.Run(() =>
{
UpdateProgressBar();
});
}
private void UpdateProgressBar()
{
int max = progressBar1.Maximum;
for (int i = 1; i <= max; i++)
{
UpdateProgressBarCallback(i);
Thread.Sleep(100);
}
}
private void UpdateProgressBarCallback(int num)
{
if (progressBar1.InvokeRequired)
{
progressBar1.Invoke(new UpdateProgressBarDelegate(UpdateProgressBarCallback), new object[] { num });
}
else
{
progressBar1.Value = num;
}
}
}
}
4、使用BackgroundWorker解决界面卡死问题
WinForm
中的BackgroundWorker
组件也可以很好地解决界面卡死问题,如下图所示:
BackgroundWorker
组件的核心就是DoWork
、ProgressChanged
、ProgressChanged
三个事件,代码如下:
using System;
using System.Threading;
using System.Windows.Forms;
namespace App
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
backgroundWorker1.WorkerReportsProgress = true;
backgroundWorker1.WorkerSupportsCancellation = true;
}
private void btn_Click(object sender, EventArgs e)
{
if (backgroundWorker1.IsBusy)
{
return;
}
progressBar1.Minimum = 0;
progressBar1.Maximum = 100;
progressBar1.Style = ProgressBarStyle.Blocks;
progressBar1.Step = 1;
progressBar1.Value = 0;
backgroundWorker1.RunWorkerAsync();
}
private void backgroundWorker1_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
int max = progressBar1.Maximum;
for (int i = 1; i <= max; i++)
{
backgroundWorker1.ReportProgress(i);
Thread.Sleep(100);
}
}
private void backgroundWorker1_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
}
private void backgroundWorker1_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
MessageBox.Show("完成");
}
}
}
5、使用async、await解决界面卡死问题
上面提到的Delegate
和BackgroundWorker
组件主要是针对UI
的更新问题,其实还有一种情况也会造成UI
的卡死,那就是某些耗时操作。先来看一段代码:
using System;
using System.Threading;
using System.Windows.Forms;
namespace App
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private void btn_Click(object sender, EventArgs e)
{
string message = GetMessage();
MessageBox.Show(message);
}
private string GetMessage()
{
Thread.Sleep(5000);
return "Hello World";
}
}
}
在上面的代码中,GetMessage
方法模拟了一个耗时操作,运行程序,发现UI
假死,界面无法拖动,此时就可以考虑使用async
和await
来解决。代码如下:
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace App
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private async void btn_Click(object sender, EventArgs e)
{
string message = await GetMessageAsync();
MessageBox.Show(message);
}
private async Task<string> GetMessageAsync()
{
return await Task.Run(() =>
{
Thread.Sleep(5000);
return "Hello World";
});
}
}
}
6、使用Task解决界面卡死问题
对于一些老项目,比如.NET 4.0
版本下的代码,开发者无法使用async
、await
,此时就可以考虑使用Task
来实现。代码如下:
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace App
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private void btn_Click(object sender, EventArgs e)
{
string message = "";
Task task = Task.Factory.StartNew(() =>
{
message = GetMessage();
})
.ContinueWith(t =>
{
MessageBox.Show(message);
});
}
private string GetMessage()
{
Thread.Sleep(5000);
return "Hello World";
}
}
}
7、结语
本文主要介绍了WinForm
中的UI
假死解决方案。在实际开发中,合理使用多线程可以有效提高界面的人机交互体验。