La Biblioteca del IDE de Xojo ofrece por omisión una buena serie de componentes de interfaz gráfica de usuario listos para usar: botones, controles de introducción de texto (incluso con soporte de estilos), listado, menús desplegables, barras de progreso, paneles, etiquetas, reproductor de películas, selectores de fecha y hora, campo de búsqueda nativos, etc. Pero en muchas ocasiones nos encontramos frente a la situación de presentar exactamente la misma serie de controles, y con el mismo diseño o distribución, en más de una ventana. ¿Qué hacer en estos casos?
Lo que está claro es que sería todo un engorro volver a añadir de nuevo los mismos controles una y otra vez en cada una de las ventanas en las que han de utilizarse, además de que en cada caso habría que dotar a cada uno de estos controles de la misma funcionalidad (código) que ya se hubiese escrito anteriormente.
Precisamente para evitar este tipo de situaciones es para lo que te vendrá de perlas el uso de los ContainerControl. Podemos decir que se tratan de un tipo de componente que se encuentra a medio camino de lo que serían el resto de los controles disponibles en la Biblioteca (descendientes de la clase Control), y una ventana.
De hecho, si consultas entre las propiedades, métodos y eventos ofrecidos por los ContainerControl, verás que ofrece parte de la funcionalidad también disponible en las ventanas (Window).
Diseño con los ContainerControl
Comenzar a diseñar un ContainerControl no puede resultar más simple: añádelo al proyecto desde la biblioteca (por omisión tendrá el nombre ContainerControl1
), selecciónalo y verás que aparece un Editor de Diseño realmente similar al utilizado por las ventanas.
A partir de aquí, sólo tendrás que arrastrar los elementos de interfaz de usuario que quieras emplear desde la biblioteca sobre el container en el Editor de diseño.
Por supuesto, podrás utilizar el Panel Inspector para asignar las propiedades o atributos que desees tanto sobre los controles añadidos como al container control propiamente dicho.
Eso sí, a la hora de utilizar los iconos de anclaje de posición en los controles (sección Lock del Panel Inspector) has de tener en cuenta que dichos anclajes actuarán sobre los márgenes del ContainerControl que los incluye, mientras que cuando estableces estos mismos anclajes sobre el ContainerControl propiamente dicho, estos actuarán sobre la ventana en la que incorpores el ContainerControl.
Usar el ContainerControl en las ventanas
Una vez que tengas el ContainerControl diseñado como deseas, y con la funcionalidad que desees, ya puedes añadirlo a las ventanas o paneles que precises. Cuando lo hagas, pasará a ser una instancia de dicho ContainerControl (por omisión tendrá el nombre ContainerControl11
, dado que el elemento a partir del cual se crea se llama ContainerControl1
).
Eso sí, has de tener en cuenta que, una vez añadido, no podrás modificar la posición de los controles del ContainerControl en el diseño de la ventana… y tampoco podrás añadir funcionalidad a dichos controles en el Navegador cuando el ContainerCcontrol cuelga de la jerarquía de controles de la ventana.
Esto es algo que queda reflejado a simple vista dado que puedes observar como los controles embebidos en el item ContainerControl11
se muestran como “desactivados” en el Editor de Diseño de la ventana.
Es decir, sólo podrás modificar el diseño del ContainerControl (añadir nuevos controles o modificar la posición y dimensiones de los ya añadidos), así como la funcionalidad de los controles embebidos cuando selecciones directamente el elemento ContainerControl1
que se ha añadido desde la Biblioteca al proyecto.
Entonces, dado que sólo puedes añadir Manejadores de Eventos a los controles en ContainerControl1
, pero no en las instancias creadas a partir de dicho container control… ¿cómo puedes hacer que los controles se “comuniquen” con las ventanas que finalmente van a incorporar cada una de dichas instancias?
Puedes optar por varias técnicas. Por ejemplo, cuando se trata de que las diferentes ventanas accedan a cada uno de los controles del container control, sólo has de mantener como “Público” (o Public) el ámbito de cada uno de los controles que hayas añadido al ContainerControl en cuestión.
Así, por ejemplo, si hubiésemos añadido un ListBox (instancia con el nombre ListBox1
) al ContainerControl manteniendo su atributo Scope
como Public
en el Panel Inspector, entonces podríamos acceder desde el evento Open
de la ventana que lo contiene (por ejemplo Window1
) utilizando la notación por punto habitual:
me.ContainerControl11.ListBox1.AddRow "New Item"
Es decir, una de las dos vías de comunicación resuelta. Pero, ¿cómo podemos hacerlo a la inversa? Es decir, que llegue por ejemplo el valor de la fila seleccionada por el usuario en ListBox1
a la ventana que lo contiene.
El escenario es el siguiente: la ventana conoce cuál es el nombre del ContainerControl que se ha añadido, así como el nombre de los controles incluidos en dicho ContainerControl… porque están definidos como públicos.
Por otro lado, los controles conocen el nombre (aunque ni siquiera es necesario) del ContainerControl en los que están embebidos, pero no pueden saber el nombre de la ventana en la que se utilizará dicho ContainerControl.
Del mismo modo, el ContainerControl conoce el nombre de los controles que incluye… pero no sabe cuál pueda ser el nombre, así como los métodos o propiedades disponibles, en la ventana o ventana sobre los cuales pueda utilizarse. Sobre esto, sumemos además que los ContainerControl no sólo pueden utilizarse sobre las ventanas y otros componentes en tiempo de diseño desde el IDE… sino que también pueden crearse y añadirse de forma dinámica en tiempo de ejecución.
La forma más sencilla de facilitar la comunicación desde los controles del ContainerControl hacia la ventana o ventanas sobre las que se pueda utilizar es mediante la creación de métodos y Definición de Eventos sobre el propio ContainerControl añadido al proyecto (en nuestro ejemplo, se trataría de ContainerControl1
). Por supuesto, existen otras técnicas que puedes usar, pero veremos a continuación esta por ser la más fácil.
Continuando con el ejemplo, podemos añadir al ContainerControl ContainerControl1
un método con la siguiente signatura:
RowSelected(value As String)
A continuación, y con nuestro ContainerControl1
aún seleccionado en el Navegador, utilizaremos la opción Insert > Event Definition. Es decir, crearemos un Evento propio utilizando la siguiente signatura:
RowSelected(value As String)
Volvemos a seleccionar el método creado anteriormente para acceder al Editor de Código asociado, escribiendo la siguiente línea:
RaiseEvent RowSelected(value)
Es decir, el método se limitará a lanzar el evento que hemos definido pasando como argumento el valor recibido.
Selecciona ahora el item ListBox1
en el elemento ContainerControl1
y añade el Manejador de Evento Change
. Es decir, este se disparará o ejecutará cada vez que el usuario cambie la fila seleccionada en el listado. Teclea a continuación el siguiente código en el Editor de Código asociado con el evento añadido:
If me.SelectedRowIndex <> -1 then self.RowSelected(me.SelectedRowValue) End if
Añade también el evento Open
sobre ListBox1
, tecleando el siguiente código en el Editor de Código asociado:
For n As Integer = 0 To 10 Me.AddRow n.ToString Next
Ahora ya sólo nos queda añadir el evento definido en ContainerControl1
sobre la instancia ContainerControl11
que hemos incluido en el diseño de Window1
. Selecciona dicho ítem y elige seguidamente la opción Add > Event Handler. Verás que entre las opciones disponibles se encuentra ahora el nombre del evento definido por nosotros. Selecciónalo y confirma haciendo clic en el botón “OK”.
En el Editor de Código asociado, escribe la siguiente línea:
MessageBox "Selected value: " + value
Por supuesto, en tus aplicaciones querrás hacer algo más útil con los valores recibidos; pero nos sirve perfectamente para mostrar cómo se pueden enviar los valores desde los controles hacia cualquiera de las ventanas o controles en los que puedan estar incluidos.
Subclases de ContainerControl
También puede darse el caso de que en tus aplicaciones quieras usar varios ContainerControl y que en muchos de estos siempre vayas a incorporar controles ListBox. Como con cualquier otra clase, sería genial definir tanto el método SelectedRow
como la Definición de Evento con el mismo nombre en una subclase de modo que, posteriormente, estén disponibles para todos aquellos ContainerControl añadidos al proyecto y que vayan a incorporar un ListBox.
La buena noticia es que en Xojo también puedes crear tus propias subclases de ContainerControl tal y como es el caso con el resto de controles y clases; si bien aquí es ligeramente distinto, tal y como queda recogido en la Guía del Usuario.
A diferencia de lo que ocurre con otros controles, el simple hecho de añadir un nuevo ContainerControl desde la Librería al Navegador e indicar en el Panel Inspector que su clase Super
es ContainerControl1
, no funcionaría. Si intentases ejecutar el siguiente código:
Var newContainerInstance As ContainerControl = new ContainerControl2
Arrojaría el siguiente error al ejecutar la aplicación:
“The project item ContainerControl2 can not inherit from a window (ContainerControl1).”
Para solucionarlo, añade una nueva clase al proyecto y cambia su nombre en el Panel Inspector a MyContainerClass
, definiendo su Super
como ContainerControl
.
A continuación, selecciona el método SelectedRow
y la Definición de Evento del mismo nombre en ControlContainer1
, usa la opción de menú Edit > Cut, selecciona MyContainerClass
y usa la opción de menú Edit > Paste. Tanto el método como la definición de evento estarán ahora en MyContainerClass
.
Vuelve a seleccionar ContainerControl1
en el Navegador y utiliza el Panel Inspector para cambiar el campo Super
a MyContainerClass
.
Añade un segundo ContainerControl al Navegador (pasará a llamarse ContainerControl2
) y define su campo Super
en el Panel Inspector a MyContainerClass
. Añade un control ListBox a dicho container y teclea el siguiente código en el Evento Open
de ListBox1
:
For n As Integer = 0 to 10 me.AddRow n.ToString Next
Añade también el evento Changed
a ListBox1
y vuelve a introducir el siguiente código:
If Me.SelectedRowIndex <> -1 Then Self.SelectedRow(Me.SelectedRowValue) End If
A continuación, agrega este segundo container ContainerControl2
a la ventana de proyecto Window1
. Por ejemplo, el diseño podría ser similar al mostrado en la siguiente imagen:
Selecciona el item ContainerControl21
bajo Window1 > Controls y añade el manejador de eventos SelectedRow
. Nuevamente, nos limitaremos a introducir sólo la línea encargada de mostrar un mensaje con el valor seleccionado:
MessageBox "Selected value: " + value
Como puedes ver, tanto ContainerControl1
como ContainerControl2
no incorporan el método SelectedRow
y tampoco la definición de evento del mismo nombre. Estos son heredados desde la clase de la cual descienden: MyContainerClass
.
Crear y Añadir nuevos ContainerControl en tiempo de ejecución
Hasta ahora hemos añadido los Container Control durante el diseño de la aplicación, pero parte de su potencia radica en el hecho de que podemos añadirlos también en tiempo de ejecución. Veamos cómo mediante otro sencillo ejemplo.
Selecciona Window1
y elimina los elementos ContainerControl11
y ContainerControl21
. Con Window1
aun seleccionado, añade una nueva propiedad utilizando los siguientes valores en el Panel Inspector:
- Name: Containers()
- Type: MyContainerClass
A continuación, añade el evento Open
a Window1
y escribe el siguiente código:
Var leftContainer As New ContainerControl1 leftContainer.Width = 200 leftContainer.LockTop = True leftContainer.LockRight = False leftContainer.LockBottom = False leftContainer.LockLeft = true leftContainer.EmbedWithin(Me,20,20) Containers.Add leftContainer Var rightContainer As New ContainerControl2 rightContainer.Width = 200 rightContainer.LockLeft = True rightContainer.LockTop = True rightContainer.LockBottom = True rightContainer.LockRight = True rightContainer.EmbedWithin(Me, Me.Width-20-rightContainer.Width, 20) Containers.Add rightContainer
Ejecuta la aplicación, cambia el tamaño de la ventana y observarás que el contenedor de la derecha ajusta su altura y ancho en función de los cambios realizados en el tamaño de la ventana, mientras que el contenedor de la izquierda se limita a mantener su posición.
Prueba a hacer clic ahora sobre cualquiera de las filas en el listado de los contenedores. ¡No pasa nada!
Como recordarás, para que la información llegase a las instancias necesitábamos añadir el Manejador de Evento SelectedRow
. Esto es algo que no podemos hacer cuando añadimos nuevas instancias de forma dinámica (es decir, en tiempo de ejecución).
La solución sin embargo es bien sencilla, utilizando para ello la palabra clave AddHandler
que nos permite redireccionar un evento a un método delegado. Para ello, con Window1
seleccionado, añade un nuevo menú con la siguiente signatura:
ListBoxSelectedRow(item As MyContainerClass, value As String)
Observa que el primer parámetro se corresponde con la instancia de MyContainerClass
que lanza el evento, mientras que en el segundo parámetro obtenemos el valor en sí.
Lo único que falta es asociar el evento con la dirección del método sobre cada una de las instancias que estamos añadiendo en tiempo de ejecución. Para ello, vuelve al evento Open
de Window1
y modifica el código para que sea el siguiente:
Var leftContainer As New ContainerControl1 leftContainer.Width = 200 leftContainer.LockTop = True leftContainer.LockRight = False leftContainer.LockBottom = False leftContainer.LockLeft = true AddHandler leftContainer.SelectedRow, WeakAddressOf ListBoxSelectedRow leftContainer.EmbedWithin(Me,20,20) Containers.Add leftContainer Var rightContainer As New ContainerControl2 AddHandler rightContainer.SelectedRow, WeakAddressOf ListBoxSelectedRow rightContainer.Width = 200 rightContainer.LockLeft = True rightContainer.LockTop = True rightContainer.LockBottom = True rightContainer.LockRight = True rightContainer.EmbedWithin(Me, Me.Width-20-rightContainer.Width, 20) Containers.Add rightContainer
Las líneas añadidas realmente interesantes son:
AddHandler leftContainer.SelectedRow, WeakAddressOf ListBoxSelectedRow AddHandler rightContainer.SelectedRow, WeakAddressOf ListBoxSelectedRow
Como ves, mediante AddHandler
estamos indicando que el evento SelectedRow
de cada instancia pasará a ser ejecutado por el método ListBoxSelectedRow
que hemos añadido a Window1
.
Por otro lado, WeakAddressOf
es un operador del lenguaje que nos permite obtener una referencia “débil” a la dirección de memoria en la que se ubica el método ListboxSelectedRow
, de forma que pueda ser invocado.
Conclusión
A lo largo de este artículo hemos podido ver algunos de los fundamentos a la hora de añadir controles ContainerControl a nuestro proyecto, utilizarlos en tiempo de diseño sobre las ventanas de la aplicación, comunicar los controles incluidos en dichos Container con la ventana en la que puedan ser añadidos, crear una subclase que comparta una serie de elementos comunes (en este caso un método y una definición de evento) sobre todas las nuevas instancias creadas a partir de ellos, añadir nuevas instancias de ContainerControl sobre la ventana en tiempo de ejecución, y también cómo podemos delegar el funcionamiento de un evento del ContainerControl a un método definido sobre la propia ventana.