31 de mayo de 2017

Extender el sistema de informes en VFP 9.0. Tiempo de ejecución. Parte 3/3

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


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

Controladores de Directivas

SFReportDirective es una clase abstracta cuyo controlador de directivas se puede subclasear. Es una subclase de SFCustom, mi clase base Custom definida en SFCtrls.VCX y tiene sólo dos métodos: HandleDirective, y que es llamado desde SFReportListenerDirective para controlar la directiva, y ProcessExpression y el que puede ser utilizado en una subclase para convertir el argumento directiva desde texto hasta un formato que pueda utilizar el controlador.

SFDynamicStyleDirective es un controlador de directiva que permite cambiar el estilo de fuente (que es, si es normal, negrita, itálica o subrayada) para un objeto informe basado en una expresión que es evaluada dinámicamente para cada registro en el conjunto de datos de informe. Especifique la directiva en el memo User de un objeto de informe utilizando la sintaxis siguiente:

*:LISTENER STYLE = StyleExpression

donde StyleExpression es una expresión que evalúa el estilo deseado.

Una complicación: los estilos se almacenan en el FRX como valores numéricos, entonces, SFDynamicStyleDirective permite utilizar #NORMAL#, #BOLD#, #ITALIC#, y #UNDERLINE# para especificar estilos más fácil. Estos valores son acumulativos, entonces #BOLD# + #ITALIC# dará un texto en negrita e itálica. El método ProcessExpression se ocupa de convertir el texto del estilo en el valor numérico apropiado (las constantes cnSTYLE_* en este código se definen en SFReporting.H)

lparameters tcExpression
local lcExpression
lcExpression = strtran(tcExpression, '#NORMAL#', transform(cnSTYLE_NORMAL))
lcExpression = strtran(lcExpression, '#BOLD#', transform(cnSTYLE_BOLD))
lcExpression = strtran(lcExpression, '#ITALIC#', transform(cnSTYLE_ITALIC))
lcExpression = strtran(lcExpression, '#UNDERLINE#', ;
transform(cnSTYLE_UNDERLINE))
return lcExpression 

He aquí un ejemplo de una directiva (tomada del campo SHIPVIA en el TestDynamicFormatting.FRX) que muestra un objeto de informe en negro bajo determinada condición y normal bajo otras)

