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

时间:2009-12-23 11:21:41

标签: c# multithreading .net-3.5 begininvoke

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

我有一个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进度条可以工作,而worker函数在Main线程中工作,并且它不会返回任何进度通知。我从不需要.NET魔术代码来自动报告进度,我只需要一个比以下更好的解决方案:

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

使进度条保持活动状态(意味着不会冻结但刷新品牌)

13 个答案:

答案 0 :(得分:43)

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

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进度条可以正常工作   工作者功能在Main中工作   线程。 。

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

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

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

答案 1 :(得分:16)

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

答案 2 :(得分:10)

在Stackoverflow上有a load of information about threading和.NET / C#,但是清除窗体形式线程的文章是我们的常驻神谕,Jon Skeet的"Threading in Windows Forms"

整个系列值得一读,以提升你的知识或从头学习。

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

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

  • 一个文本框
  • 进度条
  • 2个按钮:“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;
    }
}

答案 3 :(得分:8)

另一个例子是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);
                    }
                }));
        }
    }
}

答案 4 :(得分: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

答案 5 :(得分:2)

  

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

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

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

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

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

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

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

答案 6 :(得分:1)

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

这将至少为用户提供处理的外观并让他们理解等待,而不是仅仅点击按钮,看到没有任何反应,然后点击更多。

答案 7 :(得分: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;
}
}

refrence:from codeproject

答案 8 :(得分:0)

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

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

答案 9 :(得分: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();

答案 10 :(得分:0)

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

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

答案 11 :(得分:0)

回复:你的编辑。 您需要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% - 当进度条被告知向前移动时,它会慢慢向目标值设置动画。但如果你告诉它“向后”,它会立即跳到那个位置。因此,通过在结束时暂时将其反转,您可以让它实际显示您要求它显示的值。

答案 12 :(得分:0)

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