24 de marzo de 2021

Enlazar eventos para obtener mejores aplicaciones - Parte 4 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 3 de 4"

El uso del método Assign

Encontré más formas de usar métodos Assign que métodos Access. Aunque un método Assign le permite cambiar el valor asignado, rara vez uso esa capacidad. Más a menudo, mi código de método Assign me permite asegurarme de que sucedan cosas adicionales cuando se guarda el valor.

A menudo, el objetivo es la encapsulación; Al usar un método Assign, evito que el código que cambia una propiedad tenga que saber a qué afecta un cambio en esa propiedad.

Actualizar una marca de tiempo

En la aplicación de la que se extrae la Figura 7, hacemos un seguimiento del valor actual (el "Valor real") de cada uno de los elementos que se muestran en el formulario. Estos valores se leen del hardware real y queremos saber no solo el valor, sino también cuándo se leyó (entre otras cosas, para mostrar la última fecha de lectura en el formulario, como en la Figura 7). Un objeto comercial contiene la información de un solo artículo; hay una colección de tales artículos. El objeto comercial tiene una propiedad cActual para el valor actual y una propiedad tLastRead para la marca de tiempo. Un método Assign para actualizaciones cActual tLastRead, como en el Listado 45.

Listado 45. El método Assign para una propiedad que rastrea los valores leídos desde el hardware actualiza la marca de tiempo del valor.

LPARAMETERS tuNewValue
IF NOT (ALLTRIM(THIS.cActual) == ALLTRIM(m.tuNewValue))
 THIS.cActual = m.tuNewValue
 * Set last read time.
 THIS.tLastRead = DATETIME()
ENDIF

Establecer una bandera "sucia" (“dirty” flag)

El código del Listado 45 es en realidad solo una parte de lo que hacemos cuando leemos un nuevo valor de hardware. Necesitamos una forma de saber si los datos han cambiado desde la última vez que se almacenaron, es decir, una bandera "sucia". Los métodos Assign también proporcionan eso. Tenemos una propiedad de aplicación, lIsDirty. Siempre que guardamos datos o abrimos un nuevo archivo de datos, borramos esa propiedad. El método Assign (del Listado 45) incluye la línea adicional de código que se muestra en el Listado 46 dentro del IF; utilizamos el mismo código en los métodos Asignar de todas las propiedades donde un cambio indica que los datos ahora son diferentes de lo que eran en la última vez que se guardaron.

Listado 46. Una línea de código en un método Assign (más un pequeño código a nivel de aplicación) le permite realizar un seguimiento de si los datos han cambiado desde la última vez que se guardaron.

goApp.lIsDirty = .T.

En la aplicación de la biblioteca, ya tenemos una gran parte del marco para mantener una bandera "sucia" en toda la aplicación. Cada formulario tiene la propiedad lDataChanged. Podemos usar esas propiedades para determinar si hay datos sin guardar en la aplicación. Para hacerlo, agregamos un método Assign a lDataChanged en frmBase. Ese método llama a un método de objeto de aplicación para actualizar la bandera "sucia" de toda la aplicación.

Solo para demostrar otra posibilidad, lDataChanged_Assign también actualiza el título del formulario para que siempre que haya datos no guardados, incluya un asterisco, tal como lo hace VFP. El método se muestra en el Listado 47.

Listado 47. Este código en el método Assign de la propiedad lDataChanged del formulario ayuda a mantener un indicador "sucio" en toda la aplicación y tiene el título de cada formulario que indica si actualmente tiene cambios sin guardar.

LPARAMETERS tuNewValue
This.lDataChanged = tuNewValue
* Set app-level dirty flag
IF VARTYPE("goApp") = "O" AND NOT ISNULL(goApp) AND ;
 PEMSTATUS(goApp, "SetDirtyFlag", 5)
 goApp.SetDirtyFlag(m.tuNewValue)
ENDIF
* Update form caption
IF m.tuNewValue
 IF RIGHT(This.Caption, 1) <> "*"
 This.Caption = This.Caption + " *"
 ENDIF
ELSE
 IF RIGHT(This.Caption, 1) = "*"
 This.Caption = LEFT(This.Caption, LEN(This.Caption)-2)
 ENDIF
ENDIF

El método SetDirtyFlag del objeto de aplicación, junto con una propiedad del objeto de aplicación, lUnsavedData, hace el resto del trabajo. SetDirtyFlag, que se muestra en el Listado 48, verifica el valor que se le pasa; si es .T., algún formulario tiene datos sin guardar, por lo que la bandera se establece en .T. Si el parámetro es .F., Sabemos que al menos un formulario acaba de guardar datos o restaurar los datos anteriores, pero no conocemos el estado de los otros formularios, por lo que los recorremos hasta que encontramos uno sin guardar. datos.

Listado 48. Este método gestiona una bandera "sucia" en toda la aplicación, de modo que podamos saber con sólo comprobar una propiedad si hay datos no guardados.

PROCEDURE SetDirtyFlag(lNewValue)
* Set the application dirty flag. If the parameter
* is true, then some form has changed data, and we can
* just set this flag. If the parameter is false, some
* form has just saved or reverted its changed data
* and we need to look at all open forms.
IF m.lNewValue
 This.lUnsavedData = .T.
ELSE
 This.lUnsavedData = .F.
 FOR EACH oForm IN _VFP.Forms FOXOBJECT
 IF PEMSTATUS(oForm, "lDataChanged", 5)
 IF oForm.lDataChanged
 This.lUnsavedData = .T.
 * No need to find more than one
 EXIT
 ENDIF
 ENDIF
 ENDFOR
ENDIF
RETURN

En la mayoría de las aplicaciones de ingreso de datos, el siguiente paso sería marcar la bandera "sucia" al salir y pedirle al usuario que guarde los cambios si hay datos no guardados. La aplicación Biblioteca se diseñó para que cuando cierre un formulario, los datos se guarden automáticamente, por lo que no es necesario preguntar.

Delegar el manejo de un nuevo valor

Para una aplicación, necesitaba la capacidad de administrar un conjunto complejo de preferencias de usuario. Los elementos de preferencia se correlacionan más o menos directamente con las propiedades del objeto de la aplicación o con las propiedades de los objetos gestionados por el objeto de la aplicación. Por ejemplo, una preferencia aborda la frecuencia con la que se realiza la recolección de basura; esta propiedad debe convertirse en el intervalo de un temporizador. Al crear una propiedad de objeto de aplicación para cada preferencia y dándoles métodos Assign, me aseguré de que las actualizaciones apropiadas ocurrieran automáticamente.

La aplicación Biblioteca tiene sólo un par de elementos en su formulario de Preferencias, que se muestra en la Figura 10. Utilice mapas de barras de herramientas grandes directamente a una propiedad de la aplicación; utiliza BindEvent() para activar el cambio de tamaño de los botones en la barra de herramientas. (Tenga en cuenta que este código es incompatible con el código de cambio de tamaño de la barra de herramientas descrito anteriormente en este documento).

La casilla de verificación que controla si la aplicación se apaga después de un período específico de inactividad también se asigna a una propiedad de la aplicación. Sin embargo, el temporizador que rastrea la inactividad (descrito en "Monitoreo de la actividad del usuario" anteriormente en este documento) debe habilitarse o deshabilitarse de manera apropiada cuando el usuario cambia la casilla de verificación. De manera similar, la ruleta que determina cuánto tiempo esperar para la actividad necesita establecer el intervalo para ese temporizador. (Nada de esto sucede hasta que el usuario cierra el formulario de Preferencias). Ambos elementos utilizan métodos Assign para garantizar que se produzcan los cambios adecuados.

Figura 10. El formulario de preferencias de la aplicación de la biblioteca se basa en los métodos Assign para garantizar que el temporizador de inactividad se establezca correctamente.

