23 March 2012

Posted by: Duncan
Tags: Qt | C++

UI with Qt is all very well, but what about plotting some data?

Oddly, standard Qt is supplied with no ready-to-use dataviz capability. I say oddly because Qt is the platform of choice for desktop instrumentation and visualisation applications, taking the baton from X11 (on which it is based). Unlike admittedly (and ancient) Windows-only Borland Delphi and C++ Builder, both of which I used a lot in the distant past, Qt has nothing at all apart from a raft of sophisticated primitives to help you roll your own.

A quick rummage of the interweb revealed a couple of expensive third-party bolt-ons and a freebie from Uwe Rathman called Qwt (Qt Widgets for Technical Applications). This short blog entry is about my initial experience with Uwe's oeuvre.

The App

The requirement was for a desktop application that logged and displayed sea-surface data derived from off-shore buoys. The buoys transmitted data over GPRS at a 1-second frequency. The aim was to provide everything needed to select an optimal site for a tethered dynamo that would generate electricity from wave energy.

The application needed to be cross-platform, operate without any network connection other than with a GPRS receiver, require little or no training for its users, and be simplicity itself to install and configure. Its displays had to show raw and averaged sea state maxima alongside environmental data such as wind speed and direction. How hard could this be?

(Almost) instant gratification

It turned out to be not very hard at all. I could subclass Uwe's QwtPlot class;

WavePlot::WavePlot(QWidget *parent, bool scrollable ):QwtPlot(parent)
{
   wavePlot = this;

   this->scrollable = scrollable;
   this->setAutoReplot(true);

   QString colourName = theApp->appProperties->getStringProperty("wavePlotBG");
   QColor c;
   c.setNamedColor(colourName);
   setCanvasColor(c);
   setCanvasLineWidth(1);

   QwtPlotGrid *grid = new QwtPlotGrid;
   grid->setMajPen(QPen(Qt::darkGray, 0, Qt::DotLine));
   grid->attach(this);

   // axes n title
   setAxisScale(QwtPlot::xBottom, 0.0, 200.0);
   setAxisScale(QwtPlot::yLeft, -10.0, 10.0);
   setAxisTitle(yLeft, QwtText("Sensor to Water Surface (metres)") );

   // Avoid jumping when label with 3 digits
   // appear/disappear when scrolling vertically
   QwtScaleDraw *sd = axisScaleDraw(QwtPlot::yLeft);
   sd->setMinimumExtent( sd->extent(QPen(), axisWidget(QwtPlot::yLeft)->font()));

   plotLayout()->setAlignCanvasToScales(true);

   replot();

   // scroll wheel...
   if ( scrollable ) 
   {
      d_wheel = new QwtWheel(canvas());
      d_wheel->setOrientation(Qt::Horizontal);
      d_wheel->setRange(0, 900);
      d_wheel->setValue(0.0);
      d_wheel->setMass(0.05);
      d_wheel->setTotalAngle(2 * 360.0);
      connect(d_wheel, SIGNAL(valueChanged(double)),SLOT(scrollBottomAxis(double)));
      // we need the resize events, to lay out the wheel
      canvas()->installEventFilter(this);
      //canvas()->setCursor(Qt::PointingHandCursor);
      d_wheel->setCursor(Qt::PointingHandCursor);
   }

   colourName = theApp->appProperties->getStringProperty("wavePlotDatumCol");
   c.setNamedColor(colourName);

   int wid = theApp->appProperties->getIntProperty("wavePlotDatumWidth");

   mean_water_curve = new QwtPlotCurve();
   mean_water_curve->setPen(QPen(c, wid, Qt::SolidLine));
   mean_water_curve->attach(this);

   colourName = theApp->appProperties->getStringProperty("wavePlotCurveCol");
   c.setNamedColor(colourName);
   wid = theApp->appProperties->getIntProperty("wavePlotCurveWidth");

   wave_curve = new QwtPlotCurve();
   wave_curve->setPen(QPen(c, wid, Qt::SolidLine));
   wave_curve->attach(this);

   colourName = theApp->appProperties->getStringProperty("wavePlotZeroCrossingCol");
   c.setNamedColor(colourName);
   wid = theApp->appProperties->getIntProperty("wavePlotZeroCrossingWidth");
   zero_crossing_curve = new QwtPlotCurve();
   zero_crossing_curve->setPen(QPen(c, wid, Qt::SolidLine));
   zero_crossing_curve->attach(this);
   zero_crossing_curve->setStyle(QwtPlotCurve::Sticks);
}

Incoming data was handled by PostgreSQL via Qt's easy-to-use (but a sod to deploy) pgsql driver. Data was managed as base 64 blobs and added to the wave height curve as;

   setAxisScale(QwtPlot::yLeft, max_y, min_y);
   setAxisTitle(yLeft, QwtText("Wave height above datum (metres)") );
   wave_curve->setData ( xbuf, ysrc, currWaveHeights.num_recs );

Resulting in;

A similar approach was taken with histograms, although not quite as straightforward as curves as QwtPainter objects are required for the rendering. A bit of fiddling eventually bore fruit and enabled me to construct a generic histogram class. Here's a sample of the finished display;

Having finished the project I had a long, hard think about C++. While it was fun playing around with the dataviz (it always is!), the rest of the app wasn't fun to do at all. Having been using Python for a few years now, it was clear that a Python solution (notwithstanding some minor deployment issues) would have been built in far less time, at less cost to the client, and with no appreciable degradation in performance. This may well have been my last C++ project. I hope so anyway!

Links