Trucos Personalizar el DbGrid   (3 artículos )

Las preguntas más frecuentes en los foros Visual Basic relacionadas a la DBGrid estándar generalmente se orientan a su configuración y personalización.
En verdad DBGrid resulta un poco hostil a la hora de hacer lo que uno quiera, y a todos nos ha complicado alguna una vez.
Advierto que la intensión de este articulo no es enseñar a manejar el Control DBGrid, pues es hace parte de la documentación de Visual Basic.

La siguiente colección de trucos sirven en gran medida para que la DBGrid que suministra Visual Basic, tenga un aspecto más profesional, al
estilo de los formularios especializados de Access : Títulos en negrilla y ¡ centrados !, Registro 1 de 6 y Formato personalizado.

1. TITULOS EN NEGRILLA Y CENTRADOS

Primero que todo, DBGrid no alinea celdas individuales, solo por columnas. Es necesario ingeniarcelas para lograr hacer esto.
Mi truco se basa en adicionar espacios al lado izquierdo del texto de la celda, claro que el asunto no es tan simple como una sencilla formula, dado que la presentación sugiere fuentes escalables. El algoritmo se basa en una aproximación de ensayo y error y es bastante preciso:

     Public Sub CenterHeadColumn(f As Form, TargetColumn As Column)
         Dim TextWidth, m As Single, Hole As String

         With TargetColumn
             TextWidth = f.TextWidth(.Caption)

             If TargetColumn.Width > TextWidth Then
                m = (.Width - TextWidth) / 2
                Do
                   Hole = Hole + SP
                Loop Until f.TextWidth(Hole) >= m
                If .Alignment = dbgLeft Then
                   .Caption = Hole + .Caption
                   .AllowSizing = False
                ElseIf .Alignment = dbgRight Then
                   .Caption = .Caption + Space(Len(Hole) - 1)
                   .AllowSizing = False
                End If
             End If
         End With
     End Sub

Nótese el parámetro f como Form, esto es requerido para aplicar la función TextWidth, así, esto sugiere que la Fuente del Formulario que contiene la DBGrid y la Fuente del encabezado de la DBGrid sean las mismas. Las siguientes líneas (ejemplo) resuelven esto y fijan la fuente del encabezado de la DBGrid:

     Sub DBGridHeading()
         Dim f As New StdFont
         Dim i As Integer
         With f
             .Name = "MS Sans Serif"
             .Size = 8
             .Bold = True
         End With
         Set Me.Font = f
         Set miDBGrid.HeadFont = f

         With miDBGrid
             For i = 0 To .Columns.Count - 1
                 CenterHeadColumn Me, .Columns(i)
             Next
         End With
     End Sub



2. REGISTRO TAL DE CUANTOS

He visto pocos programas VB con la característica Registo tal de tantos, mientras que en Access es un estándar. Esto no es muy complicado de programar, aunque hacerlo bien (eficiente) es otra cosa. El truco es el siguiente:

     Option Explicit
     ...
     Private CountRecords As Variant
     ...

     Private Sub miData_Reposition()

         If IsEmpty(CountRecords) Then
            CountRecords = RecordsCountDC(miData)
         End If
         With miData
             If .Recordset.RecordCount Then
                .Caption = "Algún texto. Registro : " & _
                (.Recordset.AbsolutePosition + 1) & _
                " de " & CountRecords
             Else
                .Caption = "No hay entrada de registros"
             End If
         End With
     End Sub

Lo que queríamos ¿cierto?, 100% eficiente. Le damos soporte simplemente con la variable Cuenta_miData:

     Private Sub miData_Validate(Action As Integer, Save As Integer)
         Select Case Action
                Case vbDataActionAddNew
                     CountRecords = CountRecords + 1
                Case vbDataActionDelete
                     CountRecords = CountRecords - 1
                ...
         End Select
         ...
     End Sub

Para contabilizar el numero de registros yo he usado la siguiente función desde un modulo estándar:

     Function RecordsCountDC(DC As Data) As Long

         Dim cn As Recordset, n As Long

         If DC.Recordset.RecordCount Then
            Set cn = DC.Recordset.Clone
            cn.MoveLast
            n = cn.RecordCount
         Else
            n = 0
         End If

         RecordsCountDC = n
     End Function

