Подтвердить что ты не робот

Показывать индикатор выполнения при выполнении некоторой работы на С#?

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

У меня есть 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);

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

4b9b3361

Ответ 1

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

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

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

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

Собственно, не имеет значения, вызываете ли вы событие ProgressChanged или нет. Вся цель этого события состоит в том, чтобы временно передать управление обратно в поток GUI, чтобы сделать обновление, которое каким-то образом отражает ход работы, выполняемой BackgroundWorker. Если вы просто показываете индикатор выполнения marquee, на самом деле было бы бессмысленно поднимать событие 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 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 будет работать, пока рабочая функция работает в основном нить.,.

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

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

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

Ответ 2

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

Ответ 3

Там загрузка информации о потоковой передаче с помощью .NET/С# в Stackoverflow, но статья, которая очистила окна для обработки потоков, для меня была нашим резидентным оракулом, Джон Скит "Threading in Windows Forms" .

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

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

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

  • текстовое поле
  • индикатор прогресса
  • 2 кнопки: "buttonLongTask" и "buttonAnother"

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

Ответ 4

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

Ответ 5

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

Ответ 6

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

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

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

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

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

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

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

Ответ 7

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

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

Ответ 8

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

Ответ 9

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

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

Ответ 10

Мы используем модальную форму с 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();

Ответ 11

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

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

Ответ 12

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

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

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

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

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

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

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

Ответ 13

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