分享Windows的秘密——外壳通知消息 | ||
作者:未知 文章来源:来自网络 点击数:895 更新 时间:2005-7-11 23:34:09 | ||
如果仔细研究一下Windows API函数SHChangeNotify就会发现它可以获得外壳中发生的许多事件的信息,其中包括:文件、目录的变化,媒体设备的插入和卸载,磁盘自由空间的更新等等。比如,当从光驱中放入或取出光盘的时候,资源管理器会自动感知到这些动作,并更新相应光盘驱动器的图标,这就是通过外壳通知(Shell Notification)来实现的。 注册外壳变化通知 接收外壳的变化通知消息的关键是SHChangeNotifyRegister 函数,下面是它的定义: function SHChangeNotifyRegister(Window: HWND; Flags: DWORD; EventMask: ULONG; MessageID: UINT; ItemCount: DWORD; var Items: TNotifyRegister): THandle; stdcall; 它被用来注册一个用来接收所有系统变化消息的窗口。这是一个未经公开的函数,它在SHELL32.DLL中的索引是2。Window 参数指定了用来接收通知消息的窗口,EventMask 参数可以设定为感兴趣的事件的位掩码的组合,可以使用任意的SHCNE_xxx 常数的组合。表2.12提供了全部这些常数的定义。 表2.12 常 数 Flags 参数允许设定对通知消息的响应行为,可以过滤中断或非中断的事件,并确定是否在Windows NT下使用代理窗口。表2.13中列出了所有可以使用的标识。通常来说如果不考虑事件的发生源头的话,应该设定接收中断和非中断的事件。一般来说,中断事件是非常少的。SHCNF_NO_PROXY 标识使我们在Windows NT下可以更有效地处理外壳通知消息,但它同时会使消息处理过程变得复杂,并需要使用额外的几个未公开的函数调用,我们将在后面谈到。 表2.13 Flag MessageID 参数是将被发送给窗口的消息标识符,建议使用从WM_USER消息衍生出来的值以避免同系统消息冲突。关于消息处理的细节后面将会谈到。ItemCount 参数用来指定希望监视的路径数量,通过设定Items 参数,可以同时监视多个不同的路径。 Items 参数是一个指向TNotifyRegister记录类型的指针,下面就是类型定义: TNotifyRegister = packed record pidlPath: PItemIDList; bWatchSubtree: BOOL; end; 如果记录的 pidlPath 成员指定了一个有效的目录的PIDL,就会得到同目录相关的改变的消息通知,如果设定了bWatchSubtree 记录成员为True的话,就会收到指定文件夹的整个子目录系统的改变事件。如果pidlPath 设为nil,就可以接收到系统中所有外壳对象变化的事件通知了。如果ItemCount 参数大于1,那就需要提供相同数目的TNotifyRegister 记录以向量形式指向Items 参数,一个记录挨着一个记录,储存在一个缓冲区中。SHChangeNotifyRegister 调用成功的话,会返回一个外壳变化通知对象的句柄,句柄将会在后面用到,如果调用失败了的话,就返回0。 去除注册 结束对外壳事件的监视后,应该以SHChangeNotifyRegister调用返回的句柄作为参数调用SHChangeNotifyDeregister函数来撤销注册,它在Shell32.dll中的索引是4,函数定义如下: function SHChangeNotifyDeregister(Notification: THandle): BOOL; stdcall; 调用成功的话,返回True,如果失败的话,返回False。 获得消息 注册后,外壳将会把通知消息发送给注册时指定的窗口,我们需要从通知消息中获得有用的信息。在Delphi中,消息记录的定义如下: TMessage = record Msg: DWORD; WParam: DWORD; LParam: DWORD; Result: DWORD); end; 其中Msg 变量是用来识别通知消息的,也就是在这里我们要判断它是否同SHChangNotifyRegister中指定的MessageID 参数一致。LParam 会被外壳设定为发生事件的标识符,它可以是表1中的SHCNE_xxx 值中的任意一个或其位掩码的组合,WParam 是一个指向下面TTwoPIDLArray记录类型的指针。 记录包含两个PIDL变量,其定义如下: TTwoPIDLArray = packed record PIDL1: PItemIDList; PIDL2: PItemIDList; end; Windows NT和内存映像 对于Windows NT来说,其中每个进程使用的内存是相互独立的,一个进程试图读写另一个进程的内存会引起异常错误,也就是说我们不能简单地把消息发送给另一个进程的窗口,其进程无法正确处理消息中包含的结构指针,并取得相应数据。 为了解决这个问题,NT把所有相关的数据都复制到一个内存映像文件中,所有进程都可以存取内存映像文件。然后把这个内存映像文件的句柄和一个进程ID作为消息参数发送出去。而接收方则可以从内存映像文件中提取数据,在NT中,每当调用SHChangeNotifyRegister时,系统都会创建一个隐藏的代理窗口,它负责接收保存在内存映像中的通知消息,它的消息处理函数可以提取所有的信息,然后才把正确的消息和相关数据发送到注册的窗口。 很显然,这样做的效率是不高的,因为多了一个中间的转换步骤,解决效率的问题的办法就是设定SHCNF_NO_PROXY 标识,在调用SHChangNotifyRegister时指定了这一标识,Windows NT就不会创建代理窗口,这时内存映射句柄就会被直接发送给注册的窗口,但这时就要自己负责从映像文件中提取信息了,幸运的是,有两个函数可以完成这项任务: SHChangeNotification_Lock 和 SHChangeNotification_ Unlock。其中SHChangeNotification_Lock函数在Shell32.dll中的索引值为644,函数声明如下: function SHChangeNotification_Lock(MemoryMap: THandle; ProcessID: DWORD; var PIDLs: PTwoPIDLArray; var EventID: ULONG): THandle; stdcall; MemoryMap 就是系统创建的内存映像文件句柄,这个句柄对应于通知消息的WParam 参数中。ProcessID 是指生成内存映像的进程ID号,它对应于通知消息的LParam 参数。PIDLs 参数是一个输出变量,会返回一个TTwoPIDLArray记录类型的指针,在调用函数前应该把它赋值为nil, 当函数成功返回后,指针指向的TTwoPIDLArray记录中会包含两个与通知消息相关的PIDL。EventID 参数也是一个输出变量,在调用前可以设其为0,当函数返回后,变量会包含消息对应的事件ID。 最后函数调用成功的话会返回一个内存映像的句柄,需要保存它,因为后面还会用到,如果函数调用失败,则会返回0。 当处理完内存映像中的数据后,就需要解锁内存映像,以便系统可以正确地处理它,这可以通过SHChangeNotification_Unlock 函数来实现。SHChange-Notification_Unlock 的索引值为645,函数定义如下: function SHChangeNotification_Unlock(Lock: THandle):BOOL; stdcall; 其中Lock 参数应该设定为SHChangeNotification_Lock函数返回的内存映像句柄,调用成功的话会返回True,失败的话会返回 False。 需要注意的是,这几个函数只在Windows NT系统中定义了,不能在Windows 95下调用。因此不应该使用external关键字来静态连接,除非我们确定程序只在Windows NT下运行,否则应该使用动态连接,下面就是一个动态连接的例子: var PIDLs: PTwoPIDLArray; EventId: DWORD; Lock: THandle; begin //如果是在NT中,使用内存映射获得PIDL数据 if (SysUtils.Win32Platform = VER_PLATFORM_WIN32_NT) then begin Lock := SHChangeNotification_Lock(THandle( TheMessage.wParam), DWORD(TheMessage.lParam), PIDLs, EventId); if (Lock <> 0) then try ProcessEvent(EventId, PIDLs); finally SHChangeNotification_Unlock(Lock); end; end else // 如果不是NT的话,直接获取PIDL数据 begin EventId := DWORD(TheMessage.lParam); PIDLs := PTwoPIDLArray(TheMessage.wParam); ProcessEvent(EventId , PIDLs); end; end; 事件的源头 我们知道如何获得消息通知后,还想知道这些通知消息是如何产生的?根据Windows 文档,一个应用程序如果改变了外壳对象的话,应该使用SHChangeNotify函数通知外壳发生的变化。另外,通常外壳自己会产生绝大多数的的通知消息,而无需我们直接发出通知。 还要注意的是,通知消息有时不是很可靠,同时外壳变化事件和通知消息的产生之间会有明显的延迟,外壳本身也只维护一个10个项目的事件缓冲区,当事件溢出时,会合并产生一个SHCNE_UPDATEDIR通知消息,因此,对于关键任务程序来说,不要依赖于这些通知消息。 下面将介绍具体的事件发送情况,当打印机的状态变化时,将会引发SHCNE_ATTRIBUTES 事件,而改变文件或目录属性时会产生SHCNE_UPDATEITEM事件。 SHCNE_NETSHARE 和SHCNE_NETUNSHARE 事件会在共享或取消共享时引发,但在Windows NT上, SHCNE_NETUNSHARE事件不会被引发。看起来SHCNE_UPDATEIMAGE事件是用来指示系统图像列表中某个图像发生变化时会被触发,但实际上,这个事件当外壳对象所使用的系统图像列表中特定的图标索引有了变化时才被引发。典型的例子就是回收站,当空的和充满时图标会被切换,这时会引发这个事件。而当文件关联发生变化导致文档图标变化时,不会产生SHCNE_UPDATEIMAGE事件,而是生成SHCNE_ASSOCCHANGED 事件。 当插入或拿出软盘时并不会产生SHCNE_MEDIAINSERTED和SHCNE_MEDIAREMOVED事件。如果我们把一个文件删除进回收站的话,这时并不会产生SHCNE_DELETE事件,真正发生的是SHCNE_RENAMEITEM事件,只有当清空回收站时才会真正引发SHCNE_DELETE事件,不过想想这其实很合理,当文件被删除进回收站时,只是它的路径名发生了变化,文件实际并没有被删除。某些事件会被多次激发,比如当我们删除一个文件时,会得到两个相同的通知消息。 功能组件化 对于这些凌乱的Windows API的调用细节来说,设计一个组件来对其封装以简化使用无疑是一个非常好的办法。首先要定义组件的public接口部分,组件的功能可以简单地描述为两部分:监视什么外壳事件?怎么监视外壳事件?比如设定要监控的事件类型是中断类型事件还是非中断类型事件?要监视的文件夹是否包含所有子目录?其中指定要监控的文件夹相对特殊一些,因为在Windows中是使用PIDL来唯一标识外壳对象的,但在设计时使用PIDL来标识文件夹是不实际的,因为PIDL是不透明的数据类型,无法由开发者手工编辑。 解决办法是设计两个属性:一个是枚举类型的属性TkbSpecialLocation,它封装了多种常见外壳对象的标识常数,比如控制面板、打印机等。使用这些常数作为参数调用SHGetSpecialFolderLocation API函数就可以获得相应对象的PIDL。通过设定这个枚举类型属性,特殊类型外壳对象的选定就无需程序员输入PIDL数据。TkbSpecialLocation 一个枚举值为kbslPath。设定这个值将会使第二个属性生效,以允许程序员输入一个要监控的特定的系统路径名,下面就是最终的published属性列表: property Active: Boolean property HandledEvents: TkbShellNotifyEventTypes property InterruptOptions: TkbInterruptOptions property RootFolder: TkbSpecialLocation property RootPath: TFileName property WatchChildren: Boolean; 对于那些喜欢控制一切的程序员,组件还提供了一些运行属性供API级别的监控。一个就是通知消息句柄,一个是根节点的PIDL。下面是其定义: property Handle: THandle property RootPIDL: PItemIDList 因为组件封装了通知消息机制,因此监控事件是组件功能的核心。为了方便应用,这里我们解析了所有事件数据,每个事件都包含一个Sender 参数,和一个布尔值来确定事件是否是由事件产生的。不同的事件有不同的定义形式,下面是5种不同的事件定义: (1)没有更多的参数了。 (2)一个DWORD参数。 (3)一个非nil PIDL,代表外壳对象路径。 (4)两个非nil PIDL,代表代码路径。 (5)一般事件有两个PIDL参数,都可以为nil。 下面就是5种事件定义的实例: TkbShellNotifySimpleEvent = procedure(Sender: TObject; IsInterrupt: Boolean) of object; TkbShellNotifyIndexEvent = procedure(Sender: TObject; Index: LongInt; IsInterrupt: Boolean) of object; TkbShellNotifyGeneralEvent = procedure(Sender: TObject; PIDL: Pointer; Path: TFileName; IsInterrupt: Boolean) of object; TkbShellNotifyRenameEvent = procedure(Sender: TObject; OldPIDL: Pointer; OldPath: TFileName; NewPIDL: Pointer; NewPath: TFileName; IsInterrupt: Boolean) of object; TkbShellNotifyGenericEvent = procedure(Sender: TObject; EventType: TkbShellNotifyEventType; PIDL1: Pointer; PIDL2: Pointer; IsInterrupt: Boolean) of object; 同时我们要为每个外壳事件生成一个对应的Delphi控件事件,下面就是相应的组件事件: property OnAnyEvent: TkbShellNotifyGenericEvent property OnDiskEvent: TkbShellNotifyGenericEvent property OnGlobalEvent: TkbShellNotifyGenericEvent property OnAssociationChanged: TkbShellNotifySimpleEvent property OnAttributesChanged: TkbShellNotifyGeneralEvent property OnDriveAdded: TkbShellNotifyGeneralEvent property OnDriveRemoved: TkbShellNotifyGeneralEvent property OnExtendedEvent: TkbShellNotifyGenericEvent property OnFolderCreated: TkbShellNotifyGeneralEvent property OnFolderDeleted: TkbShellNotifyGeneralEvent property OnFolderRenamed: TkbShellNotifyRenameEvent property OnFolderUpdated: TkbShellNotifyGeneralEvent property OnFreespaceChanged: TkbShellNotifyGeneralEvent property OnImageUpdated: TkbShellNotifyIndexEvent property OnItemCreated: TkbShellNotifyGeneralEvent property OnItemDeleted: TkbShellNotifyGeneralEvent property OnItemRenamed: TkbShellNotifyRenameEvent property OnItemUpdated: TkbShellNotifyGeneralEvent property OnMediaInserted: TkbShellNotifyGeneralEvent property OnMediaRemoved: TkbShellNotifyGeneralEvent property OnNetworkDriveAdded: TkbShellNotifyGeneralEvent property OnResourceShared: TkbShellNotifyGeneralEvent property OnResourceUnshared: TkbShellNotifyGeneralEvent property OnServerDisconnected: TkbShellNotifyGeneralEvent 另外,组件还提供了一些运行方法来设定组件监控功能和某种重置功能,下面就是相应的方法定义: procedure Activate; procedure Deactivate; procedure Reset; 实现外壳通知消息的监控主要围绕着对SHChangeNotifyRegister和 SHChangeNotifyDeregister调用来说。同调用直接相关的属性是Active,设定它为True将会激活SHChangeNotifyRegister 的调用;设定为False则会调用SHChangeNotifyDeregister 来终止接收通知消息。 调用API是通过两个私有方法StartWatching和StopWatching来完成的,它们将在后面讨论,下面的一段代码是从SetActive方法中提取出来的,可以演示一下基本的逻辑过程: // 如果属性值没变化,什么也不做 if (NewValue <> Self.FActive) then { 如果为True,就开始监控} if (NewValue) then Self.StartWatching; else{ 否则停止监控} Self.StopWatching; 其他4个属性可以用来设定通知的监控模式,当Active为True的时候没有办法在运行时来更新监控模式,所以这里提供了一个Reset方法来重新设置外壳通知的监控机制。它只是调用StopWatching 和StartWatching方法来先终止通知注册,然后再根据修改了的属性重新注册接收通知事件。 接下来,就是实现一个窗口消息处理过程来处理接收到的通知消息了,Delphi VCL 提供了几个函数来简化这方面的设计,这就是Forms 单元中的AllocateHWnd、DeallocateHWnd方法。AllocateHWnd可以使用我们定义的消息处理过程生成一个非可见的窗体来接收通知消息。下面就是它的用法示意: {分配一个消息处理窗口} Self.FMessageWindow := AllocateHWnd(Self.HandleMessage); 注意最重要的是要提供一个消息处理过程,AllocateHWnd 只需要一个TWndMethod类型的参数,其原型是有一个Tmessage类型参数的类成员过程。下面是我们组件的消息处理过程: procedure TkbShellNotify.HandleMessage( var TheMessage: TMessage); var PIDLs: PTwoPIDLArray; EventId: DWORD; Lock: THandle; begin { 只处理WM_SHELLNOTIFY消息 } if (TheMessage.Msg = WM_SHELLNOTIFY) then begin { 如果是在NT中,使用内存映像来获得PIDL 数据} if SysUtils.Win32Platform=VER_PLATFORM_WIN32_NT then begin Lock := SHChangeNotification_Lock(THandle( TheMessage.wParam), DWORD(TheMessage.lParam), PIDLs, EventId); if (Lock <> 0) then try Self.ProcessEvent(EventId, PIDLs); finally SHChangeNotification_Unlock(Lock); end; end { 如果不在NT下, 直接获得PIDL数据 } else begin EventId := DWORD(TheMessage.lParam); PIDLs := PTwoPIDLArray(TheMessage.wParam); Self.ProcessEvent(EventID, PIDLs); end; end { if } { 调用缺省的windows消息处理过程来处理其他的消息} else TheMessage.Result := DefWindowProc(Self.FMessageWindow, TheMessage.Msg,TheMessage.wParam,TheMessage.lParam); end; 因为我们只对外壳通知消息感兴趣,因此要先判断接收到的消息是否是WM_SHELLNOTIFY消息,使用缺省的过程来处理其他消息。如果确定是一个通知消息,就解码LParam和Wparam来确定事件类型和相关的PIDL数据。其中具体的处理部分是通过ProcessEvent过程来实现的,在ProcessEvent 过程中,我们要从TTwoPIDLArray记录中获得PIDL,如果属于文件系统路径,还要进行相应的转化,最后把它们分发到合适的事件处理过程中。下面是其具体的实现代码: procedure TkbShellNotify.ProcessEvent(EventID: DWORD; PIDLs: PTwoPIDLArray); var EventType: TkbShellNotifyEventType; PIDL1: PItemIDList; PIDL2: PItemIDList; Path1: TFileName; Path2: TFileName; IsInterrupt: Boolean; begin { 从记录中获得两个PIDL} PIDL1 := PIDLs.PIDL1; PIDL2 := PIDLs.PIDL2; { 把PIDLs转化为路径名} Path1 := GetPathFromPIDL(PIDL1); Path2 := GetPathFromPIDL(PIDL2); { 确定事件是否是由中断引起的 } IsInterrupt := Boolean(EventID and SHCNE_INTERRUPT); { 遍历可能的事件类型并激活之} for EventType := Low(TkbShellNotifyEventType) to High(TkbShellNotifyEventType) do begin { 跳过多重事件类型} if (EventType in [kbsnAnyEvent, kbsnDiskEvent, kbsnGlobalEvent]) then Continue; {如果符合当前事件类型... } if ((ShellNotifyEnumToConst(EventType) and EventID) <> 0) then begin { 调用合适的多重事件类型 } Self.AnyEvent(EventType, PIDL1, PIDL2, IsInterrupt); if ((ShellNotifyEnumToConst(kbsnGlobalEvent) and ShellNotifyEnumToConst(EventType)) <> 0) then Self.GlobalEvent(EventType, PIDL1, PIDL2, IsInterrupt); if ((ShellNotifyEnumToConst(kbsnDiskEvent) and ShellNotifyEnumToConst(EventType)) <> 0) then Self.DiskEvent(EventType,PIDL1,PIDL2,IsInterrupt); { 激活特定的事件} case (EventType) of kbsnAssociationChanged: Self.AssociationChanged(IsInterrupt); { 其他事件处理方法... } kbsnServerDisconnected: Self.ServerDisconnected(PIDL1,Path1,IsInterrupt); end; { case } end; { if } end; { for } end; 核心过程 在所有辅助任务完成后,我们就要进行核心调用SHChangeNotifyRegister了,它在StartWatching 方法中被调用,调有程序如下: procedure TkbShellNotify.StartWatching; var NotifyPathData: TNotifyRegister; Flags: DWORD; EventType: TkbShellNotifyEventType; EventMask: DWORD; begin { 初始化标识} Flags := SHCNF_NO_PROXY; if kbioAcceptInterrupts in Self.InterruptOptions then Flags := Flags or SHCNF_ACCEPT_INTERRUPTS; if kbioAcceptNonInterrupts in Self.InterruptOptions then Flags := Flags or SHCNF_ACCEPT_NON_INTERRUPTS; { 初始化事件掩码} EventMask := 0; for EventType := Low(TkbShellNotifyEventType) to High(TkbShellNotifyEventType) do if (EventType in Self.HandledEvents) then EventMask := EventMask or ShellNotifyEnumToConst(EventType); {初始化路径信息} NotifyPathData.pidlPath := Self.RootPIDL; NotifyPathData.bWatchSubtree := Self.WatchChildren; { 注册通知接收窗口并保存返回的句柄 } Self.FHandle := SHChangeNotifyRegister( Self.FMessageWindow, Flags, EventMask, WM_SHELLNOTIFY, 1, NotifyPathData); { 如果注册失败,设定Active为False. } if (Self.Handle = 0) then Self.Deactivate; end; 初始化标识时,我们总是指定SHCNF_NO_PROXY,因为这里要直接在NT上处理消息,同时还要设定SHCNF_ACCEPT_INTERRUPTS和SHCNF_ACCEPT_NON_ INTERRUPTS 标识,它们是由InterruptOptions 属性确定的。然后初始化EventMask参数以确定监控那些感兴趣的外壳事件。最后还要设定文件夹路径信息,只要把RootPIDL和WatchChildren属性简单地填充到TnotifyRegister记录中就可以了。 StopWatching 方法则要简单得多,只要把保存好的通知消息句柄作为参数调用SHChangeNotifyDeregister 函数就可以了,下面是其实现程序: procedure TkbShellNotify.StopWatching; begin {撤销通知消息注册} SHChangeNotifyDeregister(Self.FHandle); Self.FHandle := 0; end; 有了这个组件后,我们就可以忘记所有那些令人厌烦的琐碎的API调用了,同时仍然能够享受到对外壳内部活动窥视的强大功能。 |