Programación General > Visual Basic para principiantes

 Tutorial: Crear un control de usuario en VB (Controles ActiveX)

<< < (3/3)

Nebire:
En esta nueva parte acometemos una solución para realizar el foco.

Como sabeis, en un contenedor que tiene varios controles, siempre debe haber uno que sea el control 'actual', el que tiene la atención. Como efectivamente si hay varios se hace más que necesario casi obligatorio, indicar de alguna manera cual de ellos tiene la atención, el foco, ya que se requiere que el usuario final de la aplicación sepa a donde irá a parar por ejemplo una pulsación de teclado. si en este hipotético caso pulsamos la combinación de letras "ALT + P" puede tener un efecto totalmente distinto si se pulsa sobre un botón cuyo texto indica '&Pausar'  que sobre una lista de texto. Sobre el botón se ejecutará el evento 'click', mientras que para la lista se ejecuta el evento 'mousedown'. Si ahora en vez de la combinación dada pulsamos solamente "P", en el botón se ejecuta el evento mousedown, en cambio  en la lista se selecciona un ítem que empiece por 'P', la siguiente pulsación avanza al siguiente ítem que empiece por 'p'.

Por tanto es necesario que el usuario final sepa en todo momento que control tiene el foco.

Es el contenedor quien suplementa casi toda la funcionalidad del foco, por ejemplo provee la propiedad TabIndex a cada control, de modo que cuando se pulsa la tecla 'TAB' (tabulador), envía el foco al control cuya propiedad tabindex dentro del mismo contenedor le sigue al actual, y cuando alcanza el último control del contenedor, la siguiente pulsación va al siguiente control donde el contenedor (que también es un control) está contenido...

Ahora bien aunque el contenedor proporciones casi toda la funcionalidad, la de ofrecer al usuario la impresión de 'ganar el foco' o 'perder el foco' es responsabilidad total del programador que elabora el control.
VB proporciona el efecto de foco a sus controles, por ejemplo si vemos un botón que tiene el foco, éste es rodeado perimetralmente con una línea de puntos. Si miramos un listbox, hace algo casi idéntico, sobre el último elemento seleccionado, lo rodea con una línea de puntos, debido a que está seleccionado, no resulta fácil verificarlo, en cambio si cambiais la propiedad del listbox, 'Multiselect' a 1, vereis como pudiendo seleccionar más de 1  ítem, podreis apreciar claramente la linea de puntos, más aún cuando 'deseleccionais' un elemento, que deja de estar seleccionado, pero sin embargo conserva el foco.

Esto no es todo, el usercontrol proporciona una propiedad (como contenedor que es) llamada ActiveControl, que entrega el control que actualmente tiene el foco. Esta misma propiedad la podemos encontrar en el formulario... los contenedores pueden o no tenerla de acuerdo a si el diseñador 'exportó dicha propiedad o no al cliente, por ejemplo el contenedor picturebox, no lo tiene, pero nada impide que si estamos diseñando un contenedor nosotros si exportemos dicha propiedad. el usercontrol, lo mantien y es útil  si hemos incroporado varios controles en nuestro control (imaginad que en vez deconstruir el botón con tratamiento gráfico, hubiéramos incluído el icono mediante un control image y el texto mediante un control label, etc... Como queremos tener un control absoluto de lo que hace, puede hacer y como lo ha de hacer, nos decantamos (en su momento) por realizarlo todo, nosostros, y no depender de tal o cual funcionalidad que dichos controles pueden tener, carecer o simplemente que no conozcamos en profundidad su funcionamiento y nos resulte por tanto difícil adaptarlo a nuestras necesidades.

