web-dev-qa-db-fra.com

Afficher la barre de progression tout en effectuant des travaux en C #?

Je souhaite afficher une barre de progression tout en effectuant certains travaux, mais cela bloquerait l'interface utilisateur et la barre de progression ne serait pas mise à jour.

J'ai un WinForm ProgressForm avec un ProgressBar qui continuera indéfiniment de manière Marquee .

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

Il existe maintenant de nombreuses façons de résoudre le problème, comme utiliser BeginInvoke, attendre la fin de la tâche et appeler EndInvoke. Ou en utilisant BackgroundWorker ou Threads.

J'ai des problèmes avec EndInvoke, mais ce n'est pas la question. La question est de savoir quelle est la manière la meilleure et la plus simple que vous utilisez pour gérer de telles situations, où vous devez montrer à l'utilisateur que le programme fonctionne et qu'il ne répond pas, et comment gérez-vous cela avec le code le plus simple possible, efficace et gagné '' t fuite, et peut mettre à jour l'interface graphique.

Comme BackgroundWorker doit avoir plusieurs fonctions, déclarer des variables membres, etc. Vous devez également conserver une référence au formulaire ProgressBar et en disposer.

Edit : BackgroundWorker n'est pas la réponse car il se peut que je ne reçoive pas la notification de progression, ce qui signifie qu'il n'y aurait pas appel à ProgressChanged car le DoWork est un appel unique à une fonction externe, mais je dois continuer d'appeler la Application.DoEvents(); pour que la barre de progression continue de tourner.

La prime est la meilleure solution de code pour ce problème. J'ai juste besoin d'appeler Application.DoEvents() pour que la barre de progression Marque fonctionne, tandis que la fonction de travail fonctionne dans le thread principal et qu'elle ne renvoie aucune notification de progression. Je n'ai jamais eu besoin de code magique .NET pour signaler automatiquement les progrès, j'avais juste besoin d'une meilleure solution que:

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

qui maintient la barre de progression active (signifie ne pas geler mais rafraîchit la marque)

26
Priyank Bolia

Il me semble que vous opérez sur au moins une fausse hypothèse.

1. Vous n'avez pas besoin de déclencher l'événement ProgressChanged pour avoir une interface utilisateur réactive

Dans votre question, vous dites ceci:

BackgroundWorker n'est pas la réponse car il se peut que je ne reçoive pas la notification de progression, ce qui signifie qu'il n'y aurait pas d'appel à ProgressChanged car le DoWork est un seul appel à une fonction externe. . .

En fait, peu importe que vous appeliez ou non l'événement ProgressChanged. Le but de cet événement est de transférer temporairement le contrôle vers le thread GUI pour effectuer une mise à jour qui reflète en quelque sorte la progression du travail effectué par le BackgroundWorker. Si vous affichez simplement une barre de progression Marquee, il serait en fait inutile de déclencher l'événement ProgressChanged. La barre de progression continuera de tourner tant qu'elle est affichée car le BackgroundWorker fait son travail sur un thread séparé de l'interface graphique.

(En passant, DoWork est un événement, ce qui signifie qu'il ne s'agit pas simplement "d'un seul appel à une fonction externe" ; vous pouvez ajouter autant de gestionnaires que vous le souhaitez; et chacun de ces gestionnaires peut contenir autant d'appels de fonction qu'il le souhaite.)

2. Vous n'avez pas besoin d'appeler Application.DoEvents pour avoir une interface utilisateur réactive

Pour moi, il semble que vous pensiez que la seule façon de mettre à jour l'interface graphique consiste à appeler Application.DoEvents:

Je dois continuer d'appeler Application.DoEvents (); pour que la barre de progression continue de tourner.

Ce n'est pas vrai dans un scénario multithread; si vous utilisez un BackgroundWorker, l'interface graphique continuera à répondre (sur son propre thread) tandis que le BackgroundWorker fera tout ce qui a été attaché à son événement DoWork. Voici un exemple simple de la façon dont cela pourrait fonctionner pour vous.

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. Vous ne pouvez pas exécuter deux méthodes en même temps sur le même thread

Tu dis ça:

J'ai juste besoin d'appeler Application.DoEvents () pour que la barre de progression Marque fonctionne, tandis que la fonction de travail fonctionne dans le thread principal. . .

