用VB写监控程序

去年上半年发表在QQ空间里的。先移过来再讲

仅仅是用户模式下的监控,没什么意义的,另外很多的东西也都是网上可以查到的。不过大家看看倒也无妨

前段日子,一同事发一条信息问WO关于监控软件的事。说一个网友需要这方面的资料,在这之前
曾经在网上查过相关的资料,并用VC(MFC)写了一个简单的监控程序,但因没有保存。重做系统后就没了
(因为当时只是实验,所以将MFC项目的Folder直接默认的建在了C盘).故当时就只是简单的回他
说使用 SHChangeNotifyRegister API就可以了。事后,突然觉得应该把这个写成组件
那么以后就不需要重复的去写这些东西了。本来准备用VC来写,因为需要大量API及其声明。
但却因为我不会用VC来写ACTIVEX.只好选用较熟悉的 V B

WINDOWS要实现监控文件夹的方法不止一种。
你可以使用FindFirstChangeNotification,再结合WaitForMultipleObjects,和FindNextChangeNotification
实现。
也可以使用本文介绍DE SHChangeNotifyRegister 来实现。当然我更喜欢用 SHChangeNotifyRegister
SHChangeNotifyRegister 是整个监控文件夹的核心之所在,所以在往HA介绍之前,得先搞明白其具体内容
SHChangeNotifyRegisterMSDN上是这样声明的。
ULONG SHChangeNotifyRegister(HWND hwnd,
int fSources,
LONG fEvents,
UINT wMsg,
int cEntries,
SHChangeNotifyEntry *pfsne
);
第一个参数就不用详细说明了,老鬼都知道这是一个窗口句柄,指明在捕获到改变的时候将消息发到的窗体
第二个参数MSDN上讲的好象有问题(当然也有可能我理解上有问题)。
而第三个参数MSDN上讲的就给放屁一样,所以排除MSDN上重新进行讲一HA。
其中第二个参数是指明返回到窗体处理函数中的WPARAM所指向的地址处所存的数据格式
可以是SHCNF_IDLIST(0x0000)// LPITEMIDLIST
也可以SHCNF_PATHA (0x0001)// path name
如果是SHCNF_IDLIST 则WPARAM所指向的地址处存的是LPITEMIDLIST(windows的名字空间ID)
如果是SHCNF_PATHA则WPARAM所指向的地址处存的是路径.
当然这里路径很好理解就是我们通常见到的形如"C:/abc"这样的字符串。但名字空间又是个什么东西呢。
名字空间是windows里引入的目录管理概念。和路径的作用是一样的。当然你会问,和路径的作用一样
那要它做什么。用路径不就行了。当然微软不是SB.搞一个这东西出来,自然有他的作用。虽然路径是
我们常见的,但事实上windows并不使用路径来管理树状目录。使用路径只是为了兼容DOS下的习惯,另外
看着也方便,理解也容易。真正用于管理windows树状目录的是名字空间。
用过电脑的都知道。想用路径来访问windows 里的所有目录那基本上肯定是不可能的。
至少你导死都访问不到控制面板或者网上领居或者回收站这样的目录。
但事实上你却能够访问到他们,那么用路径访问不到,那他是用什么访问的呢。
当然那就是名字空间。
讲了这么多,那名字空间究竟是什么呢。其实很简单。名字空间就是名字的空间,是一个windows下所有
目录及文件的名字及约束(规则)的集合。
windows里的每个目录和文件,都有一个唯一的名字。这个名字不象路径那样容易记忆和理解,这个名字是
一个32位的数。
当然为了讲解方便,我们还要使用路径这个概念.
例如c:/abc/text/test.txt这样一个路径。所表示的信息有。
"c:/abc/text/test.txt" 是 "test.txt" 这个文件的全路径。而test.txt 是这个文件的名字
其中"c:/abc" 是文件夹"abc"的全路径(注意这里讲的是文件夹而不是目录--文件夹是路径体系的概念,而目录
是名字空间里的概念,当然我们会经常混用,这无所谓,不防碍理解)
abc 是这个文件夹的名字 "c:/abc/text" 是文件夹"text"的全路径,而text是这个s文件夹的名字.
其中text又是abc的子文件夹。test.txt是放在text文件夹里的。
C:则是一个盘符名
以上的概念应该都能明白,如果不明白的话,建设你去撞墙。
ha面我们开始一一对应。
其中C:在名字空间里的表示为FEAB2346(当然这是一个假设,真实中可能是别的)
同理abc也可以在名字空间里有个表示EA123456.
继续对应text->AAAA3DAB
test.txt->12345678
那么为了方便看全写在ha面
c: -> FEAB2346
abc-> EA123456
text -> AAAA3DAB
test.txt -> 12345678
从这个对应上可以很明白的推出一点。在名字空间里,如果想访问C盘就得使用FEAB2346(这是不对的
但为了理解,这里估且这样认为,后面会有说明)
那么如果想访问c:/abc应该怎么办呢.其实是一样的,路径体系里可以使用一个/来分隔表示
树状结构。在名字空间就更加方便.
直接把abc的ID附在C:ID的后面就可以了。所以
c:/abc 在名字空间里的全地址是
FEAB2346
EA123456
同理c:/abc/text 就是
FEAB2346
EA123456
AAAA3DAB
其它一样,就不继续推了,当然要明白一点的是,也是刚才说过的。名字空间和路径不是一一对应的。
名字空间能够访问的范围更大,而且还有一点要明确。路径是以盘符为根的。而名字空间的根为桌面。
其结构如ha.
桌面
--回收站
--网上邻居
--控制面板
.........
--C:
----abc
------text
--------test.txt
.........
--n:
现在应该知道刚才为什么说访问C盘的全地址用FEAB2346是不以的了吧。真正的全地址hai
应该在其前面再加一个桌面。

