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.prg) 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.prg

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 trozos 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.

7 de julio de 2017

VFP9 - Mejoras realizadas al Diseñador de Informes

Tanto el Motor de Informes, como su Diseñador en VFP 9.0, son los aspectos sobre los que más cambios se han incorporado. Como respuesta a la retroalimentación hecha por los usuarios, Microsoft ha mejorado significativamente el Generador de informes en VFP 9.0, ha velado por proteger todo el trabajo invertido, por lo que no modificó la estructura actual del archivo FRX.

Nuevo comando SET REPORTBEHAVIOR

Es como un interruptor que enciende o apaga la salida asistida por objetos.

Admite como parámetros, los valores 80 ó 90

Dado que antes se empleaba GDI y ahora GDI+ hay diferencias en el interlineado, alineación y espaciado. Eso puede afectar a informes existentes. Por tanto el valor predeterminado es 80. La asignación desde Herramientas - Opciones - Informes es Global; pero aun así se puede cambiar para cada informe que haga falta de forma específica.

El nuevo generador de informes se llama ReportBuilder.APP y viene con VFP 9.0.

Algunos de los logros han sido:

1. Mejorar el interfaz de usuario:

La interfaz de usuario ha sido mejorada en varios aspectos, que al sumarlos representan un importante ahorro de tiempo, veamos en detalles las diferencias entre VFP 8.0 y VFP 9.0.

- Menú contextual mejorado

El Menú contextual del Generador de informes en VFP 9.0 agrega las opciones: Bandas opcionales, Variables y Propiedades.

- Nueva ventana propiedades de Informes

Por su parte la nueva ventana Propiedades de Informe utiliza un marco de página y páginas para todos los aspectos de propiedades de informe. Esto agiliza el trabajo, ya que no hay que estar abriendo y cerrando cuadros de diálogo. Muchas de estas páginas agrupan a ventanas que ya existían en versiones anteriores como ventanas independientes. Tal es el caso de Cálculos, Condición. Pero hay otras: Protección, Entorno de datos, y Otros que son nuevas en este generador.

- Rediseñadas las ventanas Propiedades de campo y banda

En VFP 8.0 la ventana propiedades de un campo se corresponde con la ventana expresión de informe mostrada más adelante en este mismo escrito. Por su parte, VFP 9.0 ofrece una nueva ventana de Propiedades de campo, cuyo formato es también un marco de páginas con páginas que determinan los valores para los diferentes aspectos relativos al campo. El diseño actual es muy práctico e intuitivo.

Las bandas en VFP 9.0 adquieren una nueva dimensión y en el Diseñador se ha incluido una nueva ventana Propiedades para cada banda. El nuevo diseño comprende no sólo las bandas de detalle, sino todas las bandas del informe. Veamos ambos casos.

- Cambios en el Cuadro de diálogo generador de expresiones.

El primer cambio que salta a la vista es la mayor amplitud para la expresión, tanto en la ficha General como en el generador de expresiones.

- Nuevo Cuadro de diálogo selección múltiple

En VFP 9.0 existe el cuadro de diálogo Selección múltiple que permite establecer las propiedades Protección e Imprimir cuando para más de un objeto de diseño a la vez. Permite además cambiar cualquier otra propiedad de Protección a un objeto individual. Para utilizar esta nueva característica, seleccione más de un objeto, y luego haga doble clic en cualquiera de ellos para llamar al cuadro de diálogo Selección múltiple. Resulta muy productivo sobre todo el haber incluido la posibilidad de dar valor a la condición de impresión en el cuadro de texto Imprimir cuando.

- Nueva opción en el Menú Archivo

Este menú contiene una nueva opción Guardar como clase. El objetivo es guardar el Entorno de datos como clase, lo que constituye, a su vez, una de las novedades aportadas al Motor de informes en VFP 9.0. Sobre el tratamiento del Entorno de datos, ver: Visual FoxPro 9.0 - Novedades - Manipulación de entorno de datos en el diseñador de Informes

- Nueva opción en el Menú Ver

En el menú Ver hay una nueva opción - Barra de Diseñador de informes

- Nuevos botones la barra de Herramientas del Diseñador de informes

Incluye dos opciones nuevas: Ajustar página y Propiedades del tipo de letra, las que vemos ahora en la barra de Herramientas.

- Mejoras en el Menú Informes, y el menú contextual del Generador de informes

En el menú Informes hay opciones nuevas: Vista Preliminar, Carga entorno de datos, Bandas opcionales y Propiedades. El resto de las opciones están mejor agrupadas.

- Cambios en la ficha Informes de Herramientas - Opciones.

Las nuevas opciones son:

  • Generador de Expresiones - regula si el nombre del alias va a preceder al nombre del campo: las posibilidades son: siempre, solo para alias no seleccionadas, nunca.
  • Comportamiento Motor de Informes - Determina el comportamiento en tiempo de ejecución: las posibilidades son 80 que mantiene el comportamiento anterior y 90 que es la generación de informes asistida por objetos (comando SET REPORTBEHAVIOR). La vamos a configurar para que trabaje sobre la 90, asistida por objetos.
  • Escala - Agrega nuevos valores: centímetros y pulgadas ()
  • Usar alfabeto para fuentes - indica el conjunto de caracteres de lenguaje que va a estar habilitado en el cuadro de diálogo Fuente. Si marcamos esta opción, en la Ventana Fuentes se activará el combobox Alfabeto. En caso contrario, permanece desactivado.
  • Se agregaron los elementos del contenedor Grid, Forzar al Grid y Mostrar líneas del Grid

Además, el cursor del ratón cambia de forma para dar una clave visual de los objetos que pueden ser redimensionables.

2. Proporcionar nuevas posibilidades:

- Tooltips

Se pueden agregar Tooltips a los controles del informe. Para ello vamos a la ficha Otros de la ventana propiedades. El comando Modificar Tooltip nos permite acceder a un editbox en el que podemos escribir el texto deseado y este se verá reflejado al pasar el ratón por el control. Solamente se aplica a controles.

- Comentarios

De igual forma, se pueden agregar Comentarios a los controles del informe. Desde la ficha Otros de la ventana propiedades y comando Modificar Comentarios nos permite acceder a un editbox en el que podemos escribir el texto deseado y este se verá reflejado al pasar el ratón por el control. Se aplica a controles, bandas y el Informe.

- Modo de recorte para expresiones de caracteres

Se puede definir desde la ficha Formato de la Ventana propiedades, los valores se definen desde el cuadro combinado Modo para truncar expresiones de caracteres.

Si se escoge el Recorte predeterminado lo que ocurre es que se agregan 3 puntos suspensivos indicando que no cabe la expresión en el espacio actual. El resto de las opciones para aplicar recorte de caracteres, se pueden intuir por la descripción, que aparece en idioma español, si tiene instalado el IDE VFP 9.0 en español.

- Protección

En VFP 9.0, puede crear protección para uno o más objetos al utilizar el Diseñador de Informes o Diseñador de etiquetas. Esto ofrece la posibilidad de que el usuario pueda modificar un informe, sin permitirle aún hacer determinados cambios. Para configurar las banderas de protección vemos los cuadros de diálogo correspondientes a Propiedades tanto para campos, como para bandas y también para informes. En el Diseñador de informes esto se ve reflejado en la ficha Protección en la Ventana Propiedades, tanto de controles, bandas, como del informe entero.

Sobre este tema, ver más elementos en: Visual FoxPro 9.0 - Novedades - Protección de Informes

- Posicionamiento absoluto

Desde la ventana propiedades - Ficha general se puede indicar el posicionamiento absoluto con propiedades tales como Top, Left, Height y Width. Es muy útil para controles que no dependen de una banda.