Cuando el usuario cierra el formulario, los valores de control se almacenan en las propiedades de la aplicación correspondiente. El valor de la ruleta se multiplica por 60000, la cantidad de milisegundos en un minuto, primero. El valor de ruleta ajustado se almacena en una propiedad de la aplicación llamada nActivityTimerInterval. El método nActivityTimerInterval_Assign establece el intervalo del temporizador, como se muestra en el Listado 49. Tenga en cuenta que necesitamos almacenar el valor en la propiedad de la aplicación, así como establecer el intervalo del temporizador. De lo contrario, la próxima vez que abramos el cuadro de diálogo Preferencias, no configurará la ruleta en el valor actual.

Listado 49. Este método Assign establece el intervalo para el temporizador de actividad cuando el usuario cambia la configuración en Preferencias.

PROCEDURE nActivityTimerInterval_Assign(nNewValue)
* Something changed interval for activity timer. Propagate to timer
IF VARTYPE(m.nNewValue) = "N" AND m.nNewValue > 0 AND ;
 m.nNewValue <> This.oActivityTimer.Interval
 This.nActivityTimerInterval = m.nNewValue
 This.oActivityTimer.Interval = m.nNewValue
ENDIF
RETURN

El valor de la casilla de verificación se almacena en la propiedad lTrackUserActivity. Su método Assign habilita o deshabilita el temporizador, como se muestra en el Listado 50.

Listado 50. Cuando el usuario cambia la preferencia para rastrear la inactividad del usuario, se activa el método Assign de lTrackUserActivity.

PROCEDURE lTrackUserActivity_Assign(lNewValue)
* Tracking decision changed. Set up or disable timer
IF VARTYPE(m.lNewValue) = "L" AND m.lNewValue <> This.lTrackUserActivity

 This.lTrackUserActivity = m.lNewValue
 IF This.lTrackUserActivity
 This.SetupActivityTimer()
 ELSE
 This.DisableActivityTimer()
 ENDIF
ENDIF
RETURN

Compruebe la validez

Una de las cosas que puede hacer con un método Assign es comprobar la validez del nuevo valor de una propiedad y rechazar los valores no válidos. Por ejemplo, escribí un juego de Sudoku en VFP hace unos años (como demostración de objetos comerciales; se describe en http://tinyurl.com/y84fj5p3). El objeto bizGame, que representa el juego en su conjunto, tiene una propiedad llamada nSize que contiene el tamaño del tablero (el número de celdas en cualquier dirección). Dado que Sudoku requiere que el tamaño del juego sea un cuadrado perfecto, nSize tiene un método Assign que verifica; se muestra en el Listado 51.

Listado 51. El método nSize_Assign del objeto bizGame de una aplicación de Sudoku asegura que el tamaño de juego especificado sea válido.

* Ensure that size is a perfect square
LPARAMETERS tuNewValue
IF SQRT(m.tuNewValue) = INT(SQRT(m.tuNewValue))
 This.nSize = tuNewValue
ENDIF
RETURN

Propagar datos dentro de un contenedor

Una de las principales formas en que uso los métodos Assign es empujar los datos hacia abajo en una jerarquía dentro de un contenedor, de modo que solo el contenedor esté expuesto al mundo. Hay una variedad de comportamientos en este sentido que son útiles.

Anteriormente en este documento, mostré cómo usar los métodos de Access para proporcionar ToolTips dinámicos y tener el mismo ToolTip para un contenedor y su contenido. Assign proporciona una forma alternativa de hacer esto último, en situaciones en las que no necesita sugerencias dinámicas. Es decir, este enfoque funciona cuando desea asignar un ToolTip fijo a un contenedor y hacer que todos los controles internos muestren la misma sugerencia.

Mi código para esto usa una propiedad lógica personalizada, lPropagateTooltipsDown, en la clase base Contenedor. El método ToolTipText_Assign contiene el código del Listado 52. Si el contenedor tiene el indicador establecido en .T., Cuando cambia el ToolTip, recorremos los objetos en el contenedor y cambiamos ToolTipText para cada uno de ellos.

Listado 52. Este código en el método ToolTipText_Assign le permite propagar el ToolTipText de un contenedor a los objetos contenidos.

LPARAMETERS tuNewValue
This.ToolTipText = tuNewValue
LOCAL oObject
IF THIS.lPropagateToolTipDown
 FOR EACH oObject IN THIS.OBJECTS FOXOBJECT
 IF PEMSTATUS(m.oObject, "ToolTipText", 5)
 m.oObject.ToolTipText = m.tuNewValue && don't use ToolTipText on the right
 && in case it also has an access method
 ENDIF
 ENDF
ENDIF

Tenga en cuenta que no tenemos que hacer esto de forma recursiva, incluso si el contenedor contiene otros} contenedores. El método ToolTipText_Assign para los objetos contenidos se activará y, siempre que tengan lPropogateToolTipDown establecido en .T., Manejará sus objetos contenidos.

Ajustar una etiqueta

En una aplicación, necesitaba tener una etiqueta que se ejecutara verticalmente dentro de una forma. En la forma correspondiente (que se muestra en la Figura 11), los títulos de la etiqueta se determinan dinámicamente en función de los datos. Creé una subclase de etiqueta y le di a Caption un método Assign. Ese método llama a un método TurnCaption personalizado para reconstruir el título y agregar la puntuación adecuada para que se muestre verticalmente. Para las formas de doble ancho en la fila superior, el mismo método divide el título en dos cadenas de aproximadamente la misma longitud y crea la cadena de subtítulos adecuada. TurnCaption se muestra en el Listado 53. (Tenga en cuenta que CHR (160) es un espacio sin interrupciones).

Figura 11. Las etiquetas verticales dentro de los cuadros aquí se configuran mediante un método Assign.

Listado 53. Este código es llamado por el método Caption_Assign de las etiquetas verticales que se muestran en la Figura 11.

LPARAMETERS cCaption, lTwoColumns
#DEFINE LF CHR(10)
LOCAL cTurnedCaption, nLength, nChar
m.cCaption = CHRTRAN(CHRTRAN(m.cCaption, CHR(160), " "), LF, "")
nLength = LEN(m.cCaption)
IF m.lTwoColumns
 * Find a dividing point
 nWords = GETWORDCOUNT(m.cCaption)
 IF m.nWords > 1
 * Break on a word break
 cColumn1 = GETWORDNUM(m.cCaption, 1)
 cColumn2 = ALLTRIM(SUBSTR(m.cCaption, LEN(m.cColumn1) + 1))
 cNextWord = GETWORDNUM(m.cColumn2, 1)
 nWord = 2
 DO WHILE m.nWord <= m.nWords-1 AND ;
 LEN(m.cColumn1) + LEN(m.cNextWord) > LEN(m.cColumn2) - LEN(m.cNextWord)
 cColumn1 = m.cColumn1 + " " + m.cNextWord
 cColumn2 = ALLTRIM(SUBSTR(m.cColumn2, LEN(m.cNextWord) + 1))
 cNextWord = GETWORDNUM(m.cColumn2, 1)
 nWord = nWord + 1
 ENDDO

 nLength = MAX(LEN(m.cColumn1), LEN(m.cColumn2))
 ELSE
 * Just break it in half
 cColumn1 = LEFT(m.cCaption, FLOOR(m.nLength/2))
 cColumn2 = RIGHT(m.cCaption, CEILING(m.nLength/2))

 * Pad shorter string
 cColumn1 = PADR(m.cColumn1, LEN(m.cColumn2))
 ENDIF
ELSE
 cColumn1 = m.cCaption
 cColumn2 = ""
