Показывать индикатор выполнения при выполнении какой-либо работы в C #? - PullRequest
26 голосов
/ 23 декабря 2009

Я хочу отображать индикатор выполнения, выполняя некоторую работу, но при этом пользовательский интерфейс зависает, а индикатор выполнения не обновляется.

У меня есть WinForm ProgressForm с ProgressBar, который будет продолжаться бесконечно шатер .

using(ProgressForm p = new ProgressForm(this))
{
//Do Some Work
}

Теперь есть много способов решить проблему, например, используя BeginInvoke, дождитесь завершения задачи и наберите EndInvoke. Или используя BackgroundWorker или Threads.

У меня есть некоторые проблемы с EndInvoke, хотя это не вопрос. Вопрос в том, какой самый лучший и самый простой способ, которым вы пользуетесь в таких ситуациях, когда вы должны показать пользователю, что программа работает и не отвечает, и как вы справляетесь с этим с помощью максимально простого кода, который эффективен и выиграл ». t утечка, и может обновить графический интерфейс.

Как и BackgroundWorker, необходимо иметь несколько функций, объявлять переменные-члены и т. Д. Кроме того, необходимо сохранить ссылку на форму ProgressBar и избавиться от нее.

Редактировать : BackgroundWorker не является ответом, поскольку может случиться так, что я не получу уведомление о прогрессе, что означает, что не будет никакого вызова к ProgressChanged, поскольку DoWork является одиночный вызов внешней функции, но мне нужно продолжать вызывать Application.DoEvents();, чтобы индикатор выполнения продолжал вращаться.

Награда за лучшее решение кода для этой проблемы. Мне просто нужно вызвать Application.DoEvents(), чтобы индикатор выполнения Marque работал, а рабочая функция работала в главном потоке и не возвращала уведомление о ходе выполнения. Мне никогда не требовался магический код .NET для автоматического отчета о прогрессе, мне просто нужно лучшее решение, чем:

Action<String, String> exec = DoSomethingLongAndNotReturnAnyNotification;
IAsyncResult result = exec.BeginInvoke(path, parameters, null, null);
while (!result.IsCompleted)
{
  Application.DoEvents();
}
exec.EndInvoke(result);

, который поддерживает индикатор выполнения (означает не замораживание, а обновление маркера)

Ответы [ 13 ]

43 голосов
/ 30 декабря 2009

Мне кажется, что вы оперируете хотя бы одним ложным предположением.

1. Вам не нужно вызывать событие ProgressChanged, чтобы иметь отзывчивый пользовательский интерфейс

В своем вопросе вы говорите это:

BackgroundWorker не ответ потому что может случиться так, что я не понимаю уведомление о прогрессе, что означает не будет звонка Прогресс изменился, так как DoWork является одиночный вызов внешней функции. , .

На самом деле, не имеет значения, называете ли вы событие ProgressChanged или нет . Целью этого события является временная передача управления обратно в поток графического интерфейса, чтобы сделать обновление, которое каким-то образом отражает ход работы, выполняемой BackgroundWorker. Если вы просто отображаете индикатор прогресса, на самом деле было бы бессмысленно поднимать событие ProgressChanged на всех . Индикатор выполнения будет продолжать вращаться до тех пор, пока он отображается, потому что BackgroundWorker выполняет свою работу в отдельном потоке от графического интерфейса пользователя .

(примечание: DoWork - это событие, которое означает, что это не , а"одиночный вызов внешней функции"; вы можете добавить столько обработчиков, сколько захотите; и каждый из этих обработчиков может содержать столько вызовов функций, сколько ему нужно.)

2. Вам не нужно вызывать Application.DoEvents, чтобы иметь отзывчивый пользовательский интерфейс

Мне кажется, что вы верите, что единственный способ обновления графического интерфейса - это позвонить Application.DoEvents:

Мне нужно продолжать звонить Application.DoEvents (); для индикатор выполнения, чтобы продолжать вращаться.

Это не так в многопоточном сценарии ; если вы используете BackgroundWorker, графический интерфейс будет продолжать реагировать (в своем собственном потоке), в то время как BackgroundWorker делает все, что было присоединено к его событию DoWork. Ниже приведен простой пример того, как это может работать для вас.

private void ShowProgressFormWhileBackgroundWorkerRuns() {
    // this is your presumably long-running method
    Action<string, string> exec = DoSomethingLongAndNotReturnAnyNotification;

    ProgressForm p = new ProgressForm(this);

    BackgroundWorker b = new BackgroundWorker();

    // set the worker to call your long-running method
    b.DoWork += (object sender, DoWorkEventArgs e) => {
        exec.Invoke(path, parameters);
    };

    // set the worker to close your progress form when it's completed
    b.RunWorkerCompleted += (object sender, RunWorkerCompletedEventArgs e) => {
        if (p != null && p.Visible) p.Close();
    };

    // now actually show the form
    p.Show();

    // this only tells your BackgroundWorker to START working;
    // the current (i.e., GUI) thread will immediately continue,
    // which means your progress bar will update, the window
    // will continue firing button click events and all that
    // good stuff
    b.RunWorkerAsync();
}

3. Вы не можете запустить два метода одновременно в одном потоке

Вы говорите это:

Мне просто нужно позвонить Application.DoEvents (), чтобы Marque Progress Bar будет работать, в то время как рабочая функция работает в главном нить . , .

То, что вы просите, это просто не реально . «Основной» поток для приложения Windows Forms - это поток GUI, который, если он занят вашим долгосрочным методом, не предоставляет визуальные обновления. Если вы считаете иначе, я подозреваю, что вы неправильно понимаете, что делает BeginInvoke: он запускает делегата в отдельном потоке . На самом деле, пример кода, который вы включили в свой вопрос для вызова Application.DoEvents между exec.BeginInvoke и exec.EndInvoke, является избыточным; Вы на самом деле звоните Application.DoEvents несколько раз из потока GUI, , который будет обновляться в любом случае . (Если вы нашли иное, я подозреваю, что это потому, что вы сразу вызвали exec.EndInvoke, что блокировало текущий поток до завершения метода.)

Так что да, ответ, который вы ищете, это использовать BackgroundWorker.

Вы можете использовать BeginInvoke, но вместо вызова EndInvoke из потока GUI (который заблокирует его, если метод не завершен), передайте параметр AsyncCallback в BeginInvoke позвоните (вместо того, чтобы просто передать null), и закройте форму прогресса в вашем обратном вызове. Имейте в виду, однако, что если вы сделаете это, вам придется вызывать метод, который закрывает форму прогресса из потока GUI, так как в противном случае вы будете пытаться закрыть форму, которая является функцией GUI, из поток без графического интерфейса. Но на самом деле все подводные камни использования BeginInvoke / EndInvoke уже были рассмотрены с для вас с классом BackgroundWorker, даже если вы думаете, что это «.NET магический код» (для меня, это просто интуитивно понятный и полезный инструмент).

16 голосов
/ 23 декабря 2009

Для меня самый простой способ - использовать BackgroundWorker, который специально разработан для такого рода задач. Событие ProgressChanged идеально подходит для обновления индикатора выполнения, не беспокоясь о вызовах между потоками

10 голосов
/ 23 декабря 2009

В * Stackoverflow имеется масса информации о многопоточности с .NET / C #, но статья, которая очистила многопоточность оконных форм для меня, была нашим резидентным оракулом, "Потоком в Windows Forms Джона Скита «.

Целую серию стоит прочитать, чтобы освежить свои знания или учиться с нуля.

Я нетерпелив, просто покажи мне код

Что касается "покажи мне код", ниже показано, как я могу сделать это с C # 3.5. Форма содержит 4 элемента управления:

  • текстовое поле
  • индикатор выполнения
  • 2 кнопки: «buttonLongTask» и «buttonAnother»

buttonAnother служит для демонстрации того, что пользовательский интерфейс не блокируется во время выполнения задачи count-100.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void buttonLongTask_Click(object sender, EventArgs e)
    {
        Thread thread = new Thread(LongTask);
        thread.IsBackground = true;
        thread.Start();
    }

    private void buttonAnother_Click(object sender, EventArgs e)
    {
        textBox1.Text = "Have you seen this?";
    }

    private void LongTask()
    {
        for (int i = 0; i < 100; i++)
        {
            Update1(i);
            Thread.Sleep(500);
        }
    }

    public void Update1(int i)
    {
        if (InvokeRequired)
        {
            this.BeginInvoke(new Action<int>(Update1), new object[] { i });
            return;
        }

        progressBar1.Value = i;
    }
}
9 голосов
/ 28 декабря 2009

И еще один пример того, что BackgroundWorker - верный способ сделать это ...

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

namespace SerialSample
{
    public partial class Form1 : Form
    {
        private BackgroundWorker _BackgroundWorker;
        private Random _Random;

        public Form1()
        {
            InitializeComponent();
            _ProgressBar.Style = ProgressBarStyle.Marquee;
            _ProgressBar.Visible = false;
            _Random = new Random();

            InitializeBackgroundWorker();
        }

        private void InitializeBackgroundWorker()
        {
            _BackgroundWorker = new BackgroundWorker();
            _BackgroundWorker.WorkerReportsProgress = true;

            _BackgroundWorker.DoWork += (sender, e) => ((MethodInvoker)e.Argument).Invoke();
            _BackgroundWorker.ProgressChanged += (sender, e) =>
                {
                    _ProgressBar.Style = ProgressBarStyle.Continuous;
                    _ProgressBar.Value = e.ProgressPercentage;
                };
            _BackgroundWorker.RunWorkerCompleted += (sender, e) =>
            {
                if (_ProgressBar.Style == ProgressBarStyle.Marquee)
                {
                    _ProgressBar.Visible = false;
                }
            };
        }

        private void buttonStart_Click(object sender, EventArgs e)
        {
            _BackgroundWorker.RunWorkerAsync(new MethodInvoker(() =>
                {
                    _ProgressBar.BeginInvoke(new MethodInvoker(() => _ProgressBar.Visible = true));
                    for (int i = 0; i < 1000; i++)
                    {
                        Thread.Sleep(10);
                        _BackgroundWorker.ReportProgress(i / 10);
                    }
                }));
        }
    }
}
3 голосов
/ 23 декабря 2009

Действительно, вы на правильном пути. Вы должны использовать другой поток, и вы определили лучшие способы сделать это. Остальное просто обновление индикатора выполнения. Если вы не хотите использовать BackgroundWorker, как предлагали другие, есть одна хитрость, о которой следует помнить. Хитрость в том, что вы не можете обновить индикатор выполнения из рабочего потока, потому что пользовательским интерфейсом можно управлять только из потока пользовательского интерфейса. Таким образом, вы используете метод Invoke. Это выглядит примерно так (исправьте синтаксические ошибки самостоятельно, я просто пишу небольшой пример):

class MyForm: Form
{
    private void delegate UpdateDelegate(int Progress);

    private void UpdateProgress(int Progress)
    {
        if ( this.InvokeRequired )
            this.Invoke((UpdateDelegate)UpdateProgress, Progress);
        else
            this.MyProgressBar.Progress = Progress;
    }
}

Свойство InvokeRequired вернет true в каждом потоке, кроме того, который владеет формой. Метод Invoke вызовет метод в потоке пользовательского интерфейса и будет блокироваться до его завершения. Если вы не хотите блокировать, вы можете вместо этого позвонить BeginInvoke.

2 голосов
/ 28 декабря 2009

BackgroundWorker - это не ответ, потому что, возможно, я не получаю уведомление о прогрессе ...

