• Viernes 6 de Diciembre de 2024, 06:08

Autor Tema:  Tutorial: Crear un control de usuario en VB (Controles ActiveX)  (Leído 28519 veces)

Nebire

  • Miembro HIPER activo
  • ****
  • Mensajes: 670
    • Ver Perfil
Tutorial: Crear un control de usuario en VB (Controles ActiveX)
« en: Martes 7 de Septiembre de 2010, 03:31 »
0
En este tutorial, se enseña como crear controles de usuario en VB6, desde 0. Crearemos un botón totalmente personalizado, partiendo de las características siguientes:
1 - Que admita una imagen ajustada siempre al tamaño del control.
2 - Que pueda llevar opcionalmente un icono.
3 - Que pueda llevar opcionalmente un texto y que este pueda ser alineado.
Otras características se irán definiendo a lo largo del tutorial, en la docena (aprox.) de mensajes que acabará teniendo el tutorial.

El tutorial se va presentando en partes de modo que cada parte es un mensaje y el único motivo de ser de cada parte es el tiempo libre que disponga mientras lo desarrollo. Aun así, a modo de índice, se expone lo que se discute en cada una de las partes.

:smartass:

Parte 1: Preparación del proyecto y creación de los archivos.

Parte 2: Se diserta acerca de la intencionalidad del control y se informa bastante acerca de las propiedades, creamos nuestra primera propiedad y se explica la persistencia de las propiedades más allá de la sesión.

Parte 3: Proporcionamos más propiedades (ColorTexto, Texto, Alineaciontexto, Fuente e Icono) y ahondamos en más detalles sobre las mismas. Inicialización de las propiedades. También señalamos cuando existe la importancia o no de provocar errores, o de 'salir adelante' cuando el error es perfectamente derivable.

Parte 4: Se añaden 4 propiedades más, necesarias para poder empezar a dibujar, Activo, Imagen , Icono y IconoTamaño además se detalla como inicializar el control con una imagen cargada que luego es pasada a una variable interna.

Parte 5: Se realiza la primera parte del dibujado, se explica los 2 métodos para dibujar y mantener actualizado los gráficos, se recalca la importancia de ordenar en capas, se explica el evento resize y se diserta acerca de cómo superar los escollos de errores que a veces resultan casi imperceptibles exponiendo un ejemplo, finalmente se acomete una optimización y se explica los 'porteros' como control de repetición de código por llamadas que a su vez llaman  a quien los ha invocado (recursividad involuntaria).

Parte 6: Se ahonda en la importancia del evento resize para mostrar como se controla el tamaño del control y se termina de pintar el control, haciendo el Relieve y dando una solución gráfica al estado deshabilitado del control.

Parte 7: Aprovechando un pequeño bug, se explica el orden e importancia de la cascada de eventos de inicialización, se señalan optimizaciones para el repintado y también se aborda un par de optimizaciones funcionales para la propiedad imagen. Se indica como aportar y guardar la info (documentación) de las propiedades, métodos y eventos disponibles para el cliente .

Parte 8: Se empieza a explicar lo fundamental de los eventos, partes de que se compone. Dónde se debe ubicar un evento y porqué. Control sobre bucles infinitos a causa de los eventos. Control sobre desbordamiento de pila a causa de los eventos. Cómo se logra una propiedad de X escrituras-lecturas. Eventos síncronos y asíncronos, como generarlos. Prioridad en los eventos.

Parte 9: Se realizan los eventos de teclado y ratón, que al no tener apenas código adicional, no precisa muchos comentarios.

Parte 10: Se explican cuestiones referentes al foco, y se aborda una solución gráfica para reflejarlo, la ganancia y pérdida de foco, se sugieren 2 rutinas parecidas entre sí y se invita a realizar otras de curso similar...
« última modificación: Viernes 1 de Febrero de 2013, 01:14 por Nebire »
«Ma non troppo»
----> ModoVacaciones = False<----

Nebire

  • Miembro HIPER activo
  • ****
  • Mensajes: 670
    • Ver Perfil
Re: Guía: Crear controles de usuario en VB
« Respuesta #1 en: Martes 7 de Septiembre de 2010, 03:35 »
0
En esta primera parte vamos a describir todo lo necesario para poder desarollar los proyectos y crear los archivos necesarios para crear el control y probarlo. Incluyendo la primera prueba del control, sin código alguno, listo para la siguiente parte en que iré exponiendo código y las razones de qué, cómo, cuándo y porqué de cada cosa.

______________________________
Para empezar es conveniente saber que un control de usaurio se crea cuando necesitamos exponer una interfaz de usuario, es decir algo gráfico. Desde este punto de vista estos controles deberían mejor llamarse 'control gráfico de usuario' frente a las clases que podrían llamarse 'control no gráfico de usuario', como el orden de suceso de las cosas fueron de una manera cada cosa se llamó como en su momento se consideró oportuno. También es importante señalar que quien ya ha trabajado con clases encontrará bastante asequible trabajar con controles de usuario.

Ahora crearemos todos los archivos y proyectos que necesitamos para empezar, se creará un proyecto para elcontrol y un proyecto para probarlo y ambos proyectos serán controlados por lo que en el entorno VB se llama 'grupo de proyectos' . Así abramos el entorno de desarrollo y elegimos eltipo de proyecto 'control Activex'.  Ahora cambiamos el nombre del proyecto (para no perdernos en explicaciones conviene que le des el mismo nombre que sugiero, luego en tu desarrollo dale el nombre que prefieras, éste nombre elegido es por comodidad en la didáctica, nada más) desde 'proyecto1' a 'PryBoton'. Ahora igualmente cambiamos el nombre del control de usuario desde 'UserControl1' a 'CtlBoton'. finalmente le damos al botón guardar para guardar el proyecto, la ruta elígela como prefieras yo sugiero algo como: 'C:\Mis proyectos\controles\Botón simple'.

Una vez creado el proyecto del control, podremos cerrar el entorno y volverlo a abrir o bien desde el menú 'archivo' elegir 'crear nuevo proyecto', ahora elegimos un proyecto de tipo 'exe estándar'. Igualmente que con el anterior a este proyecto le renombramos desde 'proyecto1' a 'Probar_ctlBoton' . el nombre del formulario no importa. Ahora guardamos el proyecto, la ruta será la misma que elegimos para el control pero creamos una carpeta llamada 'pruebas' y lo guardamos dentro de esa carpeta ('C:\Mis proyectos\controles\Botón simple\Pruebas'). Este proyecto está destinado a probar el control mientras lo estamos diseñando e incluso para probarlo una vez hayamos compilado el control.

Para que ambos proyecto trabajen juntos ambos deben estar dentro de un grupo de proyectos, así ahora vamos al menú 'archivo' y elegimos 'añadir proyecto' y desde la ficha 'recientes' elegimos el proyecto 'pryBoton' (o bien lo localizamos desde la ficha existente, también podríamos haber creado en este momento el proyecto del botón) y lo abrimos. Si nos fijamos bien en el cuadro de herramientas se nos ha añadido el icono para el control que hemos añadido (más adelante se indicará como proporcionar un icono personalizado para el control). Seleccionamos dicho icono del control (veremos que el tooltip del icono señala el tipo de control que es (ctlBoton) . Bien pués ahora pongamos una instancia del control en el formulario del proyecto de prueba.
Ahora pulsamos el botón de guardar proyecto, el archivo del grupo de proyectos 'grupo1.vbg' (vb-Group ) lo guardamos también donde el proyecto de prueba, no es necesario cambiarle el nombre. Si ahora cerramos el entorno cuando volvamos a abrir el entorno ahora buscaremos abrir el archivo de proyectos, 'grupo1.vbg', ya que si sólo abrimos uno de los proyectos se abrirán sólo los archivos concerniente a ese proyecto, este archivo de 'grupo' es el que vincula 2 o más proyectos entre sí. Mas adelante se darán las indicaciones oportunas a la hora de compilar el control.

Bien ahora ya tenemos todo listo para empezar. Vayamos al control de usaurio y abramos el control, sobre las propiedades del control busquemos la propiedad backcolor cambiémosla por cualquier color que no sea gris... rojo, negro va bien. Si apartamos la ventana del control lo suficiente para ver el formulario veremos que el control aparece 'rayado', esto es indicativo de que el control está en 'modo diseño' . Cerremos la ventana del control (la ventana no el proyecto) veremos que inmediatamente nuestro control en el formulario cambia, no sólo desaparece el rayado sino que ahora aparece del color que le elegimos para el fondo, se dice que ahora el control está en 'modo de ejecución'. Esto es importante tenerlo en cuenta, ya que un poyecto de vb estaba en ejecución cuando pulsábamos f5 y aquí aún no lo hemos pulsado.

Podemos proceder, pero primero damos al botón paa guardar todos los cambios operados (yo en mis opciones de vb tengo por defecto que cuando ejecute un proyecto me pida si se guardan o no los cambios así, yo no necesito pulsar el botón, pero hay que acostumbrarse a guardar los cambios antes de ejecutar (el proyecto deprueba). Pulsemos la tecla F5 y veremos nuestro control en ejecución, aunque de momento no hace nada, sin embargo se ve y (en mi caso aparece rojo).

Ya hemos creado los archivos necesarios del proyecto, por tanto desde ahora abriremos el entorno desde el archivo Group1.vbg que como señalé es el que mantiene unido ambos proyectos el del control y el de prueba.

Hay que señalar que cuando hay un grupo de proyectos, sólo uno es el 'proyecto inicial', el que se va a ejecutar cuando se arranque el proyecto para su ejecución, en este caso el proyecto inicial es el de prueba ya que el del control actúa como secundario dentro del proyecto de prueba... justo cuando se inicializa el formulario (cuando se carga los controles ya deben haberse cargado, de hecho se carga justo cuando el último control constituyende del formulario se ha cargado) se invoca la carga de todos y cada uno de los controles que lo integran. Para hacer la prueba vayamos al código del formulario y escojamos el procedimiento de evento del formulario initialize, este sería su código :
Código: Visual Basic
  1.  
  2. Private Sub Form_Initialize()
  3.     MsgBox "Formulario inicializando"
  4. End Sub
  5.  
  6.  
Acto seguido vamos a la ventana de código del control, también en su procedimiento de evento initialize.... este sería su código:
Código: Visual Basic
  1.  
  2. Private Sub UserControl_Initialize()
  3.     MsgBox "Control inicializando"
  4. End Sub
  5.  
  6.  

Para verificar lo que se ha dicho finalmente exponemos código en el load del formulario:
Código: Visual Basic
  1.  
  2. Private Sub Form_Load()
  3.     MsgBox "formulario inicializado  y cargando..."
  4. End Sub
  5.  
  6.  

Ahora ejecutamos paso a paso el grupo de proyectos (tecla F8).. veremos que vamos al procedimiento initialize del formulario, luego al initialize del control y finalmente al load del formulario.

Para el diseño del control esto no tiene mucha importancia, pero es precisamente lo que nos ayudará a entender la cascada de sucesos de arranque del control. Si se piensa bien el evento initialize del formulario ocurre una sola vez, en cambio el evento load del formulario puede ocurrir varias veces durante la vida del mismo, esto mismo sucede con el control, aunque más adelante se detallará esta cuestión. Ahora lo que nos importa es saber si el proyecto de inicio fue exitosamente el de prueba. si fue el del control debemos ir al 'explorador de proyectos' (CTRL + R)  y fijarnos cualo de los dos proyectos aparece en negrita, eso marca el proyecto inicial, si es necesario, por tanto pulsamos click sobre el proyecto de prueba y sobre el menú emergente pulsamos en 'establecer como inicial'.

Debe quedar constancia que ambos proyectos pueden ser creados en el mismo momento, si elegimos crear un proyecto 'exe', después podemos indicar ene l menú añadir nuevo proyecto y elegir un proyecto de tipo 'control Activex', o también al revés. conviene en todo caso, guardar cada proyecto en su carpeta y lo mejor es que dentro de la carpeta del control haya unca carpeta "probar" o "pruebas", donde constará el proyecto de prueba y el ejecutable del grupo de proyectos. El proyecto de prueba no tiene otro interés que ir probando que la funcionalidad que se dota al control es la que queremos darle y que funciona correctamente. Para ello, se usará un formulario donde al menos se deberá poner una instancia del control y utilizarlo para probarlo.

También es posible crear un proyecto de tipo 'exe' y adjuntar un 'nuevo control Activex', pero al carecer éste de su propio proyecto, el control será privado y se compilaría sólo con el proyecto, 'exe', no aparte. Esa es la opción ideal cuando deba construirse un control que sólo se va a usar en un único proyecto y del que no pretendamos usar en ninguna otra parte. al compilarlo de esta manera, se compila como parte del ejecutable (proyecto1.exe) y por tanto no se genera un UserControl1.ocx. Es importante tener esto en cuenta...

Ya borramos todo el código introducido anteriormente y guardamos el proyecto.
Ya estamos listos para empezar a diseñar nuestro botón. Esta es la primera parte, los preparativos iniciales, realmente lleva más tiempo explicarlo que hacerlo... y bastante más si además se tiene que teclear las indicaciones así que ruego paciencia.

Mañana empezaremos a diseñar el botón y explicaremos el ciclo de vida de un control de usuario.
« última modificación: Jueves 31 de Enero de 2013, 19:58 por Nebire »
«Ma non troppo»
----> ModoVacaciones = False<----

Nebire

  • Miembro HIPER activo
  • ****
  • Mensajes: 670
    • Ver Perfil
Re: Guía: Crear controles de usuario en VB
« Respuesta #2 en: Martes 7 de Septiembre de 2010, 03:40 »
0
Empezamos la siguiente parte. Hoy se describe acerca de la intencionalidad del control y parte acerca de las propiedades, así como algunas características de las propiedades que son arropadas por el usercontrol.

Cuando vamos a crear un control, previamente es conveniente hacer una descripción sobre un papel (papel, fichero de texto, eso al gusto de cada uno) de que funcionalidad es la que esperamos que tenga, evidentemente siempre en medio del proyecto podremos hacer añadidos y de hecho los haremos a propósito para ver como afecta los añadidos a última hora. Sin embargo cuanto mejor tengamos explicitado  lo que queremos que haga en mejores condiciones estaremos para proveer lo que precisa...

Es habitual que la razón por la que queramos construir un control es que determinado existente no tenga tal o cual característica/s por lo que nuestro modelo podría ser una copia en cierto modo similar a otro ya existente pero con algunos cambios.... por ejemplo, a mi nunca me gustó que los botones de microsoft no tuvieran una propiedad 'forecolor', por narices siempre es negro (se puede cambiar empleando APIs, pero no hay una propiedad), si queremos cambiar el color del fondo debemos cambiar la propiedad estilo a 'graphical', sólo entonces toma el color que hayamos designado a la propiedad backcolor, ese comportamiento también nos parece 'idiota', otra cuestión es que por ejemplo admite una imagen, pero se pega al tamaño real, también debe estar establecido estilo a graphical (Command1.style=1: Command1.picture= loadpicture(ruta...)) . Para colmo incluso creando una imagen justo a la medida del botón, si es oscurapor abajo nos 'tapa' el texto del botón... en fin como puede sentirse, hay razones que ya justifican nuestro botón y ya de entrada tenemos unas descripciones de lo que tiene que ser...

Vamos pués a proceder a dar una somera descripción de lo que pretendemos crear... El control debe poder cambiar el color del fondo, el color del texto, alinear el texto horizontalmente, admitir una imagen ajustada en todo momento al fondo, tener capacidad para un icono alineado siempre a la izquierda y poseer un borde perimetral que de momento no definimos como se comportará (podría ser fijo o que se hunda al pulsar y se eleve al soltar), debe tener algunas propiedades típicas, activo (enabled), font, etc...

Es una descripción pequeña, pero para empezar a contruirlo nos vale, luego a medida que vayamos diseñando y probando terminaremos de definir tal vez nuevas propiedades o cambios de funcionalidad... esto debe entenderse como algo lógico y razonable, tampoco es estrictamente necesario tener una descripción exhaustiva del mismo, siendo como es para nosotros... A medida que uno se familiariza creando controles bastantes ideas de la descripción están ya en la cabeza y no necesitamos tener que plasmarlas por escrito, en cambio si conviene dejar escrito esas funcionalidades específicas que pretendíamos para el control ya que si no, cabe la posibilidad de que tras algún tiempo sin retomar el proyecto se nos haya olvidado por completo la misma, la funcionalidad que lo haría diferente de otros modelos previos que ya hayamos realizado, por ejemplo puede que este nuevo botón se deba a que queramos unir varios botones en línea y columna formando una tabla y por ello queramos redefinir la forma en que se dibuja el relieve de los mismos... o podríamos querer en un nuevo botón que tenga una pequeña animación cuando se pasa el ratón por encima, o simplemente añadir un sonido cuando se oprime y suelta elbotón... estas 2 últimas funcionalidades las utilñizaremos como un añadido de última hora cuando se dé al control por terminado...

El mejor sitio para dejar escrito la funcionalidad (las ideas de como debe operar) es usar un archivo de texto en la misma carpeta que el control, así siempre estará disponible.

Por tanto empezaremos definiendo propiedades bastante conocidas: Backcolor, forecolor, enabled, Text... pero a mi particularmente me gusta usar el español que es mi idioma, por tanto todas las propiedades y demás se reflejarán en lo posible en español... hay una razón más o menos poderosa para dejarlo en inglés, que ya para el final se indicará si no me olvido, y si me olvido me lo recordais (me refiero al final de la elaboración del control).

Las propiedades pueden usarse tanto en clases como en formularios y por supuesto en los controles que es donde más lo vemos. Las propiedades son un trío de funciones realmente pero que el entorno de VB lo enmascara como si fueran algo distinto de funciones, de hecho podríamos olvidar las propiedades y tratarlas como funciones pero nos perderíamos ciertas funcionalidades del entorno, como mostrar los valores de las propiedades del control en la 'Ventana de propiedades'.

Las propiedades como digo son un trío, una devuelve un valor y las otras 2 entregan un valor. de estas 2 una se utiliza cuando se usa un tipo de datos genérico y la otra cuando se usa como tipo de datos un objeto.
A continuación iremos poniendo código y explicando...
Código: Visual Basic
  1.  
  2. Public Property Get ColorTapiz() As OLE_COLOR
  3.      ColorTapiz = UserControl.BackColor
  4. End Property
  5.     Public Property Let ColorTapiz(ByVal ct As OLE_COLOR)
  6.         If ct <> UserControl.BackColor Then
  7.             UserControl.BackColor = ct
  8.             UserControl.PropertyChanged "ColorTapiz"
  9.             ' añadidos posteriores
  10.         End If
  11.     End Property
  12.  
  13.  
Acabamos de definir la propiedad ColorTapiz (colorFondo, BackColor) para el control, analicemos cada parte...
GET: es como una función, devuelve un valor, se usaría así en el formulario: dim X as long: x= CtlBoton1.Colortapiz : msgbox x
Public: porque queremos que esta propiedad sea legible, si quisiéramos que no se pudiera leer pondríamos private y sería una propiedad sin derecho a lectura.
LET: es la asignación de la propiedad, puede verse que es como una función tiene un parámetro que es por donde se recibe el valor, a dicho parámetro le hemos lamado, ct (abreviatura de Colortapiz, yo suelo usar esta convención), este parámetro no es visible desde fuera, luego no importa el nombre que se le dé. Let también se declaró public, si lo hubiéramos declarado private la propiedad estaría privada de escritura, es decir sólo podría invocarse para ser leída pero nunca podría escribirse.
Más adelante se mostrará que es posible dar más de un parámetro a las propiedades, sin embargo debe quedar claro que 1º no es obligatorio el par de procedimientos de la propiedad, puede usarse sólo el get y no aparecer el let, o al revés. 2º si aparecen ambos deben ser congruentes en el tipo de datos y en la cantidad de parámetros, veremos que siempre LET tiene un parámetro más que GET, 3º en las propiedades del control (en el formulario) sólo aparece una propiedad si aparecen ambos métodos y también ambos son públicos, si uno de los 2 es privado no aparece en las propiedades del control. (conviene que luego se pruebe a cambiarlos a private uno de ellos y se verá como deja de aparecer).

La propiedad almacena un color que a fin de cuentas es un tipo de datos long, porqué lo hemos declarado del tipo OLE_COLOR ?, porque cuando se declara de esa manera el entorno de VB nos despliega una 'página de propiedades'  para elegir visualmente el color, si utilizamos un tipo de datos long, desde el entorno sólo podríamos introducir valores numéricos desde el teclado. Los controles pueden poseer páginas de propiedades que es casi un pequeño mundo lo mismo que los controles pero que una vez conocidos los controles son más sencillos de conocer, el uso de páginas de propiedades para los controles facilita al programador poder cambiar cómodamente algunas propiedades incluso a varios controles a la vez, la contrapartida es que diseñar una página de propiedades nos lleva más tiempo y hace al control más pesado (el control una vez compilado pesa más)... si al final hay ganas podríamos crear una sencilla página de propiedades.

 Véase que usando una propiedad en vez de 2 funciones, ambos métodos se llaman iguales, usando funciones por necesidad cada uno debería tener un nombre distinto)... Ahora ponemos un ejemplo de como se asigna la propiedad del control en el formulario.... Dim x as long : x= 1234567: CtlBoton1.ColorTapiz = x .Aquí escritura y antes dimos un ejemplo de lectura de la propiedad. Antes de seguir, podemos probar ya esta nueva propiedad, para ello debemos ir al formulario y pulsar en el control, en las propiedades del control veremos que aparece la propiedad ColorTapiz y podemos ver como tiene el color que le asignamos (el tipo Ole_color hace que el entorno nos muestre el color visualmente en vez de un número y podemos cambiar el color y ver como el cambio se refleja en el control...por ejemplo cambíalo a azul... IMPORTANTE: si el control en el formulario aparece 'rayado diagonalmente', tal como se dijo ayer es porque está en modo diseño, para poder probarlo en el formulario es preciso ponerlo en modo ejecución, y para ponerlo en modo ejecución tenemos que cerrar la ventana del control (no la de código si no la de la interfaz del control)... esto no lo voy a repetir más veces, debe quedar claro YA.

Sólo nos queda describir el código introducido en los métodos de la propiedad ColorTapiz... para el get le decimos que colortapiz es igual a un valor que el propio control ya mantiene una variable... (Cada contenedor provee determinada funcionalidad, disponible para los objetos insertados en él y los contenedores del entorno de VB proveen propiedades como Backcolor,y algunas otras que se irán describiendo, al objeto Usercontrol, nuestra instancia. Por tanto la línea 'ColorTapiz = UserControl.BackColor' le está diciendo que  entregue la propiedad almacenada en la variable Backcolor que provee ya el control.
En cuanto al código de la asignación LET, lo primero que hacemos es verificar si el color recibido es el mismo, y sólo si es distinto se lo asignamos a la propiedad backcolor del usercontrol. La razón de esto es doble por un lado más adelante tenemos que añadir funcionalidad al final del código de este método, dicho código queremos que sólo se ejecute si realmente el color asignado cambió si no estaremos ejecutando cosas para que quede todo como estaba antes. La otra razón es que ahora el valor del color se guarda en la propiedad backcolor del control (en el que hemos delegado), pero si (cambiamos el color del tapiz a azul como se sugirió más arriba al probar y cerramos el entorno y volvemos a abrirlo, volverá a ser rojo, es decir el cambio no se ha mantenido, justo para eso está la siguiente línea: PropertyChanged "ColorTapiz", esta línea le  dice al entorno que esta propiedad ha cambiado, sin embargo esta línea no trabaja sola sino que trabaja en conjunto con 2 métodos del control, uno se encarga de guardar a fichero el cambio registrado (cuando le demos al botón de guardar cambios en el proyecto), y la otra funcionalidad se encargará de volver a leer el valor cambiado cuando arranquemos el proyecto, o se ponga en modo de ejecución el proyecto.

La última línea de código del método LET es una línea comentada, más adelante tenemos que introducir código ya que el cambio del color del fondo, implica que nos borrará el resto del contenido gráfico del control, por lo que se añadirá una línea que salte a una función que vuelva a dibujar todo lo que teníamos y que se ha borrado. Como ese método aún no lo tenemos, dejamos una línea comentándolo.

Justo ahora describiremos estos métodos que ayudan a almacenar de forma permanente las propiedades y que permiten luego al ejecutarlo volver a leer ese valor guardado y lo dejamos por hoy, mañana iremos más rápido con las siguientes propiedades una vez que hemos comprendido de forma general acerca de las propiedades, faltan detallesque se irán presentando según surjan los casos.

Para probar que el color no permanece, vayamos al modo ejecución y cambiemos el color del fondo... y acto segudio pulsemos la tecla F5, veremos que aparecerá el formulario pero con el color que le asignamos en la propiedad backcolor de la ventana de propiedades de la interfaz del control.

Ahora añadamos este código al control (abajo del todo):
Código: Visual Basic
  1.  
  2. Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
  3.     With PropBag
  4.         .WriteProperty "ColorTapiz", UserControl.BackColor, Ambient.BackColor
  5.    
  6.     End With
  7. End Sub
  8.  
  9. Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
  10.     With PropBag
  11.         UserControl.BackColor = .ReadProperty "ColorTapiz", Ambient.BackColor
  12.        
  13.     End With
  14. End Sub
  15.  
  16.  
Estos métodos pueden localizarse en la ventana de código del control sobre la lista de procedimientos del control (usarcontrol), por tanto elparámetro éste de propbag, lo provee el entorno de VB, este es un objeto interno de VB cuya funcionalidad procede de una interfaz cuyo objetivo es precisamente la de almacenar y recuperar valores de propiedades que se han asignado durante el diseño (impronta), es algo así como si compras un vehículo, cuando se fabrica es metal y debe ser pintado para evitar que se oxide, aunque a los vehículos se los elige el color y se piden se nosenvía cuando la fábrica dispone de una demanda suficiente de ese color para cambiar los tambores de pinturas en el proceso en cadena, por ejemplo para pintar 200 vehículos seguidios de ese color. sin embargo aquí en VB es como si todos salieran con una capa antióxido gris (que en el entorno se llama  Ambient.Backcolor y que es el contenedor quien decide ese color, de hecho es el color del contenedor, es como si la fábrica fuera azul por defecto todos los vehículos salieran azules), luego para asignar el color que actualmente hay guardado el objeto Propbag (Bolsa de propiedades) le dice asigna a la variable (Usercontrol.Backcolor) leyendo en las propiedades guardadas ( .Readproperties) el valor de la propiedad ColorTapiz  que se guardó bajo ese mismo nombre en el fichero, y si no se encuentra en el fichero un valor guardado tomamos el valor almacenado en usercontrol.backcolor actualmente.

Es decir el método readproperties se compone de 3 partes la asignación (usercontrol.Backcolor =), la invocación  del método del objeto pararecuperar el valor (propbag.Readproperties), pero que como usamos with, nos ahorramos escribir en cada línea el nombre del objeto), y los parámetros que necesita esta invocación, que al caso son 2, el nombre de la propiedad guardada y un valor que se asigna por defecto si tal propiedad no se encuentra: UserControl.BackColor = .ReadProperty ("ColorTapiz", Ambient.BackColor) igualmente en vez de poner por defecto el color del contenedor podríamos poner un color específico, por ejemplo: .ReadProperty( "ColorTapiz", vbBlue) o incluso un valor numérico: .ReadProperty ("ColorTapiz", 234567)

El método readproperties ocurre 1 única vez (realmente es un evento que se dispara para el usercontrol) y es cuando el control entra en modo de ejecución, una vez leídas las propiedades ya no se vuelve a invocar dicho método.

Writeproperties, es el método que usa el control para que se guarden las propiedades y ocurre también una sola vez, justo antes de que el control sea descargado de memoria. Esto sólo sucede mientras la 'aplicación cliente' está en diseño, es decir el control está en modo de ejecución, ya que en modo diseño lo que se guarda es el valor de la propiedad que nosostros le dimos en la ventana de propiedades del control, pero ese no es accesible a nosotros. Es decir nosotros hemos diseñado que cada vez que un cliente utilice el control éste aparecerá cuando se cargue por primera vez la instancia el color rojo, sin embargo el cliente querrá y podrá cambiar  que cuando su aplicación se ejecute el control aparecerá con el color que él eligió, es ese valor el que nosotros guardamos en writeproperties, los cambios que el cliente introduce cuando está diseñando la aplicación.

Este método consta de un parámetro más que el método readproperties del objeto propertybag: guarda a fichero (writeporperties) una propiedad con el nombre "ColorTapiz", cuyo valor actual está en la variable: Usercontrol.Backcolor (la variable que actalmente contiene su valor) y si el valor no se encuentra o no es válido entonces asigna éste por defecto (Ambient.Backcolor, el del contenedor donde se halleel control). Igualmente que para readproperties podría asignar un valor directo, como vbBlue ó 56789. Conviene en cualquier caso que en ambos sitios el color asignado por defecto sea el mismo en ambos casos

Bien ahora que ya hemos descrito como se logra que las propiedades permanezcan entre sesiones, podemos volver al formulario cambiar el color a uno que no hayamos usado (por ejemplo un violeta) y guardar el proyecto para que ocurra un evento writeproperties y efectivamente se guarden, luego pulsamos la tecla F5, con lo que ahora se crea el control (recordemos el evento initialize que probamos al principio de esta parte) y luego se invoca el evento readproperties, que asigna las propiedades que hemos guardado. en el evento initialize el color será rojo, pero ahora tras el readproperties será el que le dimos la última vez (violeta)...
Y finalmente por hoy ya que hemos guardado el color violeta para esta instancia veamos como al añadir una nueva instancia del control al formulario el color del fondo de éste es el rojo. Ahora debería quedar claro que valores son los que se guardan en writeproperties y se leen con readproperties Y también debería quedar claro el significado del método propertychanged.
También podemos probar a cambiar la propiedad del control por código, no sólo desde la ventana de propiedades de los objetos del formulario. Por ejemplo añade un botón microsoft y escribe código, para que cuando se pulse cambie nuestro control de color o nos diga el color actual.

Después hacer todas las pruebas que queramos, conviene dejar sólo una instancia del control (del que estamos diseñando) en el formulario para no liarnos cuando mañana continuemos...

Mañana ahondaremos en las propiedades y crearemos bastantes más ahora que se ha explicado como crearlas. También mañana explicaremos como se soportan propiedades que ni siquiera existen, backcolor lo posee el usercontrol y enabled, por lo que en estos caso podemos delegar en elas aunque como hemos visto podemos cambiarle el nombre por el camino...
«Ma non troppo»
----> ModoVacaciones = False<----

Nebire

  • Miembro HIPER activo
  • ****
  • Mensajes: 670
    • Ver Perfil
Re: Guía: Crear controles de usuario en VB
« Respuesta #3 en: Martes 7 de Septiembre de 2010, 03:46 »
0
Si alguien no se ha acabado de enterar muy bien de las propiedades puede abrir el grupo de proyectos ir al código que escribimos seleccionar la palabra property 1 pulsar la tecla F1 que nos lleva a la ayuda, naturalmente se explicará bien incluso mejor que yo el significado y declaración de la propiedad aunque todo el intríngulis de las propiedades no aparece... pero entre la ayuda y mis indicaciones creo que todo el mundo podría saciar sus dudas.

En el apartado de ayer se explicó gran parte de las propiedades y también se explicó la persistencia (preservación) de los valores de las propiedades. Hoy avanzaremos con más propiedades de nuestro control y se indicarán más detalles acerca de las propiedades a medida que surja cada caso.

Quedamos en que íbamos a requerir determinadas propiedades: ColorTapiz (BackColor), ColorTexto (ForeColor), el propio Texto del botón (Caption o Text), alinear el texto, un Icono,  una Imagen autoajustable al botón... pués hoy procederemos con el código para estas propiedades y de fondo otras característica sobre el Usercontrol...

El código para la propiedad ColorTexto (ForeColor)
Código: Visual Basic
  1.  
  2. Public Property Get ColorTexto() As OLE_COLOR
  3.      ColorTexto = UserControl.ForeColor
  4. End Property
  5.     Public Property Let ColorTexto(ByVal ct As OLE_COLOR)
  6.         If ct <> UserControl.ForeColor Then
  7.             UserControl.ForeColor = ct
  8.             UserControl.PropertyChanged "ColorTexto"
  9.             ' añadidos posteriores
  10.        End If
  11.     End Property
  12.  
  13.  
Vemos que prácticamente podemos copiar el código del Backcolor y hacer manualmente los cambios (de hecho yo lo he hecho así). ColorTexto es lo que nos va a permitir cambiar el color al texto que se escribe en el botón, de momento no hemos realizado la propiedad texto, por lo que aparece también la línea comentada 'añadidos posteriores. Lo dicho ayer sobre Usercontrol.BackColor  es aplicable también sobre Usercontrol.ForeColor, es una propiedad también sobre la que delegamos ya que la 'plantilla' que se nos presenta del contrl la trae, estas propiedades sobre las que podemos delegar pueden verse sobre la ventana depropiedades de la ventana de la interfaz del control. Igualmente si por defecto deseamos que la aplicación cliente tenga nada más cargar una instancia en un formulario un color determinado es en esa ventana donde debemos designarlo. Yo lo dejo en negro.

Debe quedar claro desde ya que todas las propiedades que vayamos a añadir y que el control posee delegaremos sobre ellas, salvo  que la funcionalidad sea totalmente distinta a la que realiza la plantilla... si surge un caso así ya se explicará.

Es conveniente (no obligatorio) que cada vez que añadimos una propiedad hagamos los propio para su persistencia, añadiendo las entradas correspondientes en los métodos Readproperties y Writeproperties... Yo copio una entrada anterior, pego y luego la modifico... entonces ya tendremos esto:
Código: Visual Basic
  1.  
  2. Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
  3.     With PropBag
  4.         UserControl.BackColor = .ReadProperty("ColorTapiz", Ambient.BackColor)
  5.         UserControl.ForeColor = .ReadProperty("ColorTexto", Ambient.ForeColor)
  6.        
  7.     End With
  8. End Sub
  9.  
  10. Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
  11.     With PropBag
  12.         .WriteProperty "ColorTapiz", UserControl.BackColor, Ambient.BackColor
  13.         .WriteProperty "ColorTexto", UserControl.ForeColor, Ambient.ForeColor
  14.        
  15.     End With
  16. End Sub
  17.  
  18.  
Es decir ayer pusimos la del color tapiz ahora metemos la del color texto.... en lo sucesivo sólo pondré la línea concreta como recordatorio, ya vosotros sabeis donde debe añadirse (a continuación de la anterior y dejamos una nueva línea para la siguiente entrada... bueno esto al gusto).

Ahora vamos con la siguiente propiedad; Texto. Vemos que el control no tienen una propiedad Text ni Caption, esto significa que no podemos delegar en ella sino que la tenemos que contruir completamente. Al hacerlo se entenderá fácilmente su lógica y se explicará como se le cede su primer valor.
Código: Visual Basic
  1.  
  2. ' esta línea de código debe ir arriba del todo, por encima de cualquier declaración de propiedades y funciones...
  3. Private p_Texto As String   ' el texto del botón se almacena en esta variable internamente.
  4.  
  5. ' esta línea podemos ponerla a continuación (debajo de) la propiedad ColorTexto.
  6. Public Property Get Texto() As String
  7.     Texto = p_Texto
  8. End Property
  9.     Public Property Let Texto(ByVal t As String)
  10.         If t <> p_Texto Then
  11.             p_Texto = t
  12.             PropertyChanged "Texto"
  13.             ' añadidos posteriores
  14.        End If
  15.     End Property
  16.  
  17.  
Como se puede ver, el valor de la propiedad realmente la almacenamos en una variable (p_Texto) declarada privada, por tanto  no es accesible desde fuera y debe verse como lógicamente la hemos declarado de tipo string. Surge una pregunta lógica de parte del aprendiz, porqué no guardamos directamente la propiedad sobre la variable p_Texto ?. De hecho podríamos hacerlo, pero entonces si tenemos algo como esto:
Código: Visual Basic
  1.  
  2. public TextoBoton as string
  3.  
  4.  
...Es realmente una propiedad con todas las de la ley, pero presenta unos inconvenientes para determinadas situaciones: 1º Si utilizamos una propiedad así, no hay forma de saber cuando el cliente ha cambiado su valor, por tanto tampoco sabemos cuando debe actualizarse lo que deba actualizarse.
2º Si necesitamos garantizar que el valor que introduce el cliente (el cliente en este caso es el programador que utiliza el control compilado) cumpla ciertos requisitos no tenemos forma de hacerlo porque como se indicó antes no sabemos cuando se introduce un nuevo valor.

Al expresarse las propiedades en forma de 2 funciones, una que entrega el valor y otra que lo recibe, la que lo recibe cuenta con 2 notificaciones, a) que se recibe un valor, B) si lo deseamos podemos comprobar si ese valor es distinto del actual, es decir si cambia de valor. Hasta ahora si os fijais en el código de las 3 propiedades que llevamos hasta el momento, en los 3 casos hemos verificado si el valor que se introduce es distinto del actual, ya se explicó ayer porque hacemos esa comprobación, no siempre la haremos como severá más adelante, básicamente cada vez que una comprobación se amás engorrosa o conlleve más tiempo que hacer las operaciones que un cambio supone.

Hemos creado la propiedad texto, pero sabemos que cuando se declara una variable antes de usarse su valor es el que VB asigna por defecto, en el caso de los números (byte, long,etc...) siempre valen 0 y en el caso de los tipo string valen "" es decir una cadena vacía. Naturalmente nosotros podemos querer que ya de entrada cuando se cree una instancia sobre el formulario del cliente éste texto (y en general casi todaslas propiedades), tenga ya un valor concreto y determinado por nuestro interés, etc... bien esto se llama inicialización de variables y nos interesa que sólo ocurra una vez, justo cuando se ha creado la instancia, luego ya sabemos que writepoprties guardará el valor  y que readproperties lo recuperará, además si el cliente cambia el valor entonces el que se guardará y luego se leerá será ese valor y no el que nosotros inicialmente le dimos, para que este funcionamiento sea así, por tanto este valor debe ejecutarse una sola vez, justodurante la creación por primera vez de la instancia. es preciso saber que el evento initialice ciertamente crea la instancia, sin embargo cuando se pasa al modo ejecución se destruye y es vuelta a crear es decir  se  vuelve a ejecutar initialize, parece pués un buen sitio para colocar ese código, pero claro si  se carga y descarga 10 veces el formulario, también 10 veces se ejecuta initialize y por tanto 10 veces le estamos asignando nuestro valor 'de capricho' siendo que luego 10 veces también se ejecutará el readproperties... no sería más útil que sólo se ejecutara 1 única vez ?.... pués esto es precisamente lo que hace un procedimiento del usercontrol.... ponemos el código (puede encontrarse el método en la lista de procediemientos de eventos cuando en la lista de la izquierda tenemos seleccionado el usercontrol, si tenemos seleccionado general, aparecerán las propiedades y funciones que vayamos creando ):

Por tanto en la lista izquierda seleccionamos usercontrol y en la lista derecha desplegamos hasta encontrar 'InitProperties'
Código: Visual Basic
  1.  
  2. Private Sub UserControl_InitProperties()
  3.     p_Texto = UserControl.Extender.Name
  4. End Sub
  5.  
  6.  
En este código hemos asignado el valor que tomará justo cuando el cliente inserta una instancia del control, sólo en esa ocasión ocurre el evento initproperties, por tanto en efecto se ejecutauna sola vez, aunque siempre sucede (cuando sucede) justo después del evento initialize (que de momento no lo tenemos presente, porque de momento no le hemos asignado código).
Hay nuevamente que recordar que hemos añadido una línea comentada, que está destinada a poner algó de código más adelante, de momento podemos cambiar el valor del texto, pero en nuestro botón no aparece el texto, esa línea comentada es justo donde le indicaremos cuando dónde y cómo dibujarse, pero esto loharemos cuando tengamos más elementos de juicio para dibujarlo, concretamente cuando decidamos como funcionará el alineamiento, ya que pensamos también incluir un icono, es lógico que el texto no quede cubierto por dicho icono... justo cuando describamos esas 2 propiedades pasaremos a dibujar el texto, porque ya tendremos definido todos los elementos que afectan al texto.

Ahora comentamos la línea de código: p_Texto = UserControl.Extender.Name
Podríamos haber puesto p_Texto = "Viva Yo y mi botón", y eso sería lo que siempre saldría cuando un cliente pusiera una nueva instancia del control sobre un formulario. Sin embargo lo lógico (y que estamos acostumbrado) es que cuando creamos un boton de microsoft , éste se llama Command1 y si ponemos otro este se llama Command2 y el nombre es precisamente el texto que contiene el botón. Esto es coherente y en esa coherencia nos movemos. Entonces que significa la parte derecha de la igualdad de p_Texto ?. Bien digamos que mientras se crea el control, VB crea y mantiene unas propiedades siempre vinculadas a cada control, esas propiedades están almacenadas en un objeto llamado VbControlExtender y de la que el usercontrol mantiene una instancia a través de la propiedad extender. Para curiosos decir que estas propiedades son aquellas que son propias e intrínsecas del control PERO DE las que el contenedor, realiza un seguimiento.

Ayer hablamos de otro objeto que es una propiedad del usercontrol Ambient. Conviene no confundir uno con otro. Extender, Por ejemplo además del nombre del control, mantiene la propiedad de posición  Left, Top y de tamaño Width y Height... si quereis saber con exactitud cuantas y cuales son las propiedades alojadas en el objeto extender, ir al formulario, seleccionad el control en el formulario y mirad las propiedades que tiene el control en la ventana de propiedades, si está en modo ejecución, pués son todas excepto las que nostros hemos creado hasta el momento, y si el control está en modo diseño (rayado) todas esas propiedades nosotros no las hemos creado, es el propio VB quien se encarga de crearlas para cada control y son aquellas que los contenedores deben tener acceso...  al contenedor no le importa ni necesita saber si yo tengo una propiedad llamada Icono ó Realte ó AlineaciónTexto... pero si necesita saber la posición del control, para saber si está parcialmente tapado o no, igual que la propiedad visible, etc... Hay también que decir que algunas de estas propiedades son de sólo lectura...
Podemos por tanto decir que un control se compone de 4 tipos de propiedades:
1 - Las que trae la propia plantilla del usercontrol (BackColor, Forecolor, Font, Picture, etc...) estas podemos usarlas o no, son opcionales no estamos obligados a usarlas si no las necesitamos.
2 - Las que se proveen a través de la propiedad Extender (del objeto VBControlExtender del que el usercontrol tiene una instancia accesible a través de la propiedad Extender) estas son obligadas y por tanto las provee el propio VB, como ya dije, Left, Top TooltipText, Name, Tag... ).
3 - Las que provee el contenedor y que tenemos acceso a ellas a través de la propiedad Ambient (del objeto AmbientProperties, como ), estas son todas de sólo lectura para el control.
4 - Y finalmente las que nosotros implementamos, ColorTapiz, ColorTexto, Texto, AlineacionTexto, Icono...

