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