A continuación encontrarás traducido al castellano el artículo escrito por Gabriel Ludosanu y publicado originalmente en el Blog oficial de Xojo.
¿Has escrito alguna vez altuna aplicación que necesitase realizar una tarea pesada como procesar un gran conjunto de archivos? Quizá hayas hecho clic en un botón… y tu aplicación de la sensación de estar bloquedada por completo. El cursor muestra el disco girando, las ventanas no responden a las acciones de los usuarios y tus usuarios probablemente se pregunten si la aplicación se ha colgado. Se trata de una experiencia frustrante; pero no te preocupes, existe una solución fantástica: ¡los hilos!
Creemos una aplicación de escritorio simple pero a la vez potente; una encargada de ajustar el tamaño de una imagen mediante el uso de hilos. Esta herramienta te permitirá seleccionar una carpeta de imágenes y cambiar su tamaño sin que tu app parezca bloqueada. Es un ejemplo perfecto de cuan fáciles y eficaces resultan los hilos en Xojo.
¡Comenzamos!
Paso 1: Diseñar la Interfaz de Usuario
Lo primero es lo primero: diseñemos la ventana de nuestra app. Emplearemos un diseño claro y simple. Todo lo que necesitaremos será crear un nuevo proyecto Xojo Desktop y añadir los siguientes controles sobre la ventana principal desde la Librería:
- DesktopButton. Este será nuestro botón de “inicio”. El usuario hará clic sobre él para seleccionar una carpeta e inicial el proceso de cambio de tamaño de las imágenes. Definamos su propiedad “Caption” como “Seleccionar carpeta e Iniciar”.
- DesktopProgressBar. Este control proporcionará información visual al usuario sobre el progreso de la tarea.
- DesktopLabel. Utilizaremos esta etiqueta para mostrar actualizaciones sobre el estado de la tarea, como por ejemplo el nombre del archivo que se está procesando o si se ha completado la tarea. Asignemos “StatusLabel” como el nombre de la instancia.
El aspecto de tu diseño debería de ser similar al siguiente:
Paso 2: Añadir el Ingrediente Mágico: El Hilo
Dirígete a la Librería en el IDE y ubica el objeto Thread. Arrástralo y suéltalo sobre la ventana. Por omisión tomará el nombre “Thread1”, pero yo lo he cambiado por “ResizeThread” para lograr que su propósito, desde el código, sea más claro.
Este objeto es donde sucederá todo el trabajo en segundo plano.
Paso 3: Disparar el Proceso
Con nuestra Interfaz de Usuario y el Hilo ya creados, es el momento de añadir el código encargado de que todo ocurra. Haz doble clic en el botón “Seleccionar Carpeta e Iniciar” para añadir el evento Pressed. Aquí es donde se ejecutará el código cuando el usuario haga clic sobre el botón.
Y el código para el evento Pressed será el siguiente:
' Solicitar al usuario la carpeta fuente (que contenga imágenes) Var f As FolderItem = FolderItem.ShowSelectFolderDialog If f = Nil Then Return ' user cancelled mSourceFolder = f ' Reiniciar la IU ProgressBar1.Value = 0 StatusLabel.Text = "Starting…" ' Iniciar la tarea en segundo plano ResizeThread.Start
Revisemos cada línea. En primer lugar pedimos al usuario que seleccione una carpeta. Si no lo hace, simplemente salimos. En el caso de que lo haga, almacenamos la carpeta seleccionada en una propiedad de la ventana cuyo nombre es “mSourceFolder” (Public Property mSourceFolder As FolderItem). Luego, reiniciamos la barra de progreso y la etiqueta de estado y, lo más importante, llamamos a ResizeThread.Start. Esta simple línea de código indicará a nuestro Hilo que se “despierte” y que empiece a ejecutar el código incluido en su evento Run.
Paso 4: El Trabajo Duro (en segundo plano)
El evento Run del hilo ResizeThread es donde residirá toda la lógica dle proceso. Aquí es donde encontraremos, cargaremos y ajustaremos el tamaño de las imágenes para guardarlas a continuación. Recuerda la regla de oro sobre los hilos: nunca accedas directamente a la interfaz de usuario desde el evento Run. Si lo haces, provocarás cuelgues de la app y un comportamiento errático.
En vez de ello, realizaremos nuestra tarea y enviaremos mensajes al hilo principal con una actualización. Para ello utilizaremos el método AddUserInterfaceUpdate.
Este es el código correspondiente al evento Run:
' Validación rápida
If mSourceFolder = Nil Or Not mSourceFolder.Exists Or Not mSourceFolder.IsFolder Then
Me.AddUserInterfaceUpdate(New Dictionary("progress":0, "msg":"No folder selected"))
Return
End If
Try
' Nos aseguramos de que exista la carpeta de salida "/resized"
Var outFolder As FolderItem = mSourceFolder.Child("resized")
If Not outFolder.Exists Then outFolder.CreateFolder
' Crear un listado con los archivos de imagen candidatas.
' Nota: usamos Picture.Open para validar que son archivos de imágenes. Es simple y robusto.
' (Para carpetas que contengan un gran número de archivos podrías aplicar un filtro previo basándote en la extensión de archivo.)
Var images() As FolderItem
For Each it As FolderItem In mSourceFolder.Children
If it = Nil Or it.IsFolder Then Continue ' Ignoramos las subcarpetas
Try
Var p As Picture = Picture.Open(it)
If p <> Nil Then images.Add(it)
Catch e As RuntimeException
' No es una imagen o no se puede leer. Lo saltamos
End Try
Next
Var total As Integer = images.Count
If total = 0 Then
Me.AddUserInterfaceUpdate(New Dictionary("progress":0, "msg":"No images found"))
Return
End If
' Ajustes para modificar el tamaño de imagen (a 800 x 800)
Const kMaxW As Double = 800.0
Const kMaxH As Double = 800.0
' Breve retardo tras cada archivo, de modo que el hilo principal pueda repintar la IU
Const kDelayMS As Integer = 50
For i As Integer = 0 To total - 1
Var src As FolderItem = images(i)
Try
' Carga la fuente (immutable)
Var pic As Picture = Picture.Open(src)
If pic = Nil Or pic.Width <= 0 Or pic.Height <= 0 Then Continue
' Calcula la escala proporcional
Var sW As Double = kMaxW / pic.Width
Var sH As Double = kMaxH / pic.Height
Var scale As Double = Min(Min(sW, sH), 1.0)
Var newW As Integer = Max(1, pic.Width * scale)
Var newH As Integer = Max(1, pic.Height * scale)
' Renderiza en un nuevo bitmap inmutable con el tamaño de destino
Var outPic As New Picture(newW, newH)
outPic.Graphics.DrawPicture(pic, 0, 0, newW, newH, 0, 0, pic.Width, pic.Height)
' Crea un nombre base de archivo seguro (se salta la última extensión; gestiona los archivos con punto)
Var name As String = src.Name
Var ext As String = name.LastField(".")
Var baseName As String
If ext = name Then
' No tiene un punto en el nombre
baseName = name
Else
baseName = name.Left(name.Length - ext.Length - 1)
End If
If baseName.Trim = "" Then baseName = "image"
' Guardar el JPEG (cambia a PNG si no está soportada la exportación como JPEG)
Var outFile As FolderItem = outFolder.Child(baseName + "_resized.jpg")
If Picture.IsExportFormatSupported(Picture.Formats.JPEG) Then
outPic.Save(outFile, Picture.Formats.JPEG, Picture.QualityHigh) ' ajusta la calidad del jpeg
Else
outPic.Save(outFolder.Child(baseName + "_resized.png"), Picture.Formats.PNG)
End If
Catch io As IOException
' Probablemente un problema con los permisos/acceso/escritura/o disco; saltamos este archivo
Catch u As UnsupportedOperationException
' Operación o formato no soportados en esta plataforma; nos lo saltamos
End Try
' Progreso + mensaje a la IU de forma segura
Var pct As Integer = ((i + 1) * 100) / total
Me.AddUserInterfaceUpdate(New Dictionary("progress":pct, _
"msg":"Resized " + src.Name + " (" + pct.ToString + "%)"))
' Dejemos que el hilo principal dibuje la actualiazción
Me.Sleep(kDelayMS, True)
Next
' Mensaje de IU final
Me.AddUserInterfaceUpdate(New Dictionary("progress":100, "msg":"Done"))
Catch e As RuntimeException
' Cualquier fallo no esperado: reportamos un mensaje en tal caso.
Me.AddUserInterfaceUpdate(New Dictionary("progress":0, "msg":"Error: " + e.Message))
End TryPaso 5: Recibir actualizaciones y actualizar la IU
¿Cómo hacemos para que el hilo principal “escuche” las actualizaciones provenientes del hilo? Mediante el evento UserInterfaceUpdate del hilo ResizeThread propiamente dicho. Este evento se dispara sobre el hilo principal, haciendo que sea el único lugar seguro en el cual podremos actualizar nuestros controles.
Este es el código para el evento UserInterfaceUpdate:
' Este evento se ejecuta en el hilo principal, donde es seguro actualizar los controles.
If data.Count = 0 Then Return
Var latest As Dictionary = data(data.LastIndex)
If latest.HasKey("progress") Then ProgressBar1.Value = latest.Value("progress")
If latest.HasKey("msg") Then StatusLabel.Text = latest.Value("msg")
' Un extra: cuando haya finalizado se encarga de abrir la carpeta de salida
If latest.HasKey("msg") And latest.Value("msg") = "Done" Then
Var outFolder As FolderItem = mSourceFolder.Child("resized")
If outFolder <> Nil And outFolder.Exists Then
outFolder.Open
End If
End IfEn este evento recibiremos un array de Diccionarios (todas las actualizaciones que hayamos encolado) Por lo general sólo nos ocuparemos de la última, de modo que obtendremos la correspondiente a data(data.LastIndex). Luego, accederemos de forma segura a los valores del diccionario y los asignaremos a ProgressBar1.Value y a StatusLabel.Text. ¡Eso es todo!
Conclusión
¡Pues ya está! Una app completamente funcional y sin bloqueos de la interfaz de usuario que te permitirá cambiar el tamaño imágenes; todo ello mediante el uso de un Hilo.
El patrón es simple:
- Inicia la tarea desde el hilo principial (ResizeThread.Start).
Añade el código correspondiente a la tarea a realizar en el evento Run del hilo. - Comunícate con la interfaz de usuario mediante AddUserInterfaceUpdate.
- Actualiza la interfaz de usuario de forma segura mediante el evento UserInterfaceUpdate.
Y eso es todo. Anímate, descarga el código fuente de este proyecto desde GitHub, ejecútalo y revisa su código y piensa luego sobre los modos en los que podrías usar los hilos en tus propias aplicaciones.
¡Feliz programación con Xojo!