如何在 UI 线程中工作时更新进度条

Posted

技术标签:

【中文标题】如何在 UI 线程中工作时更新进度条【英文标题】:How to update progress bar while working in the UI thread 【发布时间】:2015-09-03 11:50:30 【问题描述】:

我有一个ProgressBar 和一个TreeView

我用一堆数据填充了TreeView,一旦应用它,我运行TreeViewvisual tree,基本上强制它生成每个TreeViewItems。我希望ProgressBar 显示这是如何进行的。

这是我运行以创建TreeViewItems 的行为代码。一旦ItemsLoaded 属性设置为true,它就会开始处理项目。它反过来更新单例类中的属性以更新进度。

public class TreeViewBehaviors

    public static readonly DependencyProperty ItemsLoadedProperty =
        DependencyProperty.RegisterAttached("ItemsLoaded", typeof(bool), typeof(TreeViewBehaviors),
        new FrameworkPropertyMetadata(false, new PropertyChangedCallback(OnItemsLoadedPropertyChanged)));

    public static bool GetItemsLoaded(DependencyObject obj)
    
        return (bool)obj.GetValue(ItemsLoadedProperty);
    

    public static void SetItemsLoaded(DependencyObject obj, bool value)
    
        obj.SetValue(ItemsLoadedProperty, value);
    

    private static void OnItemsLoadedPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    
        if ((bool)e.NewValue)
        
            GetTotalNTreeViewItems((TreeView)sender, sender);
        
    

    public static readonly DependencyProperty NodesProcessedProperty =
        DependencyProperty.RegisterAttached("NodesProcessed", typeof(int), typeof(TreeViewBehaviors),
        new FrameworkPropertyMetadata(default(int), new PropertyChangedCallback(OnNodesProcessedPropertyChanged)));

    public static int GetNodesProcessed(DependencyObject obj)
    
        return (int)obj.GetValue(NodesProcessedProperty);
    

    public static void SetNodesProcessed(DependencyObject obj, int value)
    
        if (GetNodesProcessed(obj) != value)
        
            obj.SetValue(NodesProcessedProperty, value);
        
    

    private static void OnNodesProcessedPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    
        if (e.NewValue != null)
        
            double trouble = Math.Round(((GetProgressMaximum(sender) / GetTotalNodesToProcess(sender)) * (int)e.NewValue), 1);
            TreeViewSingletonClass.Instance.DisplayProgress = trouble;
        
    

    public static readonly DependencyProperty TotalNodesToProcessProperty =
        DependencyProperty.RegisterAttached("TotalNodesToProcess", typeof(double), typeof(TreeViewBehaviors),
        new FrameworkPropertyMetadata(default(double)));

    public static double GetTotalNodesToProcess(DependencyObject obj)
    
        return (double)obj.GetValue(TotalNodesToProcessProperty);
    

    public static void SetTotalNodesToProcess(DependencyObject obj, double value)
    
        obj.SetValue(TotalNodesToProcessProperty, value);
    


    public static readonly DependencyProperty ProgressMaximumProperty =
        DependencyProperty.RegisterAttached("ProgressMaximum", typeof(double), typeof(TreeViewBehaviors),
        new FrameworkPropertyMetadata(default(double)));

    public static double GetProgressMaximum(DependencyObject obj)
    
        return (double)obj.GetValue(ProgressMaximumProperty);
    

    public static void SetProgressMaximum(DependencyObject obj, double value)
    
        obj.SetValue(ProgressMaximumProperty, value);
    

    private static void GetTotalNTreeViewItems(ItemsControl container, DependencyObject sender)
    
        if (container != null)
        
            container.ApplyTemplate();
            ItemsPresenter itemsPresenter = (ItemsPresenter)container.Template.FindName("ItemsHost", container);
            if (itemsPresenter != null)
            
                itemsPresenter.ApplyTemplate();
            
            else
            
                // The Tree template has not named the ItemsPresenter, 
                // so walk the descendents and find the child.
                itemsPresenter = FindVisualChild<ItemsPresenter>(container);
                if (itemsPresenter == null)
                
                    container.UpdateLayout();
                    itemsPresenter = FindVisualChild<ItemsPresenter>(container);
                
            

            Panel itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);

            // Ensure that the generator for this panel has been created.
            UIElementCollection children = itemsHostPanel.Children;
            for (int i = 0, count = container.Items.Count; i < count; i++)
            
                TreeViewItem subContainer = (TreeViewItem)container.ItemContainerGenerator.ContainerFromIndex(i);
                GetTotalNTreeViewItems(subContainer, sender);
                SetNodesProcessed(sender, GetNodesProcessed(sender) + 1);
            
        
    

    private static T FindVisualChild<T>(Visual visual) where T : Visual
    
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
        
            Visual child = (Visual)VisualTreeHelper.GetChild(visual, i);
            if (child != null)
            
                T correctlyTyped = child as T;
                if (correctlyTyped != null)
                    return correctlyTyped;

                T descendent = FindVisualChild<T>(child);
                if (descendent != null)
                    return descendent;
            
        
        return null;
    

单例类

