在C#中做一些工作时显示进度条?

我想在做一些工作时显示进度条,但这会挂起UI并且进度条不会更新。

我有一个带有ProgressBar的WinForm ProgressForm,它将以一种大型方式无限期地继续。

 using(ProgressForm p = new ProgressForm(this)) { //Do Some Work } 

现在有很多方法可以解决这个问题,比如使用BeginInvoke ,等待任务完成并调用EndInvoke 。 或者使用BackgroundWorkerThreads

我在使用EndInvoke时遇到了一些问题,尽管这不是问题。 问题是你用来处理这种情况的最好和最简单的方法,你必须向用户显示程序正在运行而不是没有响应,以及如何使用最简单的代码来处理这个问题,这是有效的并且赢得了’ t泄漏,并可以更新GUI。

BackgroundWorker需要有多个函数,声明成员变量等。然后你需要保持对ProgressBar表单的引用并处理它。

编辑BackgroundWorker不是答案,因为它可能是我没有获得进度通知,这意味着不会调用ProgressChanged因为DoWork是对外部函数的单个调用,但我需要继续调用Application.DoEvents(); 进度条保持旋转。

赏金是针对此问题的最佳代码解决方案。 我只需要调用Application.DoEvents()以便Marque进度条可以工作,而worker函数在Main线程中工作,并且它不会返回任何进度通知。 我从不需要.NET魔术代码来自动报告进度,我只需要一个比以下更好的解决方案:

 Action exec = DoSomethingLongAndNotReturnAnyNotification; IAsyncResult result = exec.BeginInvoke(path, parameters, null, null); while (!result.IsCompleted) { Application.DoEvents(); } exec.EndInvoke(result); 

保持进度条活着(意味着不冻结但刷新品牌)

在我看来,你至少在一个错误的假设上运作。

1.您无需提升ProgressChanged事件以获得响应式UI

在你的问题中,你这样说:

BackgroundWorker不是答案,因为它可能是我没有获得进度通知,这意味着不会调用ProgressChanged,因为DoWork是对外部函数的单个调用。 。 。

实际上, 无论您是否调用ProgressChanged事件都无关紧要 。 该事件的整个目的是暂时将控制权转移回GUI线程,以进行更新,以某种方式反映BackgroundWorker正在完成的工作的进度。 如果您只是显示一个选取框进度条,那么提升ProgressChanged事件实际上毫无意义 。 只要显示进度条,进度条将继续旋转,因为BackgroundWorker正在与GUI的单独线程上进行工作

(另一方面, DoWork是一个事件,这意味着它不仅仅是 “对外部函数的单个调用”;您可以添加任意数量的处理程序;并且每个处理程序可以包含尽可能多的函数调用它喜欢。)

2.您无需调用Application.DoEvents即可拥有响应式UI

对我而言,听起来您认为GUI更新的唯一方法是调用Application.DoEvents

我需要继续调用Application.DoEvents(); 进度条保持旋转。

在multithreading场景中不是这样 ; 如果您使用BackgroundWorker ,GUI将继续响应(在其自己的线程上),而BackgroundWorker执行附加到其DoWork事件的任何内容。 下面是一个简单的例子,说明这可能对你有用。

 private void ShowProgressFormWhileBackgroundWorkerRuns() { // this is your presumably long-running method Action exec = DoSomethingLongAndNotReturnAnyNotification; ProgressForm p = new ProgressForm(this); BackgroundWorker b = new BackgroundWorker(); // set the worker to call your long-running method b.DoWork += (object sender, DoWorkEventArgs e) => { exec.Invoke(path, parameters); }; // set the worker to close your progress form when it's completed b.RunWorkerCompleted += (object sender, RunWorkerCompletedEventArgs e) => { if (p != null && p.Visible) p.Close(); }; // now actually show the form p.Show(); // this only tells your BackgroundWorker to START working; // the current (ie, GUI) thread will immediately continue, // which means your progress bar will update, the window // will continue firing button click events and all that // good stuff b.RunWorkerAsync(); } 

3.您不能在同一个线程上同时运行两个方法

你这样说:

我只需要调用Application.DoEvents()以便Marque进度条可以工作,而worker函数在Main线程中工作。 。 。

你所要求的只是不真实 。 Windows窗体应用程序的“主”线程是GUI线程,如果它忙于您长时间运行的方法,则不提供可视更新。 如果你不相信,我怀疑你误解了BeginInvoke作用:它在一个单独的线程上启动一个委托。 实际上,您在问题中包含的示例代码在exec.BeginInvokeexec.EndInvoke之间调用Application.DoEvents是多余的; 你实际上是从GUI线程反复调用Application.DoEvents无论如何都会更新 。 (如果你发现了其他情况,我怀疑是因为你exec.EndInvoke调用了exec.EndInvoke ,它阻止了当前线程,直到方法结束。)

所以,是的,您正在寻找的答案是使用BackgroundWorker

可以使用BeginInvoke ,但不是从GUI线程调用EndInvoke (如果方法未完成将阻止它),将AsyncCallback参数传递给BeginInvoke调用(而不是仅传递null ),并关闭进度表单。你的回调。 但请注意,如果你这样做,你将不得不从GUI线程调用关闭进度表单的方法,否则你将试图关闭一个表单,这是一个GUI函数,非GUI线程。 但实际上,使用BeginInvoke / EndInvoke所有陷阱已经用BackgroundWorker类处理过 ,即使你认为它是“.NET魔术代码”(对我来说,它只是一个直观而有用的工具)。

对我来说,最简单的方法是使用专门为此类任务设计的BackgroundWorkerProgressChanged事件非常适合更新进度条,而不必担心跨线程调用

在Stackoverflow上有大量关于使用.NET / C#进行线程处理的信息 ,但是清除Windows窗体线程的文章是我们的常驻oracle,Jon Skeet的“Windows窗体中的线程” 。

整个系列值得阅读,以了解您的知识或从头学习。

我很不耐烦,只是给我看一些代码

至于“告诉我代码”,下面是我如何使用C#3.5。 表单包含4个控件:

  • 一个文本框
  • 进度条
  • 2个按钮:“buttonLongTask”和“buttonAnother”

buttonAnother纯粹是为了certificate在count-to-100任务运行时UI没有被阻止。

 public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void buttonLongTask_Click(object sender, EventArgs e) { Thread thread = new Thread(LongTask); thread.IsBackground = true; thread.Start(); } private void buttonAnother_Click(object sender, EventArgs e) { textBox1.Text = "Have you seen this?"; } private void LongTask() { for (int i = 0; i < 100; i++) { Update1(i); Thread.Sleep(500); } } public void Update1(int i) { if (InvokeRequired) { this.BeginInvoke(new Action(Update1), new object[] { i }); return; } progressBar1.Value = i; } } 

另一个例子是BackgroundWorker是正确的方法……

 using System; using System.ComponentModel; using System.Threading; using System.Windows.Forms; namespace SerialSample { public partial class Form1 : Form { private BackgroundWorker _BackgroundWorker; private Random _Random; public Form1() { InitializeComponent(); _ProgressBar.Style = ProgressBarStyle.Marquee; _ProgressBar.Visible = false; _Random = new Random(); InitializeBackgroundWorker(); } private void InitializeBackgroundWorker() { _BackgroundWorker = new BackgroundWorker(); _BackgroundWorker.WorkerReportsProgress = true; _BackgroundWorker.DoWork += (sender, e) => ((MethodInvoker)e.Argument).Invoke(); _BackgroundWorker.ProgressChanged += (sender, e) => { _ProgressBar.Style = ProgressBarStyle.Continuous; _ProgressBar.Value = e.ProgressPercentage; }; _BackgroundWorker.RunWorkerCompleted += (sender, e) => { if (_ProgressBar.Style == ProgressBarStyle.Marquee) { _ProgressBar.Visible = false; } }; } private void buttonStart_Click(object sender, EventArgs e) { _BackgroundWorker.RunWorkerAsync(new MethodInvoker(() => { _ProgressBar.BeginInvoke(new MethodInvoker(() => _ProgressBar.Visible = true)); for (int i = 0; i < 1000; i++) { Thread.Sleep(10); _BackgroundWorker.ReportProgress(i / 10); } })); } } } 

确实,你走在正确的轨道上。 您应该使用另一个线程,并且已经确定了执行此操作的最佳方法。 其余的只是更新进度条。 如果您不想像其他人建议的那样使用BackgroundWorker,请记住一个技巧。 诀窍是您无法从工作线程更新进度条,因为UI只能从UI线程进行操作。 所以你使用Invoke方法。 它是这样的(自己修复语法错误,我只是写一个简单的例子):

 class MyForm: Form { private void delegate UpdateDelegate(int Progress); private void UpdateProgress(int Progress) { if ( this.InvokeRequired ) this.Invoke((UpdateDelegate)UpdateProgress, Progress); else this.MyProgressBar.Progress = Progress; } } 

InvokeRequired属性将在每个线程上返回true ,但拥有该表单的线程除外。 Invoke方法将在UI线程上调用该方法,并将阻塞直到它完成。 如果您不想阻止,可以改为调用BeginInvoke

BackgroundWorker不是答案,因为我可能没有得到进度通知……

您没有获得进度通知的事实与BackgroundWorker的使用有什么关系? 如果长期运行的任务没有可靠的机制来报告其进度,则无法可靠地报告其进度。

报告长时间运行方法进度的最简单方法是在UI线程上运行该方法,并通过更新进度条然后调用Application.DoEvents()使其报告进度。 从技术上讲,这将起作用。 但是在对Application.DoEvents()调用之间,UI将没有响应。 这是一个快速而肮脏的解决方案,正如Steve McConnell所观察到的那样,快速而肮脏的解决方案的问题在于,在忘记快速的甜味之后,肮脏的苦味仍然很长。

正如另一张海报所提到的,下一个最简单的方法是实现一个使用BackgroundWorker来执行长时间运行方法的模态表单。 这提供了通常更好的用户体验,并且它使您不必解决在长时间运行的任务执行期间UI的哪些部分保持function的潜在复杂问题 – 而模态forms是打开的,其余的都没有您的UI将响应用户操作。 这是快速而干净的解决方案。

但它仍然是用户敌对的。 在长时间运行的任务执行时,它仍会锁定UI; 它只是以一种漂亮的方式做到了。 要创建用户友好的解决方案,您需要在另一个线程上执行该任务。 最简单的方法是使用BackgroundWorker

这种方法为许多问题打开了大门。 它不会“泄漏”,不管是什么意思。 但无论长期运行的方法是什么,它现在必须完全隔离在运行时保持启用的UI部分。 完成之后,我的意思是完整的。 如果用户可以使用鼠标单击任意位置并导致对您长时间运行的方法所看到的某个对象进行某些更新,那么您将遇到问题。 您长时间运行的方法使用的任何可能引发事件的对象都是一条可能导致痛苦的道路。

就是这样,并没有让BackgroundWorker正常工作,这将成为所有痛苦的根源。

我必须在那里提出最简单的答案。 您可以随时实现进度条,并且与实际进度无关。 刚刚开始填充酒吧说每秒1%,或者每秒10%,无论你的行为看起来多么类似,如果它再次重新开始。

这将至少为用户提供处理的外观并使他们理解等待而不是仅仅单击按钮并且看不到任何事情然后再点击它。

下面是使用BackgroundWorker更新ProgressBar另一个示例代码,只需将BackgroundWorkerProgressbar添加到主窗体并使用下面的代码:

 public partial class Form1 : Form { public Form1() { InitializeComponent(); Shown += new EventHandler(Form1_Shown); // To report progress from the background worker we need to set this property backgroundWorker1.WorkerReportsProgress = true; // This event will be raised on the worker thread when the worker starts backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork); // This event will be raised when we call ReportProgress backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged); } void Form1_Shown(object sender, EventArgs e) { // Start the background worker backgroundWorker1.RunWorkerAsync(); } // On worker thread so do our thing! void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { // Your background task goes here for (int i = 0; i <= 100; i++) { // Report progress to 'UI' thread backgroundWorker1.ReportProgress(i); // Simulate long task System.Threading.Thread.Sleep(100); } } // Back on the 'UI' thread so we can update the progress bar void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { // The progress percentage is a property of e progressBar1.Value = e.ProgressPercentage; } } 

参考: 来自codeproject

使用它专为此方案设计的BackgroundWorker组件。

您可以挂钩其进度更新事件并更新进度条。 BackgroundWorker类确保将回调编组到UI线程,因此您也不必担心任何细节。

阅读您的需求最简单的方法是显示无模式表单并使用标准的System.Windows.Forms计时器来更新无模式表单的进度。 没有线程,没有可能的内存泄漏。

由于这只使用一个UI线程,因此您还需要在主处理期间的某些点调用Application.DoEvents(),以保证进度条可视化更新。

回复:你的编辑。 您需要BackgroundWorker或Thread来完成工作,但它必须定期调用ReportProgress()来告诉UI线程它正在做什么。 DotNet不能神奇地计算出你做了多少工作,所以你必须告诉它(a)你将达到的最大进度金额,然后(b)在这个过程中大约100次左右,告诉它它取决于你的数量。 (如果你报告的进度少于100次,那么progess栏会大幅跳跃。如果你报告超过100次,你只会浪费时间尝试报告比进度条有助于显示更精细的细节)

如果你的UI线程可以在后台工作程序运行时愉快地继续,那么你的工作就完成了。

但是,实际上,在需要运行进度指示的大多数情况下,您的UI需要非常小心以避免重入调用。 例如,如果在导出数据时运行进度显示,则不希望用户在导出过程中再次开始导出数据。

您可以通过两种方式处理此问题:

  • 导出操作将检查后台工作程序是否正在运行,并在导入时禁用导出选项。 这将允许用户在程序中执行除导出之外的任何操作 – 如果用户可以(例如)编辑正在导出的数据,这仍然是危险的。

  • 将进度条作为“模态”显示运行,以便程序在导出期间保持“活动”状态,但在导出完成之前,用户实际上无法执行任何操作(除取消之外)。 虽然这是最常见的方法,但DotNet在支持这方面仍然很垃圾。 在这种情况下,您需要将UI线程放入一个忙等待循环,在其中调用Application.DoEvents()以保持消息处理运行(因此进度条将起作用),但您需要添加仅允许您的应用程序的MessageFilter响应“安全”事件(例如,它将允许Paint事件,因此您的应用程序窗口继续重绘,但它会过滤掉鼠标和键盘消息,以便用户在导出过程中无法在proigram中实际执行任何操作还有一些偷偷摸摸的消息,你需要通过才能让窗口正常工作,并将这些消息计算出来需要几分钟 – 我有一份工作清单,但没有它们我很担心。这些都是明显的,比如NCHITTEST加上一个偷偷摸摸的.net(在WM_USER范围内表现出来),这对于让这个工作变得至关重要)。

可怕的dotNet进度条的最后一个“陷阱”是当你完成操作并关闭进度条时,你会发现它通常在报告像“80%”这样的值时退出。 即使你强迫它达到100%然后等待大约半秒钟,它仍然可能达不到100%。 Arrrgh! 解决方案是将进度设置为100%,然后设置为99%,然后再设置为100% – 当进度条被告知向前移动时,它会慢慢向目标值设置动画。 但如果你告诉它“向后”,它会立即跳到那个位置。 因此,通过在结束时暂时将其反转,您可以让它实际显示您要求它显示的值。

如果您想要一个“旋转”进度条,为什么不将进度条样式设置为“Marquee”并使用BackgroundWorker来保持UI响应? 与使用“Marquee”风格相比,您不会更容易实现旋转进度条…

对于这样的事情,我们使用带有BackgroundWorker模态forms。

这是快速解决方案:

  public class ProgressWorker : BackgroundWorker where TArgument : class { public Action Action { get; set; } protected override void OnDoWork(DoWorkEventArgs e) { if (Action!=null) { Action(e.Argument as TArgument); } } } public sealed partial class ProgressDlg : Form where TArgument : class { private readonly Action action; public Exception Error { get; set; } public ProgressDlg(Action action) { if (action == null) throw new ArgumentNullException("action"); this.action = action; //InitializeComponent(); //MaximumSize = Size; MaximizeBox = false; Closing += new System.ComponentModel.CancelEventHandler(ProgressDlg_Closing); } public string NotificationText { set { if (value!=null) { Invoke(new Action(s => Text = value)); } } } void ProgressDlg_Closing(object sender, System.ComponentModel.CancelEventArgs e) { FormClosingEventArgs args = (FormClosingEventArgs)e; if (args.CloseReason == CloseReason.UserClosing) { e.Cancel = true; } } private void ProgressDlg_Load(object sender, EventArgs e) { } public void RunWorker(TArgument argument) { System.Windows.Forms.Application.DoEvents(); using (var worker = new ProgressWorker {Action = action}) { worker.RunWorkerAsync(); worker.RunWorkerCompleted += worker_RunWorkerCompleted; ShowDialog(); } } void worker_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e) { if (e.Error != null) { Error = e.Error; DialogResult = DialogResult.Abort; return; } DialogResult = DialogResult.OK; } } 

以及我们如何使用它:

 var dlg = new ProgressDlg(obj => { //DoWork() Thread.Sleep(10000); MessageBox.Show("Background task completed "obj); }); dlg.RunWorker("SampleValue"); if (dlg.Error != null) { MessageBox.Show(dlg.Error.Message, "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error); } dlg.Dispose();