Programando el DataReport de VB6
Algo de Código para Gestionar el Objeto DataReport en Tiempo de Ejecución
Por Harvey Triana
El componente DataReport es una de aquellas ideas excelentes para Visual Basic, pero como siempre, parece que siempre nacen prematuras y suelen dar problemas donde aparentemente no los debería haber (siempre tenemos que encontrarnos con alguna carencia o BUG). No obstante estudiando a fondo DataReport, le he encontrado su esencia y capacidad para gestionar reportes de datos. En fin, a pesar de las múltiples carencias actuales, DataReport es sumamente atractivo, algo para programadores Visual Basic, y estoy seguro que pronto será robusto. Entre las cosas más interesantes de DataReport encuentro que se puede enlazar no solo a DataEnvironments, sino a clases de reconocimiento de datos, y a simples objetos de ADO. Este articulo muestra un ejemplo.
¿Porque que gestionar DataReport con código, y no usar los asistentes (partiendo delsde el DataEnvironment)?. Sencillamente la respuesta es la creación de reportes reutilizables (dinámicos). Por ejemplo, hace poco tenia que crear cerca de cien reportes de una base de datos petrolera. Solucione el problema con solo tres DataReport y clases que los manipulan al derecho y al revés.
Primeros Pasos con DataReport
Como siempre, mis artículos no son estrictamente didácticos, y van más halla de la documentación estándar (de otra manera no tendría sentido). Para empezar con DataReport, recomiendo los siguientes títulos de la MSDN (siga los árboles subsecuentes). Es importante que domines aquellos conceptos para seguir con esta lectura.
- Acerca del Diseñador de entorno de datos
- Escribir informes con el Diseñador de informe de datos de Microsoft
- Tener acceso a datos mediante Visual Basic
El Objeto DataReport
Se trata de unas librerías ActiveX escritas para Visual Basic, soportadas en tecnología ADO. Un DataReport se asimila mucho a un formulario, con su diseñador y todo. A grandes rasgos, he encontrado las siguientes características:
Carencias
- Los Controles para el diseñador son pocos y algo limitados.
- No permite la adición de Controles en tiempo de ejecusión.
- Los controles enlazables a datos deben obligatoriamente estar enlazados a un DataField.
- Carece de una interfaz para exportar a documentos a formatos de Office.
- El diseñador tiene limitaciones (por ejemplo no permite copiar y pegar controles).
- El problema de la orientación del papel ha hecho carrera en los News (ver MSDN: Articulo 197915 - Report Width is Larger than the Paper Width). Aun no encuentro solución para impresoras en Red.
- Debería compartir la interfaz del objeto Printer.
- La variable de tipo DataReport no acepta todas las propiedades definidas en un objeto DataReport especifico (ver MSDN: Articulo 190584- Some Properties or Methods Not Displayed in DataReport).
Beneficios
- Es manipulable desde código (tiene un modulo de código).
- Es tecnología ADO (acepta cualquier origen de datos).
- Acepta el conjunto de datos en tiempo de ejecución (siempre que sea lógico con la estructura del reporte)
- Esta bien organizado en términos de objetos
- El acceso a los controles es a través de cadenas de texto (los controles en un DataReport son diferentes a los controles ActiveX normales)
- Crea informes con buen rendimiento
Existirán mas carencias y beneficios, pero por el momento estos enunciados son suficientes.
Para Los programadores Visual Basic, el primer beneficio enunciado es suficiente para tener muy en cuanta a DataReport, ya que permitirá explorar todas su posibilidades. De eso trata este articulo.
Reportes Reutilzables
Como programador de gestión de datos: ¿alguna vez ha deseado imprimir el contenido de un conjunto de registros de forma simple (títulos y los datos en una plantilla)?, de la misma forma que abrimos una tabla o consulta en MS Access o MS FoxPro y usamos el comando Print. O, imprimir el contenido de un DataGrid tal cual, sin mucho complique. Bien, podemos intentar escribir un componente ActiveX usando un DataReport y solucionar el problema casi para cualquier situación similar. Se presentarán problemillas, que se podrán solucionar en el componente y este evolucionara de manera conveniente para nosotros.
El problema expuesto anteriormente es, desde el punto de vista de acceso a datos, sencillo, es decir no existen conjuntos de datos subyacentes (relaciónes maestro-detalle). No obstante es posible escribir reportes complejos (varios niveles de relación) y reutilizables basándose la tecnología de comandos SHAPE.
Bien, daré una solución aproximada al problema expuesto.
Ejercicio
Crear un Proyecto EXE Estándar.
Agregar referencia a MS ActiveX Data Objects 2.1 Library.
Agregar Proyecto DLL.
Agregar referencia a MS ActiveX Data Objects 2.1 Library.
Agregar referencia a MS Data Formatting Object LibraryReferencias: MS ActiveX Data Objects 2.1 Library
Agregar un Data Report (menú Proyecto)
Diseñe el DataReport como se ve en la siguiente figura:

Más detalles de los controles para reporte y se encuentra en la siguiente tabla:
|
Seccion |
Tipo |
Nombre |
|
stEncabezadoDeInforme |
RptLabel |
lblEncabezadoDeInforme_H |
|
stEncabezadoDeInforme |
RptLine |
lnEncabezadoDeInforme_H |
|
stEncabezadoDePagina |
RptLabel |
lblTituloDeCelda1 |
|
stEncabezadoDePagina |
RptLabel |
lblTituloDeCelda2 |
|
stDetalle |
RptTextBox |
txtCelda1 |
|
stDetalle |
RptTextBox |
txtCelda2 |
|
stPieDePagina |
RptLabel |
lblPieDePagina_H |
|
stPieDeInforme |
RptLabel |
lblPieDeInforme_H |
|
stPieDeInforme |
RptLine |
lnPieDeInforme_H |
El propósito de los caracteres _H al final de algunos nombres de los Controles es poder, mediante código, extender el ancho del control todo el ancho del informe, lo que es conveniente para líneas y títulos (esto nos permite ignorar el ancho del papel sin dañar el la presentación del informe).
Otras propiedades del DataReport son Name = rptGerneral1, ReportWidth = 9360 twips (para un papel de 8.5 pulgadas y se calcula mediante ReportWidth = 8.5*1440 - 1440 (LeftMargin) - 1440 (RightMargin), donde 1440 son twips por pulgada)
Por el momento no dará código al modulo del DataReport.
Agregue el siguiente bloque de código a la clase creada por defecto por la DLL, luego el nombre debe ser Name = cls_Informe1:
'// ------------------------------------------------------------
'// CLASS : Report1Level
'// DESCRIPTION : Code Template for Report 2 Leves
'// AUTHOR : Harvey T.
'// LAST UPDATE : 17/11/99
'// SOURCE : -
'// ------------------------------------------------------------
Option Explicit
'//MEMBERS
Private m_DetailMember As String
Private m_Report As rptGerneral1
'//COLLECTIONS
Private DetailCells As Collection
'//CONTANTS
Private Const nMAXCELLS As Integer = 10
Public Function AddDetailCell( _
ByVal Title As String, _
ByVal FieldName As String, _
Optional ByVal FormatString As String = vbNullString, _
Optional ByVal ColumnWidth As Long = nDEFAULCOLUMNWIDTH _
) As cls_CeldaDetalle
Static Key As Integer
Static NextLeft As Long
Dim cell As cls_CeldaDetalle
Dim txt As RptTextBox
Dim lbl As RptLabel
Dim LineRight As Long
Key = Key + 1
'//Filter maximun cells
If Key > nMAXCELLS Then Exit Function
'//Filter ReportWidth
If ColumnWidth <= 0 Then ColumnWidth = nDEFAULCOLUMNWIDTH
If NextLeft + ColumnWidth > m_Report.ReportWidth Then
'//Try Landscape
If NextLeft + ColumnWidth > gRptWidthLandscape Then
Exit Function '//No chances of add new cell
Else
'//changes orientation to Landscape
Call gChangesOrientation(vbPRORLandscape)
m_Report.ReportWidth = gRptWidthLandscape
End If
End If
'//Cell
Set cell = New cls_CeldaDetalle
Set txt = m_Report.Sections("stDetalle").Controls("txtCelda" & Key)
With txt
.DataField = FieldName
.DataMember = m_DetailMember
.Visible = True
.Width = ColumnWidth
.Left = NextLeft
LineRight = .Left + .Width
NextLeft = NextLeft + .Width
End With
If Len(FormatString) Then gGiveFormat txt, FormatString
'//Cell title
Set lbl = GetLabel("stEncabezadoDePagina", "lblTituloDeCelda" & Key)
With lbl
.Left = txt.Left
.Width = txt.Width
.Caption = gAdjustNameToWidth(lbl, Title)
.Visible = True
End With
gCellMargin txt
cell.Key = Key
Set cell.txtCell = txt
DetailCells.Add cell, CStr(Key)
Set AddDetailCell = cell
Set cell = Nothing
End Function
Public Property Get Item(vntIndexKey As Variant) As cls_CeldaDetalle
Set Item = DetailCells(vntIndexKey)
End Property
Public Property Get Count() As Long
Count = DetailCells.Count
End Property
Public Property Get NewEnum() As IUnknown
Set NewEnum = DetailCells.[_NewEnum]
End Property
Private Sub Class_Initialize()
Set DetailCells = New Collection
Set m_Report = New rptGerneral1
Call gGetPageSize(m_Report)
End Sub
Private Sub Class_Terminate()
Set DetailCells = Nothing
Set m_Report = Nothing
Call gResetPageOrient
End Sub
Public Property Get MaxCells() As Integer
MaxCells = nMAXCELLS
End Property
Public Property Let PieDePagina(ByVal v As String)
gLetCaption GetLabel("stPieDePagina", "lblPieDePagina_H"), v
End Property
Public Property Let PieDeInforme(ByVal v As String)
gLetCaption GetLabel("stPieDeInforme", "lblPieDeInforme_H"), v
End Property
Public Property Let EncabezadoDeInforme(ByVal v As String)
gLetCaption GetLabel("stEncabezadoDeInforme", _ "lblEncabezadoDeInforme_H"), v
m_Report.Caption = v
End Property
Private Function GetCaption( _
SectionName As String, _
LabelName As String _
) As String
GetCaption = _
m_Report.Sections(SectionName).Controls(LabelName).Caption
End Function
Public Property Set DataSource(v As ADODB.Recordset)
Set m_Report.DataSource = v
End Property
Public Property Set DataEnviron(v As Object)
Set m_Report.DataSource = v
End Property
Public Property Let DataMember(v As String)
m_Report.DataMember = v
End Property
Public Property Let DetailMember(v As String)
m_DetailMember = v
End Property
Public Sub ShowReport(Optional Modal As Boolean = True)
If Not m_Report.Visible Then
gCorrectPRB8456 m_Report, "stDetalle", "txtCelda", m_DetailMember
gElongedToWidth m_Report
'//Show
m_Report.Show IIf(Modal, vbModal, vbModeless)
Else
m_Report.SetFocus
End If
End Sub
Private Function GetLine( _
SectionName As String, _
LineName As String _
) As RptLine
Set GetLine = m_Report.Sections(SectionName).Controls(LineName)
End Function
Private Function GetLabel( _
SectionName As String, _
LabelName As String _
) As RptLabel
Set GetLabel = m_Report.Sections(SectionName).Controls(LabelName)
End Function
|
Luego agrega una clase, con propiedad Instancing = 2-PublicNotCreatable, Name = cls_CeldaDetalle. Esta clase será un objeto de colección de la clase cls_Informe1, y servirá para tener referencia a cada columna agregada al DataReport. El código de la clase cls_CeldaDetalle es:
'// ------------------------------------------------------------
'// CLASS : DetailCell
'// DESCRIPTION : A cell in custum report.
'// Member rpttextbox of some collection
'// AUTHOR : Harvey T.
'// LAST UPDATE : 17/11/99
'// SOURCE : -
'// ------------------------------------------------------------
Option Explicit
Public Key As Integer
Private m_txtCell As RptTextBox
Friend Property Set txtCell(v As RptTextBox)
Set m_txtCell = v
End Property
Friend Property Get txtCell() As RptTextBox
Set txtCell = m_txtCell
End Property
|
Por ultimo, agrega un modulo estándar a la DLL, con Name = modCommon y el siguiente código. Es modulo modCommon hace parte de una biblioteca de código más general escrita por mí para manipular DataReport.
'// ------------------------------------------------------------
'// MODULE : Common
'// DESCRIPTION : Shared any
'// AUTHOR : Harvey T.
'// LAST UPDATE : 29/11/99
'// ------------------------------------------------------------
Option Explicit
Public Const nDEFAULCOLUMNWIDTH As Long = 1800 '//twips
Public Const nGRIDLINESCOLOR As Long = &H808080
Public gRptWidthLandscape As Long '//twips
Public gRptWidthPortrait As Long '//twips
Public gRptCurOrientation As Long
Public gRptNewOrientation As Long
'//As global multiuse
Private groo As New ReportOrientation
Public Sub gGiveFormat(txt As RptTextBox, FormatString As String)
Dim f As New StdDataFormat
f.Format = FormatString
Set txt.DataFormat = f
txt.Alignment = rptJustifyRight
End Sub
Public Sub gCellMargin(txt As RptTextBox)
Const nCELLMARGIN As Long = 60 '//twips
With txt
.Width = .Width - 2 * nCELLMARGIN
.Left = .Left + nCELLMARGIN
End With
End Sub
Public Sub gCorrectPRB8456( _
objRpt As Object, _
SectionName As String, _
CellPrefix As String, _
MemberName As String _
)
'//rptErrInvalidDataField
'//« No se encuentra el campo de datos »
'//Solution: Give the first DataField in hide Cells
Dim txt As RptTextBox
Dim ctl As Variant
Dim s As String
'//Fisrt DataField
s = objRpt.Sections(SectionName).Controls(CellPrefix & "1").DataField
For Each ctl In objRpt.Sections(SectionName).Controls
If InStr(ctl.Name, CellPrefix) Then
Set txt = ctl
If txt.DataField = vbNullString Then
txt.DataMember = MemberName
txt.DataField = s
txt.Width = 0
End If
End If
Next
End Sub
Public Sub gMoveLine( _
ln As RptLine, _
Optional LineLeft, _
Optional LineTop, _
Optional LineWidth, _
Optional LineHeight _
)
If Not IsMissing(LineLeft) Then ln.Left = LineLeft
If Not IsMissing(LineTop) Then ln.Top = LineTop
If Not IsMissing(LineWidth) Then ln.Width = LineWidth
If Not IsMissing(LineHeight) Then ln.Height = LineHeight
If Not ln.Visible Then ln.Visible = True
End Sub
Public Sub gLetCaption( _
lbl As RptLabel, _
Caption As String _
)
lbl.Caption = Caption
If Not lbl.Visible Then lbl.Visible = True
End Sub
Public Sub gGetPageSize(objRpt As Object)
Dim ptr As Printer
Dim tmp As Long
Set ptr = Printer
With ptr
gRptCurOrientation = groo.GetPrinterOrientation( _
.DeviceName, .hDC)
gRptNewOrientation = gRptCurOrientation
.ScaleMode = vbTwips
gRptWidthPortrait = .Width - objRpt.LeftMargin - _
objRpt.RightMargin
gRptWidthLandscape = .Height - objRpt.LeftMargin - _
objRpt.RightMargin
If gRptCurOrientation = vbPRORLandscape Then
'//Swap
tmp = gRptWidthPortrait
gRptWidthPortrait = gRptWidthLandscape
gRptWidthLandscape = tmp
objRpt.ReportWidth = gRptWidthLandscape
End If
End With
Set ptr = Nothing
End Sub
Public Sub gChangesOrientation(ro As Enum_ReportOriention)
gRptNewOrientation = ro
groo.SetPrinterOrientation ro
End Sub
Public Sub gElongedToWidth(objRpt As Object)
Const sFLAG As String = "_H"
Dim sect As Section
Dim ctl As Variant
Dim n As Long
n = objRpt.ReportWidth
For Each sect In objRpt.Sections
For Each ctl In sect.Controls
If Right(ctl.Name, 2) = sFLAG Then
ctl.Left = 0
ctl.Width = n
End If
Next
Next
End Sub
Public Sub gResetPageOrient()
If Not gRptNewOrientation = gRptCurOrientation Then
Call gChangesOrientation(gRptCurOrientation)
End If
End Sub
Public Function gAdjustNameToWidth( _
lbl As RptLabel, _
Caption As String _
) As String
Dim rtn As String
Dim s As String
With Printer
Set .Font = lbl.Font
If .TextWidth(Caption) > lbl.Width Then
s = Caption + Space(2)
Do
s = Left(s, Len(s) - 1)
rtn = s + "..."
Loop Until .TextWidth(rtn) < lbl.Width Or Len(s) = 0
gAdjustNameToWidth = rtn
Else
gAdjustNameToWidth = Caption
End If
End With
End Function
Public Sub gGetControlsList(objRpt As Object)
Const CO As String = " "
Dim sect As Section
Dim ctl As Variant
Debug.Print "Section"; CO; "Type"; CO; "Name"
For Each sect In objRpt.Sections
For Each ctl In sect.Controls
Debug.Print sect.Name; CO; TypeName(ctl); CO; ctl.Name
Next
Next
End Sub
|
Agregue una nueva clase a la DLL. Esta clase contiene la API para manipular la orientación del papel. Observe los creditos al autor. El código de esta clase lo consigue en este Link: ReportOrientation.zip (3k)
Finalmente, al modulo del formulario del proyecto estándar, agrega un Hierarchacal FlexGrid, Name = flexMuestra, un CommanButton, Name = cmdInforme. El formulario llevara el siguiente código de ejemplo:
Los nombres y estructura de los proyectos se muestra a continuación:

El grupo de proyectos se llamará: grpReporteDeMuestra.vbg. Este grupo de proyectos es útil para depurar el componente InformeGeneral1, que posteriormente se puede dar compatibilidad binaria para colocarlo al servicio de futuros proyectos. El código del cliente (frmMuestra) es el siguiente:
'// ------------------------------------------------------------
'// FORM : frmMuestra
'// DESCRIPTION : Ejemplo de DataReport general
'// AUTHOR : Harvey T.
'// LAST MODIFY : -
'// ------------------------------------------------------------
Option Explicit
Private rs As ADODB.Recordset
Private Sub cmdInforme_Click()
flexMuestra.SetFocus
DoEvents
GenerarReporte
End Sub
Private Sub GenerarReporte()
Dim rpt As cls_Informe1
Set rpt = New cls_Informe1
With rpt
Set .DataSource = rs
.EncabezadoDeInforme = "Base de Datos NWIND (Clientes)"
.PieDeInforme = "Fin de Informe"
.PieDePagina = "Clientes con su Contacto"
.AddDetailCell "Compañía", "NombreCompañía", , 6000
.AddDetailCell "Contacto", "NombreContacto", , 3000
.ShowReport True
End With
End Sub
Private Sub Form_Load()
Call InicieConjuntoDeRegistros
'//Cofigurar Grilla
flexMuestra.ColWidth(0) = 300
flexMuestra.ColWidth(1) = 2000
flexMuestra.ColWidth(2) = 2000
Set flexMuestra.DataSource = rs
End Sub
Private Function InicieConjuntoDeRegistros()
Dim cnn As Connection
Dim cmd As Command
Set cnn = New Connection
Set cmd = New Command
Set rs = New Recordset
'//Database command connection
cnn.Open "Provider=Microsoft.Jet.OLEDB.3.51;" & _
"Data Source=D:\Archivos de programa\VB98\Nwind.mdb;"
With cmd
Set .ActiveConnection = cnn
.CommandType = adCmdText
.CommandText = "SELECT NombreCompañía, NombreContacto " & _
"FROM Clientes " & _
"ORDER BY NombreCompañía;"
End With
With rs
.CursorLocation = adUseClient
.Open cmd, , adOpenForwardOnly, adLockReadOnly
Set cmd.ActiveConnection = Nothing
Set cmd = Nothing
Set .ActiveConnection = Nothing
End With
cnn.Close
Set cnn = Nothing
End Function
Private Sub Form_Unload(Cancel As Integer)
If Not rs Is Nothing Then
rs.Close
End If
End Sub
Private Sub Form_Resize()
If Not Me.WindowState = vbMinimized Then
flexMuestra.Move 0, 0, Me.ScaleWidth, Me.ScaleHeight - 330
cmdInforme.Move 0, Me.ScaleHeight - cmdInforme.Height
End If
End Sub
|
La ejecución del proyecto muestra la siguiente interfaz de usuario:

La ejecución del informe a través del botón Informe, mostrara el siguiente Informe:

Mostrado en un Zoom = 50 %.
Discusión y Ampliación del Informe Reutilizable
Tal cual el componente InformeGeneral1, servirá para mostrar cualquier tabla o vista de datos con dos columnas, solo habrá que modificar el código del cliente, a saber el procedimiento: InicieConjuntoDeRegistros. Para ampliar la capacidad a más columnas, deberá agregar controles (debido a la limitación numero 2) RptLabel de nombre lblTituloDeCeldaX, y controles txtCeldaX a sus respectivas secciones (X es el nuevo numero del control agregado, por ejemplo si agrega una tercera columna, X = 3). Aun no termina el trabajo tedioso, tendrá que dar las propiedades pertinentes a cada nuevo control (debido a la limitación numero 5). Por ultimo deberá modificar la constante nMAXCELLS del la clase cls_Informe1 (esta contante evita el error por desbordamiento del número de columnas enviadas a DataReport).
Se puede dar una grilla a la presentación de la tabla en el informe, pero es un trabajo algo tedioso, deberá agregar controles RptLine a los lados de las celdas y sus titulo. Sin bien vale la pena y le queda de tarea.
El componente InformeGeneral1 intenta solucionar el problema de la orientación del papel de la siguiente manera: Si el numero de columnas no cabe en posición Portrait, el reporte pasa (automaticmente) a orientación LandScape, hasta que acepte un numero de columnas que cubran el área del reporte, más halla no se mostraran más columnas (sin generar error). Si estudia el código, la clase ReportOrientation contiene la API necesaria para cambiar la orientación del papel. Desdichadamente el código trabaja solo para impresoras locales.
Debido a la carencia número 3: « Los controles enlazables a datos deben obligatoriamente estar enlazados a un DataField », es necesario ejecutar el procedimiento gCorrectPRB8456 del modulo modCommon antes de mostrar el Informe. Este procedimiento da un DataField repetido y oculto a las columnas que no se utilizan.
También puede agregar más RptLabel, Imágenes, numeración de páginas, etc. para mejorar la apariencia del Informe. Un informe de ejemplo llevado sobre la base de código se muestra a continuación:

|