Tanto extender como Ambient no sólo proveen propiedades sino también funciones y eventos.

Finalmente, como hemos hecho con las otras propiedades, nos aseguramos de su  persistencia en el tiempo:
Código: Visual Basic
  1.  
  2. ' en el evento readproperties
  3. p_Texto = .ReadProperty("Texto", UserControl.Extender.Name)
  4.  
  5. ' en el evento writeproperties
  6. .WriteProperty "Texto",p_Texto, UserControl.Extender.Name
  7.  
  8.  
En lo sucesivo  ya no se comentará donde irá cada línea de persistencia de la propiedad... nos debe quedar claro donde corresponde cada una. Si pondré cada línea para que no se nos olvide...

Ahora deberíamos avanzar con una propiedad relacionada directamente con el texto AlineacionTexto:
Código: Visual Basic
  1.  
  2. ' declaramos la variable que contendrá el valor.
  3. Private p_Alineacion As TiposDeAlineacion  'AlignmentConstants  ' Guarda el valor de alineación del texto.
  4.  
  5. ' Creamos una enumeración para los valores posibles. Este código debe ir arriba del todo, incluso por encima de la declaración de variables.
  6. Public Enum TiposDeAlineacion
  7.     ALINEACION_IZQUIERDA = 0
  8.     ALINEACION_CENTRADA = 1
  9.     ALINEACION_DERECHA = 2
  10. End Enum
  11.  
  12. '  la declaración de la propiedad, la pondremos debajo de la última declaración de propiedad que pusimos
  13. Public Property Get AlineacionTexto() As TiposDeAlineacion
  14.     AlineacionTexto = p_Alineacion
  15. End Property
  16.     Public Property Let AlineacionTexto(ByVal at As TiposDeAlineacion)
  17.         If at <> p_Alineacion Then
  18.             p_Alineacion = at
  19.             PropertyChanged "AlineacionTexto"
  20.             ' añadidos posteriores
  21.        End If
  22.     End Property
  23.  
  24. ' el establcecimiento inicial de la propiedad dentro del evento initproperties, que ahora va quedando así...
  25. Private Sub UserControl_InitProperties()
  26.     p_Texto = UserControl.Extender.Name
  27.     p_Alineacion = ALINEACION_IZQUIERDA
  28.    
  29. End Sub
  30.  
  31. ' y finalmente el código de persistencia, cada línea donde corresponde ya debeis saber cual en cual...
  32.        p_Alineacion = .ReadProperty("AlineacionTexto", TiposDeAlineacion.ALINEACION_IZQUIERDA)
  33.         .WriteProperty "AlineacionTexto", p_Alineacion, TiposDeAlineacion.ALINEACION_IZQUIERDA
  34.  
  35.  
