18 de marzo de 2021

Enlazar eventos para obtener mejores aplicaciones - Parte 3 de 4

Artículo original: Bind Events for Better Applications
(Bind Events for Better Applications 2017.pdf)
Autor: Tamar E. Granor
Traducido por: Luis María Guayán


... Continuación de: "Enlazar eventos para obtener mejores aplicaciones - Parte 2 de 4"

Actualización cuando se cierra un formulario

A veces, en una aplicación, es necesario actualizar los datos de un formulario cuando se cierra otro.

Cuando el formulario que se está cerrando es modal y se llamó desde el otro formulario, esto es fácil porque todavía estás en el método que llamó al otro formulario en primer lugar. Pero cuando ambas formas no tienen modalidad, necesita una forma de conectarlas; BindEvent() ofrece una forma.

Para simplificar, asumiremos que el formulario que debe actualizarse se denomina formulario que se está cerrando. Todo lo que necesita en ese caso es vincular el evento Destroy del formulario llamado a un método del formulario llamado, como en el Listado 31.

Listado 31. El código ejecuta un formulario y vincula su método Destroy al método Refresh del formulario de llamada.

LPARAMETERS cChildForm
LOCAL oChild
TRY
 DO FORM (m.cChildForm) NAME m.oChild
 IF NOT ISNULL(m.oChild)
 BINDEVENT(m.oChild, "Destroy", This, "Refresh")
 ENDIF
CATCH
 * Nothing to do here, maybe tell user.
ENDTRY
RETURN

En la aplicación Biblioteca, el menú contextual del formulario de salida le permite abrir el formulario Miembros, mirando al miembro actual. Si el usuario cambia algunos de los datos del Miembro, queremos actualizar el formulario de pago. Debido a que actualizar esos datos es un poco más complicado que simplemente llamar al Refresh del formulario, vinculamos el formulario Members Destroy a un método personalizado del formulario de check-out, RefreshMember, que se muestra en el Listado 32. (El método GetMember recupera los datos del miembro y pone en un cursor.)

Listado 32. Este método personalizado del formulario de salida actualiza la visualización del miembro actual. Se llama cuando se cierra el formulario de prestatarios.

* Update the display for the current member
This.GetMember(This.cCurrentMemberNum)
RETURN

El elemento del menú de acceso directo "Mostrar el registro de este miembro" llama a otro método personalizado llamado ShowBorrower, para configurar las cosas; ese método se muestra en el Listado 33. Pasamos 1 para el parámetro flags de BindEvent aquí para asegurarnos de que terminamos de cerrar el formulario (y por lo tanto, los datos se guardan) antes de actualizar.

Listado 33. Este método del formulario de pago, llamado ShowBorrower, se llama cuando el usuario solicita ver los datos del prestatario actual.

LPARAMETERS cBorrowerNum
LOCAL oBorrowerForm
DO FORM Borrowers WITH m.cBorrowerNum NAME oBorrowerForm
IF VARTYPE(m.oBorrowerForm) = "O" AND NOT ISNULL(m.oBorrowerForm)
 * Make sure we refresh this form when the borrower form closes
 BINDEVENT(oBorrowerForm, "Destroy", This, "RefreshMember", 1)
ENDIF

Por supuesto, en este caso, es posible que deseemos realizar la actualización no solo cuando se cierre el formulario de prestatario, sino también cuando guardemos los datos en ese formulario. Podemos vincularnos al método Save del formulario del prestatario para obtener ese comportamiento.

Actualización de colores cuando cambia el tema de color

Soy una gran creyente en mantener el esquema de color elegido por el usuario para la mayoría de las aplicaciones. Por lo tanto, rara vez establezco colores para los controles o formularios de VFP. Sin embargo, hay ocasiones en las que quiero resaltar alguna característica. En lugar de elegir un color que me guste, prefiero elegir un color del esquema de colores seleccionado por el usuario. La función GetSysColor de la API de Windows extrae los colores del usuario del registro para que pueda usarlos. (Consulte la nota al final de esta sección sobre la ventana 10 y los colores).

Si el usuario cambia la combinación de colores mientras se ejecuta una aplicación, quiero que mi aplicación siga su ejemplo. Para hacer esto, me vinculo al mensaje de Windows HandleThemeChanged.

Envolví toda esta funcionalidad en una clase llamada GetUserColors, basada en la clase personalizada. La clase incluye métodos para recuperar los colores del esquema actual del usuario y para devolver un color particular de ese esquema. (Hay un conjunto de nombres para los distintos colores que coinciden aproximadamente con lo que ve en el cuadro de diálogo Apariencia avanzada del cuadro de diálogo Propiedades de pantalla). No revisaré todo el código de este documento; es bastante sencillo.

El método BindColorChanges, llamado desde el método Init, configura el enlace; se muestra en el Listado 34. Las dos declaraciones de función API y la llamada que las sigue almacenan información sobre el procedimiento de Windows a llamar cuando se maneja el evento.

Listado 34. Este método se une a dos mensajes de Windows, por lo que la aplicación responde cuando el usuario cambia de color.

PROCEDURE BindColorChanges
* Bind the info here to changes in the user's theme/scheme
* Prepare for binding
DECLARE integer CallWindowProc IN WIN32API ;
 integer lpPrevWndFunc, ;
 integer hWnd,integer Msg,;
 integer wParam,;
 integer lParam
DECLARE integer GetWindowLong IN WIN32API ;
 integer hWnd, ;
 integer nIndex
THIS.nOldProc=GetWindowLong(_SCREEN.HWnd, -4) && GWL_WNDPROC
BINDEVENT(_VFP.hWnd, 0x031A, THIS, "HandleThemeChange") && WM_THEMECHANGED
BINDEVENT(_VFP.hWnd, 0x0015, THIS, "HandleThemeChange") && WM_SYSCOLORCHANGE
RETURN

El método HandleThemeChange, que se muestra en el Listado 35, se llama cuando el usuario cambia el tema o un color individual. Primero, asegura que se ejecute el código apropiado de Windows; esto es el equivalente a emitir DODEFAULT() en un método de una subclase VFP.

Luego, simplemente vuelve a leer los colores del usuario en el objeto, de modo que siempre mantenga los colores actuales.

Listado 35. Este método es el delegado para dos eventos de Windows que ocurren cuando el usuario cambia los colores de Windows. Vuelve a leer los componentes del esquema de color actual.

PROCEDURE HandleThemeChange
* Respond to user's change of theme/scheme
LPARAMETERS hWnd as Integer, Msg as Integer, wParam as Integer, lParam as Integer
LOCAL lResult
lResult=0
* Note: for WM_THEMECHANGED, MSDN indicates the wParam and lParam
* are reserved so can't use them.
lResult=CallWindowProc(this.nOldProc,hWnd,msg,wParam,lParam)
This.ReadUserColors()
RETURN lResult

Para aprovechar esto, la clase base Formulario crea una instancia del objeto GetUserColors en Init y luego carga los colores necesarios en el formulario. (También puede crear una instancia de GetUserColors solo una vez en el código de inicio para el objeto de aplicación y almacenar la referencia allí). Luego, vincula el método GetUserColors.ReadUserColors al método GetUserColors del formulario. El Listado 36 muestra la parte de frmBase.Init que configura las cosas.

Listado 36. La clase base Formulario configura las cosas para que los colores del usuario estén disponibles para el formulario y se actualicen cuando el usuario cambia los colores de Windows.

* Load color object
This.oColors = NEWOBJECT("GetUserColors", "GetUserColors.PRG")
This.GetUserColors()
* Bind to color changes
BINDEVENT(This.oColors, "ReadUserColors", This, "GetUserColors", 1)

En la actualidad, solo estoy usando tres de los colores de combinación de colores directamente, por lo que el método GetUserColors de la clase base Formulario (que se muestra en el Listado 37) es bastante simple.

Listado 37. El método de formulario GetUserColors se activa cada vez que se vuelven a leer los colores del Registro.

* Load colors from user's current theme/scheme
This.nDisabledForeColor = This.oColors.GetAColor("APPWORKSPACE")
This.nDisabledBackColor = This.oColors.GetAColor("WINDOW")
This.nHighlightColor = This.oColors.GetAColor("HIGHLIGHT")

La pieza final de este esquema es una forma de actualizar los colores reales utilizados por varios controles cuando el usuario cambia de color. Eso es manejado por subclases de las clases de control base. Por ejemplo, la clase lblHighlight se usa para mostrar una etiqueta coloreada con el color de resaltado especificado por el usuario, en lugar del color de texto predeterminado. Tiene un método personalizado, GetHighlightColor, que se muestra en el Listado 38 y se llama desde la etiqueta Init. El método establece el ForeColor de la etiqueta en el nHighlightColor almacenado del formulario. El método Init también vincula los cambios de esa propiedad al mismo método. Es decir, siempre que se cambia el nHighlightColor del formulario, GetHighlightColor se activa y actualiza el ForeColor de la etiqueta.

Listado 38. El Init de la clase lblHighlight llama a este código para establecer el color del texto de la etiqueta y asegurarse de que se actualice cada vez que el usuario cambia los colores del sistema.

IF PEMSTATUS(ThisForm, "nHighlightColor", 5)
 This.ForeColor = ThisForm.nHighlightColor
ENDIF

Las clases de la biblioteca también incluyen edtEnhancedDisabled y txtEnhancedDisabled que funcionan de manera similar.

Una advertencia aquí. En mis pruebas, a veces, Windows no había actualizado completamente los colores cuando se ejecutó ReadUserColors. Es decir, los colores devueltos no siempre fueron los nuevos colores. En general, obtuve mejores resultados si esperé más tiempo entre elegir un nuevo esquema de color o tema y hacer clic en el botón Aplicar en el cuadro de diálogo Propiedades de pantalla.

Windows 10 parece reducir significativamente el color del usuario sobre los colores. Si bien el usuario puede establecer un solo color de acento, la única forma de cambiar todos los colores que solían estar disponibles es eligiendo un nuevo tema. Agregue a eso que los usuarios no pueden cambiar los colores dentro de los temas a menos que elijan un tema de alto contraste. Entonces, aunque este código funciona, es mucho menos inútil en Windows 10.

Métodos Access y Assign

A estas alturas, puede ver que la vinculación de eventos ofrece muchas oportunidades para hacer que las aplicaciones respondan mejor. Una técnica anterior proporciona algunas formas adicionales de hacer que sus aplicaciones se comporten como esperan los usuarios. Los métodos Access y Assign se introdujeron en Visual FoxPro 6 y esencialmente le permiten definir sus propios eventos en una aplicación.

Cualquier propiedad puede tener un método Access, un método Assign o ambos. El método Access se activa siempre que se hace referencia a la propiedad, mientras que el método Assign se activa siempre que cambia la propiedad. Cada uno de ellos le permite cambiar el valor de la propiedad, así como realizar otras acciones.

El método Assign recibe el nuevo valor (el que está asignando) como parámetro. En el código, puede asignar cualquier valor que desee a la propiedad, no solo el que recibe como parámetro. Entonces, entre otras cosas, puede usar el método Assign para hacer que una propiedad sea de solo lectura o de solo lectura en algunas circunstancias. Sin embargo, la mayoría de las veces utilizo los métodos Assign para asegurar que sucedan ciertas cosas cuando cambia el valor de la propiedad. En otras palabras, un método Assign se convierte en un evento sobre el que puedo actuar.

En el método Access, puede controlar qué valor ve el código de activación como el valor de la propiedad. El código de activación utiliza el valor que devuelve del método Access, incluso si el valor real almacenado en la propiedad es diferente. Por ejemplo, puede traducir el valor a un idioma diferente en función de una aplicación o configuración del sistema, o convertirlo de un mecanismo de almacenamiento interno conveniente a un formato de usuario más amigable (digamos, numérico a carácter). La mayoría de las veces, utilizo métodos Access para asegurarme de que una propiedad esté actualizada según la configuración actual. Es decir, un método Access me permite evitar tener que asegurarme de que una propiedad se actualice cada vez que cambian las cosas de las que depende. Hago la actualización cuando realmente necesito el valor.

Agregar métodos Access y Assign a una propiedad es algo diferente a agregar métodos personalizados a un objeto. En la Hoja de propiedades, haga clic con el botón derecho en la propiedad en cuestión y elija Editar propiedad / método, que se muestra en la Figura 5.

Figura 5. Utilice el cuadro de diálogo Editar propiedad / método para agregar métodos Access y Assign.

En el cuadro de diálogo Editar propiedad / método, hay casillas de verificación para Método Access y Método Assign; marque uno o ambos. El cuadro de diálogo Agregar propiedad actualizado que viene con Visual FoxPro 9 (y tiene una versión mejorada en VFPX), así como el cuadro de diálogo Editar propiedad / método mejorado (que se muestra en la Figura 6) y el Editor PEM de Thor facilitan la adición de métodos Access y Assign. al agregar una nueva propiedad o editarla.

Figura 6. El cuadro de diálogo Editar propiedad / método mejorado, disponible en VFPX, facilita la adición de métodos Access y Assign.

Los métodos Access contienen automáticamente una sola línea de código que devuelve el valor de la propiedad. Los métodos Assign reciben automáticamente el nuevo valor como parámetro y contienen una sola línea que asigna ese valor a la propiedad. Una vez que existen, puede editar el código como desee.

El uso del método Access

Como se indicó anteriormente, el uso principal que encuentro para los métodos de Access es actualizar una propiedad cuando es necesario. Sin embargo, también ofrecen una solución para un error de VFP.

Actualización justo a tiempo

Cuando el valor de una propiedad se basa en otros elementos que pueden cambiar a medida que se ejecuta la aplicación, un método de Access nos permite buscar o calcular el valor cuando sea necesario. Hacerlo tiene varios propósitos. Primero, no tenemos que insertar llamadas (o usar métodos Assign) para actualizar la propiedad cada vez que cambia uno de sus componentes. En segundo lugar, es una especie de encapsulación. Solo la propiedad tiene que saber encontrar su propio valor. Si las reglas para generar el valor cambian, lo único que tenemos que cambiar es el método Access (o un método personalizado al que llama).

Por ejemplo, en la Figura 3 (mucho antes en este documento), el bloque naranja que dice "Estado menor" indica el estado general del nodo mostrado (un nodo aquí es una subestación de servicios públicos); la determinación del estado de un nodo implica verificar una serie de condiciones.

La aplicación monitorea el hardware correspondiente y cuando hay cambios, actualiza el formulario. El estado del nodo se almacena en una propiedad llamada cStatus; El método Access de cStatus llama a otro método que calcula el estado actual y devuelve el valor calculado. Luego, el formulario muestra ese valor y colorea el fondo a su alrededor de manera apropiada (rojo para "Alarma mayor", naranja para "Alarma menor", verde para "Normal").

En la misma aplicación, varios formularios indican la última vez que se leyeron datos completos del hardware; La figura 7 muestra un ejemplo. Hay muchos datos para leer, por lo que algunos datos solo se leen por solicitud del usuario o cuando el formulario que muestra el elemento de datos está abierto.

En lugar de actualizar la marca de tiempo a nivel de formulario cada vez que se lee un elemento, un método de Access recorre todos los elementos relevantes y actualiza la marca de tiempo a nivel de formulario cuando el formulario se muestra por primera vez y cada vez que se actualiza.

Figura 7. En este formulario, el último valor leído en la parte inferior indica la marca de tiempo más antigua para cualquiera de las configuraciones que se muestran en el formulario. En lugar de actualizar cada vez que cambia una configuración, un método de Access encuentra el valor correcto cuando es necesario.

La clase base Formulario de manejo de datos en la aplicación de biblioteca, frmBizObjAware, usa esta estrategia para determinar el primer control en el formulario en orden de tabulación. La clase tiene una propiedad personalizada, cFirstControl; si el desarrollador de un formulario en particular establece la propiedad en tiempo de diseño, el control especificado se trata como el primero en el orden de tabulación. Sin embargo, eso es algo fácil de olvidar para un desarrollador, por lo que un método Access para la propiedad recorre los controles en el formulario y establece la propiedad según el TabOrder de los distintos controles. El Listado 39 muestra el código en cFirstControl_Access.

Listado 39. Este código garantiza que se especifique un primer control en el formulario, incluso si el usuario se olvida de indicar qué control viene primero.

* Handle the possibility that no first control has been set
LOCAL oControl, oLowControl, nLowTab
IF EMPTY(THIS.cFirstControl)
 * Figure it out
 nLowTab = 1000000
 FOR EACH oControl IN THISFORM.OBJECTS
 IF PEMSTATUS(oControl, "TabIndex", 5) AND PEMSTATUS(oControl, "SetFocus", 5)
 IF oControl.TABINDEX < nLowTab
 oLowControl = oControl
 nLowTab = oControl.TABINDEX
 ENDIF
 ENDIF
 ENDFOR
 THIS.cFirstControl = oLowControl.NAME
ENDIF
RETURN THIS.cFirstControl

Este código se utiliza, por ejemplo, en el botón Nuevo. Después de agregar un nuevo registro y habilitar y deshabilitar adecuadamente los controles, el foco se establece en el primer control del formulario.

El Listado 40 muestra el código en el método New de la clase base Formulario:

Listado 40. Este código en frmBizObjAware.New establece el foco en el primer control designado del formulario. El método Access en el Listado 39 se llama implícitamente para asegurarse de que cFirstControl tenga un valor no vacío.

* Add a record in this form
LOCAL lReturn
 IF MethodExists(This.oBizObj, "New")
  lReturn = This.BeforeNew()
  IF lReturn
   lReturn = This.oBizObj.New()
   IF lReturn
    This.lNewRecord = .T.
    This.lDataChanged = .F.
    lReturn = This.AfterNew()
    oFirstControl = EVALUATE("This." + This.cFirstControl)
    oFirstControl.SetFocus()
   ENDIF
  ENDIF
 ENDIF
RETURN lReturn

ToolTips dinámicos

En el formulario que se muestra en la Figura 3, necesitamos mostrar ToolTips para cada uno de los cuadros numerados, que representan puertos; el contenido de los ToolTips está determinado por el estado actual del puerto. Un método Access proporciona una manera fácil de hacer lo que se necesita; simplemente cree y devuelva la cadena apropiada en el método ToolTipText_Access.

En el formulario Catálogo de la aplicación Biblioteca, sería útil poder mostrar todos los detalles sobre el estado actual de un libro en un ToolTip en la página Copiar. (De hecho, es posible que queramos poner la información en esa página, pero con el propósito de demostrar esta técnica, usaremos un ToolTip). La información en esa página se muestra en un contenedor cuya clase es cntCopyInformation; El Listado 41 muestra el método ToolTipText_Access para ese contenedor.

Listado 41. El código del método ToolTipText_Access llama a un método de formulario para construir un ToolTip.

* Check whether the book is out and build a tooltip with that info
RETURN ThisForm.GetBookDetail(ThisForm.cBarCode)

El método GetBookDetail del formulario busca el libro que se muestra actualmente y crea la cadena adecuada para mostrar. La figura 8 muestra un ejemplo.

Figura 8. El ToolTip para el contenedor en la página Copiar se crea sobre la marcha utilizando el método Access de ToolTipText.

Delegación de ToolTips para objetos contenidos

El ejemplo de la Figura 8 también demuestra otro uso de los métodos de Access. En un contenedor como el que se muestra, es posible que deseemos el mismo ToolTip para todos los controles. Una forma fácil de hacerlo es hacer que el método Access para el ToolTipText de un control devuelva el ToolTipText del padre, según las líneas del Listado 42.

Listado 42. Para darle a un contenedor y su contenido el mismo ToolTip, coloque un código como éste en los controles contenidos.

ToolTipText_Access method.
RETURN This.Parent.ToolTipText

Un pequeño código en las clases base nos permite configurar esto en todos los ámbitos, por lo que podemos activarlo para un contenedor determinado estableciendo una sola propiedad. Primero, cntBase (la clase base Contenedor) tiene una propiedad personalizada, lBindToolTip. (Usar la palabra "bind" aquí es un poco engañoso ya que implica BindEvents, pero de hecho, realmente estamos vinculando el ToolTipText del control contenedor al padre.) Luego, el método ToolTipText_Access para cada clase de control que tiene un método ToolTipText contiene el en el Listado 43. Luego, todo lo que tiene que hacer para asegurarse de que todo en un contenedor muestre el mismo ToolTip es establecer la propiedad lBindToolTip del contenedor en .T.

Listado 43. Ponga este código en el método ToolTipText_Access de cada clase base de control.

* Check whether we're supposed to be passing tooltips up
LOCAL cTip
IF PEMSTATUS(This.Parent, "lBindToolTip", 5) AND This.Parent.lBindToolTip
 cTip = This.Parent.ToolTipText
ELSE
 cTip = This.ToolTipText
ENDIF
RETURN m.cTip

ToolTips de componentes de grillas

Un enfoque similar le permite solucionar un error en Visual FoPro 9 SP2; los objetos contenidos en una grilla no muestran su propio ToolTip, sino el ToolTip de la grilla. Un método Access para la propiedad ToolTipText de la grilla le permite profundizar en los objetos contenidos y usar su ToolTipText en su lugar.

En la aplicación de biblioteca, la clase Grilla de nivel superior, grdBase, tiene el código del Listado 44 en su método ToolTipText_Access. En realidad, este código combina la técnica de la sección anterior para propagar ToolTips desde los contenedores con la capacidad de dar a los controles dentro de una grilla sus propios consejos. Primero verifica si el padre de la grilla tiene la propiedad lBindToolTip establecida en .T. Si es así, usa la propina de los padres; de lo contrario, perfora la grilla y permite que los controles internos brinden sugerencias.

Listado 44. Utilice ToolTipText_Access para solucionar el error de VFP 9 SP2 con respecto a los ToolTips en las grillas.

* Check whether we're supposed to be passing tooltips up
LOCAL cTip
IF PEMSTATUS(This.Parent, "lBindToolTip", 5) AND This.Parent.lBindToolTip
 cTip = This.Parent.ToolTipText
ELSE
 * Let components have their own tooltips.
 * Look up the tooltip for the object currently under the mouse.
 LOCAL aMousePos[1], oColumn, oControl
 cTip = ""
 IF AMOUSEOBJ(aMousePos) > 0
 oColumn = aMousePos[1]
 IF NOT ISNULL(m.oColumn) AND UPPER(oColumn.BaseClass) = "COLUMN"
 * First, grab column-level tip in case we don't find something below
 cToolTip = oColumn.ToolTipText


 * Now, look for the right control.
 oControl = EVALUATE("oColumn." + oColumn.CurrentControl)
 IF NOT EMPTY(oControl.ToolTipText)
 cTip = oControl.ToolTipText
 ENDIF
 ENDIF
 ENDIF
ENDIF
RETURN m.cTip

En la aplicación, el formulario CheckOut tiene un ToolTip para el botón de eliminar en la tercera columna; se muestra en la Figura 9.

Figura 9. El ToolTip que se muestra proviene del botón, no de la grilla. El método ToolTipText_Access busca la sugerencia correcta para mostrar.

Continua en: "Enlazar eventos para obtener mejores aplicaciones - Parte 4 de 4" ...


Copyright 2017, Tamar E. Granor

No hay comentarios. :

Publicar un comentario

Los comentarios son moderados, por lo que pueden demorar varias horas para su publicación.