Qt学习之定时器QTimer类编程

1 引言

在进行控制系统上位机编程时,我们经常需要用到定时器,在定时服务函数中执行控制动作或者显示数据等,编程时候使用到的类是QTimer类,我们要创建两个定时器,第一个定时器是在对话框的界面线程中创建的,他的定时事件在主循环中进行,而第二个定时器是在一个子线程中创建的,它的定时事件在子线程中循环,不受主线程其他事件的影响,我认为如果程序对于定时精度要求高的话,最好采用第二种设计方法,如果是诸如显示数据之类的对定时精度要求不高的动作,则可以采用第一种设计方法,更加省时省力。

2 准备工作

定时器需要用到QTimer类,此外,为了创建和管理子线程,我们自己编写一个线程类MyThread,它继承于QThread类,对话框类的头文件如下:

#include 
#include 
#include "mythread.h"

线程类的头文件如下,这里增加QDialog类和QtWidgets类主要是为了在子线程中控制窗口控件。

#include 
#include 
#include 
#include 

3 设计界面

Qt学习之定时器QTimer类编程_第1张图片左边的编辑框用于设置定时间隔,单位是ms,我们定义了一个Int型变量,每进入一次定时事件,变量加1,并将变量值显示在右边的编辑框中。

4 定时方法

有三种设计方法:
第一种是使用startTimer()函数,输入参数为定时时间,返回值为ID值,用于区别多个定时器,需要重写定时器timerEvent(),在重写函数中判断定时器ID并执行不同的操作。
第二种是创建QTimer对象,用connect函数将该对象的timeout()函数与自定义的槽函数连接,这种方法就是本文用到的方法。
第三中是QBasicTimer类,我没有详细了解过。

5 定时精度

定时精度主要依靠平台自身的精度,用setTimerType()函数设置,输入的参数有以下几个选择:

  • Qt::PreciseTimer:QTimer 将尝试将精度保持在 1 毫秒。
  • Qt::CoarseTimer:QTimer 可能比预期更早唤醒,提前量为定时间隔的5%
  • Qt::VeryCoarseTimer :QTimer 可能比预期更早唤醒,提前量为500ms

6 程序设计

(1)构造函数
在构造函数中初始化计数变量为0,并将指针变量赋为空指针

    Mytimer=NULL;
    Timer1Count=0;
    mythread=NULL;

(2)定时器1的设计
我们设计点击打开定时器1按钮的槽函数,在函数中首先判断按钮当前的标题如果是“打开定时器”,则执行打开定时器的操作,包括,新建一个QTimer类的对象,使用setTimerType函数设置定时器的精度,将timeout()信号和我们自己编写的槽函数MyTimerFunc()连接起来,那么当定时时间到了,系统就会发出timerout()信号,从而调用我们的MyTimerFunc(),最后我们使用start()函数开启定时器,将编辑框中的内容转化为Int型传入,start函数需要传入的参数是时间间隔(Int型变量),
如果按钮当前的标题如果不是“打开定时器”,则执行关闭定时器的相关操作,如果Mytimer指针变量不为NULL,使用stop函数停止定时器,用deleteLater()释放指针,这个函数的效果类似于delete Mytimer,官方推荐当我们需要释放一个QObject对象时候,使用deleteLater()而不是delete ,因为前者是线程安全的,最后,把指针重新赋值为空指针,清空编辑框。

void MyDialog::on_OpenTimer1Btn_clicked()
{
    if(ui->OpenTimer1Btn->text()==QString("打开定时器1"))
    {
        Mytimer=new QTimer(this);
        Mytimer->setTimerType(Qt::TimerType::PreciseTimer);
        connect(Mytimer,SIGNAL(timeout()),this,SLOT(MyTimerFunc()));
        Mytimer->start(ui->TimeInterval1Edt->text().toInt());
        ui->OpenTimer1Btn->setText(QString("关闭定时器1"));
    }
    else
    {
        if(Mytimer!=NULL)
        {
            Mytimer->stop();
            Mytimer->deleteLater();
            Mytimer=NULL;
            ui->TimeCount1Edt->clear();
            ui->OpenTimer1Btn->setText(QString("打开定时器1"));
        }
    }
}

(3)定时器1的事件函数

