Mostrando las entradas con la etiqueta Informes. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Informes. Mostrar todas las entradas

20 de noviembre de 2021

Controlar la configuración de un informe en tiempo de ejecución

Articulo original: Controlling report settings at run time
http://www.ml-consult.co.uk/foxst-12.htm
Autor: Mike Lewis
Traducido por: Luis María Guayán


Cómo permitir que los usuarios elijan opciones de impresora para informes VFP.

Este artículo se aplica a Visual Foxpro 8.0 y versiones anteriores. Si está utilizando una versión posterior, consulte la nota al final del artículo.

Probablemente sepa que, al diseñar un informe de Visual FoxPro, puede seleccionar el tamaño de página, la orientación y el origen del papel en el cuadro de diálogo "Configurar impresión". Este es el cuadro de diálogo al que llega cuando elige "Archivo" - "Configuración de página" y luego hace clic en el botón "Configurar impresión" dentro del diseñador de informes.

Pero hay un inconveniente. Si realiza algún cambio en la configuración de impresión de esta manera, sus usuarios no podrán anularlos en tiempo de ejecución.

Por ejemplo, si configura el origen del papel en "Bandeja superior", el informe siempre intentará seleccionar esa bandeja. No puede usar el comando REPORT FORM ... PROMPT para anularlo en tiempo de ejecución, porque el cuadro de diálogo PROMPT no incluye una configuración para el origen del papel. (Pero puede usar el cuadro de diálogo PROMPT para seleccionar una impresora diferente).

SYS(1037) tampoco ayudará. Normalmente, esa función muestra el cuadro de diálogo "Configurar página" y permite al usuario cambiar la configuración dentro de él. Los cambios afectarán a todos los resultados impresos de la sesión actual. Pero si cambió alguna de las configuraciones de página predeterminadas dentro del diseñador de informes, el cuadro de diálogo SYS(1037) no tendrá ningún efecto en ese informe. Ni siquiera puede usarlo para enviar el informe a una impresora diferente.

Hackear el FRX

Afortunadamente, existe una solución. Implica una pequeña triquiñuela tocando el archivo del informe.

Como ya se sabe, el formato interno de un informe de VFP, es decir, el archivo FRX, es el mismo que el de una DBF. De ello se deduce que se puede abrir el archivo como si fuera una tabla de FoxPro. También puede examinarlo y editarlo. Ahora, no es necesario que le digamos que editar un archivo de informe de esta manera es peligroso. Si cambia un campo incorrecto por un valor incorrecto, podría arruinar su diseño cuidadosamente planificado. Así que tenga cuidado con lo que sigue, y asegúrese de tener una copia de seguridad antes de comenzar.

El primer paso es deshacerse de la configuración de la impresora que se guardó en el diseñador de informes. Para hacerlo, abra y explore el informe desde la Ventana de Comandos. Por ejemplo, si el informe se llama Cliente, escriba lo siguiente:

USE Customer.frx
BROWSE

En la Ventana Examinar, busque un registro donde el campo Objtype sea igual a 1 y el campo Objcode igual a 53 (generalmente es el primer registro). En este registro, busque los campos Expr, Tag y Tag2. Estos son campos Memos que contienen la configuración que realizó en el cuadro de diálogo "Configuración de impresión" del diseñador de informes. Tag y Tag2 contienen valores binarios. Expr contiene texto sin formato, que se verá así:

DRIVER=WINSPOOL
DEVICE=HP LaserJet 6P/6MP - Enhanced
OUTPUT=LPT1:
ORIENTATION=0
PAPERSIZE=9
PAPERLENGTH=2794
PAPERWIDTH=2159
COPIES=1
DEFAULTSOURCE=4
PRINTQUALITY=-4
COLOR=2

Ahora siga adelante y elimine el contenido de estos tres campos Memo. Esto devolverá todas las configuraciones guardadas a sus valores predeterminados. Si desea permitir que el usuario elija el tamaño de la página, el origen del papel o la orientación en tiempo de ejecución, ahora puede llamar a SYS(1037) para hacer el trabajo.

Todo o nada

Pero, ¿qué sucede si desea permitir que el usuario cambie algunas de las configuraciones pero no otras? ¿O desea almacenar configuraciones no predeterminadas en el informe, pero desea permitir que el usuario las anule en determinadas circunstancias? Llamar a SYS(1037) no ayudará, porque es todo o nada. No puede usarlo para permitir que el usuario cambie algunas configuraciones, pero evitar que cambie otras. En cualquier caso, dicha función no funcionará si ha realizado algún cambio en la configuración desde el diseñador de informes, incluso si son diferentes de los que desea que el usuario configure.

Esto es lo que se haría. Supongamos que siempre desea que el informe se imprima en orientación horizontal en papel A5, pero desea permitir que el usuario cambie el origen del papel entre "Alimentación manual" y "Bandeja inferior".

Comience seleccionando la orientación y el tamaño de la página en el cuadro de diálogo "Configurar impresión" desde el diseñador de informes. Mientras esté allí, configure el origen del papel en el valor predeterminado que desee. Guarde el informe como de costumbre.

En su aplicación, proporcione al usuario un mecanismo para alternar el origen del papel. Puede ser un botón de comando, una casilla de verificación, un comando del menú o lo que sea. Cuando el usuario elija, ejecute un código similar al siguiente:

USE Customer.frx
LOCATE FOR Objtype = 1 AND Objcode = 53
REPLACE Tag WITH ""
REPLACE Tag2 WITH ""
IF "DEFAULTSOURCE=2" $ Expr
  lcNew = "DEFAULTSOURCE=4"
  lcOld = "DEFAULTSOURCE=2"
ELSE
  lcNew = "DEFAULTSOURCE=2"
  lcOld = "DEFAULTSOURCE=4"
ENDIF
REPLACE expr WITH STRTRAN(Expr,lcOld,lcNew)
USE

La próxima vez que se imprima el informe, seleccionará el origen de papel que elija el usuario.

Esto es lo que hace el código. Primero, abre el archivo del informe como una tabla y localiza el registro que contiene la configuración. Luego borra los valores binarios de la configuración. Eso no es un problema, el informe funcionará bien sin ellos.

A continuación, comprueba la configuración existente del origen de papel. Si es 2, lo cambia a 4 y viceversa. Como puede imaginar, esta configuración se especifica mediante el valor DEFAULTSOURCE: 4 significa "Alimentación manual", 2 es "Bandeja inferior". Finalmente, el campo Expr se actualiza con el valor modificado y se cierra el archivo.

¿Cómo supimos qué valores usar para DEFAULTSOURCE? Desde la pantalla de ayuda de la función PrtInfo(). La función PrtInfo(), devuelve la configuración de la impresora actual, utiliza los mismos valores que el campo Expr. Entonces, si desea comprender el contenido del campo, ahí es donde debe buscar.

Una última palabra: cuando distribuya su aplicación, asegúrese de excluir el archivo FRX y su archivo FRT asociado de la compilación. En otras palabras, no vincule estos dos archivos al archivo EXE. Si lo hiciera, el código anterior no podría actualizar los archivos y su usuario vería un error en tiempo de ejecución. En su lugar, déle al usuario una copia independiente de los dos archivos.

Nota para los usuarios de VFP 9.0:

En VFP 9.0, toda la cuestión de la configuración de la impresora en los archivos de informe se ha vuelto mucho más fácil. De hecho, nunca necesitará manipular el archivo FRX de la forma descrita en este artículo. De forma predeterminada, la configuración de la impresora ya no se guarda en el FRX. Independientemente de las configuraciones que estuvieran vigentes en el momento del diseño del informe, puede usar SET PRINTER TO NAME o SYS(1037) para establecer las configuraciones de tiempo de ejecución.

Si desea anular el comportamiento por defecto, es decir, si no desea guardar la configuración del informe en el archivo FRX, abra el menú Informe en el diseñador de informes, y seleccione la opción entorno de la impresora. Puede hacer esto si desea forzar la impresión de un informe en una impresora en particular, por ejemplo. Si desea que la configuración de la impresora se guarde en todos los informes de forma predeterminada, vaya a Herramientas - Opciones - Informes y habilite la opción "Guardar entorno de impresora".


Mike Lewis Consultants Ltd.

12 de mayo de 2018

Escuchar un informe

Artículo original: Listening to a report
http://msdn.microsoft.com/library/en-us/dnfoxtk05/html/ft05b6.asp
Autor:Doug Hennig
Traducido por: Ana María Bisbé York


Microsoft ha abierto la arquitectura del motor de informes en Visual FoxPro 9 al comunicar el motor con la clase base ReportListener. Al subclasear ReportListener, los desarrolladores VFP pueden crear sus propias salidas personalizadas. Este mes, Doug Hennig, nos presenta la nueva clase  ReportListener y nos muestra como solucionar problemas del mundo real.

Estoy seguro de que ya se ha dado cuenta, de que el área que ha recibido mayor impulso en VFP 9.0 es el sistema de  informes. Tanto el Diseñador de informes como el motor de informes (responsable de ejecutar los informes) han recibido grandes mejoras y se han dotado de características nuevas muy interesantes.

Antes de VFP 9.0, el motor de informes era monolítico: controla todo el control de los datos, posicionamiento de objetos, generación, e impresión. El nuevo motor de informe de VFP 9.0 divide la responsabilidad de la generación de informes entre el motor de informes, que controla la generación, y la salida. VFP 9 incluye ambos, el nuevo motor de informes  y el viejo, así que puede ejecutar sus informes en cualquiera de los dos motores. Microsoft se refiere al motor nuevo como salida "asistida por objetos"

Utilizar la salida asistida por objetos

Existen tres formas para decirle a VFP que emplee el nuevo motor de informes:

Instanciar un ReportListener (sea una clase base o una subclase) y especificar su nombre en la nueva cláusula OBJECT del comando REPORT o LABEL. Este es el proceder más flexible porque puede especificar exactamente qué clase listener va a utilizar; pero requiere que modifique los comandos REPORT o LABEL existentes en su aplicación.

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

Especificar el tipo de listener utilizando la cláusula OBJECTTYPE. Existen varios tipos de listeners integrados: 0 significa impresora, 1 significa presentación preliminar, 4 significa XML y 5 significa salida HTML. Puede definir y utilizar tipos personalizados

report form MyReport object type 1 && preview

Utilizar el nuevo comando SET REPORTBEHAVIOR 90 antes de ejecutar el informe, usualmente, se colocará cerca del inicio de la aplicación para que todos los informes utilicen el nuevo motor. Al especificar TO PRINTER en una cláusula del comando REPORT FORM hace que VFP utilice el objecttype = 0 que tiene integrado, de igual forma, si indica PREVIEW hace que VFP utilice el type = 1. Esta es sin duda, la opción más conveniente ante las otras mencionadas, pero no le brinda el control que necesita al instanciar su propio listener. Para restablecer el motor anterior utilice SET REPORTBEHAVIOR 80.