ENDIF
cTurnedCaption = ""
FOR nChar = 1 TO nLength
 cTurnedCaption = m.cTurnedCaption + EVL(SUBSTR(m.cColumn1, nChar, 1),CHR(160))
 IF m.lTwoColumns
 cTurnedCaption = m.cTurnedCaption + CHR(160) + CHR(160) + ;
 SUBSTR(m.cColumn2, nChar, 1)
 ENDIF
 cTurnedCaption = m.cTurnedCaption + LF
ENDFOR
RETURN m.cTurnedCaption

Para la aplicación Biblioteca, puede usar este código si desea mostrar libros de forma gráfica con títulos en el lomo.

Depurar con BindEvent(), Access y Assign

Como puede imaginar, tener eventos enlazados y métodos Access y Assign hace que la depuración sea más complicada. A veces, un método parece agotarse de la nada, sin señales de que se esté llamando.

No existen fórmulas mágicas para facilitar este tipo de depuración. Sin embargo, las mismas buenas prácticas de depuración que funcionan para el código VFP generalmente son útiles aquí. En particular, considero que el comando DEBUGOUT, los puntos de interrupción y el rastreo son las herramientas más efectivas.

Cuando la secuencia de eventos no parezca tener sentido para usted, distribuya DEBUGOUT PROGRAM() a través de su código, colocándolo en cada método que parezca involucrado. Luego, cuando ejecute el código con la ventana de salida de depuración disponible, verá la secuencia de métodos llamados. (Mejor aún, no tiene que eliminar estas declaraciones antes de distribuir su aplicación. DEBUGOUT se ignora en el entorno de ejecución). Una vez que haya reducido las cosas hechas a una sección particular de código, establezca un punto de interrupción para detener la ejecución al principio de esa sección y recorra el código una línea a la vez para ver exactamente qué está sucediendo.

En algunos casos, he descubierto que ni siquiera el rastreo me resuelve el misterio, o que rastrear el código interfiere con cualquier proceso que esté viendo. En tales casos, utilizo la herramienta Registro de cobertura. Enciendo el inicio de sesión en el momento en que creo que comienza el problema, luego ejecuto el código hasta que haya ejecutado la sección del problema. Luego utilizo el registro de cobertura generado para mostrarme exactamente qué líneas de código se ejecutaron en qué orden.

La otra herramienta que es útil al depurar problemas de BindEvent() es la función AEvents(). Puedo usarlo en cualquier punto de interrupción para ver exactamente lo que está vinculado, así como para ver si un evento vinculado me llevó a este punto.

Para obtener más consejos de depuración generales, consulte mi artículo en http://tinyurl.com/ycqrjufe.

Configúrelo y olvídelo

Mientras escribía este artículo, me sorprendió la cantidad de formas que encontré para usar BindEvent(), Access y Assign. Mirando los ejemplos aquí, uno de los temas principales es "configúrelo y olvídelo". Es decir, a menudo lo que estas características le permiten hacer es configurar un comportamiento deseable en sus clases base y luego usarlo una y otra vez sin tener que recordar cómo lo hizo. En algunos casos, debe establecer una propiedad o dos, pero en general, las técnicas de este artículo reducen la cantidad de código que debe escribir. ¿No es ese el objetivo de la Programación Orientada a Objetos?







Enlace a todas las partes de este artículo:

Enlazar eventos para obtener mejores aplicaciones: Parte 1 de 4 - Parte 2 de 4 - Parte 3 de 4 - Parte 4 de 4


Copyright 2017, Tamar E. Granor

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

12 de marzo de 2021

Enlazar eventos para obtener mejores aplicaciones - Parte 2 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 1 de 4"

El seguimiento de los cambios de usuario

Las aplicaciones que realmente se comportan bien habilitan y deshabilitan los controles de forma dinámica, teniendo en cuenta la actividad del usuario. Una característica muy común es mantener el botón Guardar desactivado hasta que el usuario realmente cambie un registro. Para hacer eso, por supuesto, necesita una forma de saber que el usuario ha cambiado los datos.

En la aplicación Biblioteca, la clase de nivel superior para cada control que permite que los datos cambien (como el TextEdit y el TextBox) tiene una propiedad personalizada, lNoteChange. Cuando la propiedad es .T., indica un cambio en el valor de un control en particular que debe ser señalado al formulario, y que se deben tomar las medidas adecuadas. Además, cada control tiene un código y un método AnyChange personalizado en InteractiveChange y ProgrammaticChange que genera el evento AnyChange; ese código se muestra en el Listado 9.

Listado 9. Para poder seguir de cerca los cambios del usuario, agregue un método AnyChange personalizado a cada clase de control y coloque este código en InteractiveChange y ProgrammaticChange.

RAISEEVENT(This,"AnyChange")

La clase formulario de nivel superior también tiene un método AnyChange personalizado, así como uno llamado BindControlEvents. BindControlEvents vincula recursivamente el método AnyChange de nivel de control al método AnyChange de nivel de formulario, como en el Listado 10.

Listado 10. Este código vincula los cambios en los controles a un método de formulario, por lo que el formulario puede reaccionar.

* Bind events of controls to events of the form as appropriate
LPARAMETERS toControl
LOCAL oControl
FOR EACH oControl IN toControl.Objects
 IF PEMSTATUS(oControl, "lNoteChange", 5) AND oControl.lNoteChange
 BINDEVENT(oControl, "AnyChange", This, "AnyChange")
 ENDIF

 IF PEMSTATUS(oControl, "Objects", 5)
 This.BindControlEvents(oControl)
 ENDIF
ENDFOR

La clase base Formulario tiene una propiedad personalizada, lNoteUserChanges, que determina si se llama a BindControlEvents en el método Init del formulario. Está configurado en .F. en la clase base, pero el valor es .T. en mi clase frmBizObjAware que se usa para formularios con reconocimiento de datos.

El método AnyChange de la clase base Formulario establece una bandera y llama a un método personalizado, UpdateEnabled, para habilitar y deshabilitar controles y elementos de menú de manera apropiada; el código de AnyChange se muestra en el Listado 11.

Listado 11. Este código en el método AnyChange del formulario responde a los cambios del usuario.

This.lDataChanged = .T.
This.UpdateEnabled(.T.)
Finalmente, en la clase base Formulario, UpdateEnabled, que se muestra en el Listado 12, solo garantiza que las condiciones Skip-For del menú se vuelvan a evaluar después de un cambio. Llama a un método abstracto que puede utilizar en formularios individuales para tratar con controles específicos que deben habilitarse o deshabilitarse.

Listado 12. Para asegurarse de que los menús y las barras de herramientas se habiliten y deshabiliten adecuadamente, la clase UpdateEnabled del formulario reactiva los menús.

LPARAMETERS lForce
* Ensure that skip for conditions get re-evaluated.
ACTIVATE MENU MainMenu NOWAIT 

IF NOT EMPTY(This.cMenuName)
 ACTIVATE MENU (This.cMenuName) NOWAIT
ENDIF

This.FormUpdateEnabled(m.lForce)

Los controles de la barra de herramientas utilizan el enlace de eventos para incorporarse al menú y determinar si deben activarse o desactivarse. El método Init de tbrBase (la clase base Barra de Herramientas) llama al método BindToActiveForm personalizado, que se muestra en el Listado 13, que vincula el método UpdateEnabled personalizado de la barra de herramientas al método UpdateEnabled del formulario de llamada. Tenga en cuenta que la llamada BindEvent incluye un valor de 1 para el parámetro nFlags, lo que indica que el método delegado (el método UpdateEnabled de la barra de herramientas) debe llamarse después de que se ejecute el método fuente (el método UpdateEneabled del formulario o del objeto de aplicación). Entonces, la barra de herramientas tiene sus controles actualizados cada vez que se actualizan el formulario y el menú, pero después.

Listado 13. Este método, BindToActiveForm, asegura que los controles de la barra de herramientas se actualicen cuando los controles de formulario y el menú lo hacen.

