12 de abril de 2016

Enlazar eventos de ventana

Autor: Doug Hennig
Traducido por: Ana María Bisbé York


Una característica disponible en otros entornos de desarrollo; pero ausente en VFP es la capacidad de capturar eventos de Windows. VFP 9 extiende la función BINDEVENT() para permitir que su propio código sea llamado cuando Windows pasa cierto mensaje a ventanas de VFP. Esto tiene un amplio rango de usos, algunos de ellos los examina en este documento, Doug Hennig.

Windows comunica eventos al pasar mensajes a las aplicaciones. Aunque VFP expone algunos de estos mensajes a través de eventos en objetos de VFP, tales como MouseDown y Click, muchos mensajes no están disponibles para desarrolladores VFP.

Un requerimiento común es la capacidad de detectar un intercambio de aplicación (application switch). Por ejemplo, creo una aplicación que entre en GoldMine, un popular sistema administrador de contactos que muestra información adicional sobre el contacto actual. Si el usuario activa GoldMine, se mueve a un contacto diferente y luego vuelve a mi aplicación, sería bueno refrescar la pantalla para que muestre información sobre el nuevo contacto. Desafortunadamente, no había manera de hacerlo en versiones anteriores de VFP; tenía que confiar en un cronómetro (timer) que verificara constantemente los contactos que estaban mostrándose actualmente en GoldMine.

VFP 9 extiende la función BINDEVENT() añadida en VFP 8 para que soporte mensajes de Windows. La sintaxis para su uso es:

bindevent(hWnd, nMessage, oEventHandler, cDelegate) 

En este caso, hWnd es el controlador de Windows para la ventana que recibe el evento, nMessage es el número del mensaje de Windows y oEventHandler y cDelegate son los objetos y métodos que se disparan cuando se recibe el mensaje por la ventana. A diferencia de los eventos VFP, sólo un controlador puede enlazar una combinación particular de hWnd y nMessage. Al especificar un segundo objeto controlador de evento o método delegado provoca que el primer enlace sea reemplazado por el segundo. VFP no verifica valores válidos de hWnd o nMessage; incluso si es no válido, no ocurre nada porque la ventana especificada no puede recibir el mensaje especificado.

Puede especificar _Screen.hWnd o _VFP.hWnd para hWnd de forma tal que atrape mensajes enviados a hWnd de la aplicación o un formulario para aquellos mensajes enviados al formulario. Los controles de VFP no tienen controlador de Windows, pero lo hace un control ActiveX, así que usted también puede enlazarlos.

Existen cientos de mensajes de Windows. Ejemplos de algunos mensajes son:

WM_POWERBROADCAST (0x0218), el que es enviado cuando ocurre un evento de encendido (power) como puede ser batería baja o intercambio a modo standbay; WM_THEMECHANGED (0x031A), el que indica que ha cambiado el tema de Windows XP; y WM_ACTIVATE (0x0006), que se inicia cuando se dispara para o desde de una aplicación. (Los mensajes de Windows se referencian usualmente por un nombre que comienza con WM_.) La documentación para casi todos los mensajes de Windows está disponible en http://msdn.microsoft.com/library/en-us/winui/winui/windowsuserinterface/windowui.asp. Los valores para las constantes WM_ están en el archivo WinUser.H que es parte de la plataforma SDK, la que se puede bajar desde www.microsoft.com/msdownload/platformsdk/sdkupdate.

El método controlador de eventos acepta cuatro parámetros: hWnd, el controlador para la ventana que recibe el mensaje; nMessage, el número de mensaje de Windows y dos parámetros enteros, el contenido de los cuales varía en dependencia de los mensajes Windows (la documentación para cada mensaje describe los valores de esos parámetros). El método debe devolver un valor Entero, que contiene un valor resultante. Uno de los valores posibles devueltos es BROADCAST_QUERY_DENY (0x424D5144, el que representa la cadena "BMQD") que advierte que el evento puede ocurrir.

Si desea que un mensaje sea procesado de forma normal, es decir que haga algo que el controlador de eventos debe hacer, debe llamar al controlador de mensajes de Windows de VFP en su método controlador de eventos; es algo como DODEFAULT() en código de un método VFP. Su método controlador de eventos probablemente devolverá el valor que fue devuelto por el controlador de eventos de Windows de VFP. He aquí un ejemplo de un controlador de eventos que hace esto (no hace otra cosa):

lparameters hWnd, ;
  Msg, ;
  wParam, ;
  lParam
