Afficher la barre de progression tout en faisant du travail en C#?

je veux afficher une barre de progression tout en faisant du travail, mais cela suspendrait L'UI et la barre de progression ne sera pas mise à jour.

j'ai un ProgressForm WinForm avec un ProgressBar qui continuera indéfiniment dans un marquee mode.

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

maintenant, il y a plusieurs façons de résoudre le problème, comme utiliser BeginInvoke , attendre que la tâche soit terminée et appeler EndInvoke . Ou en utilisant le BackgroundWorker ou Threads .

j'ai quelques problèmes avec L'EndInvoke, bien que ce ne soit pas la question. La question Est de savoir quelle est la meilleure et la manière la plus simple que vous utilisez pour gérer de telles situations, où vous devez montrer à l'utilisateur que le programme fonctionne et ne répond pas, et comment gérer cela avec le code le plus simple possible qui est efficace et ne fuira pas, et peut mettre à jour l'interface graphique.

Comme BackgroundWorker d'avoir de multiples fonctions, déclarer variables membres, etc. Aussi, vous devez alors tenir une référence à la forme ProgressBar et se débarrasser de lui.

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 d'appel à ProgressChanged comme le DoWork est un appel unique à une fonction externe, mais je dois garder l'appel le Application.DoEvents(); pour la barre de progression pour continuer à tourner.

le bounty est pour la meilleure solution de code pour ce problème. J'ai juste besoin d'appeler Application.DoEvents() pour que la barre de progression de Marque fonctionne, alors que la fonction worker fonctionne dans le thread principal, et qu'elle ne renvoie aucune notification de progrès. Je n'ai jamais eu besoin du code magique .NET pour rendre compte automatiquement des 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 en vie (signifie Ne pas geler mais rafraîchit la marque)

26
demandé sur combo_ci 2009-12-23 14:21:41

13 réponses

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

1. Vous n'avez pas besoin d'élever l'événement modifié de progrès pour avoir un UI responsive

dans votre question vous dites ceci:

BackgroundWorker n'est pas la réponse parce que c'est peut-être que je ne comprends pas notification de progrès, ce qui signifie il n'y aurait pas d'appel à la ProgressChanged que l' DoWork est un seul appel à une fonction externe . . .

en fait, peu importe que vous appeliez l'événement ProgressChanged ou non . Le but de cet événement est de transférer temporairement le contrôle sur le thread GUI pour faire une mise à jour qui reflète en quelque sorte l'état d'avancement du travail effectué par le BackgroundWorker . si vous affichez simplement une barre de progression marquee, il serait en fait inutile de lever l'événement ProgressChanged du tout . La barre de progression continue de tourner aussi longtemps qu'elle est affichée parce que le BackgroundWorker fait son travail sur un fil séparé du GUI .