También sabeis que el posible enviar el foco a un control mediante la función SetFocus del propio control y que esto sólo funciona si se dan 3 condiciones, el control (o contenedor, incluso un formulario) ha sido cargado, está visible y su propiedad 'enabled' es true, de otro modo´, falla. Por supuesto el usercontrol, proporciona dicha función, pero además cuando se compila el control dicha función se incluye (función extendida) al control,  por lo que no es necesario exponer dicho método en nuestro control explícitamente (el compilador lo hace por nosotros) salvo que necesitemos incluir código, para, por ejemplo, redirigir  el foco a un control constituyente, o realizar determinadas acciones.

Cada contenedor, es de alguna manera responsable, quien se encarga de enviar el flujo al control que corresponda, de hecho sucede una cascada de eventos que es preciso conocer para dotar a nuestros controles de la debida funcionalidad.
Esto sucede a través del evento 'GotFocus', y 'LostFocus', sin embargo hay otros  2 eventos disponibles durante la creación de un control, 'EnterFocus' y 'ExitFocus'. (los eventos  'GotFocus', y 'LostFocus', también se adhieren al compilar si nosostros no los hemos incluído).
EnterFocus, sucede antes que GotFocus, y ExitFocus sucede después de LostFocus. Esto es importante saberlo para poner el código justo en el evento deseado. No es frecuente que necesitemos usar Enterfocus y ExitFocus, pero si que se usa con cierta frecuencia Gotfocus y LostFocus.

Bien, habiendo quedado claro algunos aspectos relativos al foco, ya nos centramos propiamente en el diseño de nuestro control. Como hemos dicho queremos y tenemos que reflejar de alguna manera cuando nuestro control tiene el foco, esto lo haremos de 2 maneras, la primera (que es la que hoy nos ocupa) es gráfica, visual pero para ayudar a usuarios con deficiencias visuales, también la haremos sonora, pero en la siguiente parte.

El modo en que VB refleja el foco es simple pero sobrio y a la vez soso, de hecho hay una API, que realiza esto mismo, dibujar el foco en la manera que lo hace VB, sin embargo como nuestro control provee un relieve perimetral pudiera entorpecer y en cualquier caso no nos complace lo simple que resulta, luego para el caso , he decidido dibujar una pelota que se mueve rebotando por las paredes del control... cual espectro fantasmal en un castillo...  Eso si, la pelota ha de tener un tamaño lo suficientemente pequeño para que no resulte molesto y los suficientemente grande como para que sea advertido, en cualquier caso nunca conviene que sea mayor de cierto tamaño para evitar un exceso de consumo gráfico, por tanto una 'pelota' de entre 6 y 20 ´px, de radio es suficiente para controles muy pequeños hasta controles muy grandes. en nuestro caso vamos a fijar el tamaño de la pelota a 12 píxeles.

Definamos entonces con claridad como ha de operar:
   Una pelota de 12px (tamaño fijo)
   Cuando gana el foco, la pelota se ve y se mueve
   El movimiento es en la forma: rebotando diagonalmente (angulos de 45º) contra los bordes del control.
   Cuando pierde el foco, se queda invisible y deja de moverse
   Cuando se redimensiona el control, la pelota se centra en el control y su movto. continúa en la dirección que llevaba
   Para dibujar la pelota usaremos un control de tipo  shape, que son muy ligeros y nos permiten algunso efectos gráficos sencillos sin complicarnos en exceso. Además nos permite elegir entre varias formas geométricas (que uno podría seleccionar aleatoriamente).
    La pelota ha de moverse en unidades de tiempo, que podemos probar y fijar valores una vez estemos de acuerdo en como funciona. Básicamente si es muy rápido puede resultar molesto y distraer en exceso laatención, y si es muy lento, puede pasar desapercibido... de un modo simple se acepta que 300msg. es un tiempo de respuesta que el ojo percibe con claridad, luego procuremos que exista un movimiento suficiente en esos 300msg. para que la atención visual lo perciba sin distraernos  (que no cambie de ser observado por un ojo  a otro en su mayoría).

Antes de empezar a meter código, primero declararemos las variables que vamos a usar...:

--- Código: Visual Basic --- ' Variables que usa la pelota del foco'Private s_UcAncho           As Long  ' limite a la derecha, ya la tenemos declarada y sus valores convenientemente actualizados'Private s_UcAlto            As Long  ' limita abajo, ya la tenemos declarada   "  ídem                 "                  "' 0 limite izquierdo y superiorPrivate dX                  As Long   ' desplazamiento unitario en el eje XPrivate dY                  As Long   ' desplazamiento unitario en el eje YPrivate sX                  As Long   ' mitad del  ancho de la pelota,  (de momento)Private sY                  As Long   ' mitad del alto de la pelota, (de momento)  
Vayamos pués escribiendo el código. Lo 1º es añadir un control shape a la interfaz del control, que llamamos ShaFoco. Así mismo el control (la pelota), lairemos moviendo con un control Timer que llamaremos TimFoco. Y ahora si, creamos dentro del evento timer una llamada a una función que realiza el movimiento definido como acordamos, le pelota al chocar contra los límites del control, rebota... El movimiento es diagonal (45º).

--- Código: Visual Basic --- Private Sub ReboteDiagonal()    With ShaFoco        ' desplazamos la pelota        Call .Move(.Left + dX, .Top + dY)        ' comprobamos si choca y        ' cuando alcanza un límite cambia el avance en esa dirección por el opuesto        If .Top <= -sY Then dY = -dY        If .Top >= s_UcAlto Then dY = -dY        If .Left <= -sX Then dX = -dX        If .Left >= s_UcAncho Then dX = -dX    End WithEnd Sub  
El movimiento de la pelota ya está descrito, ahora fijamos donde se le llama y cuando...


--- Código: Visual Basic --- ' la función es llamada desde el timer (no ejecutada en el timer), porque así podemos turnar entre diferentes funciones demovimiento que hayamos diseñado... Como se verá un poco más adelante cuando modifiquemos.Private Sub TimFoco_Timer()    Call ReboteDiagonalEnd Sub ' en el método readproperties, al final:    TimFoco.Interval = 20    dX = 3: dY = 3  
En diseño desactivamos el timer TimFoco, no queremos que se ejecute nada más cargarse el control si no sólo cuando reciba el foco. Podría ponerse opcionalmente en el evento initiproperties la línea: TimFoco.Enabled = False, para garantizar o recordar que así sea y además también en Readproperties, junto a las líneas que acabamos de poner.
El intérvalo del timer, también puede serfijado en diseño sobrela interfaz, pero resulta muy evidente como recordatorio si se especifica también en readproperties.
Debe tenerse en cuenta que tanto la propiedad interval del timfoco como los valores de desplazamiento de lapleota (dX y DY), son los que determinan la sensación de velocidad de movimiento de la pelota.. valores altos de Dx y DY proporciona una sensación de avance a saltos, cuanto más pequeño más fluído pero entonces el avance por unidad de tiempo es escasa, con lo que necesitamos disminuir interval, si interval se reduce mucho la rutina de avance de la pelota se ejecuta muchas veces, debeis encontrar un punto que satisfaga tanto la visual como el 'castigo' de la CPU. en lo expuesto avanza 3 px cada 20 msg, es decir en 300msg, avanza 45 px. para un tamaño típico de botón puede ser adecuado o incluso mucho. Yo lo pongo fijo, pero sería acorde que tanto el tamaño de la pelota como el avance dependiera del tamaño del control. El objetivo es ilustrar el efecto y vosotros ya lo adpatereis a vuestras consideraciones y necesidades.

Fijaos que dichos valores dX y DY, no son guardadas como propiedades, sino que se fijan a un valor inicial y si es preciso se cambian dinámicamente, debeis eso si aseguraros que cuando se asigne un valor basado en una función, nunca tomen  valor 0, que implicaría linea recta en la otra dimensión, o peor aún, si ambos son 0, se queda fijo y no se mueve. En nuestro caso los valores posibles que tomarán  serán siempre o 3 o -3, además logramos que el movimeinto sea diagonal en el momento que dX=dY, si quereis algún otro ángulo debeis jugar con dicha relación. dX=dY proporciona los ángulos 45º+0, 45+90, 45+180, 45+270 Esto es simples matemáticas..

