VFP中轻松绑定 Windows 事件

轻松绑定 Windows 事件
作者:Doug Hennig
译者:fbilo

 VFP 所缺少的在其它开发环境中的一个功能是捕捉 Windows 事件的能力。VFP 9 扩展了 BindEvent()函数的功能,现在,当 Windows 传递某些特定的消息给 VFP 窗口的时候,BindEvent()可以调用我们自己的代码。这个功能有着很广泛的用途,Doug Hennig 在这里为我们讲述了一部分。
 Windows 通过传递消息来把事件传送给应用程序。尽管 VFP 已经通过 VFP 对象中的事件为我们提供了这些消息的一部分——比如 MouseDown 和 Click——但还是有许多消息对 VFP 程序员来说是不可用的。
一个常见的需求是要能够检测到用户在应用程序之间切换的事件。例如,我建立了一个应用程序,该应用挂钩在一个著名的联系人管理系统 GoldMine 中,以为当前联系人显示更多的信息。如果用户切换到 GoldMine,然后选择了另一个联系人,那么,当他再次切换回我的应用程序的时候,我的应用程序就应该进行刷新,以显示新联系人的信息。不幸的是,在以前版本的 VFP 中是不可能做到这一点的;我只有依靠定时器来不断的检查当前显示在 GoldMine 中的是哪个联系人。
VFP 9 扩展了在 VFP 8 中增加的 BindEvent() 函数的功能以支持 Windows 消息。实现这个功能的语法是:
bindevent(hWnd, nMessage, oEventHandler, cDelegate)
在这里,hWnd 是接收事件的窗口的 Windows 句柄,nMessage 是 Windows 消息编号,而 oEventHandler 和 cDelegate 是当窗口接收到消息的时候被触发的对象和方法。与 VFP 事件不同的是,只有一个事件处理器(oEventHandler)可以被绑定给一个特定的 hWnd 和 nMessage 组合。指定第二个事件处理器对象或者代理方法会导致第一个绑定被替换为第二个。VFP 不会对 hWnd 或 nMessage 的值的有效性进行检查;如果其中一个无效,就什么都不会发生,因为指定的窗口将无法接收到指定的消息。
对于 hWnd,你可以指定 _Screen.hWnd 或者 _VFP.hWnd 来跟踪那些发送给应用程序的消息,或者指定一个表单的 hWnd 来跟踪发送给该表单的消息。VFP 控件没有 Windows 句柄,但是 ActiveX 控件有,所以你也可以绑定到它们身上。
Windows 中有着数百个 Windows 消息。这类的消息有:WM_POWERBROADCAST(0x0218),该消息在电池电量过低、或者切换到挂起模式这样的电源事件发生时被发出、WM_THEMECHANGED(0x031A),它表示 Windows XP 桌面主题已经被更换;还有 WM_ACTIVATE(0x0006),它在切换到一个应用程序、或者从应用程序切换出来的时候被触发(Windows 消息通常用一个以WM_开头的名字来引用)。
在地址:
http://msdn.microsoft.com/library/en-us/winui/winui/windowsuserinterface/windowui.asp 
处有着几乎所有的 Windows 消息的文档。而作为 Platform SDK 一部分的 WinUser.H 文件中有着那些 WM_ 开头的常量的值,你可以从 www.microsoft.com/msdownload/platformsdk/sdkupdate/ 处下载这个 Platform SDK。
事件处理器的方法必须接收4个参数:hWnd,是接收消息的窗口的句柄;nMessage,是 Windows 消息的编号;还有两个 Integer 参数,它们的值随着 Windows 消息的不同而不同(每个消息的文档解释了这些参数的值)。方法必须返回一个 Integer,其中包含着一个结果的值。一种可能的返回值是 BROADCAST_QUERY_DENY(0x424D5144,代表字符串“BMQD”),意思是事件的发生被阻止了。
如果你想让消息通过那种大多数事件处理器应做的常规方式来被处理,你就必须在你的事件处理器方法中调用 VFP 的 Windows 消息处理器;这是种就像是在 VFP 方法代码中使用 DODEFAULT() 这样的办法。你的事件处理器方法很象是返回 VFP 的 Windows 消息处理器的返回值。这里是一个事件处理器完成这种任务的一个例子(别的它什么都不干):
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
当然,事件处理器不需要每次都声明 Windows API 函数或者调用 GetWindowLog;你可以把这些代码放在类的 Init 方法中,把 GetWindowLong 的返回值保存在一个自定义属性中,然后在事件处理器要调用 CallWindowProc 的时候使用这个属性。后面的例子会用这种方法来做。
为了判定绑定的是哪种消息,可是使用 AEVENTS(ArrayName,1)。它会在指定的数组中为每个绑定分配一行和四列,并用被传递给 BINDEVENT()的参数的值对元素们进行填充。
你可以使用 UNBINDEVENT(hWnd[,nMessage])来取消事件绑定。忽略第二个参数的话,就会取消对指定窗口的所有消息的绑定。只给这个函数传递一个0会取消对所有窗口的所有消息的绑定。在事件处理器对象被释放以后,再次发生消息时事件也会自动取消绑定。
一个新版本的 VFP 怎么会没有一些新的 SYS() 函数呢?在 VFP 9.0 中新增了三个与 Windows 事件相关的 SYS()函数:
 SYS(2325, wHandle)为一个由传递进来的wHandle(VFP对 hWnd 的一个内部封装)指定的窗口返回它的客户端窗口的 wHandle(一个客户端窗口是一个在一个窗口内部的窗口;例如,_Screen 是 _VFP 的一个客户端窗口)。
 SYS(2326,hWnd)返回用 hWnd 指定的窗口的 wHandle。
 SYS(2327,wHandle)为用 wHandle 指定的窗口返回其 hWnd。这些函数的文档指出,它们是在使用了 VFP API 库结构工具包的情况下用于BINDEVENT()函数的。不过,从下面的例子中可以看到,你也可以使用它们来获得一个 VFP IDE 窗口的客户端窗口的 hWnd。