- Tratamiento de imágenes

Se han ampliado las posibilidades para definición de objetos tipo Imagen. Si tenemos definida una clase con valor en la propiedad PictureVal se puede emplear como origen de una imagen en un informe.

- Múltiples bandas de detalle

Contar con la posibilidad de crear múltiples bandas de detalle es una de las mejoras más importantes y más solicitadas. A partir de VFP 9.0 es posible procesar múltiples tablas hijas para cada registro de la tabla padre. Existen posibilidades ilimitadas de lo que se puede hacer con esta nueva característica. En el Diseñador de informes esto se ve reflejado en nuevas opciones de menú, la ficha Bandas Opcionales en la Ventana Propiedades.

Hemos visto algunos elementos, digamos, los más importantes. Seguramente quedan aspectos por explorar.

Espero que haya resultado de utilidad.

Saludos,

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


3 de julio de 2017

¿Donde se encuentra el control?

Artículo original: Where is that control?
http://www.jamesbooth.com/containership.htm
Autor: Jim Booth
Traducción: José Luis Santana Blasco


Contenedores, todos hemos oído esta palabra. “Visual FoxPro tiene un modelo de contenedores muy bueno.” ¿Qué son los contenedores y porque nos tenemos que preocupar por ellos? Por un parte, trabajamos con Visual FoxPro y por tanto necesitamos hacer uso de los contenedores de VFP, pero se pueden dar más razones por las que deberíamos conocer mejor los contenedores. El uso de los contenedores en nuestro beneficio nos permitirá crear clases más reutilizables y flexibles.

Contenedores y  POO

El concepto de contenedores no es nuevo en la Programación Orientada a Objetos. Uno de los diagramas empleado para diseñar sistemas OO se llama Diagrama Todo/Parte. Este diagrama describe objetos que están compuestos de otros objetos. Por ejemplo, un Formulario de entrada de datos contiene un gran número de controles, algunos de estos controles pueden encontrarse agrupados dentro de algún tipo de contenedor, etc.

Los contenedores necesitan que se indique el objeto donde "viven".  Esto no se diferencia de enviar una carta a alguien, en VFP se debe indicar  donde se encuentra el objeto que queremos emplear. Si quisieran enviarme una carta, no pueden  remitirla a Jim Booth sin más indicaciones. El cartero no podrá encontrarme así. Necesitarán decirle al cartero que vivo en un país llamado USA, en un estado llamado Connecticut, en una ciudad llamada Prospect, en una calle llamada Birchwood Terrace, en la casa numero 1, y que mi nombre es Jim Booth. En VFP la sintaxis sería algo así:

USA.Connecticut.Prospect.BirchwoodTerrace.One.JimBooth

Debemos localizar los objetos de VFP de la misma manera, mediante su referencia dentro de su ámbito de actuación. Un cuadro de texto llamado Text1 que se encuentre en un formulario llamado Form1 se referencia como Form1.Text1. Coloca este cuadro de texto en la página 1 de un marco de página en Form1 y su referencia será entonces Form1.PageFrame1.Page1.Text1. Esta es la referencia al contenedor que VFP necesita.

Podemos pensar en este comportamiento como si estuviésemos refiriéndonos a cajas. El formulario es una caja que contiene cosas, dentro de la caja formulario hay una caja Marco de página que contiene únicamente unas cosas llamadas páginas, y en una de estas cajas páginas hay un cuadro de texto que es a lo que queremos hacer referencia. Entonces, le decimos a VFP que mire dentro de la caja formulario una caja marco de página llamada Pageframe1, y que en esta caja PageFrame1 debe buscar una caja página llamada page1 y que a su vez busque dentro de la caja Page1 algo llamado text1.

Comprendiendo el modelo de contenedores de VFP

Visual FoxPro tiene un numero de clases base que son  originalmente contenedores, esto es, que pueden contener otros objetos. Estas clases contenedoras son: Conjunto de Formularios, Formulario, Contenedor, Custom, Control, Cuadricula, Marcos de Página, Página y Columna (se debe tener cuidado de no confundir la clase Contenedor con la habilidad de una clase de ser un contenedor).

Estas clases base pueden ser empleadas para proveer una aproximación a lo que en diseño Orientado a Objetos se conoce como composición. Composición es la combinación de un número de objetos dentro de otro objeto más complejo. La composición se puede realizar de dos maneras: temprana o tardía.

La composición temprana se produce cuando se construye el objeto y posteriormente se le da comportamiento a los miembros que forman la composición. La composición tardía se diferencia de la temprana en que cada clase individual tiene completamente definido todo su comportamiento antes de combinarse en el contenedor. En conveniente seguir una estrategia de diseño basada en  la composición tardía. 

Como ejemplo, consideremos una entrada de datos de una dirección. Esta clase compuesta se puede construir mediante un cuadro de texto para el nombre, dos cuadros de texto para la dirección de la calle, uno más para la ciudad, el estado, y el código postal. Todos estos cuadros de texto se colocan en una clase contenedor para emplearlos como si fuesen un solo objeto. ¿Cual es el beneficio de crear esta clase? El más obvio es la reutilización, en cada formulario de una aplicación que necesite una dirección simplemente pondremos nuestro objeto dirección en el formulario y con esto tendremos todo el trabajo hecho.

Comparemos ahora  la composición temprana en contraposición a la composición tardía. En la composición temprana podemos construir la clase contenedora con los cuadros de texto, y entonces escribir código dentro de varios cuadros de texto para darle el comportamiento. Posiblemente escribamos código dentro de los Valid de los cuadros de texto de Estado y de Código Postal para validar la entrada de estados de USA y el dato del Código Postal. En la composición tardía, definiremos los cuadros de texto de Estado y de Código Postal como clases independientes, y posteriormente combinaremos estas con los otros cuadros de texto en el contenedor.

¿Cual es la diferencia entre los dos métodos? Las diferencias se ven cuando deseamos modificar el comportamiento de la clase dirección. Supongamos que necesita crear una dirección internacional. Con la clase realizada mediante composición temprana, podemos realizar una subclase de la dirección y entonces intentar alterar el comportamiento de los objetos contenidos. Esto es bastante fácil, siempre y cuando queramos conservar todos los controles pero, si se quiere eliminar los cuadros de texto de la dirección, y reemplazar estos por un cuadro de edición nos encontraremos con una sorpresa. Visual FoxPro no permite eliminar ninguno de los cuadros de texto porque son miembros de la clase padre, por tanto la única opción es hacer los cuadros de texto invisibles o construir una nueva clase dirección. Si se escogemos esto último tendremos que rescribir el código de comportamiento para todos los cuadros de texto que deseemos conservar ya que ese comportamiento se define en la clase contenedor y no en los cuadros de texto.

Con composición tardía simplemente realizaremos una nueva clase dirección y pondremos dentro lo que queramos. Dado que la definición del comportamiento de cada cuadro de texto se encuentra en su propia clase, incorporaremos los que queramos y dejaremos fuera aquellos que no necesitemos.

Escribiendo código en contenedores

Bien, hemos visto los beneficios de los contenedores y de la composición pero, ¿cómo se relaciona nuestro código con los contenedores?. Antes hemos visto que necesitamos emplear la jerarquía del contenedor para referenciar en tiempo de ejecución los objetos que se encuentran dentro de él, sin embargo, hay más de una forma de referenciar un objeto, y unas son mejores que otras.

Por ejemplo, mire el formulario de la figura 1.

Figura 1 un formulario con varios niveles de contenedores.

