C#如何对button按钮实现进度条功能
在编写WinForm程序时,我们有很多时候需要用到进度条,下面我来分享一下我在处理进度条时所采用的各种方法。
创建一个Winform窗体应用项目。添加一个新的窗体(progressForm.cs)用来承载进度条;在progressForm窗体添加一个进度条控件。
一.单线程进度条
向progressForm.cs添加一个方法Addprogess用来推动进度条。
public void Addprogess()
{
progressBar1.Value++;
}
接下来便是在主窗体添加启动的方法。先添加一个Boutton控件,用来启动。
在按钮单击事件里添加以下代码。
private void button1_Click(object sender, EventArgs e)
{
progressForm progress = new progressForm();
progress.Show();
for (int i = 0; i < 100; i++)
{
progress.Addprogess();
Thread.Sleep(50);
}
progress.Close();
}
上文中用到一个for循环来模拟实际项目中需要循环插入数据或执行的方法,Thread.Seep(50)将线程挂起50毫秒模拟实际项目中每次操作需要的时间。
这样一个进度条便完成了。在Winform里添加进度条是不是非常简单呢?通过不超过10行的代码,就能完成一个进度条。
在完成这个进度条后,虽然看起来是能正常运作,但是我们会发现一个问题:在进度条执行时,我们无法对主窗体进行操作,这也是很麻烦的一个问题。当然,在初学者阶段很多人都不会去考虑这个问题。但是在我们工作后,完成一个程序的时需要考虑到的是程序的性能,易用性。这时我们就需要做一下改进。
二.多线程进度条
先简单普及一下多线程:
线程(Thread): 是进程中的基本执行单元,是操作系统分配CPU时间的基本单位,一个进程可以包含若干个线程,在进程入口执行的第一个线程被视为这个进程的主线程。在.NET应用程序中,都是以Main()方法作为入口的,当调用此方法时系统就会自动创建一个主线程。线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。
多线程的优点: 可以同时完成多个任务;可以使程序的响应速度更快;可以让占用大量处理时间的任务或当前没有进行处理的任务定期将处理时间让给别的任务;可以随时停止任务;可以设置每个任务的优先级以优化程序性能。
那么可能有人会问:为什么可以多线程执行呢? 总结起来有下面两方面的原因:
1、CPU运行速度太快,硬件处理速度跟不上,所以操作系统进行分时间片管理。这样,从宏观角度来说是多线程并发的,因为CPU速度太快,察觉不到,看起来是同一时刻执行了不同的操作。但是从微观角度来讲,同一时刻只能有一个线程在处理。
2、目前电脑都是多核多CPU的,一个CPU在同一时刻只能运行一个线程,但是多个CPU在同一时刻就可以运行多个线程。
然而,多线程虽然有很多优点,但是也必须认识到多线程可能存在影响系统性能的不利方面,才能正确使用线程。不利方面主要有如下几点:
(1)线程也是程序,所以线程需要占用内存,线程越多,占用内存也越多。
(2)多线程需要协调和管理,所以需要占用CPU时间以便跟踪线程。
(3)线程之间对共享资源的访问会相互影响,必须解决争用共享资源的问题。
(4)线程太多会导致控制太复杂,最终可能造成很多程序缺陷。
当启动一个可执行程序时,将创建一个主线程。在默认的情况下,C#程序具有一个线程,此线程执行程序中以Main方法开始和结束的代码,Main()方法直接或间接执行的每一个命令都有默认线程(主线程)执行,当Main()方法返回时此线程也将终止。
一个进程可以创建一个或多个线程以执行与该进程关联的部分程序代码。在C#中,线程是使用Thread类处理的,该类在System.Threading命名空间中。使用Thread类创建线程时,只需要提供线程入口,线程入口告诉程序让这个线程做什么。通过实例化一个Thread类的对象就可以创建一个线程。创建新的Thread对象时,将创建新的托管线程。Thread类接收一个ThreadStart委托或ParameterizedThreadStart委托的构造函数,该委托包装了调用Start方法时由新线程调用的方法,示例代码如下:
Thread thread=new Thread(new ThreadStart(method)); //创建线程
thread.Start(); //启动线程
上面代码实例化了一个Thread对象,并指明将要调用的方法method(),然后启动线程。ThreadStart委托中作为参数的方法不需要参数,并且没有返回值。ParameterizedThreadStart委托一个对象作为参数,利用这个参数可以很方便地向线程传递参数,示例代码如下:
Thread thread=new Thread(new ParameterizedThreadStart(method));//创建线程
thread.Start(3); //启动线程
创建多线程的步骤:
1、编写线程所要执行的方法
2、实例化Thread类,并传入一个指向线程所要执行方法的委托。(这时线程已经产生,但还没有运行)
3、调用Thread实例的Start方法,标记该线程可以被CPU执行了,但具体执行时间由CPU决定
以上对多线程的解释都引用于:(C#多线程)
看完上面的解释对多线程多少也应该大致了解一点了,接下来开始修改Boutton点击事件里的代码。
private void button1_Click(object sender, EventArgs e)
{
Thread progressthread = new Thread(new ParameterizedThreadStart(thread));
progressthread.Start();
}
public void thread(object length)
{
progressForm progress = new progressForm();
progress.Show();
for (int i = 0; i < 100; i++)
{
progress.Addprogess();
Thread.Sleep(50);
}
progress.Close();
}
从代码中可以看到,这儿我们只是多添加了一个thread()方法。在这个方法里放了原先放在button_Click里的那部分代码而已。
thread这个方法用来开辟新线程,将进度条放到了新线程里。修改成这样后,运行发现在进度条加载时,还能同时对主窗体进行操作,避免了程序进入“假死”的状态。这对于用户来讲是极大的提高了他们对程序的操作感受。
来到这里似乎没什么问题了。在应用层上来讲确实没什么问题了,但是对于程序本身来讲,我总觉得有那么一点问题。个人觉得,主线程处理的业务不应放到进度条所在的线程。进度条所在的线程只需负责进度条就好了。主业务我们可以放到主线程或再开辟一条新的线程来处理。
三.使用委托和invoke方法跨线程UIl控制来实现进度条
这是进一步完善进度条。上面的进度条虽然用上了线程,但是当我们运行时就会发现鼠标到进度条窗口,总是变成加载状态。这个我也不太清楚,估计和线程的阻塞有关。不管怎样这个情况似乎严重不符合我们对程序的要求,毕竟看起来像bug。
我们改一下上面boutton控件的名称。
Boutton:【Name:StartButton 】
添加一个lable控件(显示文件处理的数量):
Name:DataNumber
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp8
{
public partial class Form1 : Form
{
public delegate void addProgress();//创建委托
progressForm progress;//声明一个progressForm窗口变量
public int n;//声明一个变量显示文件处理数量
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
n = 0;//为变量n赋值,每次点击按钮都能初始化n的值(不初始化第二次开始时DataNumber.Text的值会从上一次结束时的值开始累加)
progress = new progressForm();//实例化progressForm
progress.Show();//显示progressForm窗口实例
Thread progressthread = new Thread(new ParameterizedThreadStart(thread));//创建线程
progressthread.Start();//启动线程
}
public void thread(object length)
{
int temp = 1;//中间变量,实际数值等于进度条的值
for (int i = 0; i < 450; i++)//模拟使用环境处理450条信息
{
if (i / 4.5 >temp)//进度条value最大值为100,要控制每处理4.5个数据调用一次方法使value自增1
{
this.Invoke(new addProgress( progress.Addprogess));//invoke方法: 同步执行指定委托
temp++;
}
this.Invoke(new addProgress(DataNumberAdd));//同步执行委托
Thread.Sleep(50);//使线程挂起50毫秒,减缓进度条的变化
}
this.Invoke(new addProgress(progress.Close));//同步执行委托 关闭进度条窗口
}
/// <summary>
/// DataNumber自增方法
/// </summary>
public void DataNumberAdd()
{
n++;
DataNumber.Text = n.ToString();//改变DataNumber的值
}
}
}
四.使用异步委托执行线程来完成进度条
异步委托不做多解释,因为这个解释起来并不是太容易,如果有心更进一步学习.Net,那么可以再网上搜索相对应的资料。
其实异步委托不适合做进度条,毕竟进度条是用来可视化反映数据加载或数据处理进度的,这一个同步反映的过程,异步委托完全不搭调。这里只做了解,如果真的实用的话这个不做考虑。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
namespace TheardProgress
{
public partial class Form1 : Form
{
public List<int> Sum = new List<int>();
public delegate int addProgress(int i);
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
Thread progressthread = new Thread(new ParameterizedThreadStart(thread));
progressthread.Start();
label1.Text = Sum.Count().ToString();
}
public void thread(object length)
{
bool a = true;
progressForm progress = new progressForm();
progress.Show();
addProgress pro = add;
for (int i = 0; i < 100; i++)
{
IAsyncResult asyncResult = pro.BeginInvoke(i, null, null);
while (!asyncResult.IsCompleted)
{
Thread.Sleep(100);
}
//progress.Addprogess();
progress.labletxt(pro.EndInvoke(asyncResult));
}
}
public int add(int i)
{
Sum.Add(i);
return Sum.Count();
}
}
}
通过IAsyncResult接口的IsComplete属性来检查委托是否完成了任务。EndInvote方法获取执行结果。Sum模拟主业务的执行进度。这样在进度条线程里创建一个IAsyncResul实例就可通过异步调用addProgress委托的实例pro,pro实现主业务,使用while(){}监控每一组业务是否完成,for循环表示所有业务循环完成。完成每一组业务,都会执行progress.labletxt()方法,更新进度条的进度。
在progressForm 添加labletxt()方法,
public void labletxt( int value)
{
progressBar1.Value = value;
}
五.使用Async,await完成进度条
首先表述一下我对这两个关键字理解:
Async表示异步,只要方法加上该关键字,那么每次执行该方法时,都会在一个新的子线程上运行该方法。
await表示等待,表示在该线程内,等待该关键字后面方法运行结束后才继续执行后面的逻辑。
主窗体:
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp21
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
int Number = 0;//Number 表示完成的任务量,这个Number会直接传到Form2(进度条承载窗口)作为进度条的Value
public async void WorkAsync()
{
//async 关键字表示同步方法
Form2 progress = new Form2();
progress.Show();
for (int i = 0; i < 100; i++)
{
//等待每部分任务完成后,返回的结果
int num = await Task.Run(() =>
{
//任务逻辑
Thread.Sleep(1000);
return Number++;//完成任务后,Number自加
});
progress.SetProgerssBar(num);//设置进度条value
}
Number = 0;
}
private void button1_Click(object sender, EventArgs e)
{
WorkAsync();
}
}
}
进度条窗体:
using System.Windows.Forms;
namespace WindowsFormsApp21
{
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
}
public void SetProgerssBar(int Number)
{
progressBar1.Value = Number;
}
}
}
这样一个进度条就算完成了,但是即使如此,这个进度条可能还存在些许缺陷等待完善。 其实小小的进度条,如果要做得好,依然需要不少的硬知识。学无止境。