public class TreeViewSingletonClass : INotifyPropertyChanged

    private static double m_DisplayProgress = 0;
    public double DisplayProgress
    
        get  return m_DisplayProgress; 
        set
        
            if (m_DisplayProgress == value)
                return;
            m_DisplayProgress = value;
            NotifyPropertyChanged();
        
    

    private static TreeViewSingletonClass m_Instance;
    public static TreeViewSingletonClass Instance
    
        get
        
            if (m_Instance == null)
                m_Instance = new TreeViewSingletonClass();
            return m_Instance;
        
    

    private TreeViewSingletonClass()

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
    
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    

XAML:

<ProgressBar Grid.Column="2" Grid.Row="1" Margin="5" 
             Width="20" Height="150" 
             VerticalAlignment="Top" 
             Value="Binding Source=x:Static helpers:TreeViewSingletonClass.Instance, Path=DisplayProgress" 
             Maximum="Binding ProgressMaximum"  />

我的问题是,一切都得到了正确处理,只是 ProgressBar 直到最后才更新。我意识到两者都在同一个UI thread 上内联工作,所以这将是问题所在。

所以我的问题是,这两个都在同一个线程上工作,我怎样才能让这个ProgressBar 更新。

[编辑]

这个 WPF 是 UserControl 中的 WinForm ElementHost,我只是将以下内容放入 WinForm 以便我可以访问 Application.Current

if ( null == System.Windows.Application.Current )

   new System.Windows.Application();

在尝试实施 Xavier 的第二个建议后:将工作拆分为更小的部分,并使用 BeginInvoke 将这些部分与调度程序单独排队(例如,将循环体转换为调度程序调用)

所以在for 循环中我坚持了以下内容:

for (int i = 0, count = container.Items.Count; i < count; i++)

    Application.Current.Dispatcher.BeginInvoke(new Action(delegate()
    
        TreeViewItem subContainer = (TreeViewItem)container.ItemContainerGenerator.ContainerFromIndex(i);
        GetTotalNTreeViewItems(subContainer, sender);
        SetNodesProcessed(sender, GetNodesProcessed(sender) + 1); 
    ));

不幸的是,这没有奏效,一定是做错了什么。

【问题讨论】:

【参考方案1】:

WPF 中的 UI 线程使用 Dispatcher 来安排和处理 UI 的所有更新。调度程序基本上维护一个任务队列以在线程上运行。如果你独占了线程,队列只会备份,直到你有机会再次运行。

您的问题有多种可能的解决方案。这是一对...

在单独的线程上工作

我可能首先考虑的解决方案是将长时间运行的任务转移到另一个线程,而不是接管 UI 线程。您需要从该线程对 UI 进行的任何更新都可以通过使用 BeginInvoke 方法通过 UI 线程的 Dispatcher 来完成。例如,如果我想将进度条的值加 1,我可能会这样做:

Dispatcher.BeginInvoke(new Action(delegate()  mProgress.Value += 1.0; ));

注意:确保您的工作线程能够从 UI 线程引用调度程序。不要从工作线程调用Dispatcher.CurrentDispatcher,否则您将获得该线程的调度程序,它无法访问 UI。相反,您可以将调度程序传递给线程,或通过从 UI 线程设置的成员或属性访问它。

使用 Dispatcher 共享 UI 线程

如果您出于某种原因确实想在 UI 线程上执行所有工作(如果您正在执行大量可视化树遍历或其他以 UI 为中心的任务,您可能会这样做),请考虑以下其中一项:

将工作分成更小的部分,并使用BeginInvoke 将这些部分单独排列到调度程序中。确保优先级足够低,以使 UI 更新不会一直等到结束。例如:

for (int i = 0; i < 100; ++i)

    Dispatcher.CurrentDispatcher.BeginInvoke(new Action(delegate()
    
        mProgress.Value += 1.0;
        // Only sleeping to artificially simulate a long running operation
        Thread.Sleep(100);
    ), DispatcherPriority.Background);

在长时间运行的操作期间根据需要处理调度程序队列。在PushFrame 方法文档的“备注”部分中有一个为此目的创建DoEvents 方法的示例。

【讨论】:

所以我可能需要一点帮助,如果我使用“将工作拆分成更小的部分并排队”。请给我一个例子。 我也忘了提到这个 WPF 是 WinForm ElementHost 中的一个 UserControl,这有什么不同吗? 我在答案中添加了一个示例。您不需要Application 实例来访问调度程序。只需导入System.Windows.Threading 命名空间并使用Dispatcher.CurrentDispatcher。我第一次忘记提到的一件事是,您需要指定足够低的DispatcherPriority,以确保任何 UI 更新都不会在等待所有操作完成时卡住。 DispatcherPriority.Background 通常很好。 P.S.我不知道 WinForms ElementHost 内部的控件是否有所作为。这不是我曾经试图处理的事情。 似乎没有什么不同,效果很好,谢谢您的解决方案!

以上是关于如何在 UI 线程中工作时更新进度条的主要内容,如果未能解决你的问题,请参考以下文章

多线程任务更新 1 个进度条 - UI C# WPF

pyqt使用线程更新进度条

更新原始活动中的集成进度条

mfc编写一个flash播放器slider进度条的程序,在新建线程中,怎么实现进度条的更新

android:异步任务asyncTask介绍及异步任务下载图片(带进度条)

QWidget::repaint:更新进度条时检测到递归重绘