LPARAMETERS oForm
* Bind updating of controls to updating on active form
DO CASE
CASE VARTYPE(m.oForm) = "O" AND NOT ISNULL(m.oForm)
 BINDEVENT(m.oForm, "UpdateEnabled", This, "UpdateEnabled", 1)

CASE VARTYPE(goApp) = "O" AND MethodExists(goApp.oActiveForm, "UpdateEnabled")
 BINDEVENT(goApp.oActiveForm, "UpdateEnabled", This, "UpdateEnabled", 1)
ENDCASE

El método UpdateEnabled de la barra de herramientas se basa en los propios controles para conocer la condición para habilitar o deshabilitar. Simplemente recorre todos los controles y llama al código al que apunta el control. El método se muestra en el Listado 14.

Listado 14. El método UpdateEnabled de la barra de herramientas le dice a cada control que evalúe su propia condición Skip For.

* Update the buttons on the toolbar to reflect the current state of affairs
* Each button should have a cSkipFor expression to use for this.
* If not, leave it as is
* Use TRY-CATCH to avoid problems in special cases (such as the app is closing)
FOR EACH oControl IN THIS.Controls
 IF TYPE("oControl.cSkipFor")="C"
 TRY
 oControl.Enabled = NOT EVALUATE(oControl.cSkipFor)
 CATCH
 * Nothing to do
 ENDTRY
 ENDIF
ENDFOR

Debido a que varios formularios pueden compartir la misma barra de herramientas, no es suficiente vincular la barra de herramientas al formulario en el método Init. Necesitamos cambiar el enlace ya que el usuario activa diferentes formularios.

El objeto de la aplicación rastrea el formulario activo en la aplicación mediante otro uso de BindEvent(); se llama a un par de métodos de aplicación cuando cambia el formulario activo. El método Activate de cada formulario está vinculado al método SetActiveForm del objeto aplicación, mientras que el método Deactivate de cada formulario está vinculado al método ClearActiveForm del objeto aplicación. El método Init del formulario llama al método BindForm del objeto aplicación para configurar esto. BindForm se muestra en el Listado 15. SetActiveForm y ClearActiveForm simplemente cambian la propiedad de una aplicación para que siempre apunte al formulario activo.

Listado 15. El método BindForm del objeto aplicación permite que la aplicación realice un seguimiento de qué formulario está activo actualmente.

PROCEDURE BindForm(oForm)
* Bind a form's events as needed
BINDEVENT(oForm, "Activate", This, "SetActiveForm", 1)
BINDEVENT(oForm, "Deactivate", This, "ClearActiveForm")
IF PEMSTATUS(oForm, "GotBarCode",5)
 BINDEVENT(oForm, "GotBarCode", This, "ProcessBarCode")
ENDIF
RETURN

El método Init de tbrBase se vincula a este modelo vinculando SetActiveForm y ClearActiveForm a los métodos BindToActiveForm y UnbindActiveForm de la barra de herramientas.

Nuevamente aquí, el método delegado (BindActiveForm o UnbindActiveForm) se ejecuta después del método fuente (SetActiveForm o ClearActiveForm). El método Init de la barra de herramientas se muestra en el Listado 16, mientras que el método UnbindActiveForm está en el Listado 17.

Listado 16. El método Init de la clase base Barra de Herramientas usa BindEvent() para asegurar que los controles de la barra de herramientas se vinculen y desvinculen de manera apropiada.

LPARAMETERS oCallingForm
* Reset enable/disable of controls when active form changes
IF MethodExists(goApp, "ClearActiveForm")
 * Disconnect from old form
 BINDEVENT(goApp, "ClearActiveForm", This,"UnbindActiveForm",1)
ENDIF
IF MethodExists(goApp, "SetActiveForm")
 * Connect to new form
 BINDEVENT(goApp, "SetActiveForm", This,"BindToActiveForm",1)
ENDIF
* Bind to current form
IF VARTYPE(m.oCallingForm) = "O" AND NOT ISNULL(m.oCallingForm)
 This.BindToActiveForm(m.oCallingForm)
ENDIF

Listado 17. Este método de barra de herramientas, UnbindActiveForm, se llama cuando el formulario al que están enlazados los controles de la barra de herramientas está desactivado. Libera las vinculaciones, de modo que la barra de herramientas puede, si corresponde, vincularse a otro formulario.

* Unbind from active form.
IF VARTYPE("goApp") = "O" AND VARTYPE("goApp.oActiveForm") = "O" AND ;
 PEMSTATUS(goApp.oActiveForm, "UpdateEnabled", 5)
 UNBINDEVENTS(goApp.oActiveForm, "UpdateEnabled", This, "UpdateEnabled")
ENDIF

Toda esta secuencia probablemente parece bastante complicada, pero de hecho, proporciona un mecanismo virtualmente invisible para mantener los elementos del menú y los controles de la barra de herramientas correctamente habilitados y deshabilitados. Todo lo que se requiere al crear formularios es el código de nivel de formulario personalizado en FormUpdateEnabled. Para los menús, configurar la condición Skip-For para que funcione, mientras que para las barras de herramientas, todo lo que tiene que hacer es configurar la propiedad personalizada cSkipFor de cada control.

Cambiar el tamaño de las barras de herramientas

La adición de la propiedad Anchor en Visual FoxPro 9 hizo que fuera mucho más fácil cambiar el tamaño de los controles correctamente cuando se cambia el tamaño de un formulario. Pero hay situaciones en las que Anchor no es suficiente. En una aplicación, utilizo controles dentro de una barra de herramientas para proporcionar formularios acoplables dentro del formulario principal de la aplicación (que es un formulario de nivel superior). Las barras de herramientas no tienen una propiedad Anchor; su tamaño está controlado por el tamaño de su contenido. Cuando el usuario cambia el tamaño de la aplicación, quiero asegurarme de que la barra de herramientas cambia de tamaño; para hacerlo, tengo que cambiar el tamaño de los objetos en la barra de herramientas. BindEvent() lo hace posible. Sin embargo, si no se hace con cuidado, también puede bloquear la aplicación.

Primero construí esta funcionalidad para proporcionar una barra de estado para un formulario de nivel superior que es el formulario principal de la aplicación. Pongo el control ActiveX StatusBar dentro de una barra de herramientas. Vinculé el método Resize del formulario a un método personalizado de la barra de herramientas, llamado ResizeStatus. Tenga en cuenta que esto es, en cierto sentido, lo contrario de las vinculaciones en algunos de los ejemplos anteriores. Allí, empujamos un evento desde un control hasta su contenedor o formulario contenedor. Aquí, lo estamos empujando hacia abajo para que un control particular reaccione cuando algo le sucede al formulario que lo contiene.

Lo que dificulta esta operación es que el método Resize del formulario se activa repetidamente siempre que cambie el tamaño del formulario, en lugar de una vez cuando haya terminado. Restablecer el tamaño de la barra de estado cada vez que se activa el método Cambiar tamaño del formulario hace que la barra de herramientas cambie de tamaño repetidamente, lo que afecta al tamaño del formulario, etc. Esa secuencia finalmente bloquea VFP 9.

Entonces necesitaba una forma de controlar el cambio de tamaño y hacerlo solo una vez, cada vez que se cambia el tamaño del formulario. Subclasé la clase OLEControl y le agregué el control de la barra de estado. Esa clase, sbrMSStatus, tiene un método ResizeStatus personalizado que contiene el código del Listado 18.

Listado 18. El método ResizeStatus personalizado del control de la barra de estado establece su ancho en un valor especificado.

LPARAMETERS nNewWidth

IF VARTYPE(m.nNewWidth) = "N"
 This.Width = m.nNewWidth
ELSE
 This.Width = ThisForm.Width
ENDIF

