Objetos en Visual Basic

 Y tú... ¿Qué coleccionas?

 

Fecha: 15/Ago/98
por Guillermo "guille" Som
Publicado originalmente en Algoritmo en Marzo/Abril del 98


Después de romper, arrugar y tirar, como en las viejas películas, un montón de papeles a la papelera... la de verdad, no la virtual de Windows... me decidí por hacer un artículo eminentemente práctico, sin olvidar las buenas costumbres ni la parte técnica/teórica, aunque eso de la técnica nunca ha sido mi predilección, no es que no me guste... cuando hago fotografías, el conocimiento de la técnica fotográfica me ayuda a mejorar la parte artística... pero aquí no estamos hablando de fotografía... así que, volvamos a la programación con el Visual Basic. Las cosas que escribo sobre el VB, me gustan que sean cosas prácticas, o al menos entendibles y, sobre todo, que puedan ponerse en acción y ser utilizadas en cualquier aplicación; lo que ocurre muchas veces es que al intentar hacer cosas sencillas o entendibles para la mayoría, al menos para los menos iniciados, uno se pierde con tanta simpleza... cayendo en lo que algunos llaman tutoriales, bueno, sí, es posible... ya que lo que se pretende es enseñar cómo hacerlo... pero en ocasiones los que no sabemos todo o para los que no tenemos todo tan claro como quisiéramos, nos viene bien que alguien nos explique la parte práctica de algo que, en la mayoría de las ocasiones, sólo se toca de forma técnica o teórica...

Bueno, dejemos todo esto y pasemos a lo que realmente interesa.

Este artículo tratará de las colecciones de objetos en Visual Basic, pero no sólo me centraré en decir que es una colección o cómo hacerla para que sea más consistente o "anti-soplidos de lobos"... incluso puede que lo consiga, ¿quién sabe?

Lo que es seguro es que voy a tratar el tema de las colecciones de clases, incluso veremos, aunque sea por encima, cómo conseguir una jerarquía de objetos, eso sí, sin gráficos explicando cómo está relacionada esa jerarquía... los gráficos os lo dejo a vosotros...

También veremos cómo conseguir que, salvo cuando sea necesario, unos formularios sean totalmente reutilizables, para poder usarlos, siempre en temas relacionados con el que se trata. Otra técnica que trataré, será la de crear formularios auto adaptables a los controles que se incluyan, ya verás a lo que me refiero en cuanto tratemos la aplicación de ejemplo.

Parte del código que voy a utilizar es algo que he estado usando en muchas de mis aplicaciones de bases de datos. Siempre he intentado reutilizar el código que escribo, algunas veces realizando pequeños cambios y en otras prácticamente sin nada que cambiar. Pero siempre que se hacen cosas genéricas, nos encontramos que de una forma u otra dependen de algo... esto es, en ocasiones, casi necesario u obligatorio... ¿seguro? Bueno, ya veremos si podemos codificar cosas que nos puedan servir en otras aplicaciones sin nada que cambiar... o casi...

Como comentaba al principio del párrafo anterior, cuando he trabajado con bases de datos, he necesitado personalizar algunas características de los campos de una tabla; antes de conocer, (o mejor dicho, antes de que existiesen), las clases en Visual Basic, lo solucionaba con tipos definidos y un array en el que incluir cada uno de los campos de la tabla. La verdad es que no me costó mucho cambiar el array de tipos definidos por una colección de objetos; por suerte, salvando las distancias, la forma de acceder a los elementos de un tipo definido y las propiedades de un objeto es prácticamente la misma.

Te voy a explicar un poco de que va todo esto: cuando creamos la estructura de una tabla de una base de datos, solemos nombrar, por ejemplo, los campos de la tabla con nombres más o menos descriptivos, incluso con nombres que podemos usar directamente como etiquetas descriptivas del campo que representa; como ya sabes estos nombres pueden contener espacios, acentos, etc., lo cual no nos impide que sean lo suficientemente descriptivos como para poder usarlos en etiquetas y en otros sitios visuales que necesitemos.

Pero pueden existir ocasiones en la que no nos interese mostrar el nombre interno del campo.

Se me ocurren, así a botepronto, dos posibles situaciones:

La primera para un uso en un idioma distinto, no es necesario crear una aplicación a nivel internacional, aquí mismo en España tenemos varios idiomas, aunque también podría ser una de las aplicaciones.

La segunda sería para usar la tabla en una situación diferente para la que en un principio fue pensada, puede ser que el usuario se sienta más cómodo visualizando un nombre diferente al que se asignó internamente a un campo determinado.

