在C#中执行某项任务时如何显示进度条?

26

我想在执行某些工作时显示进度条,但这会挂起用户界面并且进度条不会更新。

我有一个带有ProgressBar的WinForm ProgressForm,它将以跑马灯的方式无限期地继续。

using(ProgressForm p = new ProgressForm(this))
{
//Do Some Work
}
现在有许多解决问题的方法,比如使用BeginInvoke,等待任务完成并调用EndInvoke。或者使用BackgroundWorkerThreads
虽然我在使用EndInvoke时遇到了一些问题,但这不是我的问题。问题是在处理这种情况时,您使用的最佳和最简单的方法是什么,即当您必须向用户显示程序正在工作且未响应时,如何使用最简单的代码来处理它,以及如何更新GUI,而不会泄漏任何信息,并且高效率。
例如,BackgroundWorker需要有多个函数、声明成员变量等。此外,您需要保留对ProgressBar表单的引用并将其处理掉。 编辑:BackgroundWorker不是答案,因为可能无法得到进度通知,这意味着没有调用ProgressChanged,因为DoWork只调用一个外部函数,但我需要继续调用Application.DoEvents();以使进度条继续旋转。
赏金是为了找到解决此问题的最佳代码解决方案。我只需要调用Application.DoEvents(),以便Marque进度条可以工作,而工作函数在主线程中工作,并且它不返回任何进度通知。我从来没有需要.NET魔法代码自动报告进度,我只需要比以下更好的解决方案:
Action<String, String> exec = DoSomethingLongAndNotReturnAnyNotification;
IAsyncResult result = exec.BeginInvoke(path, parameters, null, null);
while (!result.IsCompleted)
{
  Application.DoEvents();
}
exec.EndInvoke(result);

保持进度条活动状态(即不冻结但可以刷新滚动条)


这是哪个版本的 .NET? - Chris S
2
@Priyank - 不管你使用 BackgroundWorker 还是什么方式,如果你没有获取到进度更新通知,你打算怎样更新进度条? - Paolo
@Paolo:它是Marque,无限旋转,我只想让用户知道正在进行一些处理。 - Priyank Bolia
2
如果您的进度条只是在等待后台任务完成之前不停旋转,为什么要费力处理所有这些复杂性呢?只需在 UI 线程上运行动画:如果使用任何合理的方法启动后台任务,则后台任务将不会阻塞 UI。在完成通知时终止动画即可。 - Godeke
@Godeke:我想在主线程上运行所谓的后台任务,而不使用任何合理的方法,如线程、BackgroundWorker、SyncronizationContext。 - Priyank Bolia
13个回答

44

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

1. 您不需要触发 ProgressChanged 事件以获得响应的 UI

您在问题中说到:

BackgroundWorker 不是答案, 因为可能无法收到进度通知, 这意味着没有调用 ProgressChanged, 因为 DoWork 是对外部函数的单个调用。   。.

实际上,无论是否调用 ProgressChanged 事件都无所谓。该事件的整个目的是暂时将控件传回 GUI 线程,以进行某种反映 BackgroundWorker 执行的工作的进度更新。 如果您只是显示一个跑马灯进度条,则完全没有必要触发 ProgressChanged 事件。 进度条将继续旋转,因为 BackgroundWorker 在与 GUI 分离的线程上执行其工作

(顺带一提,DoWork 是一个事件,这意味着它不仅仅是“对外部函数的单个调用”;您可以添加任意数量的处理程序;每个处理程序可以包含任意数量的函数调用。)

2. 您不需要调用 Application.DoEvents 来获得响应的 UI

对我来说,听起来您似乎相信 唯一 更新 GUI 的方法是调用 Application.DoEvents

我需要不断调用 Application.DoEvents(); 来使进度条继续旋转。

这在多线程场景中是错误的;如果您使用 BackgroundWorker,GUI 将继续响应(在其自己的线程上),而 BackgroundWorker 执行附加到其 DoWork 事件的任何操作。下面是一个简单的示例,展示了如何为您工作。