*:LISTENER STYLE = iif(SHIPVIA = 3, #BOLD#, #NORMAL#) 

El método HandleDirective llama al método ProcessExpression para convertir el texto del estilo en apropiados valores numéricos, luego evalúa la expresión. Si la expresión es válida, entonces, establece la propiedad FontStyle de las propiedades de objetos al estilo dinámico y establece Reload en .T. de tal forma que el motor sepa que el objeto de informe ha sido modificado.

lparameters toListener, ;
  tcExpression, ;
  toObjProperties
local lcExpr, ;
  lnStyle 
* Convierte cualquier estilo especificado en los valores adecuados de estilo. 
lcExpr = This.ProcessExpression(tcExpression) 
* Trata de evaluar la expresión (se podría utilizar la estructura TRY si fallara 
* silenciosamente). Si está bien, cambia el estilo de fuente del objeto y flag it
* lo necesario hasta ser llamado. 
lnStyle = evaluate(lcExpr)
if vartype(lnStyle) = 'N'
  toObjProperties.FontStyle = lnStyle
  toObjProperties.Reload = .T.
endif vartype(lnStyle) = 'N'

SFDynamicAlphaDirective es muy similar a SFDynamicStyleDirective, pero establece la propiedad PenAlpha del objeto informe a un valor específico. Especifica la directiva utilizando la sintaxis siguiente:

*:LISTENER ALPHA = AlphaExpression 

SFDynamicColorDirective también es muy similar a SFDynamicStyleDirective, pero tiene que ver con el color del objeto de informe en lugar del estilo de la fuente. Como con los estilos, los colores deben ser especificados como valores RGB, entonces SFDynamicColorDirective soporta colores para ser especificados como texto, #RED#, #BLUE#, and #YELLOW#. Especifique la directiva utilizando la siguiente sintaxis.

*:LISTENER FORECOLOR = ColorExpression 

donde ColorExpression es una expresión que evalúa el color deseado .

El código en el método HandleDirective es similar a SFDynamicStyleDirective; pero llama a SetColor en lugar de configurar la propiedad FontStyle. SetColor es abstracto en este método, es implementado en dos subclases de SFDynamicColorDirective: SFDynamicBackColorDirective y SFDynamicForeColorDirective. He aquí el código de SFDynamicBackColorDirective que muestra cómo se establece el color:

lparameters toObjProperties, ;
  tnColor
with toObjProperties
  .FillRed = bitand(tnColor, 255)
  .FillGreen = bitrshift(bitand(tnColor, 0x00FF00), 8)
  .FillBlue = bitrshift(bitand(tnColor, 0xFF0000), 16)
endwith 

El código en el método ProcessExpression de SFDynamicColorDirective es además similar al que hay en SFDynamicStyleDirective, convierte el texto del color en los correspondientes valores RGB.

TestDynamicFormatting.FRX muestra cómo trabajan esos dos controladores de directivas (y SFReportListenerDirective). Imprime los registros de la tabla Orders de la DBC ejemplo Northwind, que viene con VFP. El campo SHIPPEDDATE tiene lo siguiente en User:

*:LISTENER FORECOLOR = iif(SHIPPEDDATE > ORDERDATE + 10, #RED#, #BLACK#) 

Esta directiva dice al listener que muestre este campo en rojo si la fecha del registro fue enviado hace más de diez días más tarde de que fuera ordenado o en negro en caso contrario, negro. El campo SHIPVIA muestra en negrita si el método para enviar la mercancía es 3 o normal sino, como vimos antes cuando discutimos la directiva SFDynamicStyleDirective. La expresión para el campo SHIPVIA muestra el uso de otra característica de VFP 9: La función ICASE. Esta función es como IIF() excepto que un CASE inmediato, en lugar de un IF inmediato. Esta expresión muestra “Fedex” si SHIPVIA es 1, “UPS” si es 2 y “Mail” si es 3.

icase(SHIPVIA = 1, 'Fedex', SHIPVIA = 2, 'UPS', SHIPVIA = 3, 'Mail') 

El siguiente código (tomado de TestDynamicFormatting.PRG) muestra como ejecutar este informe con SFReportListenerDirective como su listener. Este código también utiliza SFRotateDirective, el que veremos en su momento, y muestra cuántos listeners múltiples pueden ser encadenados en un informe.

use _samples + 'Northwind\orders'
loListener = newobject('SFRotateDirective', 'SFReportListener')
loListener.Successor = newobject('SFReportListenerDirective', ;
  'SFReportListener')
report form TestDynamicFormatting.FRX preview object loListener next 20 

SFTranslateDirective permite crear informes multi-lenguajes definiendo que ciertos campos deben ser traducidos. Su método HandleDirective abre una tabla STRINGS que contiene cada cadena en su propio registro y columnas para cada lenguaje. Asume una variable global llamada gcLanguage que contiene el lenguaje para utilizar en el informe, por supuesto, puede sustituir por otro mecanismo que desee. Mira el texto en el campo actual en STRINGS y encuentra la traducción apropiada para la columna para el lenguaje deseado. Si el texto es diferente, se escribe en la propiedad Text del objeto propiedades y Reload se hace igual a .T. así que el motor de informe puede utilizar la nueva cadena.

lparameters toListener, ;
  tcExpression, ;
  toObjProperties
local lcText, ;
  lcNewText, ;
  lnI, ;
  lcWord 
* Abre la tabla Strings si es necesario. 
if not used('STRINGS')
  use STRINGS again shared in 0
endif not used('STRINGS') 
* Trata de encontrar el texto actual para el objeto y lo sustituye con el texto en
* el lenguaje deseado (almacenado en la variable global gcLanguage). 
store toObjProperties.Text to lcText, lcNewText
for lnI = 1 to getwordcount(lcText)
  lcWord = getwordnum(lcText, lnI)
  if seek(upper(lcWord), 'STRINGS', 'ENGLISH')
    lcNewText = strtran(lcNewText, lcWord, trim(evaluate('STRINGS.' + ;
      gcLanguage)))
  endif seek(upper(lcWord), 'STRINGS', 'ENGLISH')
next lnI
if not lcNewText == toObjProperties.Text
  toObjProperties.Text = lcNewText
  toObjProperties.Reload = .T.
endif not lcNewText == toObjProperties.Text

Para usar este listener simplemente coloque *:LISTENER TRANSLATE en el memo User para cualquier campo objeto que desee traducir y establezca gcLanguage como lenguaje deseado. Note que ya que EvaluateContents sólo es llamado para objetos campo, tendrá que utilizarlos en lugar de los objetos etiquetas, incluso para el texto que no cambie. TestTranslate.PRG muestra como agregar SFTranslateDirective al controlador de directivas reconocido por SFReportListenerDirective. Este ejemplo utiliza Pig Latin porque no puedo recordar suficiente francés de mi escuela secundaria para crear un mensaje entendible.

use _samples + 'Northwind\customers'
loListener = newobject('SFReportListenerDirective', 'SFReportListener')
loHandler = newobject('SFTranslateDirective', 'SFReportListener.vcx')
loListener.oDirectiveHandlers.Add(loHandler, 'TRANSLATE')
gcLanguage = 'PigLatin'
report form TestTranslate.FRX preview object loListener 

SFRotateDirective es otra directiva de controlador, excepto que se basa en SFReportListener en lugar de SFReportDirective porque no sólo cambia las propiedades del objeto de informe a partir del objeto propiedades. En su lugar, sobrescribe el método Render para rotar el objeto informe.

Para especificar que un objeto informe debe ser rotado, ponga esta directiva en el memo User utilizando la siguiente sintaxis:

*:LISTENER ROTATE = Angle 

donde Angle es el ángulo para rotar (los ángulos que se crea en dirección de las manecillas del reloj se especifican en valores positivos y los contrarios a las manecillas del reloj en negativo). Esto no soporta expresiones dinámicas porque no quiero utilizarlo con este objetivo; pero un simple cambio en SFRotateDirective podría agregar esta capacidad.

El método INIT declara algunas funciones GDI+ necesarias para Render y dimensiona aRecords a 2 columnas (el código BeforeReport en SFReportListener va a redimensionar entonces a tantas columnas como registros en el FRX).

declare integer GdipRotateWorldTransform in GDIPlus.DLL ;
  integer graphics, single angle, integer enumMatrixOrder_order
declare integer GdipSaveGraphics in GDIPlus.DLL ;
  integer graphics, integer @state
declare integer GdipRestoreGraphics in GDIPlus.DLL ;
  integer graphics, integer state
declare integer GdipTranslateWorldTransform in GDIPlus.DLL ;
  integer graphics, single dx, single dy, integer enumMatrixOrder_order 
* Dimensiona aRecords a la cantidad de registros que necesitamos. 
dimension This.aRecords[1, 2] 

El método Render comienza por verificar si hemos visto las directivas en este objeto de informe o no. Si no, llama a HandleListenerDirectives para hacerlo (no veremos el código de este método, es similar, incluso más simple que el que hay en SFReportDirective). Si ha especificado una directiva de rotación de ángulos, Render utiliza el método GDI+ para cambiar la generación de los ángulos para el objeto, utiliza DODEFAULT() para hacer esta regeneración, restablece la configuración GDI+ y utiliza NODEFAULT así que la regeneración no se hace la segunda vez que exista este método.

lparameters tnFRXRecno, ;
  tnLeft, ;
  tnTop, ;
  tnWidth, ;
  tnHeight, ;
  tnObjectContinuationType, ;
  tcContentsToBeRendered, ;
  tnGDIPlusImage
local llHandled, ;
  lnAngle, ;
 lnState
with This 
  * Si no hemos determinado si este registro tiene una directiva de rotación o no,
  * lo hacemos. 
  if not .aRecords[tnFRXRecno, 2]
    .UpdateListenerDirectives(tnFRXRecno)
  endif not .aRecords[tnFRXRecno, 2] 
  * Si se supone que rotemos este objeto, lo hacemos. 
  llHandled = .F.
  lnAngle = .aRecords[tnFRXRecno, 1]
  if lnAngle <> 0 
    * Guardamos el estado actual del controlador gráfico. 
    lnState = 0
    GdipSaveGraphics(.GDIPlusGraphics, @lnState) 
    * Mueve el punto 0,0 hasta donde deseamos, al rotar, rotamos sobre el punto deseado 
    GdipTranslateWorldTransform(.GDIPlusGraphics, tnLeft, tnTop, 0) 
    * Cambia el ángulo sobre el cual debe ocurrir la generación. 
    GdipRotateWorldTransform(.GDIPlusGraphics, lnAngle, 0) 
    * Restablece el punto 0,0. 
    GdipTranslateWorldTransform(.GDIPlusGraphics, -tnLeft, -tnTop, 0) 
    * Llama explícitamente al comportamiento de la clase base para hacer el dibujo. 
    dodefault(tnFRXRecNo, tnLeft, tnTop, tnWidth, tnHeight, ;
      tnObjectContinuationType, teContentsToBeRendered, tnFirstLine, ;
      tnLastLine, toImage) 
    * Devuelve el estado de controlador gráfico. 
    GdipRestoreGraphics(.GDIPlusGraphics, lnState) 
    * Ya hemos hecho el dibujo, no lo dejamos que lo vuelva a hacer. 
    nodefault 
    * Flag que ya hemos regenerado. 
    llHandled = .T.
  endif lnAngle <> 0
endwith
return llHandled 

Vimos esta funcionalidad en el ejemplo TestDynamicFormatting. TestRotate.FRX es otro ejemplo de informe que muestra cómo trabaja esto. El encabezado de columna para los campos fecha tiene directivas de rotación para que las fechas puedan aparecer más cerca unas de otras. El siguiente código (tomado de TestRotate.PRG) muestra cómo se ejecuta este informe con SFRotateDirective como su listener:

use _samples + 'Northwind\orders'loListener = newobject('SFRotateDirective', 'SFReportListener')
report form TestRotate.FRX preview object loListener next 20 

SFReportProgressMeter

Si desea información de progreso durante la ejecución de un informe, SFReportProgressMeter va a hacer el truco. Su método INIT instancia una clase formulario progreso. BeforeReport establece la propiedad de usuario lRendering en .T. y AfterReport igual a .F. para que pueda decir las diferencias entre las fases de generación y ejecución. Debido a que uno podría ser utilizado para actualizar status, ambos DoStatus y UpdateStatus llaman al método de usuario UpdateProgressMeter. Este método muestra diferentes mensajes (utilizando las propiedades de usuario cProgressMeter*) y actualiza el medidor de forma diferente de si lRendering es .T. o no.

lparameters tcMessage
local lcMessage
with This 
  * Si estamos generando páginas, toma el número de registros en el conjunto de datos
  * Si es la primera vez que lo llamamos, toma la cantidad de registros a procesar y 
  * prepara el título para la barra de progreso.
  if .lRendering
    if .nProgress = 0
      .oThermometer.SetMaximum(.CommandClauses.RecordTotal)
      .oThermometer.SetTitle(.cProgressMeterRenderingTitle)
    endif .nProgress = 0
    .nProgress = recno() 
    * Si no estamos generando, entonces estamos sacando páginas.
    * Toma el número de página actual. Si es la primera vez que lo hemos llamado, 
    * toma el número total de páginas y prepara el título para la barra de progreso.
  else
    if .nProgress = 0
      .oThermometer.SetMaximum(.OutputPageCount)
      .oThermometer.SetTitle(.cProgressMeterOutputtingTitle)
    endif .nProgress = 0
    .nProgress = .nOutputPageNo
  endif .lRendering 
  * Toma el mensaje a mostrar si no se ha pasado. 
  do case
    case vartype(tcMessage) = 'C'
      lcMessage = tcMessage
    case .lRendering
      lcMessage = textmerge(.cProgressMeterRenderingMessage)
    otherwise
      lcMessage = textmerge(.cProgressMeterOutputtingMessage)
  endcase 
  * Actualiza el termómetro. 
  .oThermometer.Update(.nProgress, lcMessage)
endwith 

TestGraphicOutput.PRG, que veremos en la siguiente sección, muestra como utilizar esta clase listener.

SFReportListenerGraphic

El método OutputPage de ReportListener soporta página de salida para archivos gráficos. Para hacerlo más fácil, he creado una clase SFReportListenerGraphic como una subclase de SFReportListener. Tiene dos propiedades de usuario: cFileName, que debe tener el nombre del archivo a crear, y nFileType, que puede ser el número que representa el tipo de archivo o izquierda de 0, en cuyo caso SFReportListenerGraphic toma el valor adecuado basado en la extensión de un nombre de fichero en cFileName.

Si ListenerType es 2 (predeterminado para esta clase), OutputPage es llamado después de cada página es generada. En ese caso, OutputPage va a controlar la salida del archivo especificado. Si el ListenerType es 3, las páginas son sólo de salida cuando OutputPage es llamado específicamente, entonces AfterReport genera las páginas y llama a OutputPage para cada una. Note que si se especifica un archivo TIFF multi-página, la primera página puede salir como un archivo TIFF de una única página. He aquí el código de AfterReport, OutputPage es similar; pero ligeramente más sencillo. En este código, LISTENER_DEVICE_TYPE_* son constantes definidas en SFReporting.H.

local lcBaseName, ;
  lcExt, ;
  lnI, ;
  lcFileName
with This
  dodefault()
  if .ListenerType = 3
    if .nFileType = 0
      .GetGraphicsType()
    endif .nFileType = 0
    lcBaseName = addbs(justpath(.cFileName)) + juststem(.cFileName)
    lcExt = justext(.cFileName)
    for lnI = 1 to .OutputPageCount
      do case
        case .nFileType <> LISTENER_DEVICE_TYPE_MULTI_PAGE_TIFF
          lcFileName = forceext(lcBaseName + transform(lnI), lcExt)
          .OutputPage(lnI, lcFileName, .nFileType)
        case not file(.cFileName)
          .OutputPage(lnI, .cFileName, LISTENER_DEVICE_TYPE_TIFF)
        otherwise
          .OutputPage(lnI, .cFileName, .nFileType)
      endcase
      .DoStatus('Page ' + transform(lnI) + ' of ' + ;
      transform(.OutputPageCount))
    next lnI
    .ClearStatus()
  endif .ListenerType = 3
endwith 

TestGraphicOutput.PRG muestra cómo trabaja SFReportListenerGraphic. Combina los efectos de múltiples listeners para generar el informe adecuadamente (utiliza el mismo TestDynamicFormatting.FRX que hemos visto antes), muestra un medidor de progreso, y salida para archivos gráficos.

use _samples + 'Northwind\orders'
loListener = newobject('SFReportListenerGraphic', 'SFReportListener')
loListener.cFileName = fullpath('TestReport.gif')
loListener.Successor = newobject('SFRotateDirective', 'SFReportListener')
loListener.Successor.Successor = newobject('SFReportListenerDirective', ;
  'SFReportListener')
loListener.Successor.Successor.Successor = ;
  newobject('SFReportListenerProgressMeter', 'SFReportListener')
report form TestDynamicFormatting.FRX object loListener range 1, 6 
* Muestra los resultados. 
declare integer ShellExecute in SHELL32.DLL ;
  integer nWinHandle, ; && controla de ventana padre
  string cOperation, ; && operación a realizar
  string cFileName, ; && nombre de archivo
  string cParameters, ; && parámetros para el ejecutable
  string cDirectory, ; && carpeta predeterminada
  integer nShowWindow && estado de la ventana
  ShellExecute(0, 'Open', loListener.cFileName, '', '', 1)

¿Y los PDF?

Por supuesto, la cuestión que se preguntará es ¿Qué pasa con los PDFs? Hasta este momento, Microsoft no tiene planes de incluir salida PDF con VFP 9. Sin embargo, hay varias formas para obtener salida PDF desde VFP (ahora o en VFP 9):

  • Utilice Adobe u otro PDF writer.
  • Utilice una herramienta de terceros específica para VFP para salidas PDF, como Mind’s Eye Report Engine, XFRX, o FRX2Any.
  • En VFP 9, puede obtener archivos TIFF y luego utiliza la utilidad GhostScript para convertir un archivo PDF.
    Debido a que fue escrito antes que la beta VFP 9, puede aun solucionarse. Manténgase conectado, puede que aun haya grandes soluciones disponibles.

Otras ideas

El cielo es el límite en términos de lo que se puede hacer con report listeners. Hay otras ideas. Aquí dejo algunas, seguramente podrá encontrar otras nuevas:

  • Agregar una marca de agua a un informe en tiempo de ejecución, no de diseño.
  • Tener líneas en la banda de detalles muestra con colores alternativos (como los antiguos folios con bandas)
  • Crear un report listener que encadene informes juntos de tal forma que pueda ser ejecutado con un solo comando.
  • Crear objetos diseñados por el usuario, como gráficos de pastel o barra.

Resumen

Microsoft ha hecho un gran trabajo abriendo el motor de informes de VFP, tanto en tiempo de diseño como en tiempo de ejecución. Al pasar eventos de informe a objetos ReportListener Xbase, permite reaccionar a estos eventos y hacer justamente lo que necesitamos, desde proporcionar retroalimentación de usuarios para proporcionar diferentes tipos de salidas que cambien dinámicamente la forma en que son generados los objetos.

Nota: dado que este documento fue escrito durante la fase beta de VFP 9, muchos de los detalles descritos en este documento pueden ser diferentes en la versión final. Asegúrese de chequear mi sitio Web (www.stonefield.com) para las actualizaciones de este documento y los ejemplos acompañantes.

Copyright © 2004 Doug Hennig. All Rights Reserved

24 de mayo de 2017

Extender el sistema de informes en VFP 9.0. Tiempo de ejecución. Parte 2/3

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


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

ReportListener

La nueva clase base ReportListener es una de las claves para informes más flexibles y con soporte de nuevos tipos de salida, además de pantalla e impresora. Al ejecutar un informe, VFP expone eventos de informe a los ReportListeners tal y como ocurren. La clase base ReportListener tiene un comportamiento nativo; pero la emoción llega cuando se crean y utilizan subclases propias.

Veamos las propiedades, eventos y métodos de ReportListener para entender su potencialidad

Propiedades

PropiedadTipoDescripción
AllowModalMessagesLSi .T., permite mensajes modales mostrando el progreso del informe (.F. es predeterminado)
CommandClausesOObjeto Empty con las propiedades que indican qué cláusulas del comando REPORT se utilizaron
CurrentDataSessionNID de sesión de datos actual
CurrentPassNIndica el paso actual por el informe. Un informe con _Pagetotal requerirá dos pases, los otros sólo requieren un pase, entonces CurrentPass siempre será igual 1
DynamicLineHeightL.T. (predeterminado) para utilizar espaciado de líneas, que varían de acuerdo a las características de la fuente, o .F. para utilizar el viejo estilo de espaciado.
FRXDataSessionNID de la sesión de datos para el cursor FRX (una copia del archivo de informe que el motor de informe está ejecutando, abierto para un uso de ReportListener)
GDIPlusGraphicsNEl objeto controlador de GDI+ utilizado para el dibujo. De solo lectura.
ListenerTypeNTipo de salida que produce el listener. El predeterminado es -1, que especifica no salida, de tal forma que tiene que cambiar este valor a uno más razonable. Vea la descripción del método OutputPage para una lista de valores.
OutputPageCountNNúmero de páginas configuradas
OutputTypeNTipo de salida que especifica en la cláusula OBJECT TYPE del comando REPORT
PageNoNNúmero actual de la página que se está dibujando
PageTotalNNúmero total de páginas en el informe
PreviewContainerOUna referencia a la superficie de muestra, en la cual el informe será dibujado para ser pre-visualizado
PrintJobNameCNombre del trabajo de impresión como aparece en la ventana Cola de trabajos de impresión.
QuietModeL.T. para suprimir la información del progreso. Predeterminado es .F.
SendGDIPlusImageN1 para enviar un controlador para una imagen para el campo General en el método Render (0 es predeterminado)
TwoPassProcessLIndica si se utilizarán dos pases para el informe

La propiedad CommandClauses contiene una referencia a un objeto Empty con propiedades que representan las cláusulas del comando REPORT más algunas otras golosinas.

PropiedadTipoDescripción
ASCIIL .T. si la palabra clave ASCII fue especificada al indicar salida a un archivo.
DE_NameC Nombre del objeto DataEnvironment para el informe. O por el nombre especificado en la cláusula NAME o el nombre del informe si no se ha especificado.
EnvironmentL .T. si se especificó la palabra clave ENVIRONMENT
FileC Nombre del informe a ejecutar
HeadingC Encabezado especificado con la palabra clave HEADING
IsDesignerLoadedL Indica si la persona que diseña el informe ha estado bebiendo. Ahora en serio, .T. si el informe se está ejecutando desde dentro del diseñador de informes
InScreenL .T. si se especificó la palabra clave INSCREEN
InWindowC Nombre de la ventana especificada con la palabra clave IN WINDOW
IsReportL .T. si es un informe o .F. si es una etiqueta
NoConsoleL .T. se especificó la palabra clave NOCONSOLE
NoDialogL .T. se especificó la palabra clave NODIALOG
NoEjectL .T. se especificó la palabra clave NOEJECT
NoPageEjectL .T. se especificó la palabra clave NOPAGEEJECT
NoResetL .T. se especificó la palabra clave NORESET
NoWaitL .T. se especificó la palabra clave NOWAIT con la palabra clave PREVIEW
OffL .T. se especificó la palabra clave OFF
OutputToN Tipo de salida especificada en la cláusula TO: 0 = no se especificó TO, 1 - printer, 2 - file
PDSetupL .T. se especificó la palabra clave PDSETUP con el comando LABEL
PlainL .T. se especificó la palabra clave PLAIN
PreviewL .T. se especificó la palabra clave PREVIEW
PromptL .T. se especificó la palabra clave PROMPT
RangeFromN Página de inicio especificada en la cláusula RANGE, o 1 si no se especifica
RangeToN Página final especificada en la cláusula RANGE, o 1 si no se especifica
RecordTotalN Número total de registros en el cursor principal del informe
SampleL .T. se especificó la palabra clave SAMPLE con el comando LABEL
SummaryL .T. se especificó la palabra clave SUMMARY
ToFileC Nombre del archivo especificado en la cláusula TO FILE
ToFileAdditiveL .T. se especificó la palabra clave ADDITIVE en la salida a un archivo.
WindowC Nombre de la ventana especificada en la cláusula TO FILE

Un comentario especial sobre el control de la sesión de datos. En realidad hay 3 sesiones de datos involucradas cuando se ejecuta un informe. La primera sesión de datos es en la que se instancia el ReportListener. Esta será la predeterminada.

La segunda es la sesión de datos, en la que se abre el cursor FRX. La propiedad FRXDataSession contiene el ID de la sesión de datos para este cursor, entonces, utilice SET DATASESSION TO This.FRXDataSession si necesita acceder al FRX.

La tercera es la sesión de datos, de los datos del informe. Si el informe tiene sesión privada de datos, tendrá una sesión de datos única, en caso contrario, será la sesión de datos predeterminada. La propiedad CurrentDataSession dice cuál sesión de datos utilizar, entonces, si el ReportListener necesita acceder a los datos del informe, necesita SET DATASESSION TO This.CurrentDataSession. Recuerde guardar la sesión de datos del ReportListener y regresar a esta sesión después de seleccionar o la sesión de datos del FRX o la sesión de datos del informe.

Eventos

Hay varios tipos de eventos, así que los vamos a ver de forma separada.

Eventos de informe

Los eventos de informe son aquellos que se disparan cuando algo afecta al informe como tal.

EventoParámetroDescripción
LoadReportNingunoAnálogo al evento Load de un formulario en que es el primer evento que se dispara antes de que se carga el FRX y se abra la bandeja de la impresora, es un lugar donde puede cambiar el contenido del FRX en el disco o cambiar el entorno de impresora antes de que se ejecute el informe.
UnloadReportNingunoComo el evento UnLoad de un formulario, se dispara después que el informe se ha ejecutado. Se utiliza típicamente para las tareas de limpieza.
BeforeReportNingunoSe dispara después que se carga el FRX; pero antes de que se ejecute el informe
AfterReportNingunoSe dispara después que se ejecuta un informe
OnPreviewClosetlPrintSe dispara cuando el usuario cierra la ventana preliminar o imprime un informe desde la presentación preliminar.
Eventos de banda

Se disparan cuando se procesa una banda

EventoParámetroDescripción
BeforeBand tnObjCode,tnFRXRecnoSe dispara antes de que se procese una banda. Este primer parámetro representa el valor con el campo OBJCODE en el FRX para la banda especificada, y el segundo es el número de registro en el cursor FRX para el registro de banda.
AfterBandtnObjCode,tnFRXRecnoSe dispara después que se procesa una banda. Los mismos parámetros que BeforeBand
Eventos de objetos

Estos eventos se activan cuando se procesa un objeto

EvaluateContents(tnFRXRecno, toObjProperties): Este evento se activa al inicio del procesamiento de la banda para cada objeto campo (pero no etiqueta), y da la oportunidad de cambiar la apariencia del campo. El primer parámetro es el número de registro FRX para el objeto campo que se está procesando y el segundo es un objeto que contiene propiedades sobre el objeto campo. Las propiedades que contiene este objeto se muestran en la siguiente tabla. Puede cambiar alguna de estas propiedades para cambiar la apariencia del campo en el informe. Si hace esto, establezca la propiedad Reload del objeto = .T. para notificar al motor de informe que desea cambiar una o más propiedades adicionales. Además, devuelva .T. si otros listeners pueden hacer otros cambios al campo. Veremos algunos ejemplos prácticos más adelante.

PropiedadTipoDescripción
FillAlphaNAlfa, o transparente o color de relleno. Permite un control más fino que simplemente opaco o transparente. Los rangos de valores son desde 0 para transparente a 255 para opaco.
FillBlueNValor azul en un color RGB() para el color de relleno.
FillGreenNValor verde en un color RGB() para el color de relleno.
FillRedNValor rojo en un color RGB() para el color de relleno.
FontNameCNombre de la fuente
FontSizeNTamaño de la fuente
FontStyleNUn valor que representa el estilo de la fuente. Son valores acumulativos 1- negrita, 2 – itálica, 4 - subrayada, 128 - tachada
PenAlphaNEl valor alfa del color del pincel
PenBlueNValor azul en un color RGB() para el color del pincel.
PenGreenNValor verde en un color RGB() para el color del pincel.
PenRedNValor rojo en un color RGB() para el color del pincel.
ReloadLEstablezca a .T. para notificar al motor de informe que ha cambiado una o más propiedades
TextCEl texto de salida del objeto campo

AdjustObjectSize(tnFRXRecno, toObjProperties): este evento se activa al inicio del procesamiento de la banda para cada objeto shape. Le brinda la posibilidad de cambiar un objeto, y es utilizado usualmente cuando desea sustituir una forma con un objeto custom y necesita dimensionar el objeto dinámicamente. El primer parámetro es el número de registro del RFX para el objeto shape que está siendo procesado y el segundo es un objeto que contiene las propiedades del objeto shape. Las propiedades que este objeto contiene se muestran en la siguiente tabla. Si cambia Height o Width, entonces establezca la propiedad Reload en .T. para notificar al motor de informes que ha cambiado esas propiedades.

PropiedadTipoDescripción
HeightNAltura del objeto, de 0 a 64000. Cambiar estos valores a un valor mayor (un valor menor se ignora) provoca que otros objetos flotantes en la banda sean movidos hacia abajo y la banda se expanda.
LeftNPosición a la izquierda del objeto. Solo-lectura, se proporciona solo para referencia
TopNPosición superior del objeto. Solo-lectura, se proporciona solo para referencia
WidthNAltura del objeto, de 0 a 64000. Cambiar estos valores no altera el comportamiento del motor de informe; pero el nuevo valor se pasa al método Render para que el listener pueda hacer algo con el.
ReloadLEstablezca a .T. para notificar al motor de informe que ha cambiado una o más propiedades

Render(tnFRXRecno, tnLeft, tnTop, tnWidth, tnHeight, tnObjectContinuationType, tcContentsToBeRendered, tnGDIPlusImage): este método es muy grande, es llamado al menos una vez por cada objeto que se está procesando (puede ser llamado más de una vez por objetos que están en las bandas o páginas). Como el resto de eventos de objeto, su primer parámetro es el número del registro FRX para el objeto que se procesa. Los cuatro parámetros siguientes representan la posición y el tamaño del objeto. tnObjectContinuationType indica si un campo, forma o línea del objeto se expande en una banda o página, contiene 4 valores posibles:

ValorDescripción
0Este objeto está completo, no continúa en la siguiente banda o página
1El objeto ha comenzado; pero no finaliza en la página actual.
2El objeto está en la mitad de su generación, ni comienza ni termina en la página actual.
3El objeto ha terminado en la página actual.

tcContentsToBeRendered contiene el texto o el campo o el nombre de archivo de la imagen. Para campos, el contenido se proporciona en Unicote, adecuadamente traducido a la configuración local utilizando la información FontCharSet del registro FRX.
Utilice STRCONV() para convertir la cadena si desea hacer algo con ella, por ejemplo almacenarla en una tabla. Se utiliza tnGDIPlusImage si una imagen viene de un campo General y la propiedad SendGDIPlusImage es .T., contiene el controlador gráfico de la imagen.
Puede colocar código en este método si desea re-generar un objeto de forma diferente a como sería hecho. Nota, sin embargo, antes de nada, necesitará llamar a funciones API GDI+, ya que esto es el alma de todo. Para mayor información sobre GLI+, ver http://msdn.microsoft.com/library/en-us/gdicpp/GDIPlus/GDIPlusReference/FlatGraphics.asp. Más adelante veremos un ejemplo de sobrescribir esta generación.

Métodos

EventoParámetroDescripción
CancelReport NingunoPermite al código Xbase terminar el informe más pronto. Es requerido para que el ReportListener pueda realizar la limpieza necesaria para prevenir roturas de configuración, cerrar la bandeja de impresión, etc.
OutputPagetnPageNo,teDevice,tnDeviceType,tnLeft,tnTop
tnWidth,tnHeight,tnClipLeft,tnClipTop,tnClipWidth,tnClipHeight
Saca la página que se está generando por el dispositivo indicado. Los parámetros opcionales permiten al Listener especificar exactamente qué área del dispositivo debe ser utilizada para la generación cuando el tipo de dispositivo es un contenedor.
Se detalla con más detenimiento más adelante.
IncludePageInOutputtnPageNo Indica si la página especificada se incluye en la salida o no.
SupportsListenerTypetnTypeIndica si el listener soporta el tipo de salida especificada
GetPageWidthNingunoDevuelve el ancho de página durante la ejecución de un informe
GetPageHeightNingunoDevuelve la altura de página durante la ejecución de un informe
DoStatustcMessageProporciona un retorno no modal durante la ejecución de un informe
UpdateStatusNingunoActualiza el retorno de interfaz de usuario
ClearStatusNingunoElimina el retorno no modal de interfaz de usuario
DoMessagetcMessage,tiParams,tcTitleProporciona un retorno modal durante la ejecución de un informe si es AllowModalMessages .T. En caso contrario, llama a DoStatus

El método OutputPage justifica mayor discusión. El parámetro tnDeviceType determina el tipo de salida que este método debe realizar, determina además qué tipo de parámetros se esperan para tcDevice. La siguiente tabla lista los tipos de salida soportados en la clase base ReportListener. Las subclases pueden soportar otros tipos de salida.

tnDeviceTypeDescripciónteDevice
0ImpresosaControlador de impresora
1 Dispositivo gráficoControlador gráfico GDI+
2Ventana preliminarXbaseReferencia a control de salida VFP
101Archivo EMFNombre de archivo
102Archivo TIFFNombre de archivo
103Archivo JPEGNombre de archivo
104Archivo GIFNombre de archivo
105Archivo BMPNombre de archivo
201Archivo TIFF Multi-páginaNombre de archivo (el archivo ya debe existir)

Existen cuatro valores para la propiedad y cada uno afecta de forma diferente en cómo es llamado:

ListenerTypeDescripción
0OutputPage es llamado por el motor de informes después que es generada cada página para salir a la impresora. Pasa 0 (impresora) a tnDeviceType y el controlador GDI+ para la impresora para teDevice.
1OutputPage es llamado por la presentación preliminar (previewer) para mostrar la página especificada después que se completa toda la generación.
2OutputPage es llamado por el motor de informe después que cada página es generada; pero no es enviada a la salida por impresora, pasa -1 para tnDeviceType y 0 para teDevice
3OutputPage debe ser llamado manualmente para la página especificada después que se completa la generación.

A propósito, debido a que los report listeners utilizan código Xbase, ahora es posible hacer traza del código durante la ejecución de informes, algo que no era posible antes y que causó mucha frustración entre los que utilizaban funciones definidas por el usuario (UDF) en los informes.

Registrar Listeners

Ahora que sabemos la apariencia que tienen los ReportListener, vamos a crear diferentes subclases y darles el comportamiento que necesitamos. Antes de hacer esto, veamos cómo hablarles de ellos a ReportOutput.APP.

Debido a que ReportBuilder.APP (para ver más detalles sobre ReportBuilder.APP, vea mi artículo Extender el sistema de informes en tiempo de diseño), ReportOutput.APP utiliza una tabla de registro para definir sobre qué listeners tiene conocimiento. Aunque esta tabla se genera dentro de ReportOutput.APP puede crear una copia de ella llamada OutputConfig.DBF utilizando DO (_ReportOutput) WITH -100 (este mecanismo pudiera cambiar en futuras versiones). Si ReportOutput.APP encuentra una tabla con este nombre en el directorio actual o la ruta VFP, será utilizado como una fuente para los listeners que busca cuando ejecuta un informe. He aquí la estructura de la tabla:

Nombre de campoTipoValoresDescripción
OBJTYPEI100 para un registro listenes
Otro tipo de registro se utiliza también, vea la documentación de VFP para los detalles.
OBJCODEICualquier valor válido de tipo de listener
Tipo de listener (por ejemplo 1 para presentación preliminar)
OBJNAMEV(60) 
Clase a instanciar
OBJVALUEV(60) 
Biblioteca de clase en la que se encuentra
OBJINFOM 
Aplicación que contiene la biblioteca de clases.
   
 
   
 
-- 
-
-- 
-

Observe que ReportOutput.APP sólo busca el primer registro con OBJTYPE = 100 y OBJCODE establece el tipo de listener deseado. Entonces, va a necesitar eliminar o desregistrar (establecer OBJCODE a otro valor, algo así como sumarle 100) otros registros listeners del mismo tipo. InstallListener.PRG puede controlar esto por usted. Pase la clase listener, la biblioteca y el tipo y el se encarga de los detalles.

lparameters tcClass, ;
  tcLibrary, ;
  tnObjCode
local lcClass, ;
  lcLibrary 
* Abre la tabla report listener (la crea si es necesario). 
if not used('OutputConfig')
  if not file('OutputConfig.DBF')
    do (_ReportOutput) with -100
  endif not file('OutputConfig.DBF')
  use OutputConfig
endif not used('OutputConfig') 
* Si ya está el listener especificado, se asegura de habilitarlo 
* dando a ObjCode un valor correcto. En otro caso, lo agrega. 
lcClass = upper(tcClass)
lcLibrary = upper(tcLibrary)
locate for ObjType = 100 and upper(ObjName) == lcClass and ;
upper(ObjValue) == lcLibrary
if found()
  replace ObjCode with tnObjCode
else
  insert into OutputConfig ;
    (ObjType, ;
    ObjCode, ;
    ObjName, ;
    ObjValue) ;
    values ;
    (100, ;
    tnObjCode, ;
    tcClass, ;
    tcLibrary)
endif found() 
* Deshabilita cualquier otro listener con el mismo ObjCode 
* dando un valor inusual a ObjCode
replace ObjCode with ObjCode + 100 for ObjType = 100 and ;
ObjCode = tnObjCode and not upper(ObjName) == lcClass 
* Limpia y cierra. 
use 

Vea que no necesita registrar un listener para utilizarlo, simplemente instáncielo manualmente y pase la referencia a la cláusula OBJECTO del comando REPORT. Este mecanismo es un poco más trabajoso; pero le da el control al desarrollador, no requiere una copia externa de la tabla de registro ReportOutput.APP, y permite hacer cadenas de report listeners juntos, como veremos en breve.

SFReportListener

Debido a que nunca debemos utilizar las clases base de VFP, he creado una subclase de la clase ReportListener llamado SFReportListener, definido en SFReportListener.VCX. Proporciona unos pocos métodos utilitarios que requieren la mayoría de los listeners. Permite además encadenar listeners ya que posee la propiedad Successor que puede contener una referencia de objeto a otro listener y teniendo todas las llamadas a eventos el mismo método en el objeto sucesor, si este existe, usando un código similar a:

if vartype(This.Successor) = 'O'
  This.Successor.ThisMethodName()
endif vartype(This.Successor) = 'O'

SelectFRX activa las sesiones de datos en la que está un cursor FRX, guarda el puntero de registro en una propiedad de usuario y opcionalmente lo posiciona el FRX si el número de registro fue especificado.

lparameters tnFRXRecno
This.nDataSession = set('DATASESSION')
set datasession to This.FRXDataSession
This.nFRXRecno = recno()
if pcount() = 1
  go tnFRXRecno
endif pcount() = 1 

UnselectFRX restablece el puntero de registro FRX y restablece la sesión de datos anterior.

go This.nFRXRecno
set datasession to This.nDataSession 

GetReportObject utiliza estos dos métodos para devolver un objeto del registro especificado en el FRX. Esto facilita examinar la información sobre el objeto FRX.

lparameters tnFRXRecno
local loObject
This.SelectFRX(tnFRXRecno)
scatter memo name loObject
This.UnSelectFRX()
return loObject

Debido a que eventos tales como Render y EvaluateContents se ejecutan solo una vez por cada registro del FRX; pero para cada objeto que será generado (esto es, se dispara tantas veces como registros hay en el FRX y la cantidad de registros en el dato que ha de ser procesado), puede querer minimizar la cantidad de trabajo hecho por estos métodos. Por ejemplo, si almacena una directiva en el campo memo User que dice al listener cómo procesar un objeto de informe, cualquier código que analiza el memo User será llamado muchas veces, incluso si realmente necesita sólo una vez. Entonces, he agregado una matriz propiedad de usuario llamada aRecords que puede contener cualquier información sobre los registros FRX que necesite. Por ejemplo, la primera vez que el objeto es generado, puede verificar si hay una entrada en aRecords para el objeto. Si no hay, hace lo que sea necesario (como parking User) y guardar alguna información en aRecords. De otro modo, simplemente recupera la información de aRecords.

Para soportar este concepto, el evento BeforeReport dimensiona aRecords a tantos registros como hay en el FRX, así no habrá que redimensionarlo mientras se ejecuta el informe.

with This 
  * Dimensiona aRecords tantos registros como hay en FRX para no tener que
  * redimensionarlo cuando se ejecuta el informe. 
  .SelectFRX()
  if alen(.aRecords, 2) > 0
    dimension .aRecords[reccount(), alen(.aRecords, 2)]
  else
    dimension .aRecords[reccount()]
  endif alen(.aRecords, 2) > 0
  .UnSelectFRX() 
  * Controla la cadena de sucesión. 
  if vartype(.Successor) = 'O'
    .Successor.CurrentDataSession =.CurrentDataSession
    .Successor.BeforeReport()
  endif vartype(.Successor) = 'O'
endwith 

Debido a que OutputPage es llamado con un número de página determinado, y un sucesor no tiene necesariamente conocer qué página es, este método guarda el número de página pasado a una propiedad de usuario nOutputPageNo, que otros listeners podrán utilizar. La clase SFReportListenerProgressMeter, por ejemplo, lo utiliza para mostrar qué página está siendo procesada en la barra de salida.

SFReportListenerDirective

SFReportListenerDirective es una subclase de SFReportListener. Su objetivo es soportar las directivas en el memo User que dicen al listener cómo procesar el objeto de informe. Un ejemplo de una directiva puede ser *:LISTENER ROTATE = -45, que dice al listener que rote este objeto 45 grados en contra de las manecillas del reloj. Debido a que User puede ser utilizado para varios propósitos, las directivas soportadas por SFReportListenerDirective deben comenzar con *:LISTENER (aquellos que utilizaron GENSCRNX en los días de FoxPro 2.x reconocerán este tipo de directiva).

Son controladas diferentes directivas por diferentes objetos. Si sólo cambian propiedades del objeto que se está generando, no tienen necesariamente que ser subclases de ReportListener (algunos de los ejemplos que veremos más tarde se basan en Custom). Debido a que puede utilizar múltiples directivas para un mismo objeto, SFReportListenerDirective mantiene una colección de controles de directivas y llama a la apropiada cuando es necesario.

El método INIT crea la colección de controladores de directivas y lo llena con los controladores más comúnmente utilizados. Controladores adicionales pueden ser agregados en una subclase o después de que esta clase sea instanciada agregando a la colección (observe que la palabra clave utilizada por la colección debe estar en mayúsculas).

with This
  .oDirectiveHandlers = createobject('Collection')
  loHandler = newobject('SFDynamicForeColorDirective', 'SFReportListener.vcx')
  .oDirectiveHandlers.Add(loHandler, 'FORECOLOR')
  loHandler = newobject('SFDynamicBackColorDirective', 'SFReportListener.vcx')
  .oDirectiveHandlers.Add(loHandler, 'BACKCOLOR')
  loHandler = newobject('SFDynamicStyleDirective', 'SFReportListener.vcx')
  .oDirectiveHandlers.Add(loHandler, 'STYLE')
  loHandler = newobject('SFDynamicAlphaDirective', 'SFReportListener.vcx')
  .oDirectiveHandlers.Add(loHandler, 'ALPHA') 
  * Se redimensiona aRecords ala cantidad de columnas necesarias
  dimension .aRecords[1, 2]
endwith 

El método EvaluateContents verifica si hemos creado ya el objeto actual del informe para directivas, en ese caso, la segunda columna de la fila correspondiente en aRecords será .T. Si no, llamamos a UpdateListenerDirectives para ver si hay alguna directiva para el objeto de informe y actualizar aRecords adecuadamente. Entonces, se verifica la primera columna en aRecords que contiene una colección de directivas para este objeto de informe (porque podemos tener más de una directiva para un objeto dado). Si es así, cada elemento de la colección contiene el nombre de un controlador de directiva en la colección oDirectiveHandlers y el argumento de directiva (por ejemplo, si la directiva es *:LISTENER ROTATE = -45, este argumento será "-45"). Llamamos al método HandleDirective de cada controlador que se supone que hagamos, pasando sus propiedades en el objeto pasado y el argumento de directiva.

lparameters tnFRXRecno, ;
  toObjProperties
local loDirective, ;
  loHandler
with This 
  * Por razones de rendimiento, queremos minimizar la cantidad de trabajo que hacemos en
  * este método, entonces chequeamos la colección de registros para ver si ya lo hemos
  * procesado. En caso contrario, llamamos a UpdateListenerDirectives para que
  * actualice la colección 
  if not .aRecords[tnFRXRecno, 2]
    .UpdateListenerDirectives(tnFRXRecno)
  endif not .aRecords[tnFRXRecno, 2] 
  * Si tenemos cualquier directiva de listener para este registro, llamamos a cada una. 
  if vartype(.aRecords[tnFRXRecno, 1]) = 'O'
    for each loDirective in .aRecords[tnFRXRecno, 1]
      loHandler = .oDirectiveHandlers.Item(loDirective.DirectiveHandler)
      loHandler.HandleDirective(This, loDirective.Expression, ;
        toObjProperties)
    next loDirective
  endif vartype(.aRecords[tnFRXRecno, 1]) = 'O'
endwith 

UpdateListenerDirectives llama al método GetReportObject (definido en la clase padre) para encontrar el objeto de informe en el FRX y devuelve un objeto SCATTER NAME que contiene propiedades para cada campo en el registro FRX. Luego analiza el memo User del objeto informe, buscando directivas *:LISTENER

Se verifica la validez de cualquier directiva que se haya encontrado mirando si existe un controlador para ella en la colección oDirectiveHandlers y si es así, la directiva se agrega al objeto colección almacenado en la primera columna de la fila aRecords para el objeto de informe. La segunda columna de la fila aRecords es igual a .T. si alguna directiva ha sido encontrada o no para indicar que este objeto de informe ha sido procesado y no necesita ser procesado nuevamente.

lparameters tnFRXRecno
local loFRX, ;
  laLines[1], ;
  lnLines, ;
  lnI, ;
  lcLine, ;
  lnPos, ;
  lcClause, ;
  lcExpr
  loHandler, ;
  loDirective
with This 
  * Establecemos un Flag indicando que hemos procesado este registro. 
  .aRecords[tnFRXRecno, 2] = .T. 
  * Toma el registro especificado de FRX y procesa cualquier línea en el memo User. 
  loFRX = .GetReportObject(tnFRXRecno)
  lnLines = alines(laLines, loFRX.User)
  for lnI = 1 to lnLines
    lcLine = alltrim(laLines[lnI]) 
    * Si encontramos una directiva de listener y es una directiva que soportamos, 
    * la agrega, a la colección, junto a la expresión especificada 
    * (crea la colección la primera vez que es necesario) 
    if upper(left(lcLine, 10)) = '*:LISTENER'
      lcLine = substr(lcLine, 12)
      lnPos = at('=', lcLine)
      lcClause = alltrim(left(lcLine, lnPos - 1))
      lcExpr = alltrim(substr(lcLine, lnPos + 1))
      lnPos = ascan(.aDirectiveHandlers, lcClause, -1, -1, 1, 15)
      try
        loHandler = .oDirectiveHandlers.Item(upper(lcClause))
        loDirective = createobject('Empty')
        addproperty(loDirective, 'DirectiveHandler', lcClause)
        addproperty(loDirective, 'Expression', lcExpr) 
       * Crea una colección de todas las directivas que tiene este registro. 
       if vartype(.aRecords[tnFRXRecno, 1]) <> 'O'
         .aRecords[tnFRXRecno, 1] = createobject('Collection')
       endif vartype(.aRecords[tnFRXRecno, 1]) <> 'O'
       .aRecords[tnFRXRecno, 1].Add(loDirective)
      catch
      endtry
    endif upper(left(lcLine, 10)) = '*:LISTENER'
  next lnI
endwith

17 de mayo de 2017

Extender el sistema de informes en VFP 9.0. Tiempo de ejecución. Parte 1/3

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


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

Resumen

Además de la extensibilidad en tiempos de diseño que fue discutida en la primera parte de este documento (ver), VFP 9 proporciona también la posibilidad de extender el comportamiento de los informes aun cuando están siendo ejecutados. En este documento aprenderá sobre el concepto del listener incorporado en VFP 9, verá cómo recibe eventos cuando está siendo ejecutado un informe y cómo puede crear sus propios listeners para proporcionar diferentes tipos de salidas además de las tradicionales pantalla e impresora.

Introducción

Uno de los mayores cambios en VFP 9.0 es la increíble mejoría hecha en el sistema de informes. Existen varios aspectos para esto, uno de los cuales expondremos en este documento: la capacidad de extender el Diseñador de informes en tiempo de ejecución

El equipo de desarrollo de VFP tuvo varios objetivos en mente al trabajar en las mejoras en tiempo de ejecución, entre ellos:

  • Obtener más tipos de salidas, además de pantalla e impresora.
  • Utilizar GDI+ para las salidas de informes. Esto proporciona mejoras significativas, mejor dibujado, una escala más suave de subida y bajada de imágenes y fuentes, y capacidades adicionales como son rotación de texto.
  • Proporciona un sistema de informes más flexible y extensible.

VFP 9 incluye ambos motores de informe el viejo y el nuevo, así que puede ejecutar los informes en el motor que desee. Sin embargo, una vez que vea los beneficios del nuevo motor de informes, seguramente no querrá volver al viejo estilo.

Arquitectura del motor de informes.

Antes de VFP 9, el motor de informes era monolítico, controlaba todo y salvo algunas excepciones (funciones definidas por el usuario, expresiones para OnEntry y OnExit de las bandas, etc.), no podíamos interactuar con el informe durante su ejecución.

El nuevo motor de informe de VFP 9 divide la responsabilidad entre el motor de informes, que ahora controla la manipulación de datos y posición de los objetos y un objeto nuevo conocido como report listener, el que controla el dibujado y la salida. Dado que los report listeners son clases, podemos ahora interactuar con el proceso de informes de formas que antes no soñamos.

Los report listeners realizan la salida de dos formas: El modo "una página cada vez" ("Page-at-a-time") dibuja una página y luego la saca. Esto es utilizado típicamente para salidas por impresora.

En el modo "todas las páginas a la vez" ("all-pages-at-once") el report listener configura todas las páginas y las almacena en memoria. Luego las saca según la demanda. Eso es utilizado fundamentalmente en presentación preliminar.

Nueva sintaxis de informes

VFP 9 admite la ejecución de informes utilizando el viejo motor de informes, simplemente utilice el comando REPORT como hacía antes (aunque como veremos en un momento, puede utilizar un comando nuevo para sobrescribir el comportamiento de REPORT). Para obtener un comportamiento del estilo nuevo, utilice la cláusula OBJECT del comando REPORTS. Esta cláusula OBJECT admite dos formas para su uso: especificar un report listener y especificar un tipo de informe. Microsoft se refiere a esto como "salida asistida por objetos"
Un report listener es un objeto que proporciona el nuevo comportamiento para informes. Los report listeners se basan en una nueva clase base en VFP 9, ReportListener. Veremos esta clase con más detalle luego. Para decirle a VFP que utilice un listener específico para un informe, instancie la clase listener y luego especifique el nombre de objeto de la cláusula OBJECT del comando REPORT. He aquí un ejemplo.

loListener = createobject('MyReportListener')
report form MyReport object loListener 

Si no ha instanciado un listener manualmente, puede decirle a VFP que lo haga automáticamente especificando el tipo de informe.
report form MyReport object type 1

Los tipos definidos son: 0 para salida por impresora, 1 para presentación preliminar, 2 para modo "una página cada vez" pero no por impresora, 3 para "todas las páginas a la vez"; pero no invoca la presentación preliminar, 4 para salida XML, 5 para salida HTML. Se pueden definir otros tipos de salidas.

Al ejecutar un informe de esta forma, es invocada la aplicación especificada en la nueva variable del sistema _REPORTOUTPUT (de forma predeterminada ReportOutput.APP de la carpeta raíz de VFP). Esta aplicación determina qué clase listener debe instanciar para un tipo especificado. Esto se hace mirando el tipo de listener en una tabla de registro de listener, más adelante veremos este aspecto en detalles. Si encuentra la clase deseada, instancia la clase y da una referencia al objeto listener al motor de informe. De esta manera, utilizando OBJECT TYPE un tipo en el comando REPORT es esencialmente lo mismo que:

loListener = .NULL.
do (_ReportOutput) with SomeType, loListener
report form MyReport object loListener 

Probablemente piense "Pero si tengo montones de informes en mi aplicación. ¿Tendré que modificar cada comando REPORT por toda mi aplicación?" Afortunadamente, existe una forma muy sencilla: SET REPORTBEHAVIOR 90 establece la salida asistida por objetos como predeterminada. Esto significa que el comando REPORT se comporta como si hubiera especificado OBJECT TYPE 0 cuando utiliza la cláusula TO PRINT u OBJECT TYPE 1, cuando utiliza la cláusula PREVIEW. Por su parte, SET REPORTBEHAVIOR 80 revierte a VFP 80 y al comportamiento anterior. Si la mayoría de los informes de su aplicación trabajan bien con salida asistida por objetos, utilice SET REPORTBEHAVIOR 90 en el inicio de la aplicación. Debido a que existen diferencias en cuanto al dibujado y la configuración entre los dos estilos, algunos informes pueden necesitar unos retoques para que trabajen adecuadamente con el nuevo estilo. Entonces o bien, los retoca o bien utiliza SET REPORTBEHAVIOR 80 para esos informes.

La aplicación ReportOutput.APP es ante todo una fábrica de objetos, instancia el listener apropiado para un informe. Además, incluye algunos listeners que proporcionan salida XML y HTML. Sin embargo, debido a que es una aplicación Xbase puede sustituirla por su propia aplicación, estableciendo el valor correspondiente en _REPORTOUTPUT.

Otra variable de sistema, nueva es _REPORTPREVIEW y especifica el nombre de una aplicación Xbase utilizada como ventana para los informes. De forma predeterminada, esta variable apunta a ReportPreview.APP en la carpeta raíz de VFP; pero se puede sustituir por una aplicación propia si se desea. Por ejemplo, puede querer un formulario que proporcione un lista de informes a la izquierda y un área de vista preliminar a la derecha. Cuando el usuario hace doble clic en un informe, se muestra la vista preliminar en su área. La posibilidad de usar una aplicación Xbase como ventana preliminar nos proporciona casi control ilimitado sobre la apariencia y comportamiento de la presentación preliminar. Este documento no discute la aplicación ReportPreview.APP ni los requerimientos para sustituirla.

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

8 de mayo de 2017

Extender informes en VFP 9.0 - Tiempo de Diseño (Parte 1/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)

Resumen

Entre las novedades y mejoras del sistema de informes de VFP 9.0 está la posibilidad de extender el Diseñador de informes para proporcionar una escritura de informes más flexible, poderosa y fácil de utilizar por su equipo de desarrollo e incluso por sus usuarios finales. En este documento aprenderá sobre la nueva aplicación Report Builder, como captura y manipula eventos invocados por el Diseñador de informes, y cómo puede crear sus propios manipuladores para extender el Diseñador de Informes de VFP de manera que nunca antes imaginó.

Introducción

Uno de los mayores cambios en VFP 9.0 es la increíble mejoría hecha en el sistema de informes. Uno de los aspectos mejorados será expuesto en este documento: la capacidad de extender el Diseñador de informes.

El equipo de desarrollo de VFP tuvo varios objetivos en mente cuando trabajaron en las mejoras en tiempo de diseño, entre ellos:

  • Simplificar y mejorar la interfaz de usuario. En VFP 8.0 y anterior, había muchos diálogos relativos a informes. Algunos tenían una interfaz verdaderamente inusual y otros aun generaban más cuadros de diálogo. Puede ver un ejemplo de interfaz incómoda al hacer doble clic en un objeto de texto: esta acción mostraba la ventana propiedades para los objetos; pero no permitía cambiar la fuente ni el color del objeto. En VFP 9.0 hay pocos diálogos, porque todas las propiedades para un objeto se encuentran ahora en un solo lugar.
  • Nuevas características. En VFP 9.0 están disponibles novedades como la protección de informe y objetos, reutilización de Entorno de datos, posicionamiento absoluto y etiquetas en tiempo de diseño.
  • Extensible. VFP tuvo siempre un IDE muy extensible, pero VFP 9.0 rompió el record de extensibilidad. Ahora es posible reemplazar algunos o todos los diálogos del Diseñador de informes y cambiar completamente la interfaz de usuario y el comportamiento de los eventos de informe si desea.

Eventos de informes en tiempo de Diseño

Versiones anteriores de VFP brindaban una mínima posibilidad de personalizar el Diseñador de informes, prácticamente sólo la apariencia de la ventana en el que se mostraba el diseñador. Uno de los éxitos alcanzados en VFP 9.0 es proporcionar un mecanismo para entrar dentro del comportamiento del Diseñador de informes, de tal forma que su apariencia y comportamiento pueden ser personalizados lo más posible. Esto fue implementado con eventos en el Diseñador de informes, tales como abrir un informe, agregar un objeto, que se pasan a un componente Xbase, que puede realizar cualquier acción necesaria cuando ocurren eventos.

La nueva variable de sistema _REPORTBUILDER apunta a una aplicación (En este documento se le llama "report builder application"), que recibe la notificación sobre los eventos del Diseñador de informes. Cuando ocurre un evento de informes en tiempo de diseño y la variable _REPORTLISTENER apunta a una aplicación existente, el Diseñador de informes crea una sesión privada de datos y abre una copia del archivo FRX que está siendo editado actualmente en esa sesión de datos, entonces, llama a la aplicación del generador de informes (report builder application). De forma predeterminada, _REPORTBUILDER apunta a ReportBuilder.APP, que se encuentra en el directorio raíz de VFP; pero, si se desea, se puede sustituir por otra aplicación. Si se especifica una aplicación inexistente no se genera un error; pero, en ese caso, no se desatan los eventos. Cualquier aplicación especificada por _REPORTLISTENER debe ser modal por naturaleza porque el Diseñador de informes espera a llamarla y recibir de esa aplicación un valor que indica qué ocurre.

El Diseñador de informes pasa los siguientes parámetros a la aplicación del generador de informes cuando ocurre un evento

ParámetroTipoDescripción
ReturnFlagsNPasado por referencia con valor inicial -1. Se utiliza para devolver valores al Diseñador de informes.
EventTypeNUn entero que representa el evento que ha ocurrido.
CommandClausesOObjeto Empty con las propiedades que indican qué cláusulas fueron utilizadas en el comando CREATE / MODIFY REPORT
DesignerSessionIDNID de la sesión de datos del Diseñador de informes.

ReturnFlags se utiliza para devolver valores al Diseñador de informes. Los posibles valores se muestran en la tabla a continuación, junto con las constantes que representan esos valores, que deben ser definidas en FOXPRO.H en la versión 9.0. Los valores son flags de bit que, si se desea, se pueden sumar.

ValorConstanteDescripción
1FRX_REPBLDR_HANDLE_EVENTEl evento ha sido controlado por la aplicación del generador de informes, por lo que la acción predeterminada del Generador de informes no se realizará (es como una especie de NODEFAULT)
2FRX_REPBLDR_RELOAD_CHANGESLa aplicación del generador de informes hizo cambios en el cursor FRX por lo que el Diseñador de informes debe recargar los cambios en su copia interna del FRX.

EventType contiene un valor que identifica el evento que ha ocurrido. Los valores posibles, junto a las constantes que los representan y el tipo de evento (evento de informe, de objeto o de banda), se muestran en la siguiente tabla. La tabla indica además, si el evento se puede suprimir agregando 1 al parámetro ReturnFlags. "Debe eliminar registro" significa que una vez que debido a que un registro de objeto creado nuevo ya ha sido agregado al FRX, debe suspender la creación de un objeto, debe eliminar ese registro en el cursor FRX y establecer el parámetro ReturnFlags en 3 (el evento fue controlado y los cambios deben ser re-cargados).

ValorConstanteDescripciónTipoSe puede eliminar
1FRX_BLDR_EVENT_PROPERTIESSe ha invocado una ventana Propiedades, haciendo doble clic o por acción del menú (para el informe, objeto o banda)Informe,
Objeto,
Banda
Si
2FRX_BLDR_EVENT_OBJECTCREATESe ha creado un objeto o banda Objeto,
Banda
Si (debe eliminar)
3FRX_BLDR_EVENT_OBJECTCHANGESe ha movido o redimensionado un objeto (parece que se dispara sólo para bandas)Objeto, BandaSi
4FRX_BLDR_EVENT_OBJECTREMOVESe ha eliminado un objeto o banda Objeto,
Banda
Si
5FRX_BLDR_EVENT_OBJECTPASTEUno o más objetos se han pegado al informe desde el portapapeles. Este evento se dispara para cada objeto que ha sido pegadoObjetoSi (debe eliminar)
6FRX_BLDR_EVENT_REPORTSAVEEl informe ha sido guardadoInforme No
7FRX_BLDR_EVENT_REPORTOPENEl informe ha sido abiertoInforme No
8FRX_BLDR_EVENT_REPORTCLOSEEl informe ha sido cerradoInforme Si
9FRX_BLDR_EVENT_DATAENVEl entorno de datos ha sido abierto InformeSi
10FRX_BLDR_EVENT_PREVIEWMODESe ha invocado el modo Presentación preliminar (preview)InformeSi
11FRX_BLDR_EVENT_OPTIONALBANDSSe ha invocado el diálogo Bandas opcionalesInformeSi
12FRX_BLDR_EVENT_DATAGROUPINGSe ha invocado el diálogo Agrupación de datosInformeSi
13FRX_BLDR_EVENT_VARIABLESSe ha invocado el diálogo Variables InformeSi
14FRX_BLDR_EVENT_EDITINPLACESe ha presionado Ctrl.+E sobre un objeto etiquetaObjetoSi
15FRX_BLDR_EVENT_SETGRIDSCALESe ha invocado el diálogo Escala de rejillaInformeSi
16FRX_BLDR_EVENT_OBJECTDROPUno o más objetos han sido creados por operaciones arrastrar y soltar desde el Entorno de datos, una DBC o el Administrador de informes. Este evento se dispara una vez para cada objeto que ha sido creado, y si se crea una etiqueta, también se dispara una vez por cada etiqueta.ObjetoSi (debe eliminar)
17FRX_BLDR_EVENT_IMPORTDEDesde el menú informes se seleccionó "Load data environment" - Cargar entorno de datosInformeSi
18FRX_BLDR_EVENT_REPORTEl informe será impresoInformeSi
19FRX_BLDR_EVENT_QUICKREPORTDesde el menú informes se seleccionó "Quick Report" - Informe rápidoInformeSi

Las propiedades de CommandClauses se muestran en la siguiente tabla.

PropiedadTipoDescripción
AddTableToDEL.T. si la opción Add Table (Agregar tabla) al Entorno de datos se marcó en el diálogo del Quick Report
AliasL.T. si la opción Add Alias (Agregar alias) al Entorno de datos se marcó en el diálogo del Quick Report o se especificó la cláusula ALIAS en el comando CREATE REPORT FROM
FieldListOUna colección de campos numéricos que representan los campos especificados en el diálogo Quick Report o en la cláusula FIELDS del comando CREATE REPORT FROM
FileCEl nombre del archivo FRX abierto en el Diseñador de informes. Este archivo puede no existir realmente si se utiliza CREATE REPORT / LABEL.
FormL.T. si se escogió un diseño en el diálogo Quick Report o la cláusula FORM se especificó en el comando CREATE REPORT FROM y .F. si se especificó si un diseño de columna o COLUMN
FromCContiene un nombre de tabla especificado en el comando CREATE REPORT FROM
InScreenL.T. si la cláusula IN SCREEN fue especificada en el comando CREATE/MODIFY REPORT/LABEL
InWindowCEl nombre de la ventana especificada es la cláusula IN <nombre ventana> del comando CREATE/MODIFY REPORT/LABEL
IsCreateL.T. si el comando fue CREATE REPORT/LABEL o .F. para MODIFY REPORT/LABEL
IsReportL.F. si el comando fue CREATE REPORT/LABEL o .T. para MODIFY REPORT/LABEL
IsQuickReportFromMenuL.T. si se ha invocado Quick Report
NoEnvironmentL.T. si la cláusula NOENVIRONMENT fue especificada en el comando MODIFY REPORT/LABEL
NoOverwriteL.T. si la cláusula NOOVERWRITE fue especificada en el comando CREATE REPORT/LABEL
NoWaitL.T. si la cláusula NOWAIT fue especificada en el comando CREATE/MODIFY REPORT/LABEL
ProtectedL.T. si la cláusula PROTECTED fue especificada en el comando MODIFY REPORT/LABEL
TitlesL.T. si la opción Titles se marcó en el diálogo Quick Report o la cláusula TITLES se especificó en el comando CREATE REPORT FROM
WidthNNúmero de columnas especificado en el comando CREATE REPORT FROM
WindowCNombre de la ventana especificada en la cláusula WINDOW <nombre> en el comando CREATE/MODIFY REPORT/LABEL

La aplicación del generador de informes se ejecuta dentro de una sesión privada de datos que contiene el cursor FRX. El Diseñador de informes pasa su propio datassesion ID a la aplicación del generador de informes en caso que necesite acceder a las tablas abiertas en el Entorno de datos del Diseñador de informes.

El cursor FRX del Diseñador de informes creado por la aplicación del generador de informes tiene el alias FRX. El puntero de registro está en el registro del objeto para el que se invocó el evento, puede ser el registro Encabezado de informes (primer registro en el cursor) si es un evento de informe, en lugar de un evento de objetos. El registro para cualquier objeto seleccionado en el Diseñador de informes tiene el campo CURPOS = .T. Con esto hay una complicación, debido a que CURPOS se utiliza por el registro encabezado de informes para guardar el valor de la configuración Show Position (Mostrar posición), debe ignorar este registro si está buscando los registros con CURPOS = .T. Por ejemplo, para contar el número de objetos seleccionados utilice:

count for CURPOS and recno() > 1 

ReportBuilder.APP

De forma predeterminada, _REPORTBUILDER apunta a ReportBuilder.APP en el directorio raíz de VFP. Esta aplicación proporciona un framework para controlar eventos de informes en tiempo de diseño, además proporciona un nuevo conjunto de diálogos atractivos y funcionales que sustituye los nativos utilizados para el Diseñador de informes. ReportBuilder.APP puede ser distribuida con sus aplicaciones para proporcionar este comportamiento en entorno de ejecución.

Además de ser llamado automáticamente por el Diseñador de informes, puede llamar a ReportBuilder.APP manualmente para cambiar su comportamiento para la sesión actual de VFP (no escribe la configuración para ninguna localización externa, como una tabla, archivo INI o registro de Windows, así que con una excepción, que veremos enseguida, el estado no se conserva de una sesión a otra)

  • Si la llama sin parámetros o pasando 1, mostrará un diálogo en el que puede cambiar el comportamiento de la aplicación del generador de informes. Puede además hacer clic derecho en el diálogo Propiedades y escoger Opciones para llamar al cuadro de diálogo Opciones.
  • Para mostrar un informe pase 2 y opcionalmente el nombre de un archivo FRX.
  • Para utilizarlo como controlador de registro pase 3 y el nombre de un archivo DBF (veremos esto más tarde)

El cuadro de diálogo Opciones tiene las siguientes características:

  • Puede definir que ocurre cuando ReportBuilder.APP recibe un evento de informe. Las posibilidades son buscar una clase controladora en la tabla del controlador de registro, utilizar un controlador "depurador" para eventos (muestra un diálogo en el que se ve el FRX en un control Grid y permite modificarlo así como otros parámetros), utilizar un controlador "inspector de eventos" (muestra información sobre el evento y el FRX en la ventana MESSAGEBOX()), o ignorar los eventos de informe.
  • Puede especificar que tabla controladora de registro debe ser utilizada, o hacer una copia de una tabla interna (más adelante lo veremos)

Existen varias formas de extender la funcionalidad en el Diseñador de informes.

  • Puede sustituir ReportBuilder.APP con una aplicación propia cambiando el valor de _REPORTBUILDER.
  • Puede envolver ReportBuilder.APP cambiando _REPORTBUILDER a una aplicación propia y haciendo que la aplicación haga una llamada a ReportBuilder.APP. Para aquellos que utilizaron FoxPro 2.x, pudiera recordarle al GENSCRNX.
  • Puede registrar objetos controladores de eventos en la tabla registro ReportBuilder.APP. Sospecho que esta será la opción más popular porque ReportBuilder.APP proporciona un framework de generador de informe y permite enfocarse simplemente en el control de los eventos de informes.

Registrar controladores de eventos de informes

Si desea crear su propio controlador de eventos de informes que sea llamado por ReportBuilder.APP cuando se disparen eventos de objeto, se requieren dos pasos. El primer paso es crear una clase que implemente el comportamiento deseado. Puede tener un método Execute (Ejecutar) que acepte un evento de objeto como parámetro, porque es lo que espera ReportBuilder.APP. El segundo paso es registrar el controlador en la tabla de registro ReportBuilder.APP. Veremos la segunda tarea primero, luego dedicaremos el resto del documento discutiendo sobre la primera.

Cuando ocurre un evento de informe, ReportBuilder.APP mira en la tabla de registro para encontrar un controlador para el evento. De forma predeterminada utiliza la tabla del registro del controlador generada dentro del archivo APP. Esta tabla proporciona manipuladores para muchos eventos de informes entonces los diálogos del nuevo Diseñador de informes XBase se utilizan en lugar de los nativos. Sin embargo, si desea registrar sus propios controladores, tiene que decirle a ReportBuilder.APP que utilice otra tabla de registro de controlador.

Existen diferentes formas de hacerlo:

  • DO (_REPORTBUILDER) y haga Clic en el botón Create Copy (Crear copia) para escribir la tabla del registro del controlador interno y uno externo. De forma predeterminada, esta copia se llamará ReportBuilder.DBF. Cuando ReportBuilder.APP comienza, busca una tabla llamada ReportBuilder.DBF en el directorio actual o la ruta de VFP. Si encuentra esta tabla, la utiliza en lugar de la tabla interna. Entonces, simplemente al crear esa copia significa que el ReportBuilder.APP la utilizará sin tener que hacer nada más.
  • DO (_REPORTBUILDER) y haga Clic en el botón Choose (Seleccionar) para seleccionar la tabla a utilizar.
  • Como mencioné antes, DO (_REPORTBUILDER) WITH 3, <DBF a utilizar> para especificar la tabla del manipulador deseado sin ninguna interfaz de usuario.

Una vez que se ha especificado una tabla externa de registro del manipulador, puede agregar manualmente o modificar registros en esa tabla o en el diálogo Opciones del ReportBuilder.APP, haga clic en el botón Explore registry (Explorar registro) y modificar los registros en el diálogo resultante.

La tabla de registro del controlador tiene la siguiente estructura:

Nombre del campoTipoValoresDescripción
REC_TYPEC(1)E, F, G, H, o XEspecifica el tipo de registro (se detalla luego):
H: registro de controlador de evento de informe
F: registro de filtro de evento de informe
X: registro de controlador de salida
G: Registro de cobertura de GetExpresion (wrapper record)
E: Clase editor de extensión en tiempo de ejecución.
HNDL_CLASSC(25) Nombre de clase
HNDL_LIBC(25) Biblioteca de clases
EVENTTYPEI0 - 16 ó -1ID de tipo de evento de informe (-1 significa cualquier evento)
OBJTYPEI0 - 10 ó -1Tipo de objeto de informe (-1 para cualquier objeto)
OBJCODEI0 - 26 ó -1Código de objeto de informe (-1 para cualquier código)
NATIVEL.T. ó .F..T. para forzar el evento de informe que sea devuelto al Diseñador de informes para un comportamiento nativo
DEBUGL.T. ó .F..T. para forzar el controlador de depuración para que sea utilizado para esta combinación evento/objeto
FLTR_ORDRC(1)" ", "1", "2", ..Solo para controladores de filtros y salida, especifica el orden en que deben ser aplicados.
NOTESC(35)  No se utiliza por el ReportBuilder.APP.

Cuando ocurre un evento de informe, ReportBuilder.APP busca la clase controladora para instanciar buscando el registro donde EVENTYPE se corresponda con el ID de evento de informe (o sea igual a -1). OBJTYPE se corresponda con la columna OBJTYPE del registro seleccionado del FRX (o sea igual a -1) y OBJCODE se corresponda con la columna OBJCODE del registro seleccionado del FRX (o sea igual a -1). Debido a que ReportBuilder.APP utiliza el primer registro del controlador que encuentre que cumpla las condiciones, si desea implementar controladores propios, puede ser necesario eliminar o inhabilitar los controladores integrados (built-in).

Una forma en que puede inhabilitar el registro sin eliminarlo es cambiar EVENTTYPE a un valor no válido. Yo utilizo 100 como valor para EVENTTYPE para inhabilitar el registro, debido a que puedo re-habilitarlo fácilmente substrayendo 100.

Como puede ver en la tabla, REC_TYPE registra diferentes tipos de registros. Veamos los tipos posibles:

  • Un controlador de evento de informe controla un evento de informe.
  • Un filtro de evento de informe es una clase que obtiene una falla antes de lo que lo hace el controlador de eventos del informe. Un filtro de evento de informe es una clase del informe. Una vez que haya sido instanciado un único controlador, ReportBuilder.APP instancia la clase especificada en todos los registros filtrados, en el orden FLTR_ORDER, y llama su método HasHandled. Si algún filtro de objeto devuelve .T. desde este método (lo que significa que el filtro ha controlado el evento), entonces no ocurre ningún otro procesamiento y el Diseñador de informes es informado que el evento ha sido manipulado.

Como puede ver de esta descripción, aunque ambos pueden responder a eventos de informe, existe una gran diferencia entre filtros de eventos y controladores de eventos:

  • Los filtros son instanciados en cada evento de informe, mientras un controlador es instanciado sólo para el evento si ha sido registrado en la tabla de registro del controlador.
  • Todos los filtros se instancian en un evento, mientras hay un solo controlador.

Esto significa que los filtros son buenos para el comportamiento que se desea que ocurra en múltiples o todos los eventos, mientras los controladores son específicos para un tipo de evento.

  • Los controladores de salida son similares a la combinación de controladores de filtros y eventos, en que después que los otros procesos se han hecho, ReportBuilder.APP ejecuta todos los controladores de salida instanciando las clases adecuadas en el orden FLTR_ORDER y llamando al método Execute. Estos controladores son justamente para garantizar un comportamiento de limpieza post-evento.
  • GetExpresion proporciona un envoltorio para sustituir el diálogo GETEXPR.
  • Los editores de extensión en tiempo de ejecución, sustituyen el diálogo mostrado cuando hace clic en el botón Edit Settings (Modificar configuración) para la propiedad Run-time Extension en la ficha Otros de la ventana propiedades de un objeto.

Proceso de control de eventos de informe

Cuando se llama a ReportBuilder.APP porque ha ocurrido un evento de informe la aplicación hace lo siguiente:

  • Ejecuta cualquier filtro registrado como se ha descrito antes. Se suspende el procesamiento si alguno devuelve .T.
  • Trata de encontrar el controlador del evento utilizando el siguiente patrón de diseño.
    REC_TYPE="H", EVENTTYPE, OBJTYPE, OBJCODE ...
  • Si se encuentra un controlador, ReportBuilder.APP instancia y llama su método Execute. Si no hay ningún controlador, el evento se pasa al Diseñador de informe y se realiza el comportamiento nativo.
  • Si se encuentra un controlador, ReportBuilder.APP ejecuta todos los registros de controladores de salida como se ha descrito antes.

Interfaces de controlador

Los filtros de evento, controladores de evento y controladores de salida, envolturas GetExpression y editores de extensiones en tiempo de ejecución pueden basarse en cualquier clase y tener sólo un único método requerido.

Los métodos son:

  • Filtros: HasHandled (toevent). ReportBuilder.APP pasa a este método una referencia de objeto de evento, el que va a mirar más tarde, cuyas propiedades contienen información del evento. Para evitar futuros procesamientos, devuelve .T. desde este método, en tal caso, la propiedad ReturnFlags del evento debe estar establecida adecuadamente.
  • Controlador de evento: Execute(toevent). Este método también recibe un objeto evento. El valor devuelto no es importante; pero si lo es la propiedad ReturnFlags del objeto evento.
  • Controlador de salida: Execute(). El valor devuelto no es importante.
  • Envoltura GetExpression: GetExpression(tcDefaultExpr [, tcCalledFrom]). Donde tcDefaultExpr es la expresión predeterminada para el diálogo, tcCalledFrom indica de dónde ha sido llamado este método desde "PrintWhenExpression", "FieldExpression", "OLEBoundField", "OLEBoundExpression", "BandGroupOnExpression", "VariableValueToStore", o "VariableInitialValue". El valor devuelto es la expresión
  • Editores de Extensión en tiempo de ejecución. Igual que para controladores de eventos.

Objetos de eventos

ReportBuilder.APP pasa un objeto de evento al método HasHandled de los filtros y al método Execute de los controladores de evento. Este evento incluye como propiedades el parámetro pasado por el Diseñador de informes al ReportBuilder.APP, más alguna otra información útil.

PropiedadTipoDescripción
BuilderPathCRuta del ReportBuilder.APP.
CommandClausesOEl mismo objeto CommandClauses pasado al ReportBuilder.APP
DefaultRecnoIPuntero del registro en el cursor FRX
DefaultSessionIDISesión de datos del Diseñador de informes (el 4to parámetro pasado desde el Diseñador de informes)
EventTypeITipo de eventos (el 2do parámetro pasado desde el Diseñador de informes)
FRXCursorOObjeto auxiliar que contiene funciones para interactuar con el cursor FRX
FRXSessionIDISesión de datos en la que se abre el cursor FRX (la sesión predeterminada cuando se instancia el controlador de eventos.)
ObjCodeIValor del campo OBJCODE del registro seleccionado en el cursor FRX
ObjTypeIValor del campo OBJCTYPE del registro seleccionado en el cursor FRX
ProtectedL.T. si el Diseñador de informes se invocó con la cláusula PROTECTED
ReturnFlagsIEl valor de esta propiedad es devuelto por el Diseñador de informes en el primer parámetro pasado desde el Diseñador de informes. Inicialmente se establece igual a 1 (FRX_REPBLDR_HANDLE_EVENT), debe establecerlo a 3 si su clase hace cambios en el cursor FRX que necesite ser recargado al diseño (layout). (FRX_REPBLDR_HANDLE_EVENT + FRX_REPBLDR_RELOAD_CHANGES)
SelectedObjectCountICantidad de objetos seleccionados en el diseño de informes, determinado por CURPOS = .T. en el cursor FRX (no cuenta el registro cabecera)
SessionData OUna referencia al par Nombre-Valor del objeto controlador utilizado para guardar el dato entre llamados a ReportBuilder.APP
UniqueID Valor de el campo UNIQUEID del registro seleccionado en el cursor FRX


Tiene tres métodos públicos:

MétodoParámetrosDevuelveDescripción
GetEventTypeTexttiEventCDevuelve el nombre de un tipo de evento dado.
GetExtensionEditorNoneODevuelve una referencia a una clase editora de extensión en tiempo de ejecución como especificado en la tabla de registro del controlador.
GetTargetTypeTexttiObjType, tiObjCodeCDevuelve el nombre de un objeto dado tipo objeto / código objeto. Envuelve el método GetTargetTypeText() para el objeto FRXCursor.

Objeto ayudante FRXCursor

Este objeto, referenciado en la propiedad FRXCursor, proporciona métodos que puede encontrar útiles cuando trabaja con un FRX.

MétodoParámetrosDevuelveDescripción
BinStringToInttcBytesNDevuelve un equivalente del dato binario en una cadena de bytes.
BinToInttcValueIConvierte una cadena binaria a entero
CreateBandCursor[tcFRXAlias] LCrea un cursor con el alias "Bands" que contiene información útil sobre las bandas de informe.
CreateCalcResetOnCursor[tcFRXAlias]LCrea un cursor con el alias "Reset_On" que contiene información útil para cada opción en el cuadro combinado Calculation Reset.
CreateDefaultPrintEnvCursor[tcFRXAlias
[, tcDestAlias]]
LCrea un cursor de un registro nombrado "DefPrnEnv" con la misma estructura como el FRX, con la información de entorno predeterminado de impresora cargado en EXPR, TAG y TAG2
CreateGroupCursor[tcFRXAlias] LCrea un cursor con el alias "Groups" que contiene información útil sobre las agrupaciones de datos del informe.
CreateMemberDataCursor[tcFRXAlias
[, tcMDAlias]]
LCrea un cursor de un registro con los atributos del MemberData del informe, extrayendo y convirtiendo en XML desde el desde el campo STYLE actualmente seleccionado en el FRX.
CreateObjectCursor[tcFRXAlias
[, tiOption ]]
LCrea un cursor con el alias "Objects" que contiene información útil sobre los objetos de diseño del informe. Llama a CreateBandCursor() si es necesario. Los valores para tiOption son:
0 (predeterminado) todos los objetos en el FRX, ignorando los elementos agrupados
1: sólo registros seleccionados (CURPOS = .T.)
2: todos los objetos, mostrando los objetos agrupados como un único registro
3: ruptura o salto de elemento de grupo
Algunos registros pueden ser marcados para borrar (deleted). Utilice SET DELETED ON o ignore manualmente los registros marcados.
CreateVariableCursor[tcFRXAlias] LCrea un cursor con el alias "Vars" que contiene información útil sobre las variables de informe.
FRUToPixelstnFRU IDevuelve los píxeles correspondientes para un número dado de FRU (FixPro Reports Units, 1/10000 pulgadas)
GetBandFortcUniqueID
[, tlStart]
ODevuelve un objeto que contiene información sobre el inicio o fin de banda que rodea al objeto de informe dado.
GetFRUTextHeighttcText, tcTypeFace, tiSize
[, tcStyle]
NDevuelve la altura en FRU del texto y especificación de fuente dados.
GetFRUTextWidthtcText, tcTypeFace, tiSize
[, tcStyle]
NDevuelve el ancho en FRU del texto y especificación de fuente dados.
GetFRXTimeStampttDateTimeIDevuelve una marca de tiempo FoxPro para una fechahora dada.
GetObjectsInBandtcBandID [, tlRecnos] ODevuelve una colección de valores UNIQUE o números de registros (dependiendo del valor del segundo parámetro) para cada objeto contenido en una banda especificada.
GetReportAttributetcAttrib
[, tiAlternate]
ValueDevuelve un atributo dado del encabezado de informe. Algunos atributos tienen información asociada que se puede obtener pasando un segundo parámetro opcional de 1
GetSelectedObjectCountNingunoIDevuelve la cantidad de objetos actualmente seleccionados (CURPOS = .T.) en el cursor FRX.
GetTargetTypeTexttiObjType, tiObjCodeCDevuelve el nombre de un tipo de objeto o código de objeto dado
GetTimeStampStringtiTimeStampCConvierte una marca de tiempo de FoxPro en una cadena legible
HasBandtiObjCodeLDevuelve .T., si el informe contiene una banda del tipo dado.
HasDetailHeadertcDetailBandIDLDevuelve .T., si el registro de banda especificado tiene un registro de encabezado de banda asociado.
HasProtectionFlagtcBytes, tiFlagLDevuelve .T. si el dato binario dado (bytes) tiene un conjunto de flag de protección especificados.
InsertBandtiObjCode-Inserta una banda de un tipo dado en el cursor FRX. Asume que se ha seleccionado el cursor FRX y está posicionado en la localización correcta para insertar.
InsertDetailBandNinguno-Inserta un registro de Banda de detalle en el cursor FRX. Asume que se ha seleccionado el cursor FRX y está posicionado en el registro adecuado para insertar el registro de detalle.
InsertDetailHeaderFooterNinguno-Inserta un registro de Encabezado / Pie de Banda de detalle en el cursor FRX. Asume que se ha seleccionado el cursor FRX, que está posicionado en el registro de banda de detalle al que hay que agregar registro de Encabezado/Pie de Banda y que esta Banda de detalle aun no tiene registro de Encabezado/Pie de Banda
InsertSummaryBandtlNewPage, tlPageHeader, tlPageFooter- Inserta una banda Resumen (Summary) en el cursor FRX. Asume que se ha seleccionado el cursor FRX y que no tiene aun esta banda.
InsertTitleBandtlNewPage-Inserta una banda Title en el cursor FRX. Asume que se ha seleccionado el cursor FRX y que no tiene aun esta banda.
IntToBintiValueCConvierte un entero a una cadena binaria
IntToBinStringtiValue CDevuelve una cadena de bytes de una versión binaria de un entero.
PixelsToFRUtnPixelsNDevuelve el correspondiente valor FRU para un número determinado de píxeles
StoreMemberDataCursor[tcFRXAlias
[, tcMDAlias]]
LToma un cursor de un registro de los atributos de informe MemberData, convierte los atributos del MemberData XML y los guarda en el campo STYLE del registro actualmente seleccionado del FRX
SynchObjectPositions Ninguno L Reinicia los objetos no marcados para borrar en el FRX relativo al iniciar la banda en la que están. Asume que los cursores Bandas y Objetos han sido creados, la tabla actualmente seleccionada es el cursor FRX y no necesita restablecer el puntero registro.