使用多层信号从线程更新QML是否正确?

问题描述:

我正在写一个演示应用程序来缓解我的QT学习曲线。我的目标是更新来自作为数据生成器在后台运行的线程的值。我编写了QML,并使用QT标准数据绑定方法(即Q_Property)将C++成员绑定到它。目前该解决方案按预期工作,但希望确认这是否是实施相同的正确方法。使用多层信号从线程更新QML是否正确?

思想

  1. 在一个线程(类DemoData)生成数据
  2. 发射信号,以通知另一个类(类VitalData)
  3. 发光Q_PROPERTY信号(来自类VitalData)来更新UI

查询

  1. 我应该生成数据并通知UI有关单个类中的更改并将该类实例发送到新线程吗?在这种情况下,我可以使用单个信号来更新UI。
  2. 基于目前的设计是否会受到糟糕的性能影响,或者在最糟糕的情况下,由于快速信号时隙,UI部分可能会遗漏一些数据?

我的目标是保持数据生成器类的解耦。

最后的代码

//A data generator class - this can be altered by some other class if neccessary 
class DemoData : public QObject 
{ 
    Q_OBJECT 
    int nextUpdateIndex = 0; 

public slots: 
    void generateData() 
    { 
     int hrValIndex = 0, spo2ValIndex = 0, respValIndex = 0, co2ValIndex = 0; 

     while(true) { 
      switch(nextUpdateIndex) { 
      case 0: 
       emit valueUpdated(nextUpdateIndex, demoHRRates[hrValIndex]); 
       if(hrValIndex == ((sizeof demoHRRates)/(sizeof(int))) - 1) 
        hrValIndex = 0; 
       else 
        hrValIndex++; 
       nextUpdateIndex = 1; 
       break; 
      } 
      QThread::sleep(1); 
     } 
    } 
signals: 
    //Signal to notify the UI about new value 
    void valueUpdated(int index, int data); 
}; 


//Class to interact with QML UI layer. This class only hold properties and it's binding 
class VitalData : public QObject 
{ 
    Q_OBJECT 
    Q_PROPERTY(int hrRate READ getHrRate NOTIFY hrRateChanged) 

    public: 
    int getHrRate() const { 
     return m_hrRate; 
    } 

public slots: 
    void getData(int index, int value) 
    { 
     switch(index){ 
     case 0: 
      m_hrRate = value; 
      emit hrRateChanged(); 
      break; 
     } 
    } 

signals: 
    //This signal actually notifies QML to update it value 
    void hrRateChanged(); 
}; 

int main() 
{ 
    QGuiApplication app(argc, argv); 

    //Data generator class is getting linked with UI data feeder class 
    VitalData med; 
    DemoData demo; 
    QObject::connect(&demo, SIGNAL(valueUpdated(int, int)), &med, SLOT(getData(int, int))); 

    //Standard way to launch QML view 
    QQuickView view; 
    view.rootContext()->setContextProperty("med", &med); 
    view.setSource(QUrl(QStringLiteral("qrc:/main.qml"))); 
    view.show(); 

    //Moving data generator to a background thread 
    QThread thread; 
    demo.moveToThread(&thread); 
    QObject::connect(&thread, SIGNAL(started()), &demo, SLOT(generateData())); 
    thread.start(); 

    return app.exec(); 
} 

为线程退出

int main() 
{ 
    QThread thread; 
    demo.moveToThread(&thread); 
    QObject::connect(&thread, SIGNAL(started()), &demo, SLOT(generateData())); 
    QObject::connect(qApp, &QCoreApplication::aboutToQuit, &thread, [&thread](){ 
     thread.requestInterruption(); 
     thread.wait(); 
    }); 
    thread.start(); 
} 


class DemoData : public QObject 
{ 
    Q_OBJECT 
public slots: 
    void generateData() 
    { 
     while(!QThread::currentThread()->isInterruptionRequested()) { 
      switch(nextUpdateIndex) { 
       case 0: 
       break; 
      } 
      QThread::msleep(200); 
      qDebug() << "Thread running.."; 
     } 

     //This quit was necessary. Otherwise even with requestInterruption call thread was not closing though the above debug log stopped 
     QThread::currentThread()->quit(); 
    } 
}; 

新的代码对于广大的设计:

我看不错。就个人而言,我总是在运行之前运行moveToThread,但这不应该影响这种情况下的结果。 (唯一令人困惑的是,您将方法命名为getData,它不是一个吸气器,应该相应命名)

但是,您的数据生成是可能的,但不是最优的。使用QThread::sleep(1),您将阻止事件回调,从而无法优雅地停止线程。相反,你应该使用一个计时器。定时器和DemoData类仍然在该线程上运行,但是通过使用timer和eventloop。这样的的QThread仍然可以接收事件等(例如,如果你需要数据后发送到你的类,你可以使用一个插槽,但只,如果线程的事件循环可以运行):

class DemoData : public QObject 
{ 
    Q_OBJECT 
    int nextUpdateIndex = 0; 

public slots: 
    void generateData() 
    { 
     auto timer = new QTimer(this); 
     connect(timer, &QTimer::timeout, this, &DemoData::generate); 
     timer->start(1000); 
    } 

private slots: 
    void generate() 
    { 
     //code to generate data here, without the loop 
     //as this method gets called every second by the timer 
    } 
}; 

还有一种方法,如果你不想使用定时器。您必须重新实现QThread并自己处理事件,但只有在没有其他选择时才应该这样做。您将不得不覆盖QThread::run

优雅地退出线程相当容易,但取决于线程的构建方式。如果您有一个有效的事件回复,即没有长时间阻塞操作,则可以简单地拨打QThread::quitQThread::wait。然而,这只适用于Eventloop正在运行的QThread(因此需要定时器)。

QObject::connect(qApp, &QCoreApplication::aboutToQuit, &thread, [&thread](){ 
    thread.quit(); 
    thread.wait(5000); 
}); 

如果您的线程没有正确运行eventloop,则可以使用中断请求。不要退出,请致电QThread::requestInterruption。在你generateData方法,你就必须用短的时间间隔,并检查QThread::isInterruptionRequested每次:

void generateData() 
{ 
    int hrValIndex = 0, spo2ValIndex = 0, respValIndex = 0, co2ValIndex = 0; 

    while(!QThread::currentThread()->isInterruptionRequested()) { 
     // code... 
     QThread::sleep(1); 
    } 
} 
+0

那么最初我去定时器,转移到线程详细了解QT线程。我来自Win32/MFC/C#的背景,所以在QT中尝试所有可能的东西。是的'getData'应该改成与setter相关的名字,将会改正。最后一个简短的问题。在app退出时优雅地关闭线程的正确方法是什么?我可以通过休息检查将一秒钟的睡眠分成最短的毫秒。正在尝试连接'aboutToQuit()'插槽,但这并没有帮助我。 – Anup

+1

即使QThreads有他们自己的事件循环。我会更新相应的答案 – Felix

+0

如果我正确理解了你的话,我需要把'while(!QThread :: currentThread() - > isInterruptionRequested())'赶上中断并激发'aboutToQuit'信号的中断。即'QObject :: connect(qApp,&QCoreApplication :: aboutToQuit,&thread,[&thread]()thread.requestInterruption(); thread.wait(1000); });' – Anup