Ce que vous demandez, c'est tout simplement pas réel. Le thread "principal" pour une application Windows Forms est le thread GUI qui, s'il est occupé par votre méthode de longue durée, ne fournit pas de mises à jour visuelles. Si vous croyez le contraire, je soupçonne que vous comprenez mal ce que fait BeginInvoke: il lance un délégué sur un fil séparé. En fait, l'exemple de code que vous avez inclus dans votre question pour appeler Application.DoEvents entre exec.BeginInvoke et exec.EndInvoke est redondant; vous appelez en fait Application.DoEvents à plusieurs reprises à partir du thread GUI, qui serait de toute façon mis à jour. (Si vous avez trouvé le contraire, je suppose que c'est parce que vous avez appelé exec.EndInvoke tout de suite, ce qui a bloqué le thread actuel jusqu'à la fin de la méthode.)

Alors oui, la réponse que vous cherchez est d'utiliser un BackgroundWorker.

Vous pourriez utiliser BeginInvoke, mais au lieu d'appeler EndInvoke à partir du thread GUI (qui le bloquera si la méthode n'est pas terminé), passez un paramètre AsyncCallback à votre appel BeginInvoke (au lieu de simplement passer null) et fermez le formulaire de progression dans votre rappel. Sachez cependant que si vous faites cela, vous devrez invoquer la méthode qui ferme le formulaire de progression à partir du thread GUI, car sinon vous essayerez de fermer un formulaire, qui est une fonction GUI, à partir de un thread non GUI. Mais vraiment, tous les pièges de l'utilisation de BeginInvoke/EndInvoke ont déjà été traités pour vous avec le BackgroundWorker class, même si vous pensez que c'est du "code magique .NET" (pour moi, c'est juste un outil intuitif et utile).

43
Dan Tao

Pour moi, le moyen le plus simple est certainement d'utiliser un BackgroundWorker, spécialement conçu pour ce type de tâche. L'événement ProgressChanged est parfaitement adapté pour mettre à jour une barre de progression, sans se soucier des appels inter-threads

16
Thomas Levesque

Il y a ne charge d'informations sur le filetage avec .NET/C # sur Stackoverflow, mais l'article qui a éclairci le filetage des formulaires Windows pour moi était notre résident Oracle, Jon Skeet's "Filetage dans Windows Forms" .

Toute la série mérite d'être lue pour parfaire vos connaissances ou apprendre à partir de zéro.

Je suis impatient, montre-moi juste du code

En ce qui concerne "montrez-moi le code", voici comment je le ferais avec C # 3.5. Le formulaire contient 4 contrôles:

  • une zone de texte
  • une barre de progression
  • 2 boutons: "buttonLongTask" et "buttonAnother"

buttonAnother sert uniquement à démontrer que l'interface utilisateur n'est pas bloquée pendant l'exécution de la tâche de comptage jusqu'à 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;
    }
}
10
Chris S

Et un autre exemple que BackgroundWorker est la bonne façon de le faire ...

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);
                    }
                }));
        }
    }
}
9
Oliver

En effet, vous êtes sur la bonne voie. Vous devez utiliser un autre fil, et vous avez identifié les meilleures façons de le faire. Le reste ne fait que mettre à jour la barre de progression. Si vous ne souhaitez pas utiliser BackgroundWorker comme d'autres l'ont suggéré, il y a une astuce à garder à l'esprit. L'astuce est que vous ne pouvez pas mettre à jour la barre de progression à partir du thread de travail car l'interface utilisateur ne peut être manipulée qu'à partir du thread d'interface utilisateur. Vous utilisez donc la méthode Invoke. Cela va quelque chose comme ça (corrigez les erreurs de syntaxe vous-même, je ne fais qu'écrire un exemple rapide):

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;
    }
}

La propriété InvokeRequired renverra true sur chaque thread sauf celui qui possède le formulaire. La méthode Invoke appellera la méthode sur le thread d'interface utilisateur et la bloquera jusqu'à ce qu'elle se termine. Si vous ne voulez pas bloquer, vous pouvez appeler BeginInvoke à la place.

3
Vilx-

BackgroundWorker n'est pas la réponse car il se peut que je ne reçoive pas la notification de progression ...

Qu'est-ce que le fait de ne pas recevoir de notification de progression a à voir avec l'utilisation de BackgroundWorker? Si votre tâche de longue durée ne dispose pas d'un mécanisme fiable pour signaler sa progression, il n'y a aucun moyen de signaler de manière fiable sa progression.

La façon la plus simple possible de signaler la progression d'une méthode de longue durée est d'exécuter la méthode sur le thread d'interface utilisateur et de la faire rapporter la progression en mettant à jour la barre de progression, puis en appelant Application.DoEvents(). Cela fonctionnera, techniquement. Mais l'interface utilisateur ne répondra pas entre les appels à Application.DoEvents(). C'est la solution rapide et sale, et comme l'observe Steve McConnell, le problème avec les solutions rapides et sales est que l'amertume du sale reste longtemps après que la douceur du rapide soit oubliée.

La prochaine façon la plus simple, comme le mentionne une autre affiche, consiste à implémenter un formulaire modal qui utilise un BackgroundWorker pour exécuter la méthode longue. Cela offre une expérience utilisateur généralement meilleure et vous évite d'avoir à résoudre le problème potentiellement compliqué des parties de votre interface utilisateur à laisser fonctionnelles pendant l'exécution de la tâche de longue durée - tandis que le formulaire modal est ouvert, aucun du reste de votre interface utilisateur répondra aux actions des utilisateurs. Ceci est la solution rapide et propre.

Mais c'est encore assez hostile aux utilisateurs. Il verrouille toujours l'interface utilisateur pendant l'exécution de la tâche de longue durée; il le fait simplement d'une belle manière. Pour créer une solution conviviale, vous devez exécuter la tâche sur un autre thread. La façon la plus simple de le faire est d'utiliser un BackgroundWorker.

Cette approche ouvre la porte à de nombreux problèmes. Il ne "fuira" pas, quoi que cela soit censé signifier. Mais quelle que soit la méthode de longue durée, elle doit maintenant le faire en totale isolation des éléments de l'interface utilisateur qui restent activés pendant son exécution. Et par complet, je veux dire complet. Si l'utilisateur peut cliquer n'importe où avec une souris et provoquer une mise à jour d'un objet que votre méthode de longue date examine, vous aurez des problèmes. Tout objet utilisé par votre méthode de longue date qui peut déclencher un événement est un chemin potentiel vers la misère.

C'est cela, et ne pas faire fonctionner BackgroundWorker correctement, ce sera la source de toute la douleur.

2
Robert Rossney

Je dois jeter la réponse la plus simple. Vous pouvez toujours simplement implémenter la barre de progression et n'avoir aucune relation avec quoi que ce soit de progrès réel. Commencez simplement à remplir la barre, dites 1% par seconde, ou 10% par seconde, ce qui semble similaire à votre action et si elle se remplit pour recommencer.

Cela donnera au moins à l'utilisateur l'apparence du traitement et lui fera comprendre d'attendre au lieu de simplement cliquer sur un bouton et de ne rien voir puis de le cliquer davantage.

1
Chris Marisic

Voici un autre exemple de code à utiliser BackgroundWorker pour mettre à jour ProgressBar, ajoutez simplement BackgroundWorker et Progressbar à votre formulaire principal et utilisez le code ci-dessous:

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;
}
}

réfrence: du codeproject

1
Hamid

Pour lire vos besoins, la manière la plus simple consiste à afficher un formulaire sans mode et à utiliser un minuteur System.Windows.Forms standard pour mettre à jour la progression du formulaire sans mode. Aucun thread, aucune fuite de mémoire possible.

Comme cela n'utilise qu'un seul thread d'interface utilisateur, vous devrez également appeler Application.DoEvents () à certains moments de votre traitement principal pour garantir que la barre de progression est mise à jour visuellement.

0
Ash

Si vous voulez une barre de progression "tournante", pourquoi ne pas définir le style de la barre de progression sur "Marquee" et utiliser un BackgroundWorker pour garder l'interface utilisateur réactive? Vous ne réaliserez pas une barre de progression rotative plus facilement qu'avec le style "Marquee" ...

0
Thorsten Dittmar

Utilisez le composant BackgroundWorker pour lequel il est conçu exactement pour ce scénario.