Este formulario tiene dos instancias de una clase contenedora que contiene un cuadro de texto y un botón de comando. La primera instancia se encuentra en Page1 del marco de página  exterior (contenedor 1), la segunda se encuentra en la Page1 dentro del marco de página interior (contenedor 2). El comportamiento programado en el botón de comando hace que cambie el valor de su respectivo cuadro de texto para hacerlo coincidir con el título del botón.

Una solución para esto, en el contenedor 2, es escribir el siguiente código: 

ThisForm.PageFrame1.Page1.PageFrame1.Page1.Container1.Text1.Value = “ABC”

Hemos Empleado la referencia absoluta del control dentro del contenedor. Sin embargo, si se emplea este código y el contenedor 1 se crea copiando desde el contenedor 2 y pegándolo en la otra página, ¿qué comportamiento podemos esperar al pulsar el botón que se encuentra en el contenedor 1? El botón del contenedor 1 cambiará el cuadro de texto en el contenedor 2, dado que la referencia es absoluta.

En vez de programarlo así, podemos emplear referencia relativa para el código del botón:

This.Parent.Text1.Value = “ABC”

Ahora ambos botones referencian el cuadro de texto asociado con él dentro del mismo contenedor. Un diseño mejor sería programar en el contenedor un método para la actualización de los controles y hacer que el botón llame a este método (patrón mediador). ¿ Por qué es mejor el patrón mediador?, porque ahora podremos cambiar el nombre del cuadro de texto y solo necesitaremos corregir el código en el contenedor, el botón no se tendrá que preocupar más por el nombre del cuadro de texto. No solo eso, podremos añadir más controles al contenedor y hacer que el botón actúe sobre todos ellos sin modificar el código del botón.

Algunas Definiciones

En las secciones anteriores se han introducido algunos componentes sintácticos que podrían ser nuevos para alguien.  Os proporciono algunas definiciones.

Término                    Definición

THIS

Una referencia al objeto que contiene el código.

THISFORM

Una referencia al formulario que contiene el código, incluso si el código se encuentra dentro de un objeto contenido en el formulario.

THISFORMSET

Una referencia al conjunto de formularios que contiene el código, incluso si el código se encuentra dentro de un objeto contenido en el formulario o en un formulario contenido en el conjunto de formularios.

Parent

Una referencia al contenedor inmediato superior del objeto que contiene el código. Parent puede ser combinado para ir hacia atrás en el árbol de contenedores, así si un cuadro de texto se encuentra en una página de un marco de página de un formulario, This.parent.Parent.Parent hace referencia al formulario.

ActiveX y Controles OLE

Se han dado un gran número de comunicaciones indicando que el nuevo control Calendar 8.0 no funciona correctamente. La gente que alertó  de esto, intentaba establecer o leer el valor de una propiedad del Control calendario, y recibió un mensaje de error equivocado que les indicó que la propiedad no existía.

Para comprender la razón de este error y la forma de no obtenerlo es importante comprender el contenedor de controles ActiveX de Visual FoxPro. VFP emplea el Control OLE como contenedor de los controles ActiveX. El control calendario se encuentra actualmente dentro de un Control OLE. Para referenciar el control contenido se debe emplear la propiedad Object del Control OLE. Esta propiedad es una referencia al objeto que se encuentra contenido dentro del Control OLE. Las siguientes dos lineas de código explican lo que significa aquí:

ThisForm.OLEControl1.Value = {^1999/01/01} && produce un error

ThisForm.OLEControl1.Object.Value = {^1999/01/01} && no produce error

Aparentemente, VFP se confunde con la propiedad llamada Value, debido a que el Control OLE no la tiene y por tanto VFP informa con un error. Normalmente solo necesitas emplear la propiedad Object cuando ambos, el objeto contenido y el Control OLE comparten propiedades con el mismo nombre. Mi experiencia es que se tiene menos problemas si siempre se emplea la propiedad Object cuando se referencia al objeto que se encuentra dentro del Control OLE.

Sumario

En este artículo únicamente se han tratado las nociones básicas del tema de los contenedores en Visual FoxPro. Es un tema que podría ocupar un libro entero. Se ha comentado los aspectos principales de los contenedores y como les afecta a nuestro código y a nuestros sistemas. Ha visto como sacarle el máximo provecho al modelo de contenedores de Visual FoxPro en su trabajo y como evitar problemas que pueden venir de una mala comprensión de la forma de trabajo de los contenedores.

28 de junio de 2017

Múltiples bandas de detalle en informes (2)

La necesidad de obtener informes con múltiples bandas de detalle en informes de Visual FoxPro, es tan antigua como la propia herramienta. Pero hasta ahora no contábamos con facilidades para lograr el resultado esperado por los usuarios sin que nos representara un dolor de cabeza.

Hace unos días, escribí sobre este tema. (Múltiples bandas de detalle en informes (1)). Luego de haber adquirido un poco más de experiencia, me decido a revisar un poco el tema, para complementar algunos de sus aspectos.

¿Qué ocurría antes de VFP 9.0?

Pues, lo podíamos lograr. Pero no era nativo y había que recurrir a procesos que si bien, eran posibles, también eran complejos. Se trataba, en pocas palabras, de crear un cursor con los campos necesarios a mostrar en cada banda y algún o algunos campos adicionales, que controlaban que los datos fueran agrupados correctamente en bandas.

Dentro del Diseñador de informes, la tarea consistía en crear dentro de la única banda de detalle existente, tantos grupos de campos a mostrar como bandas finales necesitamos. En este caso, al ver el diseñador, teníamos campos superpuestos y resultaba bien difícil, como ya dije, era complejo; pero era posible. Felizmente ahora este proceder es nativo.

Múltiples bandas de detalle

Contar con la posibilidad de crear múltiples bandas de detalle es una de las mejoras más importantes y más solicitadas. A partir de VFP 9.0 es posible procesar múltiples tablas hijas para cada registro de la tabla padre. Existen posibilidades ilimitadas de lo que se puede hacer con esta nueva característica. Veamos como se controla el procesamiento de registros.

Procesamiento de registros

El informe tiene un cursor conductor - este cursor se procesa una única vez. El procesamiento de sus registros se pausa por los cambios diseñados en los grupos. Entonces el motor de informes deja de moverse por el cursor conductor y realiza las acciones pertinentes, ejemplo escribe el pie de grupo para el grupo que termina y el encabezado de grupo para el siguiente grupo y luego continúa procesando el cursor. Se admiten encabezados y pies para bandas de detalle. Son similares a los encabezados y pies de grupos en alguna medida, tienen algunas diferencias.

Para cada registro padre que se procesa, ocurre lo siguiente:

  • Se procesa el encabezado de la banda 1.
  • Se procesa el detalle de la banda 1 una vez para cada registro hijo en el alias destino asociado.
  • Se procesa el pie de la banda 1.
  • Se procesa el encabezado de la banda n
  • Se procesa el detalle de la banda n una vez para cada registro hijo en el alias destino asociado.
  • Se procesa el pie de la banda n.

Donde,

Expresión del Alias destino - es el término utilizado para describir qué tabla es la tabla conductora para una banda de detalle en particular.

Definir multibandas - De forma predeterminada los informes se crean con una sola banda de detalle. Se pueden adicionar bandas desde el diálogo Bandas Opcionales incrementando el valor dentro del control Spinner (hasta 20). Luego, hay que definir el alias para cada banda, este aspecto lo veremos con detenimiento. Al aumentar un número aparece una nueva banda de detalle y al disminuir uno se elimina la banda con mayor número (la última en haber sido creada)