Después de cualquier miData.Refresh, agregue la línea CountRecords = RecordsCountDC(miData), pero esto aplica solo para un formulario tipo Form-SubForm (relaciones de uno a varios), y en ambientes multiusuario, donde solo sabremos el numero de registros hasta dar un Refresh.



3. FORMATO PERSONALIZADO

Esta es fácil, pero apuesto que pocos la sabían. Referente al siguiente bloque de códogo, en su orden, después del With, (1) cambio la titulo de la columna, (2) cambio el ancho de la columna, (3) Doy un formato numérico (tipo 2,000.0), (4) No quiero que se tecleen caracteres en la celda (esto es muy importante cuando es un Query de tablas relacionadas y se usa la columna solo para guía).

     With miDBGrid.Columns(i)
         .Caption = "Distribuidores Minoristas"
         .Width = Me.TextWidth(String(24, "S"))
         .NumberFormat = "##,##0.0"
         .Locked = True
     End With

Donde i es el índice de la Columna que se quiere personalizar (recuerde que las columnas se enumeran empezando desde 0 hasta Columns.Count - 1).

Harvey Triana, psoft@latino.net.co

4. UNA LISTA DESPLEGABLE EN UNA CELDA

Confieso que el truco no me lo ingenie, Ottmar Figuero de Nicaragua me puso en camino de un articulo de APEX © (versión reciente) que orientaba como hacerlo. Sin embargo, no me limite a fusilar el articulo, aquí le cuento como crear varias listas en diferentes celdas y todo en base a código (el articulo lo hace en modo de tutor en tiempo de diseño). Tambien entrego una perspectiva para una programación avanzada.

Sin mucho rodeo, se logra a través de la propiedad Button del objeto Column del DBGrid y su evento Clic. Rápidamente, digamos que ya tiene su DBGrid enlazada a datos y trabaja bien, solo desea agregar un ComboBox a una celda para dar una lista de ítems a sus usuarios.
Digamos que el Field se llama "Idioma"

1.Identifique el índice de columna del campo que implementará una lista. Sencillamente, las columnas van enumeradas de 0 , 1, 2 , etc.
Digamos que el campo Idioma se despliega en la columna 3, entonces defina la siguiente constante:

     DefInt A-Z
     Option Explicit

     '// Indice de la columna que tiene un lista desplegable
     '// Recomiendo la sintaxis ColIndex_NombreDeLista. P.e:
     Const ColIndex_List1 = 3

2.Agregue un control List al formulario. Me imagino que sabe de sobra que un List o ComboBox se llena de datos al comenzar el programa. También especifique la propiedad Button. Esto se haría en el evento Cargar del formulario, use de guía el ejemplo:

     Private Sub Form_Load()
        '// Llena la lista de soporte al campo Idioma
        '// Ejemplo sencillo
         With List1
            .AddItem "Español"
            .AddItem "Ingles"
            .AddItem "Ruso"
            .AddItem "Aleman"
            .AddItem "Frances"
            .AddItem "Chino"
         End With

        '//Requerido antes de hacer referencia a Campos de DBGrid
         Data1.Refresh

        '//La celda de la columna "Idioma" mostrara un botón
        DBGrid1.Columns(ColIndex_List1).Button = True

        '//Opcional, La Celda actual es negra al estilo Access
        DBGrid1.MarqueeStyle = dbgHighlightCell
     End Sub

