Curso Básico de Programación
en Visual Basic

 

Entrega Cuarenta y cinco: 09/Feb/2003
por Guillermo "guille" Som

Si quieres linkar con las otras entregas, desde el índice lo puedes hacer

 

Bueno, parece ser que me estoy "portando bien", ya que esta nueva entrega está dentro de los plazos que me impuse hace poco: publicar como mínimo una entrega cada mes. Sí, ya se que deberían ser más entregas por mes, pero al menos es mejor tener una entrega al mes que no tener que estar varios meses sin ninguna nueva... así que no te quejes mucho... je, je.

 

Las soluciones de la entrega 44

Empezaremos viendo las soluciones al ejercicio propuesto en la entrega (44) anterior, además de algo que no estaba explícitamente propuesto, pero que si no lo has hecho, no te funcionaría bien.

Este sería el código a usar en los métodos Guardar y Leer de la clase cConfig.
Empezaremos por el método Guardar que es el más simple y después veremos el método Leer en el que habrá que hacer algunas comprobaciones extras:

Public Sub Guardar(ByVal fichero As String)
    ' guardar en el fichero indicado los valores de las propiedades de la clase
    '
    ' el código está omitido para que lo hagas como ejercicio
    ' SOLUCIÓN:
    Dim nFic As Long
    '
    On Error Resume Next

    ' asignar un "canal" libre
    nFic = FreeFile
    ' abrir el fichero para escritura en el canal indicado
    Open fichero For Output As nFic
    ' guardar las cuatro propiedades de esta clase
    Print #nFic, Me.Valor1
    Print #nFic, Me.Valor2
    Print #nFic, Me.Valor3
    Print #nFic, Me.Opcion1
    ' cerrar el canal abierto
    Close nFic
    '
End Sub

Como puedes comprobar, el código es de lo más simple. Una vez abierto el fichero indicado, se guardan los valores de las cuatro propiedades de la clase. Usamos On Error Resume Next, por si el disco estuviera lleno o no disponible...

Veamos ahora el código de Leer.

Public Sub Leer(ByVal fichero As String)
    ' leer del fichero indicado los valores de las propiedades de la clase
    '
    ' el código está omitido para que lo hagas como ejercicio
    ' SOLUCIÓN:
    Dim nFic As Long
    Dim s As String
    '
    ' comprobar si existe el fichero
    ' si no existe, se sale del procedimiento y no se hace nada
'    If Dir$(fichero) = "" Then
'        Exit Sub
'    End If
    '
    ' NOTA:
    '   Es posible, que si el nombre del fichero está mal formado
    '   (no es un nombre válido), se produzca un error.
    '   En ese caso se podría usar el siguiente código,
    '   o usar una función ExistFile como se ha mostrado en entregas anteriores.
    Dim i As Long
    '
    On Error Resume Next

    i = Len(Dir$(fichero))
    ' Si se produce un error en la línea anterior, es que no existe.
    ' Además de que si i vale cero, es que no existe.
    If Err.Number <> 0 Or i = 0 Then
        Exit Sub
    End If
    '
    '
    ' asignar un "canal" libre
    nFic = FreeFile
    ' abrir el fichero para lectura en el canal indicado
    Open fichero For Input As nFic
    ' leer los cuatro valores en el mismo orden en el que se guardaron
    Line Input #nFic, s
    Me.Valor1 = s
    Line Input #nFic, s
    Me.Valor2 = s
    Line Input #nFic, s
    Me.Valor3 = s
    Line Input #nFic, s
    ' Nota:
    '   Esta asignación también puede dar error si se ha manipulado el fichero,
    '   para evitar ese error, podemos usar On Error Resume Next para evitar
    '   que el programa se detenga.
    '
    '   Como resulta que ya tenemos un On Error activo, no será necesario
    '   indicarlo nuevamente, pero si la comprobación de que el fichero existe
    '   se hace desde una función, habría que quitar el comentario.
    'On Error Resume Next
    Me.Opcion1 = CBool(s)
    ' cerrar el canal abierto
    Close nFic
    '
End Sub

Este tampoco es complicado, aunque algo más "largo" que el anterior, entre otras cosas porque se hace una comprobación de que el fichero indicado existe y otra de que el valor asignado a la propiedad Opcion1 es el correcto. En teoría no habría que hacer estas comparaciones, o al menos no habría porqué usar On Error, ya que se supone que el nombre del fichero es correcto, independientemente de que dicho fichero exista o no y por otro lado, cuando se lea ese fichero los datos estarán guardados de forma correcta. Pero siempre es preferible prevenir.

Ahora veamos el código del formulario principal, tanto del evento producido al guardar como al leer:

Private Sub cmdGuardar_Click()
    ' guardar los datos en un fichero
    '
    ' SOLUCIÓN:
    ' (aunque no estaba como ejercicio, si no se hace, no se guarda correctamente)
    '
    ' Asignamos los valores a la clase y guardarlos
    mConfig.Valor1 = Text1
    mConfig.Valor2 = Text2
    mConfig.Valor3 = Text3
    If Check1.Value = vbChecked Then
        mConfig.Opcion1 = True
    Else
        mConfig.Opcion1 = False
    End If
    '
    mConfig.Guardar sFic
End Sub

Private Sub cmdLeer_Click()
    ' leer los datos del fichero indicado
    mConfig.Leer sFic
    '
    '
    '$POR HACER: actualizar los controles con los valores de mConfig
    ' SOLUCIÓN:
    asignarConfig mConfig
    '
    '
End Sub

En el caso del evento producido al pulsar en el botón Guardar, antes de llamar al método correspondiente de la clase, hay que asignar los valores a las propiedades de dicha clase.
Para el evento del botón Leer lo tenemos más fácil ya que todo el trabajo de asignar el contenido de la clase en los controles del formulario se hace por medio del procedimiento asignarConfig.

Espero y confío en que lo hayas hecho bien... y si no es así... pues tampoco pasa nada, ya que por eso te doy la solución... je, je.

 

 

A la vuelta con la Encapsulación

Como has podido comprobar, al "encapsular" ciertas tareas a realizar por una clase, (en este caso las acciones de leer y guardar la información), el código de nuestros proyectos será más fácil de mantener, ya que si por alguna circunstancia necesitamos cambiar la forma de acceder a ese fichero, bien porque hayamos añadido alguna nueva propiedad o porque haya cambiado el tipo de datos, tan sólo tendremos que cambiar el código de esos procedimientos, de forma que en el resto del código no tengamos que hacer ningún cambio.

Por esa razón, es importante y sobre todo aconsejable, que en la medida de lo posible, hagamos que sea el código incluido en la propia clase el que se encargue de manejar la información o los datos que dicha clase manipulará.
Ya que, al menos eso es lo que se supone, la propia clase es la que "mejor" sabe cómo manipular los datos que contiene. Si, por ejemplo, tienes que crear una función o procedimiento para clasificar esos datos, siempre será más conveniente que el código que se encargue de realizar esa clasificación esté contenido en la propia clase que usar funciones o procedimientos externos. Haciéndolo de esta forma, sobre todo, ganamos más legibilidad en nuestro código.

 

Siguiendo con el tema de la encapsulación y la forma de manejar los datos o la información de las clases, en ocasiones nos podemos encontrar con la necesidad de que sea la propia clase la que nos avise de que ha sucedido algo. Puede ser interesante que, por ejemplo, si la clase usada en el proyecto de la entrega anterior detecta algún tipo de error al leer o guardar los datos, nos lo comunique. O bien, que si son muchos los datos que tiene que guardar o leer, nos vaya mostrando cada uno de los datos que está procesando.
Esto lo podemos conseguir mediante los eventos.

Definir Eventos en las clases

Hasta ahora hemos estado usando los eventos de los formularios y los controles.
Ya sabes que esa es la forma en la que el propio sistema operativo se comunica con esos controles y formularios, de forma que podamos saber cuando se ha pulsado en un control o se ha movido el ratón o... cualquiera de las otras cosas que Visual Basic tenga que saber... ya que, aunque Windows comunica muchas cosas a los controles y formularios, no le comunica TODO lo que ocurre... pero, bueno, ese será el tema de otra entrega, lo que ahora nos interesa saber es que podemos crear nuestros propios eventos en nuestras clases y que podemos usarlos desde cualquier otro sitio.

Lo primero que debemos aprender es cómo definir y "disparar" dichos eventos en nuestras clases.

Para que los objetos creados a partir de nuestras clases puedan comunicarse con dicha clase mediante eventos, tenemos que definirlos.
De la misma forma que existen instrucciones o palabras clave para definir una función o un procedimiento, existe una instrucción que nos permite indicarle a nuestro querido VB que queremos crear un evento, la instrucción que usaremos será: Event seguida del nombre del evento y opcionalmente los parámetros que deba tener.
Por ejemplo:

Event Prueba(ByVal mensaje As String)

Nota:
De forma predeterminada, los eventos son públicos, así que, es opcional indicar esa instrucción.

Esta línea definiría un evento llamado Prueba que recibe un parámetro por valor de tipo String.
Si tuviésemos un objeto declarado con la clase que contiene ese evento, lo usaríamos de la siguiente forma:
Private elObjeto_Prueba(ByVal mensaje As String)
    ' ... el código
End Sub

Es decir, de la misma forma que hasta ahora hemos estado usando los eventos.
Fíjate que también se sigue la nomenclatura estándar de usar el nombre del objeto un guión bajo y el nombre del evento; aunque este es un detalle del que tenemos que despreocuparnos, ya que es el propio entorno de desarrollo el que se encarga de dar nombre a los eventos de la forma correcta.

Cómo lanzar un evento

Una vez que hemos declarado un evento en una de nuestras clases, nos queda por saber cómo hacer que dicho evento se lance, es decir cómo hacemos para "avisar" al código que usa nuestra clase de que dicho evento se ha producido.
Esto se consigue usando la instrucción RaiseEvent seguida del nombre del evento y del parámetro (o parámetros) que tenga dicho evento.
Por ejemplo, si queremos lanzar ese evento, haríamos algo como lo siguiente:
RaiseEvent Prueba("El evento prueba")

Cuando escribimos la instrucción RaiseEvent, el IDE de Visual Basic nos muestra los eventos que podemos lanzar, en la siguiente figura, podemos ver que nos muestra los tres eventos que tenemos definidos:


Fig.1, el IDE de VB muestra los eventos que la clase puede producir.

Como sabrás, cuando no indicamos si el parámetro es por valor o por referencia, se supone que es por referencia, por tanto, en el caso del evento ElementoSeleccionado, el parámetro index será ByRef y en los otros dos, tal y como se indica en la declaración mostrada en la figura, son del tipo ByVal.

Nota:
Como te dije antes, cuando declaramos un evento en una clase, ese evento está definido como público, pero el que esté definido como público no significa que se pueda lanzar desde cualquier sitio, ya que sólo se puede usar RaiseEvent nombreEvento() desde la propia clase en la que se ha definido el evento.

 

Cómo indicar que una clase debe interceptar eventos

Debido a la forma "especial" en la que Visual Basic define los procedimientos de evento, no basta con declarar una variable del tipo de la clase que produce los eventos para poder usarlos.
Me explico, para que lo comprendas mejor: Supongamos que tenemos una clase llamada cConEventos, la cual produce los tres eventos mostrados en la figura 1. Si declaramos una variable de ese tipo en otra parte de nuestro proyecto, lo normal es que lo hagamos de esta forma:

Dim mConEvento As cConEventos

Después creamos el objeto en la memoria (lo instanciamos) usando New:

Set mConEvento = New cConEventos

A partir de este momento podemos usar dicho objeto, ya que se ha creado en la memoria. Esto ya lo tenemos claro, ¿verdad? ya que al fin y al cabo es la forma "habitual" de declarar e instanciar una clase.

Pero esto no nos permitiría usar los eventos declarados en la clase, a pesar de que tengamos definidos los procedimientos Sub de los eventos, los cuales se definirían de la siguiente forma:

Private Sub mConEvento_Aviso(ByVal elAviso As String)
    '
End Sub

Private Sub mConEvento_ElementoSeleccionado(index As Integer)
    '
End Sub

Private Sub mConEvento_Prueba(ByVal mensaje As String)
    '
End Sub