El trabajo multibandas nos ofrece nuevas posibilidades; pero nos exige ser mucho más cuidadosos, porque ahora en cada banda no estará la tabla padre sino una de las hijas, y hay que definir el nombre del alias en el cuadro de texto Expresión del Alias destino (Target alias expresion,) del Cuadro de diálogo Propiedades de la banda de detalle (Detail band propierties). En caso de estar trabajando con una sola banda, pues no pasa nada si la dejamos en blanco, toma la tabla padre que será justo lo que deseamos.

El cuadro de diálogo Detalles contiene las propiedades de la banda de Detalles.

Alias destino (Target alias) - Es el alias que se recorre en la banda donde se encuentra. Es una expresión. Debe ir entre comillas, para que evalúe la expresión y obtenga los valores existentes en el Alias indicado. Si no se ponen las comillas, se entiende que es una expresión, por ejemplo Clientes, pudiendo ser una variable, o un campo que contiene el nombre del alias que se obtiene en tiempo de ejecución. Este nombre va a aparecer en la barra gris que define la banda. Se recomienda definir los campos incluyendo el nombre del alias para evitar confusiones. El tipo de alias dependerá del contenido que se quiera mostrar.

Nueva función para cláusula SUMMARY - Elimina toda la información de las bandas, incluyendo su detalle, encabezado y pie de banda. Si existen expresiones a evaluar en al entrar y al salir de alguna banda no se van a ejecutar con la cláusula SUMMARY. Por otra parte no elimina los encabezados y pies de grupos que se hayan definido y sus expresiones a evaluar para en al entrar y al salir del grupo sí se van a evaluar.

Las variables y expresiones calculadas en el informe adquieren una nueva dimensión, Reset based on, en la ficha Variables de la ventana Propiedades indica que se deben procesar los registros de la tabla hija y no se limpia hasta que no termine el informe, lo que permite realizar cálculos y combinaciones entre las bandas utilizando valores calculados de unas en otras. Al seleccionar un número de banda específica, VFP procesa el cálculo solamente durante el proceso de esa banda. Si se escoge más de un sitio, pues se procesa mientras duren las bandas seleccionadas.

Diferencias entre las Bandas de Detalles y las Agrupaciones de Datos (Data Grouping)

Sirven para mostrar diferentes tipos de relaciones entre los datos. Las múltiples bandas de detalle sirven para mostrar diferentes datos de tablas hijas de una tabla principal que no estén relacionadas entre sí. Mientras, las agrupaciones de datos, permiten, agrupar datos por diferentes criterios a partir de tablas relacionadas entre sí con índices creados y activos en ese momento.

Bandas de Detalles Agrupaciones de Datos
No se anidan, son consecutivas Se pueden anidar
No se crean subgrupos dentro de ellas Se crean subgrupos
No dependen del alias (target alias). Se pueden mostrar independientemente de si existan o no registros que cumplan las condiciones en esa banda, por tanto pueden existir sin que haya banda de detalle asociado. Dependen del alias (target alias) que se le indique. No se muestran si no existe al menos un registro en la banda detalle de ese grupo
Su límite máximo es de 20 bandas Su límite máximo es de 74 bandas
Su límite mínimo es una banda Su límite mínimo es cero bandas
Cuando termina una banda, si existe otra banda de detalle el puntero se mueve nuevamente al inicio del alias actual Cuando acaba un grupo, el puntero se mueve al siguiente registro
La expresión para el alcance de una banda de detalle (target alias) se limita a una expresión que de nombre a una tabla abierta en la sesión de datos actual No existe límite para la creación de una expresión que defina el alcance de un grupo

Diferencias entre los encabezados y pies de Bandas de Detalle y los encabezados y pies de Bandas de Grupos

Encabezados y pies de Bandas de Detalle Encabezados y pies de Bandas de Grupos
Se pueden mostrar los encabezados y pie de banda (son opcionales). Si existen, se muestran incluso cuando no hay datos, por ejemplo que no haya coincidencia entre la tabla padre y la hija. Si no se desea imprimir el encabezado de la banda de detalle en este caso, debe utilizar una expresión del tipo NOT EOF(<target alias>) en Imprimir cuando (Print When) Si no hay elemento de agrupación en la selección, no se puede mostrar encabezado y/o pie para él, debido a que no existe tampoco el grupo como tal. Tiene que existir algún elemento, incluso si no se desea imprimir el detalle y se emplea la cláusula SUMMARY en el comando REPORT FORM, que se encarga de eliminarla.
La cláusula SUMMARY en el comando REPORT FORM, elimina todas las bandas de detalle, así como sus correspondientes encabezados y pies. La cláusula SUMMARY en el comando REPORT FORM, NO elimina los encabezados y pies de las bandas de grupo.
Si se incluye la cláusula SUMMARY, NO se ejecutan los eventos OnEntry / OnExit. Aunque se incluya la cláusula SUMMARY, los eventos OnEntry / OnExit SIEMPRE se ejecutan.

Los encabezados y pies de ambos tipos de bandas tienen características similares, por ejemplo la evaluación de su contenido y el código colocado en sus eventos OnEntry y OnExit ocurren en el mismo momento.

Variables y Cálculos del informe

Con la introducción de las múltiples bandas de detalle, las variables y cálculos del informe tienen algunas peculiaridades. Es necesario entender completamente como se procesan para utilizarlas con eficiencia. De otro modo, puede obtener resultados inesperados.

El cuadro desplegable Reiniciar basado en (Reset value based on) que vemos en el cuadro de diálogo Variables del informe determina que la variable se reinicia basada en los cambios del valor de la opción seleccionada. Si se define en el informe más de una banda, cada banda de detalle se agrega al cuadro desplegable.

Al seleccionar una Banda de detalle en particular se procesa este cálculo sólo para los registros del target alias de esta banda de detalle. La variable de informe no se altera mientras se procesan los registros en otros alias. Esto permite asociar una variable de informe para una banda de detalle en particular. El valor de la variable no se limpia hasta que el encabezado de esa misma banda de detalle se procesa para el siguiente registro de la tabla padre.

Si selecciona otro valor en Reiniciar en base a diferente a una banda de detalle, el cálculo se procesa en varios lugares. Primero, se aplica el cálculo para cada registro padre. Segundo, se aplica para cada registro en el alias destino de la primera banda de detalle. Luego, se aplica el cálculo para cada registro en el alias destino de la segunda banda de detalle, etc.

Informes sencillos

Un informe sencillo con múltiples bandas de detalle consiste en una tabla padre que conduce el informe y dos o más tablas hijas que están relacionadas con la tabla padre.

Un ejemplo de este caso, lo podemos ver en el panel Solutions, al que accedemos desde Menú - Herramientas - Administrador del panel de Tareas. A los ejemplos Solutions también se puede acceder desde la ventana de comandos

DO (HOME(2)+"Solution\Solution") 

Entonces, buscamos en el panel Solutions Samples, abrimos el árbol - New in Visual FoxPro 9.0 y buscamos "The typical multiple detail band report"

El aspecto del informe visto desde el Diseñador es el que se muestra en la siguiente figura. En ella se aprecian dos bandas de detalle asociadas a dos alias diferentes, agrupadas a su vez a nivel de la tabla principal, padre o conductora del informe. El resultado en este caso es un informe donde para cada Employee.EmployeeID se van a mostrar los datos que están almacenados en los alias employeeterritories y orders.

Cálculos (sum / count)

Un informe con múltiples bandas de detalle no necesariamente necesita tener múltiples tablas hijas. La misma tabla hija puede ser utilizada en más de una banda de detalle.

Por ejemplo para obtener Totales de grupos, antes de VFP 90, era muy difícil imprimir subtotales en la banda encabezado de grupo. El dato tenía que ser procesado previamente para calcular los totales antes de ejecutar el informe. Con VFP90, no se requiere procesamiento previo. Para ello la idea es crear bandas múltiples de detalle.