我这里例子中的事件函数比较简单,仅仅在函数中对计数变量加1并显示,代码如下:

void MyDialog::MyTimerFunc()
{
    if(Timer1Count==10000)
        Timer1Count=0;
    else
        Timer1Count++;
    ui->TimeCount1Edt->setText(QString("%1").arg(Timer1Count));
}

(4)定时器2的设计
定时器2的设计比较麻烦,我们先来看一看自己编写的线程类中的头文件

class MyThread : public QThread
{
    Q_OBJECT
public:
    MyThread(int timeinterval);
    ~MyThread();
    QLineEdit* mythreadEdt;
    void SetUiEdt(QLineEdit* p) {mythreadEdt=p;}
private:
    int MyTimerInterval;
    int MyCount;
protected:
    void run();
private slots:
    void MyTimerFunc();
signals:
    void MySignalChangeEdt(const QString&);
};

根据线程的相关知识,我们在继承QThread的时候,必须重写基类的run()函数,函数中的内容就是我们需要在线程中执行的内容。那么我们在设计的过程中,打开和关闭定时器2的操作实际上是打开和关闭子线程的操作,在子线程的run()函数中创建定时器对象,并且在子线程的定时器事件函数执行计数值累加的操作。具体我们可以看到打开和关闭定时器2的代码如下:

void MyDialog::on_OpenTimer2Btn_clicked()
{
    if(ui->OpenTimer2Btn->text()==QString("打开定时器2"))
    {
        mythread=new MyThread(ui->TimeInterval2Edt->text().toInt());
        connect(mythread,&MyThread::finished,this,&MyDialog::closeMythread);
        mythread->SetUiEdt(ui->TimeCount2Edt);
        mythread->start();
        ui->OpenTimer2Btn->setText(QString("关闭定时器2"));
    }
    else
    {
        mythread->exit();
        ui->OpenTimer2Btn->setText(QString("打开定时器2"));
    }
}

这里我们为指针变量mythread创建一个MyThread对象,构造函数的参数是时间间隔,当然你可以设计别的方法将编辑框中的数据传递给线程对象,然后将线程对象的finished()信号,和我们自己定义的closeMythread函数连接起来,finished()信号是QThread类自带的一个信号,当run()函数执行完以后自动发出,这时候代表这个线程把它该做的事情都做完了,我们就可以利用deleteLater()函数释放掉指针,最后将mythread指针赋为空指针,防止它成为野指针。

void MyDialog::closeMythread()
{
    mythread->deleteLater();
    mythread=NULL;
}

SetUiEdt()函数的目的是将我们对话框上的用于显示计数变量的编辑框的指针传递给MyThread类,MyThread类有一个QLineEdit类型的指针变量接收,通过这种方式我们就可以在MyThread类的函数中操作对话框上的控件了。最后,我们使用start()函数开启线程。
在关闭定时器2的操作中,我们使用exit()函数停止线程,这一点后边会详细说明,同时,因为线程结束会发出finish()信号,我们已经将这个信号和释放指针等操作连接起来了,所以这里就不用再写这些操作了。
(4)子线程的设计
在线程类的构造函数中,我们初始化编辑控件变量指针和相关参数,moveToThread(this)这句话的意思将在后面解释。

MyThread::MyThread(int timeinterval)
{
    MyTimerInterval=timeinterval;
    MyCount=0;
    mythreadEdt=NULL;
    moveToThread(this);
}

析构函数中,清空编辑框内容,并将指针设置为空指针

MyThread::~MyThread()
{
    if(mythreadEdt!=NULL)
    {
        mythreadEdt->clear();
        mythreadEdt=NULL;
    }
};

接下来我们看看线程函数中要执行的内容:

void MyThread::run()
{
    connect(this,SIGNAL(MySignalChangeEdt(const QString &)),
                       mythreadEdt,SLOT(setText(const QString &)));
    QTimer *MyThreadQtimer=new QTimer();
    MyThreadQtimer->setTimerType(Qt::TimerType::PreciseTimer);
    connect(MyThreadQtimer,SIGNAL(timeout()),this,SLOT(MyTimerFunc()));
    MyThreadQtimer->start(MyTimerInterval);
    exec();
    if(MyThreadQtimer!=NULL)
    {
        MyThreadQtimer->deleteLater();
        MyThreadQtimer=NULL;
    }
}