当然看了上面的说明,你会有种想打人的冲动.没办法,WO的表达能力就这 水平。
不过想你继续往HA看会再回过头来看看,慢慢就会明白的。
第三个参数是指要捕获的事件类型,这里我们捕获SHCNE_ALLEVENTS(0x7FFFFFFF) Or SHCNE_INTERRUPT(0x7FFFFFFF) 所有的事件。
第四个消息是指明要发送给窗体的消息,当然这个消息要自定义
这里我们把它定义为WM_SHNOTIFY = &H401
虽然在这里我是这样写的,但推荐使用 WM_*** = WM_USER+n的方法.
第五个参数是个整型数,和第六个一起使用.
为了更好讲解,先介绍第六个参数。
第六个参数是一个结构体的指针,先看其定义
typedef struct {
LPCITEMIDLIST pidl;
BOOL fRecursive;
} SHChangeNotifyEntry
从名字上就可以看出来,这是一个入口结构。 是说明我们要监控那个目录用的。
他的第一个元素是LPCITEMIDLIST类型的pidl,不要被这么长的名字吓到了。其实
很简单,这就是一个指针,指向就是前面我们讲到的名字空间ID的指针。
第二个元素说明是否监控子目录,TRUE的时候监控。否则不监控.
ha面讲第五个参数,因为说明了第六个,第五个就很好说明了。其实第五个参数就是说明。
第六个参数的数组个数的,看到这里可能你们有点糊涂。第六个参数不是一个结构体吗,
怎么成了数组了。在此声明一ha。从来没讲过第六个参数是一个结构体。因为一开始就讲
第六个参数是一个结构体的指针。当然这个描述不太准确的,在这里准确的说应该是一个
结构体数组的指针。
好了。几个参数已经讲完了,当然以我的表达能力能看懂的应该不是很多。所以现在再通盘的
描述一ha。
ULONG SHChangeNotifyRegister(HWND hwnd,
SHChangeNotifyEntry *pfsne
);
其实这个函数的作用,就是监控 由cEntries和pfsne所表明的一个目录(文件)或者一组目录(文件) 的变更。
当这些目录发生了fEvents所描述的事件范围内的事件的话,就将一个wMsg消息放到 窗体 hwnd
的消息队队列里。
好了核心函数到此已经介绍完了,继续开始往ha走。
------------------------------------------------------
当然只知道这个函数是无法继续前行的。 如果你真正理解了上面的函数。你就会发现目前有几件必须解决的事。 第一件就是要实现监控一个目录。从第六个参数来看我们不能传一个路径进去,而必须传一个 名字空间的指针。那么这个应该怎么获得。 第二个是在VB里无法象SDK里那样直接写消息循环。那么我们应该怎么去获得我们自定义的那个消息。问题提出来后,一个个解决。 首先第一个。 实现第一个的目的,方法也有很多,可以使用 SHGetSpecialFolderLocation得到桌面的pidl 然后再一层层枚举。直到找到我们要监控的文件夹。 当然这样是相当耗费资源的,基本上不会使用这种方法。 所以在这里选用这二种方法。使用 IShellFolder. 因为 IShellFolder 有一个方法 ParseDisplayName 可以很方便的将路径转成pidl,当然那些特殊 目录的pidl我们再使用 SHGetSpecialFolderLocation 来获得,这样就可以实现任何目录的监控了。 IShellFolder 是一个 COM对象接口。因为VB本身没有对COM底层开发的一些支持。所以在这里我们 需要一个.tlb文件来让VB知道IShellFolder接口的实现的布局。 ha在是VB的源程序。在这里我把他放在了一个方法里。 Public Function getPIDL(path) As Long Dim isf As IShellFolder Dim temp As Long If SUCCEEDED(SHGetDesktopFolder(temp)) Then 'SUCCEEDED是一个方法,判断是否成功 MoveMemory isf,temp,4 End If Dim pathlen As Long Dim pidl As Long isf.ParseDisplayName vbNull,StrConv(path,vbUnicode),pathlen,pidl,vbNull getPIDL = pidl isf.Release 'EnumPIDL isf,1,desktoppidl End Function 上面的过程里出现了,两个没有见过的API函数。 SHGetDesktopFolder MoveMemory 这两个函数在MSDN上的声明如ha. HRESULT SHGetDesktopFolder( IShellFolder **ppshf ); 此函数接收一个存放桌面的IShellFolder接口的实例指针的地址。 void MoveMemory( PVOID Destination,const VOID* Source,SIZE_T Length ); 此函数用于从内存中移动数据。 其中Destination参数为要移动至的地址 Source为源地址 Length 为要移到的字节数 虽然从这个声明看,Destination和Source是不同的。其实我们看看PVOID的声明 typedef void *PVOID; 就知道,其实这两个是一样的。都是指向未知类型的指针(注意不是空指针),只是一个指向的 是常量而已。 看了上面的函数解释再结合程序就很容易明白上面程序的意思了。 Public Function getPIDL(path) As Long Dim isf As IShellFolder '定义一个IShellFolder对象(其实是一个指针-在VB里就是一个long值) Dim temp As Long '定义一个临时变量,用于存放返回的IShellFolder对象的指针的指针 If SUCCEEDED(SHGetDesktopFolder(temp)) Then 'SUCCEEDED是一个方法,判断是否成功 MoveMemory isf,4如果成功将IShellFolder的指针移到isf所表示的地址处. End If Dim pathlen As Long Dim pidl As Long isf.ParseDisplayName vbNull,vbNull '转换path为pidl 转 化后的pidl存在在变量pidl里。 getPIDL = pidl isf.Release '释放isf. 'EnumPIDL isf,desktoppidl End Function 讲上面的过程体转换为汇编后可能更容易理解他的含义。 当然下面的汇编只是一个伪代码,并且为了简化过程取消了SUCCEEDED的过程调用,假设。 SHGetDesktopFolder永远是成功的. isf dw ?//dim isf as IShellFolder temp dw ? //dim temp as Long pathlen dw ?//dim pathlen as Long pidl dw ? // dim pidl as Long mov eax,addr temp push eax call SHGetDesktopFolder //SHGetDesktopFolder(temp) //到此时,temp所对应的地址处存放的是IShellFolder对象的实例的指针(其实应该就是一个指向 vTable的指针--猜想) xor eax,eax mov eax,temp mov dword ptr isf,eax//MoveMeory isf,4 注:上一句其实并不是API MoveMemory的实现。 只是其中的一部分而已。 MoveMemory 是在其基础上加了地址冲突检查的。而汇编的MOV是不管这些的。 在这里不象上面一句一样使用 MASM 中的call API的形式,是为了更好的说明地址的移动哈。 ha面的语句因为直接转化为汇编可能需要一些假设,所以先说明一ha后再进行转化。 当然这些说明里所进行的一系列操作都是编译器(或解析器)做的工作, 并不会被生成到二进制文件里(PCODE的情况)。 先看一ha这一句 isf.ParseDisplayName vbNull,vbNull 当编译器(解释器)看到这句的时候。就会先查表。找到isf对应的类型为IShellFolder 然后 根据引用进来的tlb文件声明的接口布局。得到ParseDisplayName在类实例结构体里的偏移。这里 我们假设为n.然后再用isf里所指向的接口实例地址(这个地址是后期绑定的--通过SHGetDesktopFolder) 加上这个n,得到这个方法的虚拟地址。然后再直接call这个地址就可以了。 xor eax,isf push NULL push addr pidl push addr pathlen push addr path //此处省略了转换为Unicode编码的过程 push 0 push NULL call [eax+n] 注:微软的API,大部分采用的都是stdcall调用约定,右边的参数先入栈。恢复栈的工作在过程体内进行 能够通过路径得到pidl后,我们就可以完善我们刚才的程序了。 假如只想监控一个目录:假设为 c:/abc dim pidl as long pidl = getPIDL("c:/abc") dim entry as SHChangeNotifyEntry'当然要使用这样的结构体,你必须先在VB中声明这样一个结构体 一些公开函数所使用的结构直接用VB自带的API查看器就可以得到其结构的VB声明。 但SHChangeNotifyRegister是属于win 2000版本后才公开的函数。在VB6出来时,还属于非公开函数。 所以无法使用API查看器查看其VB声明。必须自已转化。转化很简单。在此不作介绍 entry.pidl = pidl entry.fRecursive = true//如果你不想监控子目录就设为false SHChangeNotifyRegister(hWnd,_ SHCNF_TYPE Or SHCNF_IDLIST,_ SHCNE_ALLEVENTS Or SHCNE_INTERRUPT,_ WM_SHNOTIFY,entry) 假如想监控两个或者更多。只须稍作变动 dim pidl as Long,pidl1 as Long pidl = getPIDL("c:/abc") pidl1= getPIDL("c:/bcd") dim entry(2) as SHChangeNotifyEntry entry(0).pidl = pidl entry(0).fRecursive = true//如果你不想监控子目录就设为false entry(1).pidl = pidl1 entry(1).fRecursive = true//如果你不想监控子目录就设为false SHChangeNotifyRegister(hWnd,2,entry(0)) 上面的hWnd。很好获得 在VB里 任何一个窗体类里用 Me.hWnd 就可以了 通过上面的方法,我们就可以实现当c:/abc c:/bcd里发生改变的时候就会将一个WM_SHNOTIFY的消息 发送到hWnd这个窗体的消息队列里。 当然通过第三个参数可以设定捕获事件的范围,因为我们这里设为SHCNE_ALLEVENTS Or SHCNE_INTERRUPT 所以可以捕获所有事件。 那么ha面的事就是处理这个消息了。于是我们必须着手去解决一开始提到第二个问题了

相关文章

Format[$] ( expr [ , fmt ] ) format 返回变体型 format$ 强...
VB6或者ASP 格式化时间为 MM/dd/yyyy 格式,竟然没有好的办...
在项目中添加如下代码:新建窗口来显示异常信息。 Namespace...
转了这一篇文章,原来一直想用C#做k3的插件开发,vb没有C#用...
Sub 分列() ‘以空格为分隔符,连续空格只算1个。对所选...
  窗体代码 1 Private Sub Text1_OLEDragDrop(Data As Dat...