También podíamos necesitar campos adicionales a los que las tablas de una base de datos nos ofrece. Por ejemplo: podríamos usar un campo para almacenar el contenido del último registro modificado, con la intención de usarlo para deshacer una modificación o para pegarlo en otro registro.

Otra cosa que me gusta es tener definido un campo que representa la longitud en caracteres que quiero que muestre al efectuar un listado o la justificación a la hora de mostrarlo, no siempre quiero que todos los campos numéricos los justifique a la derecha y como esto, cualquier otra cosa que pueda necesitar.

Si creamos una clase que manejase todo esto, mejor aún, sobre todo si es lo autosuficiente como para hacer sus propias asignaciones, es decir: en principio sólo necesita saber que tabla usar para preparar la estructura de cada uno de los campos y, por supuesto, de cuantos campos se compone dicha tabla.

Para empezar a preparar esta clase que actuará a su vez como contenedora de una colección, necesitamos otra clase más básica que es la que contendrá las propiedades de cada uno de los campos de la tabla.

Además de nuestras propiedades, esta clase tendrá algunas de las que ya se incluyen "de fábrica" en los campos de una tabla: el nombre real, el tipo de datos que contiene y la longitud de ese campo.

Por tanto nuestra clase básica tendría estas propiedades:

Nombre   El nombre real del campo, es decir el definido al crear la tabla (Name del campo)
Tamaño   Tamaño original del campo (Size del campo)
Tipo   El tipo de datos que contiene (Type del campo)
Etiqueta   El nombre a mostrar, inicialmente será el nombre original del campo
Contenido   Para almacenar el contenido "copiado" del campo
Alineación   La alineación o justificación que se le dará al mostrarlo
LongMostrar   La longitud deseada a la hora de mostrar los datos
Mostrar   Si queremos que este campo se muestre

A estas propiedades podemos añadirles otras, la cuestión es que se adapte a nuestras necesidades.

Como ya te he comentado, ésta es la clase básica para representar a cada uno de los campos de nuestra tabla y formarán parte de una colección de campos.

Por tanto necesitaremos otra clase que sea una colección; para ello implementaremos en un módulo de clase el interface IColeccion (ver Algoritmo de Noviembre 1997)

¿Por qué hacer esta implementación y no codificarla directamente?

Por la sencilla razón de que así la haremos más segura, aunque también un poco más tediosa de manejar.

Cuando se implementa una interface, las propiedades y métodos se suelen dejar privados, de esta forma evitamos el acceso directo a ellos desde cualquier parte de nuestra aplicación. La única forma de hacerlo será mediante otro objeto del mismo tipo que la interface implementada. Esta seguridad viene porque lo que se le asigna a ese objeto intermedio es un puntero a la clase real, así nos aseguramos de que no podrá destruirse por una asignación errónea o maliciosa, según se mire.

¿Cómo funciona esto?

Una vez que se prueba, es fácil de entender... aunque me vas a permitir que abunde un poco más en este tema, ya que considero, desde mi perspectiva de cateto del VB, que esto de las implementaciones de interfaces no está demasiado bien explicado en los manuales, al menos para gente de miras estrechas, (o duras de mollera), como yo...

La recomendación del uso de interfaces es para que el Visual Basic sepa, antes de compilar, que propiedades y métodos soporta esa clase; de esta forma cuando se acceda a un objeto de ese tipo, el VB no pierde el tiempo comprobando si soporta la propiedad o método al que se quiere acceder, por tanto el resultado será una compilación más rápida y el acceso no tendrá ningún tipo de sobrecarga añadida... Si no te enteras, no te preocupes demasiado, ya veremos los ejemplos y casi con toda seguridad te quedará claro... sobre todo cuando vuelvas a leerte el manual...

Pero antes de ver un ejemplo, vamos a seguir con las clases que necesitaremos para llegar a completar nuestra jerarquía de objetos.

Hasta ahora tenemos una clase con las propiedades de un campo, también otra que contendrá todos los campos de la tabla, (con las propiedades y métodos habituales de cualquier colección), la siguiente clase será la que contenga las propiedades y métodos para manipular esos campos. Esta clase será la que represente a la tabla que queremos manipular, en principio tendrá estas propiedades y métodos:

 

Nombre   El nombre real de la tabla.
Base   Una referencia a la base de datos.
Campos   Referencia a la colección de campos.
Inicializar   Este método se encargará de crear y asignar los valores por defecto de los campos que formarán parte de lacolección.
CopiarContenidos   Copiará en la propiedad Contenido el contenido del registro actual.
PegarContenidos   Pegará lo que haya en la propiedad Contenido lo que haya en el registro actual.
MostrarContenidos   Este método asignará al array que se le pase lo que haya en la propiedad Contenido de la colección.

Además de estos, podemos añadirle algunos otros que nos permita manipular el contenido de los campos de la tabla. Por ejemplo, asignar a la propiedad contenido de la colección de campos del registro actual el contenido de estos o recuperar ese contenido y asignarlo a los campos del registro actual. En estos dos casos, los campos de tipo contador no se deberán actualizar, ya que no es conveniente que se modifiquen directamente por nuestra aplicación, si no queremos provocar una pequeña catástrofe o un error...

Pero eso, como en otras ocasiones te recomiendo será si lo crees conveniente.

Una vez que tenemos las clases que necesitaremos, vamos a ver la forma en que codificaremos cada una de las propiedades y métodos de las mismas.

La clase básica, a la que llamaré cCampo, no tiene demasiada complicación, sólo contendrá propiedades y aunque éstas pueden declararse como variables públicas, las voy a declarar como propiedades, ya que además de ser una forma más elegante de hacerlo, podremos insertar cualquier tipo de comprobación que creamos conveniente. También lo voy a hacer, porque estoy un poco harto de ver que esto es lo que se recomienda en muchos manuales y libros, pero casi nunca se hace... y para más INRI casi nunca en los listados de ejemplo, se ponen en práctica...

A mi particularmente me gusta tener, en las clases que formarán parte de una colección, una propiedad llamada ID, ésta será la que servirá de clave en esa colección y por tanto debe ser única, como esta clase representará a un campo de una tabla, el nombre interno nos servirá, ya que no se pueden tener nombres de campos repetidos en la misma tabla.

Pero hay que asegurarse que esa asignación no se cambie, para ello bastará con tener una variable estática que tenga en cuenta que ya ha sido asignada esta propiedad.

Veamos el código de esta propiedad:

Cuando usamos procedimientos de propiedades en lugar de usar variables públicas, se suelen tener unas variables que serán las que contengan esos valores para que al recuperarlo mediante la propiedad GET, se sepa que valor debe devolver, en este caso esa variable es m_ID, veamos cómo se usa para devolver el valor de la propiedad ID:

 

Para el resto de propiedades haremos lo mismo, estas son las variables a nivel de módulo:

 

Fíjate que algunas de nuestras propiedades tendrán valores especiales, como son el tipo y la alineación, en estos casos, sólo podremos hacerlo así con versiones superiores al VB4, en caso de que intentes codificar con esa versión deberás cambiarlo al tipo adecuado, por ejemplo: LONG.

Hay veces en las que nos puede interesar que el Visual Basic se encargue de comprobar que el tipo asignado es el adecuado, de no ser así dará un error y detendrá el programa, salvo que hayamos puesto detección de errores para que esto no ocurra.

Otra forma sería permitir que se pueda asignar cualquier valor y hacer nuestras propias comprobaciones dentro de cada asignación de propiedad.

Para ello los datos recibidos para la asignación tendrán que ser Variants y nosotros nos encargaremos de convertirla al tipo que proceda. El inconveniente es que tendremos variables (propiedades) que ocuparán más memoria de lo necesario, pero... es cuestión de gustos... en el código final no lo voy a hacer de esta forma, pero aquí te muestro cómo se podría implementar en el caso de LongMostrar:

 

Aquí notarás que también uso unos valores por defecto, éstos son los que quiero que se asignen inicialmente a las propiedades, para curarme en salud, estos valores habrá que asignarlos cuando se inicie la clase:

 

Estos valores por defecto son constantes declaradas a nivel de módulo:

 

Una atención especial sería para el contenido, ya que es muy posible que sea cualquier cosa y para ello no sólo debemos tener la propiedad LET, sino que necesitaremos también SET, puesto que puede contener cualquier cosa... El problema viene cuando queremos devolver ese contenido con GET. En esta situación he intentado ser lo más genérico posible y poder evitar el error de que no hemos devuelto el tipo correcto, si lo dudas, prueba a asignar una imagen a Contenido y después recupérala, si es que te deja, claro...

Para solventar, en la medida de lo posible esta situación he hecho lo siguiente con esta propiedad de la clase básica:

 

En este caso he hecho una comprobación del tipo de datos que tenemos guardado y según ese tipo, usamos una simple asignación o la asignamos con SET propiedad = variable, en caso de que no se adapte a todos los tipos que podamos manipular, siempre habrá tiempo de arreglar estas comprobaciones...

Con las cadenas podríamos hacer otro tipo de comprobaciones y es que si el tamaño asignado es mayor que el que nosotros queremos, se recorte... esto con un simple Left$ se arregla.

Por ejemplo si sabemos que el nombre no puede contener más de 255 caracteres podríamos hacer algo como esto:

 

Un caso especial son las propiedades de tipo Boolean, estas deberían recibir sólo valores True o False, pero, al menos a mi me ha ocurrido, cuando trabajamos con idiomas diferentes del Visual Basic, puede que el valor True se convierta en Verdadero... Sí, ya sé que es lo mismo, pero en ocasiones, al recuperar este valor de un fichero, incluso del registro del Windows, me he topado con un Type Mismatch como un castillo de grande...

Si ese no es tu caso, no necesitas hacer comprobaciones extras, pero si te ha ocurrido o no quieres que te ocurra, podrías hacer esto:

 

Como comprobarás, el valor debe ser Variant, ya que de no ser así, el error se producirá antes de llegar a esta propiedad, porque el VB comprueba antes de asignar el valor...

Si no lo crees, no necesitas siquiera tener el VB versión inglesa para comprobarlo, prueba esto, pero deja la propiedad Mostrar como Boolean:

Dim i&
Dim sTmp$
i = FreeFile
Open "C:\Prueba.txt" For Output As i
Print #i, True
Close i
i = FreeFile
Open "C:\Prueba.txt" For Input As i
Input #i, sTmp
Close i
tCampos(1).Mostrar = sTmp

 

Vale, de acuerdo, sTmp es un string y por eso da error, pero en el caso de que ese valor lo guardaras y recuperaras con las APIs de recuperar y escribir en un INI... ya verías que no funciona ni siquiera el CBool(sTmp).

Dejemos esto, porque parece ser que sólo me ocurre a mi... pero me ha dado un montón de quebraderos de cabeza... al cambiar de la versión inglesa del VB4 a la española del VB5.

 

La clase colección sólo será una clase que tendrá una colección interna en la que se guardarán los campos y tendrá implementada el interface IColeccion, veamos la parte de las declaraciones:

 

En el evento Class_Initialize crearemos la colección y la destruiremos en Class_Terminate:

 

Los métodos serán los que contiene IColeccion, pero en esta clase será privados, de forma que sólo se podrán acceder a ellos usando un objeto del tipo IColeccion. Veamos el código:

 

En la implementación es dónde debemos asignar que la propiedad Item sea la propiedad por defecto y que el método New_Enum esté oculto y con el id. de procedimiento a -4.

 

Ahora pasemos a ver el código de la última de las clases, la que representará a la tabla.

En la parte de las declaraciones tendremos esto:

 

Aquí se declaran las variables que usaremos para asignar las distintas propiedades, veamos esas propiedades:

 

En el caso de Base, se usa SET porque sólo se usará mediante una referencia, por otro lado Campos devolverá la clase/colección de campos.

Por último nos queda el método Inicializar, en él se asignarán los campos, y de paso otros valores necesarios para esta clase, también recorrerá las tablas incluidas en esa base para poder usar los campos de la tabla especificada, si no se ha asignado ningún nombre de tabla, se tomará como tabla origen la primera que se encuentre:

 

También esta clase dispone de unos métodos que se encargarán de copiar en la propiedad Contenido el contenido del registro actual así como volver a ponerlos en el registro actual, además de otro que se encargará de asignar a un array lo que tengamos en Contenido, este array puede ser de cualquier tipo, incluso objetos como TextBoxes, Labels, etc., siempre que esos objetos acepten en la propiedad por defecto esa clase de datos.

 

Bien, ya tenemos todo el código necesario para nuestras clases, ahora sólo queda hacer una pequeña aplicación que pueda usar todas estas clases.

Primero veremos cómo usarla de forma que no se asigne una tabla real, esto servirá para saber cómo acceder a la colección, así como para comprobar cómo se deben asignar los campos. Para ello crea un form y añade un par de Labels y un control ListBox, añade este código al Form_Load y verás cómo funciona.

 

Dim tCampo As cCampo
Dim tColeccion As IColeccion
Dim tTabla As cTabla
'Asignar los valores
Set tTabla = New cTabla
'Asignarlos
With tTabla
    .Nombre = "Prueba"
    .Campos("uno") = "Campo Uno"
    'Se puede asignar de cualquiera de estas dos formas
    .Campos("dos") = "Campo Dos"
    '.Campos(2) = "Campo Dos"
    'También se puede especificar la propiedad a asignar:
    .Campos("tres").Nombre = "Campo Tres"
