当前位置: 首页 > news >正文

Qt 倒计时功能从入门到弃坑:一个老码农的实战笔记

Qt 倒计时功能从入门到弃坑:一个老码农的实战笔记

事情是这样的,上周接到一个需求:做个倒计时功能,要能传秒数、能弹窗、能触发信号,还得在多个地方同时用。我一听,这不挺简单吗?结果写着写着发现坑还不少…

先说说为啥要自己写

Qt 自带QTimer,但你要做个像样的倒计时,光靠它还真不够。比如:

  • 定时器会有累积误差(别问我是怎么发现的)
  • 需要同时管理多个倒计时实例
  • 还得支持 QML 绑定(产品经理临时加的)

所以干脆封装一个,一劳永逸。

最终成品长这样

头文件countdowntimer.h

#ifndefCOUNTDOWNTIMER_H#defineCOUNTDOWNTIMER_H#include<QObject>#include<QTimer>#include<QElapsedTimer>#include<QString>classCountdownTimer:publicQObject{Q_OBJECTQ_PROPERTY(intremainingSeconds READ remainingSeconds NOTIFY remainingSecondsChanged)public:explicitCountdownTimer(QObject*parent=nullptr);// 开始倒计时// seconds: 倒计时秒数(必须 > 0)// finishMessage: 倒计时结束时的弹窗文本// showPopup: 是否显示倒计时结束弹窗voidstart(intseconds,constQString&finishMessage,boolshowPopup=true);// 停止倒计时voidstop();boolisActive()const;intremainingSeconds()const;signals:voidfinished();// 倒计时结束信号voidtick(intsecondsLeft);// 每秒触发一次voidremainingSecondsChanged();// 剩余秒数变化信号privateslots:voidonTimeout();private:QTimer m_timer;QElapsedTimer m_elapsedTimer;intm_totalSeconds;intm_lastRemaining;QString m_finishMessage;boolm_showPopup;boolm_isActive;};#endif

实现文件countdowntimer.cpp

#include"countdowntimer.h"#include<QMessageBox>#include<QApplication>CountdownTimer::CountdownTimer(QObject*parent):QObject(parent),m_totalSeconds(0),m_lastRemaining(0),m_showPopup(true),m_isActive(false){connect(&m_timer,&QTimer::timeout,this,&CountdownTimer::onTimeout);m_timer.setSingleShot(false);m_timer.setInterval(1000);}voidCountdownTimer::start(intseconds,constQString&finishMessage,boolshowPopup){if(seconds<=0){qWarning()<<"CountdownTimer: seconds must be > 0";return;}if(m_isActive){stop();}m_totalSeconds=seconds;m_finishMessage=finishMessage;m_showPopup=showPopup;m_isActive=true;m_elapsedTimer.start();m_lastRemaining=seconds;emitremainingSecondsChanged();emittick(seconds);if(seconds==0){onTimeout();}else{m_timer.start();}}voidCountdownTimer::stop(){if(!m_isActive)return;m_timer.stop();m_isActive=false;m_totalSeconds=0;m_finishMessage.clear();}boolCountdownTimer::isActive()const{returnm_isActive;}intCountdownTimer::remainingSeconds()const{if(!m_isActive)return0;qint64 elapsedMs=m_elapsedTimer.elapsed();intremaining=m_totalSeconds-static_cast<int>(elapsedMs/1000);returnremaining<0?0:remaining;}voidCountdownTimer::onTimeout(){if(!m_isActive)return;qint64 elapsedMs=m_elapsedTimer.elapsed();intremaining=m_totalSeconds-static_cast<int>(elapsedMs/1000);if(remaining<=0){m_timer.stop();m_isActive=false;emitfinished();if(m_showPopup&&!m_finishMessage.isEmpty()){QMessageBox::information(qApp->activeWindow(),tr("倒计时结束"),m_finishMessage);}m_totalSeconds=0;m_finishMessage.clear();}else{if(remaining!=m_lastRemaining){m_lastRemaining=remaining;emitremainingSecondsChanged();emittick(remaining);}}}

用起来啥感觉

基础用法

CountdownTimer*timer=newCountdownTimer(this);// 60秒倒计时,结束弹窗"休息一下"timer->start(60,"休息一下",true);// 结束干点别的connect(timer,&CountdownTimer::finished,this,&MyClass::onTimeout);// 实时显示剩余时间connect(timer,&CountdownTimer::tick,[](intsecondsLeft){qDebug()<<"还剩"<<secondsLeft<<"秒";});

多个倒计时同时跑

// 支付倒计时(15分钟,弹窗提醒)autopaymentTimer=newCountdownTimer(this);paymentTimer->start(900,"支付超时,订单已取消",true);// 验证码倒计时(60秒,不弹窗,只更新按钮文字)autocodeTimer=newCountdownTimer(this);codeTimer->start(60,"",false);connect(codeTimer,&CountdownTimer::tick,[](intleft){ui->getCodeBtn->setText(QString("重新获取(%1)").arg(left));});

踩过的坑和经验

1. 定时器误差问题

QTimer不是精确定时器,累积误差会越来越大。用QElapsedTimer记录实际经过的时间,每次根据实际耗时计算剩余秒数,完美解决。

// 错误做法:依赖定时器次数累加m_remaining--;if(m_remaining<=0){/* 结束 */}// 正确做法:根据实际耗时计算qint64 elapsedMs=m_elapsedTimer.elapsed();intremaining=m_totalSeconds-(elapsedMs/1000);

2. 弹窗的父窗口传什么