(sur une note latérale, DoWork est un événement, ce qui signifie qu'il n'est pas juste "un seul appel à une fonction externe"; vous pouvez ajouter autant de gestionnaires que vous voulez; et chacun de ces gestionnaires peut contenir autant de fonctions les appels qu'elle aime.)

2. Vous n'avez pas besoin d'appeler l'Application.DoEvents d'avoir une réponse de l'INTERFACE utilisateur

pour moi, il semble que vous croyez que la seule façon pour L'interface graphique à mettre à jour est en appelant Application.DoEvents :

j'ai besoin de garder appelez le Application.DoEvents(); pour la barre de progression pour continuer à tourner.

scénario multithread ; si vous utilisez un BackgroundWorker , L'interface graphique continuera d'être responsive (sur son propre thread) tandis que le BackgroundWorker fait 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

vous dites ceci:

j'ai juste besoin d'appeler Application.DoEvents () de sorte que le Marque progress bar fonctionnera, tandis que la fonction de travailleur fonctionne dans le fil. . .

ce que vous demandez est tout simplement pas réel . Le thread "principal" pour une application Windows Forms est le thread GUI, qui, s'il est occupé avec votre méthode de longue durée, ne fournit pas de mises à jour visuelles. Si vous croyez le contraire, je soupçonne que vous vous méprenez sur ce que BeginInvoke fait: 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 fil GUI, qui serait mise à jour de toute façon . (Si vous avez trouvé le contraire, je soupçonne que c'est parce que vous avez appelé exec.EndInvoke tout de suite, qui a bloqué le fil courant jusqu'à la fin de la méthode.)

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

You could use BeginInvoke , mais au lieu d'appeler EndInvoke à partir du thread GUI (qui le bloquera si la méthode n'est pas terminée), passez un paramètre AsyncCallback à votre appel BeginInvoke (au lieu de simplement passer null ), et fermez le formulaire de progression dans votre callback. Sachez, cependant, que si vous faites cela, vous allez devoir invoquer le méthode qui ferme la forme de progression à partir du thread GUI, sinon vous essaierez de fermer une forme, qui est une fonction GUI, à partir d'un thread non-GUI. Mais vraiment, tous les pièges de l'utilisation BeginInvoke / EndInvoke ont déjà été traités avec pour vous avec la classe BackgroundWorker , même si vous pensez que c'est ".net magic code" (pour moi, c'est juste un outil intuitif et utile).

42
répondu Dan Tao 2009-12-30 02:31:53

Pour moi, le plus simple est certainement d'utiliser un BackgroundWorker , qui est spécifiquement conçu pour ce genre de tâche. L'événement ProgressChanged est parfaitement adapté pour mettre à jour une barre de progression, sans se soucier des appels de fils croisés

16
répondu Thomas Levesque 2009-12-23 11:27:52

il y a une charge d'informations sur le filetage avec .NET/C# sur Stackoverflow, mais l'article qui a nettoyé les formes de windows threading pour moi était notre oracle résident, Jon Skeet " filetage dans les formes de fenêtres " .

toute la série vaut la peine d'être lue pour parfaire vos connaissances ou apprendre à partir de zéro.

je suis impatient, montre moi un peu de code

autant Que "montrer moi le code " va, ci-dessous est comment je le ferais avec C# 3.5. Le formulaire contient 4 contrôles:

    "1519150920 une" zone de texte
  • progressbar
  • 2 boutons: "buttonLongTask" et "buttonAnother"

buttonAnother est là purement pour démontrer que L'UI n'est pas bloqué pendant que la tâche compte-à-100 est en cours.

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
répondu Chris S 2017-05-23 12:09:39

et un autre exemple que le travailleur de fond 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);
                    }
                }));
        }
    }
}
8
répondu Oliver 2009-12-29 10:10:24

