27 de abril de 2021

Gráficos de barras simples a través de Grid y BackStyle_Access

Artículo original: Simple Bar Graphs via Grid and Backstyle_Access
http://www.sweetpotatosoftware.com/spsblog/2005/12/10/SimpleBarGraphsViaGridAndBackstyleAccess.aspx
Autor: Craig Boyd
Traducido por: Luis María Guayán


Me llegó un correo electrónico

Sue Cunningham me envió un correo electrónico y me contó sobre un uso muy bueno que había encontrado para la técnica BackStyle_Access que había explicado en una publicación de mi blog. Si no tienes tiempo para leer esa entrada de blog, aquí tienes la versión resumida: Al usar el método BackStyle_Access de un control contenido en la columna de un Grid, se puede hacer casi cualquier cosa que se desee, ya que ese método se activará cuando se pinte cada fila visible en el Grid. Realmente ni siquiera hay una necesidad de todos esos métodos DynamicLoQueSea que el Team FoxPro de Microsoft agregó al objeto Columna, cuando usas BackStyle_Access. También permite realizar operaciones mucho más complejas y efectos visuales dentro de las filas del Grid. De todos modos, Sue me envió un correo electrónico y me dijo que había ideado una forma relativamente simple para producir gráficos de barras. Algo que se parece mucho a las barras de degradado recientes sobre las que Calvin Hsia escribió en su blog, pero mucho más simple, sin necesidad de GDI+.

Yo decidí probarlo

Sue no me proporcionó el código para su solución, pero según la explicación que me había dado en su correo electrónico, sentí que tenía la idea básica y decidí intentarlo y ver qué podía crear. El resultado (ver la imagen a continuación) fue tan agradable como fácil. Sue también me dijo que estaba encontrando algunos resultados impredecibles al colocar el Grid dentro de un control contenedor, por lo que también produje un ejemplo (Formulario Example2 que se puede descargar al final) que también usa esta técnica para mostrar gráficos en dos páginas separadas de un marco de página. No encontré ninguno de los resultados inconsistentes (usando VFP 9.0 SP1) que comentó Sue, así que no estoy seguro de qué le está sucediendo a ella. La técnica parece funcionar perfectamente (Nota: las barras se truncarán y/o tendrán problemas de pintura si permito que el Grid muestre ambos paneles al mismo tiempo y el panel izquierdo es un Change Panel. El Change Panel es el único lugar que vi este comportamiento no deseado, aparte de eso, todo está bien).

Todo esta involucrado

Tengo una subclase Grid que contiene dos columnas. La primer columna se usa para la descripción de lo que sea el elemento que estoy graficando ... puedes pensar en ello como la etiqueta del eje, supongo. La segunda columna contiene una subclase Container bastante simple que contiene un Shape y una subclase Label. el control Shape se usa para producir las barras y el control Label se usa para mostrar el valor que fue graficado. La magia de todo esto está en el método BackStyle_Access del control Container (que está en la columna2). Aquí está el código actual que es bastante pequeño ...

LOCAL lnTotalTicks, lnValue, lnWidth
lnTotalTicks = (this.Parent.Parent.MaxValue - This.Parent.Parent.MinValue)
lnValue = EVALUATE(this.Parent.Parent.recordsource + ".value")
lnWidth = (This.Parent.Width - 35) * (lnValue/lnTotalTicks) && - 35 to leave room for the value caption
This.cshape1.Width = lnWidth
This.clabel1.Caption = TRANSFORM(lnValue)
This.cLabel1.Left = lnWidth + 5 && leave a five pixel margin
RETURN THIS.BackStyle

Eso es básicamente todo lo que hay que hacer. Las barras serán del color del Shape en el contenedor. Sin duda, podría mejorar lo que estoy mostrando aquí agregando una propiedad de color de barra para el Grid o tal vez mostrando las barras en diferentes filas y en diferentes colores basado en algunos criterios, o llamando a un método del Grid desde el método BackStyle_Access del contenedor para que el código anterior no esté tan oculto. Solo estoy mostrando lo básico aquí, esto ciertamente no es un producto terminado con todo el cotillón posible. Además, a medida que observa los ejemplos, puede notar que hay propiedades MinValue y MaxValue que se han agregado a la subclase del Grid. Estas son para que no tengamos que graficar siempre en 0-100 o lo que sea (ver el código arriba). Los valores graficados pueden ser cualquier cosa, como 1000-10000, 250-350, etc.

Finalmente, para ver los cursores con los que se ejecutan los gráficos de barra del Grid, mire en los métodos Load de los formularios de ejemplo donde encontrará algo como el siguiente código ...

LOCAL lnCounter
CREATE CURSOR crsValues (Descript c(25), Value I)
=RAND(-1)
FOR lnCounter = 1 TO 100
 INSERT INTO crsValues (Descript, Value) VALUES ("Item " + TRANSFORM(lnCounter), INT(100 * RAND( ) + 1))
ENDFOR
GO TOP IN crsValues

Como puede ver, el cursor que se necesita para esto es extremadamente simple, de hecho solo dos campos, uno para la descripción y el otro para los valores. En cualquier caso, esto solo muestra una vez más el valor de la clase Grid de Visual FoxPro cuando se acopla con el método BackStyle_Access. Con muy pocas líneas de código podemos producir una clase bastante útil para mostrar datos visualmente a nuestros usuarios. Gracias por la sugerencia enviada por correo electrónico Sue!

Código fuente y captura de pantalla

Cuando descargue el archivo zip, extraiga el contenido. Abra el proyecto en Visual FoxPro y ejecute los formularios Example y Example2.

Descargue el código fuente de Grid Graph con los ejemplos desde el siguiente enlace: gridgraph.zip (23 KB aprox.)


17 de abril de 2021

Breve Reseña Sobre Sesiones de Datos Públicas Y Privadas

Si bién antiguamente usábamos cualquier tabla desde cualquier programa (lo que luego se denominó "Sesión de datos Pública") ahora es más frecuente, aunque no imprescindible, usar "Sesión de datos Privada", que permite abrir otra copia las mismas tablas como si se tratara de otra sesión de VFP (como cuando abrís VFP 2 veces o más).

Qué tipo de Sesión de Datos quieras usar dependerá de cómo pretendas encarar tu proyecto. Voy a intentar hacer un breve resúmen de ventajas y desventajas de cada caso.

1) Ventajas de la Sesión de datos Pública

  • Las tablas son visibles desde todos los módulos del sistema (PRG, SCX, VCX, MNX, FRX, etc.)
  • El hecho de que se las tablas se abran una sola vez implica un cierto ahorro de memoria RAM, ya que por cada tabla abierta se guardan en memoria datos como: Indice actual, tipo de ordenamiento del índice, puntero de registro, etc.
  • La sesión, al ser Pública, implica usar sólo una copia en memoria RAM de los seteos (comandos SET ON/OFF) que son afectados por las sesiones de datos (en la ayuda del comando SET dice si es afectado o no por la sesión de datos)
  • Es compatible con el modo de funcionamiento de FoxPro 2.X, lo que permitiría mantener código heredado de esa versión

2) Desventajas de la Sesión de datos Pública:

  • En cada programa tenés que hacer tu propio manejo de los punteros de las tablas para que cuando abrís un programa nuevo o cambiás a otro, cada uno restaure el puntero del registro al lugar correspondiente.
  • Si desde un procedimiento llamás a otro procedimiento que cambia el puntero de una tabla o el orden de un índice que estabas usando y no lo restaurás, es muy probable que le ocaciones problemas al procedimiento original, pudiéndose llegar a cancelar el programa
  • No es recomendable para objetos COM
  • Los comandos SET ON/OFF afectan a todos los módulos por igual
  • Si la misma tabla se abre varias veces, se debe hacer con alias distintos (USE MiTabla AGAIN SHARED ALIAS ...)
  • Se dificulta hacer código genérico para manejar tablas, ya que hay que agregar manejo de punteros, índices, etc.
  • El bloqueo / desbloqueo de registros (LOCK / RLOCK / FLOCK) es más complejo de manejar, ya que si un módulo bloquea uno o más registros, otro módulo puede desbloquear esos mismos registros pudiendo provocar un problema de seguridad y/o coherencia de datos entre módulos

3) Ventajas de la Sesión de datos Privada (Objeto Session / Form / Reporte):

  • Varios comandos SET ON/OFF son dependientes de la sesión en la que se los usa (Ej: SET DELETED),lo que permite tener distintos valores entre sesiones de datos privadas (cada sesión puede tener un valor distinto para el mismo comando, sin afectar a las demás)
  • Una misma tabla se puede abrir con el mismo alias en distintas sesiones privadas
  • Si la misma tabla se abre en distintas sesiones, cada sesión mantiene automáticamente el puntero de registro de las tablas y el orden de los índices
  • Es más fácil hacer código genérico para manejar tablas, ya que el mantenimiento de punteros de registro e índices es automático
  • Es recomendable para hacer objetos COM
  • El bloqueo / desbloqueo de registros (LOCK / RLOCK / FLOCK) es más fácil de manejar, ya que si una sesión bloquea uno o más registros de una tabla, sólo esa sesión podrá desbloquear esos mismos registros, lo que beneficia a la seguridad y/o coherencia de datos entre módulos
  • Una sesión de datos Privada se comporta comosi fuera una nueva Sesión de VFP (como abrir 2 ó mas veces VFP)

4) Desventajas de la Sesión de datos Privada (Objeto Session / Form / Reporte):

  • El mantenimiento independiente de los punteros de registro, índices, valores de los comandos SET ON/OFF, etc. por cada sesión implica un mayor uso de memoria RAM por cada una de las tablas abiertas y de los comandos SET ON/OFF. Ej: Si se usa una tabla con un indice en 3 sesiones privadas distintas, en realidad es como si se estuvieran usando 6 archivos distintos.
  • El código heredado de versiones de FoxPro 2.X no funcionará del modo esperado, ya que no estaba preparado para este esquema de trabajo por Sesiones

Seguramente hay más cosas para agregar en cada caso, pero esto es para dar una idea.

Saludos

Fernando D. Bozzo

1 de abril de 2021

Un cuadro de diálogo genérico "Acerca de"

Artículo original: A Generic About Dialog
https://doughennig.com/Papers/Pub/201611adhen.pdf
Autor: Doug Hennig
Traductor: Luis María Guayán


Hace un tiempo, Doug discutió un par de diálogos dinámicos creados con la ayuda de Dynamic Form. En este artículo, presenta otro diálogo dinámico y genérico, esta vez para mostrar información de la aplicación.

La mayoría de las aplicaciones tienen una función "Acerca de" en el menú "Ayuda" que muestra información sobre la aplicación, como el nombre de la empresa, el número de versión, la carpeta actual, etc.

Como mencioné anteriormente, después de haber creado varios cuadros de diálogo "Acerca de" a lo largo de los años, decidí crear uno genérico que muestre dinámicamente la configuración que desee. Si bien este cuadro de diálogo se basa en datos como los ejemplos anteriores, no utiliza el proyecto Dynamic Form de VFPx para la representación de control. En su lugar, utiliza un control ListView ActiveX.

La Figura 1 muestra cómo se ve. Todo lo que ve en el cuadro de diálogo, excepto el enlace "Copiar configuración" y el botón "Aceptar", se personaliza pasando parámetros o agregando registros a una tabla. Este diálogo tiene las siguientes características:

  • ListView enumera tantos elementos como desee en pares de descripción/valor. El contenido de la lista proviene de una tabla llamada About.dbf la cual veremos luego.
  • Algunos de los elementos de la lista aparecen en texto azul que indica que tienen hipervínculos: al hacer clic en ellos, se realiza alguna acción. Por ejemplo, al hacer clic en el elemento "Carpeta del programa", aparece una ventana del Explorador de archivos que muestra el contenido de la carpeta del programa.
  • El botón "Copiar configuración" copia los elementos del ListView al Portapapeles de Windows para que se puedan pegar en algún otro lugar.
  • El título del formulario, la imagen, el nombre de la aplicación y el número de versión no están codificados, sino que se pasan como parámetros al formulario.
  • Cambiar el tamaño del cuadro de diálogo cambia automáticamente el tamaño del ListView y su segunda columna. Puede ajustar manualmente el tamaño de las columnas como lo haría normalmente con un control ListView.


Figura 1. Este cuadro de diálogo "Acerca de" es completamente genérico.

El método Init acepta tres parámetros: el nombre de la aplicación, el número de versión (como una cadena) y el nombre de una imagen para usar como la aplicación o el logo de la empresa. Utiliza estos parámetros para el título del formulario y las etiquetas para el nombre de la aplicación y el número de versión y para la imagen del logo. Luego llama al método LoadList para cargar la configuración. Aquí está el código del Init:

LPARAMETERS tcAppName, ;
  tcVersion, ;
  tcLogoImage
DODEFAULT()
WITH THIS
  .CAPTION = 'About ' + tcAppName
  .lblProduct.CAPTION = tcAppName
  .lblVersion.CAPTION = 'Version ' + ;
    TRANSFORM(tcVersion)
  .imgLogo.PICTURE = tcLogoImage
  .LoadList()
ENDWITH

LoadList comienza abriendo la tabla About.dbf. Esta tabla contiene los elementos que se mostrarán en el cuadro de diálogo "Acerca de". La Tabla 1 muestra la estructura de About.dbf y la Figura 2 muestra los registros en la tabla de ejemplo que acompaña a este artículo.

NombreTipoPropósito
OrderIEl orden del artículo en el diálogo
CaptionC (40)La descripción del artículo
ValueMEl valor como expresión a evaluar
HyperlinkL.T. si este elemento debe aparecer como un hipervínculo
LinkMUna expresión para evaluar la acción de hipervínculo; déjelo en blanco para usar el dato de Value

Tabla 1. La estructura de About.dbf.


Figura 2. El ejemplo de la tabla About.dbf.

LoadList revisa los registros de la tabla About.dbf para determinar el ancho de la descripción más larga y aumentar ese ancho en 10 píxeles para tener en cuenta el espacio en el ListView. Luego, establece algunas propiedades del control ListView y crea dos columnas: una para la descripción de cada elemento y otra para el valor. Dado que el valor suele ser más ancho que la descripción, la columna de descripción solo ocupa el ancho calculado anteriormente y el valor ocupa el resto del ancho del ListView. Finalmente, LoadList revisa los registros en la tabla About.dbf nuevamente, esta vez agregando cada elemento al ListView llamando al método AddSettingToList, que veremos mas adelante. Aquí está el código de LoadList:

LOCAL lnSelect, ;
  lnWidth, ;
  lnCurrWidth, ;
  luValue
lnSelect = SELECT()
SELECT 0
USE ABOUT ORDER ORDER AGAIN SHARED
lnWidth = 0
SCAN
  lnCurrWidth = TXTWIDTH(TRIM(CAPTION), ;
    THIS.FONTNAME, 8)
  lnCurrWidth = lnCurrWidth * ;
    FONTMETRIC(6, THIS.FONTNAME, 8)
  lnWidth = MAX(CEILING(lnCurrWidth), ;
    lnWidth)
ENDSCAN
lnWidth = lnWidth + 10
* Set some properties of the ListView.
WITH THIS.oList
  .VIEW = 3
  .LabelEdit = 1
  .GRIDLINES = .T.
  .OBJECT.FONT.NAME = THIS.FONTNAME
  .OBJECT.FONT.SIZE = 8
  .FullRowSelect = .T.
  * Create some columns for the ListView.
  .COLUMNHEADERS.ADD(, 'Description', ;
    'Description', lnWidth)
  .COLUMNHEADERS.ADD(, 'Value', ;
    'Value', .WIDTH - lnWidth)
ENDWITH
* Add the settings we want displayed.
SCAN
  luValue = TRANSFORM(EVALUATE(VALUE))
  THIS.AddSettingToList(TRIM(CAPTION), ;
    luValue, HYPERLINK, ;
    IIF(EMPTY(LINK), '', EVALUATE(LINK)))
ENDSCAN
USE
SELECT (lnSelect)

AddSettingToList acepta cuatro parámetros: la descripción del item, el valor del item, .T. si tiene un hipervínculo y el valor que se utilizará para el hipervínculo. Comprueba si el elemento ya está en el array aItems del formulario y, si no, agrega un nuevo elemento al ListView y al array. Establece el valor del elemento ListView y, si se supone que el elemento tiene un hipervínculo, cambia el color a azul para que parezca un hipervínculo y almacena la expresión del hipervínculo o el valor en la propiedad Tag del elemento (si la expresión está en blanco, se utiliza el campo Value). Aquí está el código de AddSettingToList:

LPARAMETERS tcDescription, ;
  tcValue, ;
  tlHyperlink, ;
  tcLink