La clase base Barra de Herramientas contiene una instancia de sbrMSStatus y tiene dos propiedades personalizadas. oForm contiene una referencia de objeto al formulario contenedor, mientras que lResizingNow es una marca para indicar si estamos en medio de un cambio de tamaño. La barra de herramientas tiene un método ResizeStatus personalizado, que contiene el código del Listado 19.

Listado 19. El método ResizeStatus de la barra de herramientas asegura que cambiemos el tamaño de la barra de estado solo una vez.

LOCAL aFired[1]
IF NOT This.lResizingNow
 This.lResizingNow = .T.
 * Need width of calling form to pass in
 AEVENTS(aFired, 0)
 This.oStatusBar.ResizeStatus(aFired[1].Width)
 This.lResizingNow = .F.
ENDIF

El método Init de la barra de herramientas, que se muestra en el Listado 20, llena la propiedad oForm, y el método BeforeDock, que se muestra en el Listado 21, asegura que la barra de estado llene todo el ancho del formulario cuando lo acople. (El código probablemente debería asegurarse de que solo lo esté acoplando arriba o abajo antes de ajustar el ancho).

Listado 20. La barra de herramientas tiene un puntero al formulario contenedor, almacenado en el método Init.

DODEFAULT()
This.oForm = _VFP.ActiveForm

Listado 21. El método BeforeDock de la barra de herramientas hace que el ancho de la barra de estado coincida con el del formulario.

LPARAMETERS nLocation
* Make sure status bar fills form width
This.oStatusBar.Width = This.oForm.Width

El formulario hace su parte del trabajo en el método Activate. Si la barra de herramientas aún no existe, se crea una instancia y el método Resize del formulario está vinculado al método ResizeStatus de la barra de herramientas. Si la barra de herramientas aún no está acoplada en la parte inferior, la acoplamos. El Listado 22 muestra el código de activación.

Listado 22. El método Activate del formulario configura la barra de herramientas de la barra de estado la primera vez que se ejecuta.

IF ISNULL(This.oStatusBar)
 This.oStatusBar = NEWOBJECT("tbrStatusBar", "ebControls")
 BINDEVENT(This, "Resize", This.oStatusBar, "ResizeStatus", 1)
 This.oStatusBar.Show()
ENDIF

IF This.oStatusBar.DockPosition <> 3
 This.oStatusBar.Dock(3)
ENDIF

Tenga en cuenta que, incluso con este enfoque, esta barra de herramientas no es compatible con el formulario principal de la aplicación Biblioteca, donde los botones de las barras de herramientas pueden tener diferentes tamaños. Ese escenario colapsa VFP.

Este es otro ejemplo en el que el método delegado se ejecuta después del método del objeto de origen.

Necesitamos hacer las cosas en ese orden para que el Ancho del formulario haya cambiado para cuando queramos pasarlo al método ResizeStatus de la barra de estado.

Hay un ejemplo de un formulario con una barra de herramientas de barra de estado en los materiales de esta sesión, pero no se usa en la aplicación Biblioteca porque esa aplicación acopla y desacopla otras barras de herramientas. Para probarlo, ejecute ebMain.PRG.

Supervisión de la actividad del usuario

En algunas aplicaciones, queremos prestar atención a si el usuario está haciendo algo. Podríamos estar ejecutando un proceso en segundo plano que debería detenerse cuando el usuario quiera hacer otra cosa, o podríamos querer cerrar la sesión del usuario después de un período de inactividad. En cualquier caso, necesitamos saber cada vez que el usuario mueve el ratón o escribe. BindEvent() nos da una forma de rastrear la actividad del usuario.

El seguimiento de la actividad del usuario es una tarea a nivel de aplicación, por lo que la clase Aplicación, cusApp, necesita varias propiedades personalizadas. Los dos primeros administran el seguimiento de la actividad: lTrackUserActivity, que indica si estamos rastreando la actividad del usuario; y oActivityTimer, una referencia a un objeto temporizador utilizado para el seguimiento. Dos propiedades adicionales nos permiten indicar qué temporizador usar para el seguimiento de la actividad: cTimerClass y cTimerClassLib apuntan a la clase Timer a usar.

El método Init de la clase Aplicación configura el temporizador, si la propiedad lTrackUserActivity es verdadera, como se muestra en el Listado 23.

Listado 23. El método Init de la clase Aplicación configura un temporizador para rastrear la actividad del usuario.

IF This.lTrackUserActivity
 This.SetupActivityTimer()
ENDIF

El método SetupActivityTimer, que se muestra en el Listado 24, simplemente crea una instancia del temporizador y almacena una referencia en la propiedad oActivityTimer. TRY-CATCH se utiliza en caso de que haya algún problema.

Listado 24. Configurar el temporizador de actividad es tan fácil como crear una instancia del objeto correcto.

LOCAL lSuccess
TRY
 This.oActivityTimer = NEWOBJECT(This.cTimerClass, This.cTimerClassLib)
 lSuccess = .T.
CATCH
 lSuccess = .F.
ENDTRY
RETURN m.lSuccess

La clase base Formulario (frmBase) tiene una propiedad personalizada, lBindUserActions, y un método personalizado, BindUserActions. En el método Init del formulario, verificamos la propiedad y, si es verdadera, llamamos al método, como se muestra en el Listado 25.

Listado 25. La propiedad lBindUserActions determina si rastrear la actividad del usuario en el formulario.

IF This.lBindUserActions
 This.BindUserActions()
ENDIF

Quizás se pregunte por qué necesitamos la propiedad lBindUserActions en el nivel de formulario cuando ya realizamos un seguimiento a nivel de aplicación. Puede haber situaciones en las que un formulario en particular deba excluirse del seguimiento de la actividad o, a la inversa, en las que solo las acciones en ciertos formularios se consideren actividad del usuario.

El método BindUserActions del formulario, que se muestra en el Listado 26, llama a un método del objeto de aplicación. La declaración IF garantiza que podamos ejecutar formularios de forma independiente para realizar pruebas (es decir, cuando no se ha creado una instancia del objeto de la aplicación). Tenga en cuenta que pasamos una referencia de objeto al formulario de llamada al método BindUserActions de la aplicación.

Listado 26. El método BindUserActions de la clase base Formulario llama a BindUserActions del objeto Aplicación para realizar el enlace.

* If we have an application-level object watching for user
* activity, bind things in this form to it.
IF TYPE("goApp.oActivityTimer") = "O"
 goApp.BindUserActions(THIS)
ENDIF

El método BindUserActions de la aplicación confirma que estamos rastreando y le pide al temporizador que realice el enlace real; se muestra en el Listado 27. Pasa la referencia al formulario.

Listado 27. El método BindUserActions de la aplicación delega la tarea real de vinculación al temporizador de actividad.

LPARAMETERS oObject
* Bind user actions in the specified object to the activity timer.
IF This.lTrackUserActivity
 * Only do this if we're tracking
 IF ISNULL(This.oActivityTimer)
 * In case we haven't already set it up, do so now
 This.SetupActivityTimer()
 ENDIF
 This.oActivityTimer.BindUserActionInObject(m.oObject)
ENDIF
RETURN

Todo el trabajo real ocurre en el objeto del temporizador; tmrUserActivity se deriva de la clase base Timer (tmrBase) y tiene una propiedad personalizada, lUserActed, que es .T. cuando ha habido actividad del usuario dentro del período de tiempo especificado y .F. cuando no lo ha hecho. El temporizador tiene dos métodos personalizados, BindUserActionInObject y UserActivity. BindUserActionInObject, que se muestra en el Listado 28 y se llama desde el método BindUserActions del objeto aplicación, vincula los eventos KeyPress y MouseMove de cada control en el objeto especificado al método UserActivity, y profundiza de forma recursiva para garantizar que no importa dónde el usuario escriba o mueva el mouse , lo atrapamos.

Listado 28. El método BindUserActionInObject del temporizador de actividad vincula los eventos KeyPress y MouseMove del objeto especificado y (recursivamente) cada control que contiene al método UserActivity del temporizador.

LPARAMETERS oObject
IF PEMSTATUS(oObject, "KeyPress", 5)
 BINDEVENT(oObject, "KeyPress", This, "UserActivity")
ENDIF
IF PEMSTATUS(oObject, "MouseMove", 5)
 BINDEVENT(oObject, "MouseMove", This, "UserActivity")
ENDIF
IF PEMSTATUS(oObject, "Objects", 5)
 FOR EACH oChild IN oObject.Objects
 This.BindUserActionInObject(m.oChild)
 ENDFOR
ENDIF
RETURN

Por lo tanto, cualquier acción del usuario en el formulario activa el método UserActivity del temporizador.

La clase tmrUserActivity simplemente administra el indicador lUserActed; necesita ser mejorado o subclasificado para hacer algo basado en la actividad o inactividad del usuario. Aquí, el método UserActivity establece la bandera y restablece el temporizador, de modo que comienza a observar la inactividad nuevamente. El método se muestra en el Listado 29.

Listado 29. El método UserActivity se activa cada vez que el usuario escribe o mueve el mouse.

LPARAMETERS uParm1, uParm2, uParm3, uParm4
* Parameters for bound events
This.lUserActed = .T.
* So start counting again.
This.Reset()
RETURN

Vale la pena mencionar los parámetros de UserActivity. Cuando un método es un delegado de un evento, debe aceptar los mismos parámetros que el método fuente. Dado que MouseMove acepta cuatro parámetros, necesitamos cuatro parámetros, aunque no haremos nada con ellos. (KeyPress acepta dos parámetros, pero aceptar cuatro aquí no causará ningún problema). El método Timer del temporizador borra el indicador lUserActed porque cuando se activa, indica que el tiempo especificado ha pasado sin actividad del usuario.

Para facilitar la escritura de código para responder a la actividad o inactividad del usuario, la propiedad personalizada lUserActed tiene un método Assign. (Consulte la siguiente sección principal de este documento para obtener detalles sobre cómo funcionan los métodos Assign). Ese método se activa cada vez que cambiamos lUserActed, ya sea desde UserActivity o Timer. En una subclase, podemos poner código en ese método para tomar la acción apropiada según el nuevo valor.

En la aplicación para la que creé originalmente el temporizador de actividad, el objetivo era comenzar una actividad en segundo plano (sondeo de cambios en una pieza de hardware dedicado) después de que el usuario estuvo inactivo durante 10 segundos, y detenerla tan pronto como el usuario se activara otra vez.

Un ejemplo más simple, incluido en la aplicación Biblioteca, es preguntar al usuario si debe cerrar la aplicación después de un período de inactividad. Para crear este temporizador, subclasé tmrUserActivity para crear tmrShutDown. La propiedad Interval se establece en 60000, que es 1 minuto (60.000 milisegundos). En una aplicación real, probablemente establecería el intervalo mucho más alto; un minuto de inactividad es terriblemente corto para cerrar una aplicación. El método lUserActed_Assign, que se muestra en el Listado 30, verifica si el nuevo valor de la propiedad es .T. o .F. Si es .F., Se le pide al usuario que indique si debe cerrar la aplicación. Dado que un período de inactividad puede significar que el usuario ya no está sentado frente a la computadora, el InputBox() utilizado para indicarle al usuario tiene un tiempo de espera de un minuto y un tiempo de espera predeterminado de "Y" para cerrar la aplicación.

Listado 30. Este código, en el método lUserActed_Assign de la subclase del temporizador, pregunta al usuario si debe cerrar la aplicación. Si el usuario dice que sí o no responde en un minuto, la aplicación se cierra.

LPARAMETERS tuNewValue
LOCAL cResponse
* Shut down the application, if inactivity and user agrees
IF NOT m.tuNewValue
 cResponse = INPUTBOX("You don't seem to be doing anything. " + ;
 "Do you want to shut down this application (Y/N)?", ;
"No activity", "Y", 60000, "Y", "N")
 IF UPPER(m.cResponse) = "Y"
 IF TYPE("goApp") = "O" AND NOT ISNULL(m.goApp)
 goApp.ShutDownApp()
 ENDIF
 ENDIF
ENDIF
This.lUserActed = m.tuNewValue

El método ShutDownApp del objeto de la aplicación, como su nombre indica, cierra la aplicación de forma ordenada. La aplicación de la biblioteca utiliza esta configuración y permite al usuario decidir (a través del formulario de Preferencias) si cerrar después de un período de inactividad y cuánto tiempo debe ser ese período.

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


Copyright 2017, Tamar E. Granor

6 de marzo de 2021

Enlazar eventos para obtener mejores aplicaciones - Parte 1 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


A primera vista, la función BindEvents() puede parecer innecesaria. Después de todo, ¿por qué vincularse a un evento cuando solo puede escribir código en el método del evento?

En esta sesión ya popular, veremos por qué BindEvents() y sus primos, los métodos Access y Assign, son tan valiosos. Usando ejemplos extraídos de aplicaciones reales, veremos cómo la vinculación de eventos nos permite hacer cosas que de otra manera no podríamos hacer y simplifica el código para otras tareas. Los ejemplos incluyen el seguimiento de los cambios del usuario en un formulario, el seguimiento de la actividad del usuario, ToolTips dinámicos y más. También hablaremos sobre las dificultades involucradas en la depuración de código que usa el enlace de eventos.

Cuando se agregó la función BindEvent() en Visual FoxPro 8, me costó entender por qué debería importarme. A diferencia de la función EventHandler() que permite que mi código responda a eventos para otros servidores, BindEvent() funcionó solo para eventos disparados en código VFP. ¿Por qué necesitaría enlazarme a tales eventos? ¿Por qué no podría simplemente poner el código necesario en los correspondientes métodos de los eventos?

Con bastante rapidez, me di cuenta de que BindEvent() sería bastante útil cuando se trata de código de caja negra que no se puede modificar o subclasear (como herramientas de terceros). Pero me tomó mucho más tiempo antes de que realmente viera el potencial de BindEvent() y comenzara a usarlo ampliamente. Por otro lado, con la introducción de los métodos de Access y Assign en Visual FoxPro 6, inmediatamente vi que nos brindaban la oportunidad de crear nuestros propios eventos personalizados y rápidamente comenzamos a encontrar usos para ellos. A medida que crecía mi nivel de comodidad con BindEvent(), vi que las dos capacidades estaban realmente relacionadas. Tanto los métodos Access como Assign y BindEvent nos dan más control sobre lo que sucede cuando los usuarios trabajan con nuestras aplicaciones. Además, nos permiten construir más capacidades en nuestras bibliotecas de clases, por lo que podemos usarlas en nuevos formularios y aplicaciones.

En este artículo, mostraré cómo funciona la vinculación de eventos y los métodos Access/Assign, y examinaré algunas de las formas en que estas capacidades han mejorado las aplicaciones que desarrollé. Además, echaré un breve vistazo a los desafíos que ofrecen para la depuración. Los materiales para esta sesión incluyen una (simple) aplicación que demuestra muchas de las técnicas discutidas. La aplicación está diseñada para una biblioteca de préstamos, para manejar libros que se prestan y registran, rastrear miembros y mantener el catálogo de libros.

Esta aplicación se creó originalmente como una demostración de una variedad de prácticas de interfaz de usuario; como resultado, parte de su interfaz de usuario es un poco diferente a las aplicaciones estándar de Windows.