en Effet, vous êtes sur la bonne voie. Vous devriez utiliser un autre fil, et vous avez identifié les meilleures façons de le faire. Le reste n'est qu'une mise à jour de la barre de progression. Dans le cas où vous ne voulez pas utiliser BackgroundWorker comme d'autres l'ont suggéré, il y a un truc à garder à l'esprit. L'astuce est que vous ne pouvez pas mettre à jour la barre de progression à partir du thread worker car L'UI ne peut être manipulée qu'à partir du thread UI. Vous utilisez donc la méthode Invoke. Il va quelque chose comme ceci (corriger les erreurs de syntaxe vous-même, j'écris juste 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 retournera true sur chaque fil sauf celui qui possède le formulaire. La méthode Invoke appellera la méthode sur le thread de L'interface utilisateur, et bloquera jusqu'à ce qu'elle soit terminée. Si vous ne voulez pas bloquer, vous pouvez appeler BeginInvoke à la place.

3
répondu Vilx- 2009-12-23 11:32:02

BackgroundWorker n'est pas la réponse parce qu'il se peut que je n'obtienne pas la notification de progrès...

qu'est-ce que le fait que vous ne receviez pas de notification de progrès a à voir avec l'utilisation de BackgroundWorker ? Si votre tâche de longue date ne dispose pas d'un mécanisme fiable pour rendre compte de ses progrès, il n'y a aucun moyen de rendre compte de manière fiable de ses progrès.

la façon la plus simple possible de rendre compte des progrès d'un long-running method est d'exécuter la méthode sur le thread D'UI et de lui faire rapport de progrès en mettant à jour la barre de progression et en appelant ensuite Application.DoEvents() . Ce sera, techniquement, le travail. Mais L'interface utilisateur ne répondra pas entre les appels à Application.DoEvents() . C'est la solution rapide et sale, et comme le remarque Steve McConnell, le problème avec les solutions rapides et Sales est que l'amertume de la sale reste longtemps après la douceur du rapide est oublié.

le suivant le plus simple manière, comme indiqué par une autre affiche, est d'implémenter une forme modale qui utilise un BackgroundWorker pour exécuter la méthode à long terme. Cela fournit généralement une meilleure expérience utilisateur, et cela vous libère d'avoir à résoudre le problème potentiellement compliqué de quelles parties de votre UI laisser fonctionnelle pendant que la tâche à long terme est l'exécution-alors que la forme modale est ouverte, aucun du reste de votre UI ne répondra aux actions de l'utilisateur. C'est rapide et propre solution.

Mais c'est quand même assez hostile aux utilisateurs. Il bloque toujours L'interface utilisateur pendant que la tâche à long terme est en cours d'exécution; il le fait simplement d'une jolie manière. Pour faire une solution conviviale, vous devez exécuter la tâche dans un autre thread. La façon la plus facile de le faire est avec un BackgroundWorker .

Cette approche ouvre la porte à beaucoup de problèmes. Ça ne "fuira" pas, peu importe ce que ça veut dire. Mais quelle que soit la méthode utilisée de longue date, elle doit maintenant le faire en totale isolement par rapport à les pièces de L'interface qui restent activées pendant qu'elle est en cours d'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 vers un objet que votre méthode de longue durée ne regarde jamais, vous aurez des problèmes. Tout objet que votre méthode de longue durée utilise qui peut soulever un événement est une route potentielle à la misère.

C'est ça, et ne pas obtenir BackgroundWorker pour fonctionner correctement, ça va être la source de toute la douleur.

2
répondu Robert Rossney 2009-12-28 19:33:41

j'ai jeter la réponse la plus simple là-bas. Vous pouvez toujours juste mettre en œuvre la barre de progrès et n'avoir aucun rapport avec quoi que ce soit de progrès réel. Il suffit de commencer à remplir la barre dire 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 leur fera comprendre d'attendre au lieu de simplement cliquer sur un bouton et de voir rien se produire puis en cliquant il plus.

1
répondu Chris Marisic 2009-12-28 19:04:03

voici un autre exemple de code pour utiliser BackgroundWorker pour mettre à jour ProgressBar , il suffit d'ajouter BackgroundWorker et Progressbar à votre formulaire principal et utiliser 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éference: from codeproject

1
répondu Hamid 2017-07-27 19:25:04

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

vous pouvez vous connecter à ses événements progress update et mettre à jour votre barre de progression. La classe BackgroundWorker garantit que les callbacks sont placés dans le thread de L'interface utilisateur, de sorte que vous n'avez pas à vous soucier de ces détails non plus.

0
répondu Paolo 2009-12-23 11:27:05

lire vos exigences la façon la plus simple serait d'afficher une forme sans mode et d'utiliser un système standard.Windows.Forms timer pour mettre à jour la progression sur le mode-less form. Pas de fils, pas de possibles fuites de mémoire.

comme ceci n'utilise qu'un seul thread, vous devez aussi appeler Application.DoEvents () à certains moments de votre traitement principal pour garantir que la barre de progression est mise à jour visuellement.

0
répondu Ash 2009-12-23 11:35:17

Re: Votre montage. 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 QU'il fait. DotNet ne peut pas calculer comme par magie la quantité de travail que vous avez fait, vous devez donc lui dire (a) Quel est le montant maximum de progrès que vous atteindrez, et (b) environ 100 fois au cours du processus, lui dire quel montant vous êtes prêt. (Si vous signalez des progrès moins de 100 fois, la barre de progression sautera par grands pas. Si vous rapport de plus de 100 fois, vous serez juste de gaspiller du temps à essayer de rapport d'une manière plus fine que la barre de progression sera utilement affichage)

si votre fil D'UI peut continuer pendant que le travailleur d'arrière-plan est en cours d'exécution, alors votre travail est fait.

cependant, de façon réaliste, dans la plupart des situations où l'indication de progrès doit être en cours d'exécution, votre UI doit être très prudent pour éviter un appel de rentrée. par exemple, si vous utilisez un affichage de progression en exportant des données, vous ne voulez pas permettre à l'utilisateur de recommencer à exporter des données alors que l'exportation est en cours.

Vous pouvez gérer cela de deux façons:

  • 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 alors qu'il importe déjà. Cela permettra à l'utilisateur de faire n'importe quoi dans votre programme sauf exporter - cela pourrait encore être dangereux si l'utilisateur pourrait (par exemple) de modifier les données exportées.

  • exécutez la barre de progression comme un affichage "modal" de sorte que votre programme se réalimente "alive" pendant l'exportation, mais l'utilisateur ne peut pas réellement faire quoi que ce soit (autre que d'annuler) jusqu'à ce que l'exportation complète. DotNet est nul à soutenir cela, même si c'est l'approche la plus courante. Dans ce cas, vous devez mettre le fil UI dans une boucle d'attente occupée où il appelle Application.DoEvents () pour garder le message gestion de l'exécution (de sorte que la barre de progression fonctionnera), mais vous devez ajouter un filtre de messagerie qui ne permet à votre application de répondre à des événements "sûrs" (par exemple, il permettrait aux événements de peinture de sorte que votre application windows continue à redessiner, mais il filtrerait les messages de la souris et du clavier de sorte que l'utilisateur ne peut pas réellement faire quoi que ce soit dans le proigram pendant que l'exportation est en cours. Il ya aussi un couple de messages sournois que vous aurez besoin de passer à travers pour permettre à la fenêtre de fonctionner comme d'habitude, et de calcul ces de prendre quelques minutes - j'ai une liste d'entre eux au travail, mais ne pas les avoir à part ici, je le crains. Ce sont tous les évidents comme NCHITTEST plus un .net un sournois (evilly dans la gamme WM_USER) qui est essentiel pour obtenir ce fonctionnement).

le dernier" gotcha "avec l'horrible barre de progression dotNet est que lorsque vous terminez votre opération et fermez la barre de progression, vous constaterez qu'elle disparaît généralement lorsque vous déclarez une valeur comme"80%". Même si vous le forcer à 100% et attendez environ une demi-seconde, il pourrait ne pas atteindre les 100%. Arrrgh! La solution est de définir l'état d'avancement à 100%, à 99%, puis à 100% - lorsque la barre de progression est dit d'aller de l'avant, il s'anime lentement vers la valeur cible. Mais si vous lui dites d'aller "vers l'arrière", il saute immédiatement à cette position. Par l'inverser momentanément à la fin, vous pouvez l'obtenir pour réellement montrer la valeur que vous avez demandé.

0
répondu Jason Williams 2009-12-28 18:55:22

si vous voulez une barre de progression" rotative", pourquoi ne pas mettre le style de la barre de progression à" Marquee "et utiliser un BackgroundWorker pour garder L'UI responsive? Vous n'obtiendrez pas une barre de progression rotative plus facile que d'utiliser le style "Marquee"...

0
répondu Thorsten Dittmar 2009-12-30 12:31:24

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

Voici la 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
répondu Sergey Mirvoda 2017-07-27 18:39:58