示例代码
对于很多开发者来说,Qt仅仅是一个用于开发GUI程序的库,但实际上,Qt官方一直在致力于将Qt打造成一个跨平台的开发框架。Qt中提供了大量的基础设施和非GUI库可供我们在开发非GUI程序时所用,如网络相关的QNetwork模块,音视频相关的QMutilMedia模块,还有核心的QtCore模块。这些模块都已经相当成熟和完善,而且都具有优秀的跨平台性能。在和其他语言,如Java,python相比,库的相对不够丰富是C++的一个缺陷所在,除了标准库,boost等之外,Qt其实也是一个可以考虑的强大的补充。
如果我们只是打算使用诸如QString,QTL等基础设施,只需要链接QtCore这个库即可。然而Qt很多更为强大的特性和类,如信号槽,QTimer等,必须依赖Qt事件循环才能正常工作。如果使用我们开发的库的可执行程序恰好也是基于Qt开发,那么一切工作正常,然而,作为库的开发者,我们不能对库的使用者做出假设,因此如果我们需要使用信号槽等依赖Qt事件循环的特性时,我们就需要在库中开启Qt事件循序。
可执行程序中的事件循环 我们先考虑在一个基于Qt的可执行程序中我们是如何开启事件循环的。相信下面的代码有接触过Qt开发的人肯定不会陌生。
1 2 3 4 5 6 7 8 9 10 11 #include "widget.h" #include <QApplication> int main (int argc, char *argv[]) { QApplication a (argc, argv) ; Widget w; w.show (); return a.exec (); }
这是通过Qt Creator创建Qt项目时main.cpp
中的默认实现。这段代码中,我们创建了一个QApplication
对象a
并将启动参数传递给a,然后创建其他对象,最后通过QApplication::exec
方法在主线程开启了一个事件循环,程序将会阻塞在exec
这一行直到我们调用QApplication::exit
方法退出事件循环。
库中的事件循环 根据上面的分析,我们可以很快想到在库中开启事件循环的方法,就是开启一个线程,然后在这个线程中创建QCoreApplication
并开启事件循环。下面本文将以对QTimer的简单封装为例来说明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #pragma once #include <memory> class QTimer ;class IListener {public : virtual ~IListener () = default ; public : virtual void OnTimeout () = 0 ; }; class Timer {public : Timer (std::shared_ptr<IListener> listener); ~Timer (); public : void Start (int msec) ; void Stop () ; public : std::shared_ptr<IListener> listener_; QTimer *timer_ = nullptr ; };
这是我们需要对外暴露的头文件,可以看到这个头文件中,除了一个前置的QTimer
声明(当然,可以通过PIMPL 手法做进一步的隐藏),已经将和Qt相关的部分完全隐藏了起来。QTimer
是一个严重依赖Qt事件循环的工具类,因此我们需要创建一个事件循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #pragma once #include <QCoreApplication> #include <QThread> class QEventLoopThread : public QThread {public : QEventLoopThread (const QEventLoopThread &) = delete ; QEventLoopThread operator =(const QEventLoopThread &) = delete ; ~QEventLoopThread (); public : static QEventLoopThread &GetInstance () ; protected : void run () override ; private : explicit QEventLoopThread (QObject *parent = nullptr ) ; private : QCoreApplication *app_{}; };
这里我们使用继承QThread
并重写QThread::run
方法的形式来开启一个线程。由于事件循环只能存在一个,因此这里使用了单例模式。QCoreApplication
是QApplication
的基类,这里我们不需要GUI相关的操作,不需要使用QApplication
类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include "qeventloop_thread.h" QEventLoopThread::QEventLoopThread (QObject *parent) : QThread (parent) { start (); } QEventLoopThread::~QEventLoopThread () { app_->quit (); quit (); wait (); } QEventLoopThread &QEventLoopThread::GetInstance () { static QEventLoopThread event_loop; return event_loop; } void QEventLoopThread::run () { if (app_) { return ; } int argc{}; char *argv{}; app_ = new QCoreApplication (argc, &argv); app_->exec (); }
实现很简单,我们在QAppThread::run
中做了类似于可执行程序中main方法的工作。首次调用QAppThread::GetInstance
会触发QAppThread::run
函数的执行。
然后我们去实现对外的接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include "timer.h" #include <QTimer> #include "qeventloop_thread.h" Timer::Timer (std::shared_ptr<IListener> listener) : listener_ (listener) { timer_ = new QTimer; } Timer::~Timer () { delete timer_; } void Timer::Start (int msec) { timer_->moveToThread (&QAppThread::GetInstance ()); QObject::connect (timer_, &QTimer::timeout, [this ] { listener_->OnTimeout (); }); QMetaObject::invokeMethod (timer_, "start" , Qt::QueuedConnection, Q_ARG (int , msec)); } void Timer::Stop () { QMetaObject::invokeMethod (timer_, "stop" , Qt::QueuedConnection); }
对于QObject
及其子类对象,都有一个所在线程的概念,这个线程必须是主线程或QThread
线程,默认为创建该对象的线程。这个线程会影响到该对象的槽函数的执行线程。这里我们需要将创建的QObject
及其子类对象移动到我们事件循环所在线程,否则该对象的槽函数不会被执行。这里还有一个需要注意的地方,QTimer
要求必须在其对象所在线程调用start
或stop
,因此这里我们通过QMetaObject::invokeMethod
来将对QTimer::start
和QTimer::stop
的调用放到timer_
所在线程的事件队列中,达到在timer_
所在线程中执行的目的。
我们写一个简单的demo可执行程序来测试一下我们的库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <iostream> using namespace std;#include "timer.h" class Listener : public IListener {public : void OnTimeout () override { static uint32_t elapsed_second = 0 ; ++elapsed_second; std::cout << "Elapsed time(s): " << elapsed_second << std::endl; } }; int main () { Timer timer (std::make_shared<Listener>()) ; timer.Start (1000 ); std::cin.get (); return 0 ; }
执行的可能输出如下
1 2 3 4 5 6 7 WARNING: QApplication was not created in the main() thread. Elapsed time(s): 1 Elapsed time(s): 2 QObject::killTimer: Timers cannot be stopped from another thread QObject::~QObject: Timers cannot be stopped from another thread Press <RETURN> to close this window...
这里首先会有一个警告,提示说QApplication
不是在主线程创建,由于这里我们在库里使用,必须在子线程中创建,因此这个警告可以忽略。 然后在程序退出时会提示在其他线程中调用了QTimer::stop
方法,这时因为Timer
对象的析构函数是在主线程中调用,间接地导致了在主线程中调用了QTimer::stop
方法。我们需要在退出前保证Timer
已经停止。对Timer
的析构函数修改如下:
1 2 3 4 5 6 7 Timer::~Timer () { QMetaObject::invokeMethod (timer_, &QTimer::stop, Qt::BlockingQueuedConnection); delete timer_; }
我们通过Qt::BlockingQueuedConnection
标志来确保stop
方法被调用完成后再去执行delete
操作,否则还是可能会导致析构时timer_
尚未停止。
总结 相对来说,在库中使用Qt事件循环相关的特性会麻烦一些,但和Qt提供的丰富的基础库和机制相比,这点牺牲还是值得的。如果对库的体积要求不大,是完全可以考虑在库的开发中引入Qt的。本文的只是简单介绍了如何在库中引入Qt事件循环,在代码的非侵入性,线程安全性等方面还有很大的改进空间,希望读者在使用时可以进一步完善。