C#进度条未更新

8
我有一个ProgressBarWindow,里面有一个进度条和一个取消按钮。它用于报告文件I/O的进度。然而,尽管所有的工作都在后台线程中完成,但ProgressBarWindow的UI线程和我的主窗口仍然会挂起。虽然后台线程在执行任务,但进度条以及主窗口并没有更新。以下代码是在主窗口构造函数末尾调用的:
iCountLogLinesProgressBar = new ProgressBarWindow();
iCountLogLinesProgressBar.cancelButton.Click += EventCountLogLinesProgressBarCancelButtonClicked;
iCountLogLinesProgressBar.Show();

iCountLogRecords = new BackgroundWorker();
iCountLogRecords.DoWork += EventCountLogLinesDoWork;
iCountLogRecords.ProgressChanged += EventCountLogLinesProgressChanged;
iCountLogRecords.RunWorkerCompleted += EventCountLogLinesRunWorkerCompleted;
iCountLogRecords.WorkerReportsProgress = true;
iCountLogRecords.WorkerSupportsCancellation = true;
iCountLogRecords.RunWorkerAsync(new BinaryReader(File.Open(iMainLogFilename, FileMode.Open, FileAccess.Read)));

EventCountLogLinesProgressChanged() 的样子如下:

private void EventCountLogLinesProgressChanged(object sender, ProgressChangedEventArgs e)
{
    iCountLogLinesProgressBar.Value = e.ProgressPercentage;
}

这里是ProgressBarWindow的简化版本(其余部分仅包含几个setter):
public partial class ProgressBarWindow : Window
{
    public ProgressBarWindow()
    {
        InitializeComponent();
        this.progressBar.Value = this.progressBar.Minimum = 0;
        this.progressBar.Maximum = 100;
    }

    public double Value
    {
        get
        {
            return progressBar.Value;
        }
        set
        {
            this.progressBar.Value = value;
        }
    }
} 

我尝试将值设置行包装在dispatcher.invoke委托中,但这会导致堆栈溢出(我不应该有一个dispatcher.invoke行,因为backgroundworker在UI线程中调用ProgressChanged,对吗?)。我已经查阅了msdn和谷歌,但似乎没有其他人遇到过这个问题。
编辑:抱歉,我没有意识到我的简化代码会阻塞UI线程,尽管使用了backgroundworker,但我得到了完全相同的行为,因此错误地认为它们是等价的。我应该提到我正在使用backgroundworker :P
2个回答

15

您正在阻塞UI线程,因此在您的循环完成之前,它不会重新呈现UI。

将后台处理移至单独的线程,并使用适当的 Dispatcher 调用(或 BackgroundWorker )将UI更新调用传回UI线程。

如果您的进度条实际上只是一个计时器,那么您可以使用其中一种 Timer 类来更新它。

编辑:好的,现在您已更改代码,我认为它看起来不错。由于您仅在UI线程上更改UI,因此应该是线程安全的。您的后台工作程序是否确实定期报告进度?


我有一个BackgroundWorker,但是我是否还需要一个Dispatcher.Invoke调用来设置我的值? - CalumMcCall
@teflon19:不,你不需要做任何其他的事情 - 除了确保在后台工作器中报告进度。 - Jon Skeet
只有当进度发生变化时,我才报告进度。太多的progress changed事件会减慢速度,但UI仍会冻结。一个ProgressBarWindow的UI线程为什么会受到后台工作器的影响呢? - CalumMcCall
@teflon19:如果您更新UI的频率太高,以至于它实际上没有机会重新绘制,那肯定是个问题。为什么不过滤一下,只有在(比如)四舍五入后的百分比实际变化时才报告进度。这样,您最多可以有100次更新。 - Jon Skeet
是的,我刚刚发现我的用户界面只报告了一个进度更改(进度== 100%)。由于某种原因,它只更新一次。我会仔细看看这个问题。看起来上面的问题有点误导人。无论如何,感谢您的帮助! - CalumMcCall

3

听Jon Skeet的话,或者有时候你可以调用Application.DoEvents()方法在同一个线程中更新所有UI。


2
-1:直接调用Application.DoEvents是不好的编程实践。它会导致UI线程递归地进入其消息泵过程,可能会引起一些严重问题。更好的方法是正确编写应用程序,允许UI线程正常地进行消息泵处理。 - Alex Humphrey
2
它取决于规模。例如,如果是一个小型桌面应用程序,只进行简单的更新,您可以在一个线程中使用Application.DoEvents()花费15分钟编写它,或者将其复杂化,将简单的方法调用更改为方法调用。我只是列举了另一种选择,并指出Jon Skeet的选项在大多数情况下更好。所以我因为列举选项而得到了一个反对票。不错。 - Alex
我认为,在当前形式下,你的答案可能会导致阅读问题的开发人员通过使用Application.DoEvents作为解决UI更新问题的万能方法而养成不良习惯(正如许多人所做的那样)。对于这个问题,我绝对不会使用Application.DoEvents。是的,在这种情况下听Jon Skeet的建议似乎是一个好主意,你也列举了另一种选择,但在我看来,这是一个糟糕的选择。但这只是一个人的观点,反映在你的答案得分中。 - Alex Humphrey
1
抱歉,我只是惯性地认为-1表示整个答案是错误或错误的。我只是不知道列举一个可行但远非最佳选项也可以被视为如此。 - Alex
我看到许多开发人员在这个和其他线程上抛出“调用DoEvents是一个不好的惯例,不要这样做,要做正确的事情”的评论。他们从未解释为什么这是一个不好的惯例,也很少提供实际的解决方案。DoEvents经常对一些人有用,这就是为什么他们这样做的原因。也许应该让像这样简单的东西真正起作用,不应该那么困难。 - Mike K
请不要调用 Application.DoEvents()!它只是为了与VB6向后兼容而存在于框架中。这是引入代码错误的好方法。 - Enigmativity

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