Wednesday, September 13, 2006

Copiar ficheros utilizando un Progressbar

Comenzando...

En este articulo crearemos una aplicación que mostrara el proceso de copiado de un fichero, tambien utilizaremos la programación multihebra para mejorar la interacción con la interfaz y el uso de cronometros para comparar el rendimiento de la aplicación.

Introducción:

Si haz experimentado con el espacio de nombres System.IO lo más seguro es que te hayas aventurado con la clase File, con ella puedes manipular los ficheros: moverlos,  renombrarlos, etc; pero tiene una pega, y es que no podemos mostrar el progreso a la hora de copiar, algo que no tiene mucha importancia (ya que lo importante es que se copie el fichero) pero que produce ese efecto "mágico" en nuestra aplicación, esto no tiene tamaña complicación, pero antes de escribir ninguna linea de codigo te explicare las bases de este procedimiento:

Ya sabemos que no se puede utilizar File para copiar con barra de progreso, pero en el espacio System.IO hay una clase llamada FileStream, que expone un Stream (Flujo de Bytes) alrededor de un fichero, permite un acceso aleatorio cuando se trata de archivos en disco y soporta llamadas Sincrónicas y Asincrónicas. Lo importante de FileStream es que permite la lectura/escritura desde/sobre un buffer, que no es más que una matriz de Bytes (toda las operaciones sobre ficheros son en base al tipo de dato byte).

La función Read:

Read ( array As Byte(), offset As Integer, count As Integer ) As Integer

Esta función intenta leer desde el flujo de bytes un número de bytes indicado por Count, los cuales escribira desde offset en array, o sea que se escribira en array desde offset hasta offset + count -1. Cuando digo intenta, me refiero a que es posible que lleguemos al final del fichero y no haya mas bytes para leer, en este caso lógicamente se volcaran solo los bytes leidos; el número de bytes leidos sera el resultado de la función, si devuelve cero significa que hemos llegado al fin del fichero. Quiero señalar que esta función utiliza un parametro para devolver resultados, algo muy común en la API de Win32 pero que vemos poco en la API de .NET.


El procedimiento Write:

Write ( array As Byte(), offset As Integer, count As Integer )

Escribe el número de bytes especificado en count, desde el byte en la posición definida por offset, o sea, escribe los bytes desde offset hasta count.


El algoritmo:

Dim result As Integer 
Dim buffer(buffersize) As Byte
Do
result = readfile.Read(buffer, 0, buffersize + 1)
writefile.Write(buffer, 0, result)
Loop Until result = 0

El algoritmo tiene como base un ciclo que se detiene cuando la función read, devuelve cero,  podemos implementar otros bucles, no solo un do loop-until. Para aprovechar todo el buffer hacemos que al leer count sea igual a buffersize +1 debido a que se escribe de offset hasta offset + count -1. En el procedimiento Write, escribimos desde 0 hasta result, que es el número de elementos leidos.

Mostrando el proceso:

Para mostrar el proceso en un progressbar, debemos hacer lo siguiente:


  1. La propiedad Maximun debe tomar el valor del número de bytes del fichero de origen, esto lo logramos mediante la propiedad Lenght del FileStream.
  2. En cada iteración del bucle, debemos aumentar el valor de la propiedad Value en la cantidad devuelta por Read, o sea result.

Quedaría algo así:

Me.pgbcopy.Maximum = CInt(readfile.Length)
Me.pgbcopy.Value = 0
Dim result As Integer
Dim buffer(buffersize) As Byte
Do
result = readfile.Read(buffer, 0, buffersize + 1)
writefile.Write(buffer, 0, result)
Me.pgbcopy.Value += result
Loop Until result = 0

Experimentando con el BufferSize:

De seguro ya habras pensado en probar si aumentando el tamaño del buffer aumentara la rapidez de la copia, esto no es del todo cierto, ya que si bien no es lo mismo utilizar un buffer de 2 bytes que uno de 4096 hay otros factores que influyen: si estan en el mismo disco, si es un disco extraible, etc. Para probar como se comporta el buffersize, crearemos una aplicación en la cual tendremos un log de cada proceso de copia; como la que muestra la figura:


  Para esto debes insertar:



























Control

Nombre

Características Especiales
ButtonbtnCopy-
ListviewltvLogView=Details
ProgressBarpgbCopy-
FolderBrowserDialogfbd-
OpenFileDialogofd

-


¿Como implementar cronómetros?

Para poder comparar la velocidad de copiado debemos hacer uso de cronómetros,  el framework 2.0 trae la clase StopWatch, alojada en System.Diagnostics, esta clase tiene un funcionamiento muy sencillo:

Dim Crono As New Stopwatch 'Creamos una nueva instancia
Crono.Reset() 'Borramos el tiempo transcurrido
Crono.Start() 'Iniciamos el cronómetro
Crono.Stop() 'Detenemos el cronómetro


 Para obtener el tiempo transcurrido, utilizamos la propiedad Elapsed que devuelve un objeto del tipo TimeSpan

Procedimiento Copy:

 Ya tenemos todas las piezas para armar el procedimiento principal, el procedimiento Copy:

Public Sub Copy() 
Dim Crono As New Stopwatch
Try
Crono.Start()
Dim readfile As New FileStream(sourcepath, IO.FileMode.Open)
Dim writefile As New FileStream(destpath, IO.FileMode.CreateNew)
Me.pgbcopy.Maximum = CInt(readfile.Length)
Me.pgbcopy.Value = 0
Dim result As Integer
Dim buffer(buffersize) As Byte
Do
result = readfile.Read(buffer, 0, buffersize + 1)
writefile.Write(buffer, 0, result)
Me.pgbcopy.Value += result
Loop Until result = 0
Crono.Stop()
Dim item As New ListViewItem
With item
.Text = sourcepath
.ImageIndex = 0
.SubItems.Add(destpath)
.SubItems.Add(readfile.Length.ToString)
.SubItems.Add(CStr(buffersize))
.SubItems.Add(Crono.Elapsed.ToString)
End With
readfile.Dispose()
writefile.Dispose()
Me.pgbcopy.Value = 0
Me.ltvLog.Items.Add(item)
Me.btnCopy.Enabled = True
Catch ex As Exception
MsgBox(ex.ToString)
End Try
End Sub

Lo primero que hacemos es insertar un bloque Try, ya que es muy común (poco espacio en disco, ya existe el archivo, etc) encontrarnos con excepciones a la hora de trabajar con ficheros. Luego de iniciar el cronometro, creamos los objetos FileStream, el contructor que utilizamos para esto recibe el path de cada fichero y el método de apertura del fichero. Una vez copiado el fichero, llega el momento del log, creamos un nuevo listviewitem y le añadimos la información. Justo despues de esto, liberamos los recursos de los FileStream, de lo contrario no podriamos interactuar con los ficheros porque estarian bloqueados.

 ¿Por qué es necesaria la programación multihebra?


Al principio mencione que implementariamos algo de programación multihebra, la razón es muy sencilla, si ejecutamos el procedimiento Copy en la misma hebra que el formulario experimentariamos el bloqueo de la interfaz de este último (se congela le ventana). Para esto ejecutamos el procedimiento en otro hilo, lo iniciamos, y desde ese hilo manipularemos los controles del formulario, a continuación el codigo para el evento click del botón, donde iniciamos el hilo:

Private Sub btnCopy_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCopy.Click 
If ofd.ShowDialog = Windows.Forms.DialogResult.OK Then
If fbd.ShowDialog = Windows.Forms.DialogResult.OK Then
buffersize = CInt(InputBox( "Insert the Buffer Size for this copy process" , "Buffer Size" , "128" ))
sourcepath = ofd.FileName
destpath = fbd.SelectedPath & "\" & Path.GetFileName(sourcepath)
If buffersize > 0 Then
CopyThread = New Thread(AddressOf Copy) 'Creamos un nuevo hilo, pasandole al contructor la referencia del procedimiento
Me.btnCopy.Enabled = False 'Prevenimos que no se creen más hilos
CopyThread.Start() 'Ejecutamos Copy en otro hilo
End If
End If
End If
End Sub

Debo señalar que los metodos pasados al constructor de la clase Thread (System.Threading) no pueden tener argumentos, pero esto no es impedimento para utilizar parametros, ya que los pasamos indirectamente con las variables del form:

Dim buffersize As Integer
Dim sourcepath As String
Dim destpath As String

¡Un error debido a los hilos!

Si iniciaramos un debugging del proyecto, recibiriamos una excepción: System.InvalidOperationException, no hay porque alarmarse, esta excepción es debido a que hemos intentado manipular un control desde un hilo distinto al de su creación, esto no es algo "seguro", pero en nuestro caso no hay nada de malo con ello además de que esta excepción sucede siempre cuado se hace un debug y rara vez en tiempo de ejecución, de todas formas para evitar las molestias que esto pueda ocasionar, en el evento load escribiremos esto:

Private Sub frmCopy_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load 
CheckForIllegalCrossThreadCalls = False 'Evitamos que se comprueba la seguridad de las llamadas entre los hilos
End Sub

En caso de que te se demore mucho el proceso de copiado...

Si intentas copiar un fichero de video con un buffer de 2 bytes, y la espera se te hace demasiada, lo más seguro es que cierres el formualrio, pero Ojo (0_o) esto no finaliza la aplicación (puedes comprobarlo en el debugging, cierra la aplicación mientras estes copiando, verás como el IDE sigue en modo Debug) y bloquea el acceso a los ficheros. Para que una aplicación se finalice totalmente todos sus hilos deben terminarse, este incoveniente lo evitaremos en el evento formClosing, que de dispara antes de que se cierre el formulario, antes de terminar el hilo, comprobamos que existe mediante una condición cortocircuitada:

Private Sub frmCopy_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing 
If Me.CopyThread IsNot Nothing AndAlso Me.CopyThread.ThreadState = ThreadState.Running Then
Me.CopyThread.Abort() 'Termina el Thread
End If
End Sub

Conclusiones:

Espero que hayan aprendido con este articulo de todo un poco, hilos, cronometros, ficheros. Para finalizar les dejo una captura del programa, despues de copiar varios capitulos de la seria Friends con diferentes tamaños de Buffer,

 

Finalizando...


Puedes bajarte el proyecto de aquí, y si quieres deja tu comentario del articulo ;-)

5 comments:

cibernetiko said...

esta muy bueno este ejemplo de progressbar, muy bien explicado, tenia curiosidad de saber como hacer funcionar realmente la progressbar, me has dado una buena referencia.
gracias

saludos

cibernetiko

Horacio Núñez said...

Hola Cibernetiko, muchas gracias
No me he olvidado de lo del tooltip, de heho tengo la solución para hacer una especie de MegaToolTip, escribeme a mi mail, salu2

Ah, lo de subir los programas a la red, puedes hacerlo con bitacoras.com

Anonymous said...

De verdad mejor explicado e ilustrado imposible, más claro agua.
Enhorabuena.

Pablo Guerra said...

Muy bueno, hace rato estaba buscando algo similar. Saludos. Por si acaso tienes el codigo para ver el progreso de solo un archivo. gracias. me puedes escribir a pabloguerracolombia@gmail.com

Anonymous said...

[url=http://kfarbair.com][img]http://www.kfarbair.com/_images/logo.png[/img][/url]

בית מלון [url=http://www.kfarbair.com]כפר בעיר[/url] - שירות חדרים אנו מציעים שירותי אירוח מגוונים כמו כן ישנו במקום שירות חדרים המכיל [url=http://www.kfarbair.com/eng/index.html]סעודות רומנטיות[/url] במחירים מיוחדים אשר יוגשו ישירות לחדרכם...

לפרטים נוספים נא גשו לעמוד המלון - [url=http://kfarbair.com]כפר בעיר[/url] [url=http://www.kfarbair.com/contact.html][img]http://www.kfarbair.com/_images/apixel.gif[/img][/url]