Ahora comentamos el código... Vemos que esta propiedad tampoc  delega el en objeto usercontrol, por lo que hemos declarado una variable para contener su valor  (yo a las variables de apoyo a la propiedades siempre les doy un nombre como p_nombrepropiedad , aunque no suelo poner el nombre de la propiedad completa, aldecidir un nombre de propiedad debe pensarse que debe resultar intuitivo al cliente su significado, por lo que no demos escatimar letras, si pusieramos alineación, el cliente podría tener dudas si se refiere al icono, o al control, pero con alineacionTexto, no da lugar a confusión, como internamente el cleinte no tiene acceso y sólo tengo una propiedad que tenga alineación, en la variable privada no me complico, un sencillo comentario al final de cada declaración, nos puede ayudar a recordar para qué se usa.

Se puede ver que hemos creado una enumeración para los 3 valores de la alineación, realmente vb, tiene una enumeración equivalente llamada AlignmentConstants, pero henmos desistido de usarla por 3 razones:
1º Nos sirve de excusa para aprender a usar enumeraciones como tipo de datos para cuando necesitemos variables.
2º La enumeración de windows no es correlativa de derecha a izquierda, nosotros tenemos izquierda, centro, derecha, con valores 0,1 y 2. Vb usa izquierda, derecha y centro que al menos a mi me parece  menos coherente.
3º Mantenemos en mente que estamos creando nuestro control en español (que se jodan los estadounidenses  :devil:  :devil:  :devil: así les damos algo de su propia medicina ... es broma).

En vez de una enumeración realmente para la declaración de la variable podríamos haber usado un tipo de datos byte, pero si usamos la enumeración mientras escribamos código el intellisense, nos ayudará sugiriéndono cual delos valores usar, con ello adelantamos al escribir y tampoco nos obliga a memorizar, esto limita en algo un posible error por ese problema de no haber recordado bien el valor... De modo consecuente, también se ha optado por usar el tipo de datos enumeración para la propiedad, porque así en la ventana de propiedades cuando el cliente decida cambiar el valor se despliega una lista precisamente exponiendo los nombres que les hemos dado a nuestras constantes de alineación.

Respecto de la alineación, nosotros sólo realizaremos una alineación horizontal, queda como ejercicio para el interesado cambiarlo por una alineación también vertical. A este respecto VB tiene otra enumeración AlignConstant, que usa valores semejantes a los anteriores pero que además añade top y bottom.

El código de la propiedad no está realmente bien terminado, si el lector es avispado hace rato que debería estar rondándole en la cabeza lo que vamos a describir a continuación...
Puesto que la alineación sólo tiene 3 posibilidades, que le impide al cliente introducir un valor de por ejemplo 25 ?. Realmente si así lo hiciera ahoraese valor sería introducido y si nuestro cálculo de reposicionamiento del texto estuviera ya listo, nos arrojaría un error, nos falta por tanto un 'hervor' en el código de la asignación de la propiedad.
Ese hervor es controlar que valor se introduce, y al efecto sólo debemos permitir que se introduzca uno de los 3 valoresposibles. Hay 2 formas (coherentes) de resolver esto, si el control es para nosotros, (como lo es) lo solucionaremos de una manera 'llana y sencilla' porque conocemos los detalles y sabemos que entenderemos laforma de funcionar que le vamos a dar, ahora bien si el control fuera un encargo de una empresa, lo que deberíamos hacer realmente es marcar un error cuando se introdujera un valor incorrecto, con ello el cleinte sabría que ha introducido un valor noadmitido y se supone que acabaría por 'conocer' bien el control. Pondremos ambos ejemplos de código.

' el código para un encargo de empresa:
Código: Visual Basic
  1.  
  2. Public Property Let AlineacionTexto(ByVal at As TiposDeAlineacion)
  3.         If at <> p_Alineacion Then
  4.            If (at < 0) or (at > 2) Then
  5.                Raise Error(380) ' el error 380 tiene asignado el significado de: Se ha asignado a una propiedad un valor incorrecto
  6.           else
  7.                 p_Alineacion = at
  8.                 PropertyChanged "AlineacionTexto"
  9.                 ' añadidos posteriores
  10.            End If
  11.         End If
  12.     End Property
  13.  
  14.  

El código, más inteligente, para gente más inteligente:
Código: Visual Basic
  1.  
  2. Public Property Let AlineacionTexto(ByVal at As TiposDeAlineacion)
  3.         If at <> p_Alineacion Then
  4.             If at < 0 Then
  5.                 at = 0 'ALINEACION_IZQUIERDA
  6.            ElseIf at > 2 Then
  7.                 at = 2 ' ALINEACION_DERECHA
  8.            end if
  9.  
  10.             p_Alineacion = at
  11.             PropertyChanged "AlineacionTexto"
  12.             ' añadidos posteriores
  13.        End If
  14.     End Property
  15.  
  16.  

En este caso hemos optado por no señalar un error, sino que cualquier valor por encima de 2 lo hacemos igual a 2 y cualquier valor menor que 0 lo hacemos = a 0. Cyuando el cliente está en modo de diseño y escribe una propiedad, se pasa el valor al procedimiento de propiedad, lapropiedad actúa, como no marcamos error, el valor queda correctamente almacenado en la variable p_Alineacion,pero partimos de que el cliente introdujo por ejemplo un valor de 37, entonces qué ocurre ?. a continuación (estando en diseño como hemos dicho),  el entorno de vb una vez termina el procedimiento let realiza una llamada (de forma transparente) al procedimiento get, para actualizar el valor en la ventana de propiedades, con lo que en efecto al leer el valor de p_Alineacion, le entregará el valor 2 que sustituirá al 37 que introdujo el cliente.

Todavía podría decir que a la propiedad alineacionTexto le falta un hervor más, pero en este caso no se aplica del todo, así que lo dejamos para más adelante cuando una propiedad lo justifique y si vamos terminando el control y no ha sucedido pués aprovechando esta propiedad locomentaríamos y dejaríamos puerta libre para que qien qiera lo aplicara o no.

Sobre la propiedad alineación sólo nos resta decir que al igual que las propiedades anteriores hemosdejado un comentario como recordatorio de que ahí va algo de código, que mañana cuando por fin pongamos el código para los gráficos se añada el código faltante en estas propiedades.

Ahora vamos a añadir una propiedad Font, para que nuestro cliente pueda decidir que tipo de letra se pone al texto del botón, este es el sitio adecuado para introducirlo (la posición).
Código: Visual Basic
  1.  
  2. ' el código de la propiedad fuente (Font)
  3. Public Property Get Fuente() As IFontDisp
  4.     Set Fuente = UserControl.Font
  5. End Property
  6.     Public Property Set Fuente(ByRef f As IFontDisp)
  7.         Set UserControl.Font = f
  8.          PropertyChanged "Fuente"
  9.         ' añadidos posteriores
  10.    End Property
  11.  
  12.  
  13.  
  14.  
Comentamos el código para esta propiedad. Esta propiedad es una propiedad de ambiente, por lo que también el usercontrol la contiene, por tanto delegamos en ella para contenerla. Ayer hablamos de que había 3 procedmientos de propiedad y que uno de ellos era para cuando el tipo de datos era un objeto. Este es el caso presente, Font es un objeto, por tanto no puede usarse 'property let' sino que deb usarse 'property Set', véase igualmente como la asignación de objeto utiliza Set para almacenarlo en la variable, el procedmiento Get, igualmente debe usarse un set y el cliente necesariamente deberá alamcenar este objeto en una variable que él designe como objeto sino obtendrá un error.
Luego hemos usado la interfaz Ifontdisp, del mismo modo y razón por la que usamos OLE_COLOR para las propiedades de color, ya que así VB presenta el cuadro de diálogo para que el cliente elija la fuente deseada así como sus propiedades (size, Bold, etc...)
Podríamos dividir la asignación de fuente en cada una delas propiedades del objeto, por ejemplo FontSize por un lado y fontName por otro, FontBold,etc... esto sólo interesa si por ejemplo queremos limitar al usuario de una de dichas características, aunque también en lapropiedad Fuente que hemos declarado, podríamos incluir código para hacer lo mismo, es cuestión de gustos, yo prefiero poner una sola propiedad fuente  en vez de varias, que llenen la ventana de propiedades... Finalmente hemos elegido pasar el dato por referencia y no por valor, aunque realmente VB hace una copia de la fuente, ya que si no un cambio originaría un cambio de donde procede, es decir el contenedor.

Ahora como ya habeis aprendido, a asignar un valor por defecto para la propiedad y también darle persistencia:
Código: Visual Basic
  1.  
  2. Private Sub UserControl_InitProperties()
  3.     p_Texto = UserControl.Extender.Name
  4.     p_Alineacion = ALINEACION_IZQUIERDA
  5.     Set UserControl.Font = Ambient.Font         ' no es estrictamente necesario, ya que esto sucede automáticamente
  6.    
  7. End Sub
  8.  
  9. ' persistencia:
  10.        Set UserControl.Font = .ReadProperty("Fuente", UserControl.Ambient.Font)
  11.         .WriteProperty "Fuente", UserControl.Font, UserControl.Ambient.Font
  12.  
  13.  


 A continuación, crearemos una propiedad icono y damos por terminado esta parte de hoy.
 
Código: Visual Basic
  1.  
  2. ' en la sección de declaración de variables....
  3. Private p_Icono As IPictureDisp ' icono que se presenta alineado enla izquierda
  4.  
  5. ' debajo de todas las propiedades hasta el momento....
  6. Public Property Get Icono() As IPictureDisp
  7.     Set Icono = p_Icono
  8. End Property
  9.     Public Property Set Icono(ByRef i As IPictureDisp)
  10.         Set p_Icono = i
  11.         PropertyChanged "Icono"
  12.         ' añadidos posteriores
  13.    End Property
  14.  
  15. ' persistencia.
  16.        Set p_Icono = .ReadProperty("Icono", Nothing)
  17.         .WriteProperty "Icono", p_Icono, Nothing
  18.  
  19.  
Esta propiedad es fácil y rápido de comentar... al como pasó para el color y la fuente usamos un tipo de datos que nos mostrará una ventana de propiedades como cuadro de diálogo para localizar y elegir la imagen deseada que se usará como icono. Hay que señalar que por defecto el icono es... ninguno, es decir si el cliente no elige un icono, no dibujamos nada. Por ello en el evento initproperties no le asignamos nada, ya es nothing...

Ahora guardar todo y podeis probar los añadidos realizados, claro que al ejecutar de momento sólo nos resulta tangible el colorTapiz ya que de momento no hacemos nada gráfico...

Mañana nos centramos en graficar todo lo que tenemos hasta ahora y que se pueda plasmar en el control. Veremos que alinear el texto tiene consecuencias según exista o no un icono, así mismo decidiremos un margen entre el control y el icono, ya que todavía nos falta una propiedad que realice un relieve sobre el contorno del control...
«Ma non troppo»
----> ModoVacaciones = False<----

Nebire

  • Miembro HIPER activo
  • ****
  • Mensajes: 670
    • Ver Perfil
Re: Guía: Crear controles de usuario en VB
« Respuesta #4 en: Martes 7 de Septiembre de 2010, 06:58 »
0
Comenzamos una nueva parte, en esta parte nos centraremos en completar todas las propiedades y detalles que nos faltan para poder procesar el grafismo que requiere el control.

Nos ponernos manos a la obra, crearemos una propiedad, la que permite que nuestro control esté enabled o disabled, como hablamos en castellano esta propiedad se llamará Activo. Queremos que cuando el control esté deshabilitado cambie de aspecto, para que el  usuario final perciba con claridad que el control está deshabilitado y no le queden dudas sobre si 'no funciona'. Sabemos que cuando un control está deshabilitado, lo que en realidad sucede es que el control no envía eventos al cliente, sin embargo es responsabilidad nuestra posibilitar esa impresión de deshabilitado, dado que esto se enmarca también dentro del proceso gráfico es necesario que dispongamos también de esta propiedad y de la forma que queramos señalar que el control está deshabilitado. Más adelante comentamos algunas cosas que podemos hacer, para reflejar esta situación, ahora vamos al código de esta propiedad y luego lo comentamos.

Código: Visual Basic
  1.  
  2. Public Property Get Activo() As Boolean
  3.     Activo = UserControl.Enabled
  4. End Property
  5.     Public Property Let Activo(ByVal a As Boolean)
  6.         If a <> UserControl.Enabled Then
  7.             UserControl.Enabled = a
  8.             PropertyChanged "Activo"
  9.             ' añadidos posteriores
  10.         End If
  11.     End Property
  12.  
  13.  
Como se ve, esta propiedad hemos delegado en el usercontrol, nosotros no tenemos que preocuparnos de activar o desactivar eventos, el asunto se lo pasamos al usercontrol y él se encarga de hacerlo, no nos importan los detalles. el código es fácil de entender, si el valor introducido es distinto del actual entonces guardamos el nuevo valor y graficaremos del modo conveniente, para eso al igual como otras propiedades que afectan al grafismo, hemos añadido la dichosa línea comentada a la espera de que sea remplazada por el código conveniente.
Personalmente me gusta poner siempre esta propiedad la primera dentro de las propiedades y la última en los métodos de persistencia... los ponemos ahora.
Código: Visual Basic
  1.  
  2. ' persistencia... cada línea en su método...
  3.              UserControl.Enabled = .ReadProperty("Activo", True)
  4.              .WriteProperty "Activo", UserControl.Enabled, True
  5.  
  6.  
Aquí podemos ver que el valor por defecto no lo hemos delegado en ambient, sino que preferimos que sea activo por defecto.
La inicialización de esta propiedad no es preciso definirla, por defecto cuando una instancia se crea su estado siempre es activo.

Recordemos que siempre que un contenedor este desactivado, per sé todos los controles contenidos también lo están, podríamos invocar un evento desde ambient de modo que cuando la propiedad enabled del contenedor cambie de estado nos informe, sin embargo no lo haremos. El estado del control no debe ir a la par que el estado del contenedor, es el propio contenedor quien debe concienciar el usuario final que está deshabilitado y que por tanto todos sus controles son inaccesibles... de hecho  el contenedor cuando está deshabilitado deja de pasar eventos al control tal como si nosotros mismos lo hubieramos deshabilitado, por lo que alcliente no le llega ningún evento.

Ayer, que se me hizo tarde también dejé sin implementar la propiedad picture que como convinimos, nuestro control tendría la posibilidad de tener 1 imagen como fondo y siempre ajustada al tamaño del control, aún en este caso nos falta decidir el comportamiento de la imagen... proponemos 2 casos (podrían ser más)...
A - La imagen está como fondo, pero cuando pulsamos (down) cambiamos a otra imagen  y cuando soltamos el botón vuelve a cambiar a la inicial.
B - La imagen permanece como fondo inalterable, la percepción de pulsación (hundir y soltar) lo reflejaremos de otra manera.

Nos vamos a decantar por la opción B, Con la opción A necesitaríamos duplicar la propiedad y darle un nombre que diferenciara claramente ambas imágenes.
Puesto que hemos elegido la opción A, pondremos el código para dicha propiedad y de paso la usaremos de mod que aprendamos algo más acerca de las propiedades... después comentamos el código de la propiedad.
Código: Visual Basic
  1.  
  2. ' esta línea con las demás en la sección de declaracion de variables..
  3. Private p_Imagen As IPictureDisp ' imagen autoajustable al fondo del control.
  4.  
  5. Public Property Get Imagen() As IPictureDisp
  6.     Set Imagen = p_Imagen
  7. End Property
  8.     Public Property Set Imagen(ByVal i As IPictureDisp)
  9.         Set p_Imagen = i
  10.         ' añadidos posteriores
  11.     End Property
  12.  
  13. ' persistencia ...
  14.                Set p_Imagen = .ReadProperty("Imagen", nothing)
  15.                .WriteProperty "Imagen", p_Imagen, nothing
  16.  
  17. ' inicialización (ya sabeis que con esto quiero señalar que va dentro del evento initproperties)
  18.      Set p_Imagen = UserControl.Picture
  19.     Set UserControl.Picture = Nothing
  20.  
  21.  
Primero hemos declarado una propiedad para contener la imagen. No delegamos en la propiedad picture del control, porque al hacerlo implicaría que el control la vuelca al control, ya se dijo en otro apartado más arriba que a veces no nos interesa como sucede una funcionalidad y que en dicho caso nosotros la debemos socorrer como nos interese. Picture no funciona bien, porque automáticamente pega la imagen sobre el control pero en unas determinadas condiciones, para empezar a tamaño real, y para terminar centrada posicionada en la cordenada 0,0 del control, ni una cosa ni la otra nos interesa, la funcionalidad que deseamos es que la imagen ocupe siempre la totalidad del control, tenga éste el tamaño que tenga. Por esto la imagen la guardamos en nuestra propiedad.
 
Como se ve en le código de la propiedad, al serr un objeto necesitamos el 'formato' SET, también puede verse que no comprobamos si se mete una imagen o está vacío, a efectos prácticos podría no bastar con verificar si el valor recibido tiene o no una imagen sino que además deberíamos verificar si en estos momentos tenemos o no una imagen ya en el control y teniendo 2 variables tendríamos 4 posibilidades de decisión... por ejemplo una posible situación sería que ahora mismo no hubiera imagen y que tampoco se recibiera imagen en cuyo casono sería necesario hacer nada porque nada cambió, en los otros 3 casos deberíamos actualizar, facilitamos la tarea si nos evitamos esas comprobaciones, después de todo a la hora de dibujar suele hacerse esta pregunta de si existe la imagen, luego sería tonto preguntarlo 2 veces... cada uno debe valorar en su situación si el código a ejecutar es más demorado que la verificación teniendo en cuenta las posibilidades que puedan darse de una verificación falida o exitosa.

Finalmente tras lapersistencia que a estas alturas no debería requerir comentarios, vemos que para la inicialización tenemos 2 líneas y un código de entrada 'algo extraño'. Bien, hemos decidido que por defecto el control siempre provea ya de 'fábrica' una imagen, si o si, luego el cliente puede optar por sustituirla o eliminarla. Ahora bien donde alojamos esa imagen ?... por un lado podríamos dejarla en los recursos y en el evento rescatarla, desde luego es una opción, no onstante yo he optado por cargarla ahora mismo durante el diseño en la propiedad picture (esto es teneís que ir a la ventana de la interfaz del contro, localizar la propiedad picture y cargar ahí una imagen que será la que se utilice) , os muestro una imagen de como aparece en mi caso:
Es una imagen tipo gif, conviene que sea algo que no tenga dibujos que al ajustarla al tamaño del botón de la apariencia de estar desproporcionado, es decir evitad que tenga cosas reales, más que una imagen podríamos considerar que es una 'textura'. También se debe tener en cuenta el tamaño de la imagen... considerando que nuestro control una vez compilado tendrá entre 30 y 60 kb, haceros una idea... en mi caso la imagen pesa 1' 73Kb. y hasta 5 kb, tampoco sería pesado...
Bien finalmente el código de esas líneas de inicialización pueden quedar claramente despejadas... en diseño hemos subido una imagen que ahora mismo está guardada en la propiedad picture del usercontrol, cuando el cliente cree una instancia de nuestro botón, se ejecutará esa y solo esa vez el evento initproperties, copia sobre la propiedad p_imagen la imagen contenida en el usercontrol.picture y luego elimina la copia de usercontrol.picture, por lo que en el control ya no aparece, desde ahora se conserva en p_imagen hasta que la persistencia se encargue de guardarla a fichero.

Hay algún detalle más acerca de la imagen, que se dirá un poco más adelante o quizás mañana y también diremos a estas alturas que debemos deciidir en que método visual va a consistir la deshabilitación del control, podríamos poner por ejemplo una imagen gris, podríamos trabajar con el relieve o hacer algún efecto gráfico. en este momento nos hacemos el loco, como si no se nos hubiera pasado aún por la cabeza (durante el diseño de un control siempre hay algún detalle que hasta que no estás sobre él no lo has decidido o bien se te había olvidado, o esperabas a ver que tal combinaba con otras opciones en conjunto, etc... por lo que está cuestión la decidiremos resolveremos en el mismo instante que se nos presente o incluso la demoraremos aún para más adelante.... (eso sí con su línea de recordatorio pertinente).

Ahora, antes de empezar a dibujar nuestro control (tampoco hemos empezado esta parte dando el código de pintar como seguramente se esperaba), para empezar a dibujar ya tenemos bastante detalles nos falta ordenarlos, yo en mi cabeza los tengo ordenados, pero evidentemente cuando uno se enfrenta por primera vez a una situación suele ser necesario anotar los detalles y ordenarlos... esto haremos en este instante que a modo de resumen fija la funcionalidad gráfica.

El control lleva un color de fondo que puede ser cambiado por el cliente en cualquier momento, lleva un texto que puede cambiar de color, además de la fuente y el propio texto, tenemos además que alinearlo sobre el control y para esto nos influye la existenciao no de un icono y finalmente (por ahora) tenemos una imagen... una vez que hemos enumerado los detalles ordenémoslos en capa, lo que tiene que ir más al fondo y lo que irá encima...

Al fondo del todo siempre va el ColorTapiz, luego irá la imagen, encima de esta irá el icono alineado a la izquierda con un margen que describiremos en el próximo párrafo y finalmente encima del todo en primer plano irá el texto. Pués es en este orden que debemos empezar a pintar....

Si colocamos un icono debe ser para que se vea, si no no tiene sentido entonces el icono debería tener unas medidas mínimas, pongamos 16x16 píxeles, luego cuando hagamos un relieve perimetral nos comeremos 2 píxeles por cada lado, y sería agradable a la vista que el icono no fuera completamente pegado al relieve luego estimemos un margen y digamos que sean 2 px. mínimo por arriba, izquierda y abajo... entonces resolvemos que para poner el icono necesitamos una medida como mínimo de 16+4+4= 24 píxeles de alto y lo mismo de ancho, si esa medida es menor aunque tengamos icono no lo pondremos. Y finalmente que haremos cuando el botón sea gigantesco... horizontalmente dijimos desde un principio que lo queríamos alineado a la izquierda, luego el icono estará ubicado en: 2px relieve + 2 px margen = 4px. y verticalmente pocríamos decidir una de 3 opciones:
A - Alineado arriba
B - Centrado verticalmente:
C - Alineado arriba y abajo, es decir se expandería al hacerlo el control.

Examinado la opción C, Hacerlo así haría que laimagen se mostrada se viera distosionada si no creciera también, proporcionalmente en dirección horizontal, pero hacer esto nos limita el acnho para el texto, luego esta opción la descartamos. Examinado la opción A, quedaría bien pero parecería más el botón de un título de algo que la descripción de un botón de ación, luego nos decantamos por la opción B.
Aún respecto del icono queda otra noción, si tenemos un tamaño mínimo de icono, podríamos tener un tamaño máximo de icono, o podría quedar fijo?.... nos decantamos porque sea elcliente quien en este caso decida el tamaño del icono, esto requerirá una nueva propiedad que exponga al cliente la capacidad de modificarlo, de momento creamos sólo la enumeración y la variable privada, luego la propiedad es como otras que ya hemos realizado... En cualquier caso nos parece que si se usa un icono tampoco deberá tener un tamaño infinito, luego parece razonable limitarlo a un tamaño máximo. en estas circunstancias parece que el icono deba ser cuadrado, pero definitivamente muchas veces un icono alargado a lo ancho o alto podría ser más vistoso, y por tanto adoptamos esta idea...  Total decidiremos un total de 24-25 tamaños de icono... que ya se presentarán en el código en una enumeración... y un último detalle para este caso, es qué ha de suceder cuando el usuario tiene un alto de botón y fija un tamaño e icono mayor que él... bien en este caso lo que haremos será fijar un alto del botón tal que sea el alto del icono + los márgenes y el relieve es decir altoicono + 8 px, lo mismo para el ancho.

Debe uno darse cuenta como este icono nos está complicando la vida casi más que el resto de propiedades que hemos definido hasta el momento. Nos parece razonable, así que aceptamos seguir adelante, confiamos en que el resultado merece la pena. No hay que olvidar que el objetivo de este control es mostrar como se diseña un control y como se resuelven ciertos detalles, podríamos haber tirado por lo sencillo y fijar que el icono tuviera un tamaño siempre fijo centrado a lo alto del botón. Todos estos detalles sin embargo nos mostrarán como nos apoyamos en otras variables e incluso constantes para lograr nuestros propósitos.
Código: Visual Basic
  1.  
  2. Public Enum TamañosDeIcono
  3.     ICONO_16x16 = -3
  4.     ICONO_24x16 = -2
  5.     ICONO_16x24 = -1
  6.     ICONO_24x24 = 0
  7.     ICONO_32x24 = 1
  8.     ICONO_24x32 = 2
  9.     ICONO_32x16 = 3
  10.     ICONO_32x32 = 4
  11.     ICONO_40x32 = 5
  12.     ICONO_32x40 = 6
  13.     ICONO_40x40 = 7
  14.     ICONO_48x40 = 8
  15.     ICONO_40x48 = 9
  16.     ICONO_48x48 = 10
  17.     ICONO_48x24 = 11
  18.     ICONO_64x32 = 12
  19.     ICONO_32x64 = 13
  20.     ICONO_64x48 = 14
  21.     ICONO_48x64 = 15
  22.     ICONO_64x64 = 16
  23.     ICONO_80x64 = 17
  24.     ICONO_64x80 = 18
  25.     ICONO_80x80 = 19
  26.     ICONO_128x128 = 20
  27. End Enum
  28.  
  29.  
La enumeración de tamaños.... 2 detalles a tener en cuenta, primero debe verse como nuestra enumeración lejos de ser compleja, ayuda al cliente a decidir que tamaño tiene el icono, pués inserto en el nombre de la constante está. el otro detalle es fijarse en como, la enumeración la hemos emepzado e -3 y no en 0, la razón es porque queremos dar la impresión general de que el tamaño de 24x24 es eltamaño normal y por tanto valores menores sería considerarlos pequeños, no hay más razonamiento que ese, y seguro estamos que el cliente tendrá esa misma impresión.
Código: Visual Basic
  1.  
  2. ' en la sección de declaraciones... todas estas..
  3. Private p_TamañoIcono As TamañosDeIcono ' Determina el tamaño de icono...
  4.  
  5. private Const c_MinIcono = -3            ' ancho y alto mínimo del icono
  6. private Const c_MaxIcono = 20
  7. Private s_AnchoIcono As Byte
  8. Private s_AltoIcono As Byte
  9.  
  10.  
Primero vemos una variable que guardará la propiedad del tamaño del icono. Luego usamos 2 constantes, que realmente no necesitamos, se pone simplemente para ilustrar su uso (nótese como yo suelo nombrar a las constantes empezando por 'c_', esto cada cual a su gusto. Los valores de las constantes son los límites de la enumeración de tamaños de los iconos, y los usamos exclusivamente para determinan en la propiedad si el cliente introduce un dato válido...
al final tenemos dos variables que son también privadas, no guardan valor de propiedad pero indirectamente guardarán la medida que representa el tamaño de icono elegido.

Código: Visual Basic
  1.  
  2. Private Sub UserControl_InitProperties()
  3.     p_Texto = UserControl.Extender.Name
  4.     p_Alineacion = ALINEACION_IZQUIERDA
  5.     Set UserControl.Font = Ambient.Font
  6.     Set p_Imagen = UserControl.Picture
  7.     Set UserControl.Picture = Nothing
  8.    
  9.     p_TamañoIcono = 0
  10.     s_AnchoIcono = 24
  11.     s_AltoIcono = 24
  12.  
  13. End Sub
  14.  
  15.  
Éste es el aspecto que viene presentando a estas alturas el código del evento initproperties, las líneas que ahora nos interesan son las 3 últimas que he separado del resto para mayor claridad... vemos que tal como deseábamos a la variable que da soporte a la propiedad del tamaño deicono le damos el valor 'normal' que hemos hecho coincidir con 0, de hecho por eso mismo, podríamos omitir esa línea las variables numéricas siempre valen 0 al inicializarse... conjuntamente con dicha variable y acorde a su valor las variables que han de mantener el valor de las medidas, deben ser siempre como corresponde, la correspondencia puede mirarse en la enumeración...

Ahora nos fijamos que algo parecido necesitamos hacer en la persistencia de la propiedad.. a la propiedad al final la hemos llamado Iconotamaño.
Código: Visual Basic
  1.  
  2. ' persistencia, cada línea donde corresponda..
  3.         p_TamañoIcono = .ReadProperty("IconoTamaño", 0)
  4.         Call MedirIcono
  5.  
  6.         .WriteProperty "IconoTamaño", p_TamañoIcono, 0
  7.  
  8.  
 
La propiedad ya sabemos guardarla, vemos que en coherencia con el valor por defecto dado en initproperties le damos el mismo valor  (vale igual si ponemos el nombre de la constante ICONO_24x24). Debajo hay una línea que llama a un método, en dicho método (cuyo código se expone a continuación) lo que se hace es obtener el valor real que tendrá el icono dado un valor asignado a la propiedad, en initproperties como sabíamos que el valor era 0, las medidas eran 24 de ancho y 24 de alto.
Código: Visual Basic
  1.  
  2.        Private Sub MedirIcono()
  3.     Select Case p_TamañoIcono
  4.         Case -3, 1
  5.             s_AnchoIcono = 16
  6.         Case 1, 3, 4, 6, 13
  7.             s_AnchoIcono = 32
  8.         Case 5, 7, 9
  9.             s_AnchoIcono = 40
  10.         Case 8, 10, 11, 15
  11.             s_AnchoIcono = 48
  12.         Case 12, 14, 16, 18
  13.             s_AnchoIcono = 64
  14.         Case 17, 19
  15.             s_AnchoIcono = 80
  16.         Case 20
  17.             s_AnchoIcono = 128
  18.         Case Else
  19.             s_AnchoIcono = 24
  20.     End Select
  21.     Select Case p_TamañoIcono
  22.         Case -3, -2, 3
  23.             s_AltoIcono = 16
  24.         Case 2, 4, 5, 12
  25.             s_AltoIcono = 32
  26.         Case 6, 7, 8
  27.             s_AltoIcono = 40
  28.         Case 9, 10, 14
  29.             s_AltoIcono = 48
  30.         Case 13, 15, 16, 17
  31.             s_AltoIcono = 64
  32.         Case 18, 19
  33.             s_AltoIcono = 80
  34.         Case 20
  35.              s_AltoIcono = 128
  36.         Case Else
  37.             s_AltoIcono = 24
  38.     End Select
  39. End Sub
  40.  
  41.  
El método para calcular cada medida no tiene más complejidad que la correspondencia siendo que únicamente hemos unificado casos comunes.
También es de resaltar que podríamos haber hecho persistencia sobre las variables así, al leer los valores de las propiedades no tendríamos que saltar a esta rutina y buscar (más que calcular) el valor correspondiente. La razón es que el evento readproperties sólo se ejecuta una única vez justo cuando la aplicación se inicia luego, no hay apenas sacrificio de CPU, después de todo acceder a disco para leer valores seguramente no es más rápido que esto. Finalmente si sólo se usara esta rutina en dicho lugar quizás no merecería la pena las líneas de código empleada, pero dado que también la precisa la propiedad justo está bien...

Ahora rematando la cuestión del icono, su propiedad:
Código: Visual Basic
  1.  
  2. Public Property Get IconoTamaño() As TamañosDeIcono
  3.     IconoTamaño = p_TamañoIcono
  4. End Property
  5.     Public Property Let IconoTamaño(ByVal ti As TamañosDeIcono)
  6.         If ti <> p_TamañoIcono Then
  7.             If (ti >= c_MinIcono) And (ti <= c_MaxIcono) Then
  8.                 p_TamañoIcono = ti
  9.                 Call MedirIcono
  10.                 PropertyChanged "IconoTamaño"
  11.                 ' añadidos posteriores
  12.             End If
  13.         End If
  14.     End Property
  15.  
  16.  

Ya creo que la propiedad no merece ser comentada... ved como usamos las constantes, realmente si no las vamos a usar en ninguna parte más, se  podrían omitir, no obstante una razón para usar constantes es que te recuerdan para qué sirven por e hecho de tener un nombre, una vez pasado el tiempo al ver el código y ver un -3 y un 20 uno podría preguntarse, porque 20 y no 30... su nombre refleja su significado... ya sabeis que al compilar, las constantes son remplazadas in situ por su valor, y por tanto no hay traducción de valores ya con la aplicación compilada en ejecución, como puede suceder cuando el código es interpretado.

Y como al final nos hemos enrollado con propiedades y el texto se ha hecho largo, cortamos aquí esta parte y finalmente no va aser hoy cuando pintemos aunque ya tenemos todos los detalles... (excepto el relieve, del que de momento hemos decidido que es perimetral, pegado al margen y de 2 px de ancho) para hacerlo.
«Ma non troppo»
----> ModoVacaciones = False<----

Nebire

  • Miembro HIPER activo
  • ****
  • Mensajes: 670
    • Ver Perfil
Re: Guía: Crear controles de usuario en VB
« Respuesta #5 en: Jueves 9 de Septiembre de 2010, 14:29 »
0
Hoy por fin, vamos a pintar, el control al fin tendrá un aspecto de acabado, aunque ni mucho menos hallamos acabado, además también sugeriremos optimizaciones para pintar, aunque quedará como esfuerzo para vosotros... y hablaremos de algunas cosas más, según se vea que el texto no se alargue demasiado.

A la hora de pintar sobre un control hay que saber que hay 2 modos de hacerlo, en ambos casos hay una parte manual y una parte automática, explicaremos esto un poco por encima...

Cuando nuestro control es cubierto total o parcialmente por otro control u otra ventana, lógicamente esa parte queda ocultada, se dice que los controles dentro de un formulario están en capas (igual que los fiormularios), profundidad en el plano Z, cuando nuevamente parte o todo de lo que estaba tapado quede a la vista, el S.O. se encarga de llamar al formulario para enviarle una orden de repintado y el formulario a su vez manda la orden a nuestro control, para que se redibuje.

La cosa es algo más compleja, pero a lo que nosotros concierne esta simplificación es suficiente, simplemente entendemos que cuando eso sucede se dispara el evento Paint del control. Por tanto toodo nuestro código de dibujado puede ir perfectamente en dicho evento, siendo entonces el caso del primer método (por diferenciarlo de alguna manera).

También hay otras ocasiones en que debe ser redibujado, y es cada vez que el control tiene que cambiar algo de su apariencia, por ejemplo si cambiamos el texto, el control debe volverse redibujar... el modo pués sería que al cambiar la propiedad texto, hacemos una llamada al método del usercontrol Paint (es un evento, pero visto desde el código interno, es un método).

El 2º método puede parecer un poco más extraño, porque no utiliza nunca el evento paint, o dicho de otra manera, lo utiliza pero sin llegar al control de usuario, VB se encarga de que justo cuando se recibe una petición de dibujado, en vez de pasarlo al control él usa una imagen que tiene en memoria del control y dibuja la parte que antes estaba oculta y que ahora queda a la vista. Esto lo hace automáticamente y en cierto modo es más veloz, aunque requiere memoria adicional para guardar la imagen. La pregunta consecuente puede ser algo como, y de dónde saca la imagen o cómo le proporciono dicha imagen ?. No es necesario proporcionarle una imagen, VB ya sabe de donde debe tomar la imagen, el HDC del control es quien contiene dicha imagen nosotros sólo debemos indicarle al control, que método de dibujado queremos usar, para esto sirve precisamente la propiedad que todos conocemos, AutoRedraw, que imagino que todos habeis visto y usado en los controles tipo Picturebox. cuando se establece la propiedad autoredraw a True, cada vez que pintemos algo en el control, VB toma una 'foto' (una instantánea) que mantiene en memoria, si autoredraw es false, VB no hace seguimiento de la imagen...

Éste 2º, es el método que vamos a usar, por tanto vamos a la ventana de la interfaz del control y localizamos la propiedad autoredraw y la establecemos al valor TRUE... de paso localizamos la propiedad Scale y ponemos la constante VBPixels, es decir el 3. Ahora tendremos que ser consecuente con nuestro código, cada cambio  que requiera el control  en su apariencia debemos hacerlo nosotros, sin embargo esto es verdad cualquiera que sea el método empleado.

La diferencia entre usar el método autoredraw y el otro radica en que con el otro siempre se ejecutará el evento paint donde debe ir nuestro código de dibujado, como no recibimos información acerca de que parte hay que dibujar, simplemente se dibuja todo entero, incluso aunque estuviéramos informados de que parte se necesita pintar, ofrecer un método que pinte exclusivamente la parte 'nueva', usando métodos gráficos sencillos sería complejo. En cambio  el método autoredraw, cuando lo necesitemos pintamos todo o sólo parte y cuando partes antes ocultas sean luego visibles, VB hará el recorte necesario (enmascarando contenido) desde la imagen del control. Realmente nosotros podríamos emular el automatismo de autoredraw y seguir utilizando el método paint, pero como digo al no recibir información sobre la sección de recorte, no tendríamos más remedio que pegar toda la imagen que nosotros habríamos 'fotografiado'. Hay un par de APIs, que sirven para esto BeginPath y EndPath, pero esto es complicarnos la vida si tenemos en cuenta que Vb lo hace por nosotros y lo hace bien después de todo VB no es C.

Entonce pasemos a dibujar por fin, pondré un sencillo código y luego paso a comentarlo...
Código: Visual Basic
  1.  
  2. Public Sub DibujarTodo()
  3.     Dim DerechaIcono As Long
  4.  
  5.     UserControl.Cls ' antes que nada borramos todo
  6.     ' el color tapiz se dibuja desde donde delegamos, luego, esto no es preciso pintarlo con usercontrol.line(x,y)-(x1,y1),color,bf
  7.    
  8.     Call DibujarImagen
  9.     DerechaIcono = DibujarIcono
  10.     Call DibujarTexto(DerechaIcono)
  11.     'Call DibujarRelieve
  12. End Sub
  13.  
  14.  
Usamos una rutina de dibujado, que será invocada desde diferentes partes, que luego explicaremos.... esta rutina es equivalente a haber metido el código en el evento paint y no haber activado autoredraw, así de sencillo (por si quedó alguna duda ahora debería quedar claro).
En cuanto al código, si lo observamos bien son diversas llamadas a otros métodos, se ha de prestar atención al orden  en que se llaman, como acordamos, en alguna parte anterior, hay que dibujar en capas, primero lo que está más al fondo y lo último lo que está más 'cerca', siempre lo que está en el fondo es el color del tapiz, pero como delegamos en el usercontrol, lo hace por nosotros, si hubieramos decidido no delegar en el usercontrol (lo podeis hacer si quereis, de hecho sería más rápido y eficaz), tendríamos que crear una variable que guardara el valor del ColorTapiz por ejemplo p_ColorTapiz y remplazar Usercontrol.backcolor por nuestra variable en cada una de las partes del código done aparece. Y finalmente donde hemos comentado la línea en la rutina (del código acabado de exponer) pondríamos una orden gráfica para dibujar un cuadro relleno con  el método line del usercontrol. El código en dicho caso sería: usercontrol.line(0,0)-(x1,y1),p_colorTapiz,bf siendo X1 e Y1, las dimensiones del control.

Bien entonces nos queda que, 1º borramos el control, 2ª dibujamos el fondo, por nuestros medios o delegando en el usercontrol, 3º dibujamos la imagen, 4º dibujamos el icono , 5º dibujamos el texto y finalmente en 6º lugar el relieve que aún no nos hemos metido con él... como puede apreciarse, se ha supuesto un método para el mismo, pero se ha dejado comentado la línea....

Realmente no habría porqué utilizar diferentes rutinas, podríamos meter todo el código en el método 'PintarTodo', no obstante nos quedará más claro si cada cosa la acometemos en una rutina diferente, y además me permite realizar mejor seguimiento del código de cara a explicarlo.  

Se ha declarado una variable de tipo long 'DerechaIcono' para acoger un valor que nos devuelve la rutina DibujarIcono, y dicho valor es luego pasada a la rutina DibujarTexto, realmente podemos simplificar el código,  eliminado la variable y resumiendo las dos líneas en una sola tal que así: Call DibujarTexto(DibujarIcono) Siempre que nos quede claro y sepamos entender el código...

Ahora iré explicando una a una cada rutina, ya se ha explicado previamente cómo sería si al caso nostros dibujáramos el colorTapiz.
Código: Visual Basic
  1.  
  2. Private Sub DibujarImagen()
  3.     If Not (p_Imagen Is Nothing) Then
  4.         With UserControl
  5.             .PaintPicture p_Imagen, 0, 0, .Width, .Height, 0, 0, ScaleX(p_Imagen.Width, vbHimetric, vbTwips), ScaleY(p_Imagen.Height, vbHimetric, vbTwips)
  6.         End With
  7.     End If
  8. End Sub
  9.  
  10.  
El código para dibujar la imagen ajustada al tamaño del control es sencillo, primero empezamos preguntando si existe una imagen y en dicho caso la pegamos en el usercontrol, ocupando todo el área... aquí solo recordar que debido a que las propiedades tipo IpictureDisp, Ipicture, utilizan valores de medida siempre en la escala himetric que necesitamos recalcular, luego las tenemos que traducir a twips. Aunque hayamos designado usar escalas en pixeles, esta medida estará disponible para el cliente, nuestro width y height estarán en twips (cosas de microsoft) en cambio ScaleHeight y ScaleWidth si que los disponemos en las medidas de la escala indicada. Esto es importante tenerlo en cuenta ya que si no, nuestros resultados saldrían extraños y no acabaríamos de ver donde está el error...

Código: Visual Basic
  1.  
  2. Private Function DibujarIcono() As Long
  3.     ' si las medidas del control satisfacen las medidas mínimas fijadas para el icono, lo podremos dibujar si no, pasamos...
  4.     '  medidas mínimas = 16px icono + (2px relieve, + 2px margen) * 2 lados= 24px.
  5.    ' 1ª comprobación
  6.     If Not (p_Icono Is Nothing) Then
  7.         With UserControl
  8.           ' 2ª comprobación
  9.             If s_UcAlto >= 24 Then
  10.                 If s_UcAncho >= 24 Then
  11.                    ' 3ª comprobación
  12.                     If (s_AltoIcono + 8) >= s_UcAlto Then
  13.                         .Height = Screen.TwipsPerPixelY * (s_AltoIcono + 8)
  14.                     End If
  15.  
  16.                     If (s_AnchoIcono + 8) >= s_UcAncho Then
  17.                         .Width = Screen.TwipsPerPixelX *  (s_AnchoIcono + 8)
  18.                     End If
  19.                    '  y  finalmente dibujamos dibujamos
  20.                     .PaintPicture p_Icono, 4, s_TopIcono, s_AnchoIcono, s_AltoIcono
  21.                     DibujarIcono = s_AnchoIcono + 6
  22.                 Else
  23.                     ' podríamos avisar que si es demasiado estrecho, no dibujar texto
  24.                     DibujarIcono = 4
  25.                 End If
  26.             Else
  27.                 DibujarIcono = 4
  28.             End If
  29.         End With
  30.     Else
  31.         DibujarIcono = 4
  32.     End If
  33. End Function
  34.  
  35.  
Como tenemos que devolver un parámetro hemos decidido usarlo como una función, Esta función tiene un código más largo y expone unas variables y otras razones que nos llevará a hacer añadidos de código en varias partes del control, tal como se irá comentando...

Lo 1º (comentado en el código se marca) que hacemos es verificar si tenemos un icono almacenado, si no es así, no hay nada que pintar... Una vez verificado este punto, entramos en otra verificación (la 2ª) que cumple una de las reglas que acordamos, aquella que decía que si el control medía menos de 24 px. de ancho o alto, entonces no se dibujaba el icono, porque un icono menos de 16px, no se podrá apreciar detalles y será poco menos que una mancha, veremos que el valor de 24 se compara con variables llamadas s_Ucalto y s_Ucancho, que son abreviaturas de UserControl(Uc), estos son valores asignados en otra ubicación y de la que hablaremos cuando terminemos con esta función, nos baste saber que podríamos sustituirlos si se quiere por usercontrol.ScaleWidth y Usercontrol.ScaleHeight, es decir, mantienen las medidas actuales del control en píxeles . Una vez pasado esta regla entramos a verificar otra (la señalada como 3ª) en la que decíamos que si el icono no cabe con los márgenes acordados en el control, entonces ajustábamos el tamaño del control al icono, en aquella dimensión en la que fallara, alto, ancho o amabas... Hay que señalar al respecto que el código de esto no está completo, hay un detalle que cuando se señalen más tarde colocaremos las líneas necesarias (es una optimización), ahora no se explica para no adelantar cosas que no quedaría claro el cómo y por qué.

Una vez hechas las 3 verificaciones que se requieren, finalmente podemos pintar el icono. Para pintarlo, el icono, tiene que ser ubicado correctamente y con las medidas que decía la propiedad IconoTamaño (de momento el valor por defecto es 0 que corresponde con 24 x 24 píxeles y que se guardaban dichos valores en las variables s_anchoIcono y s_altoIcono, sabemos también que el icono va  alineado a la izquierda dejando 2 píxeles de relieve + 2 ´pixeles de margen entre éste y el icono, por tanto sólo nos falta la cordenada Y, de momento a esa distancia la llamamos s_TopIcono y debemos calcularla. Podríamos calcularla aquí, pero entonces tendríamos que calcularla cada vez que se deba dibujar y en verdad sólo debe ser calculado dicho valor cuando varíe, cosa que aquí no sucede. Por tanto ahora queda claramente explicada la línea que pinta el icono:  .PaintPicture p_Icono, 4, s_TopIcono, s_AnchoIcono, s_AltoIcono. Y por fin devolvemos un valor... el valor que devuelve la función es el punto a partir del cual queda área libre para dibujar el texto, de modo sencllo decimos que si por la razón que sea no se dibuja el icono, este valor valdrá 4 píxeles, los 2 del relieve + los 2 de margen, y por otro lado si se dibuja el icono, hay además que añadir el ancho del icono + 2 píxeles de margen entre exte y el texto, luego esto vale: s_anchoicono + 6.

Ahora pasamos a explicar dónde y cuando calculamos esa nueva variable que hemos, incluído s_TopIcono. Como acabamos de señalar lo más conveniente es no hacer cálculos más que cuando sea necesario, en los controles para muchas variables esto es únicamente cuando su valor cambia, entonces pensemos un poco cuando puede cambiar la posición en la cordenada Y del icono, sólo hay 2 casos, cuando cambia el tamaño del icono y cuando cambia el tamaño del propio control....
Bien pués entonces (esta variable la podemos crear sobre la marcha) ahora nos vamos a la sección de declaraciones y añadimos la variable, justo debajo de otras que también acompañaban valores del icono...
Código: Visual Basic
  1.  
  2. Private s_AnchoIcono As Byte
  3. Private s_AltoIcono As Byte
  4. Private s_TopIcono As Integer  '  señala  la cordenada Y donde se dibuja el icono en el control...
  5.  
  6.  
Y ahora hacemos un añadido para actualizar convenientemente este valor cuando cambie el tamaño del icono... como el tamaño del icono, lo tenemos que siempre se asigna desde la rutina 'MedirIcono', vamos allí y añadimos la línea tal como se muestra en el código...
Código: Visual Basic
  1.  
  2. Private Sub MedirIcono()
  3.    ' ......................
  4.         Case 20
  5.              s_AltoIcono = 128
  6.         Case Else
  7.             s_AltoIcono = 24
  8.     End Select
  9.     s_TopIcono = (s_UcAlto - s_AltoIcono) / 2  ' esta es la línea añadida
  10. End Sub
  11.  
  12.  
La forma de calcular, debe coincidir con otra regla que dijimos, el icono verticalmente deberá quedar centrado, la forma de centrar 2 objetos entre si es calcular el medio de ambas y hacer coincidir esos centros, es decir: (alto contenedor/2) - (alto pieza/2) en matermáticas esto se puede abreviar puesto que el divisor es el mismo como: (alto contenedor - alto pieza) /2 que es lo que hemos hecho. Nuevamente vemos que aparece la variable s_Ucalto que como dijimos puede remplazarse por Usercontrol.ScaleHeight.

Ahora ha llegado el momento de explicar cosas referentes a las medidas del control, aunque sobre esto volveremos en varias ocasiones a fin de explicar los diferentes detalles que afectan a cada situación, ahora como excusa para tomar los valores de las variables s_Ucalto y s_Ucancho y luego profundizamos en todo esto:
Código: Visual Basic
  1.  
  2. ' al final de la sección de declaraciones (de variables)
  3. Private s_UcAncho As Long  'ancho en píxeles del control
  4. Private s_UcAlto As Long  ' alto en píxeles del control
  5.  
  6.  
Estas variables se actualizan cada vez que las medidas del control cambian, y cuando estas cambian se registra un evento resize, la 1ª vez que ocurre es cuando se crea la instancia, y luego cada vez que el cliente cambia de tamaño y además cada vez que nosotros desde código le cambiamos de tamaño. Entonces ponemos la declaración del evento y se irá llenando de código como otros eventos vistos hasta ahora, es decir sobre la marcha...
Código: Visual Basic
  1.  
  2. Private Sub UserControl_Resize()
  3.     s_UcAncho = UserControl.ScaleWidth
  4.     s_UcAlto = UserControl.ScaleHeight
  5.    
  6. End Sub
  7.  
  8.  
Vemos que de momento asignamos el valor a dichas variables. es de notar que estas variables como ya se ha indicado pueden ser eliminadas, no obstante quiero hacer incapié ne dejarlas, por la brevedad con que son escritas y que es más rápido acceder a una variable local que rescatarlo desde una variable que está en un objeto... sobretodo si se usan con frecuencia, aunque en nuestro control no va ser tampoco el caso. Además hay otra circunstancia en el que estas variables (Usercontrol.ScaleHeight y Usercontrol.ScaleWidth) convienen tenerlas duplicadas, cuando se dé el caso lo explicaremos...
Dijimos anteriormente que la cordenada Y del icono dependía de 2 variables (alto del icono y alto del control), cuando cambiaba el alto del icono y cuando cambiaba el alto del control, por tanto en esta rutina también debe ir una copia de la línea que actualiza el valor de s_TopIcono... ya que es posible que haya cambiado el alto del control (aunque quizás solo haya cambiado el ancho), nos iría quedando entonces así...
Código: Visual Basic
  1.  
  2. Private Sub UserControl_Resize()
  3.     s_UcAncho = UserControl.ScaleWidth
  4.     s_UcAlto = UserControl.ScaleHeight
  5.     s_TopIcono = (s_UcAlto - s_AltoIcono) / 2  ' actualizamos el valor de posicionado del icono en la cordenada Y
  6.    
  7. End Sub
  8.  
  9.  

Sobre este evento hay que decir que cuando varía el tamaño del control también es necesario dibujar todo el control de nuevo, si no se hace puede ocurrir que al agrandarlo la parte nueva no tiene contenido gráfico y si se empqeuñece, se pierde contenido gráfico, por tanto añadimos la línea de llamada al dibujado:
Código: Visual Basic
  1.  
  2. Private Sub UserControl_Resize()
  3.     s_UcAncho = UserControl.ScaleWidth
  4.     s_UcAlto = UserControl.ScaleHeight
  5.     s_TopIcono = (s_UcAlto - s_AltoIcono) / 2
  6.  
  7.     Call DibujarTodo   ' necesitamos redibujar todo el control... para que los gráficos llenen el nuevo espacio que ocupa del control, con cada cosa perfectamente ajustado en su sitio.
  8. End Sub
  9.  
  10.  
Ahora que hemos añadido esta línea de código que manda a dibujar el control es el momento de localizar en todas las propiedades donde fuimos dejando la famosa línea comentada (' añadidos posteriores) y remplazarla por la línea de código que estábamos esperando: Call DibujarTodo   ...recordad que aparece en varios sitios, en muchas de las propiedades que hasta el momento tenemos... así pués proceded a la sustitución de la línea comentada por la indicada.

Recordemos que estábamos explicando las rutinas de dibujado, y la de icono ya ha quedado suficientemente clara (aunque volveremos a ella para señalar una optimización que se puede dar en determinadas cicunstancias. Ahora pués pasemos a la de texto, que de alguna manera es similar a la de icono, cada una con sus matices...

Primero recordemos que la rutina del icono nos mandaba si o si un valor que apuntaba a la posición DESDE donde debe empezar a pintarse el texto. Es esta rutina que lo demanda y por ello es que allí se calculaba, por tanto nuestra rutina de dibujar el texto, necesita y utiliza ese parámetro...
Código: Visual Basic
  1.  
  2. Private Sub DibujarTexto(ByVal Izquierda As Long)
  3.     If p_Texto <> "" Then
  4.         Select Case p_Alineacion
  5.             Case 0 ' izquierda:
  6.                 UserControl.CurrentX = Izquierda
  7.             Case 1 '  centro        ancho control - desplaz icono - margenes a izquierda
  8.                 UserControl.CurrentX = ((s_UcAncho - Izquierda - 4) - s_AnchoTexto) / 2
  9.             Case 2 ' derecha:       ancho control - margenes 1 lado - anchotexto
  10.                 UserControl.CurrentX = s_UcAncho - 4 - s_AnchoTexto
  11.         End Select
  12.        UserControl.CurrentY = s_TopTexto ' (s_UcAlto - s_AltoTexto) / 2
  13.         UserControl.Print p_Texto
  14.     End If
  15. End Sub
  16.  
  17.  
Vemos que si no hay texto que dibujar sinplemente no perdemos el tiempo en nada más..
Ahora necesitamos calcular donde se debe dibujar el texto, esto requiere saber las siguientes cuestiones.... 1º que alineación tenemos (recordemos que tenemos una propiedad para decidir esto), 2º en qué cordenada Y empezamos y 3º, en que cordenada X empezamos... ésta última ya está medio resuelta, pués la rutina del icono, nos ofrece este valor, sin embargo este valor no es realmente la cordenada X sino el punto desde que existe espacio para escribir el texto. Ahora esto tiene que ser combinado con cada caso de la alineación, si está alineado a la izquierda en efecto el parámetro recibido es el punto correcto para la cordenada X, si la alineación es centro, entonces tenemos que tomar desde este punto hasta el final del control, menos los píxeles que se van dejando de margen + relieve, y al igual que hicimos para centrar el icono a lo alto, hacemos ahora para centrar el texto en éste hueco al ancho,  este cálculo se hace en la línea de código del CASE 1 y si la alineación es a la derecha, al ancho del control le quitamos los 4 píxeles acordados y ahí finaliza el texto, luego donde empieza será restando su ancho.

El texto al igual que el icono está alineado a lo alto, y al igual que el icono, esto sólo cambia en situaciones contadas, por tanto tampoco es preciso calcular este valor cada vez que se dibuja sino sólo cuando su valor cambie, este valor por tanto responde a las siguientes situaciones, 1º si cambia el alto del  control debe actualizarse la cordenada Y de posición del texto, 2º también sucede cuando cambia la fuente. Antes de poder calcular esto, necesitamos previamente saber cuanto mide el texto. El texto cambia de alto sólo cuando cambia la fuente y  de ancho además de cuando cambia la fuente, cuando cambia el texto, luego necesitamos añadir código en la propiedad fuente y en la propiedad texto...
Código: Visual Basic
  1.  
  2. Public Property Set Fuente(ByRef f As IFontDisp)
  3.         Set UserControl.Font = f
  4.         Call MedirTexto                  ' línea añadida para calcular las medidas del texto
  5.         PropertyChanged "Fuente"
  6.         Call DibujarTodo
  7.  End Property
  8.  
  9. Public Property Let Texto(ByVal t As String)
  10.         If t <> p_Texto Then
  11.             p_Texto = t
  12.             Call MedirTexto                ' línea añadida para calcular las medidas del texto
  13.             PropertyChanged "Texto"
  14.             Call DibujarTodo
  15.         End If
  16.     End Property
  17.  
  18. Private Sub MedirTexto()
  19.     s_AnchoTexto = UserControl.TextWidth(p_Texto)
  20.     s_AltoTexto = UserControl.TextHeight(p_Texto)
  21.  
  22.  End Sub
  23.  
  24.  
Como se puede ver hemos incluído una llamada a una función para calcular las medidas del texto, tanto en la propiedad Texto, como fuente, y hemos creado la rutina que calcula el tamaño del texto en píxeles.
Sin embargo existe otro punto desde el cual es preciso calcular el texto, recuérdese que el texto además cambia cuando se lee desde Readproperties (antes de readpropoerties, p_texto es una cadena vacía),  para almacenarlo en la variable p_Texto, se ejecuta una sola vez, pero también allí necesitamos incluir una llamada a esta rutina... es además la razón por la que usamos una rutina y no las líneas in situ...(desde donde es llamada)
Código: Visual Basic
  1.  
  2.  
  3. Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
  4.     With PropBag
  5.         '....................
  6.         p_Texto = .ReadProperty("Texto", UserControl.Extender.Name)
  7.         Call MedirTexto   ' línea añadida para calcular las medidas del texto,   justo en este momento... después de actualizar p_Texto
  8.         '..................
  9.  
  10.  
Ahora que ya hemos calculado las medidas del texto, podremos calcular también la posición de la cordenada Y del texto, y de paso añadimos las 3 variables necesarias para controlar estas medidas referentes al texto.
Código: Visual Basic
  1.  
  2. ' en la sección de declaracion de las variables...
  3. Private s_AnchoTexto As Long       ' longitud del texto en píxeles (no en caracteres).
  4. Private s_AltoTexto As Integer       ' altura del texto       "                   "
  5. Private s_TopTexto As Integer        ' posición de la cordenada Y del texto
  6.  
  7. Private Sub UserControl_Resize()
  8.     s_UcAncho = UserControl.ScaleWidth
  9.     s_UcAlto = UserControl.ScaleHeight
  10.     s_TopTexto = (s_UcAlto - s_AltoTexto) / 2           ' cálculo actualizado de la cordenada Y del texto
  11.     s_TopIcono = (s_UcAlto - s_AltoIcono) / 2
  12.     Call DibujarTodo
  13. End Sub
  14.  
  15. Private Sub MedirTexto()
  16.     s_AnchoTexto = UserControl.TextWidth(p_Texto)
  17.     s_AltoTexto = UserControl.TextHeight(p_Texto)
  18.     s_TopTexto = (s_UcAlto - s_AltoTexto) / 2              ' cálculo actualizado de la cordenada Y del texto
  19. End Sub
  20.  
  21.  
Como se aprecia se ha añadido la línea que calcula por fin la posición en la cordenada Y del texto, como dijimos esto sucede cuando varía el alto del contenedor o el alto del texto, por eso una línea lo actualiza en el resize del usercontrol (cuando varía su tamaño)  y la otra cuando se invoca la rutina que calcula el alto del texto...

Una vez que por fin sabemos las cordenadas de destino del texto, podemos dibujarlo, los valoes de posición se establecen a las variables currentX y currentY para empezar a dibujar y finalmente con un Usercontrol.print p_Texto, tenemos dibujado el texto donde queríamos.

Ahora es el momento de guardar el proyecto, ir al formulario y hacer pruebas... empezamos por eliminar la instancia del control y ahora la añadimos de nuevo veremos como ahora si por fin se dibuja la imagen (que por defecto añadimos con cada control) y veremos como el texto aparece alineado a la izquierda, si como yo, habeis elegido una imagen un poco oscura tal vez convendría entonces ir a la ventana de la interfaz del control y poner el color del texto (forecolor) de color blanco para que se aprecie bien. probemos a modificar la alineación, el texto  y la fuente, y veremos que se ejecuta correctamente... Hay sin embargo 2 peros...

El primer 'pero, si ponemos un texto en el botón excesivamente largo, veríamos que podría hacer cosas raras, concretamente cuando la alineación  es derecha o centro. La cosa rara es que el texto retrocede hasta más atrás del punto de donde se suponía que era el espacio asignado, es decir desborda por los extremos.
Corregir esto queda como ejercicio para el interesado. Primero deberá decidir que opción entre las posiblesle convence más y luego añadir el código para resolverlo a como se ha elegido, esto sin embargo se recomienda que se deje para cuando terminemos el control a fin de que el código pueda comprobarse mientras estamos diseñando, variando y optimizando. Se comenta sin embargo algunas de las opciones posibles y se invita a que vosotros mismos ideeis otras posibles soluciones.
Si un botón ha de definir una acción, una funcionalidad adecuada es que el texto del botón tenga un tamaño finito, esto sugiere que se podría fijar una cantidad de caracteres finitos para el texto, por ejemplo 255 caracteres ya da para exponer un texto muy largo. Otra opción posible es que se corte en líneas, el texto, es decir el que no cabe en una línea debería continuar en la siguiente, procurando que las palabras queden enteras en cada línea (los botones de VB hacen este caso), una 3º opción sería descartar el texto que no quepa en el hueco proporcionado y finalmente la opción que se ha adoptado en el diseño es que el texto desborde por los extremos si es excesivamente largo.
Lo más razonable francamente es  limitar la cantidad de caracteres del texto,  si alguien necesita un texto de 2000 caracteres o 2Mb. para un botón... entonces no necesita un botón, necesitará un control de texto con barras de scroll para recorrer el texto. La opción de poner barras de scroll al botón es de todo punto una chapuza, no la considereis. También conviene recordar que el cliente puede hacer que el tooltiptext del botón contenga el mismo texto del botón si este aparece cortado, por lo que una buena opción sería enviar un evento cuando el texto desborda el hueco libre para el texto.  Cuando abordemos los eventos ya pondremos el añadido correspondiente para esta situación.

El 2º pero con el texto es un caso, que sólo se produce en unas circunstancias concretas que pasamos a describir y a darle solución. Si creamos una instancia y mientras aún no le hemos cambiado ni el texto ni la fuente, cambiamos la alineación a una alineación centrada, veremos que realmente no queda centrada... si se repasa el código de centrado se tiene que el código es correcto, entonces donde está el problema... la causa se debe a la llamada que hemos hecho en eadproperties: call MedirTexto, el texto se ha leído correctamente y la medición del texto es correcta excepto por un valor, la fuente debe ser leída antes que el texto, ya que si no nuestro calculo se realiza en base a la fuente actual, que por defecto al construirse el control es ambient.font, por tanto esto podemos corregirlo desplazando el readproperties de Fuente justo delante de la lectura de P_Texto, quedando así:
Código: Visual Basic
  1.  
  2. Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
  3.     With PropBag
  4.         ' ........................
  5.         p_Texto = .ReadProperty("Texto", UserControl.Extender.Name)
  6.        ' ....................
  7.         Set UserControl.Font = .ReadProperty("Fuente", UserControl.Ambient.Font) 'la lectura de esta propiedad debe anteceder a la de textoo bien la medición del texto hacerse después de la lectura de la propiedad fuente
  8.        Call MedirTexto
  9.         ' ........................
  10.  
  11.  
Este es el tipo de errores que podría a uno despistarle y tenerle dando vueltas hasta que entiende en qué consiste el error... Como se ve este error sólo se da en unas circunstancias muy concretas, la fuente que utiliza el texto no es la misma que la que utiliza el contenedor y la medición del texto se realiza antes que se haya realizado la lectura de la fuente con la que se dibujará el texto...

En el momento de medir el texto en la llamada hecha desde ReadProperties, s_TopTexto, tampoco arroja un valor correcto, debido a que s_UcAlto aún no se le ha asignado un valor, no obstante no incurriremos en ningún error, porque justo al salir de ReadProperties se ejecuta Resize y hay ya se da valor a s_UcAlto y también se actualiza s_TopTexto.

Podemos también probar a cargar una imagen para ver como queda en el espacio destinado al icono, (que llamemos a la propiedad icono, no implica que deba ser necesariamente un icono, vale una imagen jpg, aunque lógicamente sería absurdo cargar una imagen de 2Mb, en el espacio destinado, si la hemos llamado icono es para sugerir la conveniencia de una imagen con un tamaño pequeño...) probad también a modificar la propiedad Iconotamaño (que se prefirió a llamarla TamañoIcono que aunque es más natural en nuestro idioma, sobre la ventana de propiedades, sobre el intellisense y para hacer memoria, es bueno hacer que las propiedades relacionadas con el icono empiecen por icono hace que aparezcan juntas... Fijaos en cambio como AlineacionTexto no ha sido llamado TextoAlineacion, así teneis un contraste con el que comparar. Esta es la razón subyacente que suelo usar para la designación de propiedades, ya que si uno encuentra icono podría preguntarse si existe alguna propiedad más relacionada con el icono, sólo un recorrido completo indicaría que existe TamañoIcono, en cambio si aparece junto a Icono, IconoTamaño, resulta evidente... aunque naturalmente cada cual es libre (para sí) de darle nombres con otros criterios siempre que sean razonados y se observe en ello una intención y no acabe en un caos...  Sería conveniente unificar el criterio para ambas propiedades, o hacer que alineacionTexto se llame Textoalineacion o hacer que Iconotamaño se llame Tamañoicono, como esto es una guía, de enseñanza yo no lo toco...

En este momento, también, vamos a realizar la optimización en que hemos recalado de soslayo al hablar en la rutina de DibujarIcono. Si se observa, cuando estamos en medio de la rutina de dibujarTodo y hemos llegado hasta la rutina DibujarIcono, si se da el caso de que el alto o bien el ancho del icono fuera mayor que la medida correspondiente del control, esta medida del control se modificaba para que al menos se ajustara a la medida del icono, esto provoca acto seguido a dicha línea de cambio, un evento resize y al final del mismo hay una llamada a la rutina de DibujarTodo, pero recordemos que estamos dentro de la misma, luego estaremos ejecutando el mismo código 2 veces, más aún si además del alto, luego sucede lo mismo con el ancho (que el ancho del icono sea mayor que el ancho del control) estaríamos ejecutando el mismo código por tercera vez. entonces surge una cuestión, podríamos hacer que la rutina de dibujado se hiciera una sola vez ?. La respuesta es sí, y el método para hacerlo es variando el orden en que se calcula (que no el pintado) sobre el icono. veamos como nos queda entonces ahora el código de la rutina 'DibujarTodo' con el cambio:
Código: Visual Basic
  1.  
  2. Public Sub DibujarTodo()
  3.     Dim DerechaIcono As Long
  4.     UserControl.Cls ' antes que nada borramos todo
  5.     DerechaIcono = VerificarIcono  ' <--------------- hemos añadido una llamada a parte de la rutina que calcula acerca del icono....
  6.     ' el color tapiz se dibuja desde donde delegamos, luego, esto no es preciso pintarlo con line(x,y)-(x1,y1),color,bf
  7.        
  8.     Call DibujarImagen
  9.     if DerechaIcono = 0 then
  10.              DerechaIcono = DibujarIcono
  11.     end if
  12.     Call DibujarTexto(DerechaIcono)
  13.     'Call DibujarRelieve
  14. End Sub
  15.  
  16.  
Como se ve hemos incluído una llamada a una rutina que 'precalcula' ,  esta rutina es casi todo el cálculo que antes se hacía en dibujaricono, y aún añadiremos más código, pero justo cuando expliquemos qué código y porqué, pondremos ahora el código de la rutina VerificarIcono  y se verá más claro...
Código: Visual Basic
  1.  
  2. Private Function VerificarIcono() As Long
  3.     ' si las medidas del control satisfacen las medidas mínimas fijadas para el icono, lo podremos dibujar si no, pasamos...
  4.     '  medidas mínimas = 16px icono + (2px relieve, + 2px margen) * 2 lados= 24px.
  5.    ' 1ª comprobación
  6.     If Not (p_Icono Is Nothing) Then
  7.         With UserControl
  8.           ' 2ª comprobación
  9.             If s_UcAlto >= 24 Then
  10.                 If s_UcAncho >= 24 Then
  11.                    ' 3ª comprobación
  12.                     If (s_AltoIcono + 8) >= s_UcAlto Then
  13.                         .Height = Screen.TwipsPerPixelY * (s_AltoIcono + 8)
  14.                     End If
  15.  
  16.                     If (s_AnchoIcono + 8) >= s_UcAncho Then
  17.                         .Width = Screen.TwipsPerPixelX *  (s_AnchoIcono + 8)
  18.                     End If
  19.                    ' se devuelve 0.... y aquí no pintamos nada, no es el momento de esta capa .
  20.                 Else
  21.                     VerificarIcono = 4
  22.                 End If
  23.             Else
  24.                 VerificarIcono = 4
  25.             End If
  26.         End With
  27.     Else
  28.         VerificarIcono = 4
  29.     End If
  30. End Function
  31.  
  32.  
Como se puede apreciar es el código que había en la rutina dibujarIcono, excepto que le hemos quitado la línea que pintaba el icono y que devolvía un valor diferente de 4, ahora en su lugar se devuelve 0. Cuando se llegue a la parte de dibujar el icono (como se aprecia en la rutina de 'DibujarTodo', que acabamos de modificar, Se pregunta si el valor devuelto en la verificación es 0, en cuyo caso (antes tocaba), ahora toca dibujar el icono, además devolverá el valor que sigue necesitandose para dibujar el texto, como vemos, al final la variable que usamod para la función (DerechaIcono) es utilizada.
Exponemos el código de la rutina dibujarIcono:
Código: Visual Basic
  1.  
  2. Private Function DibujarIcono() As Long
  3.     UserControl.PaintPicture p_Icono, 4, s_TopIcono, s_AnchoIcono, s_AltoIcono
  4.     DibujarIcono = s_AnchoIcono + 6
  5. End Function
  6.  
  7.  
El código es el que hemos no hemos retirado de la rutina, para formar la rutina VerificarIcono.
Ahora si razonamos la cascada de llamadas, cuando se invoque al resize y éste a su vez volviera a llamar a DibujarIcono, sería antes de haber hecho ninguna otra cosa que borrar. Ahora lo que haremos, por tanto será impedir que avance más allá, para ello pondremos un 'portero' que se encargue de comprobar si tiene el boleto, si tiene el boleto pasa y si no, no pasa... lo vemos reflejado en la siguiente modificación del código de la rutina 'dibujartodo':
Código: Visual Basic
  1.  
  2. Public Sub DibujarTodo()
  3.     Static Portero As Boolean           ' los porteros siempre se declaran estáticos, su valor permanece entre llamadas...
  4.     Dim DerechaIcono As Long
  5.    
  6.     If Portero = False Then
  7.         Portero = True
  8.         UserControl.Cls ' antes que nada borramos todo
  9.        
  10.         DerechaIcono = VerificarIcono
  11.         ' el color tapiz se dibuja desde donde delegamos, luego, esto no es preciso pintarlo con line(x,y)-(x1,y1),color,bf
  12.            
  13.         Call DibujarImagen
  14.         If DerechaIcono = 0 Then
  15.             DerechaIcono = DibujarIcono
  16.         End If
  17.         Call DibujarTexto(DerechaIcono)
  18.         'Call DibujarRelieve
  19.        
  20.         Portero = False
  21.     End If
  22. End Sub
  23.  
  24.  
Como vemos el portero hace la siguiente pregunta: Se ha vuelto a entrar sin antes habir salido por el final ? . si es sí, se sale sin ejecutar nada, si es no, primero se sela el boleto de entrada y se ejecuta todo, y justo antes de salir el boleto se retira.

________________

Puesto que el texto de esta parte ya va bastante largo y ya hemos conseguido pintar por fin el botón (aunque aún no hemos terminado), lo dejamos por hoy. En la próxima parte terminaremos por finalizar de dibujar (el relieve y el estado desahbilitado) y haremos algún añadido también al evento resize...
«Ma non troppo»
----> ModoVacaciones = False<----

Nebire

  • Miembro HIPER activo
  • ****
  • Mensajes: 670
    • Ver Perfil
Re: Guía: Crear controles de usuario en VB
« Respuesta #6 en: Sábado 11 de Septiembre de 2010, 06:53 »
0
Hoy en esta parte, ahondaremos en el evento resize para mostrar como se controla el tamaño del control y terminaremos de pintar el control dando respuesta al estado deshabilitado y el dibujado del relieve.

En la parte anterior se mostró un 'portero', que es una técnica para asegurarse de que determinado código no se ejecuta más de una vez una vez que se ha emepezado a ejecutar, concretamente lo aplicamos para la rutina 'DibujarTodo' que podía ser reinvocada incluso 2 veces si se originaba en un cambio de tamaño provocado desde dentro del código. Analizaremos el código de modo suelto para entenderlo mejor.
Código: Visual Basic
  1.  
  2. Private Sub Rutina()
  3.     Static portero As Boolean
  4.    
  5.     If portero = False Then
  6.         portero = True
  7.            
  8.          ' aquí el código a ejecutar una sola vez
  9.          ' alguna línea de este código hace unalamada al exterior, desde donde se reinvoca esta rutina.
  10.  
  11.         portero = False
  12.     End If
  13. End Sub    
  14.  
  15.  
Si se examina el código, lo primero que encontramos es que la variable que realiza el control es declarada estática, es decir que su valor permanece, aún después de haber salido de la rutina. Es necesario que sea estática, porque aunque las variables internas conservan su valor (siguen en memoria) hasta que se salga completamente de la misma, sin embargo una rellamada a la rutina crea nuevas instancias de las variables locales al método, luego el valor del 'portero' de la nueva instancia (llamada al método) no tiene nada que ver con el vaor de la llamada anterior y aún en curso. Se entiende por salida del método cuando lo hace por el final o por una salida incondicional (Exit Sub), es decir por una salida sin retorno.
Lo siguiente que vemos es que el portero está controlando el acceso, si el portero es false(es decir no está previamente una llamada cargada en memoria) entonces accede al interior y se marca el portero como 'presente'. Se va ejecutando el código y más adelante puede suceder (o no, no es absolutamente necesario que ocurra, basta con que exista la posibilidad), una llamada al exterior, desde donde puede originarse de nuevo una llamada (directamente o indirectamente a trvés de otra/s rutina/s a la que a su vez ésta llame)  a la rutina de la que procede. Si es el caso, se observa que el portero denegará el acceso a la re-ejecución del código. Finalmente antes de salir de nuestra rutina, debemos indicar al portero, que estamos saliendo, permitiendo con ello que una futura lamada pueda tener acceso.

Como se ve, el mecanismo evita ejecutar código que es invocado recursivamente. La razón para evitarlo, pueden ser varias, desde evitar que desactualice el estado de unas variables a simplemente no perder tiempo en ejecutar código que simplemente es repetitivo de lo que estamos haciendo. Éste era el caso del uso por lo que se aplicaba a la rutina 'DibujarTodo'. Ahora también lo volveremos a aplicar a la rutina Resize, por las razones que se detallan en el siguiente párrafo.

Un control una vez que se crea una instancia de él y es puesto en el formulario del cliente, puede tener las medidas que determine el cliente, ya que de momento no hemos impuesto restricciones, sin embargo nosotros podemos determinar varias cosas a éste respecto:
1 - Podría tener un tamaño fijo e invariable.
2 - Podría tener un tamaño máximo.
3 - Podría tener un tamaño mínimo.
4 - Podríamos evitar que tuviera un tamaño dentro de un rango dado, es decir se permitiría una tamaño menor que cierta medida y un tamaño mayor que cierta medida, pero entre ambas medidas.
5 - Puede no tener restricciones de tamaño, pero si estar sujeto a ciertas reglas, por ejemplo su ancho fuera siempre múltiplo de 10px. , etc...
6 - Puede no tener restricciones de ningún tipo.
Cada caso se aplicaría según convenga o según decidamos, de momento nuestro control se aplica el caso 5, no tiene restricciones (el cliente le puede dar el tamaño que quiera) excepto, cuando el tamaño del icono supera (en alguna de sus dimensiones) las del control, en cuyo caso se fuerza a aumentar el tamaño del control hasta el tamaño del icono + 8 píxeles, por los márgenes ya descritos.
Lo cierto es que carece de sentido hacer un control tan pequeño que resulte difícil deusar, por lo que es aconsejable limitar siempre (al menos) el tamaño mínimo. Por otra parte un control no debe tener medidas cuyo valor sea 0, ya que cualquier posible cáclculo realizado con sus medidas puede ocasionarnos un problema por división entre 0 o incluso arrojarnos un valor negativo... un control jamás va a tener un ancho de -10. el modo de evitar estos posibles problemas es fijar un tamaño mínimo.
Al fijar un tamaño mínimo para un control debe tenerse en cuenta para qué se usa dicho control, y ver qué utilidad práctica resulta para dicho control tener una medida lo más pequeña posible, esto nos servirá para determinar que menor de cierto tamaño para dicho control resulta inútil.
Con esto en mente, determinamos que un botón, puede llegar a tener un tamaño muy pequeñoy aún ser útil, por ejemplo fijémonos en un control de texto previsto para contener una ruta, a menudo puede verse un botón al final del mismo que sirve para seleccionar una nueva ruta, podríamos poner un botón tan ancho que pueda acoger dicho texto: 'Seleccionar ruta', pero a veces vemos este texto es superfluo, es intuitivo que dicho botón debe servir para esto en cuyo caso se pone un botón con el mismo alto que la caja de texto y un acho que permita apenas 3 puntos suspensivos, para indicar que se abrirá una ventana que nos permitirá elegir la ruta...
La descripción de este ejemplo nos demuestra que efectivamente un botón puede llegar a tener un tamaño muy pequeño, fijamos dicho tamaño por tanto en 12píxeles (a gusto vuestro si lo quereis variar ligeramente) .
Código: Visual Basic
  1.  
  2. ' en la sección de declaraciones junto a las otras constantes....
  3. Const c_MinUc = 12               ' tamaño mínimo permitido para el alto y para el ancho del control.
  4.  
  5.  
Ahora nos vamos a limitar el tamaño del control, trabajando sobre el evento resize, antes teníamos...
Código: Visual Basic
  1.  
  2. Private Sub UserControl_Resize()
  3.         s_UcAncho = UserControl.ScaleWidth
  4.         s_UcAlto = UserControl.ScaleHeight
  5.         s_TopTexto = (s_UcAlto - s_AltoTexto) / 2
  6.         s_TopIcono = (s_UcAlto - s_AltoIcono) / 2
  7.         Call DibujarTodo
  8. End Sub
  9.  
  10.  
Ahora añadimos la regulación de las medidas, para ajustarlas al mínimo cuando las medidas que se le den sean menores, es decir comprobamos en cada cambio si las medidas son menores en cuyo caso lo aumentamos al tamaño mínimo. cada medida se hace por separado...
Código: Visual Basic
  1.  
  2. Private Sub UserControl_Resize()        
  3.         If UserControl.ScaleWidth < c_MinUc Then
  4.             UserControl.Width = c_MinUc * Screen.TwipsPerPixelX
  5.         End If
  6.         s_UcAncho = UserControl.ScaleWidth
  7.         If UserControl.ScaleHeight < c_MinUc Then
  8.             UserControl.Height = c_MinUc * Screen.TwipsPerPixelY
  9.         End If
  10.         s_UcAlto = UserControl.ScaleHeight
  11. '       ...............
  12. End Sub
  13.  
  14.  

Si se da el caso de que el ancho del control que en dicho momento se recibe es menor de 12, entonces hacemos que valga 12, lo mismo para el alto. Al hacerlo cambia la medida lo que provoca de nuevo el evento resize, y como dijimos no queremos perder tiempo en ejecutar varias veces el mismo código cuando no es necesario, como en este caso, luego decidimos ponerle un portero, y el código de la rutina, nos queda finalmente tal como se aprecia en el siguiente código:
Código: Visual Basic
  1.  
  2. Private Sub UserControl_Resize()
  3.     Static portero As Boolean
  4.    
  5.     If portero = False Then
  6.         portero = True
  7.        
  8.         If UserControl.ScaleWidth < c_MinUc Then
  9.             UserControl.Width = c_MinUc * Screen.TwipsPerPixelX
  10.         End If
  11.         s_UcAncho = UserControl.ScaleWidth
  12.         If UserControl.ScaleHeight < c_MinUc Then
  13.             UserControl.Height = c_MinUc * Screen.TwipsPerPixelY
  14.         End If
  15.         s_UcAlto = UserControl.ScaleHeight
  16.         s_TopTexto = (s_UcAlto - s_AltoTexto) / 2
  17.         s_TopIcono = (s_UcAlto - s_AltoIcono) / 2
  18.         Call DibujarTodo
  19.        
  20.         portero = False
  21.     Else
  22.         If s_ResizeDesdeIcono = True Then
  23.             s_ResizeDesdeIcono = False
  24.             s_UcAncho = UserControl.ScaleWidth
  25.             s_UcAlto = UserControl.ScaleHeight
  26.             s_TopTexto = (s_UcAlto - s_AltoTexto) / 2
  27.             s_TopIcono = (s_UcAlto - s_AltoIcono) / 2
  28.         End If
  29.     End If
  30. End Sub
  31.  
  32.  
A este código, puede verse que hay un caso aparte cuando el portero no deja acceder... si miramos bien, sucede que si el redimensionado que provoca la llamada recursiva procede desde un cambio del cliente o un cambio por ajuste interno, no necesitamos hacer nada más, ya que cuando regrese de la rellamada terminará de ejecutar las líneas que quedaban. No obstante tenemos un caso más, que desata el evento resize, por nuestra regla del icono, que dice que si éste era de mayor tamaño que el control, se ajustaba el tamaño del control, provoca también este evento. Como ahora hemos puesto un portero, no se ejecutaría el código, sin embargo en este caso necesitamos de modo excepcional que se ejecuten determinadas líneas que mantienen actualizada ciertas variables. Para complicarse más el caso, esta regla puede suceder en las 2 'direcciones', variar el alto tal que hace que sea menor que el alto del icono, o variar el alto del icono para que sea mayor que el alto del control, así las cosas, debemos observar la cascada de llamadas que produce uno y otro caso...
Caso Alto del control se hace menor y como consecuencia este valor es menor que alto del icono:
 1º salta el evento resize por cambiarse el tamaño del control, el portero de esta rutina se pone en TRUE.
 2º llega a la línea que llama a dibujartodo y establece su portero a TRUE.
 3º Esta llamada hace que llegue a la rutina 'VerificarIcono' que comprueba que en efecto el alto del control es menor que el alto del icono.
 4º fuerza por tanto un cambio de tamaño para adaptarlo, y salta un evento Resize, pero el portero está en TRUE, luego no nos permite acceder, sin embargo necesitamos actualizar las variables s_Ucalto (por ejemplo) entre otras...
 5º Sale del evento resize sin establecer el portero a false.
 6º Sale de la rutina 'dibujartodo', y regresa su portero local a False.
 7º Sale del evento resize poniendo el portero a False.

Caso alto del Icono se hace mayor y como consecuencia este valor es mayor que el alto del control:
 1º La propiedad Iconotamaño, llama a 'dibujarTodo'... quien establece su portero a TRUE.
 2º llega a la línea que llama a 'VerificarIcono', comprueba que en efecto el alto del control es menor que el alto del icono.
 3º fuerza por tanto un cambio de tamaño para adaptarlo, y salta un evento Resize, pero el portero está en False, luego nos permite acceder.
 4º El evento Resize, marca su portero a TRUE, y entra en su código, llega hasta la l´nea 'dibujartodo' pero su portero no lo deja entrar.
 5º Sale del evento resize y queda el portero en false.
 6º Sale de la rutina 'Diibujartodo' y pone su portero en false.
 7º Sale de la propiedad Iconotamaño.

Como se ve, cada caso sigue un camino distinto, podemos decir que cuando sucede desde el cambio de icono, el recorrido que hace no requiere código adicional, en cambio si se requiere código para solucionar el caso cuando se debe a un cambio desde el tamaño y SOLO SI en 'verificarIcono' nuevamente se vuelve a cambiar el tamaño para ajustarse al icono. entonces vemos que para que la salida del resize no nos deje sin actualizar las variables que necesitamos actualizar, necesitamos añadir control sobre esta situación.
De entrada creamos un caso adicional al portero del evento resize (ELSE) y creamos una variable de tipo buleana, que nos permita diferenciar éste caso concreto de otros (véase la línea: If s_ResizeDesdeIcono = True Then :  s_ResizeDesdeIcono = False ..... End If).
Y dentro colocamos el código que necesitamos se ejecute para éste caso. Sabemos que por lógica necesitamos tener actualizado el 's_TopIcono' y sabemos que éste a su vez depende de la actualización de 's_Ucalto', luego también la línea para este debe actualizarse y finalmente vemos que cada vez que actualicemos s_UcAlto, también necesitamos actualizar 's_TopTexto'. Por supuesto la variable s_Ucancho debe actualizarse. Tal vez sólo haya cambiado el alto y no el ancho, en cuyo caso no necesitaríamos actualizar s_Ucancho, o al revés que cambiara el ancho y no el alto, en cuyo caso sólo necesitaríamos actualizar sólo s_UcAncho, por tanto podríamos modificar el código para no actualizar más variables que sólo las que cambien, sinembargo puesto que el evento resize nos llega sin discriminación de a quien de las dimsensiones se debe el evento (si al alto, al ancho o a ambas), nos resultará más farragosos y largo separar cada caso que actualizar la 'otra' aunque sólo varíe la 'una'. aun así si nuestor control fuera extremadamente complejo y hubiera mucho código asociado a cada caso podría entonces darse el caso que discriminar cada situación sería más provechosa que no hacerlo. aquí sin embargo no se da el caso.

Falta eso sí, poner la línea de código que señala que el evento viene por 'resize desde icono' que es el significado de la variable s_ResizeDesdeIcono, en el evento resize, la comprobamosy la apagamos, pero no la encendemos, el lugar para encenderla es como se debiera sospechar justa en la línea anterior que proovocan o pueden provocar esta llamada, es decir en la rutina de 'VerificarIcono', tal como se señala en las líneas de código, siguientes:

Antes teníamos...
Código: Visual Basic
  1.  
  2. Private Function VerificarIcono() As Long
  3.     ' ..............
  4.    
  5.         If s_AltoIcono + 8 > s_UcAlto Then
  6.             .Height = Screen.TwipsPerPixelY * (s_AltoIcono + 8)
  7.         End If
  8.         If s_AnchoIcono + 8 > s_UcAncho Then
  9.             .Width = Screen.TwipsPerPixelX * (s_AnchoIcono + 8)
  10.         End If
  11.    ' ................    
  12. End Function
  13.  
  14.  

Ahora debemos tener...
Código: Visual Basic
  1.  
  2. Private Function VerificarIcono() As Long
  3.     ' ..............
  4.    
  5.         If s_AltoIcono + 8 > s_UcAlto Then
  6.             s_ResizeDesdeIcono = True     ' <------------- línea añadida, para visar de donde procede la llamada.
  7.             .Height = Screen.TwipsPerPixelY * (s_AltoIcono + 8)
  8.         End If
  9.         If s_AnchoIcono + 8 > s_UcAncho Then
  10.         s_ResizeDesdeIcono = True     ' <------------- línea añadida, para visar de donde procede la llamada.
  11.             .Width = Screen.TwipsPerPixelX * (s_AnchoIcono + 8)
  12.         End If
  13.    ' ................    
  14. End Function
  15.  
  16.  
Sin olvidarnos de la declaración de la variable...
Código: Visual Basic
  1.  
  2. ' en la sección de declaración de variables...
  3. Private s_ResizeDesdeIcono As Boolean   ' distingue cuando el resize se ordena desde ajuste al tamaño de icono, para actualizar según conviene.
  4.  
  5.  

Ahora ya nuestro control está limitado para que nunca tenga menos de 12 px. de ancho y/o alto, con ello evitamos divisiones entre 0 y valores de cálculo negativos como consecuencia de actualizar variables que no permiten un valor negativo
El texto siempre puede empezar a dibujarse en -10px. sin lugar a error, lo mismo que el icono, aunque el icono a diferencia del texto, nunca tendrá un top negativo, ya que lo forzamos a que como mínimo sea 4px. todavía si el texto tiene un alto muy grande puede quedar cortado. en principio esta corrección debe quedar a cargo del cliente, ya que debe ser consciente de que si el botón tiene 20px de alto no esconsecuente tener la fuente a 60px. de alto. Queda a gusto de cada uno si decide  o no que debe reducirse el tamaño del texto a para que quepa en el control a medida que el alto de éste también disminuye el alto. Yo entiendo que proporcionado una propiedad fuente es el cliente quien tiene medios para remediarlo, y aunque sucede parecido con el tamaño del icono, sin embargo el cliente no está familiarizado con el control, además el ejecrcicio de adaptación se muestra como ejemplo de lo que supone diseñar unapropiedad (icono) que tiene varias reglas y consecuncias y por tanto ejemplo de como se han ido resolviendo los posibles problemas uno a uno.

También se alienta para que cada uno decida si el texto debe o no dibujarse en otra capa, es decir antes del icono, ya que si habeis probado lo suficiente el control podreis ver (nunca con una alineación izquierda) como el texto cuando es largo respecto del control se monta sobre el icono... en partes anteriores ya se habló de posibles soluciones para deciidir que hacer con el texto en estos casos y ahora se comenta el caso de si conviene mejor dejar en la capa más cercana el icono y justo debajo el texto, o dejarlo como está. En cualquier caso tal como se avisó, este control tiene el propósito de servir de modelo para aprender a diseñar modelos, por lo que no es absolutamente necesario resolver todas las cuestiones que puedan darse, pero si comentar acerca de como puede solucionarse y que el aprendiz aporte y aplique por su cuenta su propia solución.

Ahora pasaremos a dibujar el relieve que comose recordará nos quedó pendiente. El relieve que haremos será de tipo 3D, y por tanto al botón le daremos un aspecto similar a como estamos acostumbrados a verlo, cuando el botón está en estado normal le damos un relieve 'UP' y cuando se pulse el botón, le daremos un relieve 'Down'. Puesto que estos estados están proporcionados internamente, no se establece una propiedad relieve para el botón, pero que si podría ponerse para contrles comopor ejemplo una caja de texto. Por dicho motivo la variable que ha de mantener el estado interno de dicho relieve y puesto que sólo son dos valores podemos asignarlo a una variable Boolean:
Código: Visual Basic
  1.  
  2. ' al final de la sección de declaraciones de variables...
  3. Private s_Relieve As Boolean ' TiposDeRelieve     ' define el relieve en un momento dado.
  4.  
  5.  
En cambio si nuestro relieve fuera una propiedad decidible por el cliente nos interesaría tener una enumeración, parecida a lo que se señala a continuación.
Código: Visual Basic
  1.  
  2. 'Private Enum TiposDeRelieve
  3. '    RELIEVE_HUNDIDO = -1
  4. '    RELIEVE_PLANO = 0
  5. '    RELIEVE_ALZADO = 1
  6. '    RELIEVE_BORDEADO = 2  
  7. 'End Enum
  8.  
  9.  

Hemos decidido pués como será el comportamiento de nuestro relieve, será automático, elevado en estadonormal y hundido cuando se pulse el botón. Hagamos pués la rutina de dibujado:
Código: Visual Basic
  1.  
  2. Private Sub DibujarRelieve()
  3.     Dim c(0 To 3) As Long, scol As Long
  4.    
  5.     ' prepara los colores y las líneas.
  6.     Select Case s_Relieve
  7.         Case True 'RelievesBorde.RELIEVE_HUNDIDO  ' CLAROS abajo , oscuros ARRIBA
  8.             c(2) = &HFFFFFF: c(3) = &HF1EFE2: c(1) = &H9D9DA1: c(0) = &H716F64
  9.         Case False 'RelievesBorde.RELIEVE_ELEVADO ' CLAROS ARRIBA, oscuros abajo
  10.             c(0) = &HFFFFFF: c(1) = &HF1EFE2: c(2) = &H9D9DA1: c(3) = &H716F64
  11.         'Case RelievesBorde.RELIEVE_BORDEADO
  12.          '   scol = 16777215 - UserControl.BackColor
  13.           '  c(0) = scol: c(1) = scol: c(2) = scol: c(3) = scol
  14.         'Case RelievesBorde.RELIEVE_PLANO
  15.          '   Exit Sub
  16.     End Select
  17.    
  18.     ' rodeando el control
  19.     ' vertical izquierdo externo y vertical izquierdo interno
  20.     UserControl.Line (s_UcAncho - 1, 0)-(s_UcAncho - 1, s_UcAlto), c(2)
  21.     UserControl.Line (s_UcAncho - 2, 1)-(s_UcAncho - 2, s_UcAlto - 1), c(3)
  22.     ' horizontal superior externo y horizontal superior interno
  23.     UserControl.Line (0, 0)-(s_UcAncho, 0), c(0) ' vertical
  24.     UserControl.Line (1, 1)-(s_UcAncho - 1, 1), c(1)
  25.     ' horizontal inferior externo y horizontal inferior interno
  26.     UserControl.Line (0, s_UcAlto - 1)-(s_UcAncho, s_UcAlto - 1), c(2)
  27.     UserControl.Line (1, s_UcAlto - 2)-(s_UcAncho - 1, s_UcAlto - 2), c(3)
  28.     ' vertical derecho externo y vertical derecho interno
  29.     UserControl.Line (0, 0)-(0, s_UcAlto), c(0)  ' horiz
  30.     UserControl.Line (1, 1)-(1, s_UcAlto - 1), c(1)
  31. End Sub
  32.  
  33.  
Nuestra rutina de dibujado del relieve es muy sencilla, usamos 4 colores 2 claros y 2 oscuros, los claros se colocan, arriba en la vertical y la horizontal izquierda, los oscuros en la vertical inferior y horizontal derecha, esto por sí solo nos da una impresión visual de relieve, ya el relieve y el 3D no existe realmente en un plano, siempre será una impresión visual (un engaño a la vista).
La configuración de hundido, es opuesta a la configuración de elevado, es decir donde están los colores claros se colocan los oscuros y viceversa.
Hay que señalar que existe una API, que realiza el relieve 3D, aceptando el parámetro del tipo de relieve, no obstante vemos que realizarlo a nuestro antojo es tan sencillo que no nos planteamos recurrir a ella.
Sobre el propio código y de forma comentada, está superpuesto el tratamiento que se le daría al relieve si fuera expuesto como una propiedad y ésta fuera elegible por el cliente. Obsérvese que el 'borde plano', equivaldría a no dibujar nada, es decir dejar el color del fondo o lo que muestre una imagen, y que el 'borde coloreado' requiriría una propiedad llamada 'ColorBorde' y que por tanto podría ser un color distinto del ColorTapiz, sería por tanto un recuadro perimetralde un determinado color de 2 píxeles de ancho, sin embargo se dibuja con la misma técnica en todos los casos.
El código de dibujado de las líneas, se traza línea a línea porque recordemos que cada color dibuja un diedro (las 2 líneas que van a parar a una esquina). finalmente indicar que los colores elegidos no conviene que sean justo el blanco y negro, ya que el caso habitual es que un fondo donde se hay sea blanco o negro, con lo cual una de nuestras líneas se quedaría 'perdida' con el fondo, por tanto si desviamos los colores ligeramente hay posibilidades de que queden 'definidas'... queda por supuesto al gusto de cada cual si decide usar 3 px. para dibujar el relieve en vez de 2. Se eligen 2 colores distintos precisamente para garantizar que 1 si un color queda 'comido' por el fondo externo, y el otro 'comido' por el colorTapiz (es una posibilidad) todavía los colores de la esquina diametralmente opuesta quedan válidos siendo suficientes para reflejar y quedar constancia de una impresión de relieve 3D...

Aún no hemos terminado de hablar sobre el relieve, nos faltan algunos detalles y empezamos por uno de ellos (está emparentado con el relieve luegose indicará porqué), es decir, ahora nos falta decidir como reflejamos el estado de desactivación del control para poder dibujarlo, recordamos que igual que hemos ido viendo a lo largo de la guía, hay varias opciones para llevar a cabo una idea o concepto, básicamente depende de la imaginación del diseñador, yo aquí os propongo una solución muy vistosa que sin embargo tiene algún riesgo que luego se comentará. Antes de proceder con el código, pondremos una imagen de como nos va quedando nuestro control.


Para trabajar con la deshabilitación nos vamos a la propiedad 'Activo', donde haremos cambios y/o añadidos... tal como refleja el código siguiente:
Código: Visual Basic
  1.  
  2. ' Teníamos esto antes...
  3.     Public Property Let Activo(ByVal a As Boolean)
  4.         If a <> UserControl.Enabled Then
  5.             UserControl.Enabled = a
  6.             PropertyChanged "Activo"
  7.             Call DibujarTodo
  8.         End If
  9.     End Property
  10.  
  11. ' ahora con el cambio tendremos esto:
  12.     Public Property Let Activo(ByVal a As Boolean)
  13.         If a <> UserControl.Enabled Then
  14.             UserControl.Enabled = a
  15.             PropertyChanged "Activo"
  16.             Call DibujarActivo
  17.         End If
  18.     End Property
  19.  
  20.  

Es decir hemos remplazado el nombre d ela rutina a la que llamamos, ahora pondremos el código de esa rutina y veremos que el mismo podríamos haberlo puesto dentro de la propiedad, sin embargo no lo haremos ... ¿porqué?, bueno hay razones por las que a veces conviene tener una rutina suelta y no todo el código junto, básicamente tenemos 2 razones para ello, la más razonable es que el mismo código sea llamado desde varias partes (acordaros de la rutina 'MedirTxto') y en otras ocasiones simplemente por claridad, las propiedades nos interesa tenerlas bien despejadas, que sean fácilmente legibles si nos lleva mucho código, pensemosmejor en sacar todo lo que no es el carácter específico de la propiedad fuera, colocado en una rutina (acordaros de la rutina 'MedirIcono'). Esto es lo que hemos hecho y he aquí su código:
Código: Visual Basic
  1.  
  2. Private Sub DibujarActivo()
  3.     If UserControl.Enabled = False Then
  4.         UserControl.DrawMode = 5
  5.         UserControl.Line (0, 0)-(UserControl.ScaleWidth, UserControl.ScaleHeight), vbRed, BF
  6.     Else
  7.         UserControl.DrawMode = 13
  8.         Call DibujarTodo
  9.     End If
  10. End Sub
  11.  
  12.  
Como se ve el código es muy sencillo, al entrar se le pregunta si el control está activo, si lo está (es decir hace un momento no lo estaba), entonces es señal de que estaba deshabilitado, por tanto conviene borrar todo y redibujarlo, tal como sucede en la derivación a la cuestión ¿usercontrol.Enabled=false ?.. else... dibujamos todo de nuevo. En cambio si está desactivado ( es decir hace un momento estaba activo) implica que ahora tenemos que dibujar el estado desahbilitado, y nos basta con 2 líneas de código, en la primera cambiamos el modo de dibujado, por el 5 (Mask Pen Not) y a continuación dibujamos una caja rellena con el color rojo en este modo, el resultado puede apreciarse en la siguiente imagen...


Como se aprecia tiene un efecto de 'deshabilitado' fuera de toda duda, además nos ha sido muy sencillo realizarlo. Fijémonos en el código y ved como una vez que habilitamos el control (activo=true), volvemos a establecer el modo de dibujo al valor 13 (copy Pen).
Antes mencionaba que esta técnica tiene un riesgo, y en efecto lo tiene, nosotros hemos dibujado el cuadro de relleno con el color rojo, y nos ha dado un resultado adecuado, sin embargo esto depende hasta cierto punto del colorido que tiene nuestor control, puede darse el caso de que quede completamente negro o blanco o rojo y puede darse el caso peor aún de que quede un aspecto 'arañado' de colores, es fácil de evitar si nos limitamos a usar colores 'sin mezcla' es decir que esencialmente se componen de 1 o 2 componentes rgb y muy poco o nada del 3º como son los colores fácilmente reconocibles con la vista, (rojo, amarillo, verde, azul, cyan, rosa,etc...) en cambio si elegimos colores cuyos componentes RGB estén bastante presentes, puede darnos aspectos 'desagradables' en algunos casos y más frecuentemente aspectos con una elevada transición en la fusión de colores es decir aparecerían degradados con franjas muy anchas de un tono en vez de temer multitud de franjas impreceptibles con levescambios de tonalidad, por ejemplo sustituyendo el rojo por este color: 723458, una vez deshabilitado me queda complatamente negro.
Para quedar fuera de toda duda sobre el aspecto de nuestro control deshabilitado, podríamos elegir un vaor en el modo de fusión de colores de 6 (invertir color), con lo que obtenemos el negativo... siempre nos dará un resultado aceptable, en este caso no importaq el color indicado.


Todavía nos resta poner el código que se encarga de cambiar el relieve cuando se presiona y suelta el botón...
Código: Visual Basic
  1.  
  2. Private Sub UserControl_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)
  3.     s_Relieve = True
  4.     Call DibujarRelieve
  5. End Sub
  6.  
  7. Private Sub UserControl_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single)
  8.     s_Relieve = False
  9.     Call DibujarRelieve
  10. End Sub
  11.  
  12.  
Nos vamos a los eventos mousedown y mouseUp y ponemos el estado correspondiente a nuestra variable en cada caso y luego acto seguido llamamos a la rutina 'Dibujarrelieve', fijaos que en este caso no llamamos a la rutina 'DibujarTodo', la razón es que el relieve es realmente la capa más cercana de todas, la única que puede tapar al resto y que no es tapada por ninguna otra.

Y para terminar con esta parte por si aún no lo habeis hecho se recuerda que hay que activar la línea que teníamos comentada en el código en la rutina Dibujartodo'
Código: Visual Basic
  1.  
  2. Public Sub DibujarTodo()
  3.     Static portero As Boolean
  4.     Dim DerechaIcono As Long
  5.    
  6.     If portero = False Then
  7.         portero = True
  8.         UserControl.Cls
  9.        
  10.         DerechaIcono = VerificarIcono
  11.            
  12.         Call DibujarImagen
  13.         If DerechaIcono = 0 Then
  14.             DerechaIcono = DibujarIcono
  15.         End If
  16.         Call DibujarTexto(DerechaIcono)
  17.         Call DibujarRelieve ' <---------------------- activamos esta línea que antes estaba comentada...
  18.        
  19.         portero = False
  20.     End If
  21. End Sub
  22.  
  23.  

De dibujar sólo nos resta una cosa... señalar cuando el control tiene el foco, esto lo dejaremos para más adelante, cuando hablemos de otras cuestiones.
En la parte de mañana nos meteremos con los eventos, como provocar y diseñar nuestros propios eventos.
«Ma non troppo»
----> ModoVacaciones = False<----

Nebire

  • Miembro HIPER activo
  • ****
  • Mensajes: 670
    • Ver Perfil
Re: Guía: Crear controles de usuario en VB
« Respuesta #7 en: Martes 14 de Septiembre de 2010, 12:36 »
0
En esta nueva entrega hablaremos de entrada haremos una pequeña corrección a nuestro control y que cómo excusa nos sirve para analizar la cascada de eventos de inicialización con atención a ambos métodos de pintado. También hablamos de algunas optimizaciones que pueden hacerse a la hora de repintar el control.

El correcto funcionamiento sólo puede garantizarse si seprueba exhaustivamente y se corrigen todos los errores que aparezcan  también corrigiendo una funcionalidad 'anormal' de lo que teníamos previsto. A pesar de todo lo que se enseñe, nunca es posible predecir suficientemente, cada paso que nuestro  código deba hacer, al final siempre hay código que debe ser añadido y sobre el que no se había pensado. Esto es más cierto cuanto menos experiencia se tenga respecto de una operatoria.

El presente ejemplo nos sirve a la perfeccción, para ilustrar este tipo de situaciones. si se ha probado a conciencia el control se observará que el control se comporta como deseamos, excepto si en diseño deshabilitamos el control. Esta situación debería hacer que si entonces ejecutamos la aplicación de prueba, el control debería aparecer deshabilitado (lo está, pero su aspecto no se dibuja como se diseñó). Debe entenderse como un 'bug', y para el principiante hasta cierto punto misterioso, nos ponemos pués a la caza del mismo... Si verificamos bien, acabaremos por ver, que el error está en el primer dibujado que se realiza del control, y esto es debido en parte a la forma en que se produce la cascada de eventos, hablemos ahora de esto y se entenderá mejor el error registrado.

Cuando se coloca una instancia del control sobre el formulario, la cascada de eventos que acontecen difieren ligeramente de cuando ya existe y simplemente se pone en ejecución:
A - Cuando se crea una instancia nueva: Siempre el primer evento es 1º initialize, es el lugar ideal para establecer propiedades de objetos, por ejemplo que crean instancias de clase (no es el caso, pero más adelante simularemos necesitar una propiedad dando con ello excusa para este caso) , luego se produce 2º InitProperties, que establece como ya sabemos los valores iniciales para las propiedades a nuestra necesidad, justo detrás se crea gráficamente la instancia que está vacía, pero que provoca un evento 3º resize, luego ocurre un evento 4º Show, el control previamente se ha dibujado en memoria, y ahora el contenido es volcado al monitor, el control aparece ahora dibujado en el formulario.

B - Cuando ya existe una instancia: Antes que nada es destruída, y comienza a crearla de nuevo para el modo diseño o el modo ejecución (en ambos casos la secuencia es la misma, sólo cambia una propiedad de la que más adelante hablaremos). 1º Initialize como ya dijimos, 2º Readproperties (de modo equivalente a InitProperties, se asigna los valores necesarios a las propiedades y variables), 3º Resize. Puesto que nuestro dibujado lo hacemos usando el método que procede de autoredraw, no sucede un evento Paint, ya que nuestro repintado, lo hacemos desde una llamada dentro de resize.

Si tenemos que autoredraw es false, cada vez que quede cubierta una parte del control y éste se descubra (aunque sea parcialmente) se desencadena un evento Paint.... Podemos probar a dibujar con este método si además de desactivar autoredraw, añadimos el evento Paint al formulario y escribimos el siguiente código en su interior: Call DibujarTodo. En este caso el código no estaría optimizado para este método, ya que puesto que este evento ocurre de modo automático, en algunas partes podríamos obviar la orden de RepintarTodo, es decir podría dar lugar a un código ligeramente distinto... (recordad volved a dejar activado autoredraw y eliminad el evento paint, y guardad el proyecto, para que todo siga igual que antes.) .

Bien ahora que ya conocemos la cascada de eventos de inicialización, podremos entender porqué nuestro control no se comportó como esperábamos al dibujarse cuando pasamos de diseño a ejecución de la aplicación... Cuando se realiza el readproperties, podemos consultar (si lo ejecutamos paso a paso F8), que la propiedad Activo (enabled) tiene el valor false, luego sucede el evento resize donde mandamos a pintar todo... pero la parte donde dibuja el control deshabilitado lo realiza la rutina, 'DibujarActivo', la cual sólo se invoca cuando se cambia el valor desde el procedimiento de propiedad...
En la lectura de propiedades, podríamos invocar (hacer que el flujo pase directamente) a las propiedades , es decir tenemos en Readproperties esta línea:
Código: Visual Basic
  1.  
  2. UserControl.Enabled = .ReadProperty("Activo", True)
  3.  
  4.  
y podríamos sustituirla por esta otra:
Código: Visual Basic
  1.  
  2. Activo = .ReadProperty("Activo", True)
  3.  
  4.  
Lo que supondría una llamada a la propiedad en vez de ceder simplemente el valor a su variable interna, de hecho conviene que lo probeis, duplicad la línea, comentad una de ellas y sustituid el 'Usercontrol.enabled =' por 'Activo =' en la línea que no está comentada:
Código: Visual Basic
  1.  
  2. Activo = .ReadProperty("Activo", True)
  3. 'UserControl.Enabled = .ReadProperty("Activo", True)
  4.  
  5.  
Si ahora se ejecuta paso a paso, se verá que el flujo es desviado a la propiedad, quien efectivamente salta a la rutina, 'DibujarActivo', sin embargo esto no acaba por resolver aún nuestro problema, la razón ahora si la deberíamos de sospechar, como se ha señalado, cuando ocurre el evento readproperties, el control aún no está dibujado, por tanto es inútil hacer peticiones de dibujado desde ahí, en cambio es correcto hacerlo desde el evento resize, la cuestión entonces sería como indicarle que ya que está desactivado el control indicarle desde resize (cuando el control se plasma en el formulario), que lo dibuje todo o bien la rutina DibujarActivo... La solución consiste en mantener un chivato, puesto que esto sólo va a ocurrir en esta única situación, ya sabemos que  cuando se active o desactive el control en el futuro lo hará desde la propiedad activo y por tanto se ejecutará correctamente. Luego proveamos el código mínimo necesario para solucionarlo.
Código: Visual Basic
  1.  
  2. ' en la sección de declaración de variables...
  3. Private s_Cargando As Boolean           ' chivato que al leer propiedades permite ejecutar código condicional si el control está desactivado.
  4.  
  5.  

La línea referida al usercontrol.enabled = .... la dejamos intacta, no como Activo = ..... que vimos no resolvía nuestro problema...
Código: Visual Basic
  1.  
  2.         ' ...............
  3.          UserControl.Enabled = .ReadProperty("Activo", True)
  4.     End With
  5.     s_Cargando = True   ' <------------------ esta es la línea introducida
  6. End Sub
  7.  
  8.  
y finalmente el código que se encarga de forzar el pintado de aspecto deshabilitado, .... en el evento resize... detrás de la línea Call DibujarTodo añadiremos el código...
Código: Visual Basic
  1.  
  2.         Call DibujarTodo
  3.        ' <--------------------- este código es lo nuevo añadido....
  4.         If s_Cargando = True Then
  5.             s_Cargando = False
  6.             If UserControl.Enabled = False Then
  7.                 Call DibujarActivo
  8.             End If
  9.         End If
  10.  
  11.  
Como se ve, antes de dibujar el aspecto desactivado, tenemos que tener perfectamente dibujado el control por el método que elegimos para dibjar). Le señalamos que si el control está cargándose (digamos que con ello realmente le decimos que si es la 1ª vez que esto ocurre desde que salió de Readproperties), y está el control desactivado, entonces acuda a la rutina que lo dibuja desactivado, y por spuesto como sólo es una única situación en que esto debe suceder así, luego el chivato es desactivado, para que no interfiera más... Ahora nuestro control se dibuja correctamente en estado desactivado, también cuando se 'carga'.

Podemos ver que el código no es realmente eficiente, en el sentido de que dicho código sólo se ejecuta una vez y sin embargo cada vez que se invoca resize debe investigarse si ' If s_Cargando = True Then...' cuando sabemos que esto excepto la 1º vez siempre en lo sucesivo será falso... podríamos hacer que nunca más necesitara preguntarse ?. La respuesta es no. Para que esto fuera posible, los controles deberían tener todavía un evento más que se rejecutara 1 única vez y que sucediera justo después del resize (aparte del paint, además éste no se eejcuta 1 única vez, ´se eejecuta incluso con mucha más frecuencia que el resize ) y en dicho evento podríamos poner esa llamada a 'dibujarActivo'. cuando uno ha diseñado muchos controles hecha en falta un evento  de tales circunstancias que no existe...

Veamos ahora otro aspecto de ese bug, y veremos como la cosa y el código es distinto si el método elegido para dibujar hubiera sido el Paint, (podeis si quereis volver a desactivar autoredraw e incluir el método paint con la llamada a dibujarTodo), veríais en este caso que cuando el control quedare cubierto por una ventana, luego aldescubrirse se volvería a 'DibujarTodo', por lo que el estado desactivado, nunca se dibujaría, la solución usando este método sería cambiar la llamada por una versión nueva de 'DibujarActivo' o incluso mejor, por una modificación simplificada del código que hemos añadido en el evento resize, quedando entonces el evento Paint con éste código:
Código: Visual Basic
  1.  
  2. Private Sub UserControl_Paint()
  3.     Call DibujarTodo
  4.     If UserControl.Enabled = False Then
  5.         Call DibujarActivo
  6.     End If
  7. End Sub
  8.  
  9.  
Eso si, como vemos cada vez que se necesite pintar le estamos pidiendo que compruebe si el control está o no desactivado para realizar el especto deseado... (volved a poner autroredaw en true y como mínimo dejad comentado el evento paint y su código o bien eliminado.

Realmente existe la posibilidad de hacer que el código (que dibuja el botón desactivado al 'cargar') se ejecute 1 única vez, ahora bien cada cual debe considerar si merece o no adoptar la solución que se detalla a continuación. Si añadimos anuestro control un control de tipo Timer, podríamos cambiar sus propiedades a:
Código: Visual Basic
  1.  
  2. timer1.Enabled=false
  3. Timer1.interval =300
  4.  
  5.  
Después cuando lleguemos al final del evento readproperties (allí donde colocamos el chivato) haríamos :
Código: Visual Basic
  1.  
  2. Timer1.enabled = True
  3.  
  4.  
eltimer se activaría y empezaría su cuenta, antes de que acabe empezará el evento resize y si el código no es pesado se dibjará con seguridad antes de que venza el lapso de tiempo dado al timer (debe ser así para que suceda como queremos, si no este esfuerzo no serviría para nada):
Código: Visual Basic
  1.  
  2. Private Sub Timer1_Timer()
  3.     Timer1.Enabled = False
  4.     If UserControl.Enabled = False Then
  5.         Call DibujarActivo
  6.     End If
  7. End Sub
  8.  
  9.  
Es decir desactivamos el temporizador y si procede se dibuja el estado desactivado, que como dijimos deberá suceder después de que se redibuje todo para que cumpla el efecto deseado. Naturalmente ahora vemos que no es necesario que con cada evento de resize se ejecute el código que pusimos, aunque debe sopesarse si esta nueva solución es mejor, ya que estamos añadiendo un control timer que sólo se ejecutará 1 única vez y sólo si durante el diseño el control se dejó desactivado.

La charla resulta larga, pero espero que sirva para entender como el orden de los eventos y lo que ocurre dentro de cada uno de ellos nos lleve a pensar cuando haya un problema aparentemente ilocalizable, que sea una cuestión sencilla entender que está sucediendo y por ello encontrar fácilmente la causa del problema y que además siempre suele haber más de una solución.
_______________________________________

Debe entenderse que cada método de dibujado, tiene unas pequeñas peculiaridades y que hacen que haya diferencias en el código, para hacer lo mismo, especialmente si queremos optimizar el método usado. Precisamente porque nosotros hemos elegido el método que surge de autoredraw, podemos aplicar algunas optimizaciones a nuestor sistema de dibujado que pasamos a comentar, aunque no traduciremos en código, se deja como ejercicio para el interesado.

Como vemos las diferentes ppropiedades que afectan a la parte gráfica exigen una repintado cada vez que un valor que forma parte del gráfico cambia, sin embargo nos podemos hacer la siguiente pregunta, ¿ es aboslutamente necesario repintar todo cuando cambie sólo una parte ?. La respuesta es no, no es absolutamente necesario. Para razonar la respuesta entremos en detalles, tenemos nuestro botón dibujado, estamos usando una imagen para el fondo, y tenemos el texto alineado a la izquierda, no usamos un icono, ahora deseamos poner un icono... que es más rápido, y qué es más sencillo... estas son las 2 respuestas a completar para justificar nuestra decisión,  seguramente en este caso redibujar todo, sobretodo porque el control es muy pequeño y no supone apenas consumo de CPU, no obstante supongamos que ahora lo hemos redibujado todo y ya tiene el icono, ahora el texto está desplazado, con respecto a donde antes, aunque siguue alineado a la izquierda... en un momento dado hemos decidido cambiar de icono... ahora vemos un momento claro, para no tener que redibujarlo todo de nuevo, nada se monta sobre nuestro icono, y el icono se monta sobre la imagen pero... el icono sigue ocupando la misma área, por tanto podríamos simplemente pegar el nuevo icono en su ubicación actual sin necesidad de redibujar de nuevo todo el control, cubre la misma área de la imagen, luego no es necesario volver a redibujar la imagen, y puesto que nada se monta sobre el icono, tampoco obliga a redibujar lo que no hay encima de él.

Podráimos por tanto optimizar nuestro código si en la propiedad de asignación del icono chequeamos si ya existía un icono y si ahora también se recibe un icono, podría dibujarse el icono en la misma área, sino dibujamos todo:
Código: Visual Basic
  1.  
  2.    if not (p_Icono is nothing) and not(i is nothing) then
  3.        DibujarIcono
  4.   else
  5.         Dibujartodo
  6.   end if
  7.  
  8.  
Por supuesto hacer esto, supone conocer y mantener precalculadas lascordenadas dedestino del icono, no interesa calcularlas de nuevo, si se calculó una vez, interesa mantenerlas en memoria, si las variables en un momento dado eran locales a una rutina ahora habría que hacerlas locales dentro del usercontrol. Como lo son las de las medidas del icono. En partes más arriba se mencionaba que efectivamebnte cabrían posibilidades de optimización sobre el icono, en cambio, por ejemplo si la propiedad que cambia es IconoTamaño, no queda más remedio que cubrir un área diferente, ahora bien si el área a cubrir es más grande nuevamente la optimización es posible desde el momento en que no requeriremos volver a dibujar laa imagen, toda vez que el icoono actual (el área ocupada) quede completamente cubierto por el nuevo tamaño a asignar, el caso no sería valido si varía el ancho y requiere volver a dibujar el texto para alinearlo convenientemente. Más aún un caso que requiera verificar si dada la posición actual del texto quedará cubierto o no por la nueva disposición del icono, tal vez sea o requiera pasos más complejos que redibujarTodo, o simplemente nos alarguen tanto el código que resulte difícil en un futuro entender que hace, dedicar 300 líneas de código para hacer un cálculo de este tipo quizás no merezca la pena el tiempo ahorrado al poner 1 sola línea de código donde se mande 'DibujarTodo'.

Del mismo modo, podríamos pensar acerca de la propiedad texto, por de pronto cualquier modificación delmismo requiere dibujar todo, sería posible que no fuera preciso dibjar todo sino sólo la parte involucrada ?, si de hecho hay 2 formas sencillas que explicamos, la 1ª es entender que dado que el icono está en la parte izquierda, y el texto en la parte derecha, sólo necesitaríamos dibujar esta parte, esevidente que aún así necesitamos redibujar la imagen del fondo que cubre el texto. Una solución para este caso sería mantener una copia en memoria de la imagen, de modo que cada vez que hacemos un resize hacemos lo propio con la imagen en memoria el resultado es que podríamos tomar deorigen (la imagen en memoria), las mismas cordenadas para pegar que las cordenadas de 'recorte' de la zona afectada, puesto que entre el icono y el texto hay 2 px de margen y nuestro relive delimita 2 px podríamos por tanto copiar y pegar ese área delimitada, finalmente luego sólo necesitaríamos volver a dibujar el texto. La ootra opción para redibujar el texto (que también es una solución al problema de la longitud del texto planteado algunas partes más arriba), consiste en no utilizar una orden de pintado del texto, (usercontrol.print) sino usar un control de texto (por ejemplo label), con el fondo transparente, lo cual efectivamente no requiere que personalmente dibujemos de nuevo la imagen, en este caso modificar nuestor texto necesitaría sólo actualizar el texto del label o de un control que explícitamente nosotros hubiéramos diseñado para el caso. No obstante se recuerda que una línea gráfica como usercontrol.print "el texto", es una solución más ligera que usar controles para construir el nuestro, del mismo modo que usar el usercontrol.paintpicture es una solución másligera que usar un control image y no digamos picture.

En fin se deja al esfuerzo del interesado en que aporte sluciones de optimización para el dibujado de los gráficos que sin embargo no resulten engorrosas de entender o aplicar, desde la sencillez. Es importante señalar que conviene cuando menos pensar siempre en este tipo de soluciones aunque no se apliquen finalmente ya que pueden darse casos complejos (imaginemos controles de tipo 'tabulador de fichas') donde las optimizaciones aplicadas puedan ser la diferencia entre un control pesado o fluído.

Para terminar la parte de hoy, cambiaremos parte de la funcionalidad de la imagen, es una optimización pero de funcionalidad, que teniamos prevista para unpunto posterior pero que se adecúa a este momento perfectamente.
Teníamos que nuestra imagen se carga desde el propio control, si tuviéramos muchas imágenes deberíamos pensar en guardarlas como recursos, algo que no es específico de los controles de usuario y cuya técnica se supone que domina el interesado, es por ello que se ha preferido utilizar y explicar este otro método. Nuestra imagen una vez cargada puede el cliente remplazarla por la suya propia, si quisiéramos que fuera fija simplemente haráimos que la escritura de la propiedad fuera privada o puesto que no afecta al código actual, simplemente eliminaríamos esa parte de la propiedad.
Otra opción posible del cliente es que elimine la imagen.
Código: Visual Basic
  1.  
  2. set ctlBoton1.Imagen= nothing ' ó
  3. set ctlBoton1.imagen= loadpicture("")
  4.  
  5.  
 
Sin embargo imaginemos que elimina la imagen en diseño, simplemente accesde a la propiedad y presiona la tecla 'suprimir', por error o no si ahora decide que quiere seguir usando la imagen que nosotros le proporcionamos no tiene modo alguno de hacerlo, salvo que elimine la instancia y vuelva a crearla de nuevo, teneindo con ello que elegir de nuevo las propiedades que antestenía ya ajustadas... Un remedio a esta situación es proveer un propiedad que permita al usuario decidir si elige una imagen personalizada o 'automática' , es decir la nuestra interna.

Con esta idea en mente, acometemos la propiedad Origen de la imagen, y que como ya se explico uno decida si debe llamarse OrigenImagen (más natural) o ImagenOrigen (más localizable) y en base a ello realice los cambios oportunos. esta propiedad tendrá 2 posibles valores, un valor booleano no es muy aclaratorio, por lo que se decide que una enumeración que señale Origen_externo y Origen_interno es lo más adecuado. Pasamos pués a crea la enumeración, la declaración de la variable de almcacenaje, la propiedad, y la persistencia de momento, luego veremos que código se necesita para completar la funcionalidad.
Código: Visual Basic
  1.  
  2. Public Enum OrigenesDeImagen
  3.     ORIGEN_INTERNO = 0   ' la que nosotros proveemos internamente
  4.     ORIGEN_EXTERNO = 1   ' la que el usuario elija externamente
  5. End Enum
  6.  
  7. ' debajo de la declaración p_imagen
  8. Private p_OrigenImagen As OrigenesDeImagen ' procedencia de la imagen: externa=usuario, interna=la que proveemos
  9.  
  10. '  bajo la propiedad imagen
  11. Public Property Get ImagenOrigen() As OrigenesDeImagen
  12.     ImagenOrigen = p_OrigenImagen
  13. End Property
  14.     Public Property Let ImagenOrigen(ByVal io As OrigenesDeImagen)
  15.         If io <> p_OrigenImagen Then
  16.             p_OrigenImagen = io
  17.             'codigo
  18.             PropertyChanged "ImagenOrigen"
  19.         End If
  20.     End Property
  21.  
  22. '  persistencia de la propiedad, también cada línea justo debajo de cada una de imagen, todo ordenado
  23.         p_OrigenImagen = .ReadProperty("ImagenOrigen", 0)    ' interna por defecto
  24.         .WriteProperty "ImagenOrigen", p_OrigenImagen, 0      ' interna por defecto
  25.  
  26. ' inicialización:
  27. p_OrigenImagen = ORIGEN_INTERNO ' valor 0....
  28.  
  29.  
Ya tenemos casi todo el código para esta propiedad listo, nos falta poner lo que debe hacer y en lo que se sustenta, veamos; la imagen se aloja en el usercontrol que durante el initproperties se carga en lavariable p_imagen, sin embargo si luego el usuario cambia la imagen, ya no tendríamos medio de recuperar nuestra imagen, la solución pasa por tener otra variable que contenga un duplicado de la misma (conste que la imagen del control es muy pequeña y de poco espacio, en cualquier caso sólo es duplicada en ejecución en disco sólo consta 1 imagen).
Código: Visual Basic
  1.  
  2. ' en las declaraciones
  3. Private s_Imagen As IPictureDisp        ' salvaguarda de nuestra imagen...
  4.  
  5. ' en initproperties, proveemos el duplicado.
  6.    Set p_Imagen = UserControl.Picture
  7.     Set UserControl.Picture = Nothing
  8.     Set s_Imagen = p_Imagen                     '<-------------------- copiamos la imagen interna.
  9.     p_OrigenImagen = ORIGEN_INTERNO
  10.  
  11.  
Ahora cuando el cliente modifique p_imagen, nuestra imagen se conserva intacta, de momento, (ya se explica luego porqué), ahora procedamos con el código que debe realizar nuestra propiedad.

Básicamente lo que debe hacer la propiedad ImagenOrigen es lo siguiente, si el cliente elige externa, cuando el cliente selecciones una nueva imagen podrá cambiarla, en cambio si ImagenOrigen es interna cuando el cliente intente cambiar la imagen se lo denegaremos, a su vez como el cliente no tiene medio de seleccionar nuestra imagen esta selección (cambio) se produce tan pronto como él elige la opción interna.... plasmemos esta funcionalidad en el código, necesitamos modificar tanto la propiedad imagen como añadir código a la  propiedad ImagenOrigen:
Código: Visual Basic
  1.  
  2. ' estado actual de la propiedad imagen:
  3.     Public Property Set Imagen(ByVal i As IPictureDisp)
  4.         Set p_Imagen = i
  5.         Call DibujarTodo
  6.     End Property
  7.  
  8. '  el código una vez hechos los cambios:
  9.     Public Property Set Imagen(ByVal i As IPictureDisp)
  10.         If p_OrigenImagen = ORIGEN_EXTERNO Then           '<------------------------
  11.             Set p_Imagen = i
  12.             Call DibujarTodo
  13.         Else
  14.              '..... más abajo se explica que va aquí.
  15.         End If                                                                   '<---------------------------
  16.     End Property
  17.  
  18.  
Ahora los añadidos en la propiedad ImagenOrigen:
Código: Visual Basic
  1.  
  2.     Public Property Let ImagenOrigen(ByVal io As OrigenesDeImagen)
  3.         If io <> p_OrigenImagen Then
  4.             p_OrigenImagen = io
  5.             If io = ORIGEN_INTERNO Then
  6.                 Set p_Imagen = s_Imagen
  7.                 Call DibujarTodo
  8.             End If
  9.             PropertyChanged "ImagenOrigen"
  10.         End If
  11.     End Property
  12.  
  13.  
Podemos probar ahora el código añadido sin embargo puesto que no se ha ejecutado initproperties, la variable s_imagen no contiene ninguna copia,por lo que de momento no funciona. Más aún incluso aunque carguemos unannueva instancia del control, la funcionalidad funciona sólo hasta que exista una nueva ejecución que no pase ya por initproperties, es decir cuando se destruya el control y vuelva a ser recreado ya nopasa por initproperties por lo que ya no se asigna la imagen a interna a s_imagen. La solución pasa por usar el evento initialize (por fin se le ve en uso) que es donde se inicializan las variables y el código que necesitamos que se ejecute siempre que arranque el control. Tenemos otra opción, frente a ejecutar este código siempre que se inicialice el control y es escribir a disco la variable s_imagen. Readproperties y writeproperties no sólo pueden guardar variables usadas en las propiedades, puede guardarse lo que necesitemos, incluso un array (esto se explicará en otra ocasión a la espera de que surja), no obstante nos parece que guardar 1 imagen extra a disco y luego rescatarla es más pesado que tomarla de inicialización ya que en la inicialización siempre existe la imagen que le asignamosen diseño, el código para la solución a través de inicialización (la solución más óptima) consiste es pasar las líneas decódigo del evento initproperties al evento initialize:
Código: Visual Basic
  1.  
  2. Private Sub UserControl_Initialize()
  3.     Set p_Imagen = UserControl.Picture
  4.     Set UserControl.Picture = Nothing
  5.     Set s_Imagen = p_Imagen
  6. End Sub
  7.  
  8.  
Las líneas aquí expuestas han sido retiradas del evento initproperties. Si alguien optó por valerse de a persistencia en vez de initialice para guardar y recuperar la imagen he aquí un código que puede usarse, aunque se señale que no puede garantizarse su rescate, si el archivo donde se guarda queda dañado (CtlBoton.ctx)
Código: Visual Basic
  1.  
  2. '   en persistencia
  3. Set s_Imagen = .ReadProperty("ImagenInterna", UserControl.Picture)
  4. .WriteProperty "ImagenInterna", s_Imagen, UserControl.Picture   '<----- uc.picture es un riesgo, ya que podrái almacenarse en s_imagen una copia de la imagen actual que no es la nuestra 'interna'. Aunque esto ocurrirá en situaciones muy excepcionales...
  5.  
  6. ' en initproperties, justo debajo de la asignación de la variable debemos notificar, al objeto encargado de la persistencia:
  7.    Set s_Imagen = p_Imagen
  8.     UserControl.PropertyChanged "ImagenInterna"  '<------------------------ línea añadida
  9.  
  10.  
Puede ahora, pués entenderse que el nombre de la 'propiedad' pasada como parámetro a los métodos de persistencia no tienen porqué coincidir con el nombre de la propiedad real, más aún nisiquiera es obligatorio que existan, dichos nombres simplemente son usados en el archivo para localizar el recurso requerido, por eso Imageninterna es erfectamente válido, aunque no exista ninguna propiedad llamada así. Debe entenderse por tanto que la razón para usar tal cadena de texto, no es otra que la de asimilar a que propiedad guarda 'el puesto' la variable, esta misma cadena si que debe corresponderse con la cadena que se señala en el método usercontrol.Propertychanged "lo que hemos cambiado".

He aquí el código (de formulario) para una vez en  ejecución probar la funcionalidad... señalemos para la propiedad ImagenOrigen el valor externo, y desde la propiedad imagen seleccionemos una imagen para probar, añadamos luego sobre el código del formulario el siguiente código de prueba, ejecutad y pusad en el botón vb que señala y sugiere el código...
Código: Visual Basic
  1.  
  2. Private Sub Command1_Click()
  3.     CtlBoton1.ImagenOrigen = ORIGEN_INTERNO
  4. End Sub
  5.  
  6.  
Deberíamos ver como la imagen es cambiada por la nuestra si se guardó correctamente.
Como se señalaba más arriba, es más óptimo usar lo señalado para el evento initialize, pero el interesado en aprender debería comprobar la funcionalidad de esta alternativa, de modo que una vez verificado deberíamos tornar el código al sugerido más arriba que trasadaba las 3 líneas de initproperties a initialize.

Más arriba pusimos este código:
Código: Visual Basic
  1.  
  2. Public Property Set Imagen(ByVal i As IPictureDisp)
  3.         If p_OrigenImagen = ORIGEN_EXTERNO Then          
  4.             Set p_Imagen = i
  5.             Call DibujarTodo
  6.         Else
  7.              '..... más abajo se explica que va aquí.   <--------------- ahora comentamos esto
  8.         End If                                                                
  9.     End Property
  10.  
  11.  
Hay que decir 2 cosas de esta nueva propiedad y su forma de funcionamiento, una de ellas la 1º que comentaremos nos sirve para ahondar en detales del usercontrol y los estados de diseño y ejecución, y deriva del hecho de que si el usuario intenta cambiar la propiedad imagen desde diseño, verá que al asignar una imagen esta 'no se guarda' y extrañado (porque no conoce el control) se preguntará ¿ qué pasa, por qué no puedo ponerla imagen que quero ?, una forma de evitar esta situación es ayudarle , si en la parte alternativa (else), ponemos código de ayuda, no se verá sorprendido y le ayudaremos aentender el uso del control, por ejemplo, podríamos poner simplemente un Beep, pero aún no sabría por qué pita... pongamos un mensaje...
Código: Visual Basic
  1.  
  2. Public Property Set Imagen(ByVal i As IPictureDisp)
  3.         If p_OrigenImagen = ORIGEN_EXTERNO Then          
  4.             Set p_Imagen = i
  5.             Call DibujarTodo
  6.         Else
  7.             msgbox "Para poder asignar una imagen externa, la propiedad ImagenOrigen debe contener el valor  1 (ORIGEN_EXTERNO), sólo entonces podrá asignar una imagen customizada."
  8.         End If                                                                
  9.     End Property
  10.  
  11.  

El problema del código de esta ayuda es evidente.... en tiempo de diseño será útil, pero en tiempo de ejecución ese mensaje aparecería al usuario final de la aplicación, además en ese momento, carece de sentido tal ayuda y descripción. Esto puede evitarse si usamos un control de cuando estamos en diseño y cuando en ejecución. el control de usuario provee una propiedad a este fin. UserMode. si usermode= true, entonces la aplicación está en modo de ejecución (de la aplicación) y si tiene un valor fase está en modo diseño (de la aplicación). Luego podemos aprovisionar adecuadamente a esa ayuda completando el código con la ayuda de esta propiedad:
Código: Visual Basic
  1.  
  2. Public Property Set Imagen(ByVal i As IPictureDisp)
  3.         If p_OrigenImagen = ORIGEN_EXTERNO Then
  4.             Set p_Imagen = i
  5.             Call DibujarTodo
  6.         Else
  7.             If Ambient.UserMode = False Then
  8.                 MsgBox "Para poder asignar una imagen externa, la propiedad ImagenOrigen debe contener el valor  1 (ORIGEN_EXTERNO), sólo entonces podrá asignar una imagen customizada."
  9.             End If
  10.         End If
  11.     End Property
  12.  
  13.  
Podemos probar la nueva funcionalidad y ver como en diseño, ahora nos ofrece el mensaje de aviso pero no en ejecución. si vamos paso a paso, se podrá consultar el valor de la propiedad usermode en cada ocasión.

Todavía ahy algode la funcionalidad (la 2º cuestión) que no parece ser satisfactoria, veamos como están las cosas paraentender la problemática... Selecciono, usar imagen externa, ok, cargo una imagen externa, ok... ahora quiero volver a usar laimagen externa, ok, se cargó la imagen provista internamente, pero de nuevo al volver a elegir la imagen externa de nuevo tengo que volver a localizar la imagen (durante el diseño en disco), podríamos hacer que una vez que ya he seleccionado una imagen externa y mientras no elija otra pueda alternar entre interna y externa sin localizar ninguna más... ?... Claro que podemos incluso sería una funcionalidad más 'compacta'... se deja como ejercicio para el interesado proveer los cambios que operen esta funcionalidad. Yo incorporo los cambios al código, pero no los comento, cuando al final de la guía se provea el proyecto empaquetado, podrá el interesado verificar si solución coincide o no con la suya propia y sacar las conclusiones que puedieran corresponder si las diferencias fueran grandes. A mi me ha bastado con 3 líneas de código,  cambiar otra de sitio y 3 líneas más en readproperties...
Obsérvese que sin pretenderlo con esta última funcionalidad el cliente podría disponer de 2 imágenes entre las que alternar según y cuando decida, sólo con cambiar el valor de la propiedad ImagenOrigen.

Se podrá acordar el lector que hacíamos alusión al principio de la guía acerca de la funcionalidad idiota que supone cambiar el 'backcolor' de un control commandbutton (de VB) que iba  a la par dela propiedad style... Lo que hemos hecho aquí es  (lo que hicimos al principio) la misma idiotez, subyugar a una propiedad al valorde otra, sólo que hemos superado esa idiotez, de varias maneras, 1º, la proximidad entre propiedades queda acorde (Imagen-imagenOrigen), lo que no sucede con Backcolor-Style, ni con Pictue-Style, 2º infromamos al usuario con un mensaje que le recuerda por qué no se cambia, 3º Hemos añadido funcionalidad que automatiza el cambio, que ademáspuede ser fácilmente observado y seguido por cuanto ambas propiedades afectadas están juntas en la ventana de propiedades y 4º y último porque describimos el uso de la propiedad tal cosa, tal como ahora se explica, ya que ha llegado el momento oportuno.

Hasta ahora hemos trabajado todo a golpe de teclado, pero las propiedades pueden ser provistas (un esqueleto) de modo automático, usando un wizard, no es perfecto, pero ahorra tecleo. Otra cosa que necesitamos hacer para nuestras propiedades y métodos es   proveer info sobre las mismas, como todos sabeis, cuando seleccionas una propiedad en la ventana de propiedades, al pie aparece un pequeña info que describe las propiedades... eso es lo que vamos a hacer ahora mismo, yo indico como y muestro una imagen, pero cada uno deberá hacer lo propio para cada propiedad.
Vayamos al menú 'Herramientas' de Vb, al desplegarlo pulsamos en 'Atributos del Procedmiento', se abre una ventana que contiene todos los métodos, eventos y propiedades declarados como públicos, seleccionando sobre la lista cada una, puede ponerse debajo en la caja de texto la info sobre la propiedad, evento o método. Una vez hecho debe pulsarse aplicar, para que dicha info se guarde aparejado a la propiedad. cuando hayamos terminado con todos pulsamos aceptar para cerrar la ventana, cuando guardemos el código, dicha info se guarda realmente en el código del control, pero queda oculto cuando se carga el proyecto, en cambio si abrimos el archivo CtlBoton.Ctl con worpad, por ejemplo, no nos filtra dichas líneas...un ejemplo de lo que contiene para la propiedad imagen (en mi caso):
Código: Visual Basic
  1.  
  2. Public Property Get Imagen() As IPictureDisp
  3. Attribute Imagen.VB_Description = "Devuelve o establece una imagen que se autoajusta al fondo del control. Para cargar una imagen externa, la propiedad ImagenOrigen debe tener el valor establecido a 0 (Externo)."
  4.     Set Imagen = p_Imagen
  5. End Property
  6.     Public Property Set Imagen(ByVal i As IPictureDisp)
  7.         Set s_ImgUsuario = i
  8.         If p_OrigenImagen = ORIGEN_EXTERNO Then
  9.             Set p_Imagen = i
  10.             Call DibujarTodo
  11.         Else
  12.             If Ambient.UserMode = False Then
  13.                 MsgBox "Para poder asignar una imagen externa, la propiedad ImagenOrigen debe contener el valor  1 (ORIGEN_EXTERNO), sólo entonces podrá asignar una imagen customizada."
  14.             End If
  15.         End If
  16.     End Property
  17.  
  18.  
Se ve la línea justo debajo de Public property Get..... y si vamos al formulario ahora cuando seleccionemos la propiedad imagen también al pie podrá cargarse y leerse su info (se muestra en la imagen...).

El texto de ayuda tiene un tamaño limitado, por lo que se ha de ser conciso y claro. Más adelante se describirá un poco por encima las opciones que aparecen en Avanzadas.

Una última optimización para la imagen podría consistir en proveer una funcion que permitiera cargar imágenes png. Como sabeis, VB sólo da soporte para imágenes de tipo bitmap (bmp, rle), metarchivos (emf, wmf), iconos e imágenes gif y jpg). RLE es el bormato BMP con compresión...


el texto ya va largo, por hoy dejamos esta parte, en otro momento hablamos de los eventos,cómo crearlos,  cómo provocarlos, cómo diseñarlos y donde y porqué ubicarlos...
«Ma non troppo»
----> ModoVacaciones = False<----

Nebire

  • Miembro HIPER activo
  • ****
  • Mensajes: 670
    • Ver Perfil
Re: Guía: Crear controles de usuario en VB (Controles ActiveX)
« Respuesta #8 en: Sábado 18 de Septiembre de 2010, 16:21 »
0
Hay más cosas que decir de las propiedades, más adelante veremos (al paso que explicamos eventos) como se realiza una propiedad que es escribible sólo x veces, pero ahora que nuestro control está perfectamente dibujado, es el momento de avanzar, más adelante cuando volvamos sobre las propiedades, con esos cambios que de modo pactado, sugerimos que se nos ocurrirán a última hora y que aprovecharemos para ver como añadidos de última hora nos afecta al código ya realizado, es entonces cuando volveremos sobre las propiedades y diremos algunos detalles más...

Ahora nos centramos en explicar lo fundamental que debe saberse de los eventos: cómo se declaran, como se utilizan, dónde van, qué deviene de ellos, síncronos o asíncronos, etc... . Hasta ahora podemos asignar propiedades (casi todas relacionadas con un aspecto visual) y podemos cambiar sus valores tanto por diseño como por código, pero nos falta que el control provea eventos para que el cliente pueda darle utilidad...

Los eventos son mecanismos de 'intro-alimentación', es decir provocan interrupciones dentro de su ejecución lanzando una llamada al cliente para que pueda en dicho momento introducir (ejecutar) el código deseado o tomar decisiones... y luego regresa el flujo de control al código... por tanto nos centraremos en ver los eventos que para un control son importantes y los eventos que dadas las características del control pueden ser deseables...

Al igual que con las propiedades, el usercontrol, nos provee de eventos, de hecho excepto las propiedades y funciones añadidas el resto del código descansa sobre los eventos que nos provee el usercontrol, no obstante no todos los eventos que nos provee el control son útiles para el cliente, ni los necesita, por lo que muchos de ellos quedarán ocultos y sin trascender fuera,  hay sin embargo 2 de ellos sobre los que hemos puesto código y sobre los que más adelante colocaremos código; los eventos MouseDown y mouseUp...

El modo de  crear y desatar eventos al cliente consiste en un mecanismo (visualmente) compuesto de 2 partes, en el primero se hace una declaración de los eventos que se van a emplear, de modo parecido a como se declaran variables, en la 2ª parte simplemente se usan...

La declaración es como sigue: 1º Alcance - 2º Evento - 3º nombreevento 4º (lista de parametros), veamos un ejemplo en código de una declaración, que usaremos para enviar al cliente un evento TextoCambiado:
Código: Visual Basic
  1.  
  2. ' Colocar debajo dela declaración de variables, dejando unás líneas vacías, y antes de las propiedades....
  3. Public Event TextoCambiado(ByVal NuevoTexto As String, ByVal TextoAnterior As String)
  4.  
  5.  
Aquí vemos una declaración de evento que pasamos a comentar. El alcance implica hasta donde será visible ese evento, en VB todos los eventos son públicos, por lo que puede ser omitido, se supone que en una nueva versión esperada (esto ya nunca será) podría haber eventos 'Friend' es decir que sólo podrían verse si el control se usaba dentro del proyecto (por ejemplo para crear otro control) no siendo asequible al cliente. Event, declara que es un evento, del mismo modo que Sub , Function o Const, definen que son, (los no definidos se toman por variables), luego viene el nombre del evento (el nombre que se nos antoje) TextoCambiado, esto provoca que cuando instanciemos un control de nuestor tipo (CtlBoton) dicho evento pase a verse reflejado en el formulario como 'CtlBoton1_TextoCambiado'. Finalmente los parámetros que deseemos pasar, debe fijars eque al igual que con las funciones puede ser por valor o por referencia, sin embargo no se admiten parámetros opcioneles, ni de tipo ParamArray...

Ahora la 2ª parte, donde lo que procede es usar el evento, en el ejemplo mostrado justo es acompñarlo del código donde se usaría... en la propiedad texto:
Código: Visual Basic
  1.  
  2. Public Property Let Texto(ByVal t As String)
  3.         Dim previo As String                             '<------------------
  4.         If t <> p_Texto Then
  5.             previo = p_Texto                             '<------------------
  6.             p_Texto = t
  7.             Call MedirTexto
  8.             PropertyChanged "Texto"
  9.             Call DibujarTodo
  10.             RaiseEvent TextoCambiado(t, previo)           '<------------------ <-----------------
  11.         End If
  12.     End Property
  13.  
  14.  
Se marcan flechas para el código añadido sobre lo que teníamos antes, debe uno fijarse que dado que pasamos un parámetro 'Textoanterior', necesitamos poder contar con ese valor en el momento de enviar el evento, podríamos haber optado por poner justo el evento al principio, cuando ambas variables se conservaban (se pierde la antigua cuando hacemos p_texto= t), pero diremos que los eventos de tipo 'cambiado' conviene que se ejecuten justo al final del código, es decir cuando además se haya actualizado el mismo, imaginemos que cabría pensar en una situación en la que salta un evento de texto camnbiado, que recibimos y sin embargo nosotros vemos en el gráfico que el texto es el mismo que teníamos antes... Hay que ser consecuente, un evento cuya idea es en tiempo pasado requiere enviarse cuando se haya completado, en cambio un evento en tiempo presente puede y debe enviarse mientras o antes de cambiarse, imaginemos un evento como 'event Textocambiando (byval Textosugerido as string)' tendría un lanzamiento como se señala en el siguiente código:
Código: Visual Basic
  1.  
  2. public event Textocambiando (byval Textosugerido as string)
  3.  
  4.  
  5. Public Property Let Texto(ByVal t As String)
  6.         If t <> p_Texto Then
  7.             RaiseEvent TextoCambiando(t)   '<------------------ <---------------
  8.             p_Texto = t
  9.             Call MedirTexto
  10.             PropertyChanged "Texto"
  11.             Call DibujarTodo
  12.         End If
  13.     End Property
  14.  
  15.  
Aunque francamente un evento en este caso concreto no resulta de utilidad al cliente, ya que solamente le estamos diciendo que vamos a introducir el texto que él mismo (se supone) acaba de introducir. Podríamos ser más explícitos y útiles si en cambio le aportáramos algo como el evento sugerido en el siguiente código:
Código: Visual Basic
  1.  
  2. ' en la sección de declaración de eventos (debajo de la declaración de variables)
  3. Event TextoVacio(ByRef TextoSugerido As String)
  4.  
  5.  
  6. Public Property Let Texto(ByVal t As String)
  7.         Dim TxtSug                                                  '<------------------------
  8.         If t <> p_Texto Then
  9.             If t = "" Then                                              '<------------------------
  10.                 txtSug = UserControl.Extender.Name        '<------------------------
  11.                 RaiseEvent TextoVacio(txtSug)                '<------------------------ <------------------
  12.                 t= txtsug                                                '<------------------------
  13.             End If                                                         '<------------------------
  14.             p_Texto = t
  15.             Call MedirTexto
  16.             PropertyChanged "Texto"
  17.             Call DibujarTodo
  18.         End If
  19.     End Property
  20.  
  21.  
Aquí le estamos diciendo al cliente que ha introducido un texto vacío, y que si lo desea puede dejar como texto el nombre del control, si el cliente utiliza el evento en su código, tiene la ocasión de alterar (obsérvese que el parámetro se pasa por referencia) por un nuevo texto a su conveniencia o bien permitir que el texto sugerido sea el que se utilice. Imaginemos un caso donde se está leyendo código de un archivo y una línea hubiera de asignar el texto al botón, si por casualidad se encontrara una línea en blanco elcliente podría forzar la lectura de la siguiente línea y entregarlo, habiendo utilizado nuestro aviso mediante dicho evento... Naturalmente el cliente puede aún remplazar el texto por una cadena vacía si su intención es que no haya texto. Desde luego en tiempo de ejecución dejar como texto el nombre de un botón no dice mucho más que dejarlo vacío. No se trata de ver la idoneidad o no de dejar el texto vacío para el botón sino, de ver y comprender la utilidad práctica que deban tener los eventos. Para el caso de un botón un evento de textoCambiado no resulta práctico, pero nos ha servido para ilustrar algunos ejemplos y ver la importancia de dónde deben ir.

Ahora analizaremos otra cuestión relativa a los eventos, evitar bucles infinitos. Aunque no lo parezca, un evento mal programado puede acabar enun bucle infinito. Usando la misma propiedad (Texto) y el mismo evento usado hasta ahora (TextoCambiado), veremos como puede producirse un bucle infinito y analizaremos como evitarlo y se darán indicaciones en base a ello...
Tenemos la siguiente firma y código donde se dispara el evento:
Código: Visual Basic
  1.  
  2. ' declaración del evento, nótese el 'byval'
  3. Public Event TextoCambiado(ByVal NuevoTexto As String)
  4.  
  5. '  desatar el evento..
  6. Public Property Let Texto(ByVal t As String)
  7.         If t <> p_Texto Then
  8.             p_Texto = t
  9.             Call MedirTexto
  10.             PropertyChanged "Texto"
  11.             Call DibujarTodo
  12.             RaiseEvent TextoCambiado(t)  '<--------------- <----------------
  13.         End If
  14.     End Property
  15.  
  16.  

Con este código puede crear un bucle infinito si el cliente responde a éste código con algo como lo siguiente:
Código: Visual Basic
  1.  
  2. Private Sub CtlBoton1_TextoCambiado(ByVal NuevoTexto As String)
  3.     Static cambio As Boolean
  4.    
  5.     If cambio = False Then
  6.         cambio = True
  7.         CtlBoton1.Texto = "Lunes"
  8.     Else
  9.         cambio = False
  10.         CtlBoton1.Texto = "Domingo"
  11.     End If
  12. End Sub
  13.  
  14.  
Supongamos que el cliente en ejecución, introdujo un texto al botón: 'Domingo", antes tenía el nombre del control, luego al cambiar  salta el evento, pero ahora en el código de respuesta del evento (en el formulario del cliente) yo le asigno el valor 'Lunes', luego nuevamente la propiedad se asigna y como el valor cambia de nuevo, produce un evento, ahora mi variable 'cambio' es true, luego ahora le asigno el texto 'Domingo', la propiedad ejecuta su código para actualizarse, con lo que al volver a cambiar, nuevamente me produce un evento textocambiado, ahora la variable Cambio vale  false, por lo que nuevamente le asigno a la propiedad Texto el valor 'Lunes'... el ciclo comienza de nuevo y no hay ninguna salida... estamos en un bucle infinito. Desde luego puede alguien pensar que el código del cliente está muy rebuscado, al respecto debe decirse que el cliente no tiene porqué saber que puede producirse un bucle infinito, después de todo él sólo está asignando un valor a una propiedad, también puede decirse que este caso puede darse usando un código en la parte del cliente menos clara que el código expuesto aquí y que le resultaría más difícil al cliente detectar la falta de respuesta (como si se volviera loco y no atendiera).

Hay varias formas de evitar los bucles infinitos para los eventos. La primera medida sería, como ya conocemos colocar un portero. Así mientras se esté ejecutando el código del evento, no se aceptarían  alteraciones de la propiedad... este tipo de bloqueos suele darse en las llamadas 'transacciones' una vez elegido unos valores no pueden ser modificados (en este caso de las transacciones para evitar manipulaciones y problemas de seguridad). Veamos como sería nuestro código añadiendo un portero:
Código: Visual Basic
  1.  
  2. ' declaración del evento, nótese el 'byval'
  3. Public Event TextoCambiado(ByVal NuevoTexto As String)
  4.  
  5. '  desatar el evento..
  6. Public Property Let Texto(ByVal t As String)
  7.         Static Portero As Boolean           '<-----------
  8.         If Portero = False Then             '<-----------
  9.             Portero = True                  '<-----------
  10.             If t <> p_Texto Then
  11.                 p_Texto = t
  12.                 Call MedirTexto
  13.                 PropertyChanged "Texto"
  14.                 Call DibujarTodo
  15.                 RaiseEvent TextoCambiado(t)  '<--------------- <----------------
  16.             End If
  17.             Portero = False                  '<-----------
  18.         Else                                 '<-----------
  19.             Beep                             '<-----------
  20.         End If                               '<-----------
  21.     End Property
  22.  
  23.  

Es una solución, pero entonces vemos que si el cliente decide cuando responde al evento cambiar la propiedad, el cambio no se opera, esto no es malo en si, lo malo es que el cliente no advierta que no se ha producido ningún cambio. Puede uno pensar que si el texto acaba de ser cambiado, el cliente debe saberlo y qué sentido tendría que volviera a tener que cambiarlo justo con el evento del cambio que acaba de producirse... bueno, aunque a alguien no se le ocurra una respuesta, lo cierto es que hay muchas, ya dijimos 1, al decir que el primer cambio, podría provenir por ejemplo de la lectura de un fichero que estaba tomando entradas y aparecía una línea en blanco donde se esperaba encontrar un texto para ponerlo en el botón, por tanto el cliente hace bien si detecta que el texto está en blanco en poner por ejemplo un texto por defecto (ya se habló que a nosotros no nos interesa forzar a que por narices el texto deba contener una cadena, una cadena vacía es perfectamente tolerable en un botón, de hecho a veces sóloes un estado transitorio hasta que el cliente le ponga otro). Otras razones por las que el cliente puede querer cambiar el texto que aún  acaba de ser cambiado, es por ejemplo para verificar que cumple ciertas reglas y si no las cumple debe cambiarse, evidentemente el mejor sitio para verificar dichas reglas es precisamente la oportunidad que nos brinda el evento TextoCambiado. (imaginad que en vez de un botón, que suele tener un texto más perenne, fuera un control de texto  (Textbox) donde a menudo verificamos si se trata de sólo números o sólo letras, etc...).

Entonces la solución de añadir un portero, vemos que es una solución a medias, sin embargo podemos darle un pequeño hervor más, por ejemplo si en el evento el parámetro que se le proporciona al cliente se le pasa por referencia en vez de por valor, puede en ese punto reasignar el nuevo valor para el texto y el portero por tanto no nos impide hacerlo, veamos el código, y obsérvese como hemos cambiado la ubicación del RaiseEvent, debe quedar claro el porqué..:
Código: Visual Basic
  1.  
  2. ' declaración del evento, nótese ahora el 'byRef'
  3. Public Event TextoCambiado(Byref NuevoTexto As String)
  4.  
  5. '  desatar el evento..
  6. Public Property Let Texto(ByVal t As String)
  7.         Static Portero As Boolean           '<-----------
  8.         If Portero = False Then             '<-----------
  9.             Portero = True                  '<-----------
  10.             If t <> p_Texto Then
  11.                 p_Texto = t                       '<------------------------
  12.                 Call MedirTexto               '<-----------------------
  13.                 Call DibujarTodo              '<------------------------
  14.                 RaiseEvent TextoCambiado(t)  '<--------------- <----------------  
  15.                 p_Texto = t
  16.                 Call MedirTexto
  17.                 PropertyChanged "Texto"
  18.                 Call DibujarTodo
  19.             End If
  20.             Portero = False                  '<-----------
  21.         Else                                 '<-----------
  22.             Beep                             '<-----------
  23.         End If                               '<-----------
  24.     End Property
  25.  
  26.  

Debe verse como tal como dijimos (puesto que es un control visual), antes que nada hacemos los cambios visuales oportunos,para reflejarlo (en este caos no es estrictamente necesario, ya que la velocidad de ejecución del código, hace que sea innecesario), pero eso si vemos como el disparo del evento lo hemos subido arriba, para que si el cliente cambia el parámetro (que el ve como...) NuevoTexto, dicho valor sea el que definitivamente sea aceptado.

En este caso vemos que ahora el cambio (tamto si se produjo por parte del cliente o no cuando se le envió el evento), no provoca evento, aunque vemos que hemos podido hacer un nuevo cambio a pesar del portero. Esta solución es perfectamente aceptable en la mayoría de los casos, podríamos darle todavía una 'vuelta de tuerca' a nuestro código para permitir que se aceptaran x cambios sucesivos, es decir un bucle que parecería infinito, pero que está controlado... veamos el siguiente código:
Código: Visual Basic
  1.  
  2. Public Event TextoCambiado(ByVal NuevoTexto As String)
  3.  
  4. Public Property Let Texto(ByVal t As String)
  5.         Static x As Byte                                  '<-----------
  6.         Static Portero As Boolean        
  7.         If Portero = False Or x < 12 Then             '<-----------
  8.             Portero = True                
  9.             x = x + 1                                     '<-----------
  10.             If t <> p_Texto Then
  11.                 p_Texto = t
  12.                 Call MedirTexto
  13.                 PropertyChanged "Texto"
  14.                 Call DibujarTodo
  15.                 RaiseEvent TextoCambiado(t)  '<--------------- <----------------
  16.             End If
  17.             Portero = False                  
  18.             x = 0                              '<-----------
  19.         Else                                
  20.             Beep                            
  21.         End If                            
  22.     End Property
  23.  
  24.  

Fijarse primero que hemos vuelto a la versión de byval para el parámetros del evento...
Al observar el código, veremos que hemos añadido un 2º portero, pero éste a diferencia del anterior no controla si ýa se entró, si no cuantas veces se entró, en este caso, en conjunto con el otro portero queda una regla claramente definida, se entra mientras portero= false o mientras x< 12, la única combinación por tanto que excluye la entrada es portero=true y X=12, esto proporciona hasta 12 cambios consecutivos 'en bucle infinito', sin salir por abajo (es decir que cada cambio se produce dentro de la respuesta que el cliente da al evento). Este mecanismo es más potente y versátil que usando sólo el primer portero. Cada uno deberá analizar cuando es necesario una solución y cuando conviene aplicar la otra.
Imaginad una aplicación destinada a evaluar respuestas de examen, lógicamente si un alumno cambia muchas veces una misma respuesta claramente no conoce la respuesta, por tanto proporcionar controles con un número máximo de cambios posibles antes de que el profesor resetee ese valor es una opción perfectamente válida  (Suponemos que lo que dura el examen y no sujeto a la duración de la aplicación, imaginad que alcanzo el límite impuesto y no pudiendo cambiar más, fuerzo a que se cierre la aplicación y con ello deba volver a empezar el examen, si sabemos que ese valor no cambiará en esa circunstancia nadie cerrará la aplicación por tal efecto inexistente).
Naturalmente en este caso propuesto, sobraría el portero 1º, se permiten cambios pero en número límitado, el portero permite que desde el evento, se cambie nuevamente el valor, pero hasta un número máximo de veces. Veamos un código que se encarga de hacer esto (entiéndase a la vez como una propiedad de escrituras limitadas) , véase que en dicho caso la importancia radica en la propiedad tanto como en el evento, ya que éste debe pasar por valor y no por referencia el parámetro para forzar que una nueva asignación 'entre' en la propiedad por asignación y por tanto aumente el portero contador de cambios:
Código: Visual Basic
  1.  
  2. Private ResetLimite  As Boolean
  3.  
  4. Public Event TextoCambiado(ByVal NuevoTexto As String)
  5.  
  6. Public Property Let Texto(ByVal t As String)
  7.         Static x As Byte
  8.         If ResetLimite = True Then x = 0
  9.         If x < 12 Then
  10.             x = x + 1
  11.             If t <> p_Texto Then
  12.                 p_Texto = t
  13.                 Call MedirTexto
  14.                 PropertyChanged "Texto"
  15.                 Call DibujarTodo
  16.                 RaiseEvent TextoCambiado(t)  '<--------------- <----------------
  17.             End If
  18.         Else
  19.             Beep
  20.             MsgBox "Se alcanzó el límite máximo de 12 cambios para el valor. No se permiten más cambios hasta ser 'reseteado'."
  21.         End If
  22.     End Property
  23.  
  24.  

Este código, como se podrá analizar permite un máximo de 12 cambios a la propiedad, incluso aunque estos cambios se hagan desde el disparo del evento, luego mientras no se encuentre que la variable 'ResetLimite' tenga el valor true, no dejará que se vuelva a poder hacer cambios en la propiedad. No analizamos que pasa detrás de la variable resetLimite, ya que éste no es el objetivo, sino ilustrar como un evento puede cambiar la 'vida' de una propiedad y las restricciones necesarias para controlarlo. si el disparo del evento proporcionara el parámetro por referencia habría el más posibilidades de cambio ya que en dicho caso, el cambio se haría por 'invitación' y no por la 'puerta principal'.

Sin embargo en determinadas situaciones donde absolutamente cada cambio deba ser verificado, no es aceptable, ninguna de las soluciones aportadas (esto con reservas comose indicará más adelante y sobre todo la verdadera razón) , así vamos por fin a proveer una solución que resuelve algunos de estos conflictos; cambiar una propiedad tantas veces se necesite desde donde se necesite sin quedarse colgados.
En realidad el problema real, la verdadera razón, no es el bucle infinito (aunque como tal el problema existe), si no el tamaño de la pila, puesto que cada cambio se realiza desde el propio evento, se entra de nuevo en la propiedad sin haber salido previamente, lo que supone que la dirección de retorno, del evento debe guardarse en la pila, también al entrar en la propiedad nuevamente debe guardarse la dirección de retorno para cuando salga de la propiedad, esto agota el tamaño de la pila, por el cúmulo de reentradas a la propiedad. De hecho si ejecutamos el código que pusimos en el cliente, en mi caso la pila se desborda después de 250 iteraciones.
Código: Visual Basic
  1.  
  2. Private Sub CtlBoton1_TextoCambiado(ByVal NuevoTexto As String)
  3.     Static cambio As Boolean
  4.     Static x As Long
  5.    
  6.     x = x + 1
  7.     If cambio = False Then
  8.         cambio = True
  9.         CtlBoton1.Texto = "Lunes"
  10.     Else
  11.         cambio = False
  12.         CtlBoton1.Texto = "Domingo"
  13.     End If
  14. End Sub
  15.  
  16.  
Cuando Vb marque el error 28  - espacio de pila insuficiente, pulsad en el botón 'Depurar' de la ventana de aviso, y consultad el valor de X. como vemos ni siquiera podemos caer en un bucle infinito, porque un error es un impedimento para ello...
Después de todo uno debe tener claro cuando y como se produce un bucle infinito (como el código recién expuesto), por tanto adoptaremos una solución a cambio de una complejidad añadida de evitar que la pila se colapse.... siguen vigentes por tanto las soluciones indicadas con anterioridad a din de evitar en lo posible bucles infinitos, usando porteros y por tanto todo lo dicho con anterioridad es aunque no lo pareciera después de ver sus 'defectos', perfectamente válido. Nos centramos pués en evitar el agotamiento de la pila que como se ve, preocupa más que el bucle infinito que se supone el cliente debe saber evitarlo (el código anterior es obvio que genera un bucle infinito).

La solución pasa por devolver los eventos de forma asíncrona. Para poder devolver los eventos asíncronamente, necestiamos añadir a nuestro control un timer, llamémosle timEventos, y aunque sólo vamos a programar de forma asíncrona este evento de TextoCambiado, sin embargo se muestra toda la estructura de código necesaria como si empleáramos muchos más eventos de esta forma, así no quedará ninguna duda. La primera duda que puede sugir es cómo hago que unos valores privados en una rutina puedan ser pasados a otra rutina y más cuando aquella no lo va a emplear de forma síncrona, la solución pasa por empaquetar los datos que necesitan nuestro eventos... Supóngase que empleamos varios eventos de forma asíncrona en nuestro control, y supóngase que el evento más 'largo'  ebn cuanto a parámetros utiliza 4, pués entonces parece obvio que tenemos que 'transportar' cada evento  en un mismo 'camión' en el que quepa cualquier evento, por tanto construimos un camión (paquete de datos) que tenga 4 parámetros... es decir algo tan simple como una estructura. Como se verá más adelante, una estructura no nos sirve, Vb nos marcará un error, pero esto lo se yo, lo dejamos de momento porque el principiante se toparía con ello y lo mejor es seguir el camino lógico que tomaría. cuando llegue el momento se explicará como solucionarlo.

Qué más requisitos puede necesitar nuestro 'camión', si trasporta carne, necesitaría un congelador, si transporta líquido, necesitaría ser estanco, si transporta algo peligroso debería estar encerrado, podemos optar por 2 soluciones usar un 'camión' para cada caso o usar un 'camión' único para cualquier 'paquete-carga' (evento), por tanto si optamos por este último, todos nuestros 4 parámetros deberán ser algo que pueda portar cualquier cosa, es decir de tipo variant. Y puesto que hemos decidido usar el mismo camión para todos los eventos asíncronos, necesitaremos 1 identificador de evento y un 'almacén' donde esperan los paquetes a que el camión haga un 'reparto' y venga por el siguiente....  Como se podrá observar se usa un símil y se seguirá usando, para explicar el método, ya que en este punto nuestro sistema es equivalente al de una empresa que se dedica a la paquetería (recogida y entrega de paquetes), salvando las diferencias. Usando el símil se espera que el método quede 'iluminado' por la lógica y no se vea como una solución de la que no se sabe de dónde viene o el por qué de una cosa u otra.

Iremos expresando estas ideas con código, primero el diseño de la 'caja' de nuestro camión de reparto:
Código: Visual Basic
  1.  
  2. ' el código de la estructura lo colocamos debajo de todas las enumeraciones...
  3. Private Type LanzaderaEventos
  4.     Identificador As Byte
  5.     Param1 As Variant
  6.     Param2 As Variant
  7.     Param3 As Variant
  8.     Param4 As Variant
  9. End Type
  10.  
  11.  

Como dijimos 4 parámetros, de tipo que 'acepten cualquier cosa', pescado, escombros, agua, alcohol... y un identificador que más adelante definiremos mejor... ahora necesitamos el camión de reparto en si:
Código: Visual Basic
  1.  
  2. Private s_ReparteEventos As LanzaderaEventos  ' una variable privada de la estructura es donde se alojarán los datos, nuestro camión de reparto.
  3.  
  4.  

Hemos dicho que tenemos un identificador de eventos, porque usamos 1 único camión para 'transportar' todas las 'cargas', y por tanto el que la recibe debe saberlo... mejor que dejar el identificador en un byte lo dejamos en una enumeración (las enumeraciones siempre son de tipo long ), ya sabemos que al compilar el código desaparece y en sustituído por las cosntantes que representan, pero para diseñar son muy cómodas sobre todo si sele dan nombres claros y muy explícitos:
Código: Visual Basic
  1.  
  2. Private Enum IdentificadoresDeEvento
  3.     IDENT_EVENTO_TEXTO_CAMBIADO = 1
  4.     '....
  5.     '.....
  6.     IDENT_EVENTO_ICONO_CAMBIADO = 8
  7. End Enum
  8.  
  9.  

Ya se dijo que para el control no vamos a hacer eventos asíncronos, la enumeración se expone sólo como ejemplo... no vamos a tener ningún evento Iconocambiado... Por tanto hacemos el cambio oportuno en la declaración del parámetro identificador en la estructura para que sea del tipo:
Código: Visual Basic
  1.  
  2. Private Type LanzaderaEventos
  3.     Identificador As IdentificadoresDeEvento     '<------------- cambiado a tipo long, con la enumeración creada al efecto.
  4.  '..............
  5.  
  6.  

Ahora necesitamos un 'almacén', como dijomos,  ya que como es de esperar podría darse el caso de crearse varios paquetes de entrega y no sabríamos cual entregar primero o cual después... la variable 'camión' ReparteEventos, en vez de 1 sólo (camión) podría ser una 'flotilla' de ellos', es decir una matriz, ahora bien nos tendríamos que ocupar de asignar las tareas a cada camión de la flotilla, verificar si uno está libre para otorgarle una nueva carga etc... y nos resultaría muy engorroso (aunque desde luego el código sería muy interesante para el aprendizaje), no obstante si lo analizamos bien, nunca se van a producir 2 envíos simultáneos, porque el código del programa es lineal, por tanto realmente 1 sólo camión (para el reparto) puede realizar todas las entregas, además tenemos un medio de organizar todos los 'paquetes' a enviar y toda la logística recáe en el 'almacén'  y en el 'camión' si se ponen de acuerdo, para el almacén por tanto usaremos un objeto collection...:
Código: Visual Basic
  1.  
  2. Private s_AlmacenEv As New Collection   ' podeis darle un nombre más largo para familiarizaros como s_AlmacenEventos.
  3.  
  4.  

Por tanto ya sólo nos falta, la logística de interrelación entre el que crea paquetes (disparo de eventos), el receptor de datos, el almacén, y el camión de reparto. En la práctica además de la variable 's_ReparteEventos', crearemos otra, ya que tendremos 2 camiones uno de recogida y otro de entrega, para evitar que mientras uno entrega al mismo tiempo se dé el caso de que se desee enviar y es´te ocupado, tened en cuenta que la recogida en lineal, pero la entrega es asíncrona (respecto de la recogida), luego efectivamente podrían coincidir que a la vez, un camión esté entregando mientras por otro lado se esté cargando su caja... Adiferencia de la empresa de paquetería, nuestros 'camiones' pueden estar al mismo tiempo entregando en el destinatario y recogiendo del remitente, por tanto para evitar esta circunsancia en efecto 'contratamos' 2 camiones, cada uno especializado en una tarea.

Se muestra ahora, finalmente, la recogida del paquete y su guarda en el almacén donde quedará a la espera:
Código: Visual Basic
  1.  
  2. Dim s_RecogeEventos As LanzaderaEventos
  3.  
  4. Public Property Let Texto(ByVal t As String)
  5.         If t <> p_Texto Then
  6.             p_Texto = t
  7.             Call MedirTexto
  8.             PropertyChanged "Texto"
  9.             Call DibujarTodo
  10.            ' el código nuevo
  11.             With s_RecogeEventos
  12.                 .Identificador = IDENT_EVENTO_TEXTO_CAMBIADO
  13.                 .Param1 = t
  14.             End With
  15.             Call s_AlmacenEv.Add(s_RecogeEventos)
  16.             '  esta línea desaparece de aquí: RaiseEvent TextoCambiado(t)  '<--------------- <----------------
  17.         End If
  18.     End Property
  19.  
  20.  
Como se puede ver, hemos llamado al recogedor con una variable aclaratoria, ya dentro de la propiedad texto, vemos que primero ponemos el 'remitente' (identificador) y empaquetamos los datos, dentro del 'camión', finalmente lo llevamos y depositamos en el 'almaccén' (s_AlmacenEv.Add), y como vemos aquí no se dipara ningún evento.

Como se recordará hablamos de añadir un timer al control que llamamos Timeventos, éste será el repartidor de los paquetes (eventos). El timer debería estar desactivado en diseño, y lo activaríamos justo cuando por fin esté en modo de ejecución el control, el punto adecuado para activar el timer, es justo al final del evento readproperties. :
Código: Visual Basic
  1.  
  2.     s_Cargando = True
  3.     TimEventos.Interval = 20 '<----------------------------------- líneas añadidas
  4.     TimEventos.Enabled = Ambient.UserMode           '<---------------------------
  5.  
  6.  
 
Hay que fijarse, el timer se activa en igualdad al modo de ejecución si estamos en diseño (usermode=False), el timer debe estar desactivado, sino veríamos parpadear el título de la ventana de Vb contínumanete, por efecto del timer...

También debe notarse que si el control se desactiva, no lo hace el timer, por lo que podría provocarse un redibujado posterior a deshabilitar el control, lo que como ya sabemos no dibuja el estado deshabilitado. La solución pasa por añadir una línea de control en la propiedad Activo:
Código: Visual Basic
  1.  
  2. Public Property Let Activo(ByVal a As Boolean)
  3.         If a <> UserControl.Enabled Then
  4.             UserControl.Enabled = a
  5.             TimEventos.Enabled = a And Ambient.UserMode  '<----------------------
  6.             Call DibujarActivo
  7.             PropertyChanged "Activo"
  8.         End If
  9.     End Property
  10.  
  11.  
Vemos que el timer está supeditado tanto al estado enabled del control como al modo de ejecución. Esto  hace que en tiempo de diseño siempre esté desactivado el timer sin importar el estado del control y en cambio en tiempo de ejecución irá a la par que esté el estado del control.

Para terminar de comprender el método asíncrono, nos falta entregar el 'paquete'. Ahora el código y luego la explicación y comentarios:
Código: Visual Basic
  1.  
  2. Private Sub TimEventos_Timer()
  3.     If s_AlmacenEv.Count > 0 Then
  4.         s_ReparteEventos = s_AlmacenEv.Item(1)
  5.         With s_ReparteEventos
  6.             Select Case .Identificador
  7.                 Case IdentificadoresDeEvento.IDENT_EVENTO_TEXTO_CAMBIADO ' 1
  8.                     RaiseEvent TextoCambiado(.Param1)
  9.                 ' case....
  10.                 Case IdentificadoresDeEvento.IDENT_EVENTO_ICONO_CAMBIADO ' 8
  11.             End Select
  12.         End With
  13.         Call s_AlmacenEv.Remove(1)
  14.     End If
  15. End Sub
  16.  
  17.  
Analizando el código:
1º El repartidor comprueba si hay paquetes en el 'almacén' pendientes de enviar: If s_AlmacenEv.Count > 0 Then
2º Si lo hay llenamos el camión de reparto:  (s_ReparteEventos = s_AlmacenEv.Item(1)) con la carga primera que se encuentra.
3º Luego analiza el remitente, (select case .Identificador )
4º Ya que según éste (el identificador), el destinatario será uno u otro: (Case 1 : RaiseEvent TextoCambiado ) hace el efecto del destinatario...
5º Al final depositamos su carga(los parámetros): (Case 1 : RaiseEvent TextoCambiado ( .Param1)   . este evento sólo tenía 1 parámetro que cargamos en Param1.

Si ahora ejecutamos el código, tal como se señalaba más arriba, falla, VB nos canta un error, quedamos en que dejaríamos que apareciera el error, porque el camino lógico de un principante es caer en dicho error, no sería ningún favor si ya de entrada yo marco una solución, un principante (nadie, ninguno) no conoce la solución a un problema que aún se supone que no conoce (no le ha ocurrido). El error que Vb nos arroja dice algo como: 'Sólo los tipos definidos por el usuario públicos de módulos de objeto públicos se pueden usar como parámetros'.
Con este tocho de mensaje lo que Vb nos trata de decir es que no tiene implementada una solución(otra solución que marcar error) para una asignación de datos con las siquientes características: Una estrcutura declarada privada y que está en un objeto declarado público (el usercontrol tiene que ser público para que puedan ser creadas instancias por parte del cliente) no puede ser asignado a determinados tipos. La idea que trata versa sobre el problema para el destinatario, que no sabrá como tratar esa información, en cambio cuando el objeto (clase, usercontrol) es privado, el cliente no tendrá que vérselas con esa información 'cifrada'. Si se piensa bien, es muy razonable, se explica por qué:
ya que una estructura guarda sus bytes de modo consecutivo (como una matriz), puede alojarse cualquier cosa sobre ella, sin embargo si se pasa a un cliente, no sabrá como tratar dichos datos, porque la estructura se declaró privada (no tiene acceso a la información referida a ella), obviamente al ser privada el cliente no sabría si existe identificador, ni param1, etc... ni su orden, ni los tipos que la componen  sólo vería un conglomerado de bytes, por lo que no sabría que hacer con ello. Y habría constantes errores y preguntas acerca de si alguien sabe algo acerca de tal o cual cuestión relativa, a tal o cual objeto... vamos que sería un caos.

Todavía uno podría alegar, vale, pero es que yo no lo estoy pasando al cliente, los datos están declarados privados y se usan de modo privado. La respuesta a esta cuestión es la siguiente: si la estructura (aunque sea privada) se pasa a un variant, ¿ qué impide a un variant pasarlo como parámetro a una función pública al cliente ?... nada lo impide, luego el modo de controlar que no lleguen datos incomprensibles al cliente es impedir que las estructuras privadas vuelquen sus datos a procedmientos públicas o (la otra opción)a variables que puedan usarse como parámetros en procedimientos públicos, que en este caso son los variants y object.

Si declaramos la estructura como pública, (hagámoslo para probar) vereis que el código es totalmente ejecutable y sin error(nota también debe declarase como pública la enumeración que usa el campo Identificador, las razones son partes delas mismas ya explicadas.

Cambiad la declaración de la estructura de privada a pública y de la enumeración afectada, ahora ejecutad el código  y vereis que no hay ningún error. Pero claro naturalmente nosotros queremos aislar esos datos, el cliente no tiene que usarlos, y si no son de su provecho, tampoco deben servirle de distracción. Nada le impide declarar una variable con esa estructura, pero el no podrá usarla en ninguna parte con el control, por tanto adoptaremos ahora la solución que cabe aplicar al caso.

Si  no queda más remedio  que los datos, deban ser público, al menos le impediremos crear instancias de ellos.
Vamos al menú de Vb, proyecto: elegimos agregar 'Modulo de clase', renombremos a la clase con el nombre que le dimos a la estructura y luego, cortamos (CTRL+X) el código de la estructura y la pegamos en la clase (dejando sólo los parámetros, pero declarándolos públicos), también cortamos la enumeración (que señala identificador) y la pasamos a la clase y la declaramos también pública:
Código: Visual Basic
  1.  
  2. ' dentro de módulo de código de la clase
  3.  
  4. Public Enum IdentificadoresDeEvento
  5.     IDENT_EVENTO_TEXTO_CAMBIADO = 1
  6.     '....
  7.    
  8.     IDENT_EVENTO_ICONO_CAMBIADO = 8
  9. End Enum
  10.  
  11. Public Identificador As IdentificadoresDeEvento
  12. Public Param1 As Variant
  13. Public Param2 As Variant
  14. Public Param3 As Variant
  15. Public Param4 As Variant
  16.  
  17.  

Ahora vamos a la ventana de propiedades de la clase, su nombre le cambiamos desde 'class1' al nombre que le dimos a la estructura (si os parece muy largo podeis abreviarlo, eso si añadid una línea comentada donde es explica el nombre de la clase y el propósito de la misma (proveer datos para portar los parámetros de los eventos que se lancen asincronamente), luego buscamos la propiedad instancing y cambiamos su valor a: 2 (publicNotCreatable), esto implica que el cliente verá la clase, pero no podrá crear instancias, a lo sumo usarla y sólo si nosotros le proporcionamos algún objeto público que la utilice (que no es el caso).

El siguiente paso es cambiar la declaración de las variables de 'entrega' y 'reparto':
Código: Visual Basic
  1.  
  2. Private s_RecogeEventos As New LanzaderaEventos     ' si usamos el mismo nombre para la clase que orginalmente tenía la estructura, no olvidemos en cualquier caso el NEW
  3. Private s_ReparteEventos As New LanzaderaEventos    ' ojo NEW...
  4. Private s_AlmacenEv As New Collection
  5.  
  6.  

Y ahora los cambios aplicados en los procedimeitnos recogida y reparto) con los cambios pertinentes ya corregidos, si quereis compararlos con el código anterior, comentad aquel y pegad encima o debajo el nuevo código:

Código: Visual Basic
  1.  
  2. ' el código de entrega casi no cambia, sólo se añade una línea... luego se explica porqué
  3. Public Property Let Texto(ByVal t As String)
  4.         If t <> p_Texto Then
  5.             p_Texto = t
  6.             Call MedirTexto
  7.             PropertyChanged "Texto"
  8.             Call DibujarTodo
  9.             With s_RecogeEventos
  10.                 .Identificador = IDENT_EVENTO_TEXTO_CAMBIADO
  11.                 .Param1 = t
  12.             End With
  13.             Call s_AlmacenEv.Add(s_RecogeEventos)
  14.             Set s_RecogeEventos = Nothing                     ' <----------------
  15.             ' esta línea sigue retiradaRaiseEvent TextoCambiado(t)  '<---------------
  16.         End If
  17.     End Property
  18.  
  19. ' el código de reparto:
  20. Private Sub TimEventos_Timer()
  21.     If s_AlmacenEv.Count > 0 Then
  22.         Set s_ReparteEventos = s_AlmacenEv.Item(1)
  23.         With s_ReparteEventos
  24.             Select Case .Identificador
  25.                 Case IdentificadoresDeEvento.IDENT_EVENTO_TEXTO_CAMBIADO ' 1
  26.                     RaiseEvent TextoCambiado(.Param1)
  27.                 ' case....
  28.                      ' .....................
  29.                 Case IdentificadoresDeEvento.IDENT_EVENTO_ICONO_CAMBIADO ' 8
  30.                     ' .....................
  31.             End Select
  32.         End With
  33.         Set s_ReparteEventos = Nothing   '<------------------------------
  34.         Call s_AlmacenEv.Remove(1)
  35.     End If
  36. End Sub
  37.  
  38.  
Como se puede apreciar el código no ha cambiado mucho, entre usar una estructura y usar una clase. La razón por la que se destruyen los 'camiones' después de recoger o entregar la 'carga' es porque si no lo hiciéramos podría una de sus variables componentes mantener un valor para el próximo 'encargo' falsificando con ello el nuevo servicio. Naturalmente cada evento 'sella' sus campos, pero siempre hay lugar para errores. Si destruímos los objetos tras utilizarlos (vaciamos la caja del camión en cada servicio) , nos evitamos ese problema. Puede además verse, que la clase es destruída pero qque luego no es vuelta a construir, cómo es eso...?. bien, esto no es un detalle del usercontrol sino de VB, pero bueno... cuando en VB declaramos una clase como 'as NEW', a nivel general (formulario, usercontrol, class...) siempre existirá una instancia del objeto, luego cuando hacemos un set objeto=nothing, el objeto es destruído e inmediatamente es como si declarámos de nuevo la misma variable como 'As NEW' con lo que cuando se invoque algunos de sus procedimeintos, sucede un evento initialize para el objeto. Existen por tanto toda la vida que tiene el formulario, usercontrol, clase, etc...

Ya hemos resuelto los problemas que nos originaba la estructura. Ahora podemos probar de nuevo a ejecutar ese código que pusimos en el ejemplo del cliente, veremos que (salvo que olvidáramos comentar la línea Raiseevent en la propiedad texto) ya no sigue saltando el desbordamiento de pila.. es más uno verá como el control se dibuja a una velocidad endiablada y ve uno fugazmente alternarse 'Lunes' con 'Domingo'. Debe recordarse que lo que señalamos bastante más atrás acerca de los porteros para controlar los bucles infinitos podrían seguir manteniéndose ahora, yo lo he ido retirando del código para ceñirnos al caso que explicaba en cada momento sin distraernos en otros detalles...

Respecto de los eventos finalizaremos por hoy, por comentar que si los lanzamos sincronizados, tal como nos vienen los lanzamos, y con el objeto collection en el modo asíncrono, también como nos vienen en el mismo orden los entregamos. Si fuera necesario podríamos ofrecer prioridad a determinados eventos sobre otros, usando el objeto collection, simplemente tendríamos que indicarle cuando le añadimos, que posición ocupará, cuando no indicamos nada se añade al final... imaginemos que nuestro evento TextoCambiado debiera tener una cierta prioridad sobre todo lo demás, en ese caso, en vez de ponerlo a la cola, lo pondríamos el primero, o por ejemplo el 3º si hay más de 10 eventos pendientes en el 'almacén'.:
Código: Visual Basic
  1.  
  2.     If s_AlmacenEv.Count > 10 Then
  3.          Call s_AlmacenEv.Add(s_RecogeEventos, after:=4)
  4.     Else
  5.          Call s_AlmacenEv.Add(s_RecogeEventos)
  6.     End If
  7.  
  8.  

También podríamos crear nuestro propio almacén de datos e imponerle algún tipo de manejor de prioridades, pero esto se sale del propósito de esta guía que es el usercontrol.

Hay que decir que para nuestro control, el evento TextoCambiado carece de todo interés y ha servido de excusa únicamente para aprender acerca de los eventos, podemos eliminar todo el código relativo al mismo, o bien si lo preferís simplemente comentarlo, o bien mantener 2 proyectos. En uno, con las pruebas que en cada parte se sugieren y en otro el código 'puro y limpio' que definitivamente será nuestro control Boton.


Por hoy ya sale un mensaje demasiado largo, continuaremos en otra parte comentando acerca de los eventos.
«Ma non troppo»
----> ModoVacaciones = False<----

Nebire

  • Miembro HIPER activo
  • ****
  • Mensajes: 670
    • Ver Perfil
Re: Guía: Crear controles de usuario en VB (Controles ActiveX)
« Respuesta #9 en: Jueves 30 de Septiembre de 2010, 21:48 »
0
Bueno, como al final uno siempre se acaba llenando de trabajo, el tiempo libre acaba por escasear... Asi, que acabaremos de completar el control en unas partes más breves, y como bastantes cosas aún se quedarán en el tintero, se ealizará más adelante otro control (un control tipo Listbox) donde se aprovechará para explicar todos esos detallitos que aquí no han acabado por salir, como las páginas de propiedades, etc...

Para este control, por tanto nos aplicaremos en completar la sección de eventos y en hacer 1 añadido que señalamos al principio del control y darle ''foco' al control que quedamos como pendientes y con todo ello  daremos por finalizado el control...

En esta nueva y breve parte terminamos de aplicar los eventos. En la parte anterior explicamos como crear nuestros propios eventos y los criterios que se deben seguir para colocar acertadamente los eventos y solucionar posibles conflictos, por tanto todavía nos queda explicar esos eventos que permiten al cliente darle la habitual utilidad al control, es decir los eventos del ratón, y teclado.

Como podreis imaginar a estas alturas estos eventos los provee el propio usercontrol, por lo que todo lo que necesitamos hacer es incluir los que necesitemos en el código (de entre los que están disponibles) y adicionalmente si fuere el caso añadir nuestro propio código. Por ejemplo podríamos proveer un mecanismo de Triple_Click. No obstante como se ha indicado al inicio de esta parte queremos dar por terminado el control sin más demora, por lo que simplemente delegaremos en los eventos sin apenas código añadido por nuestra parte.

Por la misma razón las observaciones que puedan indicarse más bien sobran ya que debería ser de todos conocidos al haber dado respuesta en vuestros proyectos a los mismos eventos que ahora se indicarán.

Ahora pués pondremos eventos a los estados del ratón... fiel al español, decoramos los eventos con nombres que entendemos su significado pero a los que no estamos acostumbrados (como padre de la criatura tienes pleno derecho a ponerle los nombres como mejor te parezca):

Código: Visual Basic
  1.  
  2. ' debajo de la sección de declaración de las variables y antes del código de las propiedades, ponemos las declaraciones de eventos que vayamos añadiendo
  3. Public Event RatonPulsado(Button As Integer, Shift As Integer, X As Single, Y As Single)
  4. Public Event RatonSoltado(ByVal Boton As Byte, ByVal CombinacionTeclas As Integer, ByVal X As Single, ByVal Y As Single)
  5.  
  6.  
En el primero hemos respetado la firma tal cual lo muestra Vb, en la 2º hemos alterado los nombres de los parámetros y el método de pasar los parámetros... Estos evento nosotros ya los tenemos 'medio' programados, luego sólo nos resta añadir el código para lanzar el evento al cliente.
Código: Visual Basic
  1.  
  2. Private Sub UserControl_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)
  3.     s_Relieve = True
  4.     Call DibujarRelieve
  5.     RaiseEvent RatonPulsado(Button, Shift, X, Y)          '<-------------------------- línea añadida
  6. End Sub
  7.  
  8. Private Sub UserControl_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single)
  9.     s_Relieve = False
  10.     Call DibujarRelieve
  11.     RaiseEvent RatonSoltado(Button, Shift, X, Y)                  '<-------------------------- línea añadida
  12. End Sub
  13.  
  14.  

Para el diseño de nuestro control, además de estos eventos añadiremos los eventos de Mousemove, KeyDown, KeyUp y Keypress, podeis darle la misma firma que trae VB o alterarlas a vuestra necesidad...
Código: Visual Basic
  1.  
  2. ' junto a la declaración de eventos
  3. Public Event RatonMovido(Button As Integer, Shift As Integer, X As Single, Y As Single)
  4. Public Event TeclaPulsada(KeyCode As Integer, Shift As Integer)
  5. Public Event TeclaSoltada(KeyCode As Integer, Shift As Integer)
  6. Public Event TeclaPresionada(KeyAscii As Integer)
  7.  
  8.  
  9. Private Sub UserControl_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single)
  10.     RaiseEvent RatonMovido(Button, Shift, X, Y)
  11. End Sub
  12. Private Sub UserControl_KeyDown(KeyCode As Integer, Shift As Integer)
  13.     RaiseEvent TeclaPulsada(KeyCode, Shift)
  14. End Sub
  15. Private Sub UserControl_KeyPress(KeyAscii As Integer)
  16.     RaiseEvent TeclaPresionada(KeyAscii)
  17. End Sub
  18. Private Sub UserControl_KeyUp(KeyCode As Integer, Shift As Integer)
  19.     RaiseEvent TeclaSoltada(KeyCode, Shift)
  20. End Sub
  21.  
  22.  


Y con esto damos por finalizado para este control los eventos que se vuelcan al cliente. A partir de este momento  y aunque no hayamos terminado de diseñar nuestro control, si lo compilamos ahora, ya es totalmente operativo para el cliente, pués lo que nos resta por hacer no son enteramente indispensables.

En la próxima parte,  añadiremos la funcionalidad gráfica de foco, ya que como se ha indicado en partes anteriores, para los controles gráficos siempre hay uno que es control que tiene actualmente el foco y eso tenemos el deber de informarlo para que el usuario  tenga clar en todo momento que repercusión pueda tener la pulsación de una tecla como 'intro' u otra... en un momento dado.

En la última parte y para completar el control añadiremos funcionalidad sonora, para que el control aparte de tener indicaciones visuales también las tenga sonora ayudando  con ello a que usuarios con determinados problemas de vista puedan también guiarse con el oído. se dotará por tanto al control de 3 sonidos, 1 cuando se pulse el botón, otro cuando se suelte y otro más cuando el control sin tener el foco se pase el cursor por encima...

Para completar esta funcionalidad podríamos proveer al control con alguna propiedad o función que permita remplazar los sonidos por otros a elección del cliente. No obstante lo dejaremos, y cuando nos metamos con el control ListBox allí se aplicará y explicará en detalle...

Por supuesto, quedará aparte de terminar el control, una parte (breve) explicando los detalles necesarios para compilar el control y utilizarlo en la aplicación cliente, pero ya para probarlo compilado. Esta como se indica será la última parte .
«Ma non troppo»
----> ModoVacaciones = False<----

Nebire

  • Miembro HIPER activo
  • ****
  • Mensajes: 670
    • Ver Perfil
Re: Guía: Crear controles de usuario en VB (Controles ActiveX)
« Respuesta #10 en: Martes 12 de Octubre de 2010, 23:57 »
0
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
  1.  
  2. ' Variables que usa la pelota del foco
  3. 'Private s_UcAncho           As Long  ' limite a la derecha, ya la tenemos declarada y sus valores convenientemente actualizados
  4. 'Private s_UcAlto            As Long  ' limita abajo, ya la tenemos declarada   "  ídem                 "                  "
  5. ' 0 limite izquierdo y superior
  6. Private dX                  As Long   ' desplazamiento unitario en el eje X
  7. Private dY                  As Long   ' desplazamiento unitario en el eje Y
  8. Private sX                  As Long   ' mitad del  ancho de la pelota,  (de momento)
  9. Private sY                  As Long   ' mitad del alto de la pelota, (de momento)
  10.  
  11.  

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
  1.  
  2. Private Sub ReboteDiagonal()
  3.     With ShaFoco
  4.         ' desplazamos la pelota
  5.         Call .Move(.Left + dX, .Top + dY)
  6.         ' comprobamos si choca y
  7.         ' cuando alcanza un límite cambia el avance en esa dirección por el opuesto
  8.         If .Top <= -sY Then dY = -dY
  9.         If .Top >= s_UcAlto Then dY = -dY
  10.         If .Left <= -sX Then dX = -dX
  11.         If .Left >= s_UcAncho Then dX = -dX
  12.     End With
  13. End Sub
  14.  
  15.  

El movimiento de la pelota ya está descrito, ahora fijamos donde se le llama y cuando...

Código: Visual Basic
  1.  
  2. ' 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.
  3. Private Sub TimFoco_Timer()
  4.     Call ReboteDiagonal
  5. End Sub
  6.  
  7. ' en el método readproperties, al final:
  8.     TimFoco.Interval = 20
  9.     dX = 3: dY = 3
  10.  
  11.  

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
  1.  
  2. Private Sub UserControl_GotFocus()
  3.     ShaFoco.Visible = True
  4.     TimFoco.Enabled = True
  5. End Sub
  6.  
  7. Private Sub UserControl_LostFocus()
  8.     TimFoco.Enabled = False
  9.     ShaFoco.Visible = False
  10. End Sub
  11.  
  12.  
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
  1.  
  2. ShaFoco.visible=false
  3. ShaFoco.DRawMode= 6 'invert'
  4. ShaFoco.Fillstyle=0 ' sólido
  5. ShaFoco.Shape= 3 ' círculo
  6.  
  7.  
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
  1.  
  2. 'Donde pusimos estás líneas: 'TimFoco.Interval = 20  dX = 3: dY = 3 añadimos debajo esta otra:
  3. sX = ShaFoco.Width / 2: sY = ShaFoco.Height / 2
  4.  
  5.  
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
  1.  
  2. Private Sub Timer1_Timer()
  3.     CtlBoton1.Activo = False
  4. End Sub
  5.  
  6.  
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
  1.  
  2. ' 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:
  3. Private Sub Timer1_Timer()
  4.     'CtlBoton1.Activo = False
  5.     With CtlBoton1
  6.         Call .Move(.Left, .Top, 150, 24) ' recordad que menos de 24 de medida, no dibujaba el icono
  7.     End With
  8. End Sub
  9.  
  10.  

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
  1.  
  2. ' 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...
  3.     'End If
  4.         Call ShaFoco.Move(s_UcAncho / 2, s_UcAlto / 2)
  5.     '    Portero = False
  6.     'Else
  7.  
  8.  
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
  1.  
  2.   dX=3: dY=1
  3.  
  4.  
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
  1.  
  2. ' giro antihorario
  3. Private Sub VueltasAlEstadio()
  4.     Static avance As Byte
  5.     With ShaFoco
  6.         ' desplazamos la pelota
  7.         Call .Move(.Left + dX, .Top + dY)
  8.         ' comprobamos si choca y
  9.         ' cuando alcanza un límite cambia el avance en esa dirección por la siguiente
  10.         Select Case avance
  11.             Case 0  'está bajando, se alcanzó eltope ? si, sim entonces avanzamos a derecha
  12.                 If .Top >= (s_UcAlto - sY) Then ' ahora mueve a derecha
  13.                     dX = dY: dY = 0
  14.                     avance = 1
  15.                 End If
  16.             Case 1  ' a derecha, se alcanzó el tope derecho ?, si si, avanza subiendo
  17.                 If .Left >= (s_UcAncho - sX) Then ' ahora sube
  18.                     dY = -dX: dX = 0 ' dy tendrá ahora un valor negativo
  19.                     avance = 2
  20.                 End If
  21.             Case 2  ' esta´subiendo, alcanzó el tope superior ?, si si avance a izquierda
  22.                 If .Top <= 0 Then '-sY Then ' ahora mueve a izquierda
  23.                     dX = dY: dY = 0
  24.                     avance = 3
  25.                 End If
  26.             Case 3  ' avanza a izquierda, alcanzó tope?, si, si, ahora bajará..
  27.                 If .Left <= 0 Then '-sX Then  ' ahora baja
  28.                     dY = -dX: dX = 0 ' ahora dy tomará un valor positivo
  29.                     avance = 0
  30.                 End If
  31.         End Select
  32.     End With
  33. End Sub
  34.  
  35.  
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
  1.  
  2. ' llamar a la rutina nueva en vez de a la anterior que rebotaba
  3. Private Sub TimFoco_Timer()
  4.     'Call ReboteDiagonal
  5.     Call VueltasAlEstadio
  6. End Sub
  7.  
  8.  

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
  1.  
  2. ' buscamos enreadproperties y alteramos el código, dejamos copia comentada de lo anterior para apreciar diferencias
  3.  
  4.     'dx=3 dy=3
  5.     dX = 0: dY = 2
  6.     'sX = ShaFoco.Width / 2: sY = ShaFoco.Height / 2
  7.     sX = ShaFoco.Width: sY = ShaFoco.Height
  8.  
  9.  
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
  1.  
  2. ' If .Top <= 0 Then '-sY Then ' ahora mueve a izquierda
  3.  If .Top <= MargenFocoEstadio  Then '-sY Then ' ahora mueve a izquierda
  4.  
  5.  
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...
«Ma non troppo»
----> ModoVacaciones = False<----

Nebire

  • Miembro HIPER activo
  • ****
  • Mensajes: 670
    • Ver Perfil
Re: Guía: Crear controles de usuario en VB (Controles ActiveX)
« Respuesta #11 en: Lunes 6 de Junio de 2011, 01:02 »
0
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...
«Ma non troppo»
----> ModoVacaciones = False<----

Jimbenit

  • Miembro MUY activo
  • ***
  • Mensajes: 269
  • Nacionalidad: co
    • Ver Perfil
    • http://ingenieriacivil.foroactivo.com
Re: Guía: Crear controles de usuario en VB (Controles ActiveX)
« Respuesta #12 en: Martes 7 de Junio de 2011, 04:29 »
0
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...
Tranquilo amigo, este tema es un buen manual de consulta.


Yo quiero mucho a Toph ---> Mi Web]http://ingenieriacivil.foroactivo.com[/url]