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

  1. Los Controles para el diseñador son pocos y algo limitados.
  2. No permite la adición de Controles en tiempo de ejecusión.
  3. Los controles enlazables a datos deben obligatoriamente estar enlazados a un DataField.
  4. Carece de una interfaz para exportar a documentos a formatos de Office.
  5. El diseñador tiene limitaciones (por ejemplo no permite copiar y pegar controles).
  6. 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.
  7. Debería compartir la interfaz del objeto Printer.
  8. 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

  1. Es manipulable desde código (tiene un modulo de código).
  2. Es tecnología ADO (acepta cualquier origen de datos).
  3. Acepta el conjunto de datos en tiempo de ejecución (siempre que sea lógico con la estructura del reporte)
  4. Esta bien organizado en términos de objetos
  5. 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)
  6. 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: