Visual Basic Avanzado

Cómo producir un evento en un componente desde un módulo BAS

Y consejos para usar AddressOf desde el IDE de VB6

 

Publicado: 10/Ene/2003
Actualizado: 10/Ene/2003



En ocasiones nos encontraremos que tenemos un componente ActiveX (por ejemplo una librería DLL ActiveX) en la que por circunstancias necesitemos hacer algunas operaciones en un módulo BAS, por ejemplo, porque tengamos una función o procedimiento que esté subclasificado, (el cual siempre tendrá que estar en un módulo con extensión BAS); si necesitamos que se produzca un evento en una de las clases implementadas, tendremos problemas, ya que para "lanzar" un evento de una clase sólo podremos hacerlo desde la propia clase. Esto es así porque desde cualquier otra parte del proyecto no tendremos forma de poder hacerle saber que debe producir un evento, entre otras cosas porque para acceder a esa clase tendremos que crear una nueva instancia de la clase.

La solución más simple que he encontrado para subsanar este inconveniente es el que te voy a explicar aquí, es posible que exista otra solución, pero esta que te cuento es la que yo utilizo cuando me encuentro en esta tesitura.

Antes de entrar en detalles veamos un caso en el que necesitaríamos hacer esto.

Si no lo has experimentado ya en tus carnes, puede que te encuentres con un pequeño problema que desde la introducción de Visual Basic 6.0 tenemos los que de vez en cuando nos gusta llamar a funciones "subclasificadas", es decir funciones cuya dirección hemos asignado mediante la instrucción AddressOf, las cuales debemos probar desde el entorno de desarrollo (IDE). con la versión 5.0 de VB no había mucho problema, salvo que el programa se detuviera a causa de un error o porque entráramos en modo de depuración, en cuyo caso, era habitual encontrarse con un "cuelgue" del IDE. Pero con VB6, ni aún entrando en modo de depuración o por causa de un error: lo normal es que siempre falle cuando de por medio hay una llamada a un procedimiento subclasificado.

Hace poco estaba haciendo unas pruebas con las librerías de comprimir/descomprimir en formato ZIP. Esas librerías permiten indicar la dirección de unas funciones para poder saber, por ejemplo, qué es lo que se está haciendo, es decir para recibir los mensajes que las librerías de compresión envían cuando está comprimiendo o descomprimiendo. Si ese código se prueba desde el IDE de VB6, es seguro que el entorno "casque" y se termine a causa de un fallo de protección general. Pero si dicho código está contenido en una librería ActiveX, todo funcionará bien, por tanto si quieres probar cosas que incluyan subclasificación (o llamadas a procedimientos indicados con AddressOf), te recomiendo que el código lo incluyas en un componente ActiveX.

Nota:
Lo dicho en los dos párrafos anteriores no está directamente relacionado con el "truco" que te voy a comentar sobre cómo emitir eventos desde fuera de la clase que los produce, pero es para que entres en situación, además de que te sirva para solucionar dicho problema, si es que en un futuro se te presenta o ya se te ha presentado y no sabías cómo solucionarlo.

Mediante la subclasificación de una de las funciones de las librerías de compresión, podremos saber el estado actual de la compresión/descompresión que se está realizando: DLLPrnt o UZDLLPrnt respectivamente.

Nota:
No voy a entrar en detalles de cómo usar esas librerías, ya que eso puedes verlo en las dos colaboraciones de Eduardo González sobre cómo usar Zip32.dll y Unzip32.dll.

Si hemos encapsulado el código para comprimir/descomprimir en un componente ActiveX, tendremos una clase que es la que "expone" las funciones para hacer ese trabajo. También es muy posible, además de recomendable, que esa clase produzca un evento cuando se llame a cualquiera de los dos procedimientos antes mencionados, para que se le indique al usuario que hay algo que está ocurriendo, (que para eso son los eventos al fin y al cabo), de modo que no parezca que no está ocurriendo nada, sobre todo en los casos que el proceso se tome un tiempo considerable.

Veamos primero el código de cómo indicar a la librería de compresión, (que está en una librería DLL normal y corriente, no de automatización, que es el único tipo de librerías que podemos crear con Visual Basic), que debe llamar a una función mientras esté comprimiendo, también veremos la declaración de esa función, la cual "obligatoriamente" debe estar definida en un módulo del tipo BAS.

Para que se asigne el puntero de un procedimiento (o función) codificada con Visual Basic, debemos hacer algo como esto:

Dim ZUSER As ZIPUSERFUNCTIONS
...

ZUSER.ZDLLPrnt = FnPtr(AddressOf ZDLLPrnt)

...

En este caso declaramos una variable del tipo requerido y asignamos a uno de los miembros el "puntero" a la función que tenemos definida, para ello usamos una función intermedia FnPtr que, como veremos hace poca cosa, pero que devuelve un valor de tipo Long que es el que espera el campo ZDLLPrnt de ese tipo definido.

Este sería el código de esa función "intermedia" y lo que podríamos hacer cuando se ejecute esa función "subclasificada", la cual será llamada por la librería de compresión cuando deba indicar que está ocurriendo algo, en este ejemplo se usará como si se pudiera imprimir ese mensaje, aunque esto es sólo para que sepamos que lo que esté después del Print será el mensaje a mostrar.
Notarás que hay comentarios en inglés, esos son los que estaban en el código original de esa librería, y por "respeto" los he dejado tal cual estaban, ya que lo que aquí interesa no es lo que hace...