¿Por qué?
Por la sencilla razón de que si bien la clase produce eventos, Visual Basic no sabe que queremos usarlos, es decir, esos tres procedimientos que, al menos en teoría, deberían interceptar los eventos no los interceptarán.
Para que Visual Basic se entere de que la clase se va a usar para procesar o interceptar los eventos, hay que declararla usando la instrucción WithEvents.
Sabiendo esto, cuando queramos usar una clase que produce eventos, tenemos que declararla de la siguiente forma:

Dim WithEvents mConEvento As cConEventos

A partir de ese momento, nuestro querido (y, algunas veces, tozudo) Visual Basic sabe que nuestra intención es usar los eventos de esa clase.
Además, una vez declarada una variable de esta forma, podemos comprobar que dicha variable se muestra en la lista desplegable de los objetos que producen eventos, (la que está en la izquierda del panel de código), tal como podemos ver en la siguiente imagen:


Fig. 2, Lista con los objetos que producen eventos.

Una vez seleccionado el objeto en la lista desplegable de la izquierda, podemos ver los eventos que dicha clase produce, para ello debemos desplegar la lista de la derecha. En la siguiente figura podemos ver los tres eventos que la hipotética clase cConEventos produce:


Fig. 3, Los eventos de la clase seleccionada en la lista.

Los eventos se muestran de forma alfabética, pero el que toma el foco, (el que se crea nada más que mostrar ese objeto), es el primero que hayamos definido en la clase. En este caso, el evento Prueba.

A partir de este momento podemos usar los eventos que queramos, no es necesario tener que codificarlos todos, sólo los que nos interese interceptar.
Cuando mostramos la lista con los eventos, el IDE de Visual Basic nos muestra en negrita los que ya tienen código.

Si declaramos una clase con WithEvents y esa clase no produce eventos, dicha clase no se mostrará en la lista de clases que podemos usar para declarar esa variable, además de que al ejecutar la aplicación, el Visual Basic nos mostrará un error de que dicha clase no produce eventos, tal y como podemos comprobar en la siguiente figura:


Fig. 4, Error al declarar con WithEvents una variable que no produce eventos.

 

Resumen de cómo definir y usar eventos personalizados:

Resumamos un poco todo esto, para que quede más o menos claro cómo definir y usar eventos en las clases:

1- Para que la clase tenga eventos, debemos definirlos con la instrucción Events.
2- Para lanzar un evento en nuestra clase, debemos usar RaiseEvent seguida del evento a lanzar.
3- Para poder usar los eventos de una clase desde otra parte de nuestro proyecto, debemos declarar dicha clase con la instrucción WithEvents.
4- Una vez definida la variable a partir de una clase que produce eventos, debemos escribir el código de los procedimientos de los eventos (siempre serán del tipo Sub) en la forma habitual:
NombreVariable guión bajo NombreEvento, por ejemplo: Sub mConEventos_Prueba(parámetros)

 

Para ver en la práctica todo esto que se ha comentado, vamos a crear un proyecto en el que definiremos una clase que produce eventos y los interceptaremos en un formulario.

Para ello vamos a crear un nuevo proyecto, al que añadiremos una clase llamada cConEventos que definirá dos eventos, los cuales se producirán al llamar a un método de esta misma clase.
Uno de esos eventos se producirá mientras se añaden nuevos datos a un array y el otro al terminar de añadir dichos datos, devolviendo el número total de elementos.
Veamos el código de esa clase:

Option Explicit

Private mDatos() As String
'
Event NuevoDato(ByVal elDato As String)
Event DatosCreados(ByVal total As Long)

Public Sub CrearDatos()
    Dim i As Long
    '
    ReDim mDatos(10)
    For i = 0 To 10
        mDatos(i) = "El dato número " & CStr(i)
        RaiseEvent NuevoDato(mDatos(i))
    Next
    RaiseEvent DatosCreados(11)
End Sub

Como podemos comprobar, los dos eventos que nuestra clase emitirá al "receptor" de utilice dicha clase, serán:
NuevoDato el cual tiene un parámetro de tipo String que representará al nuevo dato que se está manipulando y
DatosCreados cuyo parámetro de tipo Long nos indicará el número total de datos que la clase acaba de crear.
Esos dos eventos se lanzan (o disparan) en el método CrearDatos, que será el único que podremos usar desde cualquier variable declarada con el tipo de la clase cConEventos.

