¿Visualiza la barra de progreso mientras haces algo de trabajo en C #?

Quiero mostrar una barra de progreso mientras trabajo, pero eso colgaría la interfaz de usuario y la barra de progreso no se actualizará.

Tengo un WinForm ProgressForm con una ProgressBar que continuará indefinidamente en forma de marquesina .

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

Ahora hay muchas maneras de resolver el problema, como usar BeginInvoke , esperar a que la tarea se complete y llamar a EndInvoke . O usando BackgroundWorker o Threads .

Estoy teniendo problemas con EndInvoke, aunque esa no es la pregunta. La pregunta es cuál es la mejor y la forma más sencilla de usar para manejar tales situaciones, donde tiene que mostrarle al usuario que el progtwig está funcionando y que no responde, y cómo maneja eso con el código más simple posible que sea eficiente y no se pueda ganar. Tiene fugas y puede actualizar la GUI.

Al igual que BackgroundWorker necesita tener múltiples funciones, declarar variables miembro, etc. También necesita mantener una referencia al Formulario de ProgressBar y deshacerse de él.

Editar : BackgroundWorker no es la respuesta porque es posible que no reciba la notificación de progreso, lo que significa que no habrá llamadas a ProgressChanged ya que DoWork es una sola llamada a una función externa, pero debo mantener la llamada a la Application.DoEvents(); para la barra de progreso para seguir girando.

La recompensa es por la mejor solución de código para este problema. Solo necesito llamar a Application.DoEvents() para que la barra de progreso de Marque funcione, mientras que la función de trabajador funciona en el hilo principal y no devuelve ninguna notificación de progreso. Nunca necesité .NET magic code para informar el progreso automáticamente, solo necesitaba una solución mejor que:

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

que mantiene viva la barra de progreso (significa que no se congela pero actualiza la marca)

Me parece que estás operando al menos con una suposición falsa.

1. No es necesario que levante el evento ProgressChanged para tener una interfaz de usuario receptiva

En tu pregunta, dices esto:

BackgroundWorker no es la respuesta porque es posible que no reciba la notificación de progreso, lo que significa que no habrá llamadas a ProgressChanged ya que DoWork es una sola llamada a una función externa. . .

En realidad, no importa si llama al evento ProgressChanged o no . El objective de este evento es transferir el control de forma temporal al hilo de la GUI para realizar una actualización que refleje de alguna manera el progreso del trabajo realizado por BackgroundWorker . Si simplemente está mostrando una barra de progreso de marquesina, en realidad sería inútil plantear el evento ProgressChanged . La barra de progreso continuará girando mientras se muestre porque BackgroundWorker está haciendo su trabajo en un subproceso separado de la GUI .

(En una nota lateral, DoWork es un evento, lo que significa que no es solo “una sola llamada a una función externa”, puede agregar tantos controladores como desee y cada uno de esos manejadores puede contener tantas llamadas de funciones como le gusta.)

2. No necesita llamar a Application.DoEvents para tener una interfaz de usuario receptiva

Para mí, parece que crees que la única forma de que la GUI se actualice es llamando a Application.DoEvents :

Necesito mantener la llamada a Application.DoEvents (); para la barra de progreso para seguir girando.

