基于Qt的文件内容搜索工具设计与实现
2019-09-10章俊
章俊
摘 要:该文利用Qt框架实现了一个文件搜索工具,并支持对文件内容的搜索。该文件搜索工具是一个集多线程、图形界面、事件处理的面向对象编程的实例程序。
关键词:Qt;C++;多线程;文件搜索;事件处理
1背景
Windwos系统原生自带的“文件搜索工具”的功能简单,查找文件缓慢,而且不支持基于文本内容的文件搜索。本文利用Qt框架,实现了一个支持并发的文本内容的搜索工具。该文件搜索工具在进行搜索时,会把搜索任务合理分配给辅助线程,并由辅助线程完成任务后由事件系统通知给主线程。该文件搜索工具是一个集多线程、图形界面、事件处理的面向对象编程知识的综合应用实例程序。
2Qt框架介绍
Qt是跨平台的应用程序和图形用户接口(GUI)开发框架,由集成开发工具、跨平台类库和集成开发环境(IDE)组成,它的开发语言是C++。
Qt中的一个核心机制是信号与槽机制,它是Qt的基础。信号与槽的设计使用了观察者模式,当某个事件发生之后,它就会发出一个信号(signal),信号的发出是没有目的的,类似广播,如果有对象对这个信号感兴趣,它就会使用连接(connect)函数将自己的槽(slot)函数绑定到这个信号。信号与槽机制取代了回调函数的实现方式,当信号发出时,被连接的槽函数会自动被回调,它可以让应用程序编写人员把这些互不了解的对象绑定在一起。
事件是GUI程序的重要部分,GUI应用程序是由事件循环驱动的。事件是各种由应用程序内部或者外部产生的事情或者动作的统称,Qt中使用一个对象来表示一个事件,继承自QEvent类。事件和信号与槽并不相同,响应事件的函数并不能立即响应,而是会进入事件序列,等待执行。而信号与槽不同,信号发出后,关连的槽函数会立即得到执行。
3软件介绍
文件搜索工具会从指定的目录中搜索含有指定内容的文件,并列出匹配到的文件列表。一个文件的搜索过程涉及到把文件的内容读取到内存,然后在读取到的内容中匹配指定的搜索关键字,并把匹配到的文件路径显示出来,这是一个混合了磁盘读取和数据处理的过程。
在这个程序中,使用了多个辅助线程完成这个任务,每个线程都有需要用来搜索的一个文件列表。每个线程在进行搜索任务的同时,通过使用一个自定义事件来与主线程进行通信,把搜索到的结果通知给主线程(GUI)进行汇总与显示。
4详细设计与实现
4.1界面设计
使用Qt Creator新建一个Qt Widgets Application程序,并新建一个继承于QMainWindow类的MainWindow窗口类,在类MainWindow下创建一个搜索目录设置框,一个搜索内容设置框,一个搜索结果显示列表,一个搜索/取消按钮。界面效果如图1所示。
4.2搜索任务与界面显示设计
主窗口类MainWindow还有2个私有数据类型的项:int done和volatile bool stopped,done整型变量是已经匹配成功的文件数目,stopped布尔值用来通知辅助线程用户是否已经取消操作,此变量使用volatile来修饰,在线程间使用volatile bool是安全的。
一旦选定搜索目录、搜索内容,用户按下Search按钮就开始搜索工作了。按钮一旦被按下,该按钮就会变成Cancel按钮,所以用户可以在任何时候停止搜索工作。这个按钮与searchOrCancel()槽相连。
searchOrCancel()函数主要内容:
void MainWindow::searchOrCancel()
{
m_stopped = true;
if (QThreadPool::globalInstance()->activeThreadCount())
QThreadPool::globalInstance()->waitForDone();
if (m_searchOrCancelButton->text() == tr("&Cancel")) {
updateUi();
return;
}
QStringList sourceFiles = getFiles(m_directoryLineEdit->text());
if (!sourceFiles.isEmpty()) {
m_resultListWidget->clear();
m_statusBar->clearMessage();
searchFiles(sourceFiles);
}
}
在這个槽函数的一开始设定stopped变量值为true,通知那些运行中的辅助线程都必须停止。然后,检查是否还有辅助线程在Qt的全局线程序列中运行,如果有的话,那么就阻塞,直到所有的线程都停止。
点击Cancel按钮会调用updateUi()方法把Cancel按钮的文本改成Search并返回。
点击Search按钮,就会使用getFiles()函数获得搜索目录所有文件的列表。如果列表为空,则通知用户错误,并返回。如果列表不为空,清除搜索结果窗口中之前的搜索结果和状态栏的搜索统计结果。
4.3任务分配与线程调度设计
文件搜索工具的具体搜索工作是在多个线程内完成的,所以主线程在会把搜索任务合理的分配给多个线程。在此使用QtConcurrent::run()函数调用任务线程,它会在Qt全局线程池中的一个辅助线程中执行该函数。
在本应用中使用searchFiles()函数把任务分配给多个线程,并使线程运行。该函数接受的形参是需要搜索的文件列表,函数的主要内容:
void MainWindow::searchFiles(const QStringList &sourceFiles)
{
m_stopped = false;
updateUi();
m_total = sourceFiles.count();
m_done = 0;
const QVector<int> sizes = chunkSizes(sourceFiles.count(),QThread::idealThreadCount());
const QString &searchContent = m_contentLineEdit->text();
int offset = 0;
foreach (const int chunkSize, sizes) {
QtConcurrent::run(searchFilesTask, this, &m_stopped,
sourceFiles.mid(offset, chunkSize), searchContent);
offset += chunkSize;
}
checkIfDone();
}
在函数的一开始把stopped变量为false,然后调用updateUi()函數更新界面的显示。把done变量设置成0,因为还没有匹配到任何文件。
如果为每一个文件创建一个线程来执行搜索任务,即为每个必须要检索的文件通过函数和文件名调用QtConcurrent::run()一次,这样会建立和列表中文件数目一样多的辅助线程。对于一些非常大的文件,这样的方法或许奏效,但对于非常多却不知大小的文件来说,建立如此多的线程所付出的代价会与任务分散给辅助线程处理所得到的潜在收益不成比例。
所以在此使用QThread::idealThreadCount()函数获取计算程序运行所在平台上支持的辅助线程的最佳数目,并把任务进行划分,使得每个辅助线程都能得到一个合理的任务数量。
chunkSize()函数会完成此工作,它会对容器进行划分,该函数会根据容器中给定的文件数量和期望的文件块数(这里是辅助线程的数目),返回一个划分的块大小矢量。chunkSize()函数的内容:
QVector<int> MainWindow::chunkSizes(int size, int chunkCount)
{
if (chunkCount == 1) return QVector<int>() << size;
QVector<int> result(chunkCount, size / chunkCount);
if (int remainder = size % chunkCount) {
int index = 0;
for (int i = 0; i < remainder; ++i) {
++result[index];
++index;
index %= chunkCount;
}
}
return result;
}
接下来使用一个循环来遍历得到的任务划分矢量,并使用QtConcurrent::run()函数启动辅助线程。一旦所有的辅助线程得到启动,就会调用checkIfDone()槽函数,这个槽函数会以轮询的方式确定搜索是否完成。
4.4搜索任务线程设计
搜索匹配函数searchFilesTask()会被任务分配函数调用一次或多次,它们在一个或多个辅助线程中运行,每个任务线程都有一个唯一的需要处理文件的列表,并对列表中的文件进行搜索处理。在搜索任务线程中,每次搜索匹配文件内容之前,都会检测stopped布尔值来获取用户是否已经取消了操作,如果已经取消,函数就返回,它正在执行的线程也会转变成非激活态,如果没有取消,则继续运行。searchFilesTask()函数的主要内容:
void searchFilesTask(QObject *receiver, volatile bool *stopped,
const QStringList &sourceFiles, const QString &searchContent)
{
foreach (const QString &source, sourceFiles) {
if (*stopped) return;
QFile file(source);
if(!file.open(QIODevice::ReadOnly | QIODevice::Text)) continue;
if(file.readAll().contains(searchContent.toLocal8Bit().data())) {
QApplication::postEvent(receiver, new ProgressEvent(hit, source));
}
}
}
文件搜索過程非常简单:对于列表中的每个文件,读取文件中的内容,搜索是否含有搜索的关键字,如果没有,进行列表中下一个文件的搜索;如果有,则使用一个自定义事件,把文件路径传递给主窗口对象。QApplication::postEvent()函数进行这个自定义事件的传递。
实际上,有两种方法可用于事件的发送:QApplication::sendEvent()和QApplication::postEvent()。sendEvent()函数会立即发送事件。同时,sendEvent()不会删除事件,因此,实际应用中会在栈(stack)上创建sendEvent()事件。postEvent()函数会向接收者的事件序列中添加事件,该事件应当使用new创建在堆(heap)上,以便可以让它作为接收者事件序列循环处理过程中的一部分而得到处理,postEvent()函数会获取这个事件的所有权,它会析构并删除此事件,它与Qt的事件处理配合得非常好。
Qt完美地处理了事件从一个线程切换到另外一个线程的情况。使用自定义事件,如下是自定义事件类ProcessEvent的内容:
struct ProgressEvent : public QEvent
{
explicit ProgressEvent(bool hit_, const QString &fileName_)
: QEvent(static_cast<Type>(QEvent::User)), hit(hit_), fileName(fileName_) {}
const bool hit;
const QString fileName;
};
在此给予每一个自定义事件一个唯一的ID(QEvent::User),ID的类型为QEvent::Type,这样做就可以避免事件之间的相互混淆。我们把事件定义成struct,并从基类QEvent派生,并将布尔型的hit标志和fileName文本设置成可公开访问,便于在界面类MainWindow中访问。
在界面类MainWindow中重新实现了QWidget::event(),使其能够探测并处理自定义事件。界面类MainWindow中重新实现的QWidget::event()的内容:
bool MainWindow::event(QEvent *event)
{
if (!m_stopped && event->type() == static_cast<QEvent::Type>(QEvent::User)) {
ProgressEvent *progressEvent = static_cast<ProgressEvent*>(event);
if (progressEvent->hit) {
m_resultListWidget->addItem(progressEvent->fileName);
++m_done;
}
return true;
}
return QMainWindow::event(event);
}
此外,如果数据处理正在进行,获取自定义的ProgressEvent,会把事件的信息文本(匹配到的文件路径)添加到搜索结果窗口中,并增加匹配到的文件数量。程序还会返回一个true来表明事件已经得到了处理, Qt将会删除该事件,而不是继续寻找另一个能够处理该事件的处理器。但是,如果搜索过程已经停止,对于任何的其他事件,都把任务传递给基类的事件处理器。
5结束语
本文通过使用Qt应用框架实现了文件搜索工具,支持对文件内容的搜索,并支持多线程搜索。可为类似的图形界面、多线程复杂计算任务软件的开发工作提供参考。
本文还使用Qt框架的自定义事件系统代替传统的信号与槽实现辅助线程与主线程间通信应用进行了尝试研究工作。并对两种事件的发送方式(QApplication::sendEvent()和QApplication::postEvent())进行了研究。
参考文献
[1] Mark Summerfiled. Advanced Qt Programming[M]. Prentice Hall, 2010-7.
[2] Jasmin Blanchette, Mark Summerfield. C++ GUI Programming with Qt4[M]. Prentice Hall, 2008-2.