最近在做虚拟打印机时,需要实时监控打印文件的到达,并移动文件到另外的位置。一开始我使用了线程,在线程里去检测新文件的到达。实际上Windows提供了一个文件监控接口函数ReadDIrectoryChangesW。这个函数可以对所有文件操作进行监控。
ReadDirectoryChangesW
函数声明
1 BOOL ReadDirectoryChangesW( 2 [in] HANDLE hDirectory, 3 [out] LPVOID lpBuffer, 4 [in] DWORD nBufferLength, 5 [in] BOOL bWatchSubtree, 6 [in] DWORD dwNotifyFilter, 7 [out, optional] LPDWORD lpBytesReturned, 8 [in, out, optional] LPOVERLAPPED lpOverlapped, 9 [in, optional] LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine 10 );
参数
hDirectory
指向要监听的目录句柄,必须使用FILE_LIST_DIRECTORY访问权限打开此目录。
lpBuffer
指向要读取DWORD对齐结果的格式化缓冲区的指针。该缓冲区的结构由FILE_NOTIFY_INFORMATION结构定义
nBufferLength
lpBuffer参数指向的缓冲区大小
bWatchSubtree
指示是否以指定目录为根目录进行监控。
True:监视以指定目录为根的目录树
FALSE:仅监视指定目录
dwNotifyFilter
检查函数以确定等待操作是否满足过滤条件。此参数可以指定为以下的一个或多个
值 | 含义 |
FILE_NOTIFY_CHANGE_FILE_NAME | 监视目录或子树中的任何文件名更改(包括重命名、创建、删除文件) |
FILE_NOTIFY_CHANGE_DIR_NAME | 监视目录或子树中的任何目录名更改(包括重命名、创建、删除目录) |
FILE_NOTIFY_CHANGE_ATTRIBUTES | 监视目录或子树中的任何属性更改 |
FILE_NOTIFY_CHANGE_SIZE | 监视目录或子树中的任何文件大小更改(仅当文件写入磁盘时,操作系统才能检测到文件大小的更改) |
FILE_NOTIFY_CHANGE_LAST_WRITE | 监视目录或子树中文件上次写入时间的任何更改(只有文件写入磁盘时,操作系统才会检测到最后写入时间的更改) |
FILE_NOTIFY_CHANGE_LAST_ACCESS | 监视目录或子树中文件最后访问时间的任何更改 |
FILE_NOTIFY_CHANGE_CREATION | 监视目录或子树中文件创建时间的任何更改 |
FILE_NOTIFY_CHANGE_SECURITY | 监视目录或子树中任何安全描述符更改 |
lpBytesReturned
对于同步调用,此参数接收传输到lpBuffer参数中的字节数
lpOverlapped
指向OVERLAPPED结构的指针,提供在异步操作期间要用的数据,否则该值为NULL
lpConpletionRoutine
指向完成例程的指针,当操作已经完成或取消,并且调用线程处于可警告的等待状态时才会调用它
返回值
成功:不为0
失败:0
监控指定目录下的文件创建
假设我这里要监控D:\PrintFiles目录下的文件更改,操作如下:
打开目录,获取文件句柄
// 打开目录, 获取文件句柄
HANDLE hDirectory = ::CreateFile(L"D:\\PrintFiles", FILE_LIST_DIRECTORY,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (INVALID_HANDLE_VALUE == hDirectory)
{
return 0;
}
调用ReadDirectoryChangesW监控文件夹
我这里只演示同步调用,异步调用可以参考MSDN文档。同步调用时,如果监控的目录未发生更改,会一直阻塞在那里。
DWORD dwRet = 0;
DWORD dwBufferSize = 2048;
BYTE* pBuf = new BYTE[dwBufferSize];
FILE_NOTIFY_INFORMATION* pFileNotifyInfo = (FILE_NOTIFY_INFORMATION*)pBuf;
BOOL bRet = ReadDirectoryChangesW(hDirectory,
pFileNotifyInfo,
dwBufferSize,
TRUE,
FILE_NOTIFY_CHANGE_FILE_NAME| //修改文件名
FILE_NOTIFY_CHANGE_ATTRIBUTES | // 修改文件属性
FILE_NOTIFY_CHANGE_LAST_WRITE, // 最后一次写入
&dwRet,
NULL, NULL);
输出结果
if (FALSE == bRet)
{
DWORD dwError = GetLastError();
std::cout << "ReadDirectoryChangesW failed - " << dwError << std::endl;
}
std::wcout.imbue(std::locale("chs"));
//判断操作类型
switch (pFileNotifyInfo->Action)
{
case FILE_ACTION_ADDED:
std::wcout << "Create file " << pFileNotifyInfo->FileName << std::endl;
break;
default:
break;
}
CloseHandle(hDirectory);
delete[] pBuf;
运行效果
System.IO.FileSystemWatcher类
在C#中,可以使用 System.IO.FileSystemWatcher类来进行监听。它内部也是调用了ReadDirectoryChangesW API函数
监控文件的行为定义在System.IO.NotifyFilters
1 [Flags] 2 public enum NotifyFilters 3 { 4 FileName = 0x1, 5 DirectoryName = 0x2, 6 Attributes = 0x4, 7 Size = 0x8, 8 LastWrite = 0x10, 9 LastAccess = 0x20, 10 CreationTime = 0x40, 11 Security = 0x100 12 }
在C#中是通过事件订阅的形式来进行通知的。
C#进行了再次封装,所以会比直接在C++中使用要方便一些。创建监听后,只需要等待事件触发就行了。
FileSystemWatcher使用方法如下:
FileSystemWatcher systemWatcher = new FileSystemWatcher();
systemWatcher.Path = this.textBox1.Text;
//设置监听的行为
//这里设置为文件名
systemWatcher.NotifyFilter = NotifyFilters.FileName;
//设置文件类型过滤
systemWatcher.Filter = "*.txt";
systemWatcher.Changed += (obj, args) => { ShowMsg($"文件更改{args.Name}"); };
systemWatcher.Created += (obj, args) => { ShowMsg($"文件创建{args.Name}"); };
systemWatcher.Deleted += (obj, args) => { ShowMsg($"文件删除{args.Name}"); };
systemWatcher.Renamed += (obj, args) => { ShowMsg($"文件重命名{args.Name}"); };
//开始监听
systemWatcher.EnableRaisingEvents = true;
运行效果
示例代码
参考资料:
ReadDirectoryChangesW function (winbase.h) - Win32 apps | Microsoft Learn