// 传 nullptr:弹窗独立,可能不在最前QMessageBox::information(nullptr,"提示","时间到");// 传 activeWindow:跟随当前窗口,体验更好QMessageBox::information(qApp->activeWindow(),"提示","时间到");// 最佳实践:允许调用方传入父窗口voidstart(intseconds,constQString&msg,boolshowPopup,QWidget*parent=nullptr);

3. 信号别乱用

// tick:每秒都触发,适合刷新UIconnect(timer,&CountdownTimer::tick,ui->label,&QLabel::setNum);// remainingSecondsChanged:值变化才触发,适合QML绑定// QML里可以直接写 timer.remainingSeconds// finished:只触发一次,适合业务逻辑connect(timer,&CountdownTimer::finished,this,&Order::cancel);

Q_PROPERTY 是什么鬼

很多新手看到这个一脸懵,简单说:

Q_PROPERTY(intremainingSeconds READ remainingSeconds NOTIFY remainingSecondsChanged)

这句话的意思是:我把remainingSeconds这个属性暴露给 Qt 的元对象系统了。

有啥用?

  • QML 可以直接绑定这个属性,值变了 UI 自动更新
  • Qt Designer 里能显示和编辑
  • 可以配合 QPropertyAnimation 做动画

什么时候用?

  • 要做 QML 界面 → 必须用
  • 纯 C++ Widgets 项目 → 可以不用

我加这个是考虑到产品经理随时可能让上 QML,提前写好省的回头改。

几个真实使用场景

场景:考试系统倒计时

classExamSystem:publicQWidget{CountdownTimer*m_timer;voidstartExam(){m_timer=newCountdownTimer(this);m_timer->start(3600,"考试时间到,系统自动交卷",true);// 实时显示connect(m_timer,&CountdownTimer::tick,this,&ExamSystem::updateDisplay);// 自动交卷connect(m_timer,&CountdownTimer::finished,this,&ExamSystem::autoSubmit);// 每分钟自动保存connect(m_timer,&CountdownTimer::remainingSecondsChanged,[this](){if(m_timer->remainingSeconds()%60==0){saveProgress();}});}};

场景:游戏技能冷却

classSkillCoolDown{QMap<QString,CountdownTimer*>m_cooldowns;voidcastSkill(constQString&skillName,intcooldownSec){if(isOnCooldown(skillName)){QMessageBox::warning(nullptr,"提示","技能还在冷却中");return;}autotimer=newCountdownTimer(this);timer->start(cooldownSec,"",false);m_cooldowns[skillName]=timer;// 更新UI按钮文字connect(timer,&CountdownTimer::tick,[=](intleft){updateButtonText(skillName,left);});connect(timer,&CountdownTimer::finished,[=](){m_cooldowns.remove(skillName);enableButton(skillName);});}};

写在最后

这个倒计时类我已经在三个项目里用过了,目前没出过问题。核心就两点:

  1. 用 QElapsedTimer 保证精度
  2. 用 QTimer 做触发,但别依赖它的计次
http://www.jsqmd.com/news/658565/

相关文章:

  • ANSYS APDL谐响应分析实战:悬臂梁频响函数的MATLAB后处理与可视化
  • 视觉大模型技术演进全景:从Transformer到产业落地实践
  • 别再死记MobileNetV1结构了!用PyTorch手把手拆解Depthwise Separable Conv(附代码)
  • 04-07-07 结构化分析问题 - 学习笔记
  • 不懂 ECharts 也能做大屏?AK-Design 开源低代码,拖拽可视化直接上线,告别手写配置,ECharts 图表一键生成
  • 2025届必备的十大降重复率助手推荐
  • OpenAI 正式推出 GPT-5.4-Cyber:网络安全专属 AI 模型新突破
  • 配置爆炸危机预警!SITS2026最新数据:单系统平均配置项达2143+,AI生成方案已成P0级技术刚需——立即获取首批200个预训练领域模型访问权限
  • iOS Widget透明组件精准适配:从尺寸计算到位置布局的实战指南
  • Linux配置SSH密钥实现安全免密服务器登录
  • NPJ Precis Oncol 加拿大蒙特利尔大学医院研究中心:多组学融合网络预测结直肠癌肝转移术后早期复发
  • 终极指南:用Windhawk轻松实现Windows系统模块化定制
  • “生成即上线”时代已来:如何用轻量级RAG+符号执行实现毫秒级错误定位与自愈?——2024最新实践报告
  • 为什么电机控制观测器要使用锁相环(PLL)---学习笔记
  • 开发卡片新建卡片
  • KMS激活全攻略:5分钟搞定Windows和Office永久激活难题
  • 相控阵天线(二):从阵列因子到波束赋形实战(栅瓣抑制、加权优化与Python仿真)
  • python reno
  • FPGA加速卡实战:基于XDMA核的C2H/H2C通道性能调优与带宽测试全记录
  • 避坑指南:为什么你的Qt程序在别人电脑显示中文乱码?GBK与UTF-8编码深度解析
  • 你家的“老破小”,政府系统里也有
  • AI生成代码=自动埋雷?3层静态验证网+运行时沙箱机制,实现DevOps流水线中LLM输出100%可信准入(附开源策略引擎)
  • 从微信支付P12证书中提取关键信息:OpenSSL与Java实战指南
  • 【AIAPI代码生成实战军规】:从零构建可交付AI-Native服务的6步工作流,2026奇点大会闭门 workshop 独家流出
  • 从SiamFC到SiamMask:用PySOT工具包复现孪生网络跟踪算法全流程(附避坑指南)
  • 【多传感器融合】VIO实战:从理论到部署的挑战与优化
  • 2026年知名的交通消防器材长期合作厂家推荐 - 行业平台推荐
  • AI测试标准更新:2026年新规详解
  • 图解强化学习 |SAC
  • MySQL数据库磁盘写满后如何紧急处理_清理日志与扩容空间