SteveC
SteveC

Reputation: 111

Fast update of many widgets in Qt GUI

I'm having difficulty understanding the best approach for an appliction that must display a large amount of data to the screen, which is being updated at a high rate. I'm writing this application in Qt for windows. I won't go in to details of the actual application, but I've written an example application below to demonstrate the problem.

In this I have a thread which is calculating values. In this case it's one value which is simply a counter. In the real application its lots of values. This is updating once per millisecond. This rate is the required calculation rate of the data, not the required update rate for the GUI. As mentioned this is done in its own thread. The idea of this thread is that its just about the calculation of the data, and doesn't care about display of it.

Now to update the display of the data in this example i'm using a grid of QLabels to display the value multiple times (simulating the display of many different values). I understand from the Qt documentation that the update of Widgets must be done in the main GUI thread. So what I do here is I get the thread calculatingthe values to emit a signal with the calculated value every time it recalculates it (1ms). This is then connected to the main GUI thred, which then updates each widget in turn to display the value.

The conclusion from doing this is:

  1. The GUI thread is being swamped by the 1ms update from the data thread, so the display becomes very slow to update. On my machine, with 100 widgets updating I estimate the update to be approx 4fps.
  2. All other aspects of the GUI such as moving, resizing and button presses are all struggling to get processor time.
  3. It seems that updating just a simple QLabel with text seems quite slow. Is this normal?
  4. I've added timing code to this as well and the 1ms update in the GUI thread runs on avergae approx 1.8ms, with a max delta time of 40-75ms. So its running quite fast even though its getting behind. But presumably behind the scenes other events are going on the GUI thread event queue to actually paint updates to the screen and these are really struggling.
  5. I don't really understand what is determining the actual screen update rate? How is Qt deciding when to update the screen here?

Most importantly I'm not sure what the correct way to update the data to be displayed is. Clearly the screen doesn't need to update at 1ms and even if it could the monitor doesn't refresh that quick anyway. On the other hand I don't want my data thread to be concerning itself with the screen update rate. Is there a better way to get the data from the data thread to the GUi one without swamping the GUI event queue?

Any insight into the Qt approach to this problem would be greatly appreciated.

Here's the data generator that runs in its own thread:

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);
}

And here's the main GUI class that updates the dislay:

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));
        }
    }
}

Upvotes: 7

Views: 5775

Answers (2)

phyatt
phyatt

Reputation: 19112

If you are just putting your calculation into a generator that runs on a timer... its event loop is on the same thread as the GUI.

If you move the generator to its own thread and then set a timer to check on the generator at most 30 times per second, you should see most of the issues disappear. Telling it to visually update the display faster than what the human eye can perceive or the monitor refresh rate can even do is usually overkill. NTSC does 30 fps, and PAL does 25 fps, as far as common video rates go. For text labels of data, I've usually needed to sample/average across a second and update it visually once a second to keep it readable and not just a blur of numbers.

When you start using threads, you should see your computer use different cores to manage the different loads from your application. If you don't use multithreading, you can be very limited very fast with any demanding calculations.

Also when you connect to your threaded function, be sure to use a QueuedConnection, not the AutomaticConnection, because most of the time, if you don't spell it out, it will pick the wrong one when going between threads.

Hope that helps.

Upvotes: 1

Alex P
Alex P

Reputation: 1609

One approach that has worked for me in the past is to build accessor methods into the worker object and then have the view "pull" the data based on its own update cycle.

To use your example code, add a method like this to 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;
}

Then give the main window its own update timer that won't hammer the system very hard:

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));
        }
    }
}

Upvotes: 3

Related Questions