En particular, fue diseñada para usar códigos de barras para especificar un miembro o un libro, incluso cuando no hay formularios abiertos. Para simular códigos de barras escaneados, escriba el valor deseado con un asterisco ("*") en cada extremo. (algunos estándares de códigos de barras, como el Código 39, usan un asterisco en cada extremo como código de inicio y finalización). Por ejemplo, para especificar el miembro cuyo código de barras es "7160769048", debe escribir "*7160769048*" (sin las comillas). Sin embargo, en un campo de código de barras, puede omitir los asteriscos.

A lo largo de este artículo, usaré los términos "nivel superior" y "base" indistintamente para referirme a las subclases de primer nivel de las clases base de VFP.

Antecedentes de BindEvent()

Antes de profundizar en los ejemplos, comencemos con un poco de teoría y terminología. La función BindEvent() crea una conexión entre métodos de dos objetos. Específicamente, un evento de un objeto -el origen del evento- es controlado por otro método -el método delegado o delegado- de otro objeto -el controlador de eventos-. La sintaxis de BindEvent() se muestra en el listado

1. Dice que siempre que se activa el método cMethod de oEventSource, también se ejecutará el método cDelegateMethod de oEventHandler.

Listado 1. La función BindEvent() le permite vincular un método a otro.

BINDEVENT(oEventSource, cMethod, oEventHandler, cDelegateMethod, nFlags)

De forma predeterminada, el método delegado se ejecuta primero, pero puede cambiarlo con el parámetro nFlags. Es decir, se ejecuta el código en ambos métodos, pero puede determinar el orden.

nFlags también controla si el enlace solo se aplica cuando cMethod se activa como un evento, o también cuando se llama a cMethod mediante programación.

VFP tiene un ejemplo integrado de enlace de eventos (aunque no usa BindEvent()). Cuando establece la propiedad KeyPreview de un formulario en .T., Cada vez que se presiona una tecla en cualquier control del formulario, se activa el método KeyPress del formulario, seguido del método KeyPress del propio control. Es como si hubiera emitido BindEvent(This, "KeyPress", ThisForm, "KeyPress") para cada control del formulario.

La aplicación de biblioteca de ejemplo utiliza esta capacidad para manejar códigos de barras. Todos los formularios de entrada de datos se derivan de una clase (frmBarCodeEnabled) que tiene KeyPreview establecido en .T. y codifique en KeyPress para determinar si los datos que se ingresaron son códigos de barras.

Hay tres funciones adicionales que se ocupan del enlace de eventos. UnbindEvents() le permite desactivar el enlace de eventos. Puede desactivar todos los enlaces para un objeto en particular o solo un enlace específico. Los enlaces se desactivan automáticamente cuando cualquiera de los objetos sale del alcance, por lo que solo necesita usar UnbindEvents() si necesita eliminar un enlace mientras ambos objetos todavía están disponibles.

AEvents() le permite averiguar qué eventos están vinculados actualmente. Es más útil en el método delegado para averiguar qué objeto y evento activó el método; hay algunos ejemplos más adelante en este documento.

La última función de enlace de eventos es RaiseEvent(); le permite disparar un evento mediante programación. Se diferencia de simplemente llamar al método de evento en que garantiza que se llamen a los métodos delegados, sin importar qué parámetros haya especificado en BindEvent()

Vinculación a eventos de Windows

En Visual FoxPro 9, también puede vincularse a eventos de Windows, como cambiar aplicaciones o cambiar el esquema de color. La sintaxis en ese caso es un poco diferente; entre otras cosas, le permite especificar para qué ventana desea capturar el evento de Windows especificado. El Listado 2 muestra la estructura de la llamada.

Listado 2. Cuando se vincula a eventos de Windows, los parámetros de BindEvent() cambian.

BindEvent(hWnd | 0, nMessage, oEventHandler, cDelegateMethod [, nFlags])

El término oficial de Windows para estos eventos es "messages". Hay docenas de mensajes a los que puede responder; cada uno tiene un código numérico único. Desafortunadamente, la lista no está en la documentación de VFP. La lista completa tampoco parece estar en MSDN, pero este sitio parece tener una lista completa: http://wiki.winehq.org/List_Of_Windows_Messages.

Pase el identificador de ventana (hWnd) de la ventana cuyos mensajes desea capturar o pase 0 para capturar todos los mensajes de Windows. Mi experiencia es que, por lo general, pasar _VFP.hWnd me da lo que quiero. El controlador de eventos y los parámetros del método delegado son los mismos que cuando se vinculan eventos VFP. Aunque puede pasarlo sin errores, el parámetro nFlags se ignora al vincular un mensaje de Windows. En el método delegado, si desea que el evento de Windows se produzca como de costumbre, debe incluir código para transmitirlo. El código que necesita se muestra en "Actualización de colores cuando cambia el tema de color" más adelante en este documento.

Poner a funcionar a BindEvent()

Ahora que hemos cubierto los conceptos básicos, pasemos a algunos ejemplos que muestran cómo BindEvent() puede mejorar sus aplicaciones.

Gestión de menús contextuales

El primer lugar donde me quedó clara la utilidad real de la vinculación de eventos fue para el manejo de menús contextuales (también conocidos como menús de botón derecho). Aunque puede administrarlos en el nivel de control, generalmente encuentro que quiero hacerlo en el nivel de formulario. Es decir, quiero utilizar un método de formulario único para evaluar la situación actual y completar y mostrar un menú contextual. Hago esto emitiendo el comando en el Listado 3, en el método Init de todas mis clases de control de nivel superior.

Listado 3. Poner este comando en la inicialización de todas mis clases de control me da un manejo central de los Clics con el botón derecho.

BINDEVENT(Esto, "RightClick", ThisForm, "RightClick")

Es razonable preguntarse por qué esto es mejor que poner ThisForm.RightClick en el método RightClick de cada clase de control de nivel superior. La razón principal es que, con el enlace de eventos, puedo averiguar en el método RightClick del formulario en qué control se hizo clic con el botón derecho; No tengo que pasar el control como parámetro y recordar recibir el parámetro en el RightClick del formulario. Además, el enlace de eventos no puede ser anulado por código personalizado en el método RightClick del control (aunque, por supuesto, puede ser por código personalizado en el método Init, pero la mayoría de los desarrolladores tienen cuidado de emitir DoDefault() en el método Init.).

En cuanto a la construcción de los menús de acceso directo, mi enfoque se basa en uno que Doug Hennig publicó en FoxTalk (hace mucho tiempo en septiembre de 1997). El método RightClick de mi clase base Formulario llama a un método ShowMenu personalizado. Ese método determina la persona que llama, crea un menú emergente, llama a un método personalizado (llamado ShortcutMenu, es abstracto en la clase base Formulario) que llena la ventana emergente según quién lo llamó y otros factores, y activa la ventana emergente. El código se muestra en el Listado 4.

Listado 4. El método ShowMenu personalizado de mi clase base Formulario configura y muestra un menú contextual.

LOCAL aEventInfo[1], oObject
* Find out who called
IF AEVENTS(aEventInfo, 0) = 0
 * Called from form
 oObject = This
ELSE
 oObject = aEventInfo[1]
ENDIF

* Define menu
RELEASE POPUPS ShortCut
DEFINE POPUP ShortCut FROM MROW(), MCOL() SHORTCUT
ON SELECTION POPUP ShortCut WAIT WINDOW "Under construction." NOWAIT
* Populate menu
This.ShortcutMenu(m.oObject)
* Activate menu
IF CNTBAR("ShortCut") > 0
 ACTIVATE POPUP Shortcut
ENDIF
RELEASE POPUPS ShortCut
RETURN

ShowMenu usa AEvents() para identificar el control en el que el usuario hizo clic derecho; cuando pasa 0 como segundo parámetro a AEvents(), llena la matriz especificada (su primer parámetro) con información sobre el enlace que condujo a la rutina actual; el primer elemento de la matriz es la fuente del evento. Si la función devuelve 0, significa que este código no fue activado por un evento vinculado; en ese caso, sabemos que el usuario hizo clic con el botón derecho en el formulario.

