12 de mayo de 2017

Extender informes en VFP 9.0 - Tiempo de Diseño (Parte 2/2)

Autor: Doug Hennig (http://www.stonefield.com)
Traducido por: Ana María Bisbé York


(Sesión "Extending the VFP 9 Reporting System: Design-Time" presentada por el autor en la Conferencia DevEssentials Kansas, 2004)

Crear controladores de eventos de informe

Dado que la mayoría de los controladores de eventos de informe tienen requerimientos comunes, he creado una clase base controladora llamada SFReportEventHandler (una subclase de SFCustom que es mi clase base Custom definida en SFCtrls.VCX), definida en SFReportBuilder.VCX. Su método INIT instancia un objeto SFReportEventUtilities dentro de la propiedad oUtilities. Al igual que FRXCursor, este objeto tiene métodos utilitarios para maniobrar con los objetos y eventos de informe.

El método Execute de SFReportEventHandler guarda el evento de objeto pasado en su propiedad oEvent y la propiedad oEvent del objeto SFReportEventUtilities, entonces llama al método OnExecute, que es abstracto en esta clase. En una subclase no voy a anular el método Execute, pero en su lugar voy a colocar el código adecuado en el método OnExecute.

lparameters toEvent
with This
  .oEvent = toEvent
  .oUtilities.oEvent = toEvent
  .OnExecute()
  .oEvent = .NULL.
  .oUtilities.oEvent = .NULL.
endwith 

Para manipular un evento de informe en particular, creo una subclase de SFReportHandler y al registro en la tabla de registro del controlador. Para hacerlo más fácil en el futuro, creo un programa llamado InstallHandler.PRG. Paso la clase y la biblioteca de clases para el controlador, el número de evento, y opcionalmente, el tipo y código de objeto. El programa agrega un registro a la tabla registro (que se llama ReportBuilder.DBF, cambié las sentencias USE e INSERT INTO para emplear un nombre de tabla diferente) si no existe, e inhabilita cualquier otro controlador para el mismo evento.

lparameters tcClass, ;
  tcLibrary, ;
  tnEventType, ;
  tnObjType, ;
  tnObjCode
local lcClass, ;
  lnObjType, ;
  lnObjCode
* Abrir la tabla report builder. 
use ReportBuilder
* Si el controlador especificado existe, se asegura de que esté disponible
* asigna a EventType el correspondiente valor. En caso contrario, agrega un registro 
* utilizando los valores predeterminados para tipo y objeto, si no hubieran sido pasados 
lcClass = padr(upper(tcClass), len(Hndl_Class))
locate for upper(Hndl_Class) == lcClass
if found()
  replace EventType with tnEventType
else
  lnObjType = iif(vartype(tnObjType) = 'N', tnObjType, -1)
  lnObjCode = iif(vartype(tnObjCode) = 'N', tnObjCode, -1)
  insert into ReportBuilder ;
    (Rec_Type, ;
    Hndl_Class, ;
    Hndl_Lib, ;
    EventType, ;
    ObjType, ;
    ObjCode) ;
    values ;
    ('H', ;
    tcClass, ;
    tcLibrary, ;
    tnEventType, ;
    lnObjType, ;
    lnObjCode)
endif found() 
* Inhabilita cualquier otro controlador para el mismo evento al asignar 
* el código EventType a un valor usado. 
replace EventType with EventType + 100 ;
  for EventType = tnEventType and ;
  not upper(Hndl_Class) == lcClass 
* Limpia y cierra. 
use 

Como SFReportEventHandler, SFReportEventFilter es una clase base para los filtros de eventos de informe. También está basada en SFCustom. Su método INIT es idéntico a SFReportEventHandler y su método HasHandled es el mismo que SFReportEventHandler, excepto que llama al método OnHasHandled, el que es abstracto en esta clase, en lugar de OnExecute. En una subclase, voy a colocar el código adecuado en OnHasHandled, devolviendo .T. si se han detenido futuros procesamientos del evento.

lparameters toEvent
local llReturn
with This
  .oEvent = toEvent
  .oUtilities.oEvent = toEvent
  llReturn = .OnHasHandled()
  .oEvent = .NULL.
  .oUtilities.oEvent = .NULL.
endwith
return llReturn 

Como InstallHandler.PRG, InstallFilter.PRG hace el registro de subclases de filtros más fácil. Se pasa la clase y biblioteca para el objeto filtro y el orden en que debe ser procesado. Agrega un registro en la tabla reportbuilder si es necesario y renombra otros filtros.

lparameters tcClass, ;
  tcLibrary, ;
  tnOrder
local lcClass
* Abre la tabla report builder. 
use ReportBuilder 
* Renunera los registros de filtro existents si es necesario. 
replace Fltr_Ordr with transform(val(Fltr_Ordr) + 1) ;
  for Rec_Type = 'F' and val(Fltr_Ordr) >= tnOrder 
* Si no existe el filtro especificado,
* agrega un registro para colocarlo,
* en caso contrario, actualiza el orden 
lcClass = padr(upper(tcClass), len(Hndl_Class))
locate for Rec_Type = 'F' and upper(Hndl_Class) == lcClass
if not found()
  insert into ReportBuilder ;
    (Rec_Type, ;
    Hndl_Class, ;
    Hndl_Lib) ;
    values ;
    ('F', ;
    tcClass, ;
    tcLibrary)
endif not found()
replace Fltr_Ordr with transform(tnOrder) 
* Limpia y cierra.
 use 

Veamos un ejemplo del empleo útil de los controladores de eventos.

Plantillas de informes

Cuando crea un informe nuevo, tiene un informe en blanco. ¿No sería bueno, si VFP agregara automáticamente un grupo de elementos que deseamos tener en todos los informes? En otras palabras, queremos tener un informe que sirva de plantilla para todos los nuevos informes.

Desde los lejanos días de FoxPro 2.6, en realidad tenemos esa posibilidad (aunque no estaba documentado): ya que FoxPro siempre ha creado un informe nuevo llamado Untitle si no especifica un nombre, si tiene un informe llamado Untitled.FRX, VFP lo abrirá; pero le pregunta un nombre nuevo, cuando va a guardar el informe la primera vez. Desafortunadamente, ese truco no trabaja en VFP. Primero, si no especifica un nombre el predeterminado es ReportN, donde N es el número que incrementa desde 1 cada vez que un informe se crea en una sesión particular de VFP. Segundo, incluso si un informe llamado Report1.FRX existe y N será 1 porque este es la primera vez que utiliza CREATE REPORT al comenzar VFP, VFP no abre ese informe y da un informe en blanco. Ahora, con los eventos de informe en tiempo de diseño podemos tener plantillas de informes. No existe un evento que se dispare cuando se crea un informe; pero existe uno cuando se abre, así que solamente tenemos que comprobar si el informe abierto es nuevo o no (el método utilitario devuelve .T. si es nuevo). Si es un informe nuevo, hacemos ZAP los registros existentes en el FRX, APPEND FROM de un archivo FRX plantilla y establecer la bandera (flat) devuelta para indicar que el evento ha sido controlado y fue modificado el FRX. He aquí el código del método OnExecute de SFNewReportHandlerBasic:

local lnSelect, ;
  lnRecno
if This.oUtilities.IsNewReport()
  lnSelect = select()
  select FRX
  zap
  lnRecno = recno()
  append from (This.cTemplateReport)
  This.oEvent.ReturnFlags = FRX_REPBLDR_HANDLE_EVENT + ;
    FRX_REPBLDR_RELOAD_CHANGES
  go lnRecno
  select (lnSelect)
endif This.oUtilities.IsNewReport() 

La propiedad de usuario cTemplateReport contiene el nombre del archivo FRX plantilla a utilizar. De forma predeterminada contiene Template.FRX. Para registrar esta clase, ejecute InstallNewReportHandlerBasic.PRG.

Vamos a soñar un poco más: ¿qué tal si le preguntamos al usuario qué plantilla querría utilizar? SFNewReportHandlerFancy es una subclase de SFNewReportHandlerBasic con el siguiente código en OnExecute:

local loForm
if This.oUtilities.IsNewReport()
  loForm = newobject('SFSelectTemplateForm', 'SFReportBuilder.vcx')
  loForm.Show()
  if vartype(loForm) = 'O'
    This.cTemplateReport = loForm.cTemplate
  endif vartype(loForm) = 'O'
endif This.oUtilities.IsNewReport()
return dodefault() 

Este código utiliza la clase SFSelectTemplateForm para mostrar una lista de las plantillas disponibles. Esta lista viene de Templates.DBF cuyos campos contienen el nombre de las plantillas FRX, un nombre descriptivo para la plantilla y un campo memo que contiene comentarios acerca de la plantilla. Para registrar esta clase, ejecute InstallNewReportHandlerFancy.PRG.

Evitar el acceso al Entorno de datos (DataEnvironment)

Una de las razones para exponer el Diseñador de informes en tiempo de ejecución es permitir a los usuarios modificar el diseño de un informe. Es posible que desee permitirles mover campos, agregar el logotipo de la compañía, etc. Sin embargo, no desea darles acceso a determinadas cosas, como son el DataEnvironment. Si ellos desordenan o dañan la configuración de los datos (es decir los cursores o el código en los eventos del DataEnvironment, lo más probable es que el informe deje de funcionar. Entonces, ¿cómo podemos evitar que accedan a las funciones que expone el Diseñador de informes?

Mi primera idea fue modificar el menú contextual del Diseñador de informes y el menú Informes, para quitar las funciones que no quería darles acceso. Desafortunadamente no es posible. Entonces, lo mejor será evitar que suceda algo cuando el usuario seleccione estas funciones no deseadas. Hay que entrenar a los usuarios en que estas funciones aparecen en el menú; pero no hacen nada, eso será mejor que dar soporte telefónico sobre porqué los informes no funcionan debido a algo que se modificó.

SFEventKiller es un ejemplo extremadamente sencillo de controlador de eventos, su método OnExecute simplemente dice al Diseñador de informes que el evento ha sido controlado, por lo que no hay comportamiento nativo.

This.oEvent.ReturnFlags = FRX_REPBLDR_HANDLE_EVENT 

Para evitar el acceso a los eventos del Entorno de datos (al abrir el DataEnvironment evento 9, o al importar el DataEnvironment de una clase DE u otro FRX, evento 17), registra simplemente SFEventKiller como su controlador. InstallDEKiller.PRG hace eso justamente:

do InstallHandler with 'SFEventKiller', 'SFReportBuilder.vcx', 9
do InstallHandler with 'SFEventKiller', 'SFReportBuilder.vcx', 17 

Para ver cómo trabaja, ejecute InstallDEKiller.PRG, luego modifique un informe, clic derecho, y escoja DataEnvironment. No ocurre nada. A propósito, otra forma para hacer esto, puede ser activar la protección desde la ventana Propiedades de informe, ficha Protection (Protección) – View Data Environment setting, utilice la cláusula PROTECTED al modificar el informe.

Diálogo de usuario para nuevos campos

Una de las cosas que siempre he deseado hacer es sustituir el diálogo que aparece cuando el usuario agrega un campo nuevo a un informe. Deseo que un diálogo que sea por una parte más sencillo (muestre nombres descriptivos para tablas y campos) y más poderoso (no requiera que las tablas estén en el DataEnvironment o abiertas y tenga opciones para agregar una etiqueta que vaya junto al campo). Debido a que ahora puedo tomar el control sobre el evento de informe “campo nuevo”, puedo finalmente crear el diálogo que deseo. Es registrado SFNewTextBoxHandlecomo el controlador para campos nuevos con esta nueva línea de código (tomada de TestNewField.PRG):

do InstallHandler with 'SFNewTextBoxHandler', 'SFReportBuilder.vcx', 2, 8, 0 

TestNewField.PRG crea además un objeto metadato que tiene una colección de tablas y campos leídos de la tabla metadatos. No vamos a mirar ese objeto aquí, puede examinarlo usted mismo.

Al ejecutar TestNewField.PRG automáticamente crea un informe nuevo; pero aparentemente no hay nada diferente. Sin embargo, debido a que ahora SFNewTextBoxHandler es el controlador para campos nuevos, al agregar un campo, verá el diálogo que se muestra debajo, en lugar del acostumbrado.

Este diálogo muestra nombres descriptivos para las tablas y campos de la base de datos Northwind, la cual, por supuesto, no está en el Entorno de datos ni abiertas en VFP (ellos no lo necesitan ya que esta información llega del metadato). Esto permite además indicar si se ha creado una etiqueta o no, y si se ha creado, si puede ser colocada en la banda encabezado de página sobre el campo o en la banda de detalle a la izquierda del campo.

Como con las otras subclases de SFReportEventHandler, es el método OnExecute de SFNewTextBoxHandler el que no trabaja.

No veremos todo el código de este método, sólo algunos de los aspectos más interesantes. Lo primero que hace este método es mostrar el diálogo que vemos arriba. Luego, si el usuario hace clic en OK, recupera alguna información sobre el campo seleccionado desde el meta dato (tipo de dato, tamaño, etc). Es entonces que utiliza el objeto SFReportEventUtilities para recuperar la fuente, tamaño y estilo predeterminado desde el registro cabecera en el FRX. Luego, utiliza el objeto auxiliar para determinar la altura y ancho del campo en FRUs y actualiza los registros de los campos respectivos en el FRX.

lcFontName = .oUtilities.GetReportHeaderValue('FONTFACE')
lnFontSize = .oUtilities.GetReportHeaderValue('FONTSIZE')
lnFontStyle = .oUtilities.GetReportHeaderValue('FONTSTYLE')
lcFontStyle = .oUtilities.GetFontStyle(lnFontStyle) 
* Determina el ancho y alto para el textbox. 
lnWidth = .oEvent.FRXCursor.GetFRUTextWidth(lcText, lcFontName, ;
  lnFontSize, lcFontStyle)
lnHeight = .oEvent.FRXCursor.GetFRUTextHeight(lcText, lcFontName, ;
  lnFontSize, lcFontStyle) 
* Actualiza las propiedades del textbox nuevo. 
replace EXPR with lcField, ;
  NAME with lcName, ;
  WIDTH with lnWidth, ;
  HEIGHT with lnHeight ;
  PICTURE with iif(empty(lcPicture), '', '"' + lcPicture + '"'), ;
  FILLCHAR with lcType, ;
  FONTFACE with lcFontName, ;
  FONTSIZE with lnFontSize, ;
  FONTSTYLE with lnFontStyle, ;
  USER with 'EXPR=' + lcExpr + chr(13) + chr(10) ;
  in FRX 

Lo siguiente que hace es determinar dónde colocar la etiqueta para el campo. Si debe estar en el encabezado de página, el objeto SFReportEventUtilities debe encontrar una etiqueta en la banda encabezado que tiene “*:TEMPLATE” en su campo memo USER. Si existe el objeto, se utiliza como plantilla de una nueva etiqueta (fuente, estilo, posición vertical, etc.) Si además aparece “REMOVE”, el objeto plantilla se elimina del informe.

if lnPosition = 1
  loBand = .oUtilities.GetBandObject(FRX_OBJCOD_PAGEHEADER)
  loTemplate = .oUtilities.FindTemplateObject(loBand, ;
    FRX_OBJTYP_LABEL, '*:TEMPLATE', .T.)
  if vartype(loTemplate) = 'O'
    if '*:TEMPLATE REMOVE' $ upper(loTemplate.User)
      .oUtilities.RemoveReportObject(loTemplate.UniqueID)
      loTemplate.User = strtran(loTemplate.User, ;
        '*:TEMPLATE REMOVE', '*:TEMPLATE')
    endif '*:TEMPLATE REMOVE' $ upper(loTemplate.User)
    loObject = loTemplate
    lnVPos = loObject.VPos
  else
    lnVPos = loBand.EndFRU - lnHeight
  endif vartype(loTemplate) = 'O' 

Si se supone que la etiqueta vaya en la misma banda que el campo, el objeto SFReportEventUtilitie es llamado a buscar una banda en el informe, entonces como el código anterior, busca en la banda la etiqueta con “*:TEMPLATE” en su campo memo USER y lo utiliza como plantilla.

else
  loBand = .oUtilities.GetBandFor(FRX.UniqueID)
  loTemplate = .oUtilities.FindTemplateObject(loBand, ;
  FRX_OBJTYP_LABEL, '*:TEMPLATE', .T.)
  if vartype(loTemplate) = 'O'
    if '*:TEMPLATE REMOVE' $ upper(loTemplate.User)
      .oUtilities.RemoveReportObject(loTemplate.UniqueID)
      loTemplate.User = strtran(loTemplate.User, ;
        '*:TEMPLATE REMOVE', '*:TEMPLATE')
    endif '*:TEMPLATE REMOVE' $ upper(loTemplate.User)
    loObject = loTemplate
  endif vartype(loTemplate) = 'O'
  lnHPos = lnHPos - lnWidth - 50
endif lnPosition = 1 

Finalmente, se agrega una nueva etiqueta al informe.

with loObject
  .UniqueID = sys(2015)
  .TimeStamp = This.oEvent.FRXCursor.GetFRXTimeStamp(datetime())
  .Name = ''
  .Expr = '"' + lcCaption + '"'
  .Height = lnHeight
  .Width = lnWidth
  .VPos = lnVPos
  .HPos = lnHPos
  .ObjType = FRX_OBJTYP_LABEL
endwith
insert into FRX from name loObject 

Este es el controlador de evento más complicado que hemos visto, porque tiene que vérselas con más cosas en el FRX. Hacer este tipo de trabajo requiere un poco de conocimiento sobre la estructura de un FRX, asegúrese de chequear el informe 90FRX en la carpeta Tools\FileSpec del directorio raíz de VFP con la documentación del FRX.

A propósito, este ejemplo muestra otra nueva característica de VFP 9: etiquetas en tiempo de diseño. Note que cuando agrega un campo al informe, muestra una etiqueta en lugar del nombre del campo en el objeto campo. Esto es porque el código antes mostrado actualiza la columna NAME del campo objeto en el FRX con la etiqueta del campo. Cuando utiliza CREATE o MODIFY REPORT con la cláusula PROTECTED, el Diseñador de informes va a mostrar el contenido de la columna NAME en lugar de la columna EXPR para el objeto campo. Esto significa que puede mostrar un nombre descriptivo para campos en lugar de los nombres reales de campos. Por supuesto, verá el nombre real de los campos en la ventana propiedades; pero al menos verá nombres agradables en el área de diseño.

Generar cursores al vuelo

Una vez que ha creado un informe a partir del meta dato utilizando el controlador SFNewTextBoxHandler, ¿que ocurre cuando trata de visualizar el informe? No va a trabajar, porque los cursores no han sido abiertos. Ah; pero ¿que tal si hemos hecho un gancho y generado cursores antes de que se ejecute el informe?

SFPreviewHandler fue registrado como el controlador para eventos 10 (presentación preliminar) y 18 (imprimir)
Por InstallPreview.PRG. Su método OnExecute es más simple, genera una sentencia SQL SELECT para que busque en los campos del informe (no miraremos al código del método CreateSQLStatement), instancia un objeto que abre una conexión a la base de datos SQL Server Northwind, envía la sentencia SQL SELECT al SQL Server para crear un cursor, y decirle al motor de informe que cancele la presentación preliminar o la impresión, si hemos fallado, por alguna razón.

local lcSelect, ;
 loConnection, ;
 llOK 
* Crea la sentencia SQL SELECT que necesitamos para el informe. 
wait window 'Recuperando datos...' nowait
lcSelect = This.CreateSQLStatement() 
* Crea un objeto connection y trata de conectar con la
* Base de datos SQL Server Northwind
* Si se conecta, ejecuta la sentencia 
* SQL SELECT en la sesión de datos del informe. 
loConnection = newobject('SFConnectionMgr', 'SFConnection.vcx')
loConnection.cConnectString = 'driver=SQL Server;server=(local);' + ;
  'database=Northwind;trusted_connection=yes'
if loConnection.Connect()
  llOK = loConnection.ExecuteStatement(lcSelect, ;
  This.oEvent.DefaultSessionID, sys(2015))
endif loConnection.Connect() 
* Si falla la conexión o se produce un error al crear el cursor,
* muestra un aviso y dejamos una marca de que hemos controlado
* el evento, entonces se detiene la ejecución 
wait clear
if not llOK
  messagebox(loConnection.cErrorMessage)
  This.oEvent.ReturnFlags = FRX_REPBLDR_HANDLE_EVENT
endif not llOK 

Resumen

El equipo VFP ha cumplido sus objetivos en las mejoras que han realizado al sistema de informes. Los diálogos disponibles en ReportBuilder.APP son más atractivos y fáciles de utilizar y más capaces que en versiones anteriores. La posibilidad de hacer ganchos en los informes en tiempo de desarrollo significa que puede crear diseñadores de informe personalizado que pueden ser poderosos, flexibles, y fáciles de utilizar, tanto para el equipo de desarrollo y los usuarios finales.

Copyright © 2004 Doug Hennig. All Rights Reserved

No hay comentarios. :

Publicar un comentario