Al ejecutar un informe utilizando alguno de los dos últimos métodos, es llamada la aplicación especificada en la nueva variable de sistema _REPORTOUTPUT  (de forma predeterminada ReportOutput.APP en la carpeta raíz de VFP) para determinar qué clase listener se debe instanciar para el tipo especificado. Sin embargo, debido a que es una aplicación VFP, puede sustituir su propia aplicación estableciendo para ello el valor correspondiente en _REPORTOUTPUT.

Asegúrese de distribuir ReportOutput.APP (o su aplicación que la sustituye) a sus usuarios para que sus aplicaciones puedan utilizar salida asistida por objeto. Nota: En tiempo de ejecución fuera del entorno de desarrollo de VFP _REPORTOUTPUT debe ser establecida explícitamente en su código para apuntar a ReportOutput.APP o su sustituta. Lo mismo se aplica a las variables de sistema. _REPORTPREVIEW (ReportPreview.APP) y, si es necesario, en su aplicación en tiempo de ejecución _REPORTBUILDER (ReportBuilder.APP). Existen otras vías para controlar estos requerimientos, tales como colocar el código desde ReportOutput.APP en su proyecto. Un conjunto de tópicos muy útiles se pueden encontrar en el índice del archivo Ayuda de VFP bajo el título: "Report Output Application."

Dentro de ReportListener

Debido a que ReportListener es una clase base de VFP, se puede subclasear para implementar cualquier comportamiento que se desee. Antes de que pueda crear sus propios listeners, necesita entender qué propiedades, métodos y eventos (PEMs) están disponibles. Por cuestiones de espacio, voy a referirme solamente a las PEMs más importantes, vea la Ayuda de VFP para completar los detalles de todo el conjunto.

Nota de la traductora: Vea el artículo "Extender los informes en tiempo de Ejecución", donde el propio Doug Hennig detalla estas PEMs.

Una de las propiedades más importantes es ListenerType. Esta propiedad dice al report listener cómo debe ser la salida. El valor predeterminado es -1, lo que indica que no se produce salida alguna. Se configura con 0 para impresora o con 1 para salida a una ventana de presentación preliminar. Especificar 2 ó 3 produce resultados interesantes: el informe es ejecutado y las páginas se generan en la memoria; pero en realidad no se produce ninguna salida. Puede utilizar estos valores cuando desea un control sobre el tipo de salida a crear. Por ejemplo, con el ListenerType = 2, VFP genera la primera página y llama al método OutPutPage, luego genera la siguiente página y vuelve a llamar al método OutputPage, y así sucesivamente. Utilizar el 3 para ListenerType provoca que todas las páginas se generen en memoria; pero el método OutputPage no se llama automáticamente para cada página, permitiendo llamar a OutputPage para solicitar desde la memoria las páginas en el orden en que se deseen.

Antes de que se ejecute un informe, el motor de informe abre una copia del informe como un cursor de sólo lectura nombrado FRX en una sesión de datos privada. El ID de esta sesión de datos se guarda en la propiedad FRXDataSession del ReportListener. Si necesita acceder a los datos que serán mostrados en el informe, la propiedad CurrentDataSession indica qué sesión de datos utilizar.

La propiedad CommandClauses contiene una referencia a un objeto que contiene propiedades con información acerca de cómo se ejecutará el informe. Por ejemplo, la propiedad Preview = .T. si el informe se va a mostrar con vista preliminar y la propiedad OutputTo es 1 si el informe se va a imprimir.

El motor de informe dispara eventos del report listener cuando se ejecuta el informe. Existen además otros métodos disponibles que puede llamar si es necesario. Algunos de los eventos y métodos más importantes se muestran en la tabla 1

Tabla 1. Algunos de los métodos y eventos de ReportListener.

EventoDescripción
BeforeReportSe dispara antes de que se ejecuta el informe
AfterReport Se dispara después de que se ejecuta el informe
EvaluateContents Se dispara antes de que se genera un campo
AdjustObjectSize Se dispara antes que se genere una figura o forma
Render Se dispara cuando es generado un objeto
OutputPage Se llama para obtener determinada página en un dispositivo determinado.
CancelReport Se llama para cancelar el informe

EvaluateContents, AdjustObjectSize, y Render son especialmente útiles porque permiten cambiar algo acerca de los objetos antes de que se generen. Junto con otros parámetros (los veremos luego), estos eventos reciben el número de registro para el objeto actual en el cursor FRX. Puede encontrar ese registro en el cursor para determinar si el objeto debe ser generado de forma diferente a lo normal.

_ReportListener

La carpeta FFC del directorio raíz de VFP contiene una nueva biblioteca de clases en VFP 9.0

_ReportListener.VCX.  Esta biblioteca contiene varias subclases ReportListener. Puede considerar utilizar algunas de estas como punto de partida para su propias subclases ReportListener porque estas agregan funcionalidades muy útiles para su clase base.

Una de las mejoras más útiles es el soporte para encadenar varios listeners junto con el mecanismo de sucesores. Al establecer la propiedad Successor de uno de los listeners igual a una referencia a otro informe permite que ambos interactúen con el proceso de generación de informes. Esto significa que puede escribir pequeños listeners que hagan justamente una cosa y encadenar varios para obtener los diferentes efectos unidos. La propiedad IsSuccessor le dice si el listener es el primero, el líder, (es aquel con el que se comunica el motor de informes, porque es el especificado en la cláusula OBJECT de un comando REPORT o LABEL.)

_ReportListener brinda además un grupo de métodos útiles. SetFRXDataSession establece la sesión de datos del cursor FRX. SetCurrentDataSession establece la sesión de datos de los datos del informe. ResetDataSession restablece el ID de la sesión de datos a aquel en el que está el listener.

Ahora que tiene ya esta introducción, es hora de ver algunos ejemplos prácticos.

Establecer formatos dinámicamente.

Una de las primeras cosas en las que yo pensé utilizar un listener fue para establecer formatos dinámicamente. Estoy seguro de que ya han hecho esto antes: Su cliente quiere que un campo sea impreso en rojo bajo determinadas condiciones y en negro, bajo otras. Esto se podía hacer antes de VFP 9.0 creando dos copias para el mismo campo, una en rojo y otra en negro, con condiciones excluyentes en Imprimir cuando, algo como Cantidad >= 100 y Cantidad < 100 y se superponen en el mismo sitio en el informe. Esto trabaja, es cierto; pero es muy difícil de mantener, específicamente si tiene muchos de estos campos en el informe.

Con un report listener, puede cambiar el formato de un campo cuando se está ejecutando el informe en lugar de hacerlo en el Diseñador de informes. La clave para esto es el evento EvaluateContents, el que se dispara justo antes de que se genere cada campo. A este evento se pasa el número de registro del objeto actual del cursor FRX  y una referencia a un objeto que contiene propiedades con información acerca del campo. (vea la Tabla 2)

Tabla 2. Propiedades del objeto oObjPropierties pasado a EvaluateContents.

PropiedadTipoDescripción
FillAlpha N Color (alpha) o transparencia del color de relleno. Rango de valores desde 0 para transparente hasta 255 para opaco.
FillBlue N Porción azul del valor RGB() para el color de relleno
FillGreen N Porción verde del valor RGB() para el color de relleno
FillRed NPorción roja del valor RGB() para el color de relleno
FontName C Nombre de la fuente
FontSize NTamaño de la fuente
FontStyle NUn valor que representa el estilo de fuente. Los valores son: 1 (negrita), 2 (itálica), 4(subrayada) y 128 (tachada)
PenAlpha NColor del pincel (alpha)
PenBlue NPorción azul del valor RGB() para el color de pincel
PenGreen NPorción verde del valor RGB() para el color de pincel
PenRed NPorción roja del valor RGB() para el color de pincel
Reload LIguale a .T. para notificar al motor de informe que ha modificado una o más propiedades.
Text CTexto a mostrar para el objeto campo.
Value VariosValor real del campo a mostrar

DynamicFormatting.PRG, incluido en la descarga que se acompaña, define tres clases.

DynamicListener define qué es lo que debe hacer un listener dinámico y dos subclases: DynamicForeColorListener y DynamicStyleListener, cambian el color  y el estilo respectivamente, del campo que tenga una directiva en su campo memo USER. (Puede acceder al campo memo del objeto a generar desde la ficha Other (Otros) del diálogo Propiedades). La directiva en este ejemplo es una de las siguientes:

*:LISTENER FORECOLOR = ColorExpression
*:LISTENER STYLE = StyleExpression

ColorExpression  es una expresión que evalua a un valor RGB, algo como IIF(Cantidad > 50, RGB(255, 0, 0), RGB (0, 0, 0)), lo que significa utilizar Rojo si la cantidad es mayor que 50 y negro si no lo es. StyleExpression  es una expresión que evalúa un valor de estilo (vea la propiedad FontStyle  en la Tabla 2), algo como IIF(Cantidad > 50, 1, 0), lo que significa utilizar Negrita si la cantidad es mayor que 50 y normal si no lo es.

La primera tarea que debe hacer el listener es identificar qué campos tienen directivas. En lugar de hacerlo cada vez que se evalúa el campo, DynamicListener lo hace en el método BeforeReport. Selecciona la sesión de datos del cursor FRX al llamar a SetFRXDataSession, luego recorre el cursor, buscando registros con la directiva adecuada (especificado en la propiedad .cDirective) en el campo memo USER, y colocando la siguiente expresión la directiva del elemento del registro en el arreglo.

Debido a que DynamicListener hereda las características del sucesor de _ReportListener (que llama automaticamente al método BeforeReport  para todos los listeners sucesores), cada una de nuestras subclases de DynamicListener van a tener su propia matriz aRecords de los registros FRX que se correspondan con la propiedad .cDirective. Así, cada listener puede determinar fácilmente sobre qué campo debe accionar durante la ejecución del informe.

La siguiente tarea es aplicar la directiva cuando sea necesario. EvaluateContents verifica si el elemento actual de la  matriz tiene una expresión y si es así, la evalúa. Luego llama al método ApplyDirective, el que es abstracto en DynamicListener; pero implementado en sus dos subclase. Por ejemplo DynamicForeColorListener establece las propiedades adecuadas del color a toObjProperties e iguala la propiedad Reload  a .T., de tal forma que el motor de informes sabe si el formato del campo cambió.

He aquí otra tarea de mantenimiento: asegúrese de que el ListenerType está definido correctamente. El valor predeterminado, -1, provoca que no haya salida alguna, y no cambia ni siquiera especificando PREVIEW o TO PRINTER en los comandos REPORT o LABEL. Entonces, el método LoadReport establece el valor adecuado de ListenerType si es necesario.

He aquí el código para estas clases:

define class DynamicListener as _ReportListener of ;
  home() + 'ffc\_ReportListener.vcx'
dimension aRecords[1]
&& una matriz con información de cada registro en el FRX
cDirective = ''
&& la directiva que esperamos encontrar

* Si no se ha indicado ListenerType, se determina en dependencia
* de si el informe será impreso o visualizado (preview).
function LoadReport
  with This
    do case
      case .ListenerType <> -1
      case .CommandClauses.Preview
        .ListenerType = 1
      case .CommandClauses.OutputTo = 1
        .ListenerType = 0
    endcase
  endwith
  dodefault()
endfunc

* Antes de ejecutar el informe, recorremos el FRX y
* guardamos información sobre cada campo en el que esperamos 
* una directiva en su campo memo USER en la matriz aRecords.
function BeforeReport
  dodefault()
  with This
    .SetFRXDataSession()
    dimension .aRecords[reccount()]
    scan for .cDirective $ USER
      .aRecords[recno()] = strextract(USER, ;
        .cDirective + ' =', chr(13), 1, 3)
    endscan for .cDirective $ USER
    .ResetDataSession()
  endwith
endfunc

* Si el campo que se va a generar tiene una directiva, se aplica.
function EvaluateContents(tnFRXRecno, toObjProperties)
  local lcExpression, ;
    luValue
  with This
    lcExpression = .aRecords[tnFRXRecno]
    if not empty(lcExpression)
      luValue = evaluate(lcExpression)
      .ApplyDirective(tnFRXRecno, ;
        toObjProperties, luValue)
    endif not empty(lcExpression)
    * Si tenemos un sucesor, le damos participación también.
    if vartype(.Successor) = 'O'
      .Successor.EvaluateContents(tnFRXRecno, ;
        toObjProperties)
    endif vartype(.Successor) = 'O'
  endwith
endfunc

* Método abstracto para aplicar nuestra directiva.
function ApplyDirective(tnFRXRecno, ;
  toObjProperties, tuValue)
endfunc

enddefine

define class DynamicForeColorListener ;
  as DynamicListener
cDirective = '*:LISTENER FORECOLOR'

* Aplicar la directiva.
function ApplyDirective(tnFRXRecno, ;
  toObjProperties, tuValue)
  local lnPenRed, ;
    lnPenGreen, ;
    lnPenBlue
  if vartype(tuValue) = 'N'
    lnPenRed = bitand(tuValue, 0x0000FF)
    lnPenGreen = bitrshift(bitand(tuValue, ;
      0x00FF00), 8)
    lnPenBlue = bitrshift(bitand(tuValue, ;
      0xFF0000), 16)
    with toObjProperties
      if .PenRed <> lnPenRed or ;
        .PenGreen <> lnPenGreen or ;
        .PenBlue <> lnPenBlue
        .PenRed = lnPenRed
        .PenGreen = lnPenGreen
        .PenBlue = lnPenBlue
        .Reload = .T.
      endif .PenRed <> lnPenRed ...
    endwith
  endif vartype(tuValue) = 'N'
endfunc

enddefine

define class DynamicStyleListener as DynamicListener
  cDirective = '*:LISTENER STYLE'

* Aplicar la directiva.
function ApplyDirective(tnFRXRecno, ;
  toObjProperties, tuValue)
  if vartype(tuValue) = 'N'
    toObjProperties.FontStyle = tuValue
    toObjProperties.Reload = .T.
  endif vartype(lnStyle) = 'N'
endfunc

enddefine

El programa TestDynamicFormatting.PRG muestra cómo encadenar estos listeners, de forma tal que ambos se utilicen en la generación del informe.

use _samples + 'Northwind\Orders'
loListener = newobject('DynamicForeColorListener', ;
  'DynamicFormatting.prg')
loListener.Successor = ;
  newobject('DynamicStyleListener', ;
  'DynamicFormatting.prg')
report form TestDynamicFormatting.FRX preview ;
  object loListener

La Figura 1 muestra el resultado de la ejecucón de este programa. En algunos registros, Shipped Date se muestra en rojo y en otros casos en negro. Esto se debe a que tiene la siguiente directiva en su campo memo USER

*:LISTENER FORECOLOR = iif(SHIPPEDDATE > ORDERDATE + 10, rgb(255, 0, 0), rgb(0, 0, 0))


Figura 1

El campo Ship Via aparece a veces en negrita y algunas veces normal, porque tiene la siguiente directiva en su campo memo USER:

*:LISTENER STYLE = iif(SHIPVIA = 3, 1, 0)

(Observe que aunque este campo es numérico, muestra valores como "Fedex," "UPS," o "Mail"  debido a la expresión que hay en el campo.

¿Qué más puede hacer?

Pues casi todo lo que desee. En artículos futuros, mostraré otros listeners que permitan obtener imágenes rotadas, salidas HTML con una tabla de contenidos, y otros muchos tipos de salidas.

El Gurú de VFP Ed Leafe ha creado un sitio Web (http://reportlistener.com) que sirve como un repositorio central de clases reportlisteners. Ya existen varios ejemplos, y habrá más.

Resumen

Microsoft superó todas las ligas de la extensibilidad en VFP 9.0 de muchas formas, incluidas en el generador de informes. Gracias a su clase base, que podemos subclasear, ReportListener permite crear una salida propia, personalizada. Háganme saber, por favor de cualquier listener interesante que ha creado o que ideas se le ocurren para hacer con listeners.

12 de noviembre de 2017

Calcular el dígito de verificación para Rapipago y Pagofacil (Argentina)

En Argentina existen al menos dos redes de cobranzas extrabancarias (Rapipago y Pagofacil) que permiten el pago de facturas, servicios, etc. a través de formularios con códigos de barras que se emiten con una estructura particular para cada caso.

En primer lugar, a la cadena a codificar se le deben agregan 1 ó 2 dígitos de verificación al final. El algoritmo (en código VFP) para la generación de estos dígitos es el siguiente:

CLEAR 

lc = "1234567890123456789012345678901234567890"
? "Sin dígito de verificación...: " + lc
? "Con 1 dígito de verificación.: " + GenerarDigitoVerificador(lc, 1)
? "Con 2 dígitos de verificación: " + GenerarDigitoVerificador(lc, 2)

FUNCTION GenerarDigitoVerificador(tcCadena, tn)
  *---
  * Parámetros
  *   tcCadena = Cadena a generar el/los dígito/s de verificación
  *   tn = Cantidad de dígito/s de verificación (1 ó 2)
  * Retorno:
  *   Caracter: La cadena mas el/los dígito/s de verificación
  *---

  IF EMPTY(tn) OR NOT INLIST(tn, 1, 2)
    tn = 1
  ENDIF

  tcCadena = ALLTRIM(tcCadena)

  LOCAL lnLen, lnIni, lnSum, lcSeq, lcRet
  lnLen = LEN(tcCadena)
  lcSeq = "1" + REPLICATE("3579", CEILING(lnLen/4))
  lnSum = 0
  FOR lnIni = 1 TO lnLen
    lnSum = lnSum + VAL(SUBSTR(tcCadena, lnIni, 1)) * VAL(SUBSTR(lcSeq, lnIni, 1))
  ENDFOR
  lcRet = tcCadena + TRANSFORM(MOD(INT(lnSum / 2), 10))
  IF tn = 2
    tcCadena = ALLTRIM(lcRet)
    lnLen = LEN(tcCadena)
    lcSeq = "1" + REPLICATE("3579", CEILING(lnLen/4))
    lnSum = 0
    FOR lnIni = 1 TO lnLen
      lnSum = lnSum + VAL(SUBSTR(tcCadena, lnIni, 1)) * VAL(SUBSTR(lcSeq, lnIni, 1))
    ENDFOR
    lcRet = tcCadena + TRANSFORM(MOD(INT(lnSum / 2), 10))
  ENDIF
  RETURN lcRet
ENDFUNC

Luego de añadir los dígitos de verificación, ya se puede generar el código de barra Interleved 2 of 5 (I2of5) o Código 128 C, que son los que utilizan ambas empresas.

Si desean realizar toda la tarea con Visual FoxPro, pueden utilizar la clase FoxBarcode que soporta ambas simbologías de códigos de barra. Si utilizan el código I2of5 se debe configurar la propiedad lAddCheckDigit que no genere el dígito de control: (lAddCheckDigit = .F.)

Luis María Guayán

1 de noviembre de 2017

Escuchar un informe

Artículo original: Listening to a report (https://msdn.microsoft.com/en-us/library/ms947682.aspx)
Autor: Doug Hennig
Traducido por: Ana María Bisbé York


Microsoft ha abierto la arquitectura del motor de informes en Visual FoxPro 9 al comunicar el motor con la clase base ReportListener. Al subclasear ReportListener, los desarrolladores VFP pueden crear sus propias salidas personalizadas. Este mes, Doug Hennig, nos presenta la nueva clase  ReportListener y nos muestra como solucionar problemas del mundo real.

Estoy seguro de que ya se ha dado cuenta, de que el área que ha recibido mayor impulso en VFP 9.0 es el sistema de  informes. Tanto el Diseñador de informes como el motor de informes (responsable de ejecutar los informes) han recibido grandes mejoras y se han dotado de características nuevas muy interesantes.

Antes de VFP 9.0, el motor de informes era monolítico: controla todo el control de los datos, posicionamiento de objetos, generación, e impresión. El nuevo motor de informe de VFP 9.0 divide la responsabilidad de la generación de informes entre el motor de informes, que controla la generación, y la salida. VFP 9 incluye ambos, el nuevo motor de informes  y el viejo, así que puede ejecutar sus informes en cualquiera de los dos motores. Microsoft se refiere al motor nuevo como salida "asistida por objetos"

Utilizar la salida asistida por objetos

Existen tres formas para decirle a VFP que emplee el nuevo motor de informes:

Instanciar un ReportListener (sea una clase base o una subclase) y especificar su nombre en la nueva cláusula OBJECT del comando REPORT o LABEL. Este es el proceder más flexible porque puede especificar exactamente qué clase listener va a utilizar; pero requiere que modifique los comandos REPORT o LABEL existentes en su aplicación.

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

Especificar el tipo de listener utilizando la cláusula OBJECTTYPE. Existen varios tipos de listeners integrados: 0 significa impresora, 1 significa presentación preliminar, 4 significa XML y 5 significa salida HTML. Puede definir y utilizar tipos personalizados

report form MyReport object type 1 && preview

Utilizar el nuevo comando SET REPORTBEHAVIOR 90 antes de ejecutar el informe, usualmente, se colocará cerca del inicio de la aplicación para que todos los informes utilicen el nuevo motor. Al especificar TO PRINTER en una cláusula del comando REPORT FORM hace que VFP utilice el objecttype = 0 que tiene integrado, de igual forma, si indica PREVIEW hace que VFP utilice el type = 1. Esta es sin duda, la opción más conveniente ante las otras mencionadas, pero no le brinda el control que necesita al instanciar su propio listener. Para restablecer el motor anterior utilice SET REPORTBEHAVIOR 80.

Al ejecutar un informe utilizando alguno de los dos últimos métodos, es llamada la aplicación especificada en la nueva variable de sistema _REPORTOUTPUT  (de forma predeterminada ReportOutput.APP en la carpeta raíz de VFP) para determinar qué clase listener se debe instanciar para el tipo especificado. Sin embargo, debido a que es una aplicación VFP, puede sustituir su propia aplicación estableciendo para ello el valor correspondiente en _REPORTOUTPUT.

Asegúrese de distribuir ReportOutput.APP (o su aplicación que la sustituye) a sus usuarios para que sus aplicaciones puedan utilizar salida asistida por objeto. Nota: En tiempo de ejecución fuera del entorno de desarrollo de VFP _REPORTOUTPUT debe ser establecida explícitamente en su código para apuntar a ReportOutput.APP o su sustituta. Lo mismo se aplica a las variables de sistema. _REPORTPREVIEW (ReportPreview.APP) y, si es necesario, en su aplicación en tiempo de ejecución _REPORTBUILDER (ReportBuilder.APP). Existen otras vías para controlar estos requerimientos, tales como colocar el código desde ReportOutput.APP en su proyecto. Un conjunto de tópicos muy útiles se pueden encontrar en el índice del archivo Ayuda de VFP bajo el título: "Report Output Application."

Dentro de ReportListener

Debido a que ReportListener es una clase base de VFP, se puede subclasear para implementar cualquier comportamiento que se desee. Antes de que pueda crear sus propios listeners, necesita entender qué propiedades, métodos y eventos (PEMs) están disponibles. Por cuestiones de espacio, voy a referirme solamente a las PEMs más importantes, vea la Ayuda de VFP para completar los detalles de todo el conjunto.

Nota de la traductora: Vea el artículo "Extender los informes en tiempo de Ejecución", donde el propio Doug Hennig detalla estas PEMs.

Una de las propiedades más importantes es ListenerType. Esta propiedad dice al report listener cómo debe ser la salida. El valor predeterminado es -1, lo que indica que no se produce salida alguna. Se configura con 0 para impresora o con 1 para salida a una ventana de presentación preliminar. Especificar 2 ó 3 produce resultados interesantes: el informe es ejecutado y las páginas se generan en la memoria; pero en realidad no se produce ninguna salida. Puede utilizar estos valores cuando desea un control sobre el tipo de salida a crear. Por ejemplo, con el ListenerType = 2, VFP genera la primera página y llama al método OutPutPage, luego genera la siguiente página y vuelve a llamar al método OutputPage, y así sucesivamente. Utilizar el 3 para ListenerType provoca que todas las páginas se generen en memoria; pero el método OutputPage no se llama automáticamente para cada página, permitiendo llamar a OutputPage para solicitar desde la memoria las páginas en el orden en que se deseen.

Antes de que se ejecute un informe, el motor de informe abre una copia del informe como un cursor de sólo lectura nombrado FRX en una sesión de datos privada. El ID de esta sesión de datos se guarda en la propiedad FRXDataSession del ReportListener. Si necesita acceder a los datos que serán mostrados en el informe, la propiedad CurrentDataSession indica qué sesión de datos utilizar.

La propiedad CommandClauses contiene una referencia a un objeto que contiene propiedades con información acerca de cómo se ejecutará el informe. Por ejemplo, la propiedad Preview = .T. si el informe se va a mostrar con vista preliminar y la propiedad OutputTo es 1 si el informe se va a imprimir.

El motor de informe dispara eventos del report listener cuando se ejecuta el informe. Existen además otros métodos disponibles que puede llamar si es necesario. Algunos de los eventos y métodos más importantes se muestran en la tabla 1

Tabla 1. Algunos de los métodos y eventos de ReportListener.

EventoDescripción
BeforeReportSe dispara antes de que se ejecuta el informe
AfterReport Se dispara después de que se ejecuta el informe
EvaluateContents Se dispara antes de que se genera un campo
AdjustObjectSize Se dispara antes que se genere una figura o forma
Render Se dispara cuando es generado un objeto
OutputPage Se llama para obtener determinada página en un dispositivo determinado.
CancelReport Se llama para cancelar el informe

EvaluateContents, AdjustObjectSize, y Render son especialmente útiles porque permiten cambiar algo acerca de los objetos antes de que se generen. Junto con otros parámetros (los veremos luego), estos eventos reciben el número de registro para el objeto actual en el cursor FRX. Puede encontrar ese registro en el cursor para determinar si el objeto debe ser generado de forma diferente a lo normal.

_ReportListener

La carpeta FFC del directorio raíz de VFP contiene una nueva biblioteca de clases en VFP 9.0

_ReportListener.VCX.  Esta biblioteca contiene varias subclases ReportListener. Puede considerar utilizar algunas de estas como punto de partida para su propias subclases ReportListener porque estas agregan funcionalidades muy útiles para su clase base.

Una de las mejoras más útiles es el soporte para encadenar varios listeners junto con el mecanismo de sucesores. Al establecer la propiedad Successor de uno de los listeners igual a una referencia a otro informe permite que ambos interactúen con el proceso de generación de informes. Esto significa que puede escribir pequeños listeners que hagan justamente una cosa y encadenar varios para obtener los diferentes efectos unidos. La propiedad IsSuccessor le dice si el listener es el primero, el líder, (es aquel con el que se comunica el motor de informes, porque es el especificado en la cláusula OBJECT de un comando REPORT o LABEL.)

_ReportListener brinda además un grupo de métodos útiles. SetFRXDataSession establece la sesión de datos del cursor FRX. SetCurrentDataSession establece la sesión de datos de los datos del informe. ResetDataSession restablece el ID de la sesión de datos a aquel en el que está el listener.

Ahora que tiene ya esta introducción, es hora de ver algunos ejemplos prácticos.

Establecer formatos dinámicamente.

Una de las primeras cosas en las que yo pensé utilizar un listener fue para establecer formatos dinámicamente. Estoy seguro de que ya han hecho esto antes: Su cliente quiere que un campo sea impreso en rojo bajo determinadas condiciones y en negro, bajo otras. Esto se podía hacer antes de VFP 9.0 creando dos copias para el mismo campo, una en rojo y otra en negro, con condiciones excluyentes en Imprimir cuando, algo como Cantidad >= 100 y Cantidad < 100 y se superponen en el mismo sitio en el informe. Esto trabaja, es cierto; pero es muy difícil de mantener, específicamente si tiene muchos de estos campos en el informe.

Con un report listener, puede cambiar el formato de un campo cuando se está ejecutando el informe en lugar de hacerlo en el Diseñador de informes. La clave para esto es el evento EvaluateContents, el que se dispara justo antes de que se genere cada campo. A este evento se pasa el número de registro del objeto actual del cursor FRX  y una referencia a un objeto que contiene propiedades con información acerca del campo. (vea la Tabla 2)

Tabla 2. Propiedades del objeto oObjPropierties pasado a EvaluateContents.

PropiedadTipoDescripción
FillAlpha N Color (alpha) o transparencia del color de relleno. Rango de valores desde 0 para transparente hasta 255 para opaco.
FillBlue N Porción azul del valor RGB() para el color de relleno
FillGreen N Porción verde del valor RGB() para el color de relleno
FillRed NPorción roja del valor RGB() para el color de relleno
FontName C Nombre de la fuente
FontSize NTamaño de la fuente
FontStyle NUn valor que representa el estilo de fuente. Los valores son: 1 (negrita), 2 (itálica), 4(subrayada) y 128 (tachada)
PenAlpha NColor del pincel (alpha)
PenBlue NPorción azul del valor RGB() para el color de pincel
PenGreen NPorción verde del valor RGB() para el color de pincel
PenRed NPorción roja del valor RGB() para el color de pincel
Reload LIguale a .T. para notificar al motor de informe que ha modificado una o más propiedades.
Text CTexto a mostrar para el objeto campo.
Value VariosValor real del campo a mostrar

DynamicFormatting.PRG, incluido en la descarga que se acompaña, define tres clases.

DynamicListener define qué es lo que debe hacer un listener dinámico y dos subclases: DynamicForeColorListener y DynamicStyleListener, cambian el color  y el estilo respectivamente, del campo que tenga una directiva en su campo memo USER. (Puede acceder al campo memo del objeto a generar desde la ficha Other (Otros) del diálogo Propiedades). La directiva en este ejemplo es una de las siguientes:

*:LISTENER FORECOLOR = ColorExpression
*:LISTENER STYLE = StyleExpression

ColorExpression  es una expresión que evalua a un valor RGB, algo como IIF(Cantidad > 50, RGB(255, 0, 0), RGB (0, 0, 0)), lo que significa utilizar Rojo si la cantidad es mayor que 50 y negro si no lo es. StyleExpression  es una expresión que evalúa un valor de estilo (vea la propiedad FontStyle  en la Tabla 2), algo como IIF(Cantidad > 50, 1, 0), lo que significa utilizar Negrita si la cantidad es mayor que 50 y normal si no lo es.

La primera tarea que debe hacer el listener es identificar qué campos tienen directivas. En lugar de hacerlo cada vez que se evalúa el campo, DynamicListener lo hace en el método BeforeReport. Selecciona la sesión de datos del cursor FRX al llamar a SetFRXDataSession, luego recorre el cursor, buscando registros con la directiva adecuada (especificado en la propiedad .cDirective) en el campo memo USER, y colocando la siguiente expresión la directiva del elemento del registro en el arreglo.

Debido a que DynamicListener hereda las características del sucesor de _ReportListener (que llama automaticamente al método BeforeReport  para todos los listeners sucesores), cada una de nuestras subclases de DynamicListener van a tener su propia matriz aRecords de los registros FRX que se correspondan con la propiedad .cDirective. Así, cada listener puede determinar fácilmente sobre qué campo debe accionar durante la ejecución del informe.

La siguiente tarea es aplicar la directiva cuando sea necesario. EvaluateContents verifica si el elemento actual de la  matriz tiene una expresión y si es así, la evalúa. Luego llama al método ApplyDirective, el que es abstracto en DynamicListener; pero implementado en sus dos subclase. Por ejemplo DynamicForeColorListener establece las propiedades adecuadas del color a toObjProperties e iguala la propiedad Reload  a .T., de tal forma que el motor de informes sabe si el formato del campo cambió.

He aquí otra tarea de mantenimiento: asegúrese de que el ListenerType está definido correctamente. El valor predeterminado, -1, provoca que no haya salida alguna, y no cambia ni siquiera especificando PREVIEW o TO PRINTER en los comandos REPORT o LABEL. Entonces, el método LoadReport establece el valor adecuado de ListenerType si es necesario.

He aquí el código para estas clases:

define class DynamicListener as _ReportListener of ;
  home() + 'ffc\_ReportListener.vcx'
dimension aRecords[1]
&& una matriz con información de cada registro en el FRX
cDirective = ''
&& la directiva que esperamos encontrar

* Si no se ha indicado ListenerType, se determina en dependencia
* de si el informe será impreso o visualizado (preview).
function LoadReport
  with This
    do case
      case .ListenerType <> -1
      case .CommandClauses.Preview
        .ListenerType = 1
      case .CommandClauses.OutputTo = 1
        .ListenerType = 0
    endcase
  endwith
  dodefault()
endfunc

* Antes de ejecutar el informe, recorremos el FRX y
* guardamos información sobre cada campo en el que esperamos 
* una directiva en su campo memo USER en la matriz aRecords.
function BeforeReport
  dodefault()
  with This
    .SetFRXDataSession()
    dimension .aRecords[reccount()]
    scan for .cDirective $ USER
      .aRecords[recno()] = strextract(USER, ;
        .cDirective + ' =', chr(13), 1, 3)
    endscan for .cDirective $ USER
    .ResetDataSession()
  endwith
endfunc

* Si el campo que se va a generar tiene una directiva, se aplica.
function EvaluateContents(tnFRXRecno, toObjProperties)
  local lcExpression, ;
    luValue
  with This
    lcExpression = .aRecords[tnFRXRecno]
    if not empty(lcExpression)
      luValue = evaluate(lcExpression)
      .ApplyDirective(tnFRXRecno, ;
        toObjProperties, luValue)
    endif not empty(lcExpression)
    * Si tenemos un sucesor, le damos participación también.
    if vartype(.Successor) = 'O'
      .Successor.EvaluateContents(tnFRXRecno, ;
        toObjProperties)
    endif vartype(.Successor) = 'O'
  endwith
endfunc

* Método abstracto para aplicar nuestra directiva.
function ApplyDirective(tnFRXRecno, ;
  toObjProperties, tuValue)
endfunc

enddefine

define class DynamicForeColorListener ;
  as DynamicListener
cDirective = '*:LISTENER FORECOLOR'

* Aplicar la directiva.
function ApplyDirective(tnFRXRecno, ;
  toObjProperties, tuValue)
  local lnPenRed, ;
    lnPenGreen, ;
    lnPenBlue
  if vartype(tuValue) = 'N'
    lnPenRed = bitand(tuValue, 0x0000FF)
    lnPenGreen = bitrshift(bitand(tuValue, ;
      0x00FF00), 8)
    lnPenBlue = bitrshift(bitand(tuValue, ;
      0xFF0000), 16)
    with toObjProperties
      if .PenRed <> lnPenRed or ;
        .PenGreen <> lnPenGreen or ;
        .PenBlue <> lnPenBlue
        .PenRed = lnPenRed
        .PenGreen = lnPenGreen
        .PenBlue = lnPenBlue
        .Reload = .T.
      endif .PenRed <> lnPenRed ...
    endwith
  endif vartype(tuValue) = 'N'
endfunc

enddefine

define class DynamicStyleListener as DynamicListener
  cDirective = '*:LISTENER STYLE'

* Aplicar la directiva.
function ApplyDirective(tnFRXRecno, ;
  toObjProperties, tuValue)
  if vartype(tuValue) = 'N'
    toObjProperties.FontStyle = tuValue
    toObjProperties.Reload = .T.
  endif vartype(lnStyle) = 'N'
endfunc

enddefine

El programa TestDynamicFormatting.PRG muestra cómo encadenar estos listeners, de forma tal que ambos se utilicen en la generación del informe.

use _samples + 'Northwind\Orders'
loListener = newobject('DynamicForeColorListener', ;
  'DynamicFormatting.prg')
loListener.Successor = ;
  newobject('DynamicStyleListener', ;
  'DynamicFormatting.prg')
report form TestDynamicFormatting.FRX preview ;
  object loListener

La Figura 1 muestra el resultado de la ejecucón de este programa. En algunos registros, Shipped Date se muestra en rojo y en otros casos en negro. Esto se debe a que tiene la siguiente directiva en su campo memo USER

*:LISTENER FORECOLOR = iif(SHIPPEDDATE > ORDERDATE + 10, rgb(255, 0, 0), rgb(0, 0, 0))


Figura 1

El campo Ship Via aparece a veces en negrita y algunas veces normal, porque tiene la siguiente directiva en su campo memo USER:

*:LISTENER STYLE = iif(SHIPVIA = 3, 1, 0)

(Observe que aunque este campo es numérico, muestra valores como "Fedex," "UPS," o "Mail"  debido a la expresión que hay en el campo.

¿Qué más puede hacer?

Pues casi todo lo que desee. En artículos futuros, mostraré otros listeners que permitan obtener imágenes rotadas, salidas HTML con una tabla de contenidos, y otros muchos tipos de salidas.

Resumen

Microsoft superó todas las ligas de la extensibilidad en VFP 9.0 de muchas formas, incluidas en el generador de informes. Gracias a su clase base, que podemos subclasear, ReportListener permite crear una salida propia, personalizada. Háganme saber, por favor de cualquier listener interesante que ha creado o que ideas se le ocurren para hacer con listeners.

31 de julio de 2017

VFP9 - Novedades - Manipulación de entorno de datos en el diseñador

El Diseñador de informes de VFP 9.0 ha sido modificado en muchos de sus aspectos. El entorno de datos ha sido uno de los aspectos cuyo tratamiento se ha modificado. Este artículo pretende comentar y mostrar algunos de estos cambios.

Muchas veces encontramos que el entorno de datos necesario para la ejecución de determinado grupo de informes es exactamente igual, o muy parecido. Antes de VFP 9.0 teníamos que valernos de código de programación para establecer un entorno adecuado, o en el peor de los casos rehacerlo a mano desde el Diseñador de Informes.

El Generador de informes de VFP 9.0 nos permite compartir fácilmente Entornos de datos con otros informes. El entorno de datos puede ser guardado como una clase y luego cargado en informes, según la necesidad. Esto brinda nuevas posibilidades para definir y reutilizar entornos en informes con escenarios similares.

Veamos las diferencias en el menú Archivo.

Menú Archivo para VFP 8.0 y VFP 9.0

Una opción nueva salta a la vista: Guardar como clase

Para guardar un Entorno de datos como clase, una vez definido, seleccione la opción Guardar como clase…en el menú Archivo. No es necesario tener el entorno de datos abierto.

Como vemos la única posibilidad disponible es Guardar como clase DataEnvironment. Este tipo de clase base fue incorporada a VFP en la versión 8.0.

Si va a crear una clase nueva, basta con escribir su nombre y localizar la biblioteca de clases donde se va a almacenar, al seleccionar el botón de comandos junto a Archivo se muestra la ventana Guardar como

Veamos otro menú, en este caso el de Informes comparemos:

Menú Informe para VFP 8.0  y VFP 9.0

Además de varios cambios en materia de organización de opciones y algunas otras novedades nos detenemos en la opción Carga el Entorno de Datos. Al seleccionarla, vemos que se muestra la nueva ventana Propiedades del Informe con la ficha Entorno de datos activa.

VFP 90, además de permitirnos definir manualmente el Entorno de datos para un nuevo informe, ofrece la opción de cargar un Entorno de datos de un informe ya existente o de una clase DataEnvironment guardada previamente. La opción Carga el Entorno de Datos… en el menú Informe permite seleccionar el origen del Entorno de datos a cargar. Tenemos dos posibilidades:

  • Copiar desde otro archivo de Informe
    Al cargar el Entorno de datos desde otro informe, todo el código y los miembros del Entorno original se copian en el nuevo informe. Esto significa que cualquier cambio que se realice en el Entorno del informe original, después de ser copiado, no se va a reflejar en el informe creado a partir del informe original.
  • Enlazar con una clase DataEnvironment visual
    Al cargar un Entorno de datos desde una clase, se agrega código al Entorno de datos del nuevo informe para enlazarlo con la clase DataEnvironment original e instanciarla en tiempo de ejecución. Esto debería significar que los cambios que pueden realizarse en el futuro sobre la clase DataEnvironment se van a propagar a cualquier informe que emplee esta clase DataEnvironment. Pero no es así, porque el código se escribe sólo la primera vez y no verifica si hubo cambios en la clase.

Veamos como se traduce esta explicación teórica en un ejemplo práctico.

Supongamos que tenemos un nuevo informe, aun vacío, al que deseamos establecer un Entorno de datos, a partir de una clase guardada previamente. Al seleccionar la clase deseada nos muestra lo siguiente:

Esta pregunta se va a hacer siempre que se intente copiar el Entorno de datos, ya sea de otro informe o de una clase DataEnvironment

Al responder nos muestra la fuente que da origen al Entorno de datos y el mensaje:

Veamos qué ha ocurrido en el Entorno de Datos del Diseñador de informes

Pues lo que ha ocurrido es que se ha generado código en algunos métodos del DataEnvironment y de los cursores que lo integran. Vamos a aclarar que en el informe original no existía ningún código en estos métodos. El código que se agrega es para poder enlazar el informe con esa clase.

El enlace con la clase original se produce en el método BeforeOpenTables como se puede ver a continuación

Algunos de los métodos tienen un código tan simple como un único comando DODEFAULT(). La razón para esto es que BindEvents() no funciona si al menos no hay una línea de código. BindEvent() se emplea en BeforeOpenTables para enlazar con los eventos Init y Destroy. El evento INIT no tiene código adicional.

En cuanto al tratamiento de Entorno de datos hay un aspecto muy interesante, que ha resultado un tanto sorprendente para algunos desarrolladores Visual FoxPro y se trata de la forma en que se vinculan con la ventana Expresión de informe.

Comportamiento actual para las tablas abiertas en el Entorno de datos del Diseñador de Informe:

Si la variable de sistema _reportbuilder está vacía, todos los campos de tablas contenidas en el entorno de datos se muestran en la lista Campos (Fields). Las tablas abiertas fuera del entorno de datos no se muestran.

Si la variable de sistema _reportbuilder apunta a alguna aplicación, por ejemplo, _ reportbuilder = ’reportbuilder.app’ significa que se está empleando el nuevo Diseñador de informes, por lo que el comportamiento, y las ventanas son un tanto diferentes.

Se muestra el nuevo cuadro para generar expresiones con un cuadro combinado para seleccionar la tabla que mostrará sus campos en la lista de campos. Es aquí donde debemos prestar atención, se van a mostrar solamente las tablas que estén realmente en uso. Las tablas definidas en el Entorno de datos del informe no se abren automáticamente por el Diseñador de informes, por tanto, no aparecerán en este cuadro combinado.

Esta característica, que no es muy intuitiva al inicio, nos reporta gran beneficio, ya que es la forma más segura y limpia para garantizar que el usuario va a manipular solamente las tablas a las que le demos acceso. Se trata de extender los informes y brindar al usuario final posibilidades para su configuración; pero evitando todo tipo de riesgos. Entonces, las tablas que vamos a utilizar y no queremos que el usuario manipule las creamos en el Entorno de Datos y solo abrimos por fuera aquellas que el usuario necesita manipular.

Podemos además, proteger el Entorno de Datos y no permitir al usuario que acceda, por cierto, que la posibilidad de proteger datos es otra gran novedad del Diseñador de Informes en VFP 9.0

En tiempo de ejecución, el nuevo motor de informe de VFP 9.0 cambia el tratamiento de la sesión actual de datos, agrega sesiones nuevas, su dominio y correcta manipulación, son esenciales para la correcta obtención de los datos… de este importante tema hablaremos otro día.

Espero que haya resultado de utilidad.

Saludos,

Ana María Bisbé York
www.amby.net

21 de julio de 2017

Imprimir un gráfico creado con SimpleChart

SimpleChart es una clase creada por Mike Lewis (http://www.ml-consult.co.uk) quien logra, mediante un grupo de propiedades, facilitar el trabajo con MSChart y posibilitar que se obtengan gráficos con mucha rapidez y sin complejidades.

Para ver todos los detalles relativos a esta clase ver:

SimpleChart revisado (Mike Lewis) Traducción
https://comunidadvfp.blogspot.com/2003/10/simplechart-revisitado.html

Nos había quedado pendiente aclarar cómo se pueden imprimir estos gráficos. Pues bien, la pregunta es ¿Se puede copiar un gráfico creado con SimpleChart en un informe de VFP?

¡¡Sí se puede!! Tal y como se indica en el segundo artículo, hay que invocar el método EditCopy del gráfico.

Aquí dejo un ejemplo concreto

* Copiar al Clipboard
THISFORM.Grafico.EditCopy
* Copiar a un bmp
THISFORM.cRutaGrafico = GETENV("Temp")+SYS(2015)+".bmp"
DO Graficos WITH (THISFORM.cRutaGrafico),""

Pego directamente el código del programa gráficos escrito por el compañero J. Enrique Ramos Menchaca, quien lo publicó en PortalFox y nos autorizó a modificarlo dadas las nuevas necesidades que yo tenía.

Cambios que hicimos:

  1. Incluir 2 parámetros: tcArchivo, tcreporte
    • tcArchivo - nombre del .bmp que se va a cargar en el objeto imagen del informe
    • tcreporte - nombre del reporte a llamar
  2. No emplear campo general y sí campo de caracteres con la ruta de la imagen a mostrar, mira donde dice código nuevo.

Lo único que necesitamos es copiar y pegar el programa gráfico en nuestra aplicación e invocarlo con los parámetros adecuados.

***********************************************
* Programa Graficos
* Autor: J. Enrique Ramos Menchaca
* Modificado por Jorge Mota y Ana María Bisbé York
***********************************************
LPARAMETERS tcArchivo, tcreporte
#DEFINE CF_BITMAP 2
#DEFINE OBJ_BITMAP 7
DO decl
LOCAL hClipBmp, lcTargetFile
*lcTargetFile = "C:\clipboard.bmp"
lcTargetFile = tcArchivo && "grafime.bmp"
= OpenClipboard (0)
hClipBmp = GetClipboardData (CF_BITMAP)
= CloseClipboard()
IF hClipBmp = 0 Or GetObjectType(hClipBmp) <> OBJ_BITMAP
  = MessageBox("No se encontro imagen bitmap en el Portapapeles.",;
    64, "Clipboard to BMP")
  RETURN .F.
ENDIF
= bitmap2file (hClipBmp, lcTargetFile)
= DeleteObject (hClipBmp)
***
* crear las referencias para los arreglos
* external array laAspecto, lavalor, laporci,;
* lavalor1, lavalor2, lavalor3, lavalor4
* Código original
*!* CREATE CURSOR GRAFICO (GRAFICA G)
*!* APPEND BLANK
*!* APPEND GENERAL GRAFICO.GRAFICA FROM "c:\clipboard.bmp"
*!* REPORT FORM GRAFICA preview
*!* USE IN grafico
*!* RETURN
* Código modificado
CREATE CURSOR GRAFICO (cRuta c(250))
APPEND BLANK
REPLACE GRAFICO.cRuta WITH tcArchivo
IF !empty(tcreporte)
  REPORT FORM (tcreporte) to print prompt noconso
ENDIF
USE IN GRAFICO
RETURN
***********************************************
* InitBitsArray()
***********************************************
PROCEDURE InitBitsArray()
  #DEFINE GMEM_FIXED 0
  LOCAL lnPtr
  pnBitsSize = pnHeight * pnBytesPerScan
  lnPtr = GlobalAlloc (GMEM_FIXED, pnBitsSize)
  = ZeroMemory (lnPtr, pnBitsSize)
  RETURN lnPtr
ENDPROC

***********************************************
* String2file
***********************************************
PROCEDURE String2File (hFile, lcBuffer)
  DECLARE INTEGER WriteFile IN kernel32;
    INTEGER hFile, STRING @lpBuffer, INTEGER nBt2Write,;
    INTEGER @lpBtWritten, INTEGER lpOverlapped
  = WriteFile (hFile, @lcBuffer, Len(lcBuffer), 0, 0)
  RETURN
ENDPROC

***********************************************
* Bitmap2file
***********************************************
PROCEDURE Bitmap2file (hBitmap, lcTargetFile)
  #DEFINE DIB_RGB_COLORS 0
  PRIVATE pnWidth, pnHeight, pnBitsSize, pnRgbQuadSize, pnBytesPerScan
  STORE 0 TO pnWidth, pnHeight, pnBytesPerScan, pnBitsSize, pnRgbQuadSize
  = GetBitmapDimensions(hBitmap, @pnWidth, @pnHeight)
  LOCAL lpBitsArray, lcBInfo
  lcBInfo = InitBitmapInfo()
  lpBitsArray = InitBitsArray()
  LOCAL hwnd, hdc, hMemDC
  hwnd = GetActiveWindow()
  hdc = GetWindowDC(hwnd)
  hMemDC = CreateCompatibleDC (hdc)
  = ReleaseDC (hwnd, hdc)
  = GetDIBits (hMemDC, hBitmap, 0, pnHeight, lpBitsArray,;
    @lcBInfo, DIB_RGB_COLORS)
  #DEFINE BFHDR_SIZE 14 && BITMAPFILEHEADER
  #DEFINE BHDR_SIZE 40 && BITMAPINFOHEADER
  LOCAL hFile, lnFileSize, lnOffBits, lcBFileHdr
  lnFileSize = BFHDR_SIZE + BHDR_SIZE + pnRgbQuadSize + pnBitsSize
  lnOffBits = BFHDR_SIZE + BHDR_SIZE + pnRgbQuadSize
  lcBFileHdr = "BM" + num2dword(lnFileSize) +;
    num2dword(0) + num2dword(lnOffBits)
  #DEFINE GENERIC_WRITE 1073741824 && 0x40000000
  #DEFINE FILE_SHARE_WRITE 2
  #DEFINE CREATE_ALWAYS 2
  #DEFINE FILE_ATTRIBUTE_NORMAL 128
  #DEFINE INVALID_HANDLE_VALUE -1
  hFile = CreateFile (lcTargetFile,;
    GENERIC_WRITE,;
    FILE_SHARE_WRITE, 0,;
    CREATE_ALWAYS,;
    FILE_ATTRIBUTE_NORMAL, 0)
  IF hFile <> INVALID_HANDLE_VALUE
    WAIT WINDOW "Storing to file..." NOWAIT
    = String2File (hFile, @lcBFileHdr)
    = String2File (hFile, @lcBInfo)
    = Ptr2File (hFile, lpBitsArray, pnBitsSize)
    = CloseHandle (hFile)
  ELSE
    = MessageBox("Unable to create file: " + lcTargetFile)
  ENDIF
  = GlobalFree(lpBitsArray)
  = DeleteDC (hMemDC)
  RETURN
ENDPROC

***********************************************
* Ptr2File
***********************************************
PROCEDURE Ptr2File (hFile, lnPointer, lnBt2Write)
  DECLARE INTEGER WriteFile IN kernel32;
    INTEGER hFile, INTEGER lpBuffer, INTEGER nBt2Write,;
    INTEGER @lpBtWritten, INTEGER lpOverlapped
  = WriteFile (hFile, lnPointer, lnBt2Write, 0, 0)
  RETURN
ENDPROC

***********************************************
* InitBitmapInfo
***********************************************
PROCEDURE InitBitmapInfo(lcBIHdr)
  #DEFINE BI_RGB 0
  #DEFINE RGBQUAD_SIZE 4
  #DEFINE BHDR_SIZE 40
  LOCAL lnBitsPerPixel, lcBIHdr, lcRgbQuad
  lnBitsPerPixel = 24
  pnBytesPerScan = Int((pnWidth * lnBitsPerPixel)/8)
  IF Mod(pnBytesPerScan, 4) <> 0
    pnBytesPerScan = pnBytesPerScan + 4 - Mod(pnBytesPerScan, 4)
  ENDIF
  lcBIHdr = num2dword(BHDR_SIZE) + num2dword(pnWidth) +;
  num2dword(pnHeight) + num2word(1) + num2word(lnBitsPerPixel) +;
  num2dword(BI_RGB) + Repli(Chr(0), 20)
  IF lnBitsPerPixel <= 8
    pnRgbQuadSize = (2^lnBitsPerPixel) * RGBQUAD_SIZE
    lcRgbQuad = Repli(Chr(0), pnRgbQuadSize)
  ELSE
    lcRgbQuad = ""
  ENDIF
  RETURN lcBIHdr + lcRgbQuad
ENDPROC

***********************************************
* num2dword
***********************************************
FUNCTION num2dword (lnValue)
  #DEFINE m0 256
  #DEFINE m1 65536
  #DEFINE m2 16777216
  LOCAL b0, b1, b2, b3
  b3 = Int(lnValue/m2)
  b2 = Int((lnValue - b3*m2)/m1)
  b1 = Int((lnValue - b3*m2 - b2*m1)/m0)
  b0 = Mod(lnValue, m0)
  RETURN Chr(b0)+Chr(b1)+Chr(b2)+Chr(b3)
ENDFUNC

***********************************************
* GetBitmapDimensions
***********************************************
PROCEDURE GetBitmapDimensions(hBitmap, lnWidth, lnHeight)
  #DEFINE BITMAP_STRU_SIZE 24
  LOCAL lcBuffer
  lcBuffer = Repli(Chr(0), BITMAP_STRU_SIZE)
  IF GetObjectA (hBitmap, BITMAP_STRU_SIZE, @lcBuffer) <> 0
    lnWidth = buf2dword (SUBSTR(lcBuffer, 5,4))
    lnHeight = buf2dword (SUBSTR(lcBuffer, 9,4))
  ENDIF
  RETURN
ENDPROC

***********************************************
* buf2dword
***********************************************
FUNCTION buf2dword (lcBuffer)
  RETURN Asc(SUBSTR(lcBuffer, 1,1)) + ;
    Asc(SUBSTR(lcBuffer, 2,1)) * 256 +;
    Asc(SUBSTR(lcBuffer, 3,1)) * 65536 +;
    Asc(SUBSTR(lcBuffer, 4,1)) * 16777216
ENDFUNC

***********************************************
* num2word
***********************************************
FUNCTION num2word (lnValue)
  RETURN Chr(MOD(m.lnValue,256)) + CHR(INT(m.lnValue/256))
ENDFUNC

***********************************************
* decl
***********************************************
PROCEDURE decl
  DECLARE INTEGER GetActiveWindow IN user32
  DECLARE INTEGER GetClipboardData IN user32 INTEGER uFormat
  DECLARE INTEGER OpenClipboard IN user32 INTEGER hwnd
  DECLARE INTEGER CloseClipboard IN user32
  DECLARE INTEGER DeleteObject IN gdi32 INTEGER hObject
  DECLARE INTEGER GetWindowDC IN user32 INTEGER hwnd
  DECLARE INTEGER ReleaseDC IN user32 INTEGER hwnd, INTEGER hdc
  DECLARE INTEGER CreateCompatibleDC IN gdi32 INTEGER hdc
  DECLARE INTEGER DeleteDC IN gdi32 INTEGER hdc
  DECLARE INTEGER GlobalAlloc IN kernel32 INTEGER wFlags, INTEGER dwBytes
  DECLARE INTEGER GlobalFree IN kernel32 INTEGER hMem
  DECLARE INTEGER GetObject IN gdi32 AS GetObjectA;
    INTEGER hgdiobj, INTEGER cbBuffer, STRING @lpvObject
  DECLARE INTEGER GetObjectType IN gdi32 INTEGER h
  DECLARE RtlZeroMemory IN kernel32 As ZeroMemory;
    INTEGER dest, INTEGER numBytes
  DECLARE INTEGER GetDIBits IN gdi32;
    INTEGER hdc, INTEGER hbmp, INTEGER uStartScan,;
    INTEGER cScanLines, INTEGER lpvBits, STRING @lpbi,;
    INTEGER uUsage
  DECLARE INTEGER CreateFile IN kernel32;
    STRING lpFileName, INTEGER dwDesiredAccess,;
    INTEGER dwShareMode, INTEGER lpSecurityAttr,;
    INTEGER dwCreationDisp, INTEGER dwFlagsAndAttrs,;
    INTEGER hTemplateFile
  DECLARE INTEGER CloseHandle IN kernel32 INTEGER hObject
ENDPROC

***********************************************

Espero que sirva como complemento a la información que ya teníamos para trabajar con esta clase SimpleChart

Saludos,

Ana María Bisbé York
www.amby.net


17 de julio de 2017

Técnicas para una interfaz alternativa de Report Preview

Artículo original: Techniques for an alternative Report Preview UI
http://www.spacefold.com/colin/archive/articles/reportpreview/techniques.htm
Autor: Colin Nicholls
Traducido por: Ana María Bisbé York


Antes de comenzar

Un grupo importante de los debates sobre informes en VFP de estos últimos tiempos en Universal Thread (http://www.universalthread.com) se han referido al tema de la nueva pantalla de presentación preliminar (Report Preview) y cómo controlarla. Un aspecto particularmente debatido es el comportamiento de la barra de herramientas cuando empleamos REPORT FORM ... PREVIEW en una aplicación de nivel superior. Existe un bug en VFP 9.0 (vea el código para reproducirlo en http://www.spacefold.com/colin/archive/articles/reportpreview/repro.txt) que provoca que la barra de herramientas asociada sea visible, aunque inhabilitada a los clics del ratón. La funcionalidad existe a través del menú contextual de la ventana preliminar, pero esto se convierte en un problema de educación de los usuarios o hay que utilizar NOWAIT para forzar que la ventana preliminar se muestre como una forma no modal. ( Esto no es un gran problema, ahora que tenemos la cláusula NOPAGEEJECT en el comando report form. Muchas de las razones por la que la gente piensa que necesitan una ventana modal no se aplican.

No obstante de los bugs del producto, en mis sesiones me esfuerzo por mostrar que la presentación preliminar predeterminada es sólo eso - predeterminada - y que ahora, no sólo son posible formularios con soluciones alternativas, sino que son fáciles de implementar. Entonces, voy a mostrar hoy una interfaz de usuario alternativa para la ventana de presentación preliminar que puede utilizar y personalizar acorde a sus contenidos.

He aquí una presentación preliminar.

Esto es lo que estamos construyendo:

Es un formulario sencillo con algunos botones y un contenedor en el que reside un control forma (shape) en el que se generará una única página del informe. El formulario permite hacer acercamientos y alejamientos (zoom) y arrastrar la página, empleando el ratón, por dentro del contenedor.

Realmente, aquí no hay mucho que hacer.

Crear el marco preliminar

*------------------------------------------------
* Marco preliminar...
*------------------------------------------------
define class myFrame as Container
  Width = 296
  Height = 372
  Left = 12
  Top = 12
  SpecialEffect = 1
  BackColor = rgb(192,192,192)

  add Object canvas as myCanvas && vea debajo:
enddefine

*------------------------------------------------
* ...y su forma hija:
*------------------------------------------------
define class myCanvas as Shape
  Height = 330
  Width = 255
  Left = 20
  Top = 20
  BackColor = rgb(255,255,255)

Vamos a permitir que la forma se mueva dentro del marco añadiendo código al evento .MouseDown del objeto Shape.

  procedure MouseDown
    lparameters nButton, nShift, nXCoord, nYCoord

    offsetX = m.nXCoord - THIS.Left
    offsetY = m.nYCoord - THIS.Top
  
    THISFORM.MousePointer = 5
    do while mdown()
      THIS.Left = mcol(0,3) - m.offsetX
      THIS.Top = mrow(0,3) - m.offsetY
    enddo
    THISFORM.MousePointer = 0 
  endproc
enddefine

El .MousePointer de 5 es el puntero del ratón que muestra ambas flechas NSEO en lugar de un cursor normal. Puede verlo en la figura mostrada antes.

Crear la clase form

Ahora que ya tenemos el marco, podemos crear la clase form myPreview y colocarlo en el marco:

*------------------------------------------------
* La clase form:
*------------------------------------------------
define class myPreview as Form
  add object frame as myFrame && vea arriba

Junto con los botones de comandos:

  add object cmdPrev as myButton ;
    with Top=12, Caption = "Previous"
  add object cmdNext as myButton ;
    with Top=44, Caption = "Next"
  add object cmdZoomIn as myButton ;
    with Top=96, Caption = "Zoom In"
  add object cmdZoomOut as myButton ;
    with Top=128, Caption = "Zoom Out"
  add object cmdReset as myButton ;
    with Top=176, Caption = "Whole Page"
  add object cmdClose as myButton ;
    with Top=220, Caption = "Close"

La clase derivada myButton que he utilizado aquí es precisamente una plantilla para .FontName, .FontStyle y la posición .Left. En breve, vamos a implementar algunos .Click. Sea paciente.

Viendo cómo necesitamos interactuar con el motor de informes, vamos a darle al formulario un par de propiedades de usuarios: Un marcador de posición para una referencia a un objeto ReportListener, y una propiedad numérica para el número de página actual:

Listener = .null.
PageNo = 1

La llave para crear un formulario de presentación preliminar en VFP 9.0 es utilizar el método OutPutPage de ReportListener, pasándole tres parámetros:

  • El número de página a generar
  • El dispositivo u objeto para generar la presentación preliminar
  • Un número que indica el tipo de dispositivo u objeto que se está generando.

Ya que estamos generando un objeto Visual FoxPro - THIS.Frame.Canvas - el parámetro - tipo de dispositivo es 2. Vamos a tener el número de página en THIS.PageNo, y la referencia al ReportListener es THIS.Listener, así que la sintaxis completa será:

THIS.Listener.OutputPage(THIS.PageNo, THIS.Frame.Canvas, 2)

Vamos a colocar todo el código - con las condiciones de verificación de límites - en un método de usuario del formulario, OutPage():

procedure Outputpage()
  with THIS
    if not isnull(.Listener) and .Listener.PageTotal > 0
      .Listener.OutputPage(.PageNo, .Frame.Canvas, 2)
      .Caption = ;
        justfname(.Listener.commandClauses.file) ;
        + " - page " + trans(.PageNo)
    endif
  endwith
endproc

Como puede ver, vamos a utilizar la propiedad Caption del formulario para mostrar el nombre de archivo y el número de página actual, solo por dejar las cosas más sencillas. Para asegurar que la página de informe se generará cuando se dibuje el formulario, añadimos una llamada a nuestro método de usuario en el evento Paint del formulario:

procedure Paint()
  THIS.OutputPage()
endproc

Hacer que estos botones reaccionen al Clic

¿Recuerda aquellos botones de comando que agregamos al formulario? Tenemos un par para la navegación alrededor del informe. Es importante tener en cuenta las condiciones de límites entre datos. El máximo número de páginas que se puede generar está determinado por: ReportListener.PageTotal:

procedure cmdPrev.Click
  with THISFORM
    .PageNo = max(1,.PageNo-1)
    .OutputPage()
  endwith
endproc

procedure cmdNext.Click
  with THISFORM
    .PageNo = min(.Listener.PageTotal,.PageNo+1)
    .OutputPage()
  endwith
endproc

Tenemos además un par de comandos para ajustar el tamaño de la presentación preliminar. Es fácil: solamente cambiar el tamaño del Shape y llamar a OutPutPage para re-pintar:

#define ZOOM_SCALE 1.3

procedure cmdZoomIn.Click
  with THISFORM.Frame.Canvas
    .Width = int(.Width * ZOOM_SCALE)
    .Height = int(.Height * ZOOM_SCALE)
  endwith
  THISFORM.OutputPage()
endproc

procedure cmdZoomOut.Click
  with THISFORM.Frame.Canvas
    .Width = max(17,int(.Width / ZOOM_SCALE))
    .Height = max(22,int(.Height / ZOOM_SCALE))
  endwith
  THISFORM.OutputPage()
endproc

Como puede ver, estamos utilizando algunas condiciones de límite en el tamaño más pequeño de la Forma, explícitamente escrito en el código 8.5 x 11, sobre esto podíamos pensar en algo más.

En caso de que desee perder el acercamiento y el movimiento, es conveniente un botón para re-inicializar el tamaño y la posición de la forma (shape).

procedure cmdReset.Click
  with THISFORM.Frame.Canvas
    .Top = 20
    .Left = 20
    .Width = 255
    .Height = 330
  endwith
endproc

El botón Cerrar no es realmente necesario - el formulario tiene su cuadro Cerrar en el título que funciona perfectamente. Pero, permítanme agregarlo de todas formas:

procedure cmdClose.Click
  THISFORM.Release()
endproc

Probamos hasta ahora

Bien, ¡Lo hemos hecho!, ahora vamos a probarlo. Lo primero que necesitamos es crear un ReportListener

rl = newobject("ReportListener")
rl.ListenerType = 3

Vea que estamos utilizando un ReportListener igual a 3. Un ListenerType igual a 1 provocaría que se invocara el contenedor preliminar predeterminado que no es lo deseado en este caso: Deseamos solamente generar la salida y guardarla en memoria temporal (caché) para nuestro propio empleo.

Ahora, necesitamos el motor de informes para que envíe un informe a nuestro listener:

report form ? object m.rl

Escoja cualquier informe que desee. (Yo escogí _SAMPLES+"\Solution\Reports\wrapping.frx" porque es bonito.) Ahora debemos instanciar nuestra Interfaz de usuario y asignarle una referencia a nuestra instancia ReportListener, y pedirle que lo muestre:

x = newobject("myPreview")
x.Listener = m.rl
x.Show(1)

Esperemos que en este punto tengamos algo como lo que tenemos en la figura mostrada antes.

  • Intente navegar por el informe.
  • Intente hacer Zoom.
  • Trate de arrastrar la página.

Un pequeño problema

Bien, confieso que he omitido ante un aspecto importante. (Los escritores de artículos hacen así todo el tiempo, porque piensan que es un buen recurso didáctico.)

Como probablemente ha notado, el método ReportListener.OutputPage() utiliza la posición y las dimensiones del objeto Shape para generar la página del informe sobre la superficie del formulario. En realidad no dibuja el objeto como tal.

Como resultado, la página sobre-escribe los otros controles en el formulario, produciendo un efecto visual desconcertante. Vea la imagen.

Existe una solución: tenemos que utilizar parámetros adicionales a OutPutPage() para especificar los límites dentro de los que se va a dibujar la imagen.

Especificar un área fija para OutputPage()

La documentación para el método OutPutPage (ver ayuda VFP) indica que los parámetros adicionales no se aplican para un tipo de dispositivo igual a 2 (2 = objeto FoxPro) Esto no es estrictamente correcto. En realidad, los parámetros 4to, 5to, 6to y 7mo se ignoran; pero los parámetros nClipLeft, nClipTop, nClipWidth, y nClipHeight se respetan por el método ReportListener.OutputPage() para objetos FoxPro

Estos cuatro parámetros finales definen una "ventana" - en pixels - en el área del formulario a través de la cual la salida regenerada va a aparecer. Cualquier cosa que quede fuera de esta área no se mostrará en el formulario.

Afortunadamente para nosotros, tenemos la forma conveniente para determinar la ubicación y dimensiones de área fija: el objeto frame como tal nos lo dirá.

Substituya la línea de código resaltada en negrita en el método OutPutPage del formulario para que sean especificados los parámetros adicionales:

* .Listener.OutputPage( .PageNo, .Frame.Canvas, 2 )
.Listener.OutputPage( ;
.PageNo, .Frame.Canvas, 2, ;
  0, 0 , 0 , 0 , ;
  .Frame.Left +2, ;
  .Frame.Top +2, ;
  .Frame.Width -4, ;
  .Frame.Height -4 )

Ahora el formulario preliminar debe trabajar como esperamos (vea la siguiente figura)

Ejercicios para estudiantes:

Considero que he mostrado propiedades excitantes de las posibilidades existentes. Pero no hemos hecho todo: existen algunas cosas adicionales que va a necesitar hacer antes de emplear este código en una aplicación en producción.

Debe:

  • Ajustar el código en el evento MouseDown del objeto Shape para evitar que el ratón se arrastre fuera de los límites del borde del marco.
  • Ajustar la proporción del objeto Shape para que se corresponda con las dimensiones del informe, utilizando los métodos .GetPageWidth() y .GetPageHeight() del ReportListener.
  • Permitir al formulario mostrar una escala apropiada para el nivel DPI de la pantalla, incluyendo una opción "Zoom al 100%"
  • Ajustar el código de alejamiento-acercamiento de tal forma que el centro del área visible del objeto Forma permanezca centrado en lugar de moverse basado en la posición de la esquina superior izquierda como ocurre actualmente
  • Posiblemente establecer el nivel máximo y mínimo para el zoom.

Puede además:

  • Hacer que el formulario sea redimensionable

Y quizás, lo que puede ser aun más útil, podría agregar además código adicional a la clase, para que conecte en el sistema de informes de Visual FoxPro, proporcionando un Contenedor preliminar (Preview container) alternativo cuando SET REPORTBEHAVIOR 90 está activado.

... oh, está bien. He aquí cómo se hace:

Empaquetado como Contenedor preliminar

Necesitará agregar estos métodos en la clase myPreview

*-------------------------------------------
* Métodos requeridos para que opere como 
* un contenedor preliminar :
*-------------------------------------------
procedure Release()
  if not isnull(THIS.Listener)
    THIS.Listener.OnPreviewClose(.F.)
    THIS.Listener = .null.
  endif
  THIS.Hide()
endproc

procedure Destroy()
  THIS.Listener = null
  dodefault()
endproc

procedure QueryUnload()
  nodefault
  THIS.Release()
endproc

procedure SetReport( oRef )
  if not isnull(oRef) and vartype(oRef) = "O"
    THIS.Listener = m.oRef
  else
    THIS.Listener = .null.
    THIS.Hide()
  endif
endproc

Deseará además colocar el código al inicio del archivo, de tal forma que solamente apunte a _REPORTPREVIEW directamente al programa.

*------------------------------------------
* Utilizado como contenedor preliminar:
* _REPORTPREVIEW = "<this program>"
*
* Comprobación fuera del sistema de informes:
* DO <este programa>
*------------------------------------------
lparameters oRef
if pcount()=0
  rl = newobject("ReportListener")
  rl.ListenerType = 3
  report form ? object m.rl
  x = newobject("myPreview")
  x.Listener = m.rl
  x.Show(1)
  return
else
  oRef = newobject("myPreview")
endif
return

¡Que lo disfrute!

He aquí el código fuente para este artículo: http://www.spacefold.com/colin/archive/articles/reportpreview/tech_src.txt

Copyright (c) 2005 Colin Nicholls

11 de julio de 2017

Observaciones sobre etiquetas alineadas a la derecha en informes de VFP 9.0

Artículo original: Observations on Rightaligned labels in VFP9 Reports
http://www.spacefold.com/colin/archive/articles/vfp9reporting/rightalign/monofonts.html
Autor: Colin Nicholls
Traducido por: Ana María Bisbé York


Introducción

Este artículo se basa en un trabajo que realicé tratando de contestar a una pregunta realizada por Alejandro Sosa en Universal Thread. Alex estaba teniendo problemas al alinear correctamente a la derecha títulos con campos/expresiones alineados a la derecha. Su informe era complejo y migraba desde una versión antigua de FoxPro, por lo que he creado una prueba sencilla para mostrar lo que ocurría.

Revisando: REPORTBEHAVIOR

En Visual FoxPro 9.0, existe un nuevo motor de informes que emplea GDI+ para generar la salida. Sin embargo, está soportado el motor viejo, con compatibilidad hacia atrás. Puede alternar entre los dos motores al vuelo empleando el comando SET REPORTBEHAVIOR.

Revisando: Fuentes con un único espacio contra fuentes proporcionales

Las fuentes que emplean igual espaciado tienen un ancho constante para todos los caracteres en dependencia de una altura dada. Las fuentes proporcionales utilizan anchos variables apropiado para hacer la fuente más legible. En este artículo verá la diferencia de forma muy simple: Los textos son proporcionales y los fragmentos de código están todos en formato de espacio único.

Resumen

En REPORTBEHAVIOR=90, todas las etiquetas de texto se van a generar con longitudes mayores que las que puede esperar al ver el Diseñador de informes. Esto se debe a que la generación de texto con GDI+ emplea más espacio. Las etiquetas sufren por esto, debido a que los diseños almacenan su posición inicial exacta, lo contrario a los campos / expresiones que permiten calcular la alineación a realizar por el motor, utilizando GDI+.

Lea más para detalles.

Observaciones

Datos de prueba

Quise comparar la alineación a la derecha para ambos tipos de valores: cadena y números, así que creé dos columnas en mi cursor de pruebas:

CREATE CURSOR x ( ftitle C(10), fnumb N(8,2) )
INSERT INTO x VALUES ( 'Bagels', 32.45 )
INSERT INTO x VALUES ( 'Cucumber', 5.23)
El informe de prueba

He aquí una imagen del informe de prueba, abierto en el diseñador, con los controles etiqueta seleccionados para mostrar sus posiciones exactas. He empleado líneas rojas para mostrar los ejes de alineación:

El control campo/expresión para los campos FTITLE y FNUMB tienen que ser configurados con Justificación: Derecha, el campo tiene que ser configurado manualmente: el campo numérico está alineado a la izquierda predeterminado. Puede ver que tengo visualmente alineadas las etiquetas con el final de cada control de campo/expresión. He duplicado el diseño de estos controles para cada una de las fuentes diferentes. Tahoma y Candara son fuentes proporcionales, y Courier New, Bitstream Vera Sans Mono y Consolas son mono espaciadas.[1]

Con REPORTBEHAVIOR 80

Así se ve la Presentación preliminar con REPORTBEHAVIOR = 80:

Y el resultado impreso

Asumiendo que las líneas rojas son una referencia exacta, se puede ver que los mejores resultados se ven con Courier New y Vera Sans Mono. Consolas y las fuentes proporcionales al parecer exceden sus posiciones esperadas ligeramente. Por tanto, debe estar muy ajustada la tolerancia para una salida aceptable, al utilizar estos tipos de fuente, o será un gran problema.

Con REPORTBEHAVIOR 90

He aquí cómo se ve la Presentación preliminar con REPORTBEHAVIOR = 90:

y el resultado impreso

Conclusiones

Es fácil ver desde la comparación que con REPORTBEHAVIOR = 90, todas las etiquetas de texto exceden su longitudes esperada, no sólo en las fuentes no espaciadas. Sin embargo, el efecto es definitivamente más pronunciado en fuentes mono espaciadas.

¿Por qué ocurre esto para etiquetas y no para para campos/expresiones?

Con REPORTBEHAVIOR = 90, el nuevo motor de informe utiliza GDI+ para generar la salida, y la generación de las cadenas de textos necesitan más espacio que la forma plana del viejo GDI. Escribí sobre por qué GDI+ necesita espacio adicional antes de las cadenas de texto: http://www.spacefold.com/colin/posts/2005/08-18GDIplusinreports.html

El Diseñador de informe utiliza GDI - no GDI + para generar los componentes del diseño del informe, incluyendo todas cadenas de texto que ve. Entonces, si visualmente ha justificado a la derecha un elemento de informe, el diseñador de informe guarda la coordenada más izquierda del elemento (la posición inicial del texto) en el diseño. La longitud de la cadena generada por GDI+ será mucho más grande que lo que cree, basada en que es lo que ve en el Diseñador.

No hay dudas del hecho de que tiene que tener en cuenta este efecto al diseñar sus controles etiqueta en los informes.

Ahora, verá que este problema no ocurre con los controles campo/expresiones. Ellos están todos alineados correctamente, debido a que el Diseñador de Informe, no especifica una posición exacta para la cadena de texto, pero sí especifica la caja donde el texto va a ser generado. Entonces, el cálculo de la alineación tiene lugar en el motor de informe, mientras GDI+ tiene en cuenta el tamaño del texto.

Notas al pie:

[1] Candara y Consolas son fuentes nuevas de Microsoft, distribuidas con Windows Vista.

Agradecimientos:

Gracias a Alex Sosa por su pregunta tan interesante.