Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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));
        }
    }
}
like image 245
SteveC Avatar asked Feb 09 '14 19:02

SteveC


People also ask

What is QWidget * parent?

The tree-like organization of QWidgets (and in fact of any QObjects ) is part of the memory management strategy used within the Qt-Framework. Assigning a QObject to a parent means that ownership of the child object is transferred to the parent object. If the parent is deleted, all its children are also deleted.

What is QWidget?

The QWidget class is the base class of all user interface objects. The widget is the atom of the user interface: it receives mouse, keyboard and other events from the window system, and paints a representation of itself on the screen. Every widget is rectangular, and they are sorted in a Z-order.

How do I change the size of a widget in Qt?

Custom Widgets in LayoutsReimplement QWidget::sizeHint() to return the preferred size of the widget. Reimplement QWidget::minimumSizeHint() to return the smallest size the widget can have. Call QWidget::setSizePolicy() to specify the space requirements of the widget.

How do you set geometry in Qt?

setGeometry() method is used to set up the geometry of the PyQt5 window it self.


2 Answers

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));
        }
    }
}
like image 52
Alex P Avatar answered Sep 21 '22 09:09

Alex P


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.

like image 21
phyatt Avatar answered Sep 25 '22 09:09

phyatt