【问题标题】:Fast update of many widgets in Qt GUIQt GUI 中许多小部件的快速更新
【发布时间】:2014-03-07 00:09:31
【问题描述】:

对于必须在屏幕上显示大量数据且更新率很高的应用程序,我很难理解最佳方法。我正在用 Qt for windows 编写这个应用程序。我不会详细介绍实际应用程序,但我在下面编写了一个示例应用程序来演示该问题。

在此我有一个正在计算值的线程。在这种情况下,它是一个简单的计数器值。在实际应用中,它有很多价值。这是每毫秒更新一次。该速率是数据所需的计算速率,而不是 GUI 所需的更新速率。如前所述,这是在它自己的线程中完成的。这个线程的想法是它只关心数据的计算,而不关心它的显示。

现在要更新此示例中的数据显示,我使用 QLabels 网格多次显示值(模拟许多不同值的显示)。我从 Qt 文档中了解到,Widget 的更新必须在主 GUI 线程中完成。所以我在这里所做的是让线程计算值以在每次重新计算时(1ms)发出一个带有计算值的信号。然后将其连接到主 GUI 线程,然后依次更新每个小部件以显示值。

这样做的结论是:

  1. GUI 线程被数据线程的 1ms 更新所淹没,因此显示更新变得非常缓慢。在我的机器上,更新了 100 个小部件,我估计更新速度约为 4fps。
  2. GUI 的所有其他方面(例如移动、调整大小和按下按钮)都在努力争取处理器时间。
  3. 似乎只用文本更新一个简单的 QLabel 似乎很慢。这正常吗?
  4. 我也为此添加了计时代码,GUI 线程中的 1 毫秒更新平均运行时间约为 1.8 毫秒,最大增量时间为 40-75 毫秒。因此,即使落后,它的运行速度也非常快。但大概在幕后,其他事件正在 GUI 线程事件队列上进行,以实际将更新绘制到屏幕上,这些都非常困难。
  5. 我真的不明白是什么决定了实际的屏幕更新率? Qt 如何决定何时更新屏幕?

最重要的是,我不确定更新要显示的数据的正确方法是什么。显然,屏幕不需要在 1 毫秒内更新,即使它可以,显示器也不会刷新得那么快。另一方面,我不希望我的数据线程与屏幕更新率有关。有没有更好的方法可以在不占用 GUI 事件队列的情况下将数据从数据线程获取到 GUI 线程?

任何对 Qt 方法解决此问题的见解将不胜感激。

这是在自己的线程中运行的数据生成器:

class Generator : public QObject
{
    Q_OBJECT
public:
    explicit Generator(QObject *parent = 0);

signals:
    void    dataAvailable(int val);
public slots:
    void    run(bool run);
    void    update(void);
private:
    QTimer  *timer;
    int     value;
};

void Generator::run(bool run)
{
    if(run)
    {
        value = 0;
        timer = new QTimer;
        connect(timer, SIGNAL(timeout()), this, SLOT(update()));
        timer->start(1);
    } else
    {
        timer->stop();
        delete timer;
    }
}

void Generator::update()
{
    value++;
    emit dataAvailable(value);
}

这是更新显示的主要 GUI 类:

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);

private:
    QLabel      *labels[ROW_MAX][COL_MAX];
    Generator   *dataGenerator;

public slots:
    void dataAvailable(int val);

signals:
    void runGenerator(bool run);
};

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent)
{
    QGridLayout *layout = new QGridLayout;
    for(int iCol=0; iCol<COL_MAX; iCol++)
    {
        for(int iRow=0; iRow<ROW_MAX; iRow++)
        {
            QLabel *label = new QLabel("Hello");
            labels[iRow][iCol] = label;
            layout->addWidget(labels[iRow][iCol], iRow, iCol);
        }
    }
    centralWidget->setLayout(layout);

    dataGenerator = new Generator;
    QThread *dgThread = new QThread;
    dataGenerator->moveToThread(dgThread);
    dgThread->start(QThread::HighestPriority);

    connect(this, SIGNAL(runGenerator(bool)), dataGenerator, SLOT(run(bool)));
    connect(dataGenerator, SIGNAL(dataAvailable(int)), this, SLOT(dataAvailable(int)));

    emit runGenerator(true);

}