Esto no es cierto en un escenario multiproceso ; si utiliza un BackgroundWorker , la GUI continuará siendo receptiva (en su propio hilo) mientras que BackgroundWorker hace lo que se haya conectado a su evento DoWork . A continuación se muestra un ejemplo simple de cómo esto podría funcionar para usted.

 private void ShowProgressFormWhileBackgroundWorkerRuns() { // this is your presumbly long-running method Action 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 (ie, 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. No puede ejecutar dos métodos al mismo tiempo en el mismo hilo

Tú dices esto:

Solo necesito llamar a Application.DoEvents () para que la barra de progreso de Marque funcione, mientras que la función de trabajador funciona en el hilo principal. . .

Lo que estás pidiendo simplemente no es real . El hilo “principal” para una aplicación de Windows Forms es el hilo de la GUI, que, si está ocupado con su método de larga ejecución, no proporciona actualizaciones visuales. Si crees lo contrario, sospecho que no entiendes lo que hace BeginInvoke : lanza un delegado en un hilo separado . De hecho, el código de ejemplo que ha incluido en su pregunta para llamar a Application.DoEvents entre exec.BeginInvoke y exec.EndInvoke es redundante; en realidad está llamando a Application.DoEvents repetidamente desde el hilo de la GUI, que se estaría actualizando de todos modos . (Si descubres lo contrario, sospecho que es porque exec.EndInvoke inmediato, lo que bloqueó el hilo actual hasta que el método finalizó).

Entonces sí, la respuesta que estás buscando es usar un BackgroundWorker .

Puede utilizar BeginInvoke , pero en lugar de llamar a EndInvoke desde el hilo de la GUI (que lo bloqueará si el método no está terminado), pase un parámetro AsyncCallback a su llamada a BeginInvoke (en lugar de simplemente pasar null ) y cierre el formulario de progreso en su callback. Tenga en cuenta, sin embargo, que si hace eso, tendrá que invocar el método que cierra el formulario de progreso desde el hilo de la GUI, ya que de lo contrario tratará de cerrar un formulario, que es una función de GUI, desde un hilo que no es GUI. Pero en realidad, todas las trampas de usar BeginInvoke / EndInvoke ya han sido tratadas para ti con la clase BackgroundWorker , incluso si crees que es un “código mágico .NET” (para mí, es solo una herramienta intuitiva y útil).

Para mí, la forma más sencilla es utilizar un BackgroundWorker , que está específicamente diseñado para este tipo de tareas. El evento ProgressChanged está perfectamente preparado para actualizar una barra de progreso, sin preocuparse por las llamadas entre hilos

Hay una gran cantidad de información sobre el enhebrado con .NET / C # en Stackoverflow, pero el artículo que aclaró las formas de Windows para mí fue nuestro oracle residente, “Enhebrado en Windows Forms” de Jon Skeet.

Vale la pena leer toda la serie para repasar tus conocimientos o aprender de cero.

Estoy impaciente, solo muéstrame un código

En cuanto a “muéstrame el código”, a continuación se muestra cómo lo haría con C # 3.5. El formulario contiene 4 controles:

  • un cuadro de texto
  • una barra de progreso
  • 2 botones: “buttonLongTask” y “buttonAnother”

buttonAnother está allí puramente para demostrar que la IU no está bloqueada mientras se está ejecutando la tarea de conteo de 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(Update1), new object[] { i }); return; } progressBar1.Value = i; } } 

Y otro ejemplo de que BackgroundWorker es la forma correcta de hacerlo …

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

De hecho, estás en el camino correcto. Deberías usar otro hilo, y has identificado las mejores maneras de hacerlo. El rest solo está actualizando la barra de progreso. En caso de que no desee utilizar BackgroundWorker como otros han sugerido, hay un truco para tener en cuenta. El truco es que no puede actualizar la barra de progreso del subproceso de trabajo porque la interfaz de usuario solo se puede manipular desde el subproceso de la interfaz de usuario. Entonces usas el método Invoke. Es algo como esto (arregla los errores de syntax tú mismo, solo estoy escribiendo un ejemplo rápido):

 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 propiedad InvokeRequired devolverá true en cada thread, excepto el que posee el formulario. El método Invoke llamará al método en el subproceso UI y se bloqueará hasta que se complete. Si no desea bloquear, puede llamar a BeginInvoke en BeginInvoke lugar.

BackgroundWorker no es la respuesta porque puede ser que no reciba la notificación de progreso …

¿Qué diablos hace el hecho de que no esté recibiendo notificación de progreso tiene que ver con el uso de BackgroundWorker ? Si su tarea de larga ejecución no cuenta con un mecanismo confiable para informar su progreso, no hay manera de informar de manera confiable su progreso.

La forma más sencilla de informar el progreso de un método de ejecución prolongada es ejecutar el método en el subproceso de la interfaz de usuario y hacer que reporte el progreso actualizando la barra de progreso y luego llamando a Application.DoEvents() . Esto, técnicamente, funcionará. Pero la interfaz de usuario no responderá entre llamadas a Application.DoEvents() . Esta es la solución rápida y sucia, y como observa Steve McConnell, el problema con las soluciones rápidas y sucias es que la amargura de la suciedad permanece mucho después de que se olvida la dulzura de la comida rápida.

La siguiente manera más simple, como se alude en otro póster, es implementar un formulario modal que utiliza un BackgroundWorker para ejecutar el método de larga ejecución. Esto proporciona una experiencia de usuario generalmente mejor, y le libera de tener que resolver el problema potencialmente complicado de qué partes de su IU dejan funcional mientras se ejecuta la tarea de ejecución prolongada, mientras que la forma modal está abierta, ninguna parte del rest de su UI responderá a las acciones del usuario. Esta es la solución rápida y limpia.

Pero sigue siendo bastante hostil al usuario. Todavía bloquea la interfaz de usuario mientras se ejecuta la tarea de ejecución prolongada; simplemente lo hace de una manera bonita. Para hacer una solución fácil de usar, debe ejecutar la tarea en otro hilo. La forma más fácil de hacerlo es con un BackgroundWorker .

Este enfoque abre la puerta a muchos problemas. No se “escapará”, sea lo que sea que se supone que significa. Pero, independientemente de lo que haga el método de ejecución prolongada, ahora tiene que hacerlo en completo aislamiento de las piezas de la IU que permanecen habilitadas mientras se está ejecutando. Y por completo, quiero decir completo. Si el usuario puede hacer clic en cualquier lugar con un mouse y ocasionar que se realice alguna actualización a algún objeto que su método de ejecución prolongada alguna vez revise, tendrá problemas. Cualquier objeto que use su método de larga duración y que pueda provocar un evento es un camino potencial hacia la miseria.

Es eso y no hacer que BackgroundWorker funcione correctamente, esa será la fuente de todo el dolor.

Tengo que arrojar la respuesta más simple por ahí. Siempre puede implementar la barra de progreso y no tener ninguna relación con el progreso real. Simplemente empiece a llenar la barra, digamos 1% por segundo, o 10% por segundo, lo que parezca similar a su acción y si se llena para comenzar de nuevo.

Al menos, esto dará al usuario la apariencia de procesamiento y les hará entender que esperen en lugar de simplemente hacer clic en un botón y no ver nada, luego hacer clic en él más.

Aquí hay otro código de ejemplo para usar BackgroundWorker para actualizar ProgressBar , simplemente agregue BackgroundWorker y Progressbar a su formulario principal y use el siguiente código:

 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: del proyecto de código

Utilice el componente BackgroundWorker para el que está diseñado exactamente para este escenario.

Puede conectar sus eventos de actualización de progreso y actualizar su barra de progreso. La clase BackgroundWorker garantiza que las devoluciones de llamada se organicen en el hilo de la interfaz de usuario para que no tenga que preocuparse por ninguno de esos detalles.

Leer sus requisitos de la manera más simple sería mostrar un formulario sin modo y usar un temporizador System.Windows.Forms estándar para actualizar el progreso en el formulario sin modo. Sin hilos, sin posibles pérdidas de memoria.

Como esto solo utiliza el hilo de una interfaz de usuario, también necesitaría llamar a Application.DoEvents () en ciertos puntos durante su procesamiento principal para garantizar que la barra de progreso se actualice visualmente.

