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

How to update progress bar while working in the UI thread

我有一个ProgressBar和一个TreeView

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

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

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;
    }
}

单例Class

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 是 WinForm ElementHost 中的一个 UserControl,我只是将以下内容放入 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); 
    }));
}

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

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

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

在单独的线程上工作

我可能首先考虑的解决方案是将您的长运行ning 任务移至另一个线程,而不是接管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);
    }
    
  • 在长时间 运行ning 操作期间根据需要处理调度程序队列。在 PushFrame 方法文档的 "Remarks" 部分中有一个为此目的创建 DoEvents 方法的示例。