En la banda de detalle 1, TargetAlias = tabla hija. Una variante puede ser, marcar bandas de encabezado y pie y no colocar nada en la banda de detalle. En la banda pie para la banda de detalle 1, podemos agregar etiquetas y campos que serán calculados. Para ellos, en la ficha Cálculos de la ventana Propiedades, estableceremos: Iniciar basado en (Reset based on) Banda de detalle 1.

En la banda de detalle 2 TargetAlias = tabla hija, se marcan bandas de encabezado y pie y se agrega cualquier otro dato necesario a la banda de detalle 2.

La definición de informe descrita anteriormente, dice al Generador de informes de VFP 9.0, que procese dos veces la tabla hija para cada registro en la tabla Padre. La primera vez, realiza los cálculos que se hayan pedido (count y/o sum) y lo imprime. El segundo pase de la tabla hija imprime el detalle. Este proceso se repite para cada registro de la tabla Padre.

Otra opción puede ser no mostrar nada en la Banda 1, es decir, emplearla solamente para el cálculo. La banda 2 se encargaría entonces de mostrar los datos calculados por la Banda1.

Es muy importante comprender que el resultado de los cálculos a los que se asigne “Iniciar basado en (Reset based on) una Banda de detalle determinada, van a mantener su valor hasta el final de ejecución de todas las bandas y sólo cambiará en el momento que se vuelva a ejecutar la banda para la que se ha establecido su alcance.

Cálculos (porcentajes)

Otro concepto en los informes es mostrar el porcentaje del total de cada línea de detalle, en la medida en que se imprimen las líneas. Esto puede ser realizado también con múltiples bandas de detalle. Para ello, la idea es crear múltiples bandas de detalle. En la primera banda, TargetAlias = tabla hija, se marcan bandas de encabezado y pie y no se coloca nada en la banda de detalle.

Luego, crear algunas variables de informe, por ejemplo: una variable para el total donde Valor a almacenar igual a Tablahija.CampoATotalizar establezca Tipo de cálculo igual a Suma y Reiniciar basado en Detail 1. Y luego, crear una variable llamada pare el Porciento, donde Valor a almacenar igual a ROUND(100 * TablaHija.CampoATotalizar / nTotal, 2), Tipo de cálculo = Ninguno

Los datos van a estar en la banda de detalle 2, junto a un control que devuelve el resultado de la expresión nPorciento. En el pie de banda 2 se agrega el objeto Total, con expresión igual a Tablahija.CampoATotalizar, a Tipo de cálculo = Suma y Reiniciar basado en = Detail 2. Igual para Total Porciento, con expresión = nPorciento.

La definición de informe descrita anteriormente, dice al Generador de informes de VFP 9.0, que procese dos veces la tabla Hija para cada registro en la tabla Padre. La primera vez totaliza los valores para que puedan ser utilizados en el segundo pase. El segundo pase de la tabla Hija imprime el dato para el usuario, utilizando la variable de informe que fue calculada en la primera banda de detalle. Este proceso se repite para cada registro en la tabla Padre.

Volvemos a los ejemplos Solutions y encontramos para este caso "A multiple detail band report used for calculations"

En la siguiente figura se observan claramente cómo las dos bandas de detalle tienen el mismo alias. Lo que se hace en este caso es, que no se muestra nada en la Banda 1, la que es empleada solamente para el cálculo. La banda 2 se encarga de mostrar los datos calculados por la Banda1.

Sin dudas, es un paso muy grande de avance, con respecto a lo que existía antes de la versión VFP 9.0.

Por una parte, no es necesario ningún proceso previo para la creación de cursores y además, todos los campos son visibles en la ventana del Diseñador, lo que influye directa y favorablemente en la productividad y los tiempos dedicados al mantenimiento de los informes.

No obstante, hacerse con el control de las múltiples bandas de detalle puede tomar un tiempo. Hay que entender cómo trabajan todas las tablas juntas, establecer correctamente las alias para cada banda y es esencial e imprescindible establecer correctamente la relación entre las tablas. Debe tener también el control sobre cómo se afectan las variables y los cálculos por las múltiples bandas de detalle.

Espero que haya resultado de utilidad.

Saludos,

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


24 de junio de 2017

Múltiples bandas de detalle en informes (1)

En los foros apareció este comentario "Alguien ha realizado un reporte con dos detalles, es decir tengo 2 tablas que deseo que aparezcan en mi reporte... Que cuando termine la primera, a continuación se muestre la otra tabla"

Allí hubo una primera respuesta correcta. Vamos a ampliar un poco este tema, sobre la base de lo que hay en la beta pública de VFP 9.0. Seguramente, con el tiempo se podrá completar más; pero vamos asimilando las nuevas características de VFP 9.0 Debido a que este artículo se ha realizado con la versión Beta pública de VFP9, algunos aspectos pudieran sufrir modificaciones cuando se libere la versión definitiva.

Bandas multidetalles - Esta una de las mejoras más importantes de esta nueva versión. Veamos un ejemplo donde la tabla padre Clientes se enlaza con dos tablas hijas: Ordenes y Pagos. El objetivo del informe es obtener todos los datos agrupados del cliente. Creando 2 bandas de detalle, y teniendo las dos tablas hijas relacionadas con Cliente.dbf el resultado que obtenemos es el deseado según la pregunta planteada en el foro… " un reporte con dos detalles, 2 tablas que deseo que aparezcan en mi reporte...Que cuando termine la primera, a continuación se muestre la otra tabla"

¿Cómo se obtiene?

Definir multibandas - De forma predeterminada los informes se crean con una sola banda de detalle. Se pueden adicionar bandas desde el diálogo Bandas Opcionales (Optional Bands), incrementando el valor dentro del control Spinner (hasta 20). Luego, hay que definir el alias para cada banda, este aspecto lo veremos con detenimiento. Al aumentar un número aparece una nueva banda de detalle y al disminuir uno se elimina la banda con mayor número (la última en haber sido creada)

El trabajo multibandas nos ofrece nuevas posibilidades; pero nos exige ser mucho más cuidadosos, porque ahora en cada banda no estará la tabla padre sino una de las hijas, y hay que definir el nombre del alias en el cuadro de texto Expresión del Alias principal (Target alias expresion,) del Cuadro de diálogo Propiedades de la banda de detalle (Detail band propierties). En caso de estar trabajando con una sola banda, pues no pasa nada si la dejamos en blanco, toma la tabla padre que será justo lo que deseamos.


El cuadro de diálogo Detalles (Detail) contiene las propiedades de la banda de Detalles.

Alias principal (Target alias) - Es el alias que se recorre en la banda donde se encuentra. Es una expresión. Debe ir entre comillas, para que evalúe la expresión y obtenga los valores existentes en el Alias indicado. Si no se ponen las comillas, se entiende que es una expresión, por ejemplo Clientes, pudiendo ser una variable, o un campo que contiene el nombre del alias que se obtiene en tiempo de ejecución. Este nombre va a aparecer en la barra gris que define la banda. Se recomienda definir los campos incluyendo el nombre del alias para evitar confusiones. El tipo de alias dependerá del contenido que se quiera mostrar.

Nueva cláusula SUMMARY - Esta cláusula nos permite obtener sólo los encabezados y pie de cada banda sin mostrar el contenido de los detalles, es una buena opción, porque ahora el usuario podrá decidir si quiere la información compactada o detallada. Vea más adelante los resultados que se obtienen al emplearla.

