Programación General > Visual Basic para principiantes
Tutorial: Crear un control de usuario en VB (Controles ActiveX)
Nebire:
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 --- Public Sub DibujarTodo() Dim DerechaIcono As Long UserControl.Cls ' antes que nada borramos todo ' el color tapiz se dibuja desde donde delegamos, luego, esto no es preciso pintarlo con usercontrol.line(x,y)-(x1,y1),color,bf Call DibujarImagen DerechaIcono = DibujarIcono Call DibujarTexto(DerechaIcono) 'Call DibujarRelieveEnd Sub 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 --- Private Sub DibujarImagen() If Not (p_Imagen Is Nothing) Then With UserControl .PaintPicture p_Imagen, 0, 0, .Width, .Height, 0, 0, ScaleX(p_Imagen.Width, vbHimetric, vbTwips), ScaleY(p_Imagen.Height, vbHimetric, vbTwips) End With End IfEnd Sub 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 --- Private Function DibujarIcono() As Long ' si las medidas del control satisfacen las medidas mínimas fijadas para el icono, lo podremos dibujar si no, pasamos... ' medidas mínimas = 16px icono + (2px relieve, + 2px margen) * 2 lados= 24px. ' 1ª comprobación If Not (p_Icono Is Nothing) Then With UserControl ' 2ª comprobación If s_UcAlto >= 24 Then If s_UcAncho >= 24 Then ' 3ª comprobación If (s_AltoIcono + 8) >= s_UcAlto Then .Height = Screen.TwipsPerPixelY * (s_AltoIcono + 8) End If If (s_AnchoIcono + 8) >= s_UcAncho Then .Width = Screen.TwipsPerPixelX * (s_AnchoIcono + 8) End If ' y finalmente dibujamos dibujamos .PaintPicture p_Icono, 4, s_TopIcono, s_AnchoIcono, s_AltoIcono DibujarIcono = s_AnchoIcono + 6 Else ' podríamos avisar que si es demasiado estrecho, no dibujar texto DibujarIcono = 4 End If Else DibujarIcono = 4 End If End With Else DibujarIcono = 4 End IfEnd Function 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 --- Private s_AnchoIcono As BytePrivate s_AltoIcono As BytePrivate s_TopIcono As Integer ' señala la cordenada Y donde se dibuja el icono en el control... 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 --- Private Sub MedirIcono() ' ...................... Case 20 s_AltoIcono = 128 Case Else s_AltoIcono = 24 End Select s_TopIcono = (s_UcAlto - s_AltoIcono) / 2 ' esta es la línea añadidaEnd Sub 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 --- ' al final de la sección de declaraciones (de variables)Private s_UcAncho As Long 'ancho en píxeles del controlPrivate s_UcAlto As Long ' alto en píxeles del control 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 --- Private Sub UserControl_Resize() s_UcAncho = UserControl.ScaleWidth s_UcAlto = UserControl.ScaleHeight End Sub 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 --- Private Sub UserControl_Resize() s_UcAncho = UserControl.ScaleWidth s_UcAlto = UserControl.ScaleHeight s_TopIcono = (s_UcAlto - s_AltoIcono) / 2 ' actualizamos el valor de posicionado del icono en la cordenada Y End Sub
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 --- Private Sub UserControl_Resize() s_UcAncho = UserControl.ScaleWidth s_UcAlto = UserControl.ScaleHeight s_TopIcono = (s_UcAlto - s_AltoIcono) / 2 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.End Sub 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 --- Private Sub DibujarTexto(ByVal Izquierda As Long) If p_Texto <> "" Then Select Case p_Alineacion Case 0 ' izquierda: UserControl.CurrentX = Izquierda Case 1 ' centro ancho control - desplaz icono - margenes a izquierda UserControl.CurrentX = ((s_UcAncho - Izquierda - 4) - s_AnchoTexto) / 2 Case 2 ' derecha: ancho control - margenes 1 lado - anchotexto UserControl.CurrentX = s_UcAncho - 4 - s_AnchoTexto End Select UserControl.CurrentY = s_TopTexto ' (s_UcAlto - s_AltoTexto) / 2 UserControl.Print p_Texto End IfEnd Sub 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 --- Public Property Set Fuente(ByRef f As IFontDisp) Set UserControl.Font = f Call MedirTexto ' línea añadida para calcular las medidas del texto PropertyChanged "Fuente" Call DibujarTodo End Property Public Property Let Texto(ByVal t As String) If t <> p_Texto Then p_Texto = t Call MedirTexto ' línea añadida para calcular las medidas del texto PropertyChanged "Texto" Call DibujarTodo End If End Property Private Sub MedirTexto() s_AnchoTexto = UserControl.TextWidth(p_Texto) s_AltoTexto = UserControl.TextHeight(p_Texto) End Sub 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 --- Private Sub UserControl_ReadProperties(PropBag As PropertyBag) With PropBag '.................... p_Texto = .ReadProperty("Texto", UserControl.Extender.Name) Call MedirTexto ' línea añadida para calcular las medidas del texto, justo en este momento... después de actualizar p_Texto '.................. 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 --- ' en la sección de declaracion de las variables...Private s_AnchoTexto As Long ' longitud del texto en píxeles (no en caracteres).Private s_AltoTexto As Integer ' altura del texto " "Private s_TopTexto As Integer ' posición de la cordenada Y del texto Private Sub UserControl_Resize() s_UcAncho = UserControl.ScaleWidth s_UcAlto = UserControl.ScaleHeight s_TopTexto = (s_UcAlto - s_AltoTexto) / 2 ' cálculo actualizado de la cordenada Y del texto s_TopIcono = (s_UcAlto - s_AltoIcono) / 2 Call DibujarTodoEnd Sub Private Sub MedirTexto() s_AnchoTexto = UserControl.TextWidth(p_Texto) s_AltoTexto = UserControl.TextHeight(p_Texto) s_TopTexto = (s_UcAlto - s_AltoTexto) / 2 ' cálculo actualizado de la cordenada Y del textoEnd Sub 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 --- Private Sub UserControl_ReadProperties(PropBag As PropertyBag) With PropBag ' ........................ p_Texto = .ReadProperty("Texto", UserControl.Extender.Name) ' .................... 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 Call MedirTexto ' ........................ 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 --- Public Sub DibujarTodo() Dim DerechaIcono As Long UserControl.Cls ' antes que nada borramos todo DerechaIcono = VerificarIcono ' <--------------- hemos añadido una llamada a parte de la rutina que calcula acerca del icono.... ' el color tapiz se dibuja desde donde delegamos, luego, esto no es preciso pintarlo con line(x,y)-(x1,y1),color,bf Call DibujarImagen if DerechaIcono = 0 then DerechaIcono = DibujarIcono end if Call DibujarTexto(DerechaIcono) 'Call DibujarRelieveEnd Sub 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 --- Private Function VerificarIcono() As Long ' si las medidas del control satisfacen las medidas mínimas fijadas para el icono, lo podremos dibujar si no, pasamos... ' medidas mínimas = 16px icono + (2px relieve, + 2px margen) * 2 lados= 24px. ' 1ª comprobación If Not (p_Icono Is Nothing) Then With UserControl ' 2ª comprobación If s_UcAlto >= 24 Then If s_UcAncho >= 24 Then ' 3ª comprobación If (s_AltoIcono + 8) >= s_UcAlto Then .Height = Screen.TwipsPerPixelY * (s_AltoIcono + 8) End If If (s_AnchoIcono + 8) >= s_UcAncho Then .Width = Screen.TwipsPerPixelX * (s_AnchoIcono + 8) End If ' se devuelve 0.... y aquí no pintamos nada, no es el momento de esta capa . Else VerificarIcono = 4 End If Else VerificarIcono = 4 End If End With Else VerificarIcono = 4 End IfEnd Function 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 --- Private Function DibujarIcono() As Long UserControl.PaintPicture p_Icono, 4, s_TopIcono, s_AnchoIcono, s_AltoIcono DibujarIcono = s_AnchoIcono + 6End Function 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 --- Public Sub DibujarTodo() Static Portero As Boolean ' los porteros siempre se declaran estáticos, su valor permanece entre llamadas... Dim DerechaIcono As Long If Portero = False Then Portero = True UserControl.Cls ' antes que nada borramos todo DerechaIcono = VerificarIcono ' el color tapiz se dibuja desde donde delegamos, luego, esto no es preciso pintarlo con line(x,y)-(x1,y1),color,bf Call DibujarImagen If DerechaIcono = 0 Then DerechaIcono = DibujarIcono End If Call DibujarTexto(DerechaIcono) 'Call DibujarRelieve Portero = False End IfEnd Sub 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...
Nebire:
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 --- Private Sub Rutina() Static portero As Boolean If portero = False Then portero = True ' aquí el código a ejecutar una sola vez ' alguna línea de este código hace unalamada al exterior, desde donde se reinvoca esta rutina. portero = False End If End Sub 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 --- ' en la sección de declaraciones junto a las otras constantes....Const c_MinUc = 12 ' tamaño mínimo permitido para el alto y para el ancho del control. Ahora nos vamos a limitar el tamaño del control, trabajando sobre el evento resize, antes teníamos...
--- Código: Visual Basic --- Private Sub UserControl_Resize() s_UcAncho = UserControl.ScaleWidth s_UcAlto = UserControl.ScaleHeight s_TopTexto = (s_UcAlto - s_AltoTexto) / 2 s_TopIcono = (s_UcAlto - s_AltoIcono) / 2 Call DibujarTodoEnd Sub 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 --- Private Sub UserControl_Resize() If UserControl.ScaleWidth < c_MinUc Then UserControl.Width = c_MinUc * Screen.TwipsPerPixelX End If s_UcAncho = UserControl.ScaleWidth If UserControl.ScaleHeight < c_MinUc Then UserControl.Height = c_MinUc * Screen.TwipsPerPixelY End If s_UcAlto = UserControl.ScaleHeight' ...............End Sub
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 --- Private Sub UserControl_Resize() Static portero As Boolean If portero = False Then portero = True If UserControl.ScaleWidth < c_MinUc Then UserControl.Width = c_MinUc * Screen.TwipsPerPixelX End If s_UcAncho = UserControl.ScaleWidth If UserControl.ScaleHeight < c_MinUc Then UserControl.Height = c_MinUc * Screen.TwipsPerPixelY End If s_UcAlto = UserControl.ScaleHeight s_TopTexto = (s_UcAlto - s_AltoTexto) / 2 s_TopIcono = (s_UcAlto - s_AltoIcono) / 2 Call DibujarTodo portero = False Else If s_ResizeDesdeIcono = True Then s_ResizeDesdeIcono = False s_UcAncho = UserControl.ScaleWidth s_UcAlto = UserControl.ScaleHeight s_TopTexto = (s_UcAlto - s_AltoTexto) / 2 s_TopIcono = (s_UcAlto - s_AltoIcono) / 2 End If End IfEnd Sub 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 --- Private Function VerificarIcono() As Long ' .............. If s_AltoIcono + 8 > s_UcAlto Then .Height = Screen.TwipsPerPixelY * (s_AltoIcono + 8) End If If s_AnchoIcono + 8 > s_UcAncho Then .Width = Screen.TwipsPerPixelX * (s_AnchoIcono + 8) End If ' ................ End Function
Ahora debemos tener...
--- Código: Visual Basic --- Private Function VerificarIcono() As Long ' .............. If s_AltoIcono + 8 > s_UcAlto Then s_ResizeDesdeIcono = True ' <------------- línea añadida, para visar de donde procede la llamada. .Height = Screen.TwipsPerPixelY * (s_AltoIcono + 8) End If If s_AnchoIcono + 8 > s_UcAncho Then s_ResizeDesdeIcono = True ' <------------- línea añadida, para visar de donde procede la llamada. .Width = Screen.TwipsPerPixelX * (s_AnchoIcono + 8) End If ' ................ End Function Sin olvidarnos de la declaración de la variable...
--- Código: Visual Basic --- ' en la sección de declaración de variables...Private s_ResizeDesdeIcono As Boolean ' distingue cuando el resize se ordena desde ajuste al tamaño de icono, para actualizar según conviene.
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 --- ' al final de la sección de declaraciones de variables...Private s_Relieve As Boolean ' TiposDeRelieve ' define el relieve en un momento dado. 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 --- 'Private Enum TiposDeRelieve' RELIEVE_HUNDIDO = -1' RELIEVE_PLANO = 0' RELIEVE_ALZADO = 1' RELIEVE_BORDEADO = 2 'End Enum
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 --- Private Sub DibujarRelieve() Dim c(0 To 3) As Long, scol As Long ' prepara los colores y las líneas. Select Case s_Relieve Case True 'RelievesBorde.RELIEVE_HUNDIDO ' CLAROS abajo , oscuros ARRIBA c(2) = &HFFFFFF: c(3) = &HF1EFE2: c(1) = &H9D9DA1: c(0) = &H716F64 Case False 'RelievesBorde.RELIEVE_ELEVADO ' CLAROS ARRIBA, oscuros abajo c(0) = &HFFFFFF: c(1) = &HF1EFE2: c(2) = &H9D9DA1: c(3) = &H716F64 'Case RelievesBorde.RELIEVE_BORDEADO ' scol = 16777215 - UserControl.BackColor ' c(0) = scol: c(1) = scol: c(2) = scol: c(3) = scol 'Case RelievesBorde.RELIEVE_PLANO ' Exit Sub End Select ' rodeando el control ' vertical izquierdo externo y vertical izquierdo interno UserControl.Line (s_UcAncho - 1, 0)-(s_UcAncho - 1, s_UcAlto), c(2) UserControl.Line (s_UcAncho - 2, 1)-(s_UcAncho - 2, s_UcAlto - 1), c(3) ' horizontal superior externo y horizontal superior interno UserControl.Line (0, 0)-(s_UcAncho, 0), c(0) ' vertical UserControl.Line (1, 1)-(s_UcAncho - 1, 1), c(1) ' horizontal inferior externo y horizontal inferior interno UserControl.Line (0, s_UcAlto - 1)-(s_UcAncho, s_UcAlto - 1), c(2) UserControl.Line (1, s_UcAlto - 2)-(s_UcAncho - 1, s_UcAlto - 2), c(3) ' vertical derecho externo y vertical derecho interno UserControl.Line (0, 0)-(0, s_UcAlto), c(0) ' horiz UserControl.Line (1, 1)-(1, s_UcAlto - 1), c(1)End Sub 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 --- ' Teníamos esto antes... Public Property Let Activo(ByVal a As Boolean) If a <> UserControl.Enabled Then UserControl.Enabled = a PropertyChanged "Activo" Call DibujarTodo End If End Property ' ahora con el cambio tendremos esto: Public Property Let Activo(ByVal a As Boolean) If a <> UserControl.Enabled Then UserControl.Enabled = a PropertyChanged "Activo" Call DibujarActivo End If End Property
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 --- Private Sub DibujarActivo() If UserControl.Enabled = False Then UserControl.DrawMode = 5 UserControl.Line (0, 0)-(UserControl.ScaleWidth, UserControl.ScaleHeight), vbRed, BF Else UserControl.DrawMode = 13 Call DibujarTodo End IfEnd Sub 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 --- Private Sub UserControl_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single) s_Relieve = True Call DibujarRelieveEnd Sub Private Sub UserControl_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single) s_Relieve = False Call DibujarRelieveEnd Sub 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 --- Public Sub DibujarTodo() Static portero As Boolean Dim DerechaIcono As Long If portero = False Then portero = True UserControl.Cls DerechaIcono = VerificarIcono Call DibujarImagen If DerechaIcono = 0 Then DerechaIcono = DibujarIcono End If Call DibujarTexto(DerechaIcono) Call DibujarRelieve ' <---------------------- activamos esta línea que antes estaba comentada... portero = False End IfEnd Sub
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.
Nebire:
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 --- UserControl.Enabled = .ReadProperty("Activo", True) y podríamos sustituirla por esta otra:
--- Código: Visual Basic --- Activo = .ReadProperty("Activo", True) 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 --- Activo = .ReadProperty("Activo", True)'UserControl.Enabled = .ReadProperty("Activo", True) 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 --- ' en la sección de declaración de variables...Private s_Cargando As Boolean ' chivato que al leer propiedades permite ejecutar código condicional si el control está desactivado.
La línea referida al usercontrol.enabled = .... la dejamos intacta, no como Activo = ..... que vimos no resolvía nuestro problema...
--- Código: Visual Basic --- ' ............... UserControl.Enabled = .ReadProperty("Activo", True) End With s_Cargando = True ' <------------------ esta es la línea introducidaEnd Sub 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 --- Call DibujarTodo ' <--------------------- este código es lo nuevo añadido.... If s_Cargando = True Then s_Cargando = False If UserControl.Enabled = False Then Call DibujarActivo End If End If 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 --- Private Sub UserControl_Paint() Call DibujarTodo If UserControl.Enabled = False Then Call DibujarActivo End IfEnd Sub 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 --- timer1.Enabled=falseTimer1.interval =300 Después cuando lleguemos al final del evento readproperties (allí donde colocamos el chivato) haríamos :
--- Código: Visual Basic --- Timer1.enabled = True 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 --- Private Sub Timer1_Timer() Timer1.Enabled = False If UserControl.Enabled = False Then Call DibujarActivo End IfEnd Sub 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 --- if not (p_Icono is nothing) and not(i is nothing) then DibujarIcono else Dibujartodo end if 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 --- set ctlBoton1.Imagen= nothing ' óset ctlBoton1.imagen= loadpicture("")
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 --- Public Enum OrigenesDeImagen ORIGEN_INTERNO = 0 ' la que nosotros proveemos internamente ORIGEN_EXTERNO = 1 ' la que el usuario elija externamenteEnd Enum ' debajo de la declaración p_imagenPrivate p_OrigenImagen As OrigenesDeImagen ' procedencia de la imagen: externa=usuario, interna=la que proveemos ' bajo la propiedad imagenPublic Property Get ImagenOrigen() As OrigenesDeImagen ImagenOrigen = p_OrigenImagenEnd Property Public Property Let ImagenOrigen(ByVal io As OrigenesDeImagen) If io <> p_OrigenImagen Then p_OrigenImagen = io 'codigo PropertyChanged "ImagenOrigen" End If End Property ' persistencia de la propiedad, también cada línea justo debajo de cada una de imagen, todo ordenado p_OrigenImagen = .ReadProperty("ImagenOrigen", 0) ' interna por defecto .WriteProperty "ImagenOrigen", p_OrigenImagen, 0 ' interna por defecto ' inicialización:p_OrigenImagen = ORIGEN_INTERNO ' valor 0.... 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 --- ' en las declaracionesPrivate s_Imagen As IPictureDisp ' salvaguarda de nuestra imagen... ' en initproperties, proveemos el duplicado. Set p_Imagen = UserControl.Picture Set UserControl.Picture = Nothing Set s_Imagen = p_Imagen '<-------------------- copiamos la imagen interna. p_OrigenImagen = ORIGEN_INTERNO 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 --- ' estado actual de la propiedad imagen: Public Property Set Imagen(ByVal i As IPictureDisp) Set p_Imagen = i Call DibujarTodo End Property ' el código una vez hechos los cambios: Public Property Set Imagen(ByVal i As IPictureDisp) If p_OrigenImagen = ORIGEN_EXTERNO Then '<------------------------ Set p_Imagen = i Call DibujarTodo Else '..... más abajo se explica que va aquí. End If '<--------------------------- End Property Ahora los añadidos en la propiedad ImagenOrigen:
--- Código: Visual Basic --- Public Property Let ImagenOrigen(ByVal io As OrigenesDeImagen) If io <> p_OrigenImagen Then p_OrigenImagen = io If io = ORIGEN_INTERNO Then Set p_Imagen = s_Imagen Call DibujarTodo End If PropertyChanged "ImagenOrigen" End If End Property 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 --- Private Sub UserControl_Initialize() Set p_Imagen = UserControl.Picture Set UserControl.Picture = Nothing Set s_Imagen = p_ImagenEnd Sub 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 --- ' en persistenciaSet s_Imagen = .ReadProperty("ImagenInterna", UserControl.Picture).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... ' en initproperties, justo debajo de la asignación de la variable debemos notificar, al objeto encargado de la persistencia: Set s_Imagen = p_Imagen UserControl.PropertyChanged "ImagenInterna" '<------------------------ línea añadida 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 --- Private Sub Command1_Click() CtlBoton1.ImagenOrigen = ORIGEN_INTERNOEnd Sub 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 --- Public Property Set Imagen(ByVal i As IPictureDisp) If p_OrigenImagen = ORIGEN_EXTERNO Then Set p_Imagen = i Call DibujarTodo Else '..... más abajo se explica que va aquí. <--------------- ahora comentamos esto End If End Property 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 --- Public Property Set Imagen(ByVal i As IPictureDisp) If p_OrigenImagen = ORIGEN_EXTERNO Then Set p_Imagen = i Call DibujarTodo Else 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." End If End Property
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 --- Public Property Set Imagen(ByVal i As IPictureDisp) If p_OrigenImagen = ORIGEN_EXTERNO Then Set p_Imagen = i Call DibujarTodo Else If Ambient.UserMode = False Then 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." End If End If End Property 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 --- Public Property Get Imagen() As IPictureDispAttribute 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)." Set Imagen = p_ImagenEnd Property Public Property Set Imagen(ByVal i As IPictureDisp) Set s_ImgUsuario = i If p_OrigenImagen = ORIGEN_EXTERNO Then Set p_Imagen = i Call DibujarTodo Else If Ambient.UserMode = False Then 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." End If End If End Property 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...
Nebire:
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 --- ' Colocar debajo dela declaración de variables, dejando unás líneas vacías, y antes de las propiedades....Public Event TextoCambiado(ByVal NuevoTexto As String, ByVal TextoAnterior As String) 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 --- Public Property Let Texto(ByVal t As String) Dim previo As String '<------------------ If t <> p_Texto Then previo = p_Texto '<------------------ p_Texto = t Call MedirTexto PropertyChanged "Texto" Call DibujarTodo RaiseEvent TextoCambiado(t, previo) '<------------------ <----------------- End If End Property 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 --- public event Textocambiando (byval Textosugerido as string) Public Property Let Texto(ByVal t As String) If t <> p_Texto Then RaiseEvent TextoCambiando(t) '<------------------ <--------------- p_Texto = t Call MedirTexto PropertyChanged "Texto" Call DibujarTodo End If End Property 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 --- ' en la sección de declaración de eventos (debajo de la declaración de variables)Event TextoVacio(ByRef TextoSugerido As String) Public Property Let Texto(ByVal t As String) Dim TxtSug '<------------------------ If t <> p_Texto Then If t = "" Then '<------------------------ txtSug = UserControl.Extender.Name '<------------------------ RaiseEvent TextoVacio(txtSug) '<------------------------ <------------------ t= txtsug '<------------------------ End If '<------------------------ p_Texto = t Call MedirTexto PropertyChanged "Texto" Call DibujarTodo End If End Property 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 --- ' declaración del evento, nótese el 'byval'Public Event TextoCambiado(ByVal NuevoTexto As String) ' desatar el evento..Public Property Let Texto(ByVal t As String) If t <> p_Texto Then p_Texto = t Call MedirTexto PropertyChanged "Texto" Call DibujarTodo RaiseEvent TextoCambiado(t) '<--------------- <---------------- End If End Property
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 --- Private Sub CtlBoton1_TextoCambiado(ByVal NuevoTexto As String) Static cambio As Boolean If cambio = False Then cambio = True CtlBoton1.Texto = "Lunes" Else cambio = False CtlBoton1.Texto = "Domingo" End IfEnd Sub 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 --- ' declaración del evento, nótese el 'byval'Public Event TextoCambiado(ByVal NuevoTexto As String) ' desatar el evento..Public Property Let Texto(ByVal t As String) Static Portero As Boolean '<----------- If Portero = False Then '<----------- Portero = True '<----------- If t <> p_Texto Then p_Texto = t Call MedirTexto PropertyChanged "Texto" Call DibujarTodo RaiseEvent TextoCambiado(t) '<--------------- <---------------- End If Portero = False '<----------- Else '<----------- Beep '<----------- End If '<----------- End Property
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 --- ' declaración del evento, nótese ahora el 'byRef'Public Event TextoCambiado(Byref NuevoTexto As String) ' desatar el evento..Public Property Let Texto(ByVal t As String) Static Portero As Boolean '<----------- If Portero = False Then '<----------- Portero = True '<----------- If t <> p_Texto Then p_Texto = t '<------------------------ Call MedirTexto '<----------------------- Call DibujarTodo '<------------------------ RaiseEvent TextoCambiado(t) '<--------------- <---------------- p_Texto = t Call MedirTexto PropertyChanged "Texto" Call DibujarTodo End If Portero = False '<----------- Else '<----------- Beep '<----------- End If '<----------- End Property
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 --- Public Event TextoCambiado(ByVal NuevoTexto As String) Public Property Let Texto(ByVal t As String) Static x As Byte '<----------- Static Portero As Boolean If Portero = False Or x < 12 Then '<----------- Portero = True x = x + 1 '<----------- If t <> p_Texto Then p_Texto = t Call MedirTexto PropertyChanged "Texto" Call DibujarTodo RaiseEvent TextoCambiado(t) '<--------------- <---------------- End If Portero = False x = 0 '<----------- Else Beep End If End Property
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 --- Private ResetLimite As Boolean Public Event TextoCambiado(ByVal NuevoTexto As String) Public Property Let Texto(ByVal t As String) Static x As Byte If ResetLimite = True Then x = 0 If x < 12 Then x = x + 1 If t <> p_Texto Then p_Texto = t Call MedirTexto PropertyChanged "Texto" Call DibujarTodo RaiseEvent TextoCambiado(t) '<--------------- <---------------- End If Else Beep MsgBox "Se alcanzó el límite máximo de 12 cambios para el valor. No se permiten más cambios hasta ser 'reseteado'." End If End Property
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 --- Private Sub CtlBoton1_TextoCambiado(ByVal NuevoTexto As String) Static cambio As Boolean Static x As Long x = x + 1 If cambio = False Then cambio = True CtlBoton1.Texto = "Lunes" Else cambio = False CtlBoton1.Texto = "Domingo" End IfEnd Sub 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 --- ' el código de la estructura lo colocamos debajo de todas las enumeraciones...Private Type LanzaderaEventos Identificador As Byte Param1 As Variant Param2 As Variant Param3 As Variant Param4 As VariantEnd Type
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 --- Private s_ReparteEventos As LanzaderaEventos ' una variable privada de la estructura es donde se alojarán los datos, nuestro camión de reparto.
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 --- Private Enum IdentificadoresDeEvento IDENT_EVENTO_TEXTO_CAMBIADO = 1 '.... '..... IDENT_EVENTO_ICONO_CAMBIADO = 8End Enum
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 --- Private Type LanzaderaEventos Identificador As IdentificadoresDeEvento '<------------- cambiado a tipo long, con la enumeración creada al efecto. '..............
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 --- Private s_AlmacenEv As New Collection ' podeis darle un nombre más largo para familiarizaros como s_AlmacenEventos.
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 --- Dim s_RecogeEventos As LanzaderaEventos Public Property Let Texto(ByVal t As String) If t <> p_Texto Then p_Texto = t Call MedirTexto PropertyChanged "Texto" Call DibujarTodo ' el código nuevo With s_RecogeEventos .Identificador = IDENT_EVENTO_TEXTO_CAMBIADO .Param1 = t End With Call s_AlmacenEv.Add(s_RecogeEventos) ' esta línea desaparece de aquí: RaiseEvent TextoCambiado(t) '<--------------- <---------------- End If End Property 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 --- s_Cargando = True TimEventos.Interval = 20 '<----------------------------------- líneas añadidas TimEventos.Enabled = Ambient.UserMode '<---------------------------
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 --- Public Property Let Activo(ByVal a As Boolean) If a <> UserControl.Enabled Then UserControl.Enabled = a TimEventos.Enabled = a And Ambient.UserMode '<---------------------- Call DibujarActivo PropertyChanged "Activo" End If End Property 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 --- Private Sub TimEventos_Timer() If s_AlmacenEv.Count > 0 Then s_ReparteEventos = s_AlmacenEv.Item(1) With s_ReparteEventos Select Case .Identificador Case IdentificadoresDeEvento.IDENT_EVENTO_TEXTO_CAMBIADO ' 1 RaiseEvent TextoCambiado(.Param1) ' case.... Case IdentificadoresDeEvento.IDENT_EVENTO_ICONO_CAMBIADO ' 8 End Select End With Call s_AlmacenEv.Remove(1) End IfEnd Sub 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 --- ' dentro de módulo de código de la clase Public Enum IdentificadoresDeEvento IDENT_EVENTO_TEXTO_CAMBIADO = 1 '.... IDENT_EVENTO_ICONO_CAMBIADO = 8End Enum Public Identificador As IdentificadoresDeEventoPublic Param1 As VariantPublic Param2 As VariantPublic Param3 As VariantPublic Param4 As Variant
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 --- 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 NEWPrivate s_ReparteEventos As New LanzaderaEventos ' ojo NEW... Private s_AlmacenEv As New Collection
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 --- ' el código de entrega casi no cambia, sólo se añade una línea... luego se explica porquéPublic Property Let Texto(ByVal t As String) If t <> p_Texto Then p_Texto = t Call MedirTexto PropertyChanged "Texto" Call DibujarTodo With s_RecogeEventos .Identificador = IDENT_EVENTO_TEXTO_CAMBIADO .Param1 = t End With Call s_AlmacenEv.Add(s_RecogeEventos) Set s_RecogeEventos = Nothing ' <---------------- ' esta línea sigue retiradaRaiseEvent TextoCambiado(t) '<--------------- End If End Property ' el código de reparto:Private Sub TimEventos_Timer() If s_AlmacenEv.Count > 0 Then Set s_ReparteEventos = s_AlmacenEv.Item(1) With s_ReparteEventos Select Case .Identificador Case IdentificadoresDeEvento.IDENT_EVENTO_TEXTO_CAMBIADO ' 1 RaiseEvent TextoCambiado(.Param1) ' case.... ' ..................... Case IdentificadoresDeEvento.IDENT_EVENTO_ICONO_CAMBIADO ' 8 ' ..................... End Select End With Set s_ReparteEventos = Nothing '<------------------------------ Call s_AlmacenEv.Remove(1) End IfEnd Sub 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 --- If s_AlmacenEv.Count > 10 Then Call s_AlmacenEv.Add(s_RecogeEventos, after:=4) Else Call s_AlmacenEv.Add(s_RecogeEventos) End If
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.
Nebire:
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 --- ' 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ñadiendoPublic Event RatonPulsado(Button As Integer, Shift As Integer, X As Single, Y As Single)Public Event RatonSoltado(ByVal Boton As Byte, ByVal CombinacionTeclas As Integer, ByVal X As Single, ByVal Y As Single) 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 --- Private Sub UserControl_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single) s_Relieve = True Call DibujarRelieve RaiseEvent RatonPulsado(Button, Shift, X, Y) '<-------------------------- línea añadidaEnd Sub Private Sub UserControl_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single) s_Relieve = False Call DibujarRelieve RaiseEvent RatonSoltado(Button, Shift, X, Y) '<-------------------------- línea añadidaEnd Sub
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 --- ' junto a la declaración de eventosPublic Event RatonMovido(Button As Integer, Shift As Integer, X As Single, Y As Single)Public Event TeclaPulsada(KeyCode As Integer, Shift As Integer)Public Event TeclaSoltada(KeyCode As Integer, Shift As Integer)Public Event TeclaPresionada(KeyAscii As Integer) Private Sub UserControl_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) RaiseEvent RatonMovido(Button, Shift, X, Y)End SubPrivate Sub UserControl_KeyDown(KeyCode As Integer, Shift As Integer) RaiseEvent TeclaPulsada(KeyCode, Shift)End SubPrivate Sub UserControl_KeyPress(KeyAscii As Integer) RaiseEvent TeclaPresionada(KeyAscii)End SubPrivate Sub UserControl_KeyUp(KeyCode As Integer, Shift As Integer) RaiseEvent TeclaSoltada(KeyCode, Shift)End Sub
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 .
Navegación
[#] Página Siguiente
[*] Página Anterior
Ir a la versión completa