Обновление WPF ProgressBar в цикле

Представляю вашему переводную статью, описывающую работу с элементами пользовательского интерфейса в циклах. При необходимости перерисовать пользовательский интерфейс в Windows Forms использовался метод DoEvents(), однако в WPF такой метод не предусмотрен. Как же быть? Решение данной проблемы представлено в статье.

Настоятельно рекомендую заглянуть также и в комментарии к статье — там приведены ценные замечания по данной теме

Долгие годы ProgressBar был очень полезным и простым элементом, использующимся в приложениях Windows Forms. Все, что требовалось от программиста – установить значения Minimum и Maximum и затем последовательно увеличивать значение Value чтобы отобразить ход выполнения задачи. Когда нужно, программист добавлял вызов DoEvents() чтобы позволить форме перерисоваться и обновить progress bar.

WPF progress bar концептуально является тем же самым элементом как и Windows progress bar, однако существует очень важное отличие от стандартной техники кодирования, котороое не позволяет WPF Progress Bar корректно обновляться в процессе работы приложения. Это создает очень неприятный эффкет, особенно учитывая то, что основным назначением progress bar является отображение информации о ходе выполнении задачи.

Во многих ситуациях в коде требуется вычислительные циклы, например при чтении данных из файла. Стандартным способом кодирования является открытие файла, последовательное чтение всего файла в цикле по строкам, символам, байтам в цикле. Progress bar мог бы быть использован для отчета о ходе выполнении такой операции.

Подобные циклы показывают небольшую проблему, возникающую при использовании WPF progress bar где стандантные техники кодирования не обеспечивают тот же самый результат какой мы видим при работе с Windows Forms. Эта статья пытается прояснить ситуацию и демонстрирует каким образом можно использовать WPF ProgressBar в циклах.

WPF ProgressBar имеет метод «SetValue» который используется для обновления текущего значения ProgressBar. Однако вызов этого метода не приводит к перерисовке элемента если вызов метода происходит в работающем цикле. Поэтому необходимо использовать метод Invoke описанный в класс Dispatcher для обновления значения и обновления формы.

Класс Dispatcher обеспечивает сервисы для управления очередями задач для потоков.

Один из аргументов метода Dispatcher.Invoke method — делегат. Как только мы научимся создавать делегат который указывает на нужный нам метод мы можем создать такой делегат, который показывает на метод ProgressBar.SetValue. И делегат и метод должны иметь в точности одинаковую сигнатуру.

 //Создаем Delegate который соответствует
 //методу ProgressBar.SetValue
 private delegate void UpdateProgressBarDelegate(
         System.Windows.DependencyProperty dp, Object value);

 private void Process()
 {
     //Конфигурируем ProgressBar
     ProgressBar1.Minimum = 0;
     ProgressBar1.Maximum = short.MaxValue;
     ProgressBar1.Value = 0;

     //Сохраняем значение ProgressBar
     double value = 0;

     //Создаем новый экземпляр делегата для ProgressBar
     // который показывает на метод ProgressBar.SetValue

     UpdateProgressBarDelegate updatePbDelegate =
      new UpdateProgressBarDelegate(ProgressBar1.SetValue);

     // Цикл, в котором ProgressBar.Value движется
     // к максимальному значению

     do
     {
         value += 1;

         /*Обновляем значение ProgressBar:
             1) Передаем делегата "updatePbDelegate"
                который указывает на метод ProgressBar1.SetValue
             2) Устанавливаем значение приоритета потока диспетчера в "Background"
             3) Передаем массив объектов, в котором лежит нужное свойство
                которое мы обновляем (ProgressBar.ValueProperty) и его новое значение */

           Dispatcher.Invoke(updatePbDelegate,
           System.Windows.Threading.DispatcherPriority.Background,
              new object[] { ProgressBar.ValueProperty, value });
   }
    while (ProgressBar1.Value != ProgressBar1.Maximum);
}

Источник статьи

Обновление WPF ProgressBar в цикле: 8 комментариев

  1. Артур

    У вас тут только непноятно откуда Dispatcher взялся. Вообще конечно такой вызов выглядит немного некрасиво. Лучше уж все это выделить в отдельный метод. Кроме того уже давно изобретены лямбды и создавать свой тип делегать совершенно не обязательно (есть ведь классы Func и Action).

    1. Дмитрий Васильев Автор записи

      Спасибо за ценное замечание. Я поискал примеры — код в примере выше может быть записан примерно таким образом:
      Dispatcher.Invoke(new Action((p,v)=>progressBar1.Value = v), progressBar1, value);

  2. Артур

    Вообще использование DoEvents() — это очень плохой стиль. В нормальном WinApp надо писать ровно также как в вашем примере.

  3. Дмитрий

    Дмитрий, хороший блог, добавил в закладки, спасибо за работу.
    Есть еще один прекрасный способ работы с ProgressBar, речь идет о INotifyPropertyChanged и давно известным классом System.ComponentModel.BackgroundWorker. Идея состоит в том, чтобы изменять привязку данных при выполнении приблизительно вот такой операции _worker.ProgressChanged += new ProgressChangedEventHandler(_worker_ProgressChanged); ,
    тем самым получив бонус в виде 100% отзывчивости в пользовательском интерфейсе. Чтоб не быть многословным, я набросал приложение, которое выполняет асинхронную видимость некоего процесса и сигнализирует о выполнении http://cid-0213b80f89787c58.office.live.com/self.aspx/.Public/WPFProgressBar.rar
    Похожий код работает как DataTemplate ячейки грида и вполне себе успешно справляется с отображением чтения с USB устройства. Выглядит шикарно :). Буду рад, если принесу пользу 🙂

  4. Denis Gladkikh

    Я что-то не до понял или вы выполняете этот цикл в основном потоке? может использовать его в паре с BackgoundWorker и проблем будет меньше?
    Если операция будет действительно долгая и окно будут перекрывать другими — то скорее всего перерисовываться у вас будет скорее только сам ProgressBar, а вот остальные части приложения будут в плохом состоянии.

    1. Дмитрий Васильев Автор записи

      Да, так и есть. В примере цикл выполняется в основном потоке… И на самом деле подобные операции лучше выполнять в отдельном потоке, тут я согласен абсолютно. Но ведь если перенести его в BackgroundWorker суть реализации от этого не поменяется? Для обновления интерфейса нужно вызывать Dispatcher.Invoke и это автор хотел показать в своей статье. 😉

      1. Артем

        Как раз реализация и поменяется. Вам не нужно будет иметь дело с Dispatcher, т.к событие ProgressChanged класса BackgroundWorker вызывается из контекста основного потока, а если быть точным, то из контекста потока создавшего экземпляр BackgroundWorker.

  5. Николай

    >>Для обновления интерфейса нужно вызывать Dispatcher.Invoke и это автор хотел показать в своей статье.

    Для BackgroundWorker’a есть ReportProgress

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *