Отображать индикатор выполнения во время выполнения некоторых работ в C#?


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

У меня есть форма WinForm ProgressForm с ProgressBar, которая будет продолжаться бесконечно в виде marquee.

using(ProgressForm p = new ProgressForm(this))
{
//Do Some Work
}
Теперь есть много способов решить проблему, например, используя BeginInvoke, дождаться завершения задачи и вызвать EndInvoke. Или с помощью BackgroundWorker или Threads.

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

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

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

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

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

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

13 26

13 ответов:

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

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

В своем вопросе вы говорите следующее:

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

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

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

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

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

Мне нужно продолжать называть Приложение.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. Вы не можете запустить два метода одновременно на одном и том же нить

Вы говорите так:

Мне просто нужно позвонить. Приложение.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, из потока GUI. не-GUI поток. Но на самом деле, все подводные камни использования BeginInvoke/EndInvoke мы уже имели дело с для вас с классом BackgroundWorker, даже если вы думаете, что это "волшебный код .NET" (для меня это просто интуитивно понятный и полезный инструмент).

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

Есть объемом информации о работе с потоками с .Нетто/в C# на сайте StackOverflow, но статьи, которая прояснилась окна формы резьбонарезные для меня был наш местный оракул, Джон тарелкам "многопоточность в Windows формах".

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

Я нетерпелив, просто покажите мне какой-нибудь код

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

  • текстовое поле
  • A progressbar
  • 2 кнопки: "buttonLongTask " и"buttonAnother"

buttonAnother Есть ли там чисто, чтобы продемонстрировать, что пользовательский интерфейс не блокируется во время выполнения задачи count-to-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;
    }
}

И еще один пример того, что 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);
                    }
                }));
        }
    }
}

Действительно, Вы на правильном пути. Вы должны использовать другую нить, и вы определили лучшие способы сделать это. Остальное-это просто обновление индикатора выполнения. В случае, если вы не хотите использовать 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 вместо этого.

BackgroundWorker это не ответ, потому что может быть, что я не получаю уведомления о ходе выполнения...

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

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

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

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

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

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

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

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

Вот еще один пример кода для использования 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;
}
}

Refrence: from codeproject

Используйте компонент BackgroundWorker, он предназначен именно для этого сценария.

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

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

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

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

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

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

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

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

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

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

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

Мы используем модальную форму с BackgroundWorker для такой вещи.

Вот быстрое решение:

  public class ProgressWorker<TArgument> : BackgroundWorker where TArgument : class 
    {
        public Action<TArgument> Action { get; set; }

        protected override void OnDoWork(DoWorkEventArgs e)
        {
            if (Action!=null)
            {
                Action(e.Argument as TArgument);
            }
        }
    }


public sealed partial class ProgressDlg<TArgument> : Form where TArgument : class
{
    private readonly Action<TArgument> action;

    public Exception Error { get; set; }

    public ProgressDlg(Action<TArgument> action)
    {
        if (action == null) throw new ArgumentNullException("action");
        this.action = action;
        //InitializeComponent();
        //MaximumSize = Size;
        MaximizeBox = false;
        Closing += new System.ComponentModel.CancelEventHandler(ProgressDlg_Closing);
    }
    public string NotificationText
    {
        set
        {
            if (value!=null)
            {
                Invoke(new Action<string>(s => Text = value));  
            }

        }
    }
    void ProgressDlg_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        FormClosingEventArgs args = (FormClosingEventArgs)e;
        if (args.CloseReason == CloseReason.UserClosing)
        {
            e.Cancel = true;
        }
    }



    private void ProgressDlg_Load(object sender, EventArgs e)
    {

    }

    public void RunWorker(TArgument argument)
    {
        System.Windows.Forms.Application.DoEvents();
        using (var worker = new ProgressWorker<TArgument> {Action = action})
        {
            worker.RunWorkerAsync();
            worker.RunWorkerCompleted += worker_RunWorkerCompleted;                
            ShowDialog();
        }
    }

    void worker_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
    {
        if (e.Error != null)
        {
            Error = e.Error;
            DialogResult = DialogResult.Abort;
            return;
        }

        DialogResult = DialogResult.OK;
    }
}

И как мы его используем:

var dlg = new ProgressDlg<string>(obj =>
                                  {
                                     //DoWork()
                                     Thread.Sleep(10000);
                                     MessageBox.Show("Background task completed "obj);
                                   });
dlg.RunWorker("SampleValue");
if (dlg.Error != null)
{
  MessageBox.Show(dlg.Error.Message, "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
dlg.Dispose();