Crear un control multiplataforma en Xojo

Una de las cosas que más me gustan de Xojo es la capacidad de crear verdaderas aplicaciones multiplataforma nativas, sin intérpretes de por medio, que puedo desplegar en las principales plataformas. Por ejemplo, en escritorio es posible generar ejecutables de 32 y 64 bits para macOS, Windows, Linux (en todo tipo de distros) y RaspberryPi; pero también puedo reutilizar gran parte del código para generar la versión que se ejecute como app Web de 32 y 64 bits (ya sea bajo Apache o IIS, como CGI o Standalone), como aplicación nativa de 32 o 64 bits iOS y, en breve, también ¡sobre Android! Ahora bien, mientras que Xojo proporciona un lenguaje común que facilita enormemente la creación de estos productos, es cierto que no todas las clases son exactamente las mismas en cada una de las posibles plataformas de despliegue (Desktop, Web, iOS), puesto que cambian los métodos y/o eventos disponibles, así como otros parámetros. Sin embargo, en este artículo verás lo sencillo que resulta, y los cambios mínimos que son necesarios, para crear un control de UI personalizado multiplataforma que funcione sobre Desktop, Web y también iOS.

Descarga el proyecto desde el repositorio en GitHub.

LeftRightArrow: una flecha con mucha acción

Este artículo se basa en un control personalizado OpenSource que puedes clonar desde su repositorio de GitHub, y que también facilita que puedas seguir las explicaciones proporcionadas. Mediante este veremos como afrontar los cambios de dibujado y también de eventos disponibles, especialmente en lo que se refiere entre la plataforma de despliege iOS y el resto (Desktop y Web).

LeftRightArrow Desktop
Versión Desktop

El control propiamente dicho es bastante sencillo en su concepción y función: un botón con el dibujado de una flecha apuntando a derecha o izquierda que realice la acción programa cuando se hace clic sobre él; pero es el típico que he echado en falta de entre los disponibles en la Librería de Xojo en más de una ocasión, y esa ha sido finalmente la motivación de llevarlo a la práctica. El control personalizado está basado en la clase Canvas para Desktop (macOS, Windows, Linux y RaspberryPi), WebCanvas para los despliegues Web, y iOSCanvas para los despliegues iOS.

Para el dibujado de la flecha se utilizan los métodos recogidos bajo la clase Graphics (Desktop), WebGraphics (Web) y iOSGraphics (iOS), indicando la dirección de la misma mediante una propiedad de tipo Booleano y utilizando los colores que deseemos para los diferentes estados, también asignables utilizando las correspondientes propiedades de tipo Color: normal, apuntador sobre el control y control no activo (no disponible en iOS). Una propiedad booleana adicional también permite indicar si el control dibujará un borde de contorno, empleando en tal caso el color asignado en la correspondiente propiedad.

LeftRightArrow Web
Versión Web

Por las características inherentes de iOS, no existe un estado que indica el paso del dedo sobre o fuera del control, sino que los dos estados posibles para el control son el normal y el de acción (tap sobre el botón), representados ambos con su correspondiente color.

Adicionalmente, y para facilitar el uso del control una vez que lo incorporemos a nuestros proyectos, se ha utilizado la característica de Comportamiento del Inspector, de modo que las propiedades indicadas estén disponibles en el Panel Inspector del IDE de Xojo, así como para indicar en qué orden deseamos que aparezcan.

Por tanto, todas las propiedades del control en todas las plataformas de despliegue son las siguientes:

  • facingRight (Booleana, Pública). Indica en qué sentido se dibuja la flecha, por omisión hacia la derecha.
  • hasBorder (Booleana, Pública). Indica si el control ha de dibujar un borde de contorno en el color indicado.
  • paintColor (Color, Pública). Color para el dibujado del estado normal y activo del control.
  • mouseOverColor (Color, Pública). Color para el dibujado del estado correspondiente al apuntador dentro del control (Desktop y Web), o de tap sobre el botón (iOS).
  • disabledColor (Color, Pública, Desktop y Web). Color para el dibujado del control en el estado Disabled; es decir, no activo.
  • borderColor (Color, Pública). Color para dibujar el borde de contorno, en el caso de que se haya indicado.
  • currentColor (Color, Privada). Esta es la propiedad encargada de contener el color activo para el dibujado del control.