Что на самом деле связано с тем, что вы не получаете уведомление о прогрессе, с использованием BackgroundWorker? Если ваша долгосрочная задача не имеет надежного механизма для отчета о ее ходе, нет способа надежно сообщить о ее ходе.

Самый простой возможный способ сообщить о прогрессе в длительном методе - запустить метод в потоке пользовательского интерфейса и сообщить ему о прогрессе, обновив индикатор выполнения и затем вызвав Application.DoEvents(). Технически это будет работать. Но пользовательский интерфейс будет не отвечать между вызовами на Application.DoEvents(). Это быстрое и грязное решение, и, как заметил Стив Макконнелл, проблема с быстрыми и грязными решениями заключается в том, что горечь грязного сохраняется еще долго после того, как сладость быстрого исчезла.

Следующий простейший способ, на который ссылается другой участник, - реализовать модальную форму, которая использует BackgroundWorker для выполнения долгосрочного метода. Это обеспечивает в целом лучший пользовательский опыт и освобождает вас от необходимости решать потенциально сложную проблему, касающуюся того, какие части вашего пользовательского интерфейса оставить работоспособными во время выполнения долгосрочной задачи - пока модальная форма открыта, ни одна из остальных Ваш пользовательский интерфейс будет реагировать на действия пользователя. Это быстрое и чистое решение.

Но это все еще довольно враждебно для пользователя. Он по-прежнему блокирует пользовательский интерфейс во время выполнения длительной задачи; это просто красиво. Чтобы сделать удобное решение, вам нужно выполнить задачу в другом потоке. Самый простой способ сделать это с помощью BackgroundWorker.

Такой подход открывает двери для множества проблем. Это не "утечка", что бы это ни значило. Но что бы ни делал длительный метод, теперь он должен делать это в полной изоляции от частей пользовательского интерфейса, которые остаются включенными во время его работы. И под полным я подразумеваю полный. Если пользователь может щелкнуть мышью в любом месте и сделать какое-то обновление для какого-либо объекта, который когда-либо просматривал ваш долгосрочный метод, у вас будут проблемы. Любой объект, который использует ваш долгосрочный метод, который может вызвать событие, является потенциальной дорогой к страданию.

Это то, что, если BackgroundWorker не будет работать должным образом, это будет источником всей боли.

1 голос
/ 27 июля 2016

Вот еще один пример кода, который нужно использовать BackgroundWorker для обновления ProgressBar, просто добавьте BackgroundWorker и Progressbar в основную форму и используйте следующий код:

public partial class Form1 : Form
{
    public Form1()
    {
      InitializeComponent();
      Shown += new EventHandler(Form1_Shown);

    // To report progress from the background worker we need to set this property
    backgroundWorker1.WorkerReportsProgress = true;
    // This event will be raised on the worker thread when the worker starts
    backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork);
    // This event will be raised when we call ReportProgress
    backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
}
void Form1_Shown(object sender, EventArgs e)
{
    // Start the background worker
    backgroundWorker1.RunWorkerAsync();
}
// On worker thread so do our thing!
void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    // Your background task goes here
    for (int i = 0; i <= 100; i++)
    {
        // Report progress to 'UI' thread
        backgroundWorker1.ReportProgress(i);
        // Simulate long task
        System.Threading.Thread.Sleep(100);
    }
}
// Back on the 'UI' thread so we can update the progress bar
void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    // The progress percentage is a property of e
    progressBar1.Value = e.ProgressPercentage;
}
}

ссылка: из кода проекта

1 голос
/ 28 декабря 2009

Я должен выбросить самый простой ответ. Вы всегда можете просто реализовать индикатор выполнения и не иметь никакого отношения к чему-либо из реального прогресса. Просто начните заполнять планку, скажем, 1% в секунду, или 10% в секунду, что кажется похожим на ваше действие, и если оно заполняется, чтобы начать снова.

Это, по крайней мере, придаст пользователю вид обработки и заставит его понять, что нужно ждать, вместо того, чтобы просто нажать кнопку и увидеть, что ничего не происходит, а затем щелкнуть ее еще раз.