3.Eventos del DBGrid. El modelo es el siguiente (los comentarios documentan la acción):

     Private Sub DBGrid1_ButtonClick(ByVal ColIndex As Integer)

         Dim C As Column

         If ColIndex = ColIndex_List1 Then
            Set C = DBGrid1.Columns(ColIndex)
            With List1
                '// Despliegue de la lista al lado de la celda.
                '// Elimine los comentarios de las dos siguientes líneas
                '// y coloque comentarios a las tres posteriores. A su gusto
                '.Left = DBGrid1.Left + C.Left + C.Width
                '.Top = DBGrid1.Top + DBGrid1.RowTop(DBGrid1.Row)

                '// Lista debajo de la celda, al estilo ComboBox (3 líneas)
                .Left = DBGrid1.Left + C.Left
                .Top = DBGrid1.Top + DBGrid1.RowTop(DBGrid1.Row) + DBGrid1.RowHeight
                .Width = C.Width + 15

                .ListIndex = 0
                .Visible = True
                .ZOrder 0
                .SetFocus
            End With
         End If
     End Sub

     Private Sub DBGrid1_BeforeColEdit(ByVal ColIndex As Integer, ByVal KeyAscii As Integer, Cancel As Integer)

        '// Use este evento para que cuando el usuario teclee un caracter sobre la celda
        '// se despliegue la lista. Es decir, se obliga al usuario a usar un ítem de la lista.
        '// En caso de dar al usuario libertad de escribir, elimine las siguientes líneas (If-End If),
        '// o precedales con un comentario

         If ColIndex = ColIndex_List1 Then
            '// Se obliga a seleccionar de la lista:
            Cancel = True
            DBGrid1_ButtonClick (ColIndex)
         End If
     End Sub

     Private Sub DBGrid1_Scroll(Cancel As Integer)
         '//Oculta la lista si hace Scroll
         List1.Visible = False
     End Sub

   1.Eventos de asignación del texto a la celda y la base de datos:

     Private Sub List1_KeyPress(KeyAscii As Integer)
         '//Habilita el uso de teclado para asignar a la celda o Escape...
         Select Case KeyAscii
             Case vbKeyReturn
                  '//Asigma el texto seleccionado a la celda
                  DBGrid1.Columns(ColIndex_List1).Text = List1.Text
                  List1.Visible = False
             Case vbKeyEscape
                  List1.Visible = False
         End Select
     End Sub

     Private Sub List1_LostFocus()
         '//Oculta la lista si pierde el enfoque
         List1.Visible = False
     End Sub

     Private Sub List1_DblClick()
         List1_KeyPress vbKeyReturn
     End Sub


ANALISIS

     Implementación Avanzada

     La implementación, tal y como la pinte, ciertamente es un código poco robusto, aunque es más sólido que el descrito en el articulo de APEX, en referencia a facilidad de crear listas en los campos que desee, y cuando quiera, sin necesidad de hacer muchos cambios. Este código permite crear una lista fácilmente tan solo con cambiar el índice de columna especificado en la constante ColIndex_List1. Para multiples listas en la misma DBGrid, usaría otras constante, digamos ColIndex_List2, ColIndex_List3, y así sucesivamente, y tendría que duplicar eventos. Recomendaría hacer al siguiente modificación:

     '//---------------------------------------------------------
     '// Varias listas desplegables, ejemplo
     '//---------------------------------------------------------
     Private Sub DBGrid1_ButtonClick(ByVal ColIndex As Integer)
         Dim C As Column
         Dim L As ListBox

         Set C = DBGrid1.Columns(ColIndex)

         Select Case ColIndex
             Case ColIndex_List1: Set L = List1
             Case ColIndex_List2: Set L = list2
         End Select
         With L
            'Abajo (3):
            .Left = DBGrid1.Left + C.Left
            .Top = DBGrid1.Top + DBGrid1.RowTop(DBGrid1.Row) + DBGrid1.RowHeight
            .Width = C.Width + 15
            .ListIndex = 0
            .Visible = True
            .ZOrder 0
            .SetFocus
         End With
     End Sub

     Una implementación más sólida, y profesional, seria crear un array de objetos List y no usar constantes para los índices sino una variable Type, digamos:

     Private Type DisplayListStruct
         FieldName As String
         ColumnIndex As Integer
         QuerySource As String
         KeyRelation As String
     End Type
     Const NumberOfList = 3 'Ejemplo
     Private DisplayList(0 To NumberOfList) As DisplayListStruct

     Esto permitiría realmente automatizar la implementación, incluyo usando Querys para llenar la lista.


     Asignando Claves de una Tabla de Referencia

     Un estándar en Access es dar la lista de soporte desde una Consulta, y asignar el código de dicha tabla a un campo que establece la relación de soporte. Usaría la propiedad Data1.Recordset("ClaveDeEnlace").Value = List1.ItemData(List1.ListIndex).