local lnOldProc, ;
  lnResult
#define GWL_WNDPROC -4
declare integer GetWindowLong in Win32API ;
  integer hWnd, integer nIndex
declare integer CallWindowProc in Win32API ;
  integer lpPrevWndFunc, integer hWnd, integer Msg, ;
  integer wParam, integer lParam
lnOldProc = GetWindowLong(_screen.hWnd, GWL_WNDPROC)
lnResult = CallWindowProc(lnOldProc, hWnd, Msg, wParam, lParam)
return lnResult 

Por supuesto, el controlador de eventos no necesita declarar las funciones API de Windows ni llamar a GetWindowLong a cada momento; puede colocar este código en el método Init de la clase, guardando el valor devuelto de GetWindowLong en una propiedad de usuario, y luego utilizar esta propiedad en la llamada a CallWindowProc en el controlador de eventos. El resto de los ejemplos muestran este proceder.

Para determinar cuáles mensajes han saltado, utilice AEVENTS(ArrayName, 1). Esto llena la matriz específicada con una fila para enlazar y cuatro columnas, conteniendo los valores de los parámetros pasados a BINDEVENT().

Puede desenlazar eventos utilizando UNBINDEVENT(hWnd [, nMessage ]). Al omitir el segundo parámetro desenlaza todos los mensajes para la ventana especificada. Pase sólo 0 para desenlazar todos los mensajes de todas las ventanas. Los eventos son automáticamente desenlazados además la siguiente vez que ocurre el mensaje después que el evento controlando el objeto es destruido.

¿Qué sería una nueva versión de VFP sin nuevas funciones SYS()? El equipo VFP agregó 3 funciones SYS() relativas a eventos en VFP 9:

  • SYS(2325, wHandle) devuelve el wHandle (un envoltorio interno VFP para hWnd) para la ventana cliente de la ventana de la que es pasado wHandle como un parámetro. (Una ventana cliente es una ventana dentro de una ventana, por ejemplo, _Screen es una ventana cliente de _VFP.)
  • SYS(2326, hWnd) devuelve el wHandle para la ventana especificada con hWnd.
  • SYS(2327, wHandle) devuelve el hWnd para la ventana especificada con wHandle. La documentación para estas funciones indican que son para escenarios BINDEVENT() utilizando el VFP API Library Construction Kit. Sin embargo, puede además utilizar el hWnd para la ventana cliente de una ventana de IDE de VFP, como verá en el siguiente ejemplo.

Enlazar con eventos de ventanas de IDE VFP

TestWinEventsForIDE.PRG, incluido en esta descarga mensual, demuestra el enlace de eventos con ventanas del IDE VFP. Haga lcCaption igual al título de la ventana del IDE que desea enlazar eventos a (el siguiente código utiliza la ventana Comandos), luego ejecutar el programa. Active y desactive la ventana, muévala, redimensiónela, etc. Debe ver los eventos de la ventana mostrados en la pantalla. Al finalizar, escriba RESUME y escriba Enter en la ventana Comandos para limpiarla. Para probar con una ventana cliente de la ventana IDE, elimine el comentario del código indicado.

Puede enlazar además otros eventos, agregando sentencias BINDEVENT() de este código, utilice las constantes en WinEvents.H para el valor de los eventos deseados. Note que TestWinEventsForIDE.PRG trabaja solamente con las ventanas no ancladas del IDE, entonces antes de que ejecute este programa, haga clic derecho en la barra de título de la ventana que desea probar y asegúrese de que la propiedad Dockable está establecida a OFF.

He aquí el código para este PRG:

#include WinEvents.H
lcCaption      = 'Command'
loEventHandler = createobject('IDEWindowsEvents')
lnhWnd         = ;
  loEventHandler.FindIDEWindow(lcCaption)
* Quite los comentarios a este código para recibir eventos 
* en lugar de los usuarios de windows 
*lnhWnd         = ;
  loEventHandler.FindIDEClientWindow(lcCaption)
