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

No hay comentarios. :

Publicar un comentario