Las variables y expresiones calculadas en el informe adquieren una nueva dimensión, Reset based on, en la ficha Variables de la ventana Propiedades indica que se deben procesar los registros de la tabla hija y no se limpia hasta que no termine el informe, lo que permite realizar cálculos y combinaciones entre las bandas utilizando valores calculados de unas en otras. Al seleccionar un número de banda específica, VFP procesa el cálculo solamente durante el proceso de esa banda. Si se escoge más de un sitio, pues se procesa mientras duren las bandas seleccionadas.

Diferencias entre las Bandas de Detalles y las Agrupaciones de Datos (Data Grouping)

Bandas de DetallesAgrupaciones de Datos
Se pueden anidarNo se anidan, son consecutivas
No se crean subgrupos dentro de ellasSe crean subgrupos
Dependen del alias principal (target alias) que se le indiqueNo dependen del alias principal (target alias)
Su límite máximo es de 20 bandasSu límite máximo es de 74 bandas
Su límite mínimo es una bandaSu límite mínimo es cero bandas
Cuando termina una banda, si existe otra banda de detalle el puntero se mueve nuevamente al inicio del alias actualCuando acaba un grupo, el puntero se mueve al siguiente registro
La expresión para el alcance de una banda de detalle (target alias) se limita a una expresión que de nombre a una tabla abierta en la sesión de datos actualNo existe límite para la creación de una expresión que defina el alcance de un grupo

Diferencias entre los encabezados y pies de Bandas de Detalle y los encabezados y pies de Bandas de Grupos

Encabezados y pies de Bandas de DetalleEncabezados y pies de Bandas de Grupos
Se pueden mostrar los encabezados y pie de banda (son opcionales). Si existen, se muestran incluso cuando no hay datos, por ejemplo que no haya coincidencia entre la tabla padre y la hija. Si no se desea imprimir el encabezado de la banda de detalle en este caso, debe utilizar una expresión del tipo NOT EOF(<target alias>) en Imprimir cuando (Print When)Si no hay elemento de agrupación en la selección, no se puede mostrar encabezado y/o pie para él, debido a que no existe tampoco el grupo como tal. Tiene que existir algún elemento, incluso si no se desea imprimir el detalle y se emplea la cláusula SUMMARY en el comando REPORT FORM, que se encarga de eliminarla.
La cláusula SUMMARY en el comando REPORT FORM, elimina todas las bandas de detalle, así como sus correspondientes encabezados y pies.La cláusula SUMMARY en el comando REPORT FORM, NO elimina los encabezados y pies de las bandas de grupo.
Si se incluye la cláusula SUMMARY, NO se ejecutan los eventos OnEntry / OnExit.Aunque se incluya la cláusula SUMMARY, los eventos OnEntry / OnExit SIEMPRE se ejecutan.

Los encabezados y pies de ambos tipos de bandas tienen características similares, por ejemplo la evaluación de su contenido y el código colocado en sus eventos OnEntry y OnExit ocurren en el mismo momento.

Otros Ejemplos

1. Con un único cursor.

Para poner los totales al inicio, en el encabezado de banda.

Hacer 2 bandas con igual alias, en la primera banda se calcula el total y se muestra el valor calculado en el encabezado. Pero esta banda no tiene más nada, no hay nada en el detalle ni el pie de banda.

Entonces, en la segunda banda están los datos en el detalle; lo importante es que VFP no está apuntando al último registro de esa tabla, por haber hecho SUM, COUNT, o la operación que sea, no, el puntero de la tabla al llegar a la segunda banda apunta al registro que corresponde por la relación padre-hija que existe con la tabla padre que es quien rige el informe.

El ejemplo concreto es imprimir para cada vendedor sus ventas (valor) y el porcentaje con relación al total, para lo cual necesitamos el total previamente calculado.

En este caso se utiliza una expresión representando el alias igual al alias principal (target alias) en la primera banda de detalle. Esto le dice al VFP que desea procesar todos los registros de la tabla antes de moverse a la siguiente banda de detalles.

2. Con múltiples cursores.

Cuando tiene múltiples cursores, utilice un alias principal (target alias) en blanco cuando desee que una banda sea procesada sólo una vez. Esto es útil si se desea mostrar información resumida, algo similar a lo que brinda el encabezado y pie de grupo.

Ejemplo:

Necesitamos agrupar el informe por ClienteID. Podemos ofrecer información sobre el Cliente en el Encabezado o pie de banda de grupo. Pero, podemos además colocar una banda de detalle entre Ordenes y Pagos que contenga información adicional sobre el Cliente, que puede ser, por ejemplo el estado actual de su cuenta. Al no colocar Alias principal (Target alias) su contenido aparecerá sólo antes de que se vaya a procesar la banda de Pagos. No hace falta escribir código en Imprimir cuando (Print When) ni seleccionar Eliminar datos repetidos (Suppress Repeated Value).

¿Qué ocurre en tiempo de ejecución? VFP verifica que el dato Alias principal (target alias) esté vacío o en uso, luego verifica que sea el alias actual o hija. Si el desarrollador tiene establecido su comportamiento de uno-a-muchos, por ejemplo con SET SKIP en su entorno de datos, el VFP lo respeta. Si no tiene definido nada, crea uno y lo elimina al finalizar el informe para dejar el entorno tal y como estaba.

  • Si la primera banda de detalles no tiene Alias principal (target alias), procesa el primer registro de la tabla principal y se mueve a la siguiente banda de detalles.
  • Si la primera banda de detalles tiene Alias principal (target alias) que es un alias hijo del alias principal, procesa todos los registros de la tabla hija mostrando los relacionados y pasa a la siguiente banda. El puntero permanece en el primer registro de la tabla principal.
  • Si la primera banda tiene el nombre de alias igual al nombre del alias principal, procesa todos los registros del alias principal y luego se mueve a la siguiente banda de detalles.

Limitaciones:

  • Mezclar múltiples columnas y múltiples bandas. Utilizar múltiples columnas en informes multibandas puede trabajar en algunos casos; pero pueden darse muchos casos de efectos desagradables si trata de darle columnas diferentes a las bandas y si no inician en página nueva para cada grupo y van de arriba abajo.
  • No es posible re-ordenar las bandas. No es un proceso nativo, pero se puede hacer eliminando y adicionando bandas y re-colocando los objetos en la posición que se desea. Se puede agrandar el tamaño de otras bandas, mover hacia ellas el contenido de la banda a eliminar re-crearla y luego re-posicionar los objetos. Se pueden seleccionar todos los objetos de la banda si se hace Doble-Clic en la banda separadora.

Como vemos, las posibilidades para trabajar los informes se amplían notablemente en esta nueva versión y aunque quedaría mucho por lograr, al menos, esta pregunta de los foros, ya tiene respuesta nativa en VFP 9.0

Saludos,

Ana María Bisbé York
http://www.amby.net

20 de junio de 2017

Emplear Hiperenlaces en informes

Artículo original: Hyperlinks Your Reports
Autor: Doug Hennig
Traducido por: Ana María Bisbé York


El mes pasado, Doug Hennig abordó la nueva clase ReportListener en VFP 9 y cómo puede ser utilizada para controlar la salida de informes de formas que anteriormente eran imposibles. Este mes, habla sobre cómo agregar hiperenlaces vivos a la salida generada de informes, permitiendo algunas acciones que se pueden realizar cuando se hace clic sobre ellos.

¿No sería bueno poderle decir a VFP que agregue hiperenlaces a un campo en un informe? Entonces, el cliente podría hacer Clic sobre el hiperenlace para navegar a alguna información relacionada. Por ejemplo, un informe que muestre los clientes y sus sitios Web o sus direcciones de correo electrónico podría tener enlaces vivos, haciendo clic a sus sitios Web podría navegar el examinador (browser) a esta URL.