Harvey Triana, psoft@latino.net.co

5. Configuración de la Fuente de Datos en Tiempo de Ejecución

Francamente este tema da para muchas páginas y el código es bastante pesado para explicarlo con claridad en una sesión. Así que lo omito por el momento. Básicamente se perfila como un componente de software comercial. Sin embargo, aquí les doy la mayoría de pautas para que programadores avanzados hagan sus propias herramientas.

El objetivo es desplegar cualquier fuente de datos totalmente configurada en una línea, o de la manera más simple. El ejemplo de la gráfica es:

Set dbMain = DBEngine(0).OpenDatabase(App.Path + "\Atlas.mdb", Options:=False, ReadOnly:=False)
...
DisplayDataBrowser "Paises"

El procedimiento DisplayDataBrowser tiene un único parámetro que es DataSource el cual será una Consulta o nombre de Tabla. El software que lo recibe analiza los datos (almacenados en la estructura y propiedades de los campos) y despliega la hoja de datos. La implementación de todas las listas de soporte y generación de la consulta compleja es también automático. La contraparte de la vista DBGrid es la vista Formulario.

La automatización de los formularios de carga soporta cambios en la bases de datos (p.e. la inclusión de una nueva tabla, un nuevo campo, o la modificación de un campo existente) sin modificación de la aplicación. Esto se ve reflejado en una mejor relación Beneficio / Costo, al reducir la inversión en mantenimiento de ingeniería de software. De otra parte, en virtud de la automatización, se minimizan fallas de diseño. La parte de automatización del soporte a datos integrada con bases de datos externas (de catálogos de datos), permite la ampliación o reutilización de datos corporativos.


6. Un Campo Calculado sobre Columnas

Un campo calculado sobre una columna se debe hacer con código, -así Usted programe en Access. Sencillamente agregue un Label y Control para presentarlo e invoque un procedimiento; me gusta hacerlo con un SSPanel en relieve suave.

Este procedimiento es un ejemplo claro para calcular y presentar una sumatoria de un campo cualquiera:

     Public Sub SumCol( _
         dbg As DBGrid, _
         dat As Data, _
         FieldName As String, _
         pnl As SSPanel, _
         NumberFormat As String _
         )
         Dim s As Single
         Dim rs As Recordset
         On Error GoTo SumColErr

         Set rs = dat.Recordset.Clone
         Do Until rs.EOF
            s = s + Val(rs(FieldName))
            rs.MoveNext
         Loop
         pnl = Format(s, NumberFormat)
         Exit Sub

     SumColErr: '//Su procedimiento de mensajes
     End Sub

Desde el formulario que contiene el DBGrid se invoca desde dos eventos. Primero para iniciarlo, y luego para actualizarlo después de las modificaciones hechas por los usuarios. Digamos que mi SSPanel se llama pnl_Calc1l y deseo presentar la Suma Total de Producción:

     Prívate Sub datPrimaryRS_Reposition()
       '//Codigo...

       '//Inicia el campo calulado sobre la columna:
       If pnl_Calc1.Caption = "" Then
          SumCol grdDataGrid, datPrimaryRS, "Producción", pnl_Calc1, "0.0"
       End If
     End Sub

Actualiza el total cuando se modifica o agrega un dato:

     Prívate Sub grdDataGrid_AfterUpdate()
         SumCol grdDataGrid, datPrimaryRS, "Producción", pnl_Calc1, "0.0"
     End Sub

El procedimiento sirve de modelo para hacer promedios, cáculos estadísticos, etc. Valga comentar que un calculo sobre una fila (registro) suele hacerse con el SQL dentro de la Consulta que es origen de los datos. Sin embargo puede adaptar la técnica expuesta para hacerlo en un Control exterior al DBGrid.


7. Implementado un Zoom a las Celdas