第一个connect中的MySignalChangeEdt是我们自己定义的信号,连接的槽函数是编辑框控件指针的setText函数,效果就是如果有MySignalChangeEdt信号产生,那么就会根据信号的参数设置GUI界面编辑框中显示的内容,用于显示计数值。
接下来关于创建和设置定时器的内容上面已经说过,这里就不在重复说明了,紧接着我们调用 exec(),那么程序就会进入事件循环,在这里一直等待事件的发生,直到exit()函数被调用,exec()函数才会返回,执行下面释放QTimer对象的操作,然后退出线程,最后退出线程以后,线程对象就会自动发送finished()信号,发射过程用户是不可见的。
定时器的事件处理函数如下,每次定时时间到了,计数变量加一,并且发送MySignalChangeEdt()信号用于在编辑框中显示。

void MyThread::MyTimerFunc()
{
    if(MyCount==10000)
        MyCount=0;
    else
        MyCount++;
    emit MySignalChangeEdt(QString("%1").arg(MyCount));
}

注意:
以下内容参考自链接:QT 信号和槽在哪个线程执行问题
在这里解释一下moveToThread(this)这句话的意思,我们知道run()函数是在新线程中运行的,那我们的MyTimerFunc()是在哪儿运行的呢?实际上这是由connect函数连接时的连接方式确定的,这里给出connect函数的三种连接方式说明

  • 自动连接(Auto Connection)

     这是默认设置
     如果信号在接收者所依附的线程内发射,则等同于直接连接
     如果发射信号的线程和接受者所依附的线程不同,则等同于队列连接
     也就是这说,只存在下面两种情况
    
  • 直接连接(Direct Connection)

     当信号发射时,槽函数将直接被调用。
     无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行。
    
  • 队列连接(Queued Connection)

     当控制权回到接受者所依附线程的事件循环时,槽函数被调用。
     槽函数在接收者所依附线程执行。
    

对照我们程序中的语句connect(MyThreadQtimer,SIGNAL(timeout()),this,SLOT(MyTimerFunc()));发出信号的QTimer对象是在新线程内,接收槽函数是在MyThread类中定义的,而MyThread类的对象依附于主线程,所以属于队列连接,槽函数也就是MyTimerFunc()就会在主线程中执行,而这显然与我们想要的不符,我们想在新的线程内执行定时器服务程序以提高执行效率,因此由两种解决方法:
1将connect函数的最后一个参数设置为直接连接,但这样子需要在槽函数增加线程同步的操作,非常麻烦。
2.在线程派生类的构造函数中增加语句moveToThread(this),就是本文用到的,但这种方法不推荐

推荐的一种方法如下:
我们新建一个类,在这个类中只做一件事,就是执行我们的定时服务程序,关于这个类的声明如下所示:

class MyObject:public QObject
{
    Q_OBJECT
public:
    int MyCount;
    MyObject();
    ~MyObject();
signals:
    void MySignalChangeEdt(const QString&);
public slots:
    void MyTimerFunc();
};

那么线程的run函数变为:

void MyThread::run()
{
    MyObject obj;
    connect(&obj,SIGNAL(MySignalChangeEdt(const QString &)),
                        mythreadEdt,SLOT(setText(const QString &)));
    QTimer *MyThreadQtimer=new QTimer();
    MyThreadQtimer->setTimerType(Qt::TimerType::PreciseTimer);
    connect(MyThreadQtimer,SIGNAL(timeout()),&obj,SLOT(MyTimerFunc()));
    MyThreadQtimer->start(MyTimerInterval);
    exec();
    if(MyThreadQtimer!=NULL)
    {
        MyThreadQtimer->deleteLater();
        MyThreadQtimer=NULL;
    }
}

这里我们使用connect函数将timesignal()信号和MyObject类的对象的MyTimerFunc()函数连接起来,那么对于这个函数,它的信号发起者依附于新线程,因为QTimer对象是在新线程中创建的,接收者同样也依附于新线程,因为MyObect类的对象也是在新线程中创建,此时的连接就是直接连接,那么槽函数就会在新线程中运行了。

你可能感兴趣的:(qt,学习,c++)