Ahora la intercepción de puesta en marcha y paro del timer.

--- Código: Visual Basic --- Private Sub UserControl_GotFocus()    ShaFoco.Visible = True    TimFoco.Enabled = TrueEnd Sub Private Sub UserControl_LostFocus()    TimFoco.Enabled = False    ShaFoco.Visible = FalseEnd Sub  Como se indicó el foco sólo se recibe si el control está cargado, visible y activo, por eso usamos estos eventos para poner en marcha el timer y recíprocamente 'ver' el shape...

El shape en diseño puede ponerse la propiedad visible=false, además en diseño establecemos las siguientes propiedades (estas quedarán fijas operativamente) o bien pueden establecerse (también) al final en readproperties:

--- Código: Visual Basic --- ShaFoco.visible=falseShaFoco.DRawMode= 6 'invert'ShaFoco.Fillstyle=0 ' sólidoShaFoco.Shape= 3 ' círculo  En cuanto a fillcolor, no importa, puesto que tenemos drawmode en 'invert', si fijáramos otro valor este color se fusionaría con el de la imagen, pero según el color elegido y el de la imagen podría no ser satisfactorio el resultado, es cuestión de que probeis...
En cualquier caso una pelota de color rojo siempre destaca si el fondo no es tambén similar...

Sin embargo nuestro movimiento no es perfecto, si lo ejecutamos en este momento, porque todavía no hemos establecido los valores para el punto de impacto 'sX' y 'sY'. Añadimos la siguiente línea justo debajo de donde pusimos dX=... dY=...

--- Código: Visual Basic --- 'Donde pusimos estás líneas: 'TimFoco.Interval = 20  dX = 3: dY = 3 añadimos debajo esta otra:sX = ShaFoco.Width / 2: sY = ShaFoco.Height / 2  Ahora ya la detección de choque se realiza exactamente igual por los 4 costados. Este es el propósito de dicha línea (los valores que luego se utilizan para el cálculo de la colisión)

Ya nos falta menos.. Qué pasa si en medio de la marcha de la pelota se desactiva el control ?... el timer está en marcha, entonces, seguirá corriendo ?. No, porque cuando el control sedesactiva, se pierde el foco, el contenedor tiene que enviarlo a otro control, a qué control ?, justo al siguiente con el tabindex que le sigue dentro del mismo contenedor. cuando esto sucede se recibe un evento lostfocus, que recordemos desactivaba nuestro timer y hacía invisible al shape.
Podemos comprobar dicha funcionalidad, vayamos al formulario, añadamos varios controles, fijémosos en el tabindex de la instancia de nuestro control y en qué control sigue en el tabindex al control. Ahora añadamos un tmer al formulario pongamos en interval el valor 10000 (10 segundos) y este código dentro de su evento:

--- Código: Visual Basic --- Private Sub Timer1_Timer()    CtlBoton1.Activo = FalseEnd Sub  Ahora ejecutemos el proyecto y hagamos sólo una cosa, pulsemos en nuestro botón, para que tenga el foco y esperemos... después de 10 segundos saltará el timer y desactivará el botón, y veremos como aquel control que le segúia en el tabindex es ahora el que gana el control. si queremos verificarlo mejor, id al código del botón y poned un punto de parada en el evento: 'Private Sub UserControl_LostFocus()' . Volved a correr el proyecto, vereis como al pasar los 10 segundos, se invoca dicho evento, nuestro control pierde el foco y se para el timer y se oculta la pelota. Luego tal como hemos dicho, no tenemos necesidad de añadir código en el caso de deshabilitar el control.