Supremamente deseable en campos Memo.Se trata que al dar [Shift]+[F2], se muestre una ventana ampliada con el texto de la celda, -tal como en Access. El evento que invoca el zoom es:

     Prívate Sub DBGrid1_KeyDown(KeyCode As Integer, Shift As Integer)
         If (KeyCode = vbKeyF2) And (Shift And vbShiftMask) > 0 Then
           '//Shift+F2
            OpenZoomText DBGrid1, Data1
          End If
     End Sub

El procedimiento OpenZoomText se encarga de analizar el Campo y desplegar la ventana de zoom:

     Public Sub OpenZoomText(dbg As DBGrid, dat As Data)
         Dim f As Field, n As Integer
         '//Verify Field
         n = 0
         For Each f In dat.Recordset.Fields
             If f.Name = dbg.Columns(dbg.Col).DataField Then
                Select Case f.Type
                   Case dbText
                        n = f.Size
                   Case dbMemo
                        n = 2 ^ 15 - 1
                End Select
                Exit For
             End If
         Next f
         '//Display Zoom Form
         If n Then
            With frm_ZoomText
                .Title = f.Name
                .LetText f.Value, n, (f.Type = dbText)
                .Show vbModal
                 If .ReturnPress Then
                     dbg.Text = .Text
                     DoEvents
                 End If
            End With
            Unload frm_ZoomText
         End If
     End Sub

Por último, publico el código completo del formulario frm_ZoomText (no se justifica un download por tan reducido tamaño). Abra NotePad, pegue el código a continuación y archívelo como Zoom Cell.frm, luego agrege este formulario a su proyecto.

     VERSION 5.00
     Begin VB.Form frm_ZoomText
        BorderStyle = 4 'Fixed ToolWindow
        ClientHeight = 2340
        ClientLeft = 1695
        ClientTop = 2295
        ClientWidth = 6240
        LinkTopic = "Form1"
        MaxButton = 0 'False
        MinButton = 0 'False
        PaletteMode = 1 'UseZOrder
        ScaleHeight = 2340
        ScaleWidth = 6240
        ShowInTaskbar = 0 'False
        Tag = "ZoomText"
        Begin VB.TextBox txt
           BeginProperty Font
              Name = "MS Sans Serif"
              Size = 9.75
              Charset = 0
              Weight = 400
              Underline = 0 'False
              Italic = 0 'False
              Strikethrough = 0 'False
           EndProperty
           Height = 855
           Left = 240
           MultiLine = -1 'True
           ScrollBars = 2 'Vertical
           TabIndex = 0
           Top = 90
           Width = 4905
        End
     End
     Attribute VB_Name = "frm_ZoomText"
     Attribute VB_GlobalNameSpace = False
     Attribute VB_Creatable = False
     Attribute VB_PredeclaredId = True
     Attribute VB_Exposed = False
     DefInt A-Z
     Option Explicit

     Public ReturnPress As Boolean
     Private SingleLine As Boolean

     Private Sub Form_Resize()
         txt.Move 0, 0, ScaleWidth, ScaleHeight
     End Sub

     Private Sub txt_KeyPress(KeyPress)
            Select Case KeyPress
                Case vbKeyReturn
                     KeyPress = 0
                     ReturnPress = True
                     Hide
                Case vbKeyEscape
                     ReturnPress = False
                     Hide
         End Select
     End Sub

     Private Sub Form_Load()
         Left = (Screen.Width - Width) \ 2
     End Sub

     Public Property Get Text() As String
         Dim LF
         If SingleLine Then
            '//Discards the text thereafter CrLf
            LF = InStr(txt, vbCrLf)
            If LF Then
               Text = Left(txt, LF - 1)
            Else
               Text = txt
            End If
         Else
            Text = txt
         End If
     End Property

     Public Sub LetText(s As String, Chars As Integer, SingleLineX As Boolean)
         txt.Text = s
         txt.MaxLength = Chars
         SingleLine = SingleLineX
         ReturnPress = False
     End Sub

     Public Property Let Title(ByVal NewValue As String)
         Caption = NewValue
     End Property

El texto se traspasa a la celda con [Enter], puede cancelar con [Esc]. Para introducir un cambio de línea en un Memo, use [Ctrl]+[Enter].


8. El origen de los Datos de Varias Tablas (relaciones complejas)

Francamente este tema es algo extenso y lo omitiré. Esta extrechamente relacionado con el punto 5 descrito en este documento. Se trata de crear el SQL con código Visual Basic, basandose en la información de las propiedades predeterminadas y personalizadas de las tablas y campos.

Si desea DataBrowser, me puede escribir por mail y solicitarmelo..


9. Ordenar Rápidamente un DBGrid

El siguiente procedimiento ordena un DBGrid por la columna que tiene el enfoque. Coloque un comando como desee para ordenar ascendente y otro para ordenar descendente, e invoque el procedimiento según el caso.

     Public Sub SortDBGrid(dbg As DBGrid, dat As Data, Optional AscPmt As Variant)

         '//AscPmt: True:Ascendente (predeterminado), False: Descendente

         Dim rs As Recordset
         Dim Clause As String
         Dim FieldName As String
         Dim Asc As Boolean
         Dim CurrCol As Integer

         On Error GoTo SortErr

         If IsMissing(AscPmt) Then
            Asc = True
         Else
            Asc = CBool(AscPmt)
         End If

         CurrCol = dbg.Col
         FieldName = dbg.Columns(CurrCol).DataField

         If dat.Recordset.Fields(FieldName).Type = dbMemo Then
            Box "Los Campos Memo no son ordenables"
            Exit Sub
         End If

         Screen.MousePointer = vbHourglass
         Set rs = dat.Recordset
         Clause = "[" + FieldName + "]"
         If Not Asc Then
            Clause = Clause + " DESC"
         End If
         rs.Sort = Clause
         Set dat.Recordset = rs.OpenRecordset(rs.Type)

         dbg.Col = CurrCol
         dbg.SetFocus

         Screen.MousePointer = vbDefault
         Exit Sub

     SortErr: '//Su procedimiento de mensajes
     End Sub

El Sort se puede hacer para varios campos al escribir la clusula Campo1, Campo2, ..., Campo n. Tendra que escribir un cuadro de dialogo para este propósito y ajustar el procedimiento anterior.

Filtrar un DBDrid es muy similar al procedimiento anterior, solo tiene que especificar la Clausula de filtro y asignarla en rd.Filter = Clause; es elegante programar cuadro de dialogo que permita al usuario crear la clausula de filtro de manera simple. No obligue a sus usuarios a estudiar SQL.


10. Cambiar el Registro de un DBGrid en Tiempo de Ejecución

El siguiente procedimiento genérico permite asignar valores a una fila de un DBGrid con código. Útil cuando el recordset tiene varias claves de relación, que por supuesto no se muestran en columnas de la DBGRid pero están presentes en el control Data fuente de registros.

     Public Sub LetDBGridRecord(dbg As DBGrid, dat As Data)
         Dim rs As Recordset
         Dim OnNew As Boolean

         On Error GoTo LetDBGridRecordErr

         Set rs = dat.Recordset

         If dbg_Datos.AddNewMode Then
            OnNew = True
            rs.AddNew
         Else
            rs.Edit
         End If
         '//Asignación de valores:
         rs(AnyField1) = Value1
         rs(AnyField2) = Value2
         '...
         rs.Update
         If OnNew Then rs.MoveLast
            Set rs = Nothing
         Exit Sub

     LetDBGridRecordErr: '//Su procedimiento de mensajes
     End Sub

El procedimiento se puede generalizar al colocar un parametro Variant de contenga una matriz bidimensional (NombreDeCampo, NuevoValor).
Queda como ejercicio; pista:

Dim x As Variant
...
ReDim x(0 To n, 0 To n)
...

Como información, puede realizar cambios en el Recordset del DBGrid sin usar los métodos Edit o AddNew, pero solo es posible en el Evento BeforeUpdate de un registro recien editado en la DBGrid.

Harvey Triana, psoft@latino.net.co



Trucos Trucos

Visual Basic Página de Visual Basic

Página principal Página principal

www.jrubi.com