0 голосов
/ 30 декабря 2009

Если вы хотите «вращающийся» индикатор выполнения, почему бы не установить стиль индикатора выполнения на «Marquee» и использовать BackgroundWorker для обеспечения отзывчивости интерфейса? Вы не достигнете вращающегося индикатора выполнения легче, чем используя стиль "Marquee" ...

0 голосов
/ 28 декабря 2009

Re: Ваше редактирование. Вам нужен BackgroundWorker или Thread для выполнения работы, но он должен периодически вызывать ReportProgress (), чтобы сообщить потоку пользовательского интерфейса, что он делает. DotNet не может волшебным образом определить, сколько работы вы проделали, поэтому вы должны сказать ей (а) каков максимальный прогресс, которого вы достигнете, а затем (б) около 100 или около того раз в течение процесса, скажите это какая сумма вы до. (Если вы сообщаете о прогрессе менее 100 раз, панель прогестов будет скачкообразно перемещаться большими шагами. Если вы сообщите более 100 раз, вы просто напрасно потратите время, пытаясь сообщить более мелкие детали, чем будет полезно отображать индикатор выполнения)

Если ваш поток пользовательского интерфейса может успешно продолжаться, пока работает фоновый работник, тогда ваша работа завершена.

Однако, в действительности, в большинстве ситуаций, когда индикация прогресса должна выполняться, ваш пользовательский интерфейс должен быть очень осторожным, чтобы избежать повторного вызова. например Если вы выполняете отображение прогресса при экспорте данных, вы не хотите, чтобы пользователь мог снова начать экспорт данных во время экспорта.

Вы можете справиться с этим двумя способами:

  • Операция экспорта проверяет, работает ли фоновый работник, и отключила параметр экспорта, когда он уже импортируется. Это позволит пользователю делать что-либо в вашей программе, кроме экспорта, - это может быть опасно, если пользователь может (например) редактировать экспортируемые данные.

  • Запустите индикатор выполнения как «модальное» отображение, чтобы ваша программа «оживала» во время экспорта, но пользователь не может ничего сделать (кроме отмены), пока экспорт не завершится. DotNet - это чушь в поддержке этого, хотя это самый распространенный подход. В этом случае вам нужно поместить поток пользовательского интерфейса в занятый цикл ожидания, где он вызывает Application.DoEvents (), чтобы поддерживать обработку сообщений (так, чтобы индикатор выполнения работал), но вам нужно добавить MessageFilter, который позволяет только вашему приложению реагировать на «безопасные» события (например, это позволило бы событиям Paint, чтобы окна вашего приложения продолжали перерисовываться, но при этом отфильтровывались бы сообщения мыши и клавиатуры, так что пользователь не мог ничего сделать в программе во время экспорта Есть также пара подлых сообщений, через которые вам нужно пройти, чтобы окно работало как обычно, и выяснение этого займет несколько минут - у меня есть список их на работе, но их нет Я боюсь, что здесь есть все очевидные, такие как NCHITTEST плюс подлый .net (злобно в диапазоне WM_USER), который жизненно важен, чтобы это работало).

Последнее «уловка» с ужасным индикатором выполнения dotNet заключается в том, что когда вы закончите свою операцию и закроете индикатор выполнения, вы обнаружите, что она обычно завершается при сообщении значения, такого как «80%». Даже если вы установите его на 100%, а затем подождите около полсекунды, он все равно может не достичь 100%. Arrrgh! Решение состоит в том, чтобы установить прогресс на 100%, затем на 99%, а затем обратно на 100% - когда индикатору прогресса сообщают, что он движется вперед, он медленно анимируется к целевому значению. Но если вы скажете ему идти «назад», он сразу же перейдет к этой позиции. Таким образом, мгновенно изменив его в конце, вы можете заставить его фактически отобразить значение, которое вы просили показать.

...