Efectivamente falta menos, es el caso de cuando el control cambia de tamaño. si era pequeño y luego se hace más grande, no importa, pero imaginad un caso alrevés, un botón de 800x200px  que luego se reduce a 200x20 px... (probadlo si quereis). ...un posible resultado es que si cuando se cambia el tamaño la pelota tiene una posición fuera del nuevo tamaño, los límites ahora harán que rebote cuando se alcancen, es decir rebotarán, pero 'por fuera'. Podemos probar que sucede esto cambiando ligeramente el código del timer que añadimos al formulario cuando anteriormente hemos probado a desactivarlo:

--- Código: Visual Basic --- ' Antes que nada haced el control de un tamaño de por ejemplo 500x200px. y ejecutad el proyecto tras hacer los cambos sugeridos en el siguiente código:Private Sub Timer1_Timer()    'CtlBoton1.Activo = False    With CtlBoton1        Call .Move(.Left, .Top, 150, 24) ' recordad que menos de 24 de medida, no dibujaba el icono    End WithEnd Sub  
Esto entonces debemos controlarlo, desde el evento resize de nuestro control, reubicando la pelota (por ejemplo) en el medio de nuestro control...

--- Código: Visual Basic --- ' localizad las líneas comentadas en el código del evento resize, justo antes de portero=false, añadimos la línea de código referida al ShaFoco...    'End If        Call ShaFoco.Move(s_UcAncho / 2, s_UcAlto / 2)    '    Portero = False    'Else  Ahora ejecutad de nuevo el proyecto y esperad a que salte el timer, vereis que ahora si la pelota sigue rebotando, pero dentro, como debe ser...
También debis notar como la sensación de velocidad es mayor sobre un control  muy pequeño con respecto a un control muy grande, es por eso que os dije que puede interesar hacer dinámico (variable) la propiedad interval del timer,  el avance de la pelota dX y dY... y el tamaño de la pelota. También es adecuado pensar que en controles muy alargados sean verticalmente u horizontalmente mantener un ángulo de movimiento diferente de 45º, probad a fijar elsiguiente valores para dX y dY:

--- Código: Visual Basic ---   dX=3: dY=1  Estos valores sobre uncontrol de poca altura y muy largo permite un avance horizontal con menos rebotes verticales, que cuando dx=dy... probad a cambiar los valores entre si en dX=1: dY=3 y ejecutadlo, ahora osresulta más evidente qué solución podeis adoptar en cada caso...

Para finalizar esta parte, incluiremos una nueva rutina de 'tengo el foco'. Digamos que la rutina actual resulta muy evidente y queremos una solución parecida pero más sobria... para lo cual parece adecuado por ejemplo que nuestra pelota simplemente 'de vueltas' cual atleta alrededor del estadio... Hay que decidir en qué sentido gira para darle los valores adecuados...
Lo 1º es crear la rutina que realiza el movimiento:

--- Código: Visual Basic --- ' giro antihorarioPrivate Sub VueltasAlEstadio()    Static avance As Byte    With ShaFoco        ' desplazamos la pelota        Call .Move(.Left + dX, .Top + dY)        ' comprobamos si choca y        ' cuando alcanza un límite cambia el avance en esa dirección por la siguiente        Select Case avance            Case 0  'está bajando, se alcanzó eltope ? si, sim entonces avanzamos a derecha                If .Top >= (s_UcAlto - sY) Then ' ahora mueve a derecha                    dX = dY: dY = 0                    avance = 1                End If            Case 1  ' a derecha, se alcanzó el tope derecho ?, si si, avanza subiendo                If .Left >= (s_UcAncho - sX) Then ' ahora sube                    dY = -dX: dX = 0 ' dy tendrá ahora un valor negativo                    avance = 2                End If            Case 2  ' esta´subiendo, alcanzó el tope superior ?, si si avance a izquierda                If .Top <= 0 Then '-sY Then ' ahora mueve a izquierda                    dX = dY: dY = 0                    avance = 3                End If            Case 3  ' avanza a izquierda, alcanzó tope?, si, si, ahora bajará..                If .Left <= 0 Then '-sX Then  ' ahora baja                    dY = -dX: dX = 0 ' ahora dy tomará un valor positivo                    avance = 0                End If        End Select    End WithEnd Sub  Es importante notar que un estado de avance se comprueba hasta que dicho estado cambie, es decir suceda el caso, mientras no se comprueba ningún otro caso.... a diferencia de lo que sucedía en la rutina anterior (ReboteDiagonal) donde debían verificarse todas los topes cada vez.