LOCAL lnRow, ;
  loItem
WITH THIS
  IF VARTYPE(tcDescription) = 'C' AND ;
      NOT EMPTY(tcDescription) AND ;
      LEN(tcDescription) <= 100 AND ;
      NOT EMPTY(tcValue) AND ;
      LEN(tcValue) <= 255
    lnRow = ASCAN(.aItems, tcDescription, ;
      -1, -1, 1, 13)
    IF lnRow > 0
      loItem = .oList.ListItems(lnRow)
    ELSE
      loItem = .oList.ListItems.ADD(, ;
        SYS(2015), tcDescription)
      lnRow = IIF(EMPTY(.aItems[1]), ;
        1, ALEN(.aItems) + 1)
      DIMENSION .aItems[lnRow]
      .aItems[lnRow] = tcDescription
    ENDIF lnRow > 0
    loItem.SubItems(1) = tcValue
    IF tlHyperlink
      loItem.FORECOLOR = RGB(0, 0, 255)
      loItem.TAG = IIF(EMPTY(tcLink), ;
        tcValue, tcLink)
    ENDIF tlHyperlink
  ENDIF VARTYPE(tcDescription) = 'C' ...
ENDWITH
RETURN

Cuando el usuario hace clic en un elemento de la lista, se llama al método ItemClick del ListView. Este método comprueba si el elemento tiene algo en la propiedad Tag y, de ser así, utiliza la función ShellExecute de la API de Windows para abrir la aplicación predeterminada para el elemento (ExecuteFile.prg es un contenedor para ShellExecute). Entonces, si el valor del elemento es una ruta, el Explorador de archivos se abre con esa ruta. Si es una URL, el navegador predeterminado del usuario se abre con esa URL. En el caso de una dirección de correo electrónico, use algo como "'mailto:' + oApp.cSupportEmail" en el campo Link de la tabla About.dbf si oApp.cSupportEmail contiene la dirección de correo electrónico para usar.

LPARAMETERS toItem
IF NOT EMPTY(toItem.TAG)
  ExecuteFile(toItem.TAG)
ENDIF NOT EMPTY(toItem.TAG)

Cuando se cambia el tamaño del formulario, queremos que la columna Value del ListView cambie de tamaño también. El siguiente código en el evento Resize del formulario se encarga de eso:

LOCAL lnWidth
WITH THIS.oList
  lnWidth = .WIDTH - ;
    .COLUMNHEADERS.ITEM(1).WIDTH
  .COLUMNHEADERS.ITEM(2).WIDTH = ;
    MAX(lnWidth, 100)
ENDWITH

Main.prg es un programa de ejemplo que muestra cómo utilizar SFAbout. Crea un objeto de aplicación ficticio solo con fines de demostración. SFAbout no usa oApp, pero los registros de la tabla de ejemplo About.dbf sí lo hacen; por ejemplo, la expresión para el valor del elemento Nombre de Usuario es oApp.cUserName. Main.prg luego crea una instancia de SFAbout, pasándole "Mi aplicación de ejemplo" como el nombre de la aplicación, "1.0" como el número de versión y "KokoWhite.jpg" como la imagen a utilizar.

oApp = CREATEOBJECT('Empty')
ADDPROPERTY(oApp, 'cUserName', 'DHENNIG')
ADDPROPERTY(oApp, 'cWebSite', ;
  'http://www.stonefieldquery.com')
ADDPROPERTY(oApp, 'cSupportEmail', ;
  'websupport@stonefieldquery.com')
ADDPROPERTY(oApp, 'nLicenseCount', 5)
* Display the About dialog.
loForm = NEWOBJECT('SFAbout', 'SFAbout.vcx', ;
  '', 'My Sample Application', '1.0', ;
  'KokoWhite.jpg')
loForm.SHOW()

Resumen

Hay numerosos cuadros de diálogo que la mayoría de las aplicaciones tienen en común, incluido un cuadro de diálogo Opciones discutidos anteriormente y el cuadro de diálogo "Acerca de" discutido ahora. La creación de versiones genéricas y dinámicas de estos cuadros de diálogo significa que nunca tendrá que volver a crearlos.

Descarga de los ejemplos

https://doughennig.com/Papers/Pub/201611adhensc.zip