Mostrar barra de progreso mientras se hace algún trabajo en C#?
multithreading .net-3.5 (13)
BackgroundWorker
no es la respuesta porque puede ser que no reciba la notificación de progreso ...
¿Qué diablos tiene que ver el hecho de que no está recibiendo notificación de progreso con el uso de BackgroundWorker
? Si su tarea de larga duración no tiene un mecanismo confiable para informar su progreso, no hay manera de informar su progreso de manera confiable.
La forma más sencilla posible de informar el progreso de un método de larga duración es ejecutar el método en el subproceso de la interfaz de usuario y hacer que informe 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 las 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 lo sucio permanece mucho después de que se olvida la dulzura de lo rápido.
La siguiente forma más simple, como se alude en otro póster, es implementar una forma modal que use un BackgroundWorker
para ejecutar el método de larga ejecución. Esto proporciona una mejor experiencia de usuario en general, y le libera de tener que resolver el problema potencialmente complicado de qué partes de su interfaz de usuario para dejar en funcionamiento mientras se ejecuta la tarea de larga duración, mientras que la forma modal está abierta, ninguno del resto. Su interfaz de usuario responderá a las acciones del usuario. Esta es la solución rápida y limpia.
Pero sigue siendo bastante hostil para el 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, necesita 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 "filtrará", lo que se supone que significa eso. Pero sea lo que sea lo que esté haciendo el método de ejecución prolongada, ahora tiene que hacerlo en completo aislamiento de las partes de la interfaz de usuario que permanecen habilitadas mientras se está ejecutando. Y por completo, quiero decir completo. Si el usuario puede hacer clic en cualquier lugar con el mouse y ocasionar que se realice alguna actualización en algún objeto que su método de larga duración jamás haya visto, tendrá problemas. Cualquier objeto que utilice su método de larga duración que pueda provocar un evento es un camino potencial hacia la miseria.
Es eso, y no lograr que BackgroundWorker
funcione correctamente, será la fuente de todo el dolor.
Quiero mostrar una barra de progreso mientras hago algún trabajo, pero eso bloquearía la IU y la barra de progreso no se actualizará.
Tengo un WinForm ProgressForm con un ProgressBar
que continuará indefinidamente de manera marcada .
using(ProgressForm p = new ProgressForm(this))
{
//Do Some Work
}
Ahora hay muchas maneras de resolver el problema, como usar BeginInvoke
, esperar a que se complete la tarea y llamar a EndInvoke
. O utilizando el BackgroundWorker
o Threads
.
Tengo algunos problemas con EndInvoke, aunque esa no es la cuestión. La pregunta es cuál es la mejor y la forma más sencilla que usa para manejar tales situaciones, donde tiene que mostrarle al usuario que el programa está funcionando y no deja de responder, y cómo maneja eso con el código más simple posible, que sea eficiente y eficaz. Tiene fugas, y puede actualizar la GUI.
Al igual que BackgroundWorker
necesita tener múltiples funciones, declarar variables de miembro, etc. También debe mantener una referencia al formulario de ProgressBar y desecharla.
Edición : BackgroundWorker
no es la respuesta porque puede ser que no reciba la notificación de progreso, lo que significa que no habrá ninguna llamada a ProgressChanged
ya que DoWork
es una llamada a una función externa, pero debo seguir llamando a la Application.DoEvents();
para que la barra de progreso siga 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 trabajo funciona en el subproceso principal y no devuelve ninguna notificación de progreso. Nunca necesité el código mágico .NET para informar el progreso automáticamente, solo necesitaba una solución mejor que:
Action<String, String> 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 no congelar pero refresca la marca)
Aquí hay otro código de muestra para usar BackgroundWorker
para actualizar ProgressBar
, solo 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: de codeproject
De hecho, usted está en el camino correcto. Deberías usar otro hilo, y has identificado las mejores maneras de hacerlo. El resto es solo actualizar la barra de progreso. En caso de que no quiera usar BackgroundWorker como otros han sugerido, hay un truco que debe tener en cuenta. El truco es que no puede actualizar la barra de progreso desde el subproceso de trabajo porque la interfaz de usuario solo se puede manipular desde el subproceso de la interfaz de usuario. Así que usas el método Invoke. Es algo como esto (corrige los errores de sintaxis, solo escribo 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 subproceso, excepto el que posee el formulario. El método Invoke
llamará al método en el subproceso de la interfaz de usuario y se bloqueará hasta que se complete. Si no desea bloquear, puede llamar a BeginInvoke
en BeginInvoke
lugar.
Estamos utilizando el formulario modal con BackgroundWorker
para tal cosa.
Aquí está la solución rápida:
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;
}
}
Y como lo usamos:
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();
Hay una gran cantidad de información sobre los subprocesos con .NET / C # en , pero el artículo que aclaró los subprocesos de los formularios de Windows fue nuestro oráculo residente, "Subprocesos en formularios de Windows" de Jon Skeet.
Vale la pena leer toda la serie para repasar sus conocimientos o aprender de cero.
Estoy impaciente, solo muéstrame algo de código
En cuanto a "mostrar 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 "buttonOnother"
buttonAnother
está allí únicamente para demostrar que la IU no está bloqueada mientras se ejecuta la tarea de conteo a 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;
}
}
Leer sus requisitos de la forma más sencilla sería mostrar una forma sin modo y usar un temporizador System.Windows.Forms estándar para actualizar el progreso en la forma sin modo. Sin hilos, no hay posibles fugas de memoria.
Como esto solo usa el único subproceso de la interfaz de usuario, también deberá llamar a Application.DoEvents () en ciertos puntos durante su procesamiento principal para garantizar que la barra de progreso se actualice visualmente.
Para mí, la forma más sencilla es utilizar un BackgroundWorker
, que está diseñado específicamente para este tipo de tareas. El evento ProgressChanged
está perfectamente adaptado para actualizar una barra de progreso, sin preocuparse por las llamadas entre hilos
Re: Su edición. Necesita un BackgroundWorker o un Thread para hacer el trabajo, pero debe llamar a ReportProgress () periódicamente para decirle al thread de 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 decirle (a) cuál es la cantidad máxima de progreso que alcanzará, y luego (b) aproximadamente 100 veces durante el proceso, informe a Es qué cantidad estás haciendo. (Si informa de un progreso menos de 100 veces, la barra de progreso saltará en grandes pasos. Si informa más de 100 veces, simplemente perderá tiempo tratando de informar un detalle más fino que el que mostrará la barra de progreso)
Si su hilo de interfaz de usuario puede continuar felizmente mientras el trabajador de fondo se está ejecutando, entonces su trabajo está hecho.
Sin embargo, de manera realista, en la mayoría de las situaciones en las que la indicación de progreso debe estar en ejecución, su UI debe ser muy cuidadosa para evitar una llamada entrante. por ejemplo, si está ejecutando una pantalla de progreso mientras exporta datos, no desea permitir que el usuario comience a exportar datos nuevamente mientras la exportación está en curso.
Puedes manejar esto de dos maneras:
La operación de exportación verifica si el trabajador en segundo plano se está ejecutando y deshabilitó la opción de exportación mientras ya se está importando. Esto le permitirá al usuario hacer cualquier cosa en su programa, excepto la exportación; esto 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 programa vuelva a estar "vivo" durante la exportación, pero el usuario no puede hacer nada (excepto cancelar) hasta que finalice la exportación. DotNet es una tontería apoyar esto, aunque es el enfoque más común. En este caso, debe colocar el subproceso de la interfaz de usuario en un bucle de espera ocupado donde llame a Application.DoEvents () para mantener el manejo de mensajes en ejecución (de modo 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 eventos de Paint para que las ventanas de la aplicación continúen redibujándose, pero filtraría los mensajes del mouse y del teclado para que el usuario no pueda hacer nada en el programa mientras la exportación está en progreso También hay un par de mensajes furtivos que deberá transmitir para que la ventana funcione normalmente, y resolverlos tomará unos minutos. Tengo una lista de ellos en el trabajo, pero no los tengo. Me temo que tengo que entregarlo. Es obvio que es NCHITTEST más una .net furtiva (malvada en el rango WM_USER) que es vital para que esto funcione.
El último "gotcha" con la horrible barra de progreso de dotNet es que cuando finaliza su operación y cierra la barra de progreso, encontrará que generalmente se cierra cuando se informa un valor como "80%". Incluso si lo fuerza al 100% y luego espera aproximadamente medio segundo, es posible que no alcance el 100%. ¡Arrrgh! La solución es establecer el progreso al 100%, luego al 99% y luego al 100%; cuando se le indica a la barra de progreso que avance, se anima lentamente hacia el valor objetivo. Pero si le dices que vaya "hacia atrás", salta inmediatamente a esa posición. Entonces, invirtiéndolo momentáneamente al final, puede obtener que realmente muestre el valor que pidió que muestre.
Si desea una barra de progreso "giratoria", ¿por qué no establecer el estilo de la barra de progreso en "Marquee" y usar un BackgroundWorker
para mantener la interfaz de usuario receptiva? No logrará una barra de progreso giratoria más fácil que usar el estilo "Marquee" ...
Tengo que lanzar la respuesta más simple que hay. Siempre puedes implementar la barra de progreso y no tener ninguna relación con nada del progreso real. Simplemente comience a llenar la barra, diga 1% por segundo o 10% por segundo, lo que parezca similar a su acción y si se llena para comenzar de nuevo.
Esto al menos le dará al usuario la apariencia de procesamiento y hará que entiendan que esperar en lugar de hacer clic en un botón y ver que no suceda nada y luego hacer clic en él más.
Use el componente BackgroundWorker para el que está diseñado exactamente para este escenario.
Puede conectarse a sus eventos de actualización de progreso y actualizar su barra de progreso. La clase BackgroundWorker garantiza que las devoluciones de llamada se marquen en el subproceso de la interfaz de usuario para que no tengas que preocuparte por ninguno de esos detalles.
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);
}
}));
}
}
}
Me parece que estás operando con al menos una suposición falsa.
1. No es necesario elevar el evento ProgressChanged para tener una interfaz de usuario receptiva
En tu pregunta dices esto:
BackgroundWorker no es la respuesta porque puede ser que no reciba la notificación de progreso, lo que significa que no habrá ninguna llamada a ProgressChanged, ya que DoWork es una llamada a una función externa. . .
En realidad, no importa si llama al evento ProgressChanged
o no . El propósito principal de ese evento es transferir temporalmente el control de nuevo al hilo de la GUI para realizar una actualización que de alguna manera refleje el progreso del trabajo que realiza BackgroundWorker
. Si simplemente está mostrando una barra de progreso de marquesina, en realidad sería inútil elevar 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 al margen, 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 controladores puede contener tantas llamadas de funciones como le gusta.)
2. No es necesario que llame a Application.DoEvents para tener una interfaz de usuario receptiva
A mi me parece que crees que la única forma de actualizar la GUI es llamando a Application.DoEvents
:
Necesito seguir llamando a la aplicación. DoEvents (); para que la barra de progreso siga girando.
Esto no es cierto en un escenario multiproceso ; Si usa un BackgroundWorker
, la GUI continuará respondiendo (en su propio hilo) mientras que el BackgroundWorker
haga lo que se haya adjuntado 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 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. No puedes ejecutar dos métodos al mismo tiempo en el mismo hilo
Tú dices esto:
Solo necesito llamar a Application.DoEvents () para que funcione la barra de progreso de Marque, mientras que la función de trabajo funciona en el subproceso principal. . .
Lo que estás pidiendo simplemente no es real . El subproceso "principal" para una aplicación de Windows Forms es el subproceso de la GUI que, si está ocupado con su método de ejecución prolongada, 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ás llamando a Application.DoEvents
repetidamente desde el hilo de la GUI, que se actualizaría de todos modos . (Si descubrió lo contrario, sospecho que se debe a que llamó a exec.EndInvoke
inmediato, lo que bloqueó el subproceso actual hasta que el método terminó).
Entonces, sí, la respuesta que está buscando es usar un BackgroundWorker
.
Puede usar 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 BeginInvoke
(en lugar de simplemente pasar el null
) y cierre el formulario de progreso. su devolución de llamada. Sin embargo, tenga en cuenta que si lo hace, tendrá que invocar el método que cierra el formulario de progreso desde el hilo de la GUI, ya que de lo contrario intentará cerrar un formulario, que es una función de la GUI, desde un hilo no-GUI. Pero realmente, todos los escollos de usar BeginInvoke
/ EndInvoke
ya se han BeginInvoke
con la clase BackgroundWorker
, incluso si crees que es el "código mágico .NET" (para mí, es solo una herramienta intuitiva y útil).