1.控件介绍
进度条通常用于显示代码的执行进程进度,在一些复杂功能交互体验时告知用户进程还在继续。
在属性栏中,有三个值常用:
Value表示当前值,Minimum表示进度条范围下限,Maximum表示进度条范围上限。
2.简单实例
在一个界面下,点击按钮,进度条加载,用label显示运行耗时。
在Form1.cs中,添加点击Button1按钮功能:
private async void button1_Click(object sender, EventArgs e)
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
button1.Enabled = false;//防止重复点击
progressBar1.Value = 0;
int progressStep = 10;
for (int i = 0; i <= progressBar1.Maximum; i = i + progressStep)
{
await Task.Delay(100);
progressBar1.Value = i;
}
button1.Enabled = true;
stopwatch.Stop();
label1.Text = $"运行耗时:{stopwatch.ElapsedMilliseconds}ms";
MessageBox.Show("ok");
}
3.异步编程思想:
异步编程是一种编程范式,它允许程序在执行耗时操作时,不阻塞主线程或调用线程,从而提高程序的响应性和性能。简单来说,异步编程使得程序在等待某些操作(如网络请求、文件I/O、数据库查询等)的完成时,可以继续处理其他任务。
3.1 为什么需要异步编程?
在同步编程模式下,当程序执行一个耗时操作(例如读取文件或从网络获取数据)时,整个程序会暂停,主线程被堵塞,直到这个操作完成。这会导致程序变得不可响应,特别是在需要处理用户交互的应用程序中。
异步编程通过允许程序处理其他任务而不会被耗时操作阻塞,解决了这一问题。例如,在用户接口应用程序中,异步编程可以防止界面“卡死”,从而提升用户体验。
3.2 异步编程流程:
功能代码如下:模拟一个等待执行
private async Task LoadDataAsync()
{
await Task.Delay(5000); // 异步等待 5 秒
Console.WriteLine("数据加载完成"); // 5 秒后执行
}
(1)当线程执行到await关键字标识的位置后,系统将方法挂起,返回控制权给调用者。
(2)任务调度器记录 Task.Delay(5000),在 5 秒后标记任务为完成。
(3)5秒后,任务调度器标记 Task.Delay(5000) 完成。调度器触发回调,通知方法恢复执行。打印"数据加载完成"。
(4)在挂起期间,用户可以自行操作,不会造成UI阻塞。
3.3 await是什么?
await是C#中的关键字,用于异步编程等待异步操作的完成,不会阻塞当前进程。通常与async关键字一起用。
3.4 async是什么?
async也是C#中的关键字,用于修饰方法、匿名函数或者lambda表达式。通常和await一起用,指示他们包含异步操作。
3.5 Task是什么?
异步方法通常返回 Task 或 Task 对象,表示一个异步操作的进行。
Task 类还可以用于表示和管理异步操作。
以上三个关键字总结:async 修饰的方法通常返回 Task 或 Task < T >,而 await 用于等待任务的完成。
3.6 应用例子:
在图形用户界面(GUI)应用程序中,阻塞主线程会导致用户界面变得不可响应。例如,如果用户点击一个按钮触发一个耗时操作,整个界面会在操作完成之前冻结,无法响应用户的其他操作。
单纯摆出异步编程的例子无法体会精髓,先用一个同步编程的来对比:
同步编程:
点击button1按钮,开始执行功能代码,代码功能放到了另一个方法中去,该方法的功能就是单纯进行线程休眠,模拟耗时操作。
private void button1_Click(object sender, EventArgs e)
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
button1.Enabled = false;//防止重复点击
Form1Bar.Value = 0;
int progressStep = 10;
for (int i = 0; i <= Form1Bar.Maximum; i = i + progressStep)
{
// 同步方法,阻塞UI线程
LoadData();
Form1Bar.Value = i;
}
button1.Enabled = true;
stopwatch.Stop();
label1.Text = $"运行耗时:{stopwatch.ElapsedMilliseconds}ms";
MessageBox.Show("ok");
}
模拟耗时方法:
private void LoadData()
{
System.Threading.Thread.Sleep(1000);// 会强制阻塞线程
}
测试卡死按钮:
private void button2_Click(object sender, EventArgs e)
{
textBox1.Text = "异步编程,UI未卡死";
}
private void button3_Click(object sender, EventArgs e)
{
textBox1.Text = string.Empty;
}
运行过程中无法点击测试按钮,UI进程阻塞,GUI卡死。
异步编程(主窗口进度条):
异步编程会挂起当前await的耗时方法,不会阻塞当前线程,用户可以操作其他。
private async void button1_Click(object sender, EventArgs e)
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
button1.Enabled = false;//防止重复点击
Form1Bar.Value = 0;
int progressStep = 10;
for (int i = 0; i <= Form1Bar.Maximum; i = i + progressStep)
{
// 同步方法,阻塞UI线程
await LoadData();
Form1Bar.Value = i;
}
button1.Enabled = true;
stopwatch.Stop();
label1.Text = $"运行耗时:{stopwatch.ElapsedMilliseconds}ms";
MessageBox.Show("ok");
}
private async Task LoadData()
{
//System.Threading.Thread.Sleep(5000);// 会强制阻塞线程
await Task.Delay(500);
}
异步编程(弹出窗口进度条):
Form1作为主界面,只放Button1和label1两个控件,点击开始后,弹出Form2进度条加载。
不仅实现弹出窗口进度条,还通过异步编程实现。
关键代码:
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;//防止重复点击
Form2 form2 = new Form2();
form2.Show();
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for(int i = 0; i<= form2.Form2Bar.Maximum; i+= 10)
{
await LoadData();
form2.Form2Bar.Value = i;
}
button1.Enabled = true;
stopwatch.Stop();
label1.Text = $"运行耗时:{stopwatch.ElapsedMilliseconds}ms";
MessageBox.Show("ok");
form2.Close();
}
private async Task LoadData()
{
await Task.Delay(500);
}
4.更进一步
仔细阅读上述代码可以发现,每次进度条加载是通过i来控制的,i每次随着休眠结束会自增10,这样确实可以均匀控制进度条增长。
但是现在有这样一个问题,在实际项目中,我们随着处理的数据量不同,并不知道每次运行的固定时间,换言之,可能并不是每次均匀增长一个固定值。
在之前先介绍一下一种特殊的函数-----回调函数
4.1 回调函数
回调函数,是指函数通过参数传给另一个函数,在满足特定的条件下由后者调用。
在异步编程思想中,当某个操作完成后,回调函数会被执行,处理或相应发生的事件。
下面是一个简单的代码展示:
// 定义一个回调函数
void CallbackFunction(string message)
{
Console.WriteLine(message);
}
// 定义一个异步函数,接受回调函数作为参数
void doSomethingAsync(Action<string> callback)
{
// 模拟异步操作
Task.Run(() =>
{
// 模拟一些工作
Task.Delay(1000).Wait();
// 调用回调函数
callback("Operation completed!");
});
}
// 使用异步函数并传递回调函数
doSomethingAsync(CallbackFunction);
CallbackFunction(string message)是一个回调函数,当有字符串类型的参数传入时,会进行打印操作。
doSomethingAsync(Action callback)是一个异步函数(内部含有Task.Run),内部模拟了一个耗时异步操作,在结束后调用回调函数。
4.2 控制进度条
在不同任务耗时不一样的前提下,控制进度条的增长可以通过下面两种方法:
(1)将任务分解成为多个子任务,每个任务结束后手动增加,更新进度条。(看起来一卡一卡的)
(2)通过IProgress < T > 接口实现任务的进度报告,实时更新进度条。(进度均匀,更优雅)
两种方法其实很类似,都是需要去做一个标记,然后更新。
如果实在无法分割子任务,可使用进度条Marquee样式,实现类似跑马灯的效果,只告诉用户程序在运行,不知道结束的时间。
关键代码实例,只保留核心部分:
progress是一个报告器,接受参数,触发内部的Lambda回调函数,更新进度条。
private async void Button_Click(object sender, EventArgs e)
{
// 创建一个进度报告器,更新进度条
var progress = new Progress<int>(percent =>
{
progressBar.Value = percent; // 更新进度条
});
// 启动长时间运行的任务
await ExecuteLongRunningTask(progress);
// ...
}
private async Task ExecuteLongRunningTask(IProgress<int> progress)
{
int totalSteps = 100; // 任务的总步数(假设任务可以分为100步)
for (int i = 0; i < totalSteps; i++)
{
// 模拟长时间任务
await Task.Delay(100); // 每步等待100毫秒
// 报告进度
progress.Report((i + 1) * 100 / totalSteps);
}
}