End With
'Mostrarlos
With tTabla
    'Si se intenta borrar la colección,
    'dará error en tiempo de compilación
    'Set .Campos = Nothing
    
    Set tColeccion = .Campos
    
    Label1(0) = "Nombre de la tabla: " & .Nombre
    Label1(1) = "Hay " & tColeccion.Count & " campos"

    For Each tCampo In tColeccion
        List1.AddItem tCampo
    Next
    'Por supuesto que haciendo un bucle "tradicional"...
    Dim i&
    List1.AddItem "--- ... ---"
    For i = 1 To tColeccion.Count
        List1.AddItem i & "- " & .Campos(i)
    Next
End With
Set tColeccion = Nothing
Set tCampo = Nothing
Set tTabla = Nothing

 

Fíjate la forma de acceder a la colección, para ello, he declarado una variable del tipo IColeccion. Después he asignado a esa variable el valor devuelto por la propiedad Campos de la tabla y desde ahí ya podemos acceder a cada uno de los campos, también puedes comprobar que no podemos asignar un valor Nothing a esa propiedad, con lo cual nuestra colección estará medianamente protegida...

Pero este tipo de ejemplos resultan un poco insulsos, ya que no son realmente útiles, así que veamos una aplicación algo más útil, que además tiene un par de cosillas realmente interesantes.

Por ejemplo, un formulario que se adapte a los controles que queramos, esto resulta útil cuando queremos que la aplicación se adapte a cualquier base de datos o a cualquier tabla, sin importarnos el número de campos que tenga.

Esto mismo lo pondremos en actuación en otro formulario que nos servirá para asignar las etiquetas visibles de nuestra tabla.

Este es el aspecto del formulario principal de ejemplo:

 

No te preocupes por los colores, los he usado para ver por dónde iba cada uno de los controles.

El que está de color blanco debe ser un Picture, el cyan y el gris grande puede ser un Picture o un Frame, la cuestión es que sea un control que pueda actuar de contenedor de otros controles.

El Label y el TextBox deben ser los elementos cero de un array, ya que se usarán para mostrar la etiqueta de cada campo y el contenido de los mismos. El propio programa se encargará de ajustar el tamaño de los TextBoxes, por si estos contuvieran campos más grandes de la cuenta, de todas formas deberás poner la propiedad Multiline a True, en el evento KeyPress se tendrá esto en cuenta para que al pulsar Intro no se desplacen los que sólo contengan una línea.

Veamos ahora el código necesario:

Este código debes ponerlo en la parte de las declaraciones:

 

Creo que voy a dejar las imágenes, ya que es un poco engorroso y te mostraré el código directamente.

 

'
Private Sub Form_Load()
    '---Inicio de los controles de scroll virtual---
    '----------------------------------------------------------
    'El contenedor virtual (picScroll1)
    '   será el contenedor de:
    'El contenedor visual (picScroll2)
    '   que a su vez contendrá los controles 'visuales' de
    '   nuestro formulario
    '----------------------------------------------------------
    '
    'Es importante que el tamaño del picScroll2 sea el tamaño
    'máximo que queramos que tenga el formulario
    '
    'El contenedor visual debe estar contenido en el virtual
    Set picScroll2.Container = picScroll1
    
    'Diferencia entre el ScaleWidth con el Width
    difSW = ScaleWidth - Width
    'Diferencia entre el ScaleHeight con el Height
    difSH = ScaleHeight - Height
    
    'Para que no se vea diferente el picScroll1
    'en el entorno lo dejo en blanco para que se diferencie
    picScroll1.BackColor = picScroll2.BackColor
    picTool.BackColor = picScroll2.BackColor
    
    'Asegurarnos de que no tienen bordes
    picScroll1.BorderStyle = vbBSNone
    picScroll2.BorderStyle = vbBSNone
    
    'Ajustar el contenedor de los botones al tamaño adecuado
    picTool.Width = picScroll2.ScaleWidth
    
    HScroll1.Left = 0
    VScroll1.Top = 0
    VScroll1.SmallChange = 120 '10 en Pixels
    VScroll1.LargeChange = 360 '60 en Pixels
    HScroll1.SmallChange = 120
    HScroll1.LargeChange = 360
    
    '---Fin de las asignaciones para el Scroll virtual---
    
    Dim tCampo As cCampo
    Dim i&
    
    'Asignar los valores
    
    Set m_cTabla = New cTabla
    
    'En caso de que queramos usar unDataControl
    'Data1.DatabaseName = App.Path & "\Objetos2.mdb"
    
    Set m_Db = OpenDatabase(App.Path & "\Objetos2.mdb")
    
    'Asignarlos
    With m_cTabla
        .Inicializar m_Db
        'Data1.RecordSource = "select * from [" & .Nombre & "]"
        Set m_Rs = m_Db.OpenRecordset("select * from [" & .Nombre & "]", dbOpenSnapshot)
        
        'Copiar el contenido
        On Local Error Resume Next
        m_Rs.MoveFirst
        If Err = 0 Then
            .CopiarContenidos m_Rs
        End If
        On Local Error GoTo 0
    End With
    
    'Mostrarlos
    With m_cTabla
    
        Set tCampos = .Campos
        
        i = 0
        For Each tCampo In tCampos
            If i > 0 Then
                Load Label1(i)
                Load Text1(i)
                Text1(i).Top = Text1(i - 1).Top + Text1(i).Height + 60
                Text1(i).TabIndex = Text1(i - 1).TabIndex + 1
            End If
            With Text1(i)
                Label1(i).Visible = True
                Label1(i).Caption = tCampo.Etiqueta
                Label1(i).Top = .Top + 15
                'Si estamos usando un control Data...
                '.DataField = tCampo.Nombre
                .MaxLength = tCampo.Tamaño
                'Ajustar el tamaño del Text...
                If tCampo.Tipo = dbMemo Then
                    .Height = .Height * 5
                    .MaxLength = 0
                End If
                .Visible = True
                'Adaptar el tamaño del contenedor...
                If .Top + .Height + 120 > picScroll2.Height Then
                    picScroll2.Height = .Top + .Height + 120
                End If
            End With
            i = i + 1
        Next
        .MostrarContenidos Text1()
    End With
    
    Set tCampo = Nothing