private void ShowProgressFormWhileBackgroundWorkerRuns() {
    // this is your presumably long-running method
    Action<string, string> 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 (i.e., 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 函数在主线程中工作...

你所要求的根本不可行。对于 Windows Forms 应用程序来说,“主”线程是 GUI 线程,如果它正在忙于你的长时间运行的方法,就无法提供视觉更新。如果你认为不是这样,我怀疑你误解了 BeginInvoke 的工作原理:它在另一个线程上启动委托。实际上,你在问题中包含的调用 Application.DoEvents 的示例代码在 exec.BeginInvokeexec.EndInvoke 之间重复地从 GUI 线程中调用,GUI 线程本来就在更新。(如果你发现不是这样的情况,我怀疑是因为你立即调用了 exec.EndInvoke,它阻塞了当前线程,直到方法完成。)

所以,是的,你需要使用 BackgroundWorker

你也可以使用BeginInvoke,但是不要从 GUI 线程中调用 EndInvoke(如果方法未完成,则会阻塞它),而是将 AsyncCallback 参数传递给你的BeginInvoke 调用(而不是只传递null),并在回调函数中关闭进度窗体。但是,请注意,如果这样做,你必须从 GUI 线程中调用关闭进度窗体的方法,否则你将尝试从非 GUI 线程关闭窗体,这是不可行的。但是实际上,使用 BackgroundWorker 类已经为你处理了所有使用BeginInvoke/EndInvoke时可能遇到的问题,即使你认为它是“.NET魔术代码”(对我来说,它只是一种直观和有用的工具)。


这是唯一有意义的解释。我会等一段时间再关闭。非常感谢。 - Priyank Bolia
好的,我正在使用VB.net,我认为很糟糕的是,仅因为我想在运行一些耗时的函数时显示一个加载窗体,我必须重构整个代码。例如,我已经有了100个经过充分测试的耗时函数,我真的不想动它们。我应该能够在它们之前调用类似于BusyStart()的函数,在它们之后调用类似于BusyStop()的函数。但是我猜这在VB中是不可能的,因为我必须将整个代码移入BackgroundWorkers中。 - Pedro Moreira
1
@PedroMoreira 你可以编写一个帮助函数来调用你已经测试过的方法。这样,你仍然可以使用现有的测试/框架,并且仍然可以使用BackgroundWorkers。 - Brian J

16

对我而言,最简单的方法是使用 BackgroundWorker,它专门设计用于这种任务。 ProgressChanged 事件非常适合更新进度条,无需担心跨线程调用。


1
但与Begin和EndInvoke相比,这需要大量的额外工作。请参见我在问题中的更新。 - Priyank Bolia
好的,你不一定需要声明成员变量,而且你可以使用lambda表达式作为事件处理程序... - Thomas Levesque
1
这是我最喜欢的组件之一! - Gabriel Mongeon

11

这里有关于.NET/C#线程的大量信息可以在Stackoverflow上找到,但是对我来说解决了Windows Forms线程问题的文章是我们的专家Jon Skeet的"Threading in Windows Forms"

整个系列都值得阅读,以便巩固您的知识或从头开始学习。

我很着急,给我一些代码看看

就“给我看代码”而言,以下是我使用C# 3.5的方法。表单包含4个控件:

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

buttonAnother仅用于演示当计数到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<int>(Update1), new object[] { i });
            return;
        }

        progressBar1.Value = i;
    }
}

说实话,这与其他答案没有什么不同,你只是使用了Thread而不是BackgroundWorker,但我不认为这与我的问题有关。 - Priyank Bolia
@Chris S 谢谢! - EasyE

9

还有一个例子,说明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);
                    }
                }));
        }
    }
}

并不印象深刻,问题明确说明“我没有收到进度通知”,这意味着不会调用ProgressChanged。 - Priyank Bolia
@Priyank Bolia:您可以从工作线程中调用后台工作者的“ReportProgress”函数以调用ProgressChanged。在工作开始之前,还要确保“WorkerReportsProgress”为true,就像Oliver在他的示例中所展示的那样。http://msdn.microsoft.com/en-us/library/ka89zff4.aspx - YotaXP
@Priyank Bolia:目前不理解您所说的“不会有任何对ProgressChanges的调用”。您是指我的示例吗?如果是,请取消注释.ReportProgress行。如果您是指您的代码,请启用WorkerReportsProgress并在DoWork()中调用worker.ReportProgress() - Oliver
@Priyank Bolia:更新了示例以更好地报告进度。 - Oliver

3

确实,你正在正确的轨道上。你应该使用另一个线程,并且你已经确定了最佳方法。其余的只是更新进度条。如果你不想像其他人建议的那样使用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属性会在除了拥有该窗体的线程之外的所有线程上返回trueInvoke方法将在UI线程上调用方法,并阻塞直到它完成。如果您不想阻塞,可以使用BeginInvoke代替。

