Hace unos días, en el momento de escribir este artículo, recibí una petición por parte de un usuario sobre cómo podría resolver un problema que venía enfrentando en un control para una de las aplicaciones de escritorio utilizadas en su empresa.
Este es el escenario:
- El control debe mostrar previos de imágenes y documentos PDF.
- Los documentos a mostrar pueden arrastrarse y soltarse sobre el control, o bien abrirlos desde el diálogo estándar de selección de archivos.
- En el caso de las imágenes, también pueden añadirse al control mediante Copiar y Pegar.
- En cualquiera de los anteriores supuestos, ha de recibirse la ruta del archivo en cuestión de modo que se puedan realizar futuras acciones.
Como quinto punto adicional, ya que estamos usando Xojo, añadí de mi propia cosecha la capacidad de que dicho control fuese funcional tanto en macOS como en Windows y Linux.
Los preparativos
Atendiendo a los anteriores requisitos, y teniendo en cuenta que un mismo problema puede tener más de una solución posible, lo primero que me vino a la cabeza fue crear un control mediante la técnica de la “Composición”; es decir, varios controles de interfaz gráfica que son utilizados de forma interna pero cuyo uso es transparente para los usuarios que lo vayan a emplear de forma global, ya sea en la creación de sus propias aplicaciones o bien a la hora de utilizarlo, indistintamente, en cualquiera de las plataformas soportadas.
Y cuando se trata de la creación de un control mediante composición… el DesktopContainerControl es uno de los mejores candidatos. Entonces, una nueva clase basada en ContainerControl utilizaría de forma interna los siguientes controles principales:
- El control DesktopImageWell es el candidato perfecto para mostrar las imágenes.
- El control DesktopHTMLViewer es idóneo para mostrar la previsualización de los documentos PDF.
Ahora bien, mientras que DesktopImageWell soporta mostrar imágenes directamente mediante Arrastrar y Soltar, este no es el caso del control DesktopHTMLViewer; de modo que si el usuario del control intentase arrastrar y soltar un documento PDF o una imagen cuando es el HTMLViewer el que está mostrando el control… no funcionaría.
Uno de los trucos más utilizados para solucionar esta situación es la de añadir un tercer control a la mezcla: una instancia de DesktopCanvas que esté en todo momento como “capa” superior independientemente de cuál sea el control subyacente encargado de mostrar el previo (puede ser el ImageWell o bien el HTMLViewer). Así, el Canvas sería el encargado de capturar el evento correspondiente a que el usuario a arrastrado un archivo sobre el control global (esto es, nuestra clase basada en ContainerControl), y dado que es transparente aunque se encuentre en la capa superior… no impedirá dejar ver el previo de la imagen o del archivo PDF en el control de la capa inferior… al menos en macOS.
Pero el Canvas no es transparente en Windows, de modo que en este caso utilizaremos la compilación condicional en varios puntos de la clase para modificar la interfaz de usuario del control principal (el container), de modo que añada una área especial situada bajo los controles de previsualización (ImageViewer o HTMLViewer), y que será la utilizada por el usuario del control para arrastrar y soltar los archivos correspondientes. Para ello utilizaremos una instancia adicional de DesktopCanvas y será suficiente.
Por supuesto, también tendremos que añadir a nuestro control un botón que será el encargado de abrir el cuadro de diálogo estándar de selección de archivo para que el usuario pueda elegir el archivo a previsualizar.
Y para lidiar con otra pequeña diferencia existente entre macOS y Windows, también añadiremos una instancia de DesktopRectangle, utilizada como la capa situada más al fondo o base del control (eje Z), para que muestre el típico anillo de Foco cuando el control esté activo y seleccionado.
Diseñando el control
Con lo anterior en mente, empezaremos un nuevo proyecto Desktop en Xojo. A continuación arrastra un DesktopContainer desde la Librería sobre el Navegador. Tendrá el siguiente aspecto:
Con la nueva instancia DesktopContainer1 seleccionada en el Navegador, utiliza el Panel Inspector para cambiar los siguientes valores:
- Name: ImagePDFContainer
- Size > Width: 220
- Size > Height: 200
A continuación añadiremos el área de Arrastrar y Soltar que, recuerda, se mostrará sólo cuando el control se ejecute sobre Windows. Para ello, arrastra desde la Librería un DesktopCanvas y suéltalo en el área del tapete correspondiente al Editor de Diseño, situándolo justo bajo el container para nuestra propia comodidad.
Con Canvas1 seleccionado, utiliza el Panel Inspector para cambiar los siguientes valores:
- Name: DropArea
- Position > Left: 1
- Position > Top: 221
- Position > Width: 216
- Position > Height: 100
Por ahora, el diseño debería de ser similar a este:
Vamos a añadir ahora la capa del fondo y que será la que, en Windows, se encargará de mostrar el anillo de foco para el control. Para ello, arrastra un DesktopRectangle desde la Librería y suéltalo sobre el ContainerControl en el Editor de Diseño (el ContainerControl debería de mostrar un recuadro de color rojo indicando que el control arrastrado estará contenido como control hijo en el propio Container). A continuación, y con DesktopRectangle1 aún seleccionado en el Editor de Diseño, utiliza el Panel Inspector para modificar los siguientes valores:
- Name: BacktroundRect
- Position > Left: 0
- Position > Top: 0
- Position > Width: 220
- Position > Height: 200
- Locking: Bloquea los cuatro candados.
A continuación, añadiremos el botón encargado de mostrar el cuadro de diálogo estándar para la selección del archivo a previsualizar. Arrastra un DesktopPushButton desde la Librería y suéltalo sobre la parte inferior del Container en el Editor de diseño (el Container deberá de mostrar un recuadro de color rojo para indicar que el botón se añadirá como control hijo en el propio Container).
Luego, con Button1 aún seleccionado en el Editor de Diseño, utiliza el Panel Inspector asociado para cambiar los siguientes valores:
- Name: OpenFileButton
- Position > Left: 0
- Position > Top: 0
- Position > Width: 178
- Position > Height: 22
- Locking: Cierra los candados izquierdo, derecho e inferior. Abre el candado del borde superior.
- Mac Button Style: Recessed
- Caption: Open File…
Por el momento, el diseño del control debería de tener un aspecto similar al siguiente:
Añadamos ahora la instancia del control DesktopImageViewer encargada de previsualizar las imágenes. Como en anteriores pasos, arrastra el control DesktopImageViewer desde la Librería y suéltalo sobre el tapete del Editor de Diseño, en la parte derecha superior del Container; luego, con ImageViewer1 aún seleccionado, utiliza el Panel Inspector asociado para modificar los siguientes valores:
- Name: ImagePreviewer
- Position > Left: 238
- Position > Top: -219
- Position > Width: 218
- Position > Height: 198
- Locking: Bloquea los cuatro candados.
Arrastra un DesktopHTMLViewer desde la Librería sobre la zona libre del tapete del Editor de diseño, justo bajo el ImageViewer añadido en el paso anterior; luego, y con HTMLViewer1 aún seleccionado en el Editor de Diseño, utiliza el Panel Inspector asociado para modificar los siguientes valores:
- Name: HTMLPDF
- Position > Left: 238
- Position > Top: 0
- Position > Width: 218
- Position > Height: 198
- Locking: Bloquea los cuatro candados.
En este momento, el diseño del control debería de tener un aspecto similar al siguiente:
Por último, añadiremos el overlay que utilizaremos sólo cuando el control se ejecuta en macOS y que será el encargado de capturar la acción de “Arrastrar y Soltar” archivos por parte del usuario. Para ello, arrastra un nuevo DesktopCanvas sobre el Container en el Editor de Diseño (el Container debería mostrar un recuadro de color rojo para indicar que el nuevo control se está añadiendo como hijo del propio Container). Con Canvas1 aún seleccionado en el Editor de Diseño, utiliza el Panel Inspector asociado para modificar los siguientes valores:
- Name: CanvasOverlay
- Position > Left: 1
- Position > Top: 1
- Position > Width: 218
- Position > Height: 179
- Locking: Bloquea los cuatro candados.
Para asegurarnos de que todas las “capas” (orden de los controles en el eje Z) están como esperamos, haz clic sobre el botón “Show Tab Order” en la barra de herramientas correspondiente al Editor de Diseño. Si no es así, arrastra y suelta cada elemento hasta su nueva posición en el listado:
Añadir Funcionalidad
Con el diseño del control ya finalizado, es el momento de añadir su funcionalidad. Para ello, selecciona ImagePDFContainer en el Navegador y añade las siguientes propiedades, junto con los valores indicados en el Panel Inspector:
La primera será la encargada de contener el mensaje a mostrar en el área de “Arrastrar y Soltar”:
- Name: DropFileMessage
- Type: String
- Scope: Public
La segunda será la propiedad encargada de almacenar la referencia al archivo (instancia de FolderItem) cuya imagen / PDF se está previsualizando:
- Name: FileDropped
- Type: FolderItem
- Scope: Private
La última propiedad será la encargada de contener el color a utilizar como anillo de enfoque:
- Name: FocusColor
- Type: Color
- Scope: Public
A continuación añadiremos los eventos que precisa consumir el control para su funcionamiento; de modo que, con ImagePDFContainer aún seleccionado en el Navegador, añade los siguientes Manejadores de Evento junto con el código asociado:
Evento Opening:
HTMLPDF.Visible = False ImagePreviewer.Visible = False #If TargetWindows Then DropArea.Top = Me.Height - 50 DropArea.Height = 50 DropArea.Width = BackgroundRect.Width DropArea.Left = BackgroundRect.Left BackgroundRect.Height = BackgroundRect.Height - 50 HTMLPDF.Height = BackgroundRect.Height - 2 ImagePreviewer.Height = BackgroundRect.Height - 2 #EndIf RaiseEvent Opening
Evento MouseDown:
Me.SetFocus Return True Evento FocusReceived: #If TargetWindows Then FocusColor = Color.Blue BackgroundRect.BorderColor = FocusColor DropArea.Refresh #EndIf
Evento FocusLost:
#If TargetWindows Then FocusColor = Color.Black BackgroundRect.BorderColor = FocusColor DropArea.Refresh #EndIf
Dado que el evento Opening está utilizado por nuestra subclase ImagePDFContainer, añadiremos una definición de evento con el mismo nombre de modo que, las instancias creadas a partir de él, también puedan añadir su propio manejador de evento Opening e incluir el código adicional que se desee ejecutar
Luego tambien añadiremos una definición de evento adicional (FileDropped) y que será el enviado por la subclase ImagePDFContainer a las instancias creadas a partir de ella, y que lo implementen, para pasarles como parámetro el FolderItem correspondiente al archivo abierto / arrastrado y soltado / o, en el caso de las imágenes, que se hayan pegado sobre el control.
- Name: FileDropped
- Parameters: file As FolderItem
Dado que queremos soportar el pegado de imágenes sobre el control, también necesitaremos añadir un Manejador de Menú a la subclase ImagePDFContainer. Para ello, y con ImagePDFContainer aún seleccionado en el Navegador selecciona la opción Add to “ImagePDFContainer” > Menu Handler…
Utiliza los siguientes valores en el Panel Inspector asociado:
MenuItem Name: EditPaste
Y añade el siguiente código en el Editor de Código asociado con el Manejador de Menú:
Var pb As New Clipboard If pb.PictureAvailable Then Var f As FolderItem = SpecialFolder.Temporary.Child("temp-pict.png") If f.Exists Then f.Remove If f <> Nil Then pb.Picture.Save(f, Picture.Formats.PNG) FileDropped = f PreviewFile(FileDropped) End If Return True End If Return False
Para finalizar con esta sección, añade un nuevo método a ImagePDFContainer y utiliza los siguientes valores en el Panel Inspector asociado:
- Method Name: PreviewFile
- Parameters: f As FolderItem
Tecleando a continuación el siguiente código en el Editor de Código asociado:
FileDropped = f If f <> Nil Then If FileDropped.Name.EndsWith(".pdf") Then ImagePreviewer.Visible = False HTMLPDF.Visible = True HTMLPDF.top = BackgroundRect.top + 1 HTMLPDF.Left = BackgroundRect.Left + 1 #If TargetWindows Then HTMLPDF.Height = BackgroundRect.Height - OpenFileButton.Height #Else HTMLPDF.Height = Self.Height - OpenFileButton.Height #EndIf HTMLPDF.LoadPage(FileDropped) Else Var p As Picture = Picture.Open(FileDropped) If p <> Nil Then ImagePreviewer.top = BackgroundRect.top + 1 ImagePreviewer.Left = BackgroundRect.Left + 1 #If TargetWindows Then ImagePreviewer.Height = BackgroundRect.Height - OpenFileButton.Height #Else ImagePreviewer.Height = Self.Height - OpenFileButton.Height #EndIf ImagePreviewer.Image = p HTMLPDF.Visible = False ImagePreviewer.Visible = True End If End If RaiseEvent FileDropped(FileDropped) Else HTMLPDF.Visible = False ImagePreviewer.Visible = False End If
Añadir Funcionalidad a los Controles
Ahora es el momento de añadir funcionalidad a los controles propiamente dichos. Selecciona el ítem CanvasOverlay en el Navegador y añade los siguientes Manejadores de evento junto con el código indicado a continuación:
Manejador de Evento Opening:
Me.AcceptFileDrop("application/pdf") Me.AcceptFileDrop("image/png") Me.AcceptFileDrop("image/jpg") #If TargetWindows Then Me.Visible = False #EndIf
Manejador de Evento DropObject:
#Pragma Unused action If obj.FolderItemAvailable Then PreviewFile(obj.FolderItem) End If
Manejador de Evento MouseDown:
#Pragma Unused x #Pragma Unused y Self.SetFocus
Selecciona ahora el ítem DropArea en el Navegador y añade los siguientes Manejadores de Evento junto con el código indicado a continuación:
Manejador de Evento Opening:
Me.AcceptFileDrop("application/pdf") Me.AcceptFileDrop("image/png") Me.AcceptFileDrop("image/jpg") #If TargetMacOS Then Me.Visible = False #EndIf
Manejador de Evento DropObject:
#Pragma Unused action If obj.FolderItemAvailable Then PreviewFile(obj.FolderItem) End If
Manejador de Evento Paint:
#Pragma Unused areas g.DrawingColor = FocusColor g.PenSize = 2.0 g.DrawRectangle(0, 0, g.Width, g.Height) g.PenSize = 1.0 If DropFileMessage <> "" Then g.DrawingColor = Color.Black Var textLenght As Double = g.TextWidth( DropFileMessage ) Var x, y As Double x = Me.Width / 2 - textLenght / 2 y = Me.Height / 2 + g.TextHeight / 2 g.DrawText(DropFileMessage, x, y) End If
Selecciona ahora el item HTMLPDF en el Navegador y añade los Manejadores de Evento junto con el código indicado a continuación:
Manejador de Evento MouseDown:
#Pragma Unused x #Pragma Unused y Self.SetFocus
A continuación, selecciona el ítem ImagePreviewer en el Navegador y añade los siguientes Manejadores de Evento junto con el código indicado a continuación:
Manejador de Evento MouseDown:
#Pragma Unused x #Pragma Unused y Self.SetFocus
Por último, selecciona el ítem OpenFileButton en el Navegador y añade los siguientes Manejadores de Evento junto con el código indicado a continuación:
Manejador de Evento Pressed:
Var f As FolderItem = FolderItem.ShowOpenFileDialog("") If f <> Nil Then self.PreviewFile(f) End If
Probando el Control ImagePDFContainer
Ya hemos completado el diseño y añadido la funcionalidad requerida a nuestro control, ahora es el momento de incorporar una instancia en la ventana del proyecto para que podamos probar su funcionamiento.
Selecciona Window1 en el Navegador de modo que se muestre el Editor de Diseño asociado. A continuación arrastra ImagePDFContainer desde el Navegador y suéltalo sobre el Editor de Diseño, utilizando los siguientes valores en el Panel Inspector asociado:
- Size > Width: 220
- Size > Height: 200
- Locking: Bloquea los cuatro candados
- Appearance > Allow Focus Ring: Activado
- Behavior > Allow Focus: Activado.
- Behavior > Allow Tabs: Activado.
Con ImagePDFContainer1 seleccionado en el Navegador, añade los siguientes Manejadores de Evento junto con el código indicado a continuación:
Manejador de Evento Opening:
me.DropFileMessage = "Drop Files Here"
Manejador de Evento FileDropped:
LastFolderItemNameLabel.Text = file.NativePath
Como ves, aquí se hace referencia al control LastFolderItemNameLabel que no hemos añadido todavía al diseño de la ventana; de modo que, ¡vamos a hacerlo!
Selecciona Window1 en el Navegador para acceder al Editor de Diseño asociado y arrastra un control Label desde la Librería soltándolo sobre la parte inferior de la ventana en el Editor de diseño. A continuación, utiliza los siguientes valores en el Panel Inspector asociado:
- Name: Label1
- Position > Left: 28
- Position > Top: 360
- Position > Width: 150
- Position > Height: 20
- Locking: Bloquea el candado izquierdo e inferior. Desbloquea los candados superior y derecho.
- Text: Last FolderItem Path:
Añade un segundo Label desde la Librería y suéltalo a la derecha del anterior, utilizando los siguientes valores en el Panel Inspector asociado:
- Name: LastFolderItemNameLabel
- Position > Left: 190
- Position > Top: 360
- Position > Width: 390
- Position > Height: 20
- Locking: Bloquea los candados izquierdo, inferior y derecho. Desbloquea el candado superior.
- Text: (sin texto)
Ejecutar la aplicación
¡Listo! Ejecuta la aplicación y prueba a arrastrar y soltar archivos de imágenes (JPEG o PNG), así como archivos PDF sobre el control, este debería cambiar sobre la marcha para mostrar el visor más adecuado a cada tipo de archivo, además de mostrar la ruta del archivo en la parte inferior de la ventana.
Por supuesto, también puedes hacer clic sobre el botón para seleccionar el archivo a mostrar o bien probar a copiar imágenes y pegarlas directamente sobre el control, en cuyo caso se creará un archivo temporal de modo que se satisfaga uno de los requerimientos iniciales: poder continuar trabajando con el archivo correspondiente a la imagen o documento PDF.
Como te comentaba al inicio, esta es sólo una de las posibles implementaciones pero a buen seguro que habrás aprendido unas cuantas cosas por el camino en el caso de que no tengas mucha experiencia todavía en Xojo:
- Redefinir Eventos consumidos por una clase base.
- Crear tus propios Eventos de clase y como lanzarlos para “pasar” información a las instancias creadas a partir de dicha clase.
- Capturar y trabajar con Manejadores de Menú.
- Uso de la Compilación Condicional para que ciertas secciones de código se ejecuten sólo en una u otra plataforma.
- Y, por supuesto, como hacer que varios controles de UI co-operen y se presenten como un único control de cara al desarrollador y usuario final que van a utilizarlos.
¡Confío en que lo hayas encontrado interesante… y que lo puedas modificar / ampliar para que se ajusten mejor a tus propósitos!