【问题】
之前写了个C#的小程序:
实现ST中的歌曲下载,但是现在下载歌曲过程中,没有下载进度的显示,所以用户体验很不好。
现在想要添加进度条。
【解决过程】
1.其实,关于C#中添加进度条的事情,之前就折腾过:
【未解决】C#中添加始终滚动的进度条(跑马灯)和一格一格前进的滚动条(块)
只是没有解决。
现在打算继续去折腾折腾,看看是否能搞定。
2.看了半天,还是去参考微软官网的:
去尝试添加代码,看看能否有效果。
最后试了半天,添加了如下代码:
using System.Windows.Forms; private Timer downloadTimer; private void IncreaseDownloadProgressBar(object sender, EventArgs e) { // Increment the value of the ProgressBar a value of one each time. int currentPecent = getDownloadPercent(); //pgbDownload.Increment(1); pgbDownload.Increment(currentPecent); // Determine if we have completed by comparing the value of the Value property to the Maximum value. if (pgbDownload.Value == pgbDownload.Maximum) // Stop the timer. downloadTimer.Stop(); } // Call this method from the constructor of the form. private void InitializeDownloadTimer() { downloadTimer = new Timer(); //downloadTimer.Enabled = true; // Set the interval for the timer. downloadTimer.Interval = 100; // Connect the Tick event of the timer to its event handler. downloadTimer.Tick += new EventHandler(IncreaseDownloadProgressBar); // Start the timer. downloadTimer.Start(); } public void downloadStMusicFromUrl() { ... InitializeDownloadTimer(); //download it if (downloadSingleStMusic(singleStUrl, txbSaveTo.Text)) { showCompleteHint(); } }
结果运行的效果只是,下载过程中,还是死掉了,只有下载完毕,对应的timer的函数,才会被调用,所以还是没用。
看来还是需要另外建立一个线程,实现更新进度的效果才行。
3.还是去参考:
然后去折腾了半天,如下的代码:
private delegate void ProgressBarShow(int i); private void ShowPro(int currentProgress) { if (this.InvokeRequired) { this.Invoke(new ProgressBarShow(ShowPro), currentProgress); } else { this.pgbDownload.Value = currentProgress; } } private void updateProgress() { int currentPecent = 0; while (currentPecent <= 100) { //this.ShowPro(currentPecent); //currentPecent++; //模拟发送多少 //Thread.Sleep(10); currentPecent = getDownloadPercent(); ShowPro(currentPecent); Thread.Sleep(250); } Thread.CurrentThread.Abort(); } private void initForDownloadProcess() { Thread thread = new Thread(new ThreadStart(updateProgress)); //模拟进度条 thread.IsBackground = true; thread.Start(); } public void downloadStMusicFromUrl() { string singleStUrl = ""; //get st url singleStUrl = txbStUrl.Text; //InitializeDownloadTimer(); initForDownloadProcess(); //download it if (downloadSingleStMusic(singleStUrl, txbSaveTo.Text)) { showCompleteHint(); } }
实现出来的效果,也是最后一次性更新100%,即界面卡死,最后一次性全部更新完毕,而无法实现动态绘制更新进度条。
4.后来是看到
中有个
System.Windows.Forms.Application.DoEvents();
所以无意间去虽然找了找相关介绍,找到这个:
然后随便尝试把
System.Windows.Forms.Application.DoEvents();
加到我的此处底层最耗时下载数据的部分:
public int getUrlRespStreamBytes(ref Byte[] respBytesBuf, string url, Dictionary<string, string> headerDict, Dictionary<string, string> postDict, int timeout) { int curReadoutLen; int curBufPos = 0; int realReadoutLen = 0; try { //HttpWebResponse resp = getUrlResponse(url, headerDict, postDict, timeout); HttpWebResponse resp = getUrlResponse(url, headerDict, postDict); int expectReadoutLen = (int)resp.ContentLength; totalLength = expectReadoutLen; currentLength = 0; Stream binStream = resp.GetResponseStream(); //int streamDataLen = (int)binStream.Length; // erro: not support seek operation do { System.Windows.Forms.Application.DoEvents(); // here download logic is: // once request, return some data // request multiple time, until no more data curReadoutLen = binStream.Read(respBytesBuf, curBufPos, expectReadoutLen); if (curReadoutLen > 0) { curBufPos += curReadoutLen; currentLength = curBufPos; expectReadoutLen = expectReadoutLen - curReadoutLen; realReadoutLen += curReadoutLen; } } while (curReadoutLen > 0); } catch { realReadoutLen = -1; } return realReadoutLen; }
然后结果竟然是就成功了,可以及时动态刷新进度条了:
5.后来又继续验证,发现原先的使用timer的方法,其实也是有效的。
只要是底层的,最耗时的那个循环中有了:
System.Windows.Forms.Application.DoEvents();
此时timer就可以得到响应,就可以及时获得进度,及时刷新进度条了。
所以,此处证明了,是由于底层的耗时的函数,使得界面没得响应,而在耗时函数中的循环中,加上
System.Windows.Forms.Application.DoEvents();
后,上层界面中的内容,才能得以更新的。
【总结】
对于想要实现动态滚动刷新进度条的话,则除了更新进度相关的代码的话,不论是用另外一个进程实现,还是用一个timer实现,则都不是没用的,都还是会导致界面卡死的。
因为另外一个耗时的函数,是需要一次性执行完,才能继续执行余下的函数的。
(注:相关解释参考:关于Application.DoEvents())
所以,必须另外在底层函数函数中的循环里面,添加上:
System.Windows.Forms.Application.DoEvents();
此时,上层的界面的更新,包括进度条更新,才能起效果的。
此处再完整的解释一下,具体实现的机制:
首先,我原先的代码是:
public void downloadStMusicFromUrl() { string singleStUrl = ""; //get st url singleStUrl = txbStUrl.Text; //download it if (downloadSingleStMusic(singleStUrl, txbSaveTo.Text)) { showCompleteHint(); } }
其中,downloadSingleStMusic是那个最耗时的函数,其底层是调用:
public int getUrlRespStreamBytes(xxx) { ... do { System.Windows.Forms.Application.DoEvents(); ... curReadoutLen = binStream.Read(respBytesBuf, curBufPos, expectReadoutLen); ... } while (curReadoutLen > 0); ... }
中的binStream.Read,去读取网络返回的数据的,这部分是比较耗时的,尤其是需要获取整个MP3文件的数据。
而此时,如果不加上述的:
System.Windows.Forms.Application.DoEvents();
则会导致,此函数必须一次性执行完毕,然后上层的函数,才能接着执行后续的
showCompleteHint();
此过程,整个UI部分就是卡死掉的,无法操作的,无法更新进度条的。
在底层添加了对应的
System.Windows.Forms.Application.DoEvents();
之后,上层可以通过两种方式实现对应的进度条的更新:
1.线程法
完整代码如下:
private delegate void ProgressBarShow(int i); private void ShowPro(int currentProgress) { if (this.InvokeRequired) { this.Invoke(new ProgressBarShow(ShowPro), currentProgress); } else { this.pgbDownload.Value = currentProgress; } } private void updateProgress() { int currentPecent = 0; while (currentPecent < 100) { currentPecent = getDownloadPercent(); ShowPro(currentPecent); Thread.Sleep(100); } Thread.CurrentThread.Abort(); } private void initForDownloadProcess() { Thread thread = new Thread(new ThreadStart(updateProgress)); //模拟进度条 thread.IsBackground = true; thread.Start(); } public void downloadStMusicFromUrl() { string singleStUrl = ""; //get st url singleStUrl = txbStUrl.Text; //initializeDownloadTimer(); initForDownloadProcess(); //download it if (downloadSingleStMusic(singleStUrl, txbSaveTo.Text)) { pgbDownload.Value = 0; //pgbDownload.Update(); System.Windows.Forms.Application.DoEvents(); // must add this, otherwise the UI not update !!! showCompleteHint(); } }
2.Timer法
private void increaseDownloadProgressBar(object sender, EventArgs e) { // Increment the value of the ProgressBar a value of one each time. int currentPecent = getDownloadPercent(); //pgbDownload.Increment(1); //pgbDownload.Increment(currentPecent); pgbDownload.Value = currentPecent; // Determine if we have completed if (pgbDownload.Value >= pgbDownload.Maximum) // Stop the timer. downloadTimer.Stop(); } // Call this method from the constructor of the form. private void initializeDownloadTimer() { downloadTimer = new System.Windows.Forms.Timer(); //downloadTimer.Enabled = true; // Set the interval for the timer. downloadTimer.Interval = 100; // Connect the Tick event of the timer to its event handler. downloadTimer.Tick += new EventHandler(increaseDownloadProgressBar); // Start the timer. downloadTimer.Start(); } public void downloadStMusicFromUrl() { string singleStUrl = ""; //get st url singleStUrl = txbStUrl.Text; initializeDownloadTimer(); //initForDownloadProcess(); //download it if (downloadSingleStMusic(singleStUrl, txbSaveTo.Text)) { pgbDownload.Value = 0; //pgbDownload.Update(); System.Windows.Forms.Application.DoEvents(); // must add this, otherwise the UI not update !!! showCompleteHint(); } }
其中:
- 更新进度条的方式,此处使用直接设置值去更新的:pgbDownload.Value = currentPecent;,你也可以根据自己需求,使用相关的pgbDownload.Increment(1);去增加相应的进度
- 此种方法,相对比较简单,更容易理解。
对于上述两种方法中相同部分的代码的解释:
- pgbDownload是ProgressBar变量;
- getDownloadPercent()用于获得底层已获得的数据的比例,你根据需要,改为自己相关的函数即可;
- 在使用downloadSingleStMusic下载完毕歌曲之后,使用:
- pgbDownload.Value = 0;
- 同样无法清空进度条的100的效果,只能再添加:
- System.Windows.Forms.Application.DoEvents();
- 才能实现清空进度条,变成0的效果。
另外,据关于Application.DoEvents()中的讨论,说是Application.DoEvents会导致效率低的问题。此处暂时不去深究。等有空再研究。