绑定到 VFP IDE 窗口事件
包含在这个月下载文件中的 TestWinEventsForIDE.PRG 演示了怎么将事件绑定到 VFP 的 IDE 窗口。将你想要绑定事件的 IDE 窗口的 Caption 设置为 lcCaption(下面的代码将使用命令窗口来演示),然后运行这个程序。激活和取消激活该窗口,移动、缩放窗口等等;你将看到对 Screen 做出反应的 Windows 事件。试验完了的时候,在命令窗口里输入 RESUME 并按下回车来进行清理。要用这段代码对一个 IDE 窗口中的客户端窗口进行测试的话,需要将代码中被注释掉的部分反注释。
你也可以通过给这段代码添加 BINDEVENT() 语句来绑定其它的事件;为希望绑定的事件使用在WinEvents.H中的常量的值。注意,TestWinEventsForIDE.PRG只工作于非可停靠IDE窗口,所以,在你运行这个程序之前要先在你想要测试的窗口的标题栏上单击鼠标右键、确保可停靠选项已被关闭。
这里是这个 PRG 的代码:
#include WinEvents.H

lcCaption = 'Command'
loEventHandler = createobject('IDEWindowsEvents')
lnhWnd = ;
loEventHandler.FindIDEWindow(lcCaption)

* 反注释下面的代码以接收客户端窗口的事件
*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('The ' + lcCaption + ;
  ' window was not found.')
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)
  ? 'The ' + This.cCaption + ;
   ' window received event #' + transform(Msg)
  return CallWindowProc(This.nOldProc, hWnd, Msg, ;
   wParam, lParam)
 endfunc
