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