void MainWindow::dataAvailable(int val)
{
    for(int iCol=0; iCol< COL_MAX; iCol++)
    {
        for(int iRow=0; iRow<ROW_MAX; iRow++)
        {
            labels[iRow][iCol]->setText(QString::number(val));
        }
    }
}

【问题讨论】:

    标签: multithreading performance qt user-interface


    【解决方案1】:

    过去对我有用的一种方法是将访问器方法构建到工作对象中,然后让视图根据自己的更新周期“拉”数据。

    要使用您的示例代码,请将这样的方法添加到 Generator:

    // TODO For a more complex class, you probably want one "get all the stats" 
    // accessor rather than lots of little methods -- that way all the data is 
    // synced up
    int Generator::getCurrentValue()
    {
        QMutexLocker(mutex); // (also add a QMutex member to the class)
        return value;
    }
    

    然后给主窗口自己的更新计时器,它不会对系统造成很大的影响:

    MainWindow::MainWindow(QWidget *parent) :
        QMainWindow(parent)
    {
        // ...
        // Replace dataAvailable code with this:
        updateTimer = new QTimer;
        connect(updateTimer, SIGNAL(timeout()), this, SLOT(updateDisplayedValues());
        updateTimer->start(MY_MAIN_WINDOW_UPDATE_RATE); // some constant; try 200 ms?
        // ...
    }
    
    void MainWindow::updateDisplayedValues()
    {
        int val = dataGenerator->getCurrentValue();
    
        // TODO You might this more efficient by checking whether you *need to*
        // repaint first here; generally Qt is pretty good about not wasting cycles 
        // on currently-hidden widgets anyway
    
        for(int iCol=0; iCol< COL_MAX; iCol++)
        {
            for(int iRow=0; iRow<ROW_MAX; iRow++)
            {
                labels[iRow][iCol]->setText(QString::number(val));
            }
        }
    }
    

    【讨论】:

    • 感谢亚历克斯的建议。是的,这是有道理的。实际上,我只是尝试了类似的方法,其中我的原始 (1ms) 插槽只是保存值但不更新显示。然后我有第二个计时器,就像你建议的那样,以更合理的速度更新显示。我必须承认我非常喜欢使用信号/插槽机制来处理线程交叉问题的想法,而不是互斥锁,互斥锁似乎经常在发生锁定时隐藏情况。并不是说我能想到你的建议有什么问题。
    • 最后一个问题,有谁知道当您尝试更新小部件时发生的情况超过了它们的绘制速度,或者如何知道实际的屏幕更新率是多少?或者换句话说,除了视觉上的效果之外,你怎么知道你超出了性能限制?
    • Qt 将多个 update 调用合并到一个 repaint 中,如果发生的速度比视频缓冲区允许的速度更快。
    • @user2441128 你可以使用信号和槽来越过线程屏障,但是额外的调度可能是一个瓶颈。尽管如此,如果分析表明这是一个瓶颈,尝试并返回此方法并没有什么坏处。
    【解决方案2】:

    如果您只是将计算放入在计时器上运行的生成器中......它的事件循环与 GUI 位于同一线程上。

    如果您将生成器移至其自己的线程,然后设置一个计时器以每秒最多检查 30 次生成器,您应该会看到大部分问题都消失了。告诉它以比人眼感知或显示器刷新率甚至更快的速度在视觉上更新显示通常是矫枉过正的。就普通视频速率而言,NTSC 为 30 fps,PAL 为 25 fps。对于数据的文本标签,我通常需要在一秒钟内进行采样/平均,并在视觉上每秒更新一次以保持可读性,而不仅仅是数字模糊。

    当您开始使用线程时,您应该会看到您的计算机使用不同的内核来管理来自您的应用程序的不同负载。如果您不使用多线程,则在任何要求苛刻的计算方面都可能非常有限。

    另外,当你连接到你的线程函数时,一定要使用QueuedConnection,而不是AutomaticConnection,因为大多数时候,如果你不把它拼出来,它会在去的时候选错一个线程之间。

    希望对您有所帮助。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-15
      • 2021-07-28
      • 1970-01-01
      • 2013-12-21
      • 1970-01-01
      • 2018-08-18
      相关资源
      最近更新 更多