进度条、线程、Windows Forms,以及你
从头到尾在您的窗体上使用异步进度条。
引言
你的程序在执行耗时操作时是否曾经卡死?你是否曾想过如何实现一个漂亮、花哨的进度条来向最终用户证明程序没有崩溃?你是否曾尝试过使用进度条,却发现自己对如何让它们正常工作感到无比沮丧?
如果你对上述任何一个问题回答“是”,别担心——你并不孤单。任何使用过Visual X.NET的人都曾在编程生涯中因试图让程序的某些部分正常工作而感到愤怒不已。本文将帮助解决其中一个问题——进度条。
本文假设你对Visual Studio中Windows Forms的创建方式有基本了解,特别是如何创建窗体、在窗体上放置按钮以及将事件附加到该按钮。它还假设你对C#有了解,并对基本编程和面向对象编程概念和原则有相当强的理解。
关于进度条的注意事项
在将进度条集成到程序中之前,请先问自己程序是否真的需要它。进度条,像其他更高级的技术(如正则表达式和LINQ)一样,如果使用得当,对程序员和最终用户都非常强大和有用,但如果滥用,则会变得笨重和烦人。
以下是一些适合使用进度条的情况:
- 一个将大量文件从一个位置复制到另一个位置的实用程序。
- 一个计算圆周率第n位数的数学程序。
- 一个加载大型网页的Web浏览器。
- 一个在生成报告时从多个数据库获取信息的报告程序。
- 一个编码和保存视频信息的转换软件。
正确实现时,进度条是一种视觉上的保证工具,告诉最终用户你的程序没有卡死或冻结。这在运行可能需要数小时才能完成的任务时特别有用。如果没有某种进度指示,就无法判断程序是否已损坏或只是花费了异常长的时间。
有些使用进度条的做法应该避免,因为它们通常会导致代码臃肿和最终用户沮丧。以下是这些做法的简短列表:
- 当运行已知不会超过一两秒就能完成的进程时,在后台线程中简单地运行该进程可能比使用进度条更有效。
- 当作为单个操作的一部分运行多个连续进程时,为整个操作使用一个进度条,而不是为每个进程使用一个新的进度条。为单个操作附加多个连续进度条会让最终用户感到沮丧,因为他们不知道在整个操作完成之前会有多少个进度条。(但是,显示一个进度条用于当前进程的进度,另一个用于整个操作的进度是完全可以的。)
- 尽量根据操作的实际进度更新进度条的值,而不是为进程的每个阶段分配任意值。这将给人一种切实的进度感。
- 不要设置只有两个步骤(零百分比和完成)的进度条。如果出于某种原因你无法在操作运行时测量其进度,请使用不确定状态。
- 当运行具有许多增量(例如,数万或更大数量级)的进程时,不要为每个增量更新进度条。仅当更新进度条会产生明显的可见差异时才进行更新。在十万分之一的比例尺上增加一并不会导致进度条长度的变化。
入门
我们开始吧,好吗?首先,我们需要一个程序来运行我们的耗时进程。创建一个窗体并在其中心放置一个按钮。
没什么花哨的。为了模拟一些处理过程,请在按钮的Click
事件中添加一个Thread.Sleep
调用。
private void button1_Click(object sender, EventArgs e)
{
Thread.Sleep(5000);
MessageBox.Show("Thread completed!");
}
简直是复杂性的典范,我告诉你。运行程序并点击按钮。
点击按钮后,你会注意到在主线程处理期间整个窗体变得无响应——你甚至无法退出窗口!(当然,一旦线程停止睡眠,你对窗口所做的一切都会突然同时发生,这意味着如果你在窗体无响应时点击了红色X,程序会在睡眠结束后关闭。)
GUI 线程
这里发生的情况是这样的——基本上有一个线程处理窗体中的几乎所有事情,从初始化和监控控件到响应按钮点击和更新界面。这简化了整个过程,因为窗体的任何关键部分都不需要担心多线程。然而,这也带来了一个明显的缺点:只要主线程因任何原因卡住,所有事情都会突然停止,直到线程再次可用。
把它想象成一条繁忙的州际公路。由于某种原因,交通部门决定将这条公路设为单车道,没有交叉路口或路肩。当然,由于没有地方让过往车辆通过变道或进出交通而相互冲突,这条单车道以最高效率运行(假设所有汽车都以精确的速度限制行驶)。然而,如果发生任何事情干扰交通,比如事故或道路维护,所有汽车都必须停下来等待问题清除才能再次移动。
此时,你们中的一些人可能会想,“那么,为什么那些愚蠢的道路工程师不给那条高速公路再加一条车道呢?”对此,我说你完全正确。高速公路需要另一条车道,这样即使其中一条车道卡住,交通也能顺畅运行,就像这个程序需要另一个线程来做所有繁重的工作,这样当一个繁重的进程在其他地方运行时,主线程仍然可以做它的工作。
多线程
啊,多线程这个无处不在的编程怪物。多线程是指使程序中的两个进程相互独立地同时运行。许多程序员巧妙地利用多线程,但它也是一把双刃剑。如果你对代码不够小心,多线程很快就会变得令人困惑、笨重,甚至根本无法调试。
然而,如果管理得当,多线程是异步程序员最好的朋友。它使程序能够完成许多仅使用单线程将过于复杂或根本无法完成的事情。在这种情况下,它允许我们运行一个进程,而不会耽误主线程正在处理的所有重要事情,比如,你知道的,让界面看起来很漂亮。
让我们修改按钮的Click
事件方法中的代码,以利用这个多线程概念(别忘了在类顶部添加using System.Threading
)
private void button1_Click(object sender, EventArgs e)
{
Thread backgroundThread = new Thread(
new ThreadStart(() =>
{
Thread.Sleep(5000);
MessageBox.Show("Thread completed!");
}
));
backgroundThread.Start();
}
如果代码中有你不认识的术语,别担心。我将逐行讲解给你听。
Thread backgroundThread = new Thread
Thread
类,顾名思义,是线程的核心类。每个多线程应用程序都会在某个地方使用Thread
类。Thread
类的实例(在这个例子中是对象“backgroundThread”)代表一个可以通过该对象启动、停止和监控的线程。
有关Thread
类的更多信息,请查看MSDN上关于此主题的页面:http://msdn.microsoft.com/en-us/library/system.threading.thread.aspx。
new ThreadStart(() =>
{
Thread.Sleep(5000);
MessageBox.Show("Thread completed!");
}
Thread
类的构造函数接受一种特殊类型的委托作为参数,称为ThreadStart
。ThreadStart
类(及其近亲ParameterizedThreadStart
)代表线程将要运行的实际代码,显示在大括号内。当线程执行到代码末尾时,它会自动终止线程。
(注意:如果你不知道“() =>”语法是什么,那有点超出本文的范围。可以这样说,它被称为Lambda表达式,是LINQ结构的一部分,基本上是创建匿名方法的简写。)
如果你对ThreadStart
类感到好奇,这里是它的MSDN页面:http://msdn.microsoft.com/en-us/library/system.threading.threadstart.aspx。
backgroundThread.Start();
这就是实际启动线程的代码行。别忘了将这段代码放入你的程序中,否则你将坐着纳闷为什么你的进程没有运行,即使你按下了那个按钮。
如果你现在运行代码,你会注意到你可以随意按下按钮,这样做不会再导致窗体锁定。然而,直到线程停止睡眠并弹出对话框后,才会有任何实际发生。这意味着,在这五秒钟线程工作期间,没有任何迹象表明有事情正在发生!
那可不行。使用你程序的人需要知道他们所做的操作何时启动了一个重要进程。否则他们可能会怀疑程序是否损坏并再次点击按钮,从而启动另一个线程。(你们中的一些人可能已经发现,如果你在窗体中猛按按钮,五秒钟后你的屏幕就会被对话框狂轰滥炸。)我们需要一些东西来告诉用户他们所做的操作启动了一个重要的事情。
进度条的实践
我们终于来了——真正讨论如何使用进度条的部分!我相信我可以再写几段来谈论进度条有多么有用,但你可能已经知道了。否则你也不会在这里想学习如何使用它们。所以,闲话少说,让我们开始吧。
首先,让我们在窗体底部,按钮下方,添加一个ProgressBar
。
接下来,让我们稍微修改按钮的Click
事件方法,使其能够计算进度,然后将此进度发送到我们闪亮的新进度条。
private void button1_Click(object sender, EventArgs e)
{
Thread backgroundThread = new Thread(
new ThreadStart(() =>
{
for (int n = 0; n < 100; n++ )
{
Thread.Sleep(50);
progressBar1.Value = n;
}
MessageBox.Show("Thread completed!");
progressBar1.Value = 0;
}
));
backgroundThread.Start();
}
你可能已经注意到,我们没有调用一次Thread.Sleep
,而是有一个for循环,它调用Thread.Sleep
100次。这样做是为了让我们有一个大致与单次调用相同时间量的进程,但现在我们有了可以反馈给进度条的信息。然后,一旦for循环完成,我们希望将进度条的值重置为0,以表明进程已完成。
我们实际将信息提供给进度条的方式简单而轻松——设置Value
属性。只要你传入的值介于最小值和最大值之间(稍后会详细介绍),进度条就会更新自身,计算已填充了多少。在这种情况下,我们传入0到99之间的每个值,你只需运行程序并按下按钮即可观察到该进度。
等一下……程序对我们生气了。如果你运行此代码并按下按钮,它会向你抛出此错误:
InvalidOperationException was unhandled:
Cross-thread operation not valid: Control 'progressBar1' accessed
from a thread other than the thread it was created on.
还记得我之前说的程序只由一个线程管理的那一套说辞吗?事实证明,微软那些无所不知、仁慈的工程师决定更进一步,明确禁止其他线程干预它整齐的小控件集合。对我们程序员来说,这意味着你被禁止从创建控件的线程之外的任何线程设置任何控件的属性值。
由于窗体中的每个控件都是从一个线程(主线程)创建的,我们无法从我们正在运行的新线程设置进度条的值。然而,仍然有希望,所以不要惊慌。(以后会有很多时间来做这件事。)
Invoke 和 BeginInvoke
因为微软并非完全致力于让我们的工作尽可能困难,他们给了我们一些救命工具——Invoke
方法,特别是Invoke
和BeginInvoke
。这些方法的目的是允许程序员执行这些多线程操作,而无需逆向工程主线程在后台工作的所有更新功能。
简而言之,Invoke
方法接受一个委托并将其添加到主线程的队列中。然后,下次主线程空闲时(即没有处理我们的任何代码),它将执行该委托中的操作。通过这种方式,你可以欺骗进度条,让它认为我们的代码正在主线程上运行,即使它完全起源于其他地方。愚蠢的进度条。
Invoke
和 BeginInvoke
的区别在于,Invoke
是同步操作,这意味着一旦你调用此方法,它将等待委托在主线程中处理完毕后才继续执行;而 BeginInvoke
是异步操作,因此它会将委托交给主方法,然后继续执行,而无需等待该委托完成。这两种方法都有其优点和缺点,因此程序员需要判断哪种方法更适合程序正在执行的任务。通常情况下,除非你有理由需要等待委托处理完毕,例如委托更新了线程其余部分所依赖的关键信息,否则应使用 BeginInvoke
。
在这种情况下,我们继续使用BeginInvoke
,因为没有理由暂停我们重要且耗时的进程来等待进度条更新。更新按钮的Click
事件方法中的代码以使用此方法:
private void button1_Click(object sender, EventArgs e)
{
Thread backgroundThread = new Thread(
new ThreadStart(() =>
{
for (int n = 0; n < 100; n++ )
{
Thread.Sleep(50);
progressBar1.BeginInvoke(
new Action(() =>
{
progressBar1.Value = n;
}
));
}
MessageBox.Show("Thread completed!");
progressBar1.BeginInvoke(
new Action(() =>
{
progressBar1.Value = 0;
}
));
}
));
backgroundThread.Start();
}
这就是使用BeginInvoke
的全部内容。如果你现在运行程序并点击按钮,你应该会看到进度条慢慢填满直到最大值,然后在完成后显示一个对话框。
(如果你仔细观察,你可能会注意到代码中的另一个补充——Action类。这是因为Invoke
和BeginInvoke
方法只接受委托,所以简单地使用lambda表达式是行不通的。Action类将匿名方法包装在委托中,允许它们在这种情况下使用。你了解得越多,就越……)
多线程问题
如果你是那种喜欢看看在程序崩溃之前你能把它逼到什么程度的人(或者你就是之前被对话框“围攻”的人),你可能已经注意到,如果你猛按按钮,进度条会开始跳一些奇怪的舞步,你可能在频闪灯大会上见过。
这是因为当你多次按下按钮时,你会启动许多同时执行“数到一百”操作的线程。除了让你的处理器想要切腹自尽之外,所有这些线程在每一步都争夺进度条的注意力,使得进度条中的实际进度指示器在不同位置之间不规则地跳动。发生的情况是所有线程都以不特定的顺序将自己的进度添加到主线程的队列中。然后进度条对每个进度进行排序,但只保留队列中最后一个,并相应地更新其图形。
最简单、最合适的解决方案是只允许在按钮尚未按下时才按下。毕竟,你不会希望用户在刚开始执行同一进程时就能再次运行该进程。这会带来各种潜在的错误和故障,而调试它们的最佳方法是设计程序,使其一开始就不会出现这些问题。
幸运的是,实现这样的功能就像添加一个布尔变量和一次if检查一样简单:
private bool isProcessRunning = false;
private void button1_Click(object sender, EventArgs e)
{
if (isProcessRunning)
{
MessageBox.Show("A process is already running.");
return;
}
Thread backgroundThread = new Thread(
new ThreadStart(() =>
{
isProcessRunning = true;
for (int n = 0; n < 100; n++ )
{
Thread.Sleep(50);
progressBar1.BeginInvoke(
new Action(() =>
{
progressBar1.Value = n;
}
));
}
MessageBox.Show("Thread completed!");
progressBar1.BeginInvoke(
new Action(() =>
{
progressBar1.Value = 0;
}
));
isProcessRunning = false;
}
));
backgroundThread.Start();
}
瞧!如果你现在运行程序并点击按钮,它会像往常一样运行。现在我敢你再按一次那个按钮。试试看。没用,是吧?你的程序现在太聪明了,你无法轻易地破坏它。
现在你的程序摆脱了所有那些烦人的并发线程相互踩踏的问题,让我们转向进度条的另一种可能实现方式。
对话框中的进度条
所有这些在窗体内使用进度条的讨论对于后台进程来说很好,但如果你的任务要求用户在运行时不能触摸窗体怎么办?例如,你可能有一个保存程序当前状态的按钮。如果用户在保存进程运行时更改了该状态,那将是一个问题。
这就是进度对话框的作用。它们提供方便的进度指示器,同时在打开时禁用主窗体的任何输入。
要创建一个,只需创建一个名为ProgressDialog
的新窗体,并放上一个ProgressBar
。
如果你愿意,可以设置一堆与对话框相关的属性(例如StartPosition
和FormBorderStyle
),但为了所有意图和目的,我们认为这样就足够了。
让我们也在主窗体中添加一个新按钮,这样我们就有了一个可以启动此对话框的地方。
以前,当我们需要更新进度时,可以直接调用它,因为代码与进度条在同一个类中。但是现在,进度条将位于一个单独的窗体中,并且默认是私有的,这意味着你的线程代码将无法获得修改进度条值所需的访问权限。
现在,在你提出我们只需更改进度条的修饰符使其为公共时,我会说,是的,我们可以那样做。我们也可以用吸管和纸浆完全建造一座跨越大峡谷的桥梁,或者如果建造足够大的弹弓,我们也能到达月球。虽然你可以让控件具有公共访问权限,但通常这样做是不良实践。使控件公共会暴露其所有属性,其中一些属性如果从其他类或窗体更改,则无法正常工作。更好的做法是使用属性仅暴露你想要访问的内容,或者在我们的情况下,创建一个方法来为你进行访问。
在ProgressDialog
的代码中,创建一个名为UpdateProgress
的新公共方法,该方法接受一个整数作为参数。在该方法中,执行我们之前进行的所有BeginInvoke
等调用。你最终会得到类似这样的代码:
public void UpdateProgress(int progress)
{
progressBar1.BeginInvoke(
new Action(() =>
{
progressBar1.Value = progress;
}
));
}
此方法将处理我们的多线程目标所需的实际Invoking
。现在,我们按钮的Click
事件方法只需担心执行其极其重要的处理。我们将继续复制粘贴之前的代码,并进行一些选择性修改:
private void button2_Click(object sender, EventArgs e)
{
if (isProcessRunning)
{
MessageBox.Show("A process is already running.");
return;
}
ProgressDialog progressDialog = new ProgressDialog();
Thread backgroundThread = new Thread(
new ThreadStart(() =>
{
isProcessRunning = true;
for (int n = 0; n < 100; n++)
{
Thread.Sleep(50);
progressDialog.UpdateProgress(n); // Update progress in progressDialog
}
MessageBox.Show("Thread completed!");
// No need to reset the progress since we are closing the dialog
progressDialog.BeginInvoke(new Action(() => progressDialog.Close()));
isProcessRunning = false;
}
));
backgroundThread.Start();
progressDialog.ShowDialog();
}
如你所见,代码大部分未变。关键区别显然与我们刚刚创建的ProgressDialog
的添加有关。
ProgressDialog progressDialog = new ProgressDialog();
...
progressDialog.ShowDialog();
这些代码段创建并显示了ProgressDialog
。使用ShowDialog
方法而不是Show
方法将在对话框打开时暂停主线程,从而阻止用户在进程工作时进行任何更改。
progressDialog.UpdateProgress(i);
这是对我们之前创建的方法的调用。由于UpdateProgress
方法内部包含了对Invoke的调用,因此此处无需再次使用Invoke。
progressDialog.BeginInvoke(new Action(() => progressDialog.Close()));
此行将在进程运行完毕后关闭对话框。请注意,由于我们在不同的线程上创建了对话框,并且告诉窗体自行关闭是一个相当重大的更改,因此你需要调用Invoke才能使用它。
就是这样!运行程序并按下新按钮。它应该会弹出对话框,并附带一个正常工作并填充的进度条,最棒的是,在进程运行时,不允许与主窗体进行任何交互。
在结束本文之前,我们还有一件事需要讨论。
不确定模式
在开发带有进度条的应用程序时,你应始终设计代码,使其易于量化进程的“完成”程度。然而,有时根本无法判断特定进程距离完成还有多远。一些例子包括:
- 应用程序加载多个文件但确切数量未知时。
- 应用程序从另一个应用程序接收可变数量的数据时。
- 应用程序使用外部库且对源代码访问受限时。
在这些情况下,你无法真正给出确切的进度量,因为没有任何迹象表明进程何时会完成。但是,你仍然需要一些进度指示来向用户表明确实正在进行某项工作。这就是不确定模式发挥作用的地方。
不确定模式是进度条的一个设置,表示程序正在做某事,但不知道需要多长时间。在视觉上,进度条会显示一个持续滚动的跑马灯,永不停止。(也就是说,直到你改变进度条或关闭窗体。)
将进度条设置为不确定模式很简单。你只需要将进度条的样式设置为Marquee
。
progressBar.Style = ProgressBarStyle.Marquee;
反之,要将进度条恢复到正常的进度滚动方式,请将其样式设置为Blocks
。
progressBar.Style = ProgressBarStyle.Blocks;
(注意:进度条有第三种可能的样式,Continuous
。然而,除非程序在Windows XP或更早的环境下运行,或者禁用了视觉样式,否则它与Blocks
的处理方式相同。这些主题超出了本文的范围,但可以说,除非你有特定原因,否则你永远不应该使用Continuous
。)
为了展示这个功能,让我们向ProgressDialog
添加一个新方法,名为SetIndeterminate
,它接受一个布尔值作为参数:
public void SetIndeterminate(bool isIndeterminate)
{
progressBar1.BeginInvoke(
new Action(() =>
{
if (isIndeterminate)
{
progressBar1.Style = ProgressBarStyle.Marquee;
}
else
{
progressBar1.Style = ProgressBarStyle.Blocks;
}
}
));
}
同样,由于此方法旨在从单独的线程调用,我们必须使用Invoke
才能使其正常工作。否则,此方法相当一目了然——如果你传入一个true值,对话框会将其进度条的样式设置为Marquee
,否则会将其值设置为Blocks
。
为了展示这个功能,让我们在主窗体上再添加一个按钮。
在新按钮的Click
事件方法中,添加以下代码:
private void button3_Click(object sender, EventArgs e)
{
if (isProcessRunning)
{
MessageBox.Show("A process is already running.");
return;
}
ProgressDialog progressDialog = new ProgressDialog();
progressDialog.SetIndeterminate(true);
Thread backgroundThread = new Thread(
new ThreadStart(() =>
{
isProcessRunning = true;
Thread.Sleep(5000);
MessageBox.Show("Thread completed!");
progressDialog.BeginInvoke(new Action(() => progressDialog.Close()));
isProcessRunning = false;
}
));
backgroundThread.Start();
progressDialog.ShowDialog();
}
这段代码应该看起来有点熟悉。为了模拟一个无法量化其完成程度的进程,我们放弃了我们花哨的for循环,转而使用单个Thread.Sleep
调用。另一个新增功能是对我们刚创建的ProgressDialog
的SetIndeterminate
方法的调用。
如果你现在运行程序,你应该能够点击第三个按钮,然后我们的新的不确定模式ProgressDialog
会显示五秒钟。如果你愿意,尽情享受它的荣光吧。
处理 InvalidOperationExceptions
是的,我之前承诺不确定模式是最后一件事。然而,在我放你走之前,还有一件事需要告诉你。
使用Invoke方法的一个注意事项是,它们只有在窗体已加载、可见等情况下才能工作。如果你尝试在尚未显示或已关闭的窗体上调用这些方法,你会得到一个看起来很像这样的异常:
InvalidOperationException was unhandled:
Invoke or BeginInvoke cannot be called on a control until the window handle has been created.
这样做的原因是Invoke
方法旨在允许你将进程注入到窗体或控件的主线程中,如果该线程当前不接受请求,因为它正忙于不运行,就会出现问题。不过好消息是,在Invoke无法使用的这些时候,它实际上并不需要。你可以在任何其他线程上进行各种属性设置和操作。但是,一旦该窗体显示出来,所有约定都将失效。
这都很好,但是你怎么知道一个窗体当前是否正在显示呢?当然,你可以检查它的Visible
属性或订阅它的Load
事件方法,但有更可靠的方法。所有具有Invoke方法的窗体和控件也都有一个InvokeRequired
属性,它返回一个布尔值,告诉你(惊喜!)是否需要使用Invoke方法来执行所有特殊的属性操作魔术。
更改ProgressDialog
类中的方法,以使用这个新发现的信息瑰宝:
public void UpdateProgress(int progress)
{
if (progressBar1.InvokeRequired)
{
progressBar1.BeginInvoke(
new Action(() =>
{
progressBar1.Value = progress;
}
));
}
else
{
progressBar1.Value = progress;
}
}
public void SetIndeterminate(bool isIndeterminate)
{
if (progressBar1.InvokeRequired)
{
progressBar1.BeginInvoke(
new Action(() =>
{
if (isIndeterminate)
{
progressBar1.Style = ProgressBarStyle.Marquee;
}
else
{
progressBar1.Style = ProgressBarStyle.Blocks;
}
}
));
}
else
{
if (isIndeterminate)
{
progressBar1.Style = ProgressBarStyle.Marquee;
}
else
{
progressBar1.Style = ProgressBarStyle.Blocks;
}
}
}
就是这样。在这些方法中的任何一个中,如果窗体或控件当前需要Invoke
方法,那么它就使用Invoke
方法;如果不需要,它就不使用。这再简单不过了。
程序中还有其他一些地方也需要使用InvokeRequired
。在主窗体中第一个按钮的Click
事件方法中,对进度条进行了两次BeginInvoke
调用。如果由于任何原因在线程运行时窗体关闭,这两次调用都可能是潜在的错误点。在这两种情况下,添加对InvokeRequired
的检查将消除此过程中的潜在错误。
if (progressBar1.InvokeRequired)
progressBar1.BeginInvoke(new Action(() => progressBar1.Value = n));
...
if (progressBar1.InvokeRequired)
progressBar1.BeginInvoke(new Action(() => progressBar1.Value = 0));
最后,在其他两个按钮的Click
事件方法中,有对ProgressDialog
的Close
方法的调用。这些调用也是脆弱点,原因有几个。首先,在InvokeRequired
为true时调用Close
也是不允许的,但另一个在此处包含检查的原因是为了避免冗余——你不想尝试关闭一个已经关闭的窗体。
if (progressDialog.InvokeRequired)
progressDialog.BeginInvoke(new Action(() => progressDialog.Close()));
进度条的更多乐趣
你还可以用进度条做一些更有趣的事情,例如:
- 本文使用的是
ProgressBar
的默认最小值和最大值,分别是0和100。但是,你并不局限于这些值。它们可以通过ProgressBar
的Minimum
和Maximum
属性轻松更改。 - 你可以添加一个伴随标签,以显示状态更新和进度,从而向最终用户提供更具信息量的反馈。
- 结合
Marquee
和Blocks
样式,以便在没有所需信息来计算进度比例时显示不确定模式,然后在获得该信息后切换回默认模式。(例如,在扫描要导入的文件时,可以将进度条设置为不确定模式,然后在获得可计数的文件列表后切换回来。) - 如果你对进程的某个特定部分将花费多长时间有很好的了解,你可以利用这段时间来计算ETA(预计到达时间)。用户喜欢ETA。(有趣的是:用户喜欢准确的ETA。)
- 如果你有一个非常长的过程可以分解成多个片段,你可以使用两个进度条——一个用于每个片段,一个用于整个过程。这确实有助于给用户留下正在取得进展的印象,而无需仔细观察进度条的逐像素增量。
历史
1.0 - 发布。