用于更新进度条的专用C#类

本文关键字:专用 更新 用于 | 更新日期: 2023-09-27 18:25:25

我想创建一个专用类来更新我的应用程序中的进度条(在本例中为WPF进度条)。我做了这样的事:

public class ProgressBarUpdate : IDisposable
{
    private readonly double _delta;
    private int _current;
    private int _total;
    private readonly ProgressBar _pb;
    public ProgressBarUpdate(ProgressBar pb, int total)
    {
        _pb = pb;
        _total = total;
        // the pb.Maximum is a double so it doesn`t get truncated
        _delta = _pb.Maximum / total; 
        _current = 0;
        _pb.Visibility = Visibility.Visible;
    }
    public void Dispose()
    {
        _pb.Visibility = Visibility.Collapsed;
        _current = 0;
    }
    public void UpdateProgress()
    {
        _pb.Value =(int)_delta * (++_current);
    }

我这样使用(在UI线程中):

using (var pu = new ProgressBarUpdate(pb, totalCount)
{
  for (x=0; x<totalCount; x++)
  {
     // operations here 
     pu.UpdateProgress()
  }
}

但是UI可能被阻止了,没有正确更新。显示所有进展的最佳方式是什么?

用于更新进度条的专用C#类

Winforms/WPF程序是一个Eventing系统。有一个线程可以连续处理事件队列中的事件。这是它的主要工作,理想情况下这是它唯一应该做的事情。任何类型的UI活动都会在事件队列中生成事件,比如你在窗口上移动鼠标,或者点击某个与你的窗口重叠的东西,然后在它离开重叠位置时再次生成。所有这些事件都由UI线程处理,并使UI始终更新。

此外,Winforms/WPF只允许在UI线程上访问和/或更新控件及其属性,从而使其有必要以线程安全的方式访问和//或更新。

如果您阻塞此UI线程或对其进行其他CPU绑定计算,则您的UI响应性更新行为将受到影响。最坏情况下的UI将冻结。

因此,正确的答案是在另一个工作线程上执行计算循环,并且只通过使用Dispatcher封送对UI线程的调用来更新进度条UI。

然而,为了回答您的问题并满足您的调查,这里有一些事情是可能的,但这是不良做法,您应该永远不要做以下事情

简单地说,当您更新进度条的Value属性时,它会使进度条UI无效——因此,UI必须更新。因此,假设在事件队列中生成一个事件,该事件将导致运行一些代码,这些代码将更新UI。但是,您是在UI线程上的循环中运行的,因此,除非循环结束,否则线程没有机会处理此事件。因此,您看不到任何UI更新。诀窍是在您对进度条的Value进行下一次更新之前,让UI线程处理该事件。您可以通过将优先级较低的项强制调用到事件队列中来实现这一点,以便在进行下一次迭代之前处理正常项和优先级较高的项。

using (var pu = new ProgressBarUpdate(pb, totalCount))
{
  for (int x = 0; x < totalCount ; x++)
  {
     // operations here 
     pu.UpdateProgress();
     Dispatcher.Invoke(DispatcherPriority.Background, new Action(()=>{}));
  }
}

如果您正在UI线程上执行工作并调用UpdateProgress,则在完成工作之前它不会更新,并且UI线程可以执行其他工作(如刷新UI)。所以这永远不会奏效。

如果您在后台线程上进行工作,则需要使用Dispatcher将设置值封送至UI线程。

下面是一个来自http://tech.pro/tutorial/800/working-with-the-wpf-dispatcher

if (!myCheckBox.Dispatcher.CheckAccess())
{
  myCheckBox.Dispatcher.Invoke(
    System.Windows.Threading.DispatcherPriority.Normal,
    new Action(
      delegate()
      {
        myCheckBox.IsChecked = true;
      }
  ));
}
else
{
  myCheckBox.IsChecked = true;
}

试试这个:

public ProgressBarUpdate(ProgressBar pb, int total)
{
    _pb = pb;
    _total = total;
    _delta = _pb.MaxValue/((double)total);  /make sure you do not truncate delta
    _current = 0;
    _pb.Visibility = Visibility.Visible;
}
public void Dispose()
{
    _pb.Visibility = Visibility.Collapsed;
    _current = 0;
}
public void UpdateProgress()
{
    _pb.Value = (int)( _delta * (++_current)); //update after the increment
}

我建议也使用float而不是double

您一直在说要避免使用线程,我想是因为您不希望出现不必要的复杂性,但这真的没什么大不了的。使操作成为多线程操作是一件非常简单的事情。即使是很短很简单的任务,这也是实现你想要的最直接的方法。使用TPL,它看起来像这样:

using System.Threading.Tasks;
...
Task.Factory.StartNew(() => {
    for (...) {
        // operation...
        progressBar.Dispatcher.BeginInvoke(() => progressBar.Value = ...);
    }
});