Vous pouvez vous connecter à ses événements de mise à jour de progression et mettre à jour votre barre de progression. La classe BackgroundWorker garantit que les rappels sont organisés sur le thread d'interface utilisateur afin que vous n'ayez pas à vous soucier de ces détails non plus.

0
Paolo

Nous utilisons la forme modale avec BackgroundWorker pour une telle chose.

Voici une solution rapide:

  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;
    }
}

Et comment nous l'utilisons:

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();
0
Sergey Mirvoda

Re: Votre édition. Vous avez besoin d'un BackgroundWorker ou d'un Thread pour faire le travail, mais il doit appeler ReportProgress () périodiquement pour dire au thread de l'interface utilisateur ce qu'il fait. DotNet ne peut pas par magie déterminer la quantité de travail que vous avez fait, vous devez donc lui dire (a) quel est le montant de progression maximum que vous atteindrez, puis (b) environ 100 fois environ au cours du processus, dites quel montant vous êtes à la hauteur. (Si vous signalez une progression moins de 100 fois, la barre de progression sautera par grandes étapes. Si vous signalez plus de 100 fois, vous perdrez simplement du temps à essayer de signaler un détail plus fin que la barre de progression n'affichera utilement)

Si votre thread d'interface utilisateur peut continuer avec bonheur pendant que le travailleur en arrière-plan est en cours d'exécution, votre travail est terminé.

Cependant, de manière réaliste, dans la plupart des situations où l'indication de progression doit être en cours d'exécution, votre interface utilisateur doit être très prudente pour éviter un appel rentrant. par exemple. Si vous exécutez un affichage de progression pendant l'exportation des données, vous ne voulez pas autoriser l'utilisateur à recommencer l'exportation des données pendant que l'exportation est en cours.

Vous pouvez gérer cela de deux manières:

  • L'opération d'exportation vérifie si le travailleur d'arrière-plan est en cours d'exécution et a désactivé l'option d'exportation pendant qu'il est déjà en cours d'importation. Cela permettra à l'utilisateur de faire quoi que ce soit dans votre programme, sauf l'exportation - cela pourrait toujours être dangereux si l'utilisateur pouvait (par exemple) modifier les données qui sont exportées.

  • Exécutez la barre de progression en tant qu'affichage "modal" afin que votre programme réamorce "vivant" pendant l'exportation, mais l'utilisateur ne peut rien faire (à part annuler) jusqu'à ce que l'exportation soit terminée. DotNet est un déchet à soutenir cela, même si c'est l'approche la plus courante. Dans ce cas, vous devez placer le thread d'interface utilisateur dans une boucle d'attente occupée où il appelle Application.DoEvents () pour que la gestion des messages continue de fonctionner (pour que la barre de progression fonctionne), mais vous devez ajouter un MessageFilter qui autorise uniquement votre application pour répondre aux événements "sûrs" (par exemple, il autoriserait les événements Paint pour que les fenêtres de votre application continuent de se redessiner, mais il filtrerait les messages de la souris et du clavier afin que l'utilisateur ne puisse rien faire dans le proigramme pendant que l'exportation est en cours) . Il y a aussi quelques messages sournois que vous devrez traverser pour permettre à la fenêtre de fonctionner normalement, et les résoudre prendra quelques minutes - j'en ai une liste au travail, mais je ne les ai pas à portée de main ici, j'ai peur. Ce sont tous les éléments évidents comme NCHITTEST plus un .net sournois (mal dans la gamme WM_USER) qui est essentiel pour que cela fonctionne).

Le dernier "piège" avec la barre de progression horrible dotNet est que lorsque vous avez terminé votre opération et que vous fermez la barre de progression, vous constaterez qu'elle se ferme généralement lorsque vous signalez une valeur comme "80%". Même si vous le forcez à 100% puis attendez environ une demi-seconde, il peut ne pas atteindre 100%. Arrrgh! La solution consiste à définir la progression à 100%, puis à 99%, puis à 100% - lorsque la barre de progression est invitée à avancer, elle s'anime lentement vers la valeur cible. Mais si vous lui dites d'aller "en arrière", il saute immédiatement à cette position. Donc, en l'inversant momentanément à la fin, vous pouvez le faire afficher la valeur que vous lui avez demandé de montrer.

0
Jason Williams