End Sub

 

En este evento se preparan los controles para mostrar y asignar el tamaño automáticamente y también se asignan los valores de los campos, se lee el primer registro y se muestra en los TextBoxes.

Para controlar el tamaño de los contenedores virtuales debemos añadir este código al evento Resize del formulario:

 

Private Sub Form_Resize()
    Dim i&
    
    If WindowState <> vbMinimized Then
        
        'Esta comparación es para que no se cuelgue,
        'si se reduce "demasiado" el tamaño de la ventana.
        'Gracias a "David Sans" <[email protected]> por la corrección.
        If ScaleWidth - VScroll1.Width > 0 And ScaleHeight - HScroll1.Height > 0 Then
            ' Initialize location of both pictures.
            picScroll1.Move 0, 0, ScaleWidth - VScroll1.Width, ScaleHeight - HScroll1.Height
            picScroll2.Move 0, 0
        End If
        
        'Si el tamaño es mayor que el contenido...
        If Width - difSW > picScroll2.Width + VScroll1.Width Then
            Width = picScroll2.Width + VScroll1.Width - difSW
        End If
        'igual con el alto...
        If Height - difSH > picScroll2.Height + HScroll1.Height Then
            Height = picScroll2.Height + HScroll1.Height - difSH
        End If
        
        ' Position the horizontal scroll bar.
        HScroll1.Top = picScroll1.Height
        HScroll1.Width = picScroll1.Width
        ' Position the vertical scroll bar.
        VScroll1.Left = picScroll1.Width
        VScroll1.Height = picScroll1.Height
        ' Set the Max value for the scroll bars.
        HScroll1.Max = picScroll2.Width - picScroll1.Width
        VScroll1.Max = picScroll2.Height - picScroll1.Height
        ' Determine if child picture will fill up
        ' screen. If so, then there is no need to
        ' use scroll bars.
        VScroll1.Enabled = (picScroll1.Height < picScroll2.Height)
        HScroll1.Enabled = (picScroll1.Width < picScroll2.Width)
        
        'Líneas de separación de los botones
        For i = 0 To 1
            Line1(i).X1 = 0
            Line1(i).Y1 = picTool.Height - 15
            Line1(i).Y2 = Line1(i).Y1
            Line1(i).X2 = picTool.Width
        Next
    End If
End Sub

 

 

Este es el código que se ejecutará cuando finalicemos la aplicación:

 

Private Sub Form_Unload(Cancel As Integer)
    
    'Cerrarmos la base y quitamos las referencias
    m_Db.Close
    
    Set m_cTabla = Nothing
    Set m_Db = Nothing
    
    Set Form1 = Nothing
End Sub

 

Para manipular la parte visible, se encargan los controles VScroll y HScroll:

 

Private Sub HScroll1_Change()
    ' picScroll2.Left is set to the negative
    ' of the value because as you scroll the
    ' scroll bar to the right, the display
    ' should move to the Left, showing more
    ' of the right of the display, and vice-
    ' versa when scrolling to the left.
    picScroll2.Left = -HScroll1.Value
