Индикатор прогресса при работе в C#?

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

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

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 для автоматического отчета о прогрессе, мне просто нужно было лучшее решение, чем:

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

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

13 ответов


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

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

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

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

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

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

для меня это звучит как будто ты в это веришь!--46-->только способ обновления GUI-это вызов Application.DoEvents:

мне нужно продолжать звонить Приложение.DoEvents (); для индикатор выполнения для вращения.

это неверно в многопоточном сценарии; если вы используете BackgroundWorker, GUI будет продолжать реагировать (на свой собственный поток), в то время как 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 будет работать, в то время как функция worker работает в основном нитка. . .

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

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

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


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


здесь нагрузка информации о threading С .NET / C# на Stackoverflow, но статья, которая очистила потоки windows forms для меня, была нашим резидентом oracle, Jon Skeet's "резьба в Windows Forms".

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

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

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

  • поле
  • компонента 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 в каждом потоке, кроме того, который владеет формой. The Invoke метод вызовет метод в потоке пользовательского интерфейса и будет блокировать до его завершения. Если вы не хотите блокировать, вы можете позвонить .


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

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

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

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

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

справочные:из codeproject


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

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


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

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


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

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

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

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

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

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

последний " gotcha "с ужасным индикатором выполнения 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();