Incluso, mucho más interesante sería tener la posibilidad de navegar a algún otro lugar dentro de su propia aplicación. Por ejemplo, haciendo clic en un nombre de compañía en un informe podría saltar a un formulario de entrada de datos del cliente para el que ha seleccionado la compañía.

Debido a que la vista preliminar del informe que viene con VFP no admite objetos vivos, sobre los que se pueda hacer clic en un informe, la vía más sencilla de implementarlo es usando HTML, que admite hiperenlaces de forma nativa.

Hiperenlazando informes

VFP viene con un report listener que crea HTML (la clase HTMLListener generada en la aplicación ReportOutput.APP e incluida también en _ReportListner.VCX en la carpeta  FFC), pero yo estaba seguro que necesitaríamos mucho trabajo para lograr el resultado con hiperenlaces. Sin embargo, me sorprendí gratamente al descubrir cuán poco esfuerzo hace falta para ello.

Primeramente, una pequeña introducción. HTMLListener es una subclase de XMLDisplayListener, el que a su vez es una subclase de XMLListener y esta es una subclase de _ReportListener, la clase que he descrito el mes pasado y recomiendo su uso normalmente como clase padre para sus propios listeners.

(Nota de la traductora: Este artículo fue publicado en la Revista FoxTalk 2.0 en  Marzo-2005. El autor se refiere a un artículo publicado en el mes de Febrero en la propia revista bajo el título Lintening to a Report, recientemente ha sido también liberado y se encuentra disponible en las páginas de Microsoft enhttp://msdn.microsoft.com/library/en-us/dnfoxtk05/html/ft05b6.asp.

Cuando utiliza HTMLListener, ya sea directamente intanciándolo y utilizándolo como un listener para un informe, o al especificar OBJECT TYPE 5 en el comando REPORT, en realidad genera un XML para el informe (esto lo hace la clase padre), entonces aplica una transformación XSL del XML para crear un HTML. El XSLT que utiliza está definido en el método GetDefaultUserXSLTAsString.

El XSLT predeterminado utilizado por HTMLListener es muy complejo, y no es mucho más que un experto XSL, yo pensé que podría ser una tarea agobiante determinar qué cambios se debían agregar para que admitiera hiperenlaces. Sin embargo, comencé a escarbar en GetDefaultUserXSLTAsString, y he descubierto lo siguiente:

<xsl:when test="string-length(@href) > 0">
<A href="{@href}">
<xsl:call-template name="replaceText"/>
</A>
</xsl:when>

Este XSL agrega una etiqueta al HTML si existe un atributo HREF en el elemento actual en el XML.Esto es genial - significa que el HTMLListener ya admite hyperlinks! Sin embargo, no hay búsquedas por "HREF" en XMLDisplayListener ni XMLListener, entonces, ¿cómo se puede agregar este atributo a un elemento, especialmente de forma dinámica?

Antes de avanzar mucho más, encontré que los atributos para un elemento en particular se configuran en el métodoGetRawFormattingInfo de XMLListener. Entonces, he subclaseado HTMLListener y añadido el comportamiento deseado a este método.

El siguiente código, tomado de HyperlinkListener.PRG, proporciona un listener que genera un hiperenlace sobre un objeto en el informe si el campo User memo de este objeto contiene la directiva "*:URL =" seguida por la expresión que utiliza como URL

define class HyperlinkListener as HTMLListener ;
  of home() + 'ffc\_ReportListener.vcx'
  QuietMode = .T.
  && QuietMode queda predeterminado para que no haya retorno 
  dimension aRecords[1]
  && matriz de información de cada registro FRX
  * Antes de ejecutar el informe, se recorre el FRX y
  * se guarda información sobre cada registro con sus directivas
  * en el campo memo USER en la matriz aRecords.
  function BeforeReport
    dodefault()
    with This
      .SetFRXDataSession()
      dimension .aRecords[reccount()]
      scan for atc('*:URL', USER) > 0
        .aRecords[recno()] = ;
          alltrim(strextract(USER, '*:URL =', ;
          chr(13), 1, 3))
      endscan for atc('*:URL', USER) > 0
      .ResetDataSession()
    endwith
  endfunc
  * Si el campo actual tiene una directiva, añade el URL
  * a los atributos del nodo.
  function GetRawFormattingInfo(tnLeft, tnTop, ;
    tnWidth, tnHeight, tnObjectContinuationType)
    local lcInfo, ;
    lnURL
    with This
      lcInfo = dodefault(tnLeft, tnTop, tnWidth, ;
        tnHeight, tnObjectContinuationType)
      lcURL = .aRecords[recno('FRX')]
      if not empty(lcURL)
        .SetCurrentDataSession()
        lcInfo = lcInfo + ' href="' + ;
          textmerge(lcURL) + '"'
       .ResetDataSession()
     endif not empty(lcURL)
    endwith
    return lcInfo
  endfunc
enddefine

El evento BeforeReport se dispara justo antes de que se ejecute el informe. Utiliza el método SetFRXDataSession para seleccionar la sesión de datos en la que está el cursor FRX y luego busca a través del FRX y coloca la expresión URL para cada objeto que tiene la directiva en una matriz. Llama a ResetDataSession al final para restaurar la sesión de datos en la que está el listener.

El método GetRawFormattingInfo utiliza DODEFAULT() para configurar el comportamiento habitual, el que genera los atributos para un elemento XML como una cadena. Esto entonces verifica el elemento de matriz apropiado (la sesión de datos del cursor FRX se seleccionó por código en XMLListener antes de que se ejecute este código) para ver si el objeto actual del informe tiene la directiva y si es así, agrega un atributo HREF al elemento XML. Llama a SetCurrentDataSession para seleccionar la sesión de datos utilizada por los datos del informe y utiliza TEXTMERGE() sobre la expresión URL porque la expresión va a contener algo específico para cada registro, algo como <<CustomerID>>. Finalmente, realiza labores esenciales de mantenimiento al llamar a .ResetDataSession() para dejar la sesión de datos tal y como la encontramos.

¡¡ Es todo !! Veamos ahora algunos ejemplos de cómo podemos utilizar este listener.

Ejemplo 1: Enlaces vivos a URLs

Enlaces.FRX es un ejemplo sencillo que muestra cómo trabaja este listener. Es un informe sobre la tabla Links, que contiene una lista de nombres de compañías y sus sitios Web. El campo website en el informe tiene  "*:URL = http://<<trim(website)>>" en el campo User memo. El programa Links.PRG ejecuta este informe, utilizando HyperlinkListener como el report listener, y utiliza la clase _ShellExecute en las FFC para mostrar el archivo HTML en su examinador predeterminado. La Figura 1 muestra los resultados.

loListener = newobject('HyperlinkListener', ;
  'HyperlinkListener.prg')
loListener.TargetFileName = fullpath('Links.html')
report form Links object loListener
loShell = newobject('_ShellExecute', ;
  home() + 'ffc\_Environ.vcx')
loShell.ShellExecute(loListener.TargetFileName)
Figura 1

Ejemplo 2: Informes tipo Drilldown (cambio rápido)

HyperlinkReports.SCX  es un ejemplo más complejo. Como puede ver en la Figura 2, presenta una lista de información de cliente. Sin embargo, este HTML es mostrado en un control ActiveX Web Browser embebido en un formulario VFP en lugar de una ventana examinar. Al hacer clic en un nombre de compañía, VFP ejecuta un informe de las órdenes de ese cliente y los muestra en el formulario como se observa en la Figura 3. El informe de las órdenes tiene también hiperenlaces que vuelven a mostrar la lista de clientes. Entonces, este formulario proporciona informes del tipo drilldown (cambio rápido).

Figura 2 y Figura 3

El método Init de un formulario utiliza la clase HyperlinkListener para generar un archivo HTML hiperenlazado desde el informe HyperlinkCustomers y llama al método ShowReport para mostrarlo en el control Web Browser. Además, mantiene una colección de archivos HTML generados por el formulario, de tal forma que puedan ser eliminados cuando se cierra el formulario.

with This
  * Crea una colección de los archivos HTML que hemos creado
  * para que podamos nuclearlos (nuke) todos cuando hayamos cerrado.
  .oFiles = createobject('Collection')
  * Create the customers report.
  .oListener = newobject('HyperlinkListener', ;
    'HyperlinkListener.prg')
  .oListener.TargetFileName = ;
    fullpath('HyperlinkCustomers.html')
  report form HyperlinkCustomers object .oListener
  .oFiles.Add(.oListener.TargetFileName)
  * Display it.
  .ShowReport()
endwith

El método ShowReport simplemente comunica al control Web Browser que cargue el archivo HTML actual:

local lcFile
lcFile = This.oListener.TargetFileName
This.oBrowser.Navigate2(lcFile)

En lugar de generar informes de órdenes para cada cliente e hiperenlazarlos, he decidido generar los informes en dependencia del pedido que se haga en cada momento, al hacer clic sobre el nombre de cliente. Para hacer esto, necesité interceptar el clic al hiperlink. Afortunadamente, esto es fácil de hacer: Sencillamente coloqué código en el evento BeforeNavigate2 del control Web Browser.

Para decirle a BeforeNavigate2 que no es un hiperenlace normal, he utilizado la convención de "vfps://," el que  entiende por "VFP script," en lugar de "http://." El código para BeforeNavigate2  busca esta cadena en la URL y si la encuentra, ejecuta el código en el resto de la URL en lugar de estar navegando por ella. Por ejemplo, el campo memo User del campo CompanyName  en el HyperlinkCustomers.FRX  tiene lo siguiente:

*:URL = vfps://Thisform.ShowOrdersForCustomer('<<CustomerID>>')

El report listener HyperlinkListener convertirá esto en una etiqueta como <a href="vfps://Thisform.ShowOrdersforCustomer('ALFKI')">  para el cliente cuyo CustomerID es ALFKI. Al hacer clic en este hiperenlace en el control Web Browser, BeforeNavigate2 se dispara y el código de este evento se desentiende de este pedazo "vfps://" y ejecuta el resto. Además, establece el parámetro Cancel pasado por referencia, a .T. para indicar que la navegación normal no puede tener lugar (similar a utilizar NODEFAULT en un método VFP). He aquí el código para BeforeNavigate2:

LPARAMETERS pdisp, url, flags, targetframename, ;
  postdata, headers, cancel
local lcMethod
if url = 'vfps://'
  lcMethod = substr(url, 8)
  lcMethod = left(lcMethod, len(lcMethod) - 1)
  && strip trailing /
  &lcMethod
  cancel = .T.
endif url = 'vfps://'

El método ShowOrdersForCustomer , ejecutado al hacer Clic en un nombre de compañía, ejecuta el informe HyperlinkOrders para un cliente especificado, lo muestra en el control Web Browser, y agrega el nombre de archivo a la colección de archivos a ser eliminados cuando se cierra el formulario.

lparameters tcCustomerID
with This
  .oListener.TargetFileName = ;
    fullpath(tcCustomerID + '.html')
  report form HyperlinkOrders object .oListener ;
    for Orders.CustomerID = tcCustomerID
  .ShowReport()
  .oFiles.Add(.oListener.TargetFileName)
endwith

El campo CustomerName  en el informe HyperlinkOrders tiene  "*:URL = vfps://Thisform.ShowCustomers()"  en el campo memo User, así que al hacer clic en este hiperenlace en el informe vuelve a mostrar la lista de clientes.

Ejemplo 3 Lanzar un formulario VFP.

El formulario CustomerReport.SCX  es similar al HyperlinkReports.SCX; pero es un poco más sencillo. Contiene además un control Web Browser que muestra el HTML desde un informe, EditCustomers.FRX, el que tiene el mismo aspecto que el ejemplo anterior. Sin embargo, al hacer clic en un nombre de cliente en este formulario, muestra un mantenimiento para el cliente seleccionado.

El informe EditCustomers.FRX  es un clon del informe HyperlinkCustomers utilizado en el ejemplo anterior; pero tiene en su lugar  User "*:URL = vfps://Thisform.EditCustomer('<<CustomerID>>')" en el campo memo del campo Companyname. El método EditCustomer llamado desde BeforeNavigate2 cuando se hace clic en un nombre de cliente, lanza un formulario Customers, pasándole el CustomerID para el cliente seleccionado. El formulario Customers es un mantenimiento sencillo de la tabla Customers, con controles enlazados a cada campo y botones Save (Guardar) y Cancel (Cancelar).

NavPaneListener

El MVP Fabio Vázquez ha creado otro tipo de listener que tiene hiperenlaces,  aunque para un propósito completamente diferente. Su NavPaneListener, disponible para descarga desdehttp://ReportListener.com, ofrece una vista previa de informe HTML con una tabla de contenidos para el informe. Como puede ver en la Figura 4, a la izquierda se muestra una vista en miniatura (thumbnail) de cada página y la página actual se muestra a la derecha. Al hacer clic sobre la vista en miniatura navega a la página adecuada.

Figura 4

Al igual que HyperlinkListener, NavPaneListener es bastante sencillo. Su evento OutputPage, llamado cuando va a salir cada página, genera simplemente un archivo GIF para la página al llamarse a si mismo nuevamente con los parámetros adecuados. tnDeviceType  inicialmente es -1, lo que significa que no hay salida, ya que el ListenerType del listener es igual a 2. En ese caso, OutputPage se llama a si mismo, pasando el nombre de la ruta para un GIF para generar (la propiedad cPath predetermina la ruta actual) y un tipo de dispositivo que indica un archivo GIF. En la segunda llamada, además del comportamiento normal (generar el GIF especificado, OutputPage pasa el nombre y la ruta al método AddPage de un objeto colaborador guardado en la propiedad oNavigator. (Nota: He traducido un poco el código de Fabio a inglés para hacerlo más legible.)

procedure OutputPage(tnPageNo, teDevice, ;
  tnDeviceType)
  local lnDeviceType
  with This
    do case 
      case tnDeviceType = -1 && None
        lnDeviceType = 103 && GIF
        .OutputPage(tnPageNo, .cPath + 'Page' + ;
          transform(tnPageNo) + '.gif', lnDeviceType)
      case tnDeviceType = 103
        .oNavigator.AddPage(teDevice)
    endcase
  endwith
endproc

Después de que está listo el informe, el objeto navegador crea un par de documentos HTML - un documento que define un conjunto de paneles  con la tabla de contenidos en el panel de la izquierda y los contenidos a mostrar en el derecho y otro documento que contiene la tabla de contenidos como vistas en miniatura del GIF hiperenlazado para mostrar el archivo GIF de tamaño completo. El objeto navegador automatiza luego el Explorador de Internet para mostrar el conjunto de paneles de documentos.

Conclusiones

Observe que nada en e código en ninguno de estos ejemplos es complicado, ni tampoco hay mucho que hacer. Como resultado, toma solo unos momentos implementar informes con hiperenlaces vivos, informes tipo drilldowns, informes que lanzan otros formularios VFP y otras acciones, o informes con paneles de navegación. ¡ Esto muestra verdaderamente el poder de los report listeners !

El próximo mes, veremos un tópico similar -  vista preliminar de informes que permitan realizar algunas acciones cuando se hace clic al utilizar una técnica completamente diferente que nos brindará posibilidades como son búsqueda de texto y marcadores.