End Sub


Private Sub VScroll1_Change()
    ' picScroll2.Top is set to the negative of
    ' the value because as you scroll the
    ' scroll bar down, the display should
    ' move up, showing more of the bottom of
    ' the display, and vice-versa when
    ' scrolling up.
    picScroll2.Top = -VScroll1.Value
End Sub

 

Entre otras cosas, este sería el código para controlar que la línea del TextBox no desaparezca en caso de pulsar Intro:

 

Private Sub Text1_KeyPress(Index As Integer, KeyAscii As Integer)
    If tCampos(Text1(Index).DataField).Tipo <> dbMemo Then
        If KeyAscii = 13 Then KeyAscii = 0
    End If
End Sub

 

Para ello se comprueba si el tipo es Memo, lo cual nos permite saber que este campo puede tener más de una línea. Para conseguirlo, lo único que tenemos que hacer es asignar cero a KeyAscii cuando el tipo no sea Memo para que el control no reciba el retorno de carro.

 

Ya sólo nos queda lo que harán los botones cuando se pulse en ellos.

En el caso de Salir, sólo se hará una descarga del formulario y si queremos, un End.

En los otros dos se llamarán a los forms que se encarguen de mostrar los formularios que harán la consulta o que permitirán que cambiemos el contenido de la propiedad Etiqueta.

Aquí tienes el código:

 

Private Sub cmdConsulta_Click()
    With gsQBE
        Set .Tabla = m_cTabla
        .Show vbModal
        If MostrarBusqueda.Command1.Caption = "" Then
            Unload MostrarBusqueda
        Else
            MostrarBusqueda.Show
        End If
    End With
End Sub


Private Sub cmdModificar_Click()
    Dim i&
    Dim tCampo As cCampo
    
    With frmScroll
        Set .Tabla = m_cTabla
        .Show vbModal
        If Not .Cancelado Then
            'Mostrar los nuevos valores
            i = 0
            For Each tCampo In tCampos
               Label1(i) = tCampo.Etiqueta
               i = i + 1
            Next
        End If
    End With
    Unload frmScroll
    
    Set tCampo = Nothing
End Sub

 

Para terminar, ya que me estoy extendiendo más de la cuenta, vamos a ver el formulario que permite modificar las etiquetas y el código que se utiliza, ya que este form también maneja controles visuales para poder adaptarse al número de campos.

Este es el aspecto del form:

 

 

Tanto el Label como el TextBox son arrays cuyo índice inicial es cero.

Veamos el código, empezaremos por el que se incluye en la parte general de las declaraciones:

 

Option Explicit

Dim difSW As Long
Dim difSH As Long

Dim tCampos As IColeccion
Dim tCampo As cCampo

'Propiedades de este formulario
Public Cancelado As Boolean

 

En el Form_Load sólo prepararemos los controles que harán que se ajuste al tamaño que tenga posteriormente. Como comprobarás es prácticamente igual que el código usado en este mismo evento del form principal, salvo por las asignaciones del Container de los dos commands, esto lo hago para que estén dentro del contenedor virtual, para que también puedan verse en caso de que el form se haga demasiado grande o muy pequeño.

 

Private Sub Form_Load()
    '---Inicio de los controles de scroll virtual---
    '----------------------------------------------------------
    'El contenedor virtual (picScroll1)
    '   será el contenedor de:
    'El contenedor visual (picScroll2)
    '   que a su vez contendrá los controles 'visuales' de
    '   nuestro formulario
    '----------------------------------------------------------
    '
    'Es importante que el tamaño del picScroll2 sea el tamaño
    'máximo que queramos que tenga el formulario
    '
    'El contenedor visual debe estar contenido en el virtual
    Set picScroll2.Container = picScroll1
    Set cmdAceptar.Container = picScroll2
    Set cmdCancelar.Container = picScroll2
    
    'Diferencia entre el ScaleWidth con el Width
    difSW = ScaleWidth - Width
    'Diferencia entre el ScaleHeight con el Height
    difSH = ScaleHeight - Height
    
    'Para que no se vea diferente el picScroll1
    'en el entorno lo dejo en blanco para que se diferencie
    picScroll1.BackColor = picScroll2.BackColor
    
    'Asegurarnos de que no tienen bordes
    picScroll1.BorderStyle = vbBSNone
    picScroll2.BorderStyle = vbBSNone
    
    '---Fin de las asignaciones para el Scroll virtual---
    
End Sub

 

El evento Resize es un poco más corto que el del form principal, pero su finalidad es la misma:

 