El código usado en este último procedimiento es simple, pero te lo detallo para que no tengas problemas de comprensión... (sí, ya se que lo has entendido, pero...)

-Definimos una variable que usaremos para el bucle For.
-Redimensionamos el array para que tenga 11 elementos, de cero a diez.
-Hacemos un bucle para que se repita desde 0 a 10.
-En cada ciclo del bucle, asignamos un valor al elemento i (usando la variable contadora del bucle) del array mDatos y
-lanzamos el evento NuevoDato en cuyo parámetro indicamos el contenido del elemento que acabamos de asignar.
-Continuamos repitiendo el bucle hasta que estén asignados los once elementos.
-Por último, lanzamos el evento DatosCreados, en cuyo parámetro indicamos el valor once.

Para poder usar esta clase desde el formulario creado en el proyecto, al cual añadiremos un CommandButton al que llamaremos crearDatosCmd y un ListBox llamado List1, también definiremos la clase usando WithEvents, crearemos una nueva instancia en el evento Load del formulario y al pulsar en ese botón, llamaremos al método CrearDatos.
Debido a que la variable estará definida con WithEvents, tendremos que escribir el código en cada uno de los dos eventos para poder comprobar que todo esto que te estoy contando realmente funciona. En uno de ellos, el que se produce al añadir un nuevo elemento al array, haremos que el parámetro indicado en el evento se añada al ListBox y cuando se produzca el evento DatosCreados, mostraremos un mensaje que nos avise de cuantos elementos se han creado, para mostrar ese mensaje usaremos la instrucción MsgBox.

¿Te atreves a codificar todo esto que te acabo de decir?

No me digas que no, que me enfado...

Bueno, vale... te doy unas pistas:

Nota:
Si lo vas a hacer por tu cuenta y no quieres pistas, no leas lo que sigue... aunque dependiendo de la resolución de tu monitor, es posible que veas el resto del código... así que intentaré dejar unas cuantas líneas en blanco para que no puedas ver el código y demás pistas...
Pero me gustaría que lo intentaras antes de ver la solución...
 

Vale, la solución te la muestro en una página aparte... así no tendrás la excusa de que lo has visto sin querer...

 

 

Ampliar un formulario con eventos personalizados

Como ya te he comentado en otras ocasiones, los formularios realmente son clases, con un tratamiento especial, pero clases al fin y al cabo. Y como hemos podido comprobar, podemos definir nuestros propios eventos en las clases, por tanto, si la lógica y los silogismos no fallan, podemos definir eventos en los formularios.

La forma de declarar nuevos eventos en un formulario sería usando Event y el nombre del evento, es decir, de la misma forma que lo haríamos en cualquier otra clase.
Para poder usar este "formulario ampliado", tendríamos que declararlo también con WithEvents.

Como sabemos, Visual Basic nos permite usar los formularios sin necesidad de crear una variable que los referencie, ya que, de forma oculta crea una variable con el nombre que le hemos dado en tiempo de diseño. Por ejemplo, si en nuestro proyecto tenemos dos formularios y uno de ellos se llama Form2, podemos acceder a ese formulario usando ese nombre. Pero también vimos en la entrega anterior que podemos definir una variable del tipo de un formulario y acceder a dicho formulario por medio de esa variable. Usando este sistema es la forma en la que podemos aprovecharnos de las características "ampliables" de los formularios.

Una vez que declaramos un evento en un formulario, cuando usamos ese formulario con WithEvents, los únicos eventos a los que podremos acceder serán los que estén declarados de forma explícita.
Sin embargo, si declaramos una variable de tipo genérico Form con la instrucción WithEvents, podremos usar los eventos de dicho formulario.

Comprobemos que todo esto es cierto.
Para ello, crearemos un nuevo proyecto al que añadiremos un segundo formulario.
El primer formulario (Form1) tendrá un botón (mostrarForm2Cmd) y una etiqueta (Label1).
El segundo formulario (Form2) sólo tendrá un botón (lanzarEventoCmd).

Veamos primero el código del segundo formulario, ya que es algo más simple:

Option Explicit

Event Prueba(ByVal mensaje As String)

Private Sub lanzarEventoCmd_Click()
    RaiseEvent Prueba("El evento prueba")
End Sub