if lnhWnd > 0
  bindevent(lnhWnd, WM_SETFOCUS,      loEventHandler, ;
    'EventHandler')
  bindevent(lnhWnd, WM_KILLFOCUS,     loEventHandler, ;
    'EventHandler')
  bindevent(lnhWnd, WM_MOVE,          loEventHandler, ;
    'EventHandler')
  bindevent(lnhWnd, WM_SIZE,          loEventHandler, ;
    'EventHandler')
  bindevent(lnhWnd, WM_MOUSEACTIVATE, loEventHandler, ;
    'EventHandler')
  bindevent(lnhWnd, WM_KEYDOWN,       loEventHandler, ;
    'EventHandler')
  bindevent(lnhWnd, WM_KEYUP,         loEventHandler, ;
    'EventHandler')
  bindevent(lnhWnd, WM_CHAR,          loEventHandler, ;
    'EventHandler')
  bindevent(lnhWnd, WM_DEADCHAR,      loEventHandler, ;
    'EventHandler')
  bindevent(lnhWnd, WM_KEYLAST,       loEventHandler, ;
    'EventHandler')
  clear
  suspend
  unbindevents(0)
  clear
else
  messagebox('La ventana ' + lcCaption + ;
    ' no ha sido encontrada.')
endif lnhWnd > 0
define class IDEWindowsEvents as Custom
  cCaption = ''
  nOldProc = 0
  function Init
    declare integer GetWindowLong in Win32API ;
      integer hWnd, integer nIndex
    declare integer CallWindowProc in Win32API ;
      integer lpPrevWndFunc, ;
      integer hWnd, integer Msg, ;
      integer wParam, integer lParam
    declare integer FindWindowEx in Win32API;
      integer, integer, string, string
    declare integer GetWindowText in Win32API ;
      integer, string @, integer
    This.nOldProc = GetWindowLong(_screen.hWnd, ;
      GWL_WNDPROC)
  endfunc
  function FindIDEWindow(tcCaption)
    local lnhWnd, ;
      lnhChild, ;
      lcCaption
    This.cCaption = tcCaption
    lnhWnd        = _screen.hWnd
    lnhChild      = 0
    do while .T.
      lnhChild = FindWindowEx(lnhWnd, lnhChild, 0, 0)
      if lnhChild = 0
        exit
      endif lnhChild = 0
      lcCaption = space(80)
      GetWindowText(lnhChild, @lcCaption, len(lcCaption))
      lcCaption = upper(left(lcCaption, ;
        at(chr(0), lcCaption) - 1))
      if lcCaption = upper(tcCaption)
        exit
      endif lcCaption = upper(tcCaption)
    enddo while .T.
    return lnhChild
  endfunc
  function FindIDEClientWindow(tcCaption)
    local lnhWnd, ;
      lnwHandle, ;
      lnwChild
    lnhWnd = This.FindIDEWindow(tcCaption)
    if lnhWnd > 0
      lnwHandle = sys(2326, lnhWnd)
      lnwChild  = sys(2325, lnwHandle)
      lnhWnd    = sys(2327, lnwChild)
    endif lnhWnd > 0
    return lnhWnd
  endfunc
  function EventHandler(hWnd, Msg, wParam, lParam)
    ? 'La ventana ' + This.cCaption + ;
      ' recibió el evento #' + transform(Msg)
    return CallWindowProc(This.nOldProc, hWnd, Msg, ;
      wParam, lParam)
  endfunc
enddefine

El programa comienza instanciando la clase IDEWindowsEvents. Llama al método FindIDEWindow que toma un controlador a la ventana de la que se ha guardado el título en la variable lcCaption. Emplea entonces BINDEVENT() para enlazar ciertos eventos desde la ventana deseada al método EventHandler de la clase. Estos eventos incluyen la activación, desactivación, redimensionamiento, y movimiento de la ventana, y oprimir teclas dentro de la ventana.

El método Init de la clase IDEWindowsEvents declara las funciones Windows API utilizadas por la clase. Esto también determina el valor utilizado para llamar al mensaje del controlador de Windows VFP y lo guarda en la propiedad nOldProc; este valor es utilizado por el método EventHandler para asegurar que ha ocurrido un control normal de eventos. El método FindIDEWindow utiliza una pareja de funciones Windows API para buscar la ventana del IDE de VFP especificada. Esto lo hace buscando cada ventana hija de _VFP para ver si su título coincide con el título pasado como un parámetro. FindIDEClientWindow hace algo similar; pero utiliza las nuevas funciones SYS() para tomar el controlador para la ventana cliente de la ventana especificada.

Al ejecutar TestWinEventsForIDE.PRG, encontrará que no todos los eventos ocurren para todas las ventanas del IDE o de usuarios. Por ejemplo, no verá los eventos keypress para la ventana Propiedades. Esto es probablemente debido a la forma en que VFP implementa ventanas, las que son algo diferentes de otras aplicaciones Windows.