Private Sub Form_Resize()
    
    On Local Error Resume Next
    
    If WindowState <> vbMinimized Then
        
        'Esta comparación es para que no se cuelgue,
        'si se reduce "demasiado" el tamaño de la ventana.
        'Gracias a "David Sans" <[email protected]> por la corrección.
        If ScaleWidth > 0 And ScaleHeight > 0 Then
            ' Initialize location of both pictures.
            picScroll1.Move 0, 0, ScaleWidth, ScaleHeight
            picScroll2.Move 0, 0
        End If
        
        'Si el tamaño es mayor que el contenido...
        If Width - difSW > picScroll2.Width Then
            Width = picScroll2.Width - difSW
        End If
        'igual con el alto...
        If Height - difSH > picScroll2.Height Then
            Height = picScroll2.Height - difSH
        End If
        
    End If
    
    Err = 0
End Sub

 

La creación de los controles necesarios para cada uno de los campos se hace al asignar la propiedad Tabla, esta propiedad sólo permite que se asigne una vez, esto lo he hecho para que algún desaprensivo no intente reasignarla, ya que daría error al intentar cargar en memoria de nuevo los controles ya cargados.

 

Public Property Set Tabla(ByVal tTabla As cTabla)
    'Al asignar la tabla se adpatan y cargan los controles
    'Por tanto sólo se puede asignar una vez
    Static TablaAsignada As Boolean
    Dim i&
    
    If Not TablaAsignada Then
        TablaAsignada = True
        
        With tTabla
        
            Set tCampos = .Campos
            Caption = "Modificar las etiquetas de: " & .Nombre
            
            i = 0
            For Each tCampo In tCampos
                If i > 0 Then
                    Load Label1(i)
                    Load Text1(i)
                    Text1(i).Top = Text1(i - 1).Top + Text1(i).Height + 60
                    Text1(i).TabIndex = Text1(i - 1).TabIndex + 1
                End If
                With Text1(i)
                    Label1(i).Visible = True
                    Label1(i).Caption = tCampo.Etiqueta
                    .Text = tCampo.Etiqueta
                    Label1(i).Top = .Top + 15
                    .Visible = True
                    'Adaptar el tamaño del contenedor...
                    If .Top + .Height + 120 > picScroll2.Height Then
                        picScroll2.Height = .Top + .Height + 120
                    End If
                End With
                i = i + 1
            Next
            'Posicionar los botones...
            picScroll2.Height = picScroll2.Height + cmdAceptar.Height * 2
            cmdAceptar.Top = picScroll2.ScaleHeight - cmdAceptar.Height - 120
            cmdCancelar.Top = cmdAceptar.Top
        End With
    End If
End Property

 

Para terminar veamos el código usado para asignar los nuevos valores de las etiquetas:

 

Private Sub cmdAceptar_Click()
    Dim i&
    
    Cancelado = False
    'Asignar los nombres nuevos
    i = 0
    For Each tCampo In tCampos
       tCampo.Etiqueta = Text1(i).Text
       i = i + 1
    Next
    Hide
End Sub

 

Y el del botón cancelar:

 

Private Sub cmdCancelar_Click()
    Cancelado = True
    Hide
End Sub

 

Es importante asignar adecuadamente el valor de la propiedad Cancelar, ya que esta se tiene en cuenta cuando se regresa de este form, fijate que es importante usar Hide en lugar de descargar el form, ya que si no se hiciera así se perdería la información de Cancelar.

 

No voy a incluir el código del formulario que hace la consulta, entre otras cosas porque no lo he depurado demasiado, sólo lo he adaptado de otro código que tenía, pero el código usado, tendrás ocasión de verlo si te bajas el código de ejemplo.

 

Bien, espero que todo este rollo te haya servido para comprender mejor cómo manejar y usar prácticamente las colecciones.

 

En un futuro artículo, veremos cómo crear una jerarquía de objetos que manipule todas las tablas de una base de datos. Mientras tanto, ve practicando, ya que confío en que no tendrás problemas para hacerlo.

 

También intentaré mejorar el código del formulario de consultas, ya que creo que puede ser bastante útil para realizar y mostrar cualquier tipo de consulta en cualquier tipo de tabla, e incluso con consultas en las que intervengan varias tablas, pero esto será en otra ocasión.

 

 

Nos vemos.
Guillermo


Pulsa aquí para bajarte los listados de ejemplo (objetos2_3cod.zip 24.2 KB)
Pulsa en este link, si quieres bajarte esta página con todos los gráficos (objetos2_3.zip 125 KB)


la Luna del Guille o... el Guille que está en la Luna... tanto monta...