'-- Puts A Function Pointer In A Structure
'-- For Use With Callbacks...
Public Function FnPtr(ByVal lp As Long) As Long
    FnPtr = lp
End Function



'-- Callback For ZIP32.DLL - DLL Print Function
Public Function ZDLLPrnt(ByRef fname As ZipCBChar, ByVal x As Long) As Long
    Dim s0 As String
    Dim xx As Long

    '-- Always Put This In Callback Routines!
    On Error Resume Next

    s0 = ""

    '-- Get Zip32.DLL Message For processing
    For xx = 0 To x
        If fname.ch(xx) = 0 Then
            Exit For
        Else
            s0 = s0 & Chr(fname.ch(xx))
        End If
    Next

    '----------------------------------------------
    '-- This Is Where The DLL Passes Back Messages
    '-- To You! You Can Change The Message Printing
    '-- Below Here!
    '----------------------------------------------

    '-- Display Zip File Information
    If Asc(Right$(s0, 1)) = 10 Then
        zZipInfo = zZipInfo & Left$(s0, Len(s0) - 1) & vbCrLf
    Else
        zZipInfo = zZipInfo & s0 & " "
    End If

    '
    ' Este es el mensaje a mostrar al usuario
    '
    Print s0


    ZDLLPrnt = 0
End Function

Esta última función será llamada por la librería cuando tenga que emitir un mensaje del progreso de compresión.

Como ya te he comentado antes, este tipo de funciones subclasificadas (o de callback) deben estar definidas en un módulo BAS, por tanto no podemos usar la instrucción que "lanza" un evento declarado en una clase. Otra cosa sería que pudiéramos usar la instrucción AddressOf con funciones definidas en un módulo de clase, pero eso no es posible hacerlo en las versiones de Visual Basic "clásico", cosa que ha cambiado en la versión .NET de VB, pero ese no es el caso que nos atañe.

Veamos parte del código de la clase que expondría la funcionalidad de comprimir/descomprimir de este "hipotético" componente.
Para que la clase se pueda declarar con WithEvents para interceptar eventos, debemos tener definido un evento y, por supuesto, tenemos que tener una forma de llamar a dicho evento.
Veamos el código de la declaración de un evento:

Public Event ZipInfo(ByVal Msg As String)

Si la función subclasificada estuviese definida en la propia clase, podríamos sustituir la línea Print s0 por algo como esto: RaiseEvent ZipInfo(s0)
Pero como sabemos, esto no es posible.
Para solventar este "problemilla", vamos a crear un procedimiento en la clase, el cual definiremos como Friend para que no sea accesible fuera del proyecto, por tanto sólo será accesible desde cualquier módulo de la librería (o componente), pero para poder acceder a dicho procedimiento, se tendrá que crear una instancia de la clase para poder llamarlo, ya que sólo podemos llamar a procedimientos (métodos y propiedades) de una clase desde una variable que "apunte" a un objeto "creado" en la memoria.

Este sería el código de dicho procedimiento, el cual estaría declarado en la clase, ya que es la única forma de producir el evento:

Friend Sub OnZipInfo(ByVal Msg As String)
    RaiseEvent ZipInfo(Msg)
End Sub

Sabiendo que para producir el evento desde fuera de la clase, deberíamos llamar a ese procedimiento desde una "instancia" de la clase, (una variable creada de ese tipo), podemos hacer lo siguiente:
Creamos una variable pública del mismo tipo que la clase, pero declarada en un módulo BAS, (podemos usar el mismo módulo en el que está el procedimiento subclasificado o en cualquier otro módulo BAS), de esta forma, esa variable, al estar declarada en un módulo BAS, sólo será visible dentro del proyecto, incluso dentro del código de la propia clase, por tanto, cuando se cree un nuevo objeto de la clase, podemos asignar a esa variable esa nueva instancia creada...
¿Eso es posible? y si es posible ¿cómo se hace?

Empecemos declarando una variable en el módulo BAS del mismo tipo que la clase, para este ejemplo, supongamos que la clase se llama ZIPAX, por tanto podemos hacer esto:
Public mZipAX As ZIPAX

Y en el evento Class_Initialize de la clase ZIPAX asignaríamos a mZipAX la propia clase:
Set mZipAX = Me

Por tanto ahora podemos llamar al método OnZipInfo desde cualquier parte del proyecto usando la variable mZipAX. Así que si sustituimos el Print s0 por esto otro: mZipAX.OnZipInfo s0, conseguiremos lo que estábamos buscando: producir el evento desde un módulo BAS.

Sólo hay que tener una precaución: si se crean varias instancias de esa clase, la variable mZipAX sólo apuntará a la última instancia que se haya creado... por tanto, este "truco" sólo funcionará si no se crean varias instancias de la clase... aunque si ese fuese el uso normal de este componente, podríamos solventarlo llevando la cuenta de cuantas instancias se han creado y declarando mZipAX como un array... pero eso sería tema de otro artículo.

 

¿Un ejemplo de todo esto?
Por ahora no, pero en breve, "posiblemente", publicaré el código de esa utilidad para comprimir que utiliza un componente ActiveX, entonces tendrás la oportunidad de ver esto en funcionamiento.
Aunque, debido a que el tema tratado es "avanzado", deberías saber hacerlo... ¿verdad?

Pues eso...

 

Nos vemos.
Guillermo


Volver a Visual Basic Avanzado

ir al índice