I'm writing a GUI for different VME modules (electronics). There are several of them which are used for the data acquisition. For example, an ADC produces the digitized maximum voltage on its input. So I need a way to visualize this data. Almost always this way is a histogram. So I've chosen the Qwt library. The straightforward solution would be to connect the QTimer
's timeout signal to a slot in which the data is read and then to update a histogram. But it's bad, because no one knows in advance how long the reading and the processing would take, and most likely it would block the main loop. So, multi-threading.
The core logic was stolen from this Qt's example.
I think this code would be useful for people with the similar needs.
P.S. Sorry for the long post :)
We have two classes in this example:
DataGeneratorThread
: is aQThread
and is responsible for the actual data generation (generates random numbers according to a gaussian p.d.f. )RealTimeHistogram
: the main widget, a user interface
DataGeneratorThread
class
DataGeneratorThread.h
#pragma once
#include <QThread>
#include <QMutex>
#include <QWaitCondition>
#include <QMetaType>
#include "qwt_samples.h"
Q_DECLARE_METATYPE(QVector<QwtIntervalSample>)
class DataGeneratorThread : public QThread
{
Q_OBJECT
public :
DataGeneratorThread( QObject *parent = nullptr );
~DataGeneratorThread() override;
public slots :
// These slots are the bridge between this thread
// and the main one: they are to be called from the
// parent thread
void OnStart( size_t size );
void OnStop();
void OnPause();
void OnResume();
void Dump();
signals :
// This signal is how this thread returns data
// it is connected to the slot in the parent thread
void DataGenerated( const QVector<QwtIntervalSample>& data, unsigned nEvents );
protected :
// The main function
void run() override;
private :
// Flags to manipulate the control flow in
// the run() function
bool fAbort = false;
bool fPause = false;
bool fRestart = false;
bool fDumpRequest = false;
// Mutex to protect data access
QMutex fMutex;
// Condition to be able to put this thread
// in the sleeping state when needed
QWaitCondition fCondition;
// Parameter of data generation
size_t fSize;
};
DataGenerator.cpp
#include <algorithm>
#include <random>
#include <QMutexLocker>
#include "DataGenerator.h"
DataGeneratorThread::DataGeneratorThread( QObject* parent ) :
QThread( parent )
{
qRegisterMetaType<QVector<QwtIntervalSample>>();
}
DataGeneratorThread::~DataGeneratorThread()
{
fMutex.lock();
fAbort = true;// Signal to return from run() (see below)
fCondition.wakeOne();// Might be sleeping so awake
fMutex.unlock();
wait();// Wait for return from run() before QThread::~QThread()
}
The communication between this thread and the main one are performed through 5 public slots (they are to be called on the main thread's side) :
void DataGeneratorThread::OnStart( size_t size )
{
QMutexLocker locker( &fMutex );
fAbort = false;
fPause = false;
fSize = size;
if( !isRunning() )
{
start( LowPriority );
}
else
{
fRestart = true;// Signal to restart calculation (see below)
fCondition.wakeOne();//Might be sleeping so awake
}
}
This function is called when the main thread requests the new data to be generated.
void DataGeneratorThread::OnStop()
{
QMutexLocker locker( &fMutex );
fAbort = true;// Signal to return from run (see below)
fCondition.wakeOne();
}
This function is called when the main thread want to stop this thread.
void DataGeneratorThread::OnPause()
{
QMutexLocker locker( &fMutex );
fPause = true;//
fCondition.wakeOne();
}
This function is called when the main thread wants to temporarily stop data generation. This function puts this thread into sleep (not directly though, see the run()
function).
void DataGeneratorThread::OnResume()
{
QMutexLocker locker( &fMutex );
fPause = false;
fCondition.wakeOne();
}
This function is complementary to the OnPause()
.
void DataGeneratorThread::Dump()
{
QMutexLocker locker( &fMutex );
fDumpRequest = true;
}
This function signals that the main thread want to read the generated data.
As you might notice, there is the fCondition.wakeOne()
line in all but the last slots. This line wakes this thread if it's sleeping (see the run()
function below). We don't awake the thread in the Dump()
function intentionally, so we sure that there is no data update (no DataGenerated
signal is emitted) when the thread is sleeping.
void DataGeneratorThread::run()
{
forever
{
fMutex.lock();
const size_t size = fSize;
fMutex.unlock();
QVector<QwtIntervalSample> data( size );
unsigned n = 0;
// Initialize histogram
for( size_t i = 0; i < size; ++i )
{
data[i] = QwtIntervalSample( 0.0, i, i + 1 );
}
std::random_device rd;
std::mt19937 gen( rd() );
forever
{
if( fAbort )// Accessible from the OnStart() and OnStop()
{
return;
}
if( fDumpRequest )// Accessible from the Dump()
{
emit DataGenerated( data, n );// This signal is catched by the RealTimeHistogram
fMutex.lock();
fDumpRequest = false;
fMutex.unlock();
}
if( fPause )
{
fMutex.lock();
fCondition.wait( &fMutex );// Go to sleep
fMutex.unlock();
}
if( fRestart )
{
break;
}
std::normal_distribution<> g( size / 2., size / 20. );
size_t rand = std::round( g( gen ) );
if( rand < size )
{
data[ rand ].value += 1;// Fill histogram
n++;
}
}
// The only way to break from the innermost forever loop is the fRestart flag
fMutex.lock();
fRestart = false;
fMutex.unlock();
}
}
As you can see the run()
function consists of an infinite loop. The only way to return from it is the fAbort
flag. The meaning of other flags is obvious: fPause
puts this thread into sleep; fRestart
restarts data generation; fDumpRequest
signals that the data must be sent to the main thread.
RealTimeHistogram
class
This class contains the user interface and is supposed to be instantiated in the main thread. It is shown on the gif in the beginning of the post.
RealTimeHistogram.h
#pragma once
#include <QWidget>
#include "DataGenerator.h"
class QTimer;
class QPushButton;
class QwtPlot;
class QwtPlotHistogram;
class QwtPlotMarker;
class RealTimeHistogram : public QWidget
{
Q_OBJECT
private :
static constexpr size_t N_BINS = 4096;
QTimer *fTimer;
QPushButton *fStartButton, *fStopButton, *fPauseButton;
QwtPlot *fPlot;
QwtPlotMarker *fStatistics;
QwtPlotHistogram *fHisto;
DataGeneratorThread fThread;
private :
void CreateWidgets();
void CreateTimer();
public slots :
void InitHistogram();
void UpdateData( const QVector<QwtIntervalSample>& data, unsigned nEvents );
void UpdateStat( unsigned nEvents );
void StartTimer();
void StopTimer();
void Pause();
public :
RealTimeHistogram( QWidget* parent = nullptr );
~RealTimeHistogram() override = default;
};
RealTimeHistogram.cpp
#include <QPushButton>
#include <QTimer>
#include <QFrame>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <qwt_plot.h>
#include <qwt_plot_histogram.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_layout.h>
#include <qwt_legend.h>
#include <qwt_plot_marker.h>
#include "RealTimeHistogram.h"
RealTimeHistogram::RealTimeHistogram( QWidget* parent ) :
QWidget( parent )
{
CreateWidgets();
CreateTimer();
connect( &fThread, &DataGeneratorThread::DataGenerated, this, &RealTimeHistogram::UpdateData );
}
This is how the data goes from the DataGeneratorThread
: this class "catches" the signal with the data provided as arguments.
void RealTimeHistogram::CreateWidgets()
{
QVBoxLayout* vLayout = new QVBoxLayout();
// Buttons
fStartButton = new QPushButton( "START" );
fStartButton->setStyleSheet( "background-color: #a8ffe9" );
connect( fStartButton, &QPushButton::clicked, this, &RealTimeHistogram::InitHistogram );
connect( fStartButton, &QPushButton::clicked, this, &RealTimeHistogram::StartTimer );
fPauseButton = new QPushButton( "PAUSE" );
fPauseButton->setStyleSheet( "background-color: #b6b6b6" );
connect( fPauseButton, &QPushButton::clicked, this, &RealTimeHistogram::Pause );
fStopButton = new QPushButton( "STOP" );
fStopButton->setStyleSheet( "background-color: #6d6d6d; color: #ffffff" );
connect( fStopButton, &QPushButton::clicked, &fThread, &DataGeneratorThread::OnStop );
connect( fStopButton, &QPushButton::clicked, this, &RealTimeHistogram::StopTimer );
QFrame *buttonFrame = new QFrame();
QHBoxLayout *buttonLayout = new QHBoxLayout();
buttonLayout->addWidget( fStartButton );
buttonLayout->addWidget( fPauseButton );
buttonLayout->addWidget( fStopButton );
buttonFrame->setLayout( buttonLayout );
vLayout->addWidget( buttonFrame );
// Histogram
fPlot = new QwtPlot();
fPlot->setAxisTitle( QwtPlot::yLeft, "Number of events" );
fPlot->setAxisTitle( QwtPlot::xBottom, "Value" );
fPlot->setCanvasBackground( QColor( "#b6b6b6" ) );
fPlot->plotLayout()->setAlignCanvasToScales( true );
fPlot->insertLegend( new QwtLegend(), QwtPlot::RightLegend );
QwtPlotGrid *grid = new QwtPlotGrid;
grid->enableX( true );
grid->enableY( true );
grid->attach( fPlot );
grid->setMajorPen( QPen( QColor( "#ffffff" ), 0, Qt::DotLine ) );
fHisto = new QwtPlotHistogram( "Data" );
fHisto->setPen( QPen( QColor( "#a8ffe9" ) ) );
fHisto->setBrush( QBrush( QColor( "#a8ffe9" ) ) );
fHisto->attach( fPlot );
fStatistics = new QwtPlotMarker();
fStatistics->attach( fPlot );
fPlot->show();
fPlot->replot();
UpdateStat( 0 );
vLayout->addWidget( fPlot );
setLayout( vLayout );
}
This function creates widgets which is irrelevant to the main topic. Although it is worth to note that those slots in the DataGeneratorThread
are connected to the clicked
signals of the buttons.
void RealTimeHistogram::CreateTimer()
{
fTimer = new QTimer( this );
fTimer->setInterval( 1000 );
connect( fTimer, &QTimer::timeout, &fThread, &DataGeneratorThread::Dump );
}
That's it : the timer just generates a dump request every second.
void RealTimeHistogram::InitHistogram()
{
QVector<QwtIntervalSample> histData;
for( size_t i = 0; i < N_BINS; ++i )
{
histData.push_back( QwtIntervalSample( 0.0, i, i + 1 ) );
}
fHisto->setSamples( histData );
fThread.OnStart( N_BINS );
}
Nothing special here (and maybe even redundant), except the last line which starts (or restarts) data generation.
void RealTimeHistogram::StartTimer()
{
if( not fTimer->isActive() )
{
fTimer->start();
}
}
void RealTimeHistogram::StopTimer()
{
if( fTimer->isActive() )
{
fTimer->stop();
}
}
This is how we manage the timer.
void RealTimeHistogram::Pause()
{
QPushButton* button = qobject_cast<QPushButton*>( this->sender() );
if( button->text() == "PAUSE" )
{
fThread.OnPause();
button->setText( "RESUME" );
}
else if( button->text() == "RESUME" )
{
fThread.OnResume();
button->setText( "PAUSE" );
}
}
Depending on the name of the button this function either pauses or resumes data generation.
void RealTimeHistogram::UpdateData( const QVector<QwtIntervalSample>& data, unsigned nEvents )
{
fHisto->setSamples( data );
fPlot->replot();
UpdateStat( nEvents );
}
Here is how we use the generated data. Remember this slot is connected to the DataGeneratorThread::DataGenerated()
signal.
void RealTimeHistogram::UpdateStat( unsigned nEvents )
{
QwtScaleMap xMap = fPlot->canvasMap( QwtPlot::xBottom );
double x = (xMap.s1() + xMap.s2()) * 0.5;
QwtScaleMap yMap = fPlot->canvasMap( QwtPlot::yLeft );
double y = (yMap.s1() + yMap.s2()) * 0.3;
fStatistics->setValue( x, y );
fStatistics->setLabel( QwtText( QString( "Number of events = %1" ).arg( nEvents ) ) );
}
The function to place the label with the number of events correctly on the canvas.
EDIT
Full code can be found at this repo.