Luego quedan algunos cambos menores, como la invocación a la rutina que hemos creado ahora:

--- Código: Visual Basic --- ' llamar a la rutina nueva en vez de a la anterior que rebotabaPrivate Sub TimFoco_Timer()    'Call ReboteDiagonal    Call VueltasAlEstadioEnd Sub  
Cambiar los valores de 'dX' y 'dY', ahora se mueven en línea recta, luego uno debe valer 0 cuando el otro tenga un valor distinto, esto se hace en la rutina previa, pero hay que establecer inicialmentelos valores (no es imprescindibles, ante el primer choque se establecería un valor a 0.
También el valor de choque, antes el impacto se detectaba cuando se alcanzaba el centro de la pelota, ahora queremos que se detecte cuando alcance la parte extrema de la pelota, como antes, esto se calcula en la rutina anterior, pero necesitamos establecer los valores adecuados para 'sX' y 'sY'.

--- Código: Visual Basic --- ' buscamos enreadproperties y alteramos el código, dejamos copia comentada de lo anterior para apreciar diferencias     'dx=3 dy=3    dX = 0: dY = 2    'sX = ShaFoco.Width / 2: sY = ShaFoco.Height / 2    sX = ShaFoco.Width: sY = ShaFoco.Height   Si quisiéramos que la pelota circulaa a una distancia de margen respecto de los topes del control, añadiríamos esos píxeles a 'sX' y 'sY'. Pero además en nuestra rutina de movto. deberíamos en los topes que se comprueban con '0', poner el valor de margen elegido, lo mejor pués sería usar una variable llamada MargenFocoEstadio (o algo similar), cuando no queramos margen ese valor es 0, y esa variable se suma a sx y sy y el '0' de las comprobaciones se remplazar por dicha veriable.
quedaría por ejemplo así:

--- Código: Visual Basic --- ' If .Top <= 0 Then '-sY Then ' ahora mueve a izquierda If .Top <= MargenFocoEstadio  Then '-sY Then ' ahora mueve a izquierda  Recordar que el movto es sentido antihorario, cambiar el sentido es tan sencillo como cambiar de orden las veces que aparece la variables de 'avance', al valor correcto (o cambiando el valor de cada 'case').

Y con esto damos por terminado esta parte, sólo recordaros que podeis crear diferentes rutinas de movimiento y quedaros con la que más os guste, talvez un movimiento de zig-zag de derecha a izquierda sobre el centro del control, o... lo que se os ocurra.

---------------------

En la próxima parte trataremos un aspecto ligado al foco, pero sobre el que VB no ofrece ninguna funcionalidad, salvo para controles sin ventana, que no es precisamente el caso... hablamos concretamente de 'control apuntado' pero que no mantiene el foco aún, no ha sido enfocado, pero el cursor está sobre el control...

Nebire:
Perdonad que no haya terminado... pero he estado bastante ocupado.

En las próximas semanas espero encontrar el tiempo suficiente para terminar las partes que quedan...

Jimbenit:

--- Cita de: "Nebire" ---Perdonad que no haya terminado... pero he estado bastante ocupado.

En las próximas semanas espero encontrar el tiempo suficiente para terminar las partes que quedan...
--- Fin de la cita ---
Tranquilo amigo, este tema es un buen manual de consulta.

Navegación

[0] Índice de Mensajes

[*] Página Anterior

Ir a la versión completa