LeftRightArrow iOS
Versión iOS

Eventos consumidos, eventos definidos…

Con las propiedades ya definidas, añadimos al control los eventos que se van a utilizar para su comportmaiento. El primero de ellos es el evento Open y cuyo código es idéntico para las versiones del control Desktop y Web. Existe una ligera variación en el caso del control iOS, dado que en tal caso no se contempla la posibilidad de que un control esté inactivo.

Evento Open

El código para las versiones Web y Desktop del evento Open es el siguiente:

currentColor = If(Me.Enabled, paintColor, disabledColor)
invalidate
RaiseEvent Open

Es decir, se estable el colo actual al correspondiente en función de si el control está activado (Enabled = True) o no. Posteriormente se marca como inválido, forzando así el redibujado del mismo y se llama al evento Open (aun no definido). Esto permite que las instancias basadas en este control puedan incorporar y ejecutar su propio código dentro del evento Open.

La versión del código para la versión iOS es el siguiente:

currentColor = paintColor
Invalidate
RaiseEvent Open

Como puedes observar, la única diferencia es que la asignación del color en curso es directa al estado normal.

MouseEnter y MouseExit

Los siguientes eventos añadidos al control son MouseEnter y MouseExit. El nombre y signatura de dichos eventos es idéntica tanto en Web como en Desktop, de modo que puedes añadirlos en cualquiera de las versiones, escribir el código correspondiente y luego, simplemente, copiarlos y pegarlos bajo la clase correspondiente a la otra plataforma.

El código para MouseEnter será el siguiente:

currentColor = if(me.Enabled, mouseOverColor, disabledColor)
Invalidate
RaiseEvent MouseEnter

Y el código para MouseExit es este:

currentColor = If(Me.Enabled, paintColor, disabledColor)
invalidate
RaiseEvent MouseExit

Como puedes ver es bastante sencillo. Se limita a asignar el color correspondiente al estado en función de que el cursor apuntador se encuentre sobre el control o haya salido de los márgenes del mismo. Al igual que ocurría con el evento Open, posteriormente se marca el control para su redibujado y se llama al evento del mismo nombre (aun no definido en la clase), dando así la posibilidad de que las instancias basadas en estas clases puedan incorporar código propio para dichos estados.

En cuanto a la versión del control para iOS, el principal motivo de que no podamos copiar y pegar los eventos sobre la versión de la clase para esta plataforma es que, sencillamente, el framework de iOS no contempla estos evento y hemos de sustituirlos por PointerDown (se ha hecho tap sobre el control) y PointerUp (se ha dejado de pulsar el control).

PointerDown y PointerUp

El código para PointerDown es el siguiente:

currentColor = mouseOverColor
Invalidate
RaiseEvent PointerDown(pos, eventinfo)

Mientras que el código para PointerUp es este:

currentColor = paintColor
Invalidate
RaiseEvent PointerUp(pos, eventinfo)

Como puedes observar, no hay gran diferencia con respecto al código en comparación con la versión Desktop y Web: asignamos el color para cada estado, marcamos para su dibujado y llamamos al evento del mismo nombre para que las instancias de la clase puedan incorporar código propio sobre estos eventos.

Dibujado del control

El dibujado de los controles basados en Canvas, WebCanvas e iOSCanvas se produce en el evento Paint. Las diferencias entre Desktop, Web e iOS consiste en que la plataforma de escritorio proporciona como parámetros un contexto gráfico, de tipo Graphics y un Array de las áreas que requieren redibujado (si está vacío, entonces es preciso redibujar toda la superficie); la plataforma Web se limita a proporcionar un contexto gráfico de tipo WebGraphics; mientras que la plataforma iOS proporciona en su evento Paint un contexto gráfico de tipo iOSGraphics.

Los contextos gráficos Desktop y Web son lo suficientemente similares como para que se pueda utilizar nuestro código de dibujado sin realizar ningún tipo de modificación. Es decir, se puede copiar el siguiente fragmento en el evento Paint de ambas plataformas:

If Me.Enabled = False Then
currentColor = disabledColor
Else
If currentColor <> mouseOverColor Then currentColor = paintColor
End If
g.ForeColor = currentColor
Dim startX, startY, midX, midY, lastX, lastY As Integer
dim points() as integer
If facingRight = True Then
startX = 0
startY = 0
lastY = 0
lastX = Me.Height
midY = Me.Width
midX = Me.Height / 2
Else
startX = me.Height / 2
startY = 0
midX = 0
midY = Me.Width
lastX = Me.Height
lasty = Me.Width
End
points = Array( 0, startY, startX, midY, midX, lastY, lastX)
g.FillPolygon( points )
If hasBorder = True Then
g.ForeColor = borderColor
g.DrawPolygon( points )
End If

Sin embargo, el framework de iOS no proporciona el mismo tipo de clases para el dibujado, de modo que el código varía ligeramente quedando así:

g.FillColor = currentColor
Dim startX, startY, midX, midY, lastX, lastY As Integer
dim points() as integer
If facingRight = True Then
startX = 0
startY = 0
lastY = 0
lastX = Me.Height
midY = Me.Width
midX = Me.Height / 2
Else
startX = me.Height / 2
startY = 0
midX = 0
midY = Me.Width
lastX = Me.Height
lasty = Me.Width
End
Dim p As New iOSPath
p.MoveToPoint( starty, startx)
p.LineToPoint(midy, midx)
p.LineToPoint(lasty, lastx)
g.FillPath(p)
If hasBorder = True Then
g.fillcolor = borderColor
p.MoveToPoint( starty, startx)
p.LineToPoint(midy, midx)
p.LineToPoint(lasty, lastx)
g.FillPath(p)
End If

Cuando se trata de crear controles personalizados para nuestras interfaces de usuario en aplicaciones Web, conviene tener en cuenta que cada interacción requiere un viaje de ida y vuelta hacia el código que se ejecuta en la parte del servidor, y el correspondiente dibujado que se realiza en el lado del cliente. Por tanto, conviene mantener este tipo de acciones y la complejidad gráfica al mínimo posible. También, en estos casos, conviene mantener desactivada la opción DissableDiffEngine.

Definir nuevos eventos

Cuando definimos nuestras propias clases y consumimos una serie de eventos en ellas, estos dejan de estar disponibles para las instancias basadas en dicha clase. Es decir, si como hemos visto hemos utilizado los eventos Open, MouseEnter, MouseExit, PointerDown y PointerUp, entonces estos dejarán de estar disponibles para las instancias. ¿Qué mecanismos nos ofrece Xojo para que estos vuelvan a estar disponibles? Sencillo, definir exactamente los mismos eventos sobre la clase que estamos creando. La definición de eventos se lleva a cabo mediante la opción Insert > Event Definition, y lo único que hemos de hacer es utilizar el Panel Inspector para replicar la signatura (nombre, parámetros y tipo devuelto si procede) contemplados en el evento original que deseemos duplicar.

Una vez que se han añadido la definición de eventos, lo único que nos resta es añadir en los eventos consumidos la llamada hacia el evento definido, para lo cual se utiliza la instrucción RaiseEvent seguido del nombre del evento a llamar y proporcionando los parámetros que dicho evento pueda esperar.

La única consideración a tener en cuenta es el orden de invocación. Es decir, podemos decidir que se ejecute nuestro código en primer lugar y posteriormente invocar el evento para que se ejecute el posible código introducido por el usuario en la instancia de la clase… o hacerlo a la inversa. En este ejemplo se ejecuta primero el código de la clase, asegurando así el comportamiento esperado y, posteriormente, se da la oportunidad de que se ejecute el código de la instancia.

Conclusiones

Como puedes ver, crear una versión multiplataforma de un control de interfaz de usuario en Xojo es realmente sencillo. En la mayoría de los casos es suficiente con crear una versión de la clase (por ejemplo Desktop) y tras copiar y pegarla en el resto de plataformas (Web e iOS) cambiar la clase Super a las respectivas equivalencias de Canvas, modificar los eventos no disponibles por los equivalentes, y realizar finalmente pequeños cambios del código para adaptarlo según sea necesario. Por ejemplo, en el caso de este proyecto fue algo que no requirió más de cinco minutos. ¡Tremendamente potente!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *