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:
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));
}
}
}
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.
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.
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.
setGeometry() method is used to set up the geometry of the PyQt5 window it self.
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));
}
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With