2
“BackgroundWorker”不是答案,因为可能我无法获得进度通知……如果您的长时间运行任务没有可靠的机制来报告其进度,那么就没有可靠地报告其进度的方法。最简单的报告长时间运行方法进度的方法是在UI线程上运行该方法,并通过更新进度条并调用“Application.DoEvents()”来报告进度。这个技术上可行,但在调用“Application.DoEvents()”之间,UI将无响应。这是快速而肮脏的解决方案,正如Steve McConnell所观察到的,快速而肮脏的解决方案的问题在于肮脏的苦涩感仍然存在于快速甜美的背后。接下来最简单的方法,正如另一个帖子所暗示的,是实现一个模态窗体,该窗体使用“BackgroundWorker”来执行长时间运行的方法。这提供了更好的用户体验,并且使您无需解决在长时间运行任务执行时要保留哪些UI部分功能的潜在复杂问题 - 在模态窗口打开时,您的UI的其余部分都不会对用户操作作出响应。这是快速和干净的解决方案。但它仍然相当不友好。它仍然在长时间运行任务执行时锁定UI;它只是以一种漂亮的方式执行。要创建用户友好的解决方案,您需要在另一个线程上执行任务。最简单的方法是使用“BackgroundWorker”。这种方法会带来很多问题。它不会“泄漏”,无论那意味着什么。但是,无论长时间运行的方法正在做什么,现在它都必须完全与其余部分保持隔离,而这些部分在其运行时仍然启用。而且,我指的是完整的。如果用户可以单击鼠标上的任何位置并导致对您的长时间运行方法曾经查看的某个对象进行某些更新,则会出现问题。您的长时间运行的方法使用的任何对象都可能引发事件,这是通向痛苦的潜在道路。这将是所有痛苦的根源,而不是让“BackgroundWorker”正常工作。

这么长的注释,对我来说毫无用处,我什么时候说过“可靠的机制来报告进度”?我只需要使用Application.DoEvents(),这样跑马灯进度条就能工作了。而且,并不是所有的长时间操作都可以从另一个线程调用,比如在另一个线程上调用SAPI时会遇到很多问题。 - Priyank Bolia
你的问题不够明确。除非你更具体地描述,否则你将得不到有用的答案。 - Robert Rossney

1

这里是另一个使用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


1

我必须提供最简单的答案。您可以始终实现进度条,与任何实际进度无关。只需开始填充进度条,例如每秒1%或每秒10%,无论哪种方式看起来类似于您的操作,如果它填满并重新开始。

这至少会给用户处理的外观,并使他们理解等待而不是仅单击按钮然后看到什么都没有发生,然后再次单击它。


4
这是我所知道的最令人讨厌的用户体验之一:一个进度条会欺骗你需要等待多久。 - Robert Rossney
从评论中可以看出,@Robert似乎对所提出的问题的操作状态几乎一无所知,因此这个回答可能是最有用的。但是,看着一个进度条等待它填满并看到它重新开始确实令人沮丧。当然,你可以循环使用颜色或其他东西,至少让它有一些不同之处,看起来像它真正达到了第二阶段等。 - Chris Marisic
用户体验往往与程序的实际操作无关,完全是关于程序的主观看法。 - Chris Marisic

0

我们使用带有BackgroundWorker的模态表单来实现这样的事情。

以下是快速解决方案:

  public class ProgressWorker<TArgument> : BackgroundWorker where TArgument : class 
    {
        public Action<TArgument> Action { get; set; }

        protected override void OnDoWork(DoWorkEventArgs e)
        {
            if (Action!=null)
            {
                Action(e.Argument as TArgument);
            }
        }
    }


public sealed partial class ProgressDlg<TArgument> : Form where TArgument : class
{
    private readonly Action<TArgument> action;

    public Exception Error { get; set; }

    public ProgressDlg(Action<TArgument> 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<string>(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<TArgument> {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<string>(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();

你有一个示例应用程序吗?我很难理解这段代码,ProgressWorker是什么? - Priyank Bolia
哦,对不起。ProgressWorker 是一个带有 Action 构造参数的 BackgroundWorker。我将修改示例。 - Sergey Mirvoda

0
使用BackgroundWorker组件,它专门为这种情况设计。您可以连接到其进度更新事件并更新进度条。BackgroundWorker类确保回调被编排到UI线程中,因此您不需要担心任何细节。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接