Como puedes comprobar, declaramos un evento llamado Prueba que tiene un parámetro de tipo String.
Cuando se pulse en el botón de ese formulario, se lanzará el evento Prueba.

Ahora veamos el código del formulario principal (Form1).

Option Explicit

Private WithEvents mForm As Form
Private WithEvents mForm2 As Form2

Private Sub Form_Load()
    ' creamos una nueva instancia
    Set mForm2 = New Form2
    ' asignamos a la variable mForm una referencia al objeto recién creado
    Set mForm = mForm2
End Sub

Private Sub mForm_Click()
    ' Este evento se producirá al hacer una pulsación en el Form2
    Label1 = "Evento desde Form2: Form_Click"
End Sub

Private Sub mForm_Load()
    ' Esto evento se producirá al cargarse el Form2
    Label1 = "Evento desde Form2: Load"
End Sub

Private Sub mForm2_Prueba(ByVal mensaje As String)
    ' Este evento se producirá desde el Form2
    Label1 = "Evento desde Form2: " & mensaje
End Sub

Private Sub mostrarForm2Cmd_Click()
    ' mostrar el nuevo formulario a la derecha del principal
    mForm2.Move Me.Left + Me.Width + 60, Me.Top
    ' mostrar el formulario
    mForm2.Show
End Sub

Como te he comentado antes, si declaramos con WithEvents una variable del tipo específico Form2, sólo podremos acceder a los eventos que nosotros hayamos definido, por esa razón he declarado otra variable con WithEvents: mForm que permitirá acceder a los eventos "genéricos" del formulario, en este caso sólo interceptamos dos, pero igualmente podríamos acceder al resto.
En el evento Load de este formulario asignamos a la variable mForm2 una nueva instancia del formulario Form2 y a continuación asignamos también el objeto apuntado por esa variable a la variable mForm de forma que tanto una como la otra variable estarán apuntando al formulario Form2.
No te extrañe que se pueda realizar esa asignación, ya que esto es polimorfismo... ¿recuerdas? La clase Form2 es en realidad un formulario (del tipo Form), por tanto estamos asignando a la variable mForm la parte del Form2 que es del tipo Form, es decir todo excepto el evento que nosotros hemos definido.
Cuando se produzcan los eventos Load y Click del formulario Form2, se producirán los dos eventos interceptados con la variable mForm, los cuales mostrarán este hecho en la etiqueta Label1.
Por otro lado, cuando se produzca el evento Prueba, se interceptará por medio del evento mForm2_Prueba.
Por último, cuando se pulse en el botón para mostrar el formulario Form2, éste se posicionará a la derecha del formulario principal y a continuación se mostrará.

Cuando ejecutes el proyecto, podrás comprobar que al mostrarse por primera vez el formulario Form2, en la etiqueta se mostrará el mensaje de que se ha producido el evento Load, ese mensaje sólo se mostrará la primera vez que pulsemos en dicho botón, o cada vez que pulses en dicho botón y el segundo formulario no esté cargado en la memoria. Esto último puedes comprobarlo cerrando el segundo formulario y volviendo a pulsar en el botón.

Si añades este código al Form1, se mostrará un mensaje cuando se cierre el segundo formulario, así podrás comprobar mejor eso que te acabo de comentar.

Private Sub mForm_Unload(Cancel As Integer)
    ' Este mensaje se mostrará al cerrar el segundo formulario
    Label1 = "El segundo formulario se ha descargado."
End Sub

Si además pulsas en el segundo formulario se producirá el evento Click y se mostrará el mensaje correspondiente, lo mismo ocurrirá cuando pulses en el botón, aunque en ese caso el evento que se producirá será el que hemos definido.

 

Espero que con todo esto que te he comentado tengas más claro cómo definir y lanzar eventos en las clases, además de saber cómo poder interceptar esos eventos desde otras partes del proyecto.

En la próxima entrega veremos cómo definir una clase que amplíe el funcionamiento de un control. De esa forma tendrás la posibilidad de ampliar el funcionamiento de los controles y adaptarlos a tus necesidades.

Nos vemos
Guillermo
 

Aquí tienes el fichero zip con el código usado en esta entrega:  basico45_cod.zip 4.97 KB


 
entrega anterior ir al índice siguiente entrega

Ir al índice principal del Guille