enddefine
这段代码从建立 IDEWindowsEvents 类的实例开始。它调用 FindIDEWindow 方法来获得对一个其 Caption 储存在 lcCaption 变量中的窗口的句柄。然后它使用 BindEvent()来将所需窗口的特定事件绑定到这个类的 EventHandler 方法。这些事件包括激活、取消激活、缩放、移动窗口、以及在窗口中的键击。
IDEWindowsEvents 的 Init 方法声明了这个类要用到的 Windows API 函数。它还定义了用于调用 VFP 的 WIndows 消息处理器的值,并将这个值保存在 nOldProc 属性中;EventHandler 方法使用这个值来确保常规的事件处理会正常发生。FindIDEWindow 方法使用一对 Windows API 函数来找到指定的 VFP IDE 窗口。它是通过遍历 _VFP 的每个子窗口、并将各个子窗口的 Caption 和被作为参数传递进来的 Caption 进行对比来完成这个任务的。FindIDEClientWindow做的是类似的事情,不过使用了新的 SYS() 函数来获得对指定窗口的客户端窗口的句柄。
当你运行 TestWinEventsForIDE.PRG 的时候,你会发现并非每个 IDE 或者客户端窗口都会发生每个事件。例如,在属性窗口中你不会看到 keypress 事件的发生。这很可能是由于 VFP 实现窗口的方式与别的 Windows 应用程序不同所导致的。
注意:通常你不会在一个典型的应用程序中使用类似于这里的代码;它只是向那些想要给 VFP 的 IDE 添加自己所需行为的程序员提供的。下一个例子中用到的技术你可能会用在一个最终用户的应用程序中。
绑定到应用程序窗口和磁盘事件
WindowsMessagesDemo.SCX (见图1)演示了挂钩到激活和取消激活以及特定的一些 Windows Shell 事件——例如插入或取出一张 CD 或者 USB 设备。下面演示了一个 Windows 事件的有趣的用途:这些代码将一个 Windows Shell 事件的子集当作是一个自定义 Windows 事件,并将_VFP 注册为接收这个自定义Windows事件。
 
图1、WindowsMessagesDemo.SCX 演示了你的应用程序可能会需要的几个 Windows 事件。
这个表单的 Init 方法会处理必要的设置问题。跟 TestWinEventsForIDE.PRG 一样,它声明了几个 Windows API 函数,并将用于 VFP 的 Windows 事件处理器的值保存在 nOldProc 属性中。使用自定义的消息 WM_USER_SHNOTIFY 来调用SHChangeNotifyRegister 以让 Windows 把 _VFP 注册为接收磁盘事件、媒体的插入和移除事件、以及设备的增加和减少事件(在下面的代码中,大写的标志符是定义在 WinEvents.H 或者 ShellFileEvents.H 中的常量)。
然后这段代码为表单将激活事件和设备更动事件以及它刚刚为 _VFP 定义的自定义消息绑定到表单的 HandleEvents 方法。注意:对 SHChangeNotifyRegister 的调用要求 Windows XP 或更高的版本。如果你在使用一个更早版本的操作系统,请将为 This.nSHNotify 赋值的语句注释掉。
local lcSEntry

* 声明要用到的 Windows API 函数
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

* 为 VFP 窗口事件处理器取得一个句柄
This.nOldProc = GetWindowLong(_screen.hWnd, ;
GWL_WNDPROC)

* 注册 _VFP 来把特定的 shell 事件当作一个自定义 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)

* 绑定到我们感兴趣的Windows事件
bindevent(This.hWnd, WM_ACTIVATE, This, 'HandleEvents')
bindevent(_vfp.hWnd, WM_DEVICECHANGE, This, 'HandleEvents')
bindevent(_vfp.hWnd, WM_USER_SHNOTIFY, This, 'HandleEvents')

* 隐藏 VFP 主窗口以便更清楚的看到接下来会发生什么
_screen.Visible = .F.
HandleEvents 方法处理已注册了的事件。它使用一个 CASE 语句来判定发生了哪个事件,并相应的更新在表单上的状态标签的 Caption。特定的事件类型有着由 wParam 参数标识的“子事件”;这是用来准确的判定发生了什么事件用的。例如,当一个 WM_ACTIVATE 事件发生的时候,wParam 指出窗口当前是激活了的还是没有激活、以及激活的发生是否是由任务切换(例如用户按下了 Alt+Tab)或者在窗口上单击而导致的。
处理自定义的 Shell 事件要比其它事件复杂一些。在这个案例中,lParam 用来识别出事件,而 wParam 则包含着一个内存地址,该地址指向的是用于表示被插入或者移除的驱动器的路径。然后,SYS(2600)被用来从地址中拷贝出地址所指向的内存中的值,自定义的 BinToInt 方法(代码这里没有放出来)把这个值转换成一个整型值,而 Windows API 函数 SHGetPathFromIDList 则从这个整型值中提取出真正的路径。最后,这个方法调用 HandleWindowsMessage 方法,后者简单的调用 CallWindowsProc 来获得常规的事件处理行为。这里是 HandleEvents 的代码:
lparameters hWnd, ;
 Msg, ;
 wParam, ;
 lParam
local lcCaption, ;
 lnParm, ;
 lcPath

do case
 * 处理一个 activate 或者 deactivate 事件
 case Msg = WM_ACTIVATE
 do case
  * 处理一个 deactivate 事件
  case wParam = WA_INACTIVE
   This.lblStatus.Caption = 'Window deactivated'
  * 处理一个 activate 事件 (任务切换或者在标题栏上单击)
  case wParam = WA_ACTIVE
   This.lblStatus.Caption = ;
    '窗口激活 (任务切换)'
  * 处理一个 activate 事件 (在窗口的客户端区域单击).
  case wParam = WA_CLICKACTIVE
   This.lblStatus.Caption = ;
    '窗口激活 (单击)'
 endcase

 * 处理一个设备更动事件
 case Msg = WM_DEVICECHANGE
  do case
   case wParam = DBT_DEVNODES_CHANGED
    This.lblStatus.Caption = 'DevNodes 已变动'
   case wParam = DBT_DEVICEARRIVAL
    This.lblStatus.Caption = '设备增加'
   case wParam = DBT_DEVICEREMOVECOMPLETE
    This.lblStatus.Caption = '设备移除 ' + ;
     '完成'
  endcase
 * 处理一个自定义 shell 通知事件
 case Msg = WM_USER_SHNOTIFY
  do case
   case lParam = SHCNE_DRIVEADD
   lcCaption = '驱动器已增加'
   case lParam = SHCNE_DRIVEREMOVED
   lcCaption = '驱动器被移除'
   case lParam = SHCNE_MEDIAINSERTED
   lcCaption = '媒体已插入'
   case lParam = SHCNE_MEDIAREMOVED
   lcCaption = '媒体已移除'
  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)
运行这个表单,然后象介绍中指出的那样在其它窗口或者桌面上单击再回到表单上以显示激活和反激活事件。插入或者移除一个某种类型的可移动驱动器,比如一个 USB 驱动器或者一个数码相机,以了解事件的发生情况。
在该表单中的这类代码有几个实用的用途。例如,我在本文前面提到的 GoldMin 插件现在可以在接收到一个 Activate 事件的时候刷新自己了。当我把我的带有一个 USB 延长线的数码相机连接到我的计算机上的时候,数码相机自带的软件就会弹出窗口提示我去下载照片。一个 VFP 房地产或者医学图像应用程序可以对建筑或者伤口的照片做到类似的事情。
其它用途
绑定 Windows 事件还有大量其它的用途。例如,你可以在特定的条件下(比如一个重要的进程尚未完成)阻止 Windows 关机。在那样的情况下,你最好绑定到 WM_POWERBROADCAST 消息,并且在不应关机的时候返回 BROADCAST_QUERY_DENY。
我用 Microsoft Money 来管理我的家庭财务,当我从银行下载了一个帐单的时候,Money立刻就会知道并且显示相应的对话框,我很喜欢这种功能。这类的行为现在在 VFP 应用程序中也能够做到了;现在,你的应用程序不再需要不停的去检查一个路径以搞清是否增加(或者减少、重命名)了一个文件,这种事情一发生,你的应用程序就会被通知,从而做出相应的反应。
结论
支持 Windows 事件绑定是VFP一个令人难以置信的增强,它让你可以挂钩到发生在 Windows 中的任何事情。既然 VFP 社群已经开始研究它的能力了,那么我期待着看到它的更酷的用法。

你可能感兴趣的:(windows)