Re: Tu edición. Necesita un BackgroundWorker o Thread para hacer el trabajo, pero debe llamar ReportProgress () periódicamente para indicarle a la interfaz de usuario lo que está haciendo. DotNet no puede calcular mágicamente la cantidad de trabajo que ha realizado, por lo que debe decirlo (a) cuál es la cantidad máxima de progreso que alcanzará, y luego (b) aproximadamente 100 veces durante el proceso, dígale qué cantidad estás haciendo. (Si informas progreso menos de 100 veces, la barra de progreso saltará en grandes pasos. Si informas más de 100 veces, estarás perdiendo el tiempo tratando de informar un detalle más fino que el que la barra de progreso mostrará útilmente)

Si su subproceso de interfaz de usuario puede continuar felizmente mientras el trabajador en segundo plano se está ejecutando, entonces su trabajo está hecho.

Sin embargo, de manera realista, en la mayoría de las situaciones donde la indicación de progreso necesita ejecutarse, su IU debe ser muy cuidadosa para evitar una llamada de reentrada. Por ejemplo, si está ejecutando una pantalla de progreso mientras exporta datos, no desea permitir que el usuario empiece a exportar datos nuevamente mientras la exportación está en progreso.

Puedes manejar esto de dos maneras:

  • La operación de exportación comprueba si el trabajador en segundo plano se está ejecutando, y deshabilita la opción de exportación cuando ya está importando. Esto le permitirá al usuario hacer cualquier cosa en su progtwig excepto exportar, esto aún podría ser peligroso si el usuario pudiera (por ejemplo) editar los datos que se están exportando.

  • Ejecute la barra de progreso como una visualización “modal” para que su progtwig vuelva a aparecer “vivo” durante la exportación, pero el usuario no puede hacer nada (que no sea cancelar) hasta que finalice la exportación. DotNet es una basura para apoyar esto, a pesar de que es el enfoque más común. En este caso, debe colocar el subproceso de UI en un bucle de espera ocupado donde llama a Application.DoEvents () para mantener el manejo de mensajes en ejecución (para que la barra de progreso funcione), pero debe agregar un MessageFilter que solo permita su aplicación para responder a eventos “seguros” (por ejemplo, permitiría Paint events para que las ventanas de la aplicación continúen redibujando, pero filtraría los mensajes del mouse y del teclado para que el usuario no pueda hacer nada en el proigram mientras la exportación está en progreso También hay un par de mensajes furtivos que necesitará pasar para permitir que la ventana funcione normalmente, y calcularlos llevará unos minutos. Tengo una lista de ellos en el trabajo, pero no los tengo. a mano aquí, me temo. Son todos los más obvios, como NCHITTEST más un .net disimulado (maliciosamente en el rango WM_USER) que es vital para que esto funcione).

El último “error” con la espantosa barra de progreso de dotNet es que cuando finaliza su operación y cierra la barra de progreso, encontrará que generalmente sale cuando informa un valor como “80%”. Incluso si lo fuerza al 100% y luego espera aproximadamente medio segundo, aún puede no llegar al 100%. Arrrgh! La solución es establecer el progreso al 100%, luego al 99%, y luego volver al 100%: cuando se le dice a la barra de progreso que avance, se anima lentamente hacia el valor objective. Pero si le dices que vaya “hacia atrás”, saltará inmediatamente a esa posición. Así que si lo inviertes momentáneamente al final, puedes obtener que realmente muestre el valor que pediste que se muestre.

Si desea una barra de progreso “rotativa”, ¿por qué no establece el estilo de la barra de progreso en “Marquee” y utiliza un BackgroundWorker para que la interfaz de usuario responda? No logrará una barra de progreso giratoria más fácil que usar el estilo “Marquee” …

Estamos usando formulario modal con BackgroundWorker para tal cosa.

Aquí hay una solución rápida:

  public class ProgressWorker : BackgroundWorker where TArgument : class { public Action Action { get; set; } protected override void OnDoWork(DoWorkEventArgs e) { if (Action!=null) { Action(e.Argument as TArgument); } } } public sealed partial class ProgressDlg : Form where TArgument : class { private readonly Action action; public Exception Error { get; set; } public ProgressDlg(Action 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(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 {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; } } 

Y cómo lo usamos:

 var dlg = new ProgressDlg(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();