Nota: No utilizará este tipo de código en una aplicación típica, se dirige a los desarrolladores que deseen agregar este comportamiento al IDE VFP. El siguiente ejemplo es algo que puede utilizar en una aplicación final de usuario.

Enlazar la ventana aplicación y eventos de disco

WindowsMessagesDemo.SCX (vea Figura 1) demuestra el gancho en eventos Activate y Deactivate y eventos Shell de Windows, tales como insertar o eliminar un disco compacto o USB. El código siguiente muestra un uso interesante de eventos de Windows: El código registra _VFP para recibir un subconjunto de eventos shell de Windows como un evento de usuario de Windows.


Figura 1

El método Init de este formulario controla la configuración necesaria. Como con TestWinEventsForIDE.PRG declara varias funciones APIs de Windows y almacena el valor para el controlador de evento de Windows VFP en la propiedad nOldProc. La llamada a SHChangeNotifyRegister dice a Windows que registre _VFP para recibir eventos de disco, eventos de insertar o desactivar dispositivos de media o unidades de disco, utilizando un mensaje de usuario WM_USER_SHNOTIFY. (Los elementos en letras mayúsculas en este ejemplo son constantes definidas en WinEvents.H o ShellFileEvents.H.)

Este código enlaza luego los eventos activate del formulario y los cambios de dispositivos y el mensaje de usuario definido para _VFP por el método HandleEvents en el formulario.

Nota: La llamada a SHChangeNotifyRegister requiere Windows XP o posterior. Si utiliza un sistema operativo anterior, comente la asignación This.nSHNotify

local lcSEntry
* Declaramos las funciones API de Windows que vamos a utilizar.
declare integer GetWindowLong in Win32API ;
  integer hWnd, integer nIndex
declare integer CallWindowProc in Win32API ;
  integer lpPrevWndFunc, integer hWnd, integer Msg, ;
  integer wParam, integer lParam
declare integer SHGetPathFromIDList in shell32 ;
  integer nItemList, string @szPath
declare integer SHChangeNotifyRegister in shell32 ;
  integer hWnd, integer fSources, integer fEvents, ;
  integer wMsg, integer cEntries, string @SEntry
declare integer SHChangeNotifyDeregister in shell32 ;
  integer
* Tomamos un controlador para el controlador de eventos de Windows en VFP.
This.nOldProc = GetWindowLong(_screen.hWnd, ;
  GWL_WNDPROC)
* Nos registramos para recibir cierto evento shell  
* como un evento de usuario de Windows.
lcSEntry = replicate(chr(0), 8)
This.nShNotify = SHChangeNotifyRegister(_vfp.hWnd, ;
  SHCNE_DISKEVENTS, SHCNE_MEDIAINSERTED + ;
  SHCNE_MEDIAREMOVED + SHCNE_DRIVEADD + ;
    SHCNE_DRIVEREMOVED, WM_USER_SHNOTIFY, 1, @lcSEntry)
* Enlazamos con el evento de Windows que estamos interesados.
bindevent(This.hWnd, WM_ACTIVATE,      This, ;
  'HandleEvents')
bindevent(_vfp.hWnd, WM_DEVICECHANGE,  This, ;
  'HandleEvents')
bindevent(_vfp.hWnd, WM_USER_SHNOTIFY, This, ;
  'HandleEvents')
* Ocultamos la ventana principal de VFP para que sea más fácil ver lo que ocurre
_screen.Visible = .F.

El método HandleEvents controla los eventos registrados. Utiliza una sentencia CASE para determinar qué evento ocurrió y actualizar el título de la etiqueta de estado adecuadamente en el formulario. Ciertos tipos de eventos tienen "subeventos" como se ha indicado por el parámetro wParam; esto es utilizado para determinar qué evento ocurrió exactamente. Por ejemplo, cuando ocurre un evento WM_ACTIVATE, wParam especifica si la ventana ha sido activada o desactivada, y si la activación ocurrió por el conmutador de tareas (task switching) (como Alt+Tab) o por hacer Clic en la ventana.

Controlar el evento de usuario shell es un poco más complicado que otros eventos. En ese caso, lParam especifica el evento wParam y contiene la dirección donde se guarda la ruta para el disco que fue insertado o removido. Entonces, SYS(2600), se utiliza para copiar el valor desde la dirección, el método de usuario BinToInt (no se muestra aquí) convierte el valor en un entero, y la función API de Windows SHGetPathFromIDList proporciona el camino actual desde el entero. Finalmente, el método llama al método
HandleWindowsMessage, el que simplemente llama a CallWindowProc para tomar el comportamiento normal de controlador de eventos. He aquí el código para HandleEvents:

