【问题标题】:Display progress bar while doing some work in C#?在 C# 中做一些工作时显示进度条?
【发布时间】:2010-12-29 11:03:53
【问题描述】:

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

我有一个带有ProgressBar 的 WinForm ProgressForm,它将无限期地以 ma​​rquee 方式继续。

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

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

我在 EndInvoke 方面遇到了一些问题,但这不是问题所在。问题是哪种是处理此类情况的最佳和最简单的方法,您必须向用户展示程序正在运行且不会无响应,以及如何使用最简单的代码来处理它,该代码可能高效且成功' t 泄漏,并且可以更新 GUI。

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

编辑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?
  • @Priyank - BackgroundWorker 如果您没有收到进度更新通知,您打算如何更新进度条?
  • @Paolo:它的商标,无限轮换,我只希望用户正在进行一些处理。
  • 如果您的进度条只是在旋转直到您的后台任务完成,为什么还要为所有的复杂性而烦恼呢?只需在 UI 线程上运行动画:如果您使用任何合理的方法启动后台任务,则后台任务不会阻塞 UI。在完成通知时终止动画。
  • @Godeke:我想在主线程上运行所谓的后台任务,而不是使用任何合理的方法,如线程、BackgroundWorker、SyncronizationContext。

标签: c# multithreading .net-3.5 begininvoke


【解决方案1】:

在我看来,您至少基于一个错误的假设进行操作。

1。您无需引发 ProgressChanged 事件即可获得响应式 UI

在你的问题中你这样说:

BackgroundWorker 不是答案 因为这可能是我没有得到 进度通知,这意味着 不会有人打电话给 ProgressChanged 作为 DoWork 是 对外部函数的单次调用。 . .

其实是否调用ProgressChanged事件并不重要。该事件的全部目的是将控制权暂时转移回 GUI 线程以进行更新,以某种方式反映 BackgroundWorker 正在完成的工作的进度。 如果您只是显示一个选取框进度条,那么引发ProgressChanged 事件实际上是毫无意义的。只要进度条显示,它就会继续旋转,因为BackgroundWorker 在与 GUI 不同的线程上工作

(顺便说一句,DoWork 是一个事件,这意味着它只是“对外部函数的一次调用”;您可以添加任意数量的处理程序;每个处理程序都可以包含任意数量的函数调用。)

2。您无需调用 Application.DoEvents 即可获得响应式 UI

在我看来,您认为唯一 GUI 更新的方法是调用Application.DoEvents

我需要继续打电话给 应用程序.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 函数在 Main 中工作 线 。 . .

您要求的是根本不是真实的。 Windows 窗体应用程序的“主”线程是 GUI 线程,如果它忙于您的长时间运行的方法,则它不提供视觉更新。如果您不这么认为,我怀疑您误解了 BeginInvoke 的作用:它在单独的线程上启动委托。实际上,您在问题中包含在exec.BeginInvokeexec.EndInvoke 之间调用Application.DoEvents 的示例代码是多余的;您实际上是从 GUI 线程反复调用 Application.DoEvents无论如何都会更新。 (如果你发现不是这样,我怀疑是因为你立即调用了exec.EndInvoke,它阻塞了当前线程,直到方法完成。)

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

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

【讨论】:

  • 这是唯一对我有意义的解释。我会等一段时间,然后关闭。非常感谢。
  • 好吧,我正在使用 VB.net,我认为这真的很糟糕,因为我想在一些耗时的函数运行时显示加载表单,我必须重组整个代码。例如,我有 100 个耗时的功能已经过良好测试,我真的不想碰它们。我应该能够在它们之前调用 BusyStart() 函数,在它们之后调用 BusyStop() 函数。但我想这在 VB 中是不可能的,因为我必须将整个代码移到 BackgroundWorkers 中。
  • @PedroMoreira 您可以创建一个辅助函数来调用您经过充分测试的方法。这样,您仍然可以使用现有的测试/框架,并且仍然可以使用 BackgroundWorker。
【解决方案2】:

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

【讨论】:

  • 但与 Begin 和 EndInvoke 相比,这需要大量额外的工作。在问题中查看我的更新。
  • 好吧,你没有来声明一个成员变量,你可以使用 lambda 表达式来处理事件...
  • 这是我最喜欢的组件之一!
【解决方案3】:

在 Stackoverflow 上有带有 .NET/C# 的 a load of information about threading,但为我清理 Windows 窗体线程的文章是我们的常驻预言机,Jon Skeet 的 "Threading in Windows Forms"

整个系列值得一读,以提高您的知识或从头开始学习。

我不耐烦了,给我看看代码

就“向我展示代码”而言,以下是我将如何使用 C# 3.5 进行操作。该表单包含 4 个控件:

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

buttonAnother 纯粹是为了证明在 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<int>(Update1), new object[] { i });
            return;
        }

        progressBar1.Value = i;
    }
}

【讨论】:

  • 老实说,这与其他答案没有什么不同,您只是使用了 Thread 而不是 BackgroundWorker,但我认为这与我的问题无关。
  • @Chris S 谢谢!
【解决方案4】:

还有一个例子,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:您从工作线程调用后台工作人员的“ReportProgress”函数以调用 ProgressChanged。还要确保在工作开始之前 'WorkerReportsProgress' 为真,正如 Oliver 在他的示例中所示。 msdn.microsoft.com/en-us/library/ka89zff4.aspx
  • @Priyank Bolia:目前不明白您对 不会调用 ProgressChanges 的意思。你是说我的例子吗?如果是,只需取消注释 .ReportProgress 行。如果您的意思是您的代码,请启用WorkerReportsProgress 并在您的DoWork() 中调用worker.ReportProgress()
  • @Priyank Bolia:更新示例以更好地报告进度