Para un formulario en particular, todo lo que tengo que hacer es poner código en el método ShortcutMenu para crear las barras de menú apropiadas, según el objeto que recibe como parámetro. El Listado 5 muestra el código en el método ShorcutMenu del formulario CheckOut en la aplicación Biblioteca. La Figura 1 muestra el menú contextual cuando se muestra un miembro en el formulario y hace clic en cualquier lugar excepto sobre un libro en la grilla; La Figura 2 muestra el menú contextual cuando hace clic derecho sobre un libro en la grilla.

Listado 5. Vincular todos los clics con el botón derecho del ratón al formulario le permite centralizar el manejo de los menús contextuales.

LPARAMETERS oObject
* oObject = object actually right-clicked
* Build the shortcut menu for this form
LOCAL nNextBar
nNextBar = 1

* If we have a borrower, provide access to borrower form
IF NOT EMPTY(ThisForm.cCurrentMemberNum)
 DEFINE BAR m.nNextBar OF Shortcut PROMPT "Show this member's record"
 LOCAL cMemberNum
 cMemberNum = ThisForm.cCurrentMemberNum
 ON SELECTION BAR m.nNextBar OF Shortcut do form Borrowers with "&cMemberNum"
 nNextBar = m.nNextBar + 1
ENDIF
* If we're over a book in the grid, offer to open the catalog pointing to it.
* If the control that got us here is the grid itself, we're not over a record.
LOCAL lInGrid, oCheckObj, cBookBarCode
lInGrid = .F.
IF NOT INLIST(UPPER(oObject.BaseClass), "GRID", "FORM")
 oCheckObj = m.oObject
 DO WHILE NOT m.lInGrid AND NOT ISNULL(oCheckObj.Parent)
 oCheckObj = oCheckObj.Parent
 DO CASE
 CASE UPPER(oCheckObj.BaseClass) = "GRID"
 lInGrid = .T.
 CASE UPPER(oCheckObj.BaseClass) = "FORM"
 * If we get to a form, we're not in a grid. Get out of here.
 EXIT
 ENDCASE
 ENDDO
ENDIF
IF m.lInGrid
 DEFINE BAR m.nNextBar OF Shortcut PROMPT "Show book in catalog"
 cBookBarCode = CheckOutList.cBarCode
 ON SELECTION BAR m.nNextBar OF Shortcut do form Catalog with "C", "&cBookBarCode"
ENDIF

Figura 1. Cuando aparece un miembro en el formulario de pago, al hacer clic con el botón derecho en la mayoría de los lugares se ofrece una única opción: Mostrar el registro de este miembro.

Figura 2. Cuando hace clic con el botón derecho del ratón sobre un libro de la lista que desea sacar, tiene la opción de ver ese libro en el catálogo.

Manejo de eventos dentro de un contenedor

No es raro querer que todos los controles dentro de un contenedor deleguen el comportamiento al contenedor. Esto es especialmente cierto cuando se usa un contenedor para representar objetos gráficos como en la Figura 3 (que proviene de una aplicación cliente), donde se usan muchas capas de contenedores para representar un objeto físico. Las acciones deben tener lugar en el nivel del objeto físico, no en los controles a partir de los cuales se construye. Por ejemplo, cada una de las casillas verdes debajo de las etiquetas "Interfaz n" es un objeto contenedor, que contiene una etiqueta y cuatro contenedores adicionales. (De hecho, aunque la figura no lo muestra, la cantidad de contenedores dentro puede ser 2, 4 u 8.) Cada uno de esos contenedores adicionales contiene una etiqueta. En ésta aplicación, un doble clic en cualquier lugar dentro de uno de los cuadros verdes debería abrir otro formulario. El enlace de eventos hace que esto sea sencillo.

Figura 3. En este formulario, los contenedores contienen otros contenedores, así como etiquetas, formas y otros controles. A menudo, un clic o un doble clic deben interpretarse en el contexto del contenedor, no en el control que recibe la acción.

Mi clase Contenedor de nivel superior, cntBase, tiene propiedades personalizadas, lBindClick, lBindDblClick y lBindMouseDown, así como los métodos personalizados BindClick, BindDblClick y BindMouseDown. Los tres métodos son todos bastante similares. El Listado 6 muestra el método BindClick.

Listado 6. El método BindClick de la clase Contenedor de nivel superior profundiza a través de todos los objetos en el contenedor y vincula sus métodos Click al método Click del contenedor. Utiliza la recursividad para profundizar.

LPARAMETERS oContainer
* Bind all contents to start drag
FOR EACH oObject IN oContainer.Objects FOXOBJECT
 IF PEMSTATUS(oObject, "Click", 5)
 BINDEVENT(oObject, "Click", This, "Click")
 ENDIF

 IF PEMSTATUS(oObject, "Objects", 5)
 This.BindClick(m.oObject)
 ENDIF
ENDFOR

El método Init incluye el código en el Listado 7, que usa las propiedades para determinar si llamar a los métodos y configurar el enlace para cada uno de los tres eventos (Click, DblClick, MouseDown).

Listado 7. Este código en el método Init de la clase Contenedor de nivel superior vincula los métodos de controles Click, DblClick y MouseDown dentro del contenedor al contenedor, si se establecen los indicadores relevantes.

IF This.lBindDblClick
 This.BindDblClick(This)
ENDIF
IF This.lBindMouseDown
 This.BindMouseDown(This)
ENDIF
IF This.lBindClick
 This.BindClick(This)
ENDIF

Con esta estructura en su lugar, para cualquier contenedor dado, todo lo que tiene que hacer es establecer las propiedades lBindClick, lBindDblClick y lBindMouseDown para determinar qué acciones deben propagarse desde los controles contenidos al contenedor mismo. (Una mejora útil sería crear un único método BindMethod que acepte que el método se vincule como parámetro y reemplace los métodos individuales BindClick, etc.). MouseDown se incluye aquí porque es el evento más fácil para activar operaciones de arrastrar y soltar. Si se implementa la función de arrastrar y soltar, vincular MouseDown permite al usuario hacer clic en cualquier objeto dentro de un contenedor para arrastrar todo el contenedor.

En la aplicación Biblioteca, la pestaña Copiar del panel derecho del formulario Catálogo tiene todos sus controles en un solo contenedor. Cuando se ha seleccionado una copia de un libro, puede arrastrar desde ese contenedor al formulario CheckIn o CheckOut para agregar el libro a la lista de check-in o check-out respectivamente. No quiero que el usuario tenga que preocuparse por dónde se encuentra en la página Copiar, por lo que el contenedor, cntCopyInformation, tiene lBindMouseDown configurado en .T., Por lo que MouseDown en cualquier objeto en el contenedor activa el método MouseDown del contenedor, que contiene el código del Listado 8 para comenzar a arrastrar.

La figura 4 muestra un arrastre en curso.

Listado 8. Este código en el evento MouseDown de cntCopyInformation se activa cuando el usuario hace clic en cualquier lugar del contenedor porque el evento MouseDown de todos los objetos contenidos está vinculado al MouseDown del contenedor.

LPARAMETERS nButton, nShift, nXCoord, nYCoord
IF NOT EMPTY(This.txtBarCode.Value)
 This.OLEDrag(.T.)
ENDIF

Hay código tanto en el contenedor como en los formularios en los que puede soltar para manejar la operación de arrastrar y soltar, pero no es muy relevante para el enlace.

Figura 4. La página de copia del catálogo contiene un contenedor que identifica esta copia del libro especificado. Puede arrastrar desde el contenedor si está sobre el contenedor en sí o sobre cualquiera de sus controles contenidos.

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


Copyright 2017, Tamar E. Granor