3. QDir 与目录遍历:递归遍历、过滤器与文件监听

目录操作,说白了就是 Qt 里最基础也最容易踩坑的地方。我刚开始用 QDir 的时候,觉得这玩意儿不就是个文件夹浏览嘛,结果真到项目里,递归遍历卡死、过滤器漏文件、监听器不触发……各种问题全来了。今天咱们就把这些坑一个一个填上。

3.1 QDir 核心操作:别只会用 entryList

很多人一上来就调 entryList(),拿个 QStringList 就走。但 QDir 远不止这么简单。我习惯先搞清楚几个关键属性:

属性 作用 我常用的值
setPath() 设置目录路径 绝对路径,避免相对路径歧义
setFilter() 过滤条目类型 QDir::Files | QDir::NoDotAndDotDot
setNameFilters() 按文件名模式过滤 {"*.txt", "*.log"}
setSorting() 排序方式 QDir::Name | QDir::DirsFirst
我的习惯:每次创建 QDir 对象后,先调 exists() 检查路径是否有效。曾经有个项目,路径字符串末尾多了个空格,查了我两个小时。

3.2 递归遍历目录:别用递归函数硬写

递归遍历目录,最直接的想法就是写个递归函数,碰到子目录就调自己。但这样做有两个问题:一是深度太大会栈溢出,二是代码不好维护。

我推荐用 QDirIterator,它自带递归模式:

QDirIterator it("/path/to/dir", QDir::Files, QDirIterator::Subdirectories);
while (it.hasNext()) {
    QString filePath = it.next();
    // 处理文件
    qDebug() << filePath;
}

你看,一行 QDirIterator::Subdirectories 就搞定了递归。内部用的是迭代器模式,不会爆栈。我在一个嵌入式项目里遍历过 10 万级文件目录,稳得很。

注意:QDirIterator 默认不排序。如果你需要按文件名顺序处理,得自己收集到 QList 里再 sort。我吃过这个亏——日志文件按时间命名,结果遍历顺序是乱的。

3.3 过滤器:别漏文件也别多文件

过滤器的坑,主要出在 QDir::Filters 和 QDir::NameFilters 的配合上。很多人只设了 nameFilters,忘了设 filter,结果把目录也列出来了。

我总结了一套「黄金组合」:

QDir dir("/data/logs");
dir.setFilter(QDir::Files | QDir::NoDotAndDotDot);
dir.setNameFilters({"*.log", "*.txt"});
QFileInfoList list = dir.entryInfoList();

这样只返回 .log 和 .txt 文件,不包含目录,也不包含 . 和 ..。

避坑指南:我曾经在 Windows 上遇到过滤器不区分大小写的问题。Qt 默认是平台相关的,Windows 上 "*.LOG" 能匹配到 .log 文件,Linux 上不行。跨平台项目记得统一用小写,或者用 QDir::CaseInsensitive。

3.4 QFileSystemWatcher:监听文件变化

文件监听,说白了就是让程序「盯着」某个文件或目录,变了就通知你。QFileSystemWatcher 用起来很简单:

QFileSystemWatcher *watcher = new QFileSystemWatcher(this);
watcher->addPath("/path/to/watch");
connect(watcher, &QFileSystemWatcher::fileChanged,
        this, [](const QString &path) {
    qDebug() << "文件变了:" << path;
});

但这里有个大坑:QFileSystemWatcher 在文件被替换时可能丢失监听。比如编辑器保存文件时,先删除再创建,watcher 就失效了。

我曾经踩过的坑:监控一个配置文件,用户用 vim 修改保存后,watcher 再也不触发了。后来发现 vim 的保存方式是「写新文件-删旧文件-重命名」,watcher 监听的是旧文件的 inode,新文件它不认识。

解决方案有两个:

  • 方案一:监听目录而不是文件,用 directoryChanged 信号,然后自己检查文件列表变化。
  • 方案二:在 fileChanged 触发后,重新 addPath() 一次。

我个人倾向方案一,更可靠:

watcher->addPath("/path/to/dir");
connect(watcher, &QFileSystemWatcher::directoryChanged,
        this, [](const QString &dirPath) {
    QDir dir(dirPath);
    if (dir.exists("config.ini")) {
        // 重新加载配置
    }
});

3.5 知识结构图:目录操作全景

下面这张图把 QDir、QDirIterator、QFileSystemWatcher 的关系和典型用法串起来了。你想想看,从「创建目录对象」到「遍历文件」再到「监听变化」,其实是一条完整的数据流。

QDir 目录操作全景图 QDir 对象 entryList() / entryInfoList() QDirIterator 递归遍历 QFileSystemWatcher 监听 setFilter() + setNameFilters() Subdirectories 模式 fileChanged / directoryChanged 关键提醒 • 优先用 QDirIterator 代替手写递归,避免栈溢出 • QFileSystemWatcher 监听文件时,注意文件替换导致监听失效

3.6 实战建议:目录操作的「三板斧」

最后,我把自己在项目中沉淀下来的三个习惯分享给你:

  1. 路径统一用正斜杠:Qt 内部会处理平台差异,但你自己写路径时,统一用 "/",别用 "\"。Windows 上也支持。
  2. 善用 QFileInfo:entryInfoList() 比 entryList() 好用得多,能直接拿到大小、修改时间、权限等信息,省得你再 stat 一次。
  3. 监听目录比监听文件靠谱:如前面所说,QFileSystemWatcher 监听目录的 directoryChanged 信号,然后自己判断哪个文件变了,这样最稳定。
一句话总结:QDir 是入口,QDirIterator 是遍历利器,QFileSystemWatcher 是监控工具。三者配合,目录操作基本全覆盖。