`
gaofen100
  • 浏览: 1243400 次
文章分类
社区版块
存档分类
最新评论

QTimer源码分析(以Windows下实现为例)

 
阅读更多

起源

在newsmth上看到这样一个问题:

发信人: hgoldfish (老鱼), 信区: KDE_Qt
标  题: QTimer::singleShot()的疑问
发信站: 水木社区 (Mon Apr 11 22:03:48 2011), 站内

singleShot(0, ...)是表示下面的哪种情况呢?
1. 退出当前函数,回到事件循环的时候立即执行,忽略其它消息。
2. 把对应的QTimerEvent放到消息队列的最后,然后依次处理消息。
3. 把这个这个singleShot对应的QTimerEvent放到最后。依次处理消息。但是如果有新的消息到达时,它们会排到QTimerEvent的前面。

其中,2是假定消息队列没有优先级。1、3假定消息队列有优先级,但是1假定QTimerEvent最优先,而3假定QTimerEvent最不优先。

看文档似乎是3?

似乎挺有意思,于是,打开Qt的源码,慢慢看看,于是整理出本文。如果理解没问题的话,应该可以得出这个结论:

  • 消息队列是有优先级的。但是对与timerEvent事件,没用到优先级(Qt::NormalEventPriority?)。

  • 对于间隔不为零的timer,调用系统提供的计时器,然后等待响应系统的计时器事件
  • 对于间隔为零的timer,Qt自己进行了特殊处理。
    • 当使用静态函数QTimer::singleShot时,实际上直接调用了QueuedConnection方式的 QMetaObject::invokeMethod()

    • 当启动间隔为零的QTimer时,实际上先postEvent()派发了一个 QZeroTimerEvent到QAbstractEventDiapatch自身。在该事件的处理中,再通过sendEvent() 派发 QTimerEvent事件到对象中

一段废话,作为正文引子

每当需要一个计时器时,我们很容易想到QTimer,

  • 创建QTimer对象
  • 连接它的信号到我们的槽函数。

如果看Qt的Manual,我们还会注意到QBasicTimer和QTimeLine这两个类,也可起到计时的作用。那么这些之间那个最为根本呢?

所有这些都归结到QObject的3个成员函数中:

  • 在派生类中覆盖timerEvent()函数,进行处理
  • 通过startTimer()开启计时器
  • 通过killTimer() 结束

下面我们看看QTimer的计时事件是如何一步一步和系统提供的计时器(优先使用多媒体计时器,其次是普通的计时器)联系起来的

QTimer

看一下QTimer的源码,一切都明了了:

class QTimer:public QObject
{
...
public Q_SLOTS:
    void start(int msec);
    void start();
    void stop();
Q_SIGNALS:
    void timeout();
protected:
    void timerEvent(QTimerEvent *);
...
};

大家应该想得到了(就不贴代码了):

  • start() 调用 QObject::startTimer()
  • stop() 调用 QObject::killTimer()
  • timerEvent() 中发射信号 timeout()

QTimer::singleShot()

这是一个static成员函数,由于只需要一次事件。它其实没有创建QTimer对象,而是使用了一个QSingleShotTimer对象。这个类完整定义很简单

class QSingleShotTimer : public QObject
{
    Q_OBJECT
    int timerId;
public:
    ~QSingleShotTimer();
    QSingleShotTimer(int msec, QObject *r, const char * m);
Q_SIGNALS:
    void timeout();
protected:
    void timerEvent(QTimerEvent *);
};

这个没什么什么可介绍的,如果说特点的话,也就是 timerEvent 被调用一次后,就会将自己这个对象删除(呵呵,有点废话哈,调用一次使命就完成了呗)

另外呢,对于时间间隔为0的事件,甚至连QSingleShotTimer都不需要创建,而是直接用invokeMethod去调用相应的slot

void QTimer::singleShot(int msec, QObject *receiver, const char *member)
{
    if (receiver && member) {
        if (msec == 0) {
            // special code shortpath for 0-timers
            const char* bracketPosition = strchr(member, '(');
            if (!bracketPosition || !(member[0] >= '0' && member[0] <= '3')) {
                qWarning("QTimer::singleShot: Invalid slot specification");
                return;
            }
            QByteArray methodName(member+1, bracketPosition - 1 - member); // extract method name
            QMetaObject::invokeMethod(receiver, methodName.constData(), Qt::QueuedConnection);
            return;
        }
        (void) new QSingleShotTimer(msec, receiver, member);
    }
}

在 QMetaObject::invokeMethod的分析中,我们知道:对于QueuedConnection的连接,它最终通过QCoreApplication的postEvent() 函数派发了一个 QMetaCallEvent 事件。

QObject

  • 回归正题,看QObject的 startTimer和killTimer做了什么:

int QObject::startTimer(int interval)
{
    Q_D(QObject);
    d->pendTimer = true;                                // set timer flag
    return d->threadData->eventDispatcher->registerTimer(interval, this);
}
void QObject::killTimer(int id)
{
    Q_D(QObject);
    if (d->threadData->eventDispatcher)
        d->threadData->eventDispatcher->unregisterTimer(id);
}

代码倒是挺短,只是负担一下子交给eventDispatcher了。

QAbstractEventDispatcher

我们以windows下的情况为例看看吧:

void QEventDispatcherWin32::registerTimer(int timerId, int interval, QObject *object)
{
    Q_D(QEventDispatcherWin32);

    register WinTimerInfo *t = new WinTimerInfo;
    t->dispatcher = this;
    t->timerId  = timerId;
    t->interval = interval;
    t->obj  = object;
    t->inTimerEvent = false;
    t->fastTimerId = 0;

    if (d->internalHwnd)
        d->registerTimer(t);

    d->timerVec.append(t);                      // store in timer vector
    d->timerDict.insert(t->timerId, t);          // store timers in dict
}
bool QEventDispatcherWin32::unregisterTimer(int timerId)
{
    Q_D(QEventDispatcherWin32);
    WinTimerInfo *t = d->timerDict.value(timerId);
    d->timerDict.remove(t->timerId);
    d->timerVec.removeAll(t);
    d->unregisterTimer(t);
    return true;
}

进而,代码进入了 QEventDispatcherWin32Private

void QEventDispatcherWin32Private::registerTimer(WinTimerInfo *t)
{
    Q_Q(QEventDispatcherWin32);

    int ok = 0;
    if (t->interval > 20 || !t->interval || !qtimeSetEvent) {
        ok = 1;
        if (!t->interval)  // optimization for single-shot-zero-timer
            QCoreApplication::postEvent(q, new QZeroTimerEvent(t->timerId));
        else
            ok = SetTimer(internalHwnd, t->timerId, (uint) t->interval, 0);
    } else {
        ok = t->fastTimerId = qtimeSetEvent(t->interval, 1, qt_fast_timer_proc, (DWORD_PTR)t,
                                            TIME_CALLBACK_FUNCTION | TIME_PERIODIC | TIME_KILL_SYNCHRONOUS);
        if (ok == 0) { // fall back to normal timer if no more multimedia timers available
            ok = SetTimer(internalHwnd, t->timerId, (uint) t->interval, 0);
        }
    }
}
void QEventDispatcherWin32Private::unregisterTimer(WinTimerInfo *t, bool closingDown)
{
    // mark timer as unused
    if (!QObjectPrivate::get(t->obj)->inThreadChangeEvent && !closingDown)
        QAbstractEventDispatcherPrivate::releaseTimerId(t->timerId);

    if (t->interval == 0) {
        QCoreApplicationPrivate::removePostedTimerEvent(t->dispatcher, t->timerId);
    } else if (t->fastTimerId != 0) {
        qtimeKillEvent(t->fastTimerId);
        QCoreApplicationPrivate::removePostedTimerEvent(t->dispatcher, t->timerId);
    } else if (internalHwnd) {
        KillTimer(internalHwnd, t->timerId);
    }
    delete t;
}

呵呵,这段挺复杂的:

  • 对于时间间隔为0的timer,实际上并没有启动系统的计时器,而是创建了一个QZeroTimerEvent 事件
  • 对普通的timer,首先尝试启用多媒体定时器。若失败,则使用传统的SetTimer 和 KillTimer

非0的timer

我们先看看对正常的timer,系统如何通知程序定时事件的呢?熟悉Windows编程的应该对这个回调函数很熟悉吧(呵呵,我对windows编程不熟,说错了别怪我哈)

LRESULT QT_WIN_CALLBACK qt_internal_proc(HWND hwnd, UINT message, WPARAM wp, LPARAM lp)
{
if (message == WM_TIMER) {    
        Q_ASSERT(d != 0);
        d->sendTimerEvent(wp);
        return 0;
...
}

然后,看看这个sendTimerEvent做了什么

void QEventDispatcherWin32Private::sendTimerEvent(int timerId)
{
    WinTimerInfo *t = timerDict.value(timerId);
    if (t && !t->inTimerEvent) {
        // send event, but don't allow it to recurse
        t->inTimerEvent = true;

        QTimerEvent e(t->timerId);
        QCoreApplication::sendEvent(t->obj, &e);

        // timer could have been removed
        t = timerDict.value(timerId);
        if (t) {
            t->inTimerEvent = false;
        }
    }
}

挺简单的,就是通过QCoreApplication的sendEvent函数派发了QTimerEvent事件,剩下的工作当然就是Qt的事件系统的任务了。

间隔为0的timer

我们前面说了,对于间隔为0的timer,并没有启用系统的定时器,而是直接派发了一个QZeroTimerEvent 事件。我们知道,它进入事件系统以后,将会被派发到event函数

bool QEventDispatcherWin32::event(QEvent *e)
{
    Q_D(QEventDispatcherWin32);
    if (e->type() == QEvent::ZeroTimerEvent) {
        QZeroTimerEvent *zte = static_cast<QZeroTimerEvent*>(e);
        WinTimerInfo *t = d->timerDict.value(zte->timerId());
        if (t) {
            t->inTimerEvent = true;

            QTimerEvent te(zte->timerId());
            QCoreApplication::sendEvent(t->obj, &te);

            t = d->timerDict.value(zte->timerId());
            if (t) {
                if (t->interval == 0 && t->inTimerEvent) {
                    // post the next zero timer event as long as the timer was not restarted
                    QCoreApplication::postEvent(this, new QZeroTimerEvent(zte->timerId()));
                }

                t->inTimerEvent = false;
            }
        }
        return true;
    } else if (e->type() == QEvent::Timer) {
        QTimerEvent *te = static_cast<QTimerEvent*>(e);
        d->sendTimerEvent(te->timerId());
    }
    return QAbstractEventDispatcher::event(e);
}

看到对QZeroTimerEvent进行什么处理了吧?

  • 通过 QCoreApplication 的 sendEvent 派发出 QTimerEvent 事件
  • 同时 产生一个新的 QZeroTimerEvent 事件,放入事件队列中

分享到:
评论

相关推荐

    QT5.拼图游戏源码加可执行程序

    该游戏是为Windows 10操作系统设计的,并且可以在Visual Studio 2017环境下编译和运行。VS2017是Microsoft提供的一个强大的集成开发环境(IDE),支持多种编程语言,包括C++,这是Qt的主要编程语言。 游戏内预设了...

    55links友情链接网址跟踪器

    55links友情链接网址跟踪器,放在桌面,每次直接打开就可以访问55links友情链接交易平台,方便快捷。

    [AB PLC例程源码][MMS_046180]CompactFlash Data Storage.zip

    AB PLC例程代码项目案例 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我!

    moore_01_0909.pdf

    moore_01_0909

    FIBR English learning

    FIBR English learning

    [AB PLC例程源码][MMS_042350]How to send-receive SMS text messages using Westermo modem.zip

    AB PLC例程代码项目案例 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我!

    OIF_IEEE802.3_liaison_19OCt09.pdf

    OIF_IEEE802.3_liaison_19OCt09

    SerU,做网络安全FTP内容的实验必备

    做网络安全FTP内容的实验必备

    nagarajan_01_1107.pdf

    nagarajan_01_1107

    [AB PLC例程源码][MMS_043879]Programming in SFC and ST Language.zip

    AB PLC例程代码项目案例 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我!

    mellitz_3cd_01_0318.pdf

    mellitz_3cd_01_0318

    PyQt6实战派 配套代码

    PyQt6实战派 配套代码

    陕西省省级非物质文化遗产民俗经纬度数据统计表

    陕西省省级非物质文化遗产经纬度数据统计表 统计内容包含以下字段: 1. 项目名称 2. 遗产类别 3. 入选批次 4. 所属地区 5. 申报地区/单位 6. 地理经度 7. 地理纬度 该统计表系统记录了陕西省省级非物质文化遗产的地理空间信息,为文化遗产的数字化保护与研究工作提供了重要的数据支撑。

    ran_3ck_02a_0918.pdf

    ran_3ck_02a_0918

    毕业设计-基于springboot+vue开发的汽车租赁管理系统【源码+sql+可运行】50308.zip

    毕业设计_基于springboot+vue开发的汽车租赁管理系统【源码+sql+可运行】【50308】.zip 全部代码均可运行,亲测可用,尽我所能,为你服务; 1.代码压缩包内容 代码:springboo后端代码+vue前端页面代码; 脚本:数据库SQL脚本 效果图:运行结果请看资源详情效果图 2.环境准备: - JDK1.8+ - maven3.6+ - nodejs14+ - mysql5.6+ - redis 3.技术栈 - 后台:springboot+mybatisPlus+Shiro - 前台:vue+iview+Vuex+Axios - 开发工具: idea、navicate 4.功能列表 - 系统设置:用户管理、角色管理、资源管理、系统日志 - 业务管理:汽车管理、客户管理、租赁订单 3.运行步骤: 步骤一:修改数据库连接信息(ip、port修改) 步骤二:找到启动类xxxApplication启动 4.若不会,可私信博主!!!

    Runcorder - 跑步训练管理系统

    # Runcorder - 跑步训练管理系统 Runcorder 是一款专为跑步爱好者、马拉松运动员及高校体育生设计的本地化跑步训练管理工具,基于 Python 开发,结合 Tkinter 图形界面与强大的数据处理能力,为用户提供从训练记录到数据分析的全方位支持。无论是初学者还是专业跑者,Runcorder 都能帮助你科学规划训练、精准追踪进度,并通过可视化图表直观呈现训练成果,让你的跑步训练更智能、更高效! - **多用户管理**:支持创建、加载和删除用户档案,每个用户的数据独立存储,确保隐私与安全。 - **科学训练记录**:全维度记录跑步数据,包括日期、里程、配速、自评和晨跑标记,支持智能输入校验,避免数据错误。 - **多维数据分析**:通过动态可视化图表展示跑步里程趋势、平均配速曲线,支持自定义 Y 轴范围,帮助用户深入理解训练效果。 - **高阶功能**:提供 4 种科学训练模式(有氧/无氧/混合),支持历史记录修改与删除,数据以 JSON 格式持久化存储,跨平台兼容。

    paatzsch_01_0708.pdf

    paatzsch_01_0708

    开源AI工具下载——AnythingLLMDesktop1.7.3-r2 windows版

    AnythingLLM是一个全栈应用程序,您可以使用流行的开源大语言模型,再结合向量数据库解决方案构建个人本地AI大模型知识库

    mellitz_3ck_02_0519.pdf

    mellitz_3ck_02_0519

    petrilla_01_0708.pdf

    petrilla_01_0708

Global site tag (gtag.js) - Google Analytics