【解决方案5】:

确实,您走在正确的轨道上。您应该使用另一个线程,并且您已经确定了执行此操作的最佳方法。剩下的只是更新进度条。如果您不想像其他人建议的那样使用 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代替。

【讨论】:

    【解决方案6】:

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

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

    报告长时间运行的方法进度的最简单方法是在 UI 线程上运行该方法,并通过更新进度条然后调用 Application.DoEvents() 让它报告进度。从技术上讲,这将起作用。但在调用Application.DoEvents() 之间,UI 将无响应。这是快速而肮脏的解决方案,正如史蒂夫·麦康奈尔(Steve McConnell)所观察到的,快速而肮脏的解决方案的问题在于,在快速的甜蜜被遗忘之后,肮脏的苦涩仍然存在很长时间。

    正如另一位发帖人所暗示的,下一个最简单的方法是实现一个模态表单,该表单使用BackgroundWorker 来执行长时间运行的方法。这提供了通常更好的用户体验,并且它使您不必解决潜在的复杂问题,即当长时间运行的任务正在执行时,您的 UI 的哪些部分可以保留功能 - 当模态表单打开时,其余部分都没有您的 UI 将响应用户操作。这是快速干净的解决方案。

    但它仍然对用户充满敌意。当长时间运行的任务正在执行时,它仍然会锁定 UI;它只是以一种漂亮的方式完成。要制作用户友好的解决方案,您需要在另一个线程上执行任务。最简单的方法是使用BackgroundWorker

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

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

    【讨论】:

    • 这么长的注释,但对我没有任何用处,我什么时候说过“报告其进度的可靠机制”我只需要 Application.DoEvents() 以便 Marque 进度条能够工作。也不是所有的长操作都可以从另一个线程调用,就像我在另一个线程上调用 SAPI 时遇到很多问题一样。
    • 您的问题含糊不清。在你更具体之前,你不会得到有用的答案。
    【解决方案7】:

    我必须抛出最简单的答案。您始终可以只实现进度条,而与实际进度无关。刚开始填充条,说每秒 1% 或每秒 10% 任何看起来与你的动作相似的东西,如果它填满重新开始。

    这将至少给用户一个处理的外观,让他们明白等待,而不是仅仅点击一个按钮,看到什么都没有发生,然后再点击它。

    【讨论】:

    • 这是我所知道的最令人讨厌的用户体验之一:一个进度条告诉你你需要等待多长时间。
    • @Robert 来自海报的 cmets,听起来对上述问题中的操作状态几乎一无所知,这听起来可能是最有用的答案。但是,是的,看着一个酒吧,等待它填满并看到它回收是令人沮丧的。当然,您可以循环使用颜色或其他东西,至少让它看起来像是进入第 2 阶段等等。
    • 用户体验很多时候与程序的实际运行无关,完全是关于程序的主观意见。
    【解决方案8】:

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

    参考:from codeproject

    【讨论】:

      【解决方案9】:

      使用专为这种情况设计的 BackgroundWorker 组件。

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

      【讨论】:

        【解决方案10】:

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

        由于这仅使用一个 UI 线程,因此您还需要在主处理期间的某些时间点调用 Application.DoEvents() 以确保进度条在视觉上得到更新。

        【讨论】:

          【解决方案11】:

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

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

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

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

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

          • 将进度条作为“模态”显示运行,以便您的程序在导出期间保持“活动”状态,但在导出完成之前,用户实际上无法执行任何操作(除了取消)。 DotNet 在支持这一点方面是垃圾,尽管它是最常见的方法。在这种情况下,您需要将 UI 线程置于一个繁忙的等待循环中,它会调用 Application.DoEvents() 以保持消息处理的运行(因此进度条将起作用),但您需要添加一个 MessageFilter 只允许您的应用程序响应“安全”事件(例如,它允许 Paint 事件,以便您的应用程序窗口继续重绘,但它会过滤掉鼠标和键盘消息,以便用户在导出过程中实际上无法在程序中执行任何操作. 您还需要传递一些偷偷摸摸的消息才能让窗口正常工作,而弄清楚这些消息需要几分钟时间 - 我有一份工作清单,但没有恐怕要交给这里。这是所有明显的,比如 NCHITTEST 加上一个鬼鬼祟祟的 .net (邪恶地在 WM_USER 范围内),这对于让它工作至关重要)。

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

          【讨论】:

          • 我只需要调用 Application.DoEvents() 以便 Marque 进度条可以工作,而辅助函数在主线程中工作,并且它不会返回任何进度通知。我从来不需要 .NET 魔术代码来自动报告进度,我只需要一个比 BeginInvoke 和 EndInvoke 更好的解决方案来保持进度条处于活动状态(意味着不冻结但刷新标记)
          【解决方案12】:

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

          【讨论】:

            【解决方案13】:

            对于这样的事情,我们使用带有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 是什么?
            • 哦,对不起。 ProgressWorker 是一个以 Action 作为构造函数参数的 BackgroundWorker。我会修改示例。
            猜你喜欢
            • 2013-09-16
            • 1970-01-01
            • 2019-01-09
            • 2014-08-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多