简介:基于Qt框架实现的文件搜索工具源码,面向需要学习桌面端文件检索、列表交互与文档编辑功能的C++/Qt开发者,也适合作为课程设计或毕业设计的参考项目。项目在常见文件搜索算法基础上进行了改良,支持遍历文件夹定位目标文件,并能将结果以列表展示,双击即可打开编辑.docx、.xlsx、.pptx、.accdb、.txt等常用格式,兼顾检索效率与实用体验。压缩包内含17个文件,以6个cpp源文件和5个h头文件为核心,配合pro工程文件、qrc资源文件、manifest配置以及应用图标等,工程结构完整,整体仅12KB,便于快速阅读与二次改造。已有3646人浏览学习。代码按功能模块拆分为文件搜索对话框、选项配置、后台线程、列表委托等部分,职责划分清晰,便于理解搜索逻辑与界面解耦的方式;同时附带uac.manifest、resource.rc等发布级配置文件,可作为一份完整Qt Widgets工程组织与资源管理的参考样例。
1. 用 Qt 实现文件搜索:先搞清“慢搜索”到底慢在哪
电脑里的文件越堆越多,每到找文件的时候,搜索就成了玄学:明明记得文件名,系统自带搜索却始终转圈。第三方工具又是一个黑匣子,结果排序、索引更新都不透明。于是很多开发者会选择用 Qt 自己写一个文件搜索工具,把目录遍历、过滤规则、界面交互全部握在手里。用 Qt 做文件搜索,核心不是“调用系统 API 返回结果”,而是把递归扫描、线程回传、结果模型这一整条链路搞清楚。这套工程正好覆盖了从 UI 到工作线程的完整流程,适合刚学完 C++ 基础、想在桌面端落地的开发者复现。后面我会把每一步的参数和坑一次说清。
2. 需求拆解与界面搭建:从关键字输入到结果表格
2.1 把“搜索”拆成三个动作:定位目录、过滤规则、结果排序
先别急着写代码。文件搜索看着简单,真正拖慢体验的通常是几个问题堆在一起:搜索范围选得太大、过滤逻辑反复访问磁盘、子线程不断向 UI 发信号。拆开之后,这个需求其实只有三件事。
第一件事是“定位目录”。用户输入的可能是绝对路径,也可能是相对路径,还可能是像~/Documents这样的带环境变量的路径。第二件事是“过滤规则”。这里要同时支持按文件名、按扩展名、按路径关键词、按大小范围,后期还可能要支持正则。第三件事是“结果展示”。搜索命中的信息要能在表格里排序、点击打开、复制路径。
我一般会把这三件事固化成一组参数结构,而不是散落在界面代码里。之所以用结构体,是因为子线程启动时要一次性把参数传给工作对象,避免传错字段。如果你去看这份资源里的源码,会发现所有界面控件都只负责读取和写入SearchParams,核心逻辑完全不碰界面。
| 模块 | 核心职责 | 界面侧的输入 |
|---|---|---|
| 目录定位 | 把用户输入变成标准化绝对路径 | QLineEdit,支持环境变量 |
| 过滤规则 | 名称、扩展名、大小、隐藏文件 | 关键字框、下拉框、SpinBox |
| 结果展示 | 排序、选中、打开文件所在目录 | QTableView+ 代理模型 |
这种拆法不仅是为了好看,更多是为了测试。过滤规则可以脱离界面单独验证,比如写一个.cpp文件作为测试样本,直接调用matchRule()跑断言。很多 Qt 项目翻车,都是把逻辑揉进按钮槽函数里,最后连输入框和扫描循环搅在一起,代码根本无法维护。
struct SearchParams { QString rootPath; // 起始目录 QString namePattern; // 文件名关键字或正则 bool caseSensitive = false; // 大小写敏感 bool useRegex = false; // 是否把输入当正则表达式 QStringList extensions; // 想过滤的扩展名,比如 {"cpp", "h"} qint64 minSize = 0; // 文件大小下限,单位字节 qint64 maxSize = -1; // 文件大小上限,-1 表示不限 bool includeHidden = false; // 是否扫描隐藏目录 };这段结构体的意义在于把“用户想要什么”和“底层怎么扫”解耦。rootPath为空时就回退到当前工作目录;maxSize用 -1 而不是 0,是为了和“最小为 0”区分开。后续界面上的输入控件直接绑定到这个结构体,工作线程也只认这个结构体,不会出现界面改了、线程里还拿着旧参数的情况。
2.2 用 QLineEdit + QTableView 搭最简搜索面板
界面部分我建议用 Qt Widgets,因为文件搜索是典型的桌面工具场景,QTableView配合QStandardItemModel比 QML 更容易写出高效率的分页和排序。这里先搭一个最小面板:顶部一行输入,中间结果表格,底部状态栏显示扫描进度。
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { QWidget *central = new QWidget(this); QHBoxLayout *topBar = new QHBoxLayout; m_editDir = new QLineEdit(QDir::homePath()); m_editKeyword = new QLineEdit; m_editKeyword->setPlaceholderText("输入文件名关键字"); QPushButton *btnSearch = new QPushButton("开始搜索"); connect(btnSearch, &QPushButton::clicked, this, &MainWindow::startSearch); topBar->addWidget(new QLabel("目录:")); topBar->addWidget(m_editDir, 2); topBar->addWidget(new QLabel("关键字:")); topBar->addWidget(m_editKeyword, 1); topBar->addWidget(btnSearch); m_table = new QTableView(this); m_table->setSelectionBehavior(QAbstractItemView::SelectRows); m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); m_model = new QStandardItemModel(this); m_model->setHorizontalHeaderLabels( {"文件名", "所在路径", "大小", "修改时间"}); m_table->setModel(m_model); m_table->setColumnWidth(0, 240); m_table->setColumnWidth(1, 360); QVBoxLayout *layout = new QVBoxLayout(central); layout->addLayout(topBar); layout->addWidget(m_table); setCentralWidget(central); statusBar()->showMessage("就绪"); }这里有几个参数值得圈出来。QLineEdit默认继承焦点事件,但我故意不设默认焦点,避免用户打开工具就触发一次无意义扫描。表格设置了NoEditTriggers,防止用户双击把文件名改掉。列宽先固定成 240 和 360,等结果多了之后再根据内容动态调整。状态栏用来显示“就绪”,后续搜索开始时改成“正在扫描...”,结束时再显示共找到多少文件。
注意界面上的“开始搜索”按钮没有直接调用扫描逻辑,而是触发了startSearch()。这个槽函数要做三件事:读取参数、校验路径、通知工作线程启动。如果直接把扫描放在按钮槽里,就会发现界面在扫大目录时完全卡住,这就是后面要讲的线程问题。
2.3 用 QSortFilterProxyModel 做结果排序与搜索词高亮
搜索结果一旦超过一千行,只靠QStandardItemModel自带的 sort 其实不太理想。QStandardItemModel的sort()会要求 Item 先实现setData,而且排序时整表重排,容易产生闪烁。更稳的做法是用QSortFilterProxyModel套在外面,界面只负责展示代理模型。
class SearchSortProxy : public QSortFilterProxyModel { public: explicit SearchSortProxy(QObject *parent = nullptr) : QSortFilterProxyModel(parent) {} protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const override { // 第 2 列是大小,需要按数字比较,不能按文本比较 if (left.column() == 2 && right.column() == 2) { return sourceModel()->data(left).toLongLong() < sourceModel()->data(right).toLongLong(); } return QSortFilterProxyModel::lessThan(left, right); } };注意第 2 列是字节数,如果直接按QString排序,"999"会排在"10000"后面。代理模型里的lessThan就是用来修正这种默认行为的。要让代理生效,需要m_table->setSortingEnabled(true),并设置QTableView::setModel(proxy)。这一句的代价是,你往源模型里增加行时,代理模型会自动转发到表格,不用手动刷新。
对于“搜索词高亮”,通常要配合自定义委托来实现。比如在paint()里根据命中位置用QTextLayout渲染不同颜色,这里不展开。先把代理模型这件事想明白,后面往表格里灌结果时才不会头疼。如果后续想点击表头排序,记住setSortingEnabled(true)之后,表头默认是可点击的,但需要确保代理模型的dynamicSortFilter设置为 true,否则数据灌进来的过程里排序不会实时生效。
3. 递归遍历:QDirIterator 的用法与过滤参数详解
3.1 QDirIterator 递归模式为什么比手写递归更稳
很多人一上来就写一段递归函数调用QDir::entryList(),这种写法在小目录里没问题,扫到系统盘就会暴露出两个问题:一是entryList()会一次性把整个目录条目加载进内存,目录里文件很多时占用明显;二是递归调用深度大,函数栈压力不小,而且entryList()默认做了排序,相当于额外付出了排序成本,但你根本不需要这个顺序。
Qt 提供了QDirIterator,它内部采用迭代器风格,一次只取一个条目,配合Subdirectories标志位能自动做深度优先遍历。好处是内存占用低,也省掉自己写的递归调用栈,还能在 while 循环里及时判断是否需要提前终止。
QDirIterator it(rootPath, QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); if (cancelled.loadAcquire()) break; QFileInfo info = it.fileInfo(); if (!info.isReadable()) continue; // 对 info 做匹配过滤 }第二参数的QDir::Files表示只关心普通文件,不返回目录本身。NoSymLinks表示跳过符号链接,避免后面说的死循环。NoDotAndDotDot表示不返回.和..。第三参数Subdirectories让迭代器自动深入子目录。还有一点容易被忽略:QDirIterator默认构造出来的迭代器不会对结果排序,所以如果你在写一个小工具希望列出 abc 排序的文件,要自行对最终结果做 sort,这反而省掉了不必要的排序时间。
这里的关键点是cancelled.loadAcquire()。这个变量是QAtomicInteger<bool>,具备原子性,子线程里每处理一个文件就检查一次。如果扫描了几十万个文件,取消不能等当前文件处理完再响应,越早检查,用户点“取消”时体验越接近即时。
3.2 按扩展名、路径、大小过滤的常见写法
QDirIterator 只是负责“把候选文件送过来”,真正的筛选还要自己写。筛选函数建议统一返回 bool,这样可以直接用一个 if 合并所有条件。
bool matchRule(const SearchParams& params, const QFileInfo& info) { QString name = info.fileName(); QString filePath = info.absoluteFilePath(); QString ext = info.suffix().toLower(); // 关键字匹配:普通包含 or 正则 if (!params.namePattern.isEmpty()) { if (params.useRegex) { QRegularExpression re(params.namePattern, params.caseSensitive ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption); if (!re.match(name).hasMatch()) return false; } else { Qt::CaseSensitivity cs = params.caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive; if (!name.contains(params.namePattern, cs)) return false; } } // 扩展名过滤 if (!params.extensions.isEmpty()) { if (!params.extensions.contains(ext)) return false; } // 大小范围 if (info.size() < params.minSize) return false; if (params.maxSize > 0 && info.size() > params.maxSize) return false; return true; }参数extensions在界面层一般用逗号分隔,所以塞进SearchParams之前要先QString::split(',')并去空格。info.suffix().toLower()这步很重要,Windows 上文件名大小写不敏感,有些文件后缀是.CPP,如果存进extensions的是cpp,不转小写就会漏结果。如果你要按文件名匹配路径,别只盯着fileName(),有时候用户记得的是完整路径里的某个目录,这时用filePath.contains()会更好。
大小过滤用字节数,界面上显示“KB”“MB”要做换算,但过滤时别用人类可读单位,否则会有一天发现minSize=1024却写成了"1KB"导致解析失败。我一般会直接在界面上放两个QSpinBox,单位都固定为字节,做转换的逻辑单独放一个函数。这个函数尽量做成纯函数,方便单元测试。
3.3 权限不足、隐藏目录、符号链接三个边界条件
文件搜索的翻车点大多不在正常文件上,而在异常文件。权限不足是一个典型问题。不少系统目录里混着当前用户没有读权限的子目录,QFileInfo.isReadable()返回 false 时直接跳过最省事。如果是用QDirIterator,可以直接跳过这个文件项,但不会跳过整个目录,所以当你发现扫描卡在一个没权限的目录里时,要在进入目录前判断一次。
隐藏目录的坑要特别说。QDir::NoDotAndDotDot只是排除.和..,并不会自动排除隐藏目录。如果用户不希望扫到.git、node_modules这类隐藏目录,必须在每次it.next()拿到entryInfo()后检查entryInfo.isHidden()。默认搜索工具都会跳过隐藏项,所以这里要单独加一个开关。很多初学 Qt 的人以为加了NoDotAndDotDot就等于不扫隐藏文件,实验结果通常是把自己气到。
符号链接的“死循环”不是 Qt 的 Bug,而是文件系统的客观结构。假设某个目录里有一个链接指向它的上级目录,递归遍历就会无限下钻。用QDir::NoSymLinks可以解决链接目录,但它同时也把链接文件排除掉了。如果希望保留链接文件而只阻止链接目录,可以手动判断info.isSymLink() && info.isDir(),再决定跳过。另一个办法是维护一个“已访问目录”的QSet<QString>,把每个真实路径塞进去,但这对QDirIterator来说实现成本偏高,不如直接用NoSymLinks省心。
还有一个隐蔽问题是大目录的第 3 层子目录里出现重解析点或挂载点。某些系统目录是挂载的分区,扫描它会触发额外 IO。我通常会在SearchParams中加一个maxDepth字段,虽然QDirIterator没有直接支持深度限制,但可以通过记录当前路径的层级,超过 8 层就跳过,避免陷入递归地狱。这里建议用QFileInfo::canonicalPath()而不是absolutePath(),因为absolutePath()保留符号链接,canonicalPath()会解析到真实位置,判断深度时更可靠。
QString absPath = info.absoluteFilePath(); int depth = absPath.count('/') - params.rootPath.count('/'); if (depth > params.maxDepth) continue;这段代码在 Windows 上要注意,路径分隔符可能是反斜杠,count('/')会不准。先把absPath.replace('\\', '/')再统计,把分隔符统一成斜杠,深度计算才稳定。别小看这个细节,跨平台文件搜索工具十个有八个栽在这类路径处理上。
4. 把搜索丢进子线程:QThread、信号槽与实时回传
4.1 界面卡死“未响应”的根因
直接在主线程里做while (it.hasNext()),扫描一个 1 万个文件的小目录也许只要 200ms,但扫描系统盘根目录就可能超过 5 秒。主线程被阻塞,事件循环不处理绘制和鼠标事件,系统就会把窗口标记为“未响应”。这不是 Qt 的缺陷,而是所有 GUI 程序的通病:不要在 UI 线程做耗时的磁盘 IO。
正确的做法是把遍历放到工作线程,UI 线程只负责接收信号更新表格。Qt 的QThread有两种常见姿势:一种是继承 QThread 重写run(),另一种是创建一个 QObject 对象moveToThread()。推荐第二种,原因很直白:继承 QThread 的方式容易把业务逻辑和线程生命周期绑在一起,而 Worker 对象可以通过信号槽机制接收参数、上报结果,代码更像一台“机器”。虽然起初写起来繁琐,但后面加取消、加进度、加第二种搜索策略时都不用改线程类。
4.2 Worker 完整代码模板:信号槽怎么接
Worker 对象的核心是三个信号:找到一批结果、进度变化、搜索完成。同时还要提供一个公共槽doSearch,由线程启动信号来触发。
class SearchWorker : public QObject { Q_OBJECT public: explicit SearchWorker(QObject *parent = nullptr) : QObject(parent), m_cancelled(false) {} public slots: void doSearch(const SearchParams ¶ms) { QDirIterator it(params.rootPath, QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); QFileInfoList batch; int count = 0; while (it.hasNext()) { if (m_cancelled.loadAcquire()) break; it.next(); QFileInfo info = it.fileInfo(); if (matchRule(params, info)) { batch.append(info); if (batch.size() >= 100) { emit resultsFound(batch); batch.clear(); } } count++; if ((count & 0x3FF) == 0) emit progress(count); } if (!batch.isEmpty()) emit resultsFound(batch); emit finished(); } signals: void resultsFound(const QFileInfoList &files); void progress(int scannedCount); void finished(); public: void requestCancel() { m_cancelled.storeRelease(1); } QAtomicInteger<bool> m_cancelled; };线程端接起来是这样:
QThread *thread = new QThread(this); SearchWorker *worker = new SearchWorker; worker->moveToThread(thread); connect(thread, &QThread::started, worker, &SearchWorker::doSearch); connect(worker, &SearchWorker::resultsFound, this, &MainWindow::appendResults); connect(worker, &SearchWorker::finished, thread, &QThread::quit); connect(worker, &SearchWorker::finished, worker, &SearchWorker::deleteLater); connect(thread, &QThread::finished, thread, &QThread::deleteLater);moveToThread(thread)之后,Worker 对象的槽函数会在thread的事件循环里执行。thread->start()时就通过started信号去调用doSearch,这时doSearch内部的所有操作都在工作线程里。resultsFound是跨线程信号,默认走QueuedConnection,主线程收到后更新表格,不会阻塞工作线程。
这里有一个容易踩的坑:SearchParams里如果有自定义类型,需要在第一次用qRegisterMetaType<SearchParams>(),否则connect运行时会报错。QFileInfoList本身就是注册过的元类型,所以不需要额外注册。如果你发现程序在connect时没有任何报错,但点击搜索按钮后 Worker 的doSearch没被调用,优先检查是不是漏了qRegisterMetaType。
还有一点要注意:每次点击搜索按钮都新建一个QThread,而不是复用旧线程。复用旧线程意味着上一次的 Worker 还没清理干净,可能同时跑两个搜索,结果互相覆盖。代码里通过finished信号让线程退出并自动deleteLater,下次搜索再开新线程,这样生命周期最清晰。
4.3 取消搜索、批量回传结果与进度上报
取消搜索不是调用thread->terminate()。terminate()是强制杀死线程,可能导致 QDirIterator 内部状态未清理,甚至让程序崩溃。正确方式是请求 Worker 停止:主线程点“取消”按钮时调用worker->requestCancel(),Worker 里的 while 循环每迭代一次就检查m_cancelled。这里用的是loadAcquire/storeRelease,比volatile安全,保证多核环境下的内存可见性。
批量回传结果也需要控制频率。每找到一个文件就给主线程发一个信号,文件到 10 万个时会创建 10 万个信号事件,排队等待主线程处理,内存和 CPU 都被打爆。我一般用“凑满 100 条发一次”的策略,既保证界面有实时反馈,又不会让事件队列暴涨。前 100 条尽量在 1 秒内发出去,用户看到第一屏结果后再耐心等完整扫描,这是文件搜索工具的默认体验标准。
进度上报也别每次都发信号。上面代码里用(count & 0x3FF) == 0,意思是每 1024 个文件才发一次进度信号。0x3FF等于 1023,做按位与是一个极低成本的分频技巧,读者可以试试改成500或2048体验一下。如果你想知道当前扫描到哪个目录,可以在进度信号里附带it.fileInfo().absolutePath(),但注意这样会多一次路径字符串分配,进度信号过于频繁时反而拖慢整体速度。
还有一个默认参数容易被忽略:QFileInfoList作为信号参数时,跨线程传递会做一次深拷贝。大批量resultsFound会带着几百个 QFileInfo 拷贝到主线程,这个开销可以接受,但如果要求极致性能,可以改成传递QStringList,或者干脆在 Worker 里组装好QStandardItem的行数据再传。信号槽机制本身不是零成本,过度优化反而会牺牲代码可读性,先按常规写法实现,再用性能分析工具决定要不要改。
5. 避坑手册:Qt 文件搜索最容易踩的五个坑
5.1 现象:搜索时界面卡死“未响应”
现象:点击按钮后,窗口变成灰色,拖动无响应,标题栏显示“未响应”。
原因:把QDirIterator整个 while 循环写在了按钮的 clicked 槽函数中,扫描大目录主线程被阻塞。Windows 任务管理器里能看到进程 CPU 占用很高,但界面事件循环已经没法处理绘制消息。
解决:第一,把耗时循环挪出主线程,用第 4 章的 Worker + QThread 方案。第二,如果只是临时调试,可以先在搜索按钮槽函数里加一个QElapsedTimer,打印扫描耗时,用数据说服自己这行代码不该放主线程。第三,顺便检查一下QFileSystemWatcher是不是也放在了主线程,监听大量目录变化同样会阻塞界面,最好只保留必要数量的监听路径。
5.2 现象:子线程里直接操作界面控件,程序崩溃或界面错乱
现象:Worker 里调用m_table->setRowCount(),或者操作QStandardItemModel,有时崩溃,有时界面表格内容残缺。
原因:Qt 的 UI 类和模型类不是线程安全的,默认只能在主线程访问。子线程里操作会导致未定义行为,崩溃还不能稳定复现,这就是最常见的线程翻车现场。
解决:子线程只发信号,把结果通过QFileInfoList交给主线程,在对应槽函数里更新模型。调试时可以在槽函数和 Worker 里分别打印QThread::currentThreadId(),对比线程 ID 就能判断是不是同一个线程。如果发现信号槽使用了Qt::DirectConnection,会强制在当前线程执行槽函数,这种情况要显式改成Qt::QueuedConnection。
5.3 现象:Windows 下搜索路径匹配失败,Linux 正常
现象:同样一份代码,Linux 下能搜出结果,Windows 下输入反斜杠路径却什么都匹配不到。
原因:路径分隔符问题。Windows 支持反斜杠和正斜杠混合,但用户输入的\在 C++ 字符串里需要转义,很多地方写"C:\Users"实际内容变成了"C:Users"。其次,Qt 在 Windows 下对路径比较默认不区分大小写,如果匹配时用了区分大小写的QString::compare,就会漏掉大小写不同的文件名。
解决:统一用QDir::toNativeSeparators和QDir::fromNativeSeparators做转换。匹配路径时先用Qt::CaseInsensitive,除非用户显式要求区分大小写。在SearchParams中保存一份“界面输入原值”,在caseSensitive开启时才用敏感匹配。如果不想踩这个坑,最好从根上规定:程序内部统一使用正斜杠,只在显示给用户时转成原生分隔符。
5.4 现象:QDirIterator 递归扫描一直不结束
现象:搜索某个包含大量链接或挂载盘的目录,进度条长时间不走,甚至磁盘占用不断飙升。
原因:符号链接指向上级目录,或目录里有链接到另一个磁盘分区的挂载点,递归深度超过预期,形成了类似死循环的行为。
解决:迭代器的过滤标志启用QDir::NoSymLinks。如果产品必须支持符号链接,就改成手动管理一个“已访问目录集合”,用QSet<QString>保存绝对路径标准化后的字符串。进入每个子目录前先检查QSet,遇到重复就跳过。注意路径标准化用QFileInfo::canonicalPath(),而不是absolutePath(),因为absolutePath()不解析链接,去重很可能失效。
5.5 现象:正则表达式把 CPU 占满,结果仍有丢失
现象:用户在关键字框输入*.cpp,期望匹配所有 cpp 后缀文件,结果一个都搜不到;或者输入一个复杂正则,单核 CPU 占用直接 100%。
原因:*.cpp不是合法正则表达式,Qt 的QRegularExpression会把它当成非法表达式处理,匹配全部失败。用户以为自己在用通配符,实际上引擎是正则。复杂正则如果包含回溯过多的模式,会吃掉大量 CPU,这个现象在遍历几十万文件时尤其明显。
解决:在输入框和参数转换之间加一个判断,检测*或?开头的内容,默认转成QRegularExpression的 glob 语义,而不是当纯正则。如果用户坚持要用正则,可以提示“通配符 * 在正则里需要写成 .*”。更稳妥的方案是提供一个“通配符/正则”切换下拉框,让用户明确知道用的是哪种语法。另外在QRegularExpression构造后调用isValid()检查,非法表达式直接提示用户修改,避免在工作线程里用无效正则反复尝试匹配。
6. 进阶:给文件搜索加上索引缓存和实时监控
6.1 首次全量扫描建索引,后续增量更新
每次搜索都重新扫描整个目录是浪费。常见做法是第一次进入搜索目录时全量扫描,把「文件名 → 路径」的映射写进本地缓存,下次搜索直接读缓存。这样搜索的复杂度从磁盘扫描变成内存查询,快一个数量级。缓存结构可以使用QHash<QString, QString>,键是文件名,值是绝对路径。如果是大目录,建议把缓存落到本地文件,程序重启后还能复用。
QHash<QString, QString> indexCache; // 第一次扫描后写入缓存文件 QFile cacheFile(cacheName); if (cacheFile.open(QIODevice::WriteOnly)) { QDataStream out(&cacheFile); out << indexCache; }注意缓存文件里保存的路径要在一开始就用canonicalPath()标准化,否则用户改了目录大小写或链接关系后,缓存里的路径会成为无效引用。读取缓存也要做校验:文件根路径变了,比如用户从目录 A 切到目录 B,缓存就应该整体失效。
6.2 用 QFileSystemWatcher 监听目录变化
缓存和真实文件系统会脱节,比如用户新建了一个文件,搜索工具还抱着旧索引。用QFileSystemWatcher添加目录,新建、删除、重命名都会发出directoryChanged信号。收到信号以后,只对变化的目录做增量刷新,而不是全盘重扫。
m_fsWatcher = new QFileSystemWatcher(this); connect(m_fsWatcher, &QFileSystemWatcher::directoryChanged, this, &MainWindow::reindexChangedDir);这里要控制监听目录数量。Windows 等平台上监听目录是有限资源的,一个目录几百个监听项没问题,但如果整个盘全监听,资源消耗会很大。建议只监听用户实际搜索过的顶层目录,而不是递归监听所有子目录。深层变化可以通过分层监听或手动刷新的方式弥补。
6.3 一个值得保留的启动扫描策略
比较让我后悔的方案是“每次打开工具就全盘建索引”,看起来提前准备了缓存,实际把启动变慢,用户可能连第一次搜索都等不到。我改成懒加载:第一次搜索时才建立索引,然后把索引落盘;之后每次搜索先查缓存,再根据QFileSystemWatcher的变更记录刷新相应目录。这样工具启动几乎是秒开,第一次搜索会稍微等一下,但后续响应会快很多。
从那以后我每次写搜索工具都会强制走一遍“全量建索引 + 监听变化 + 定向刷新”的流程,不再让程序做无用功。如果一开始只是写个小 Demo,这步可以往后放,但一旦打算长期使用,索引缓存迟早得补上。希望帮到你。
本文还有配套的精品资源,点击获取