lparameters hWnd, ;
  Msg, ;
  wParam, ;
  lParam
local lcCaption, ;
  lnParm, ;
  lcPath
do case
* Controla un evento de activar o desactivar.
  case Msg = WM_ACTIVATE
    do case
* Controla un evento de desactivar.
      case wParam = WA_INACTIVE
        This.lblStatus.Caption = 'Window deactivated'
* Controla un evento activar (activar una tarea o hacer clic en una barra de título)
      case wParam = WA_ACTIVE
        This.lblStatus.Caption = ;
          'Window activated (task switch)'
* Controla un evento activar (hace clic en el área de usuario de una ventana)
      case wParam = WA_CLICKACTIVE
        This.lblStatus.Caption = ;
          'Ventana activada (clic)'
    endcase
* Controla un evento de cambio de dispositivo (device).
  case Msg = WM_DEVICECHANGE
    do case
      case wParam = DBT_DEVNODES_CHANGED
        This.lblStatus.Caption = 'DevNodes cambiado'
      case wParam = DBT_DEVICEARRIVAL
        This.lblStatus.Caption = 'Dispositivo integrado'
      case wParam = DBT_DEVICEREMOVECOMPLETE
        This.lblStatus.Caption = 'Dispositivo retirado ' + ;
          'complete'
    endcase
* Controla un evento de notificación de shell de usuario.
  case Msg = WM_USER_SHNOTIFY
    do case
      case lParam = SHCNE_DRIVEADD
        lcCaption = 'Disco agregado'
      case lParam = SHCNE_DRIVEREMOVED
        lcCaption = 'Disco retirado'
      case lParam = SHCNE_MEDIAINSERTED
        lcCaption = 'Media insertada'
      case lParam = SHCNE_MEDIAREMOVED
        lcCaption = 'Media retirada'
    endcase
    lnParm = This.BinToInt(sys(2600, wParam, 4))
    lcPath = space(270)
    SHGetPathFromIDList(lnParm, @lcPath)
    lcPath = left(lcPath, at(chr(0), lcPath) - 1)
    This.lblStatus.Caption = lcCaption + ': ' + lcPath
endcase
return This.HandleWindowsMessage(hWnd, Msg, wParam, ;
  lParam)

Ejecute el formulario como se indica en las instrucciones. Haga clic en otra ventana o el escritorio y nuevamente en el formulario para eventos activar o desactivar. Agregue o elimine un dispositivo de algún tipo, como disco USB o una cámara digital, para ver que evento ocurrió.

Existen algunos usos prácticos para el tipo de código mostrado en este formulario. Por ejemplo, el GoldMine integrado que he mencionado al inicio del artículo puede refrescarse cuando recibe un activate. Al adjuntar la cámara digital a mi PC con un cable USB, el software que viene con la cámara se activa y me pregunta por las imágenes a bajar. Una aplicación inmobiliaria o de medicina con tratamiento de imágenes puede podría hacer algo similar con imágenes de casas o de medicina.

Otros usos

Existe gran cantidad de otros usos para eventos de Windows. Por ejemplo, puede desear prevenir Windows para terminar sesión bajo ciertas condiciones, como son unos procesos de importación que no han sido completados. En este caso puede enlazar con el mensaje WM_POWERBROADCAST y devuelve BROADCAST_QUERY_DENY si se detiene la descarga.

Utilizo Microsoft Money para hacer mis financias domésticas y me ha gustado siempre el hecho que cuando descargo un dato de mi banco, Money lo sabe inmediatamente y muestra el diálogo apropiado. El tipo de comportamiento es ahora posible en una aplicación VFP, en lugar de elegir constantemente un directorio para ver si un archivo ha sido agregado (o removido o renombrado o lo que sea), su aplicación puede notificar tan pronto como ocurra y toma la acción apropiada.

Resumen

Poder enlazar con eventos de Windows es una mejora increíble a VFP; permite hondar casi dentro de cualquier cosa que ocurra en Windows. Espero ver muchos usos interesantes de esto en cuánto la comunidad de VFP comience a aprender sobre estas capacidades.


No hay comentarios. :

Publicar un comentario