Tuesday, February 18, 2014

How to use QCustomPlot library with Qt? - An Introduction

QCustomPlot is a very useful plotting library developed for Qt. One can plot various types of graphs (ex. line graph, bar graph) with flexible parameters. Using Qt's "Signal-Slot" mechanism (click here to know more), these graphs can also be plotted in a real-time scenario. For example, this might be useful when you receive data continuously from a Bluetooth device over the computer's COM port (serial port in Windows) and it is required to visualize the data as it comes. This post is about getting started with including graph plots in Qt using this QCustomPlot library. The necessary (header and source) files for QCustomPlot can be downloaded from the official website. Copy the two files (probably "qcustomplot.h" and "qcustomplot.cpp") into your Qt project's folder. Then right-click on your Qt project in the "Projects" window on the left hand side in Qt Creator and select "Add Existing Files". Choose the two files (that you just copied) in the window and add them to your project. Once you are done, you must see them included in your project's tree under "Headers" and "Sources" for "qcustomplot.h" and "qcustomplot.cpp" respectively.

If you already have a good understanding on general plotting methods, you can look into the examples that are provided in the folder extracted from the QCustomPlot library download from the link above. I will be giving a general overview of including plots in a Qt project and a few extensions to it based on my limited experience with it.

For this post, I will assume the following scenario (for Windows 7): Data is received from a COM port (can be a bluetooth or a USB port) and it is just a "double" value, one per line, with a prefix of "#" to each of them. The computer (basically your Qt project) has to receive each data (identified by a prefix, may be a "#" tag prefix) point and plot it dynamically on a graph.

(My project's main window is controlled by the source-header pair named "mainwindow")

Make sure to have the line "QT += widgets serialport printsupport" in your ".pro" file for the project.

Creating a plotting window in the GUI: (you can skip this if you already know)

  1. Open "mainwindow.ui" under the "Forms" folder in your Qt project's tree.
  2. Extend the "MainWindow" to whatever size is required at the minimum.
  3. Pick and place a "Grid Layout" to the MainWindow and maximize it to fit the whole window.
  4. Pick and place a "Widget" (found under the "Containers" category in the list) inside this grid. Right-click the widget and choose "Promote To".
  5. In the window that appears, give "qcustomplot" in the field named "Promoted class name". You should see the header being automatically defined in the next field as "qcustomplot.h". Check "Global include" and then "Add" and "Promote".
  6. Now ,in the "Objects" window on the right hand side of Qt Creator, you should see the class "QCustomPlot" against your widget's name (which is by default "widget"). I have renamed this widget to be "graph" to be more clear.
  7. Now pick and place two "QPushButton"s and rename them as "start" and "stop". I mean renaming the object and not just the caption that it carries in the GUI. These are just to control the graph.
  8. You are ready for giving "intelligence" to these objects now!

"mainwindow.h":

  1. Make sure to include the Qt Serial Port library with: #include <QtSerialPort/QSerialPort>.
  2. Create a section as "private slots:" if it does not exist already. Under that define the following slots:
    1. void readResponse( ) - to read responses from the serial port
    2. void realtimeDataSlot(double value) - to receive a "value" from readResponse() & plot it
    3. void setupGraph(QCustomPlot *graphPlot) - to set up the "graph" object with properties
    4. void on_start_clicked( ) - to define what happens when you click the "start" button
    5. void on_stop_clicked( ) - to define what happens when you click the "stop" button
  3. In the section named "private:", define the following variables:
    1. QByteArray response; - to read ans store the data from the serial port
    2. QTimer timer; - to control how often the graph is refreshed with a new data point
    3. QSerialPort serial;
    4. bool start = false; - to control when the graph should be plotting
    5. double rx_value, key_x, range_y_min = 0, range_y_max = 10

"mainwindow.cpp":

  1. MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),
    ui(new Ui::MainWindow)
    1. ui->setupUi(this);
    2. if (serial.portName() != QString("COM26")) { // change the COM port!

      serial.close();
      serial.setPortName("COM26");
      if (!serial.open(QIODevice::ReadWrite)) {
      processError(tr("Can't open %1, error code %2")
      .arg(serial.portName()).arg(serial.error()));
      return;
      }
      if (!serial.setBaudRate(QSerialPort::Baud57600)) {
      processError(tr("Can't set rate 9600 baud to port %1, error code %2")
      .arg(serial.portName()).arg(serial.error()));
      return;
      }
      if (!serial.setDataBits(QSerialPort::Data8)) {
      processError(tr("Can't set 8 data bits to port %1, error code %2")
      .arg(serial.portName()).arg(serial.error()));
      return;
      }
      if (!serial.setParity(QSerialPort::NoParity)) {
      processError(tr("Can't set no patity to port %1, error code %2")
      .arg(serial.portName()).arg(serial.error()));
      return;
      }
      if (!serial.setStopBits(QSerialPort::OneStop)) {
      processError(tr("Can't set 1 stop bit to port %1, error code %2")
      .arg(serial.portName()).arg(serial.error()));
      return;
      }
      if (!serial.setFlowControl(QSerialPort::NoFlowControl)) {
      processError(tr("Can't set no flow control to port %1, error code %2")
      .arg(serial.portName()).arg(serial.error()));
      return;
      }
      }
    3. processError( ) is just a function to display the text in its argument. Define it.
  2. void MainWindow::on_start_clicked( )
    1. Disable the "start" button: ui->start->setEnabled(false);
    2. Set the bool variable "start" to true
    3. Connect the "timeout( )" signal of "timer" to the slot "readResponse( )":
      1. connect(&timer, SIGNAL(timeout( )), this, SLOT(readResponse( )));
      2. timer.start(0); - this means the graph is refreshed all the time. If you give the parameter as 100 instead of 0, it means your graph refreshed only every 100ms i.e. the readResponse( ) routine is invoked only every 100ms.
  3. void MainWindow::on_stop_clicked( )
    1. Set the bool variable "stop" to false
    2. Exit the program: exit(0);
  4. void MainWindow::setupGraph(QCustomPlot *graphPlot)
    1. graphPlot->addGraph(); // blue dot 
    2. graphPlot->graph(0)->setPen(QPen(Qt::blue));
      graphPlot->graph(0)->setLineStyle(QCPGraph::lsLine);

    3. graphPlot->addGraph(); // blue dot 
    4. graphPlot->graph(1)->setPen(QPen(Qt::blue));
      graphPlot->graph(1)->setLineStyle(QCPGraph::lsNone);
      graphPlot->graph(1)->setScatterStyle(QCPScatterStyle::ssDisc);
    5. graphPlot->xAxis->setLabel("<-- Time -->");
      graphPlot->xAxis->setTickLabelType(QCPAxis::ltNumber);
      graphPlot->xAxis->setNumberFormat("gb");
      graphPlot->xAxis->setAutoTickStep(true);
    6. graphPlot->yAxis->setTickLabelColor(Qt::black);
      graphPlot->yAxis->setAutoTickStep(true);
    7. graphPlot->axisRect()->setupFullAxesBox();
    8. connect(graphPlot->xAxis, SIGNAL(rangeChanged(QCPRange)), graphPlot->xAxis2, SLOT(setRange(QCPRange)));
      connect(graphPlot->yAxis, SIGNAL(rangeChanged(QCPRange)), graphPlot->yAxis2, SLOT(setRange(QCPRange)));
  5. void MainWindow::readResponse( )
    1. if (start) {

      //Define your logic for reading the text. Remember that it starts with "#" 
      //Store the "double" finally extracted in "rx_value" 

      realtimeDataSlot(rx_value);
      }
    2. You can also abstract the serial port setup, reading the serial port etc. as classes which can be invoked here. This makes the project more robust.
  6. void MainWindow::realtimeDataSlot( )
    1. if (start) {

      QCustomPlot *graphPlot = (QCustomPlot*) (ui->graph);
      range_y_min = min(range_y_min,value);
      range_y_max = max(range_y_max,value);
      graphPlot->graph(0)->addData(key_x, value);
      // set data of dots:
      graphPlot->graph(1)->clearData();
      graphPlot->graph(1)->addData(key_x, value);
      graphPlot->xAxis->setRange(key_x+0.5, 50, Qt::AlignRight);
      graphPlot->yAxis->setRange(range_y_min,range_y_max);
      graphPlot->replot();
      key_x += 0.5; // defines horizontal gap between two data points on graph
      }
One simple way of implementing the data parsing logic is by reading the data character by character (using serial.getChar( )) and then doing the necessary processing in a loop. You can start this when you read the prefix (like a "#" that I had mentioned before; it should not be a character that can appear in your data!) and end it by setting a delimiter (like the new line character '\r\n'). Keep appending the read characters in a string and when you reach the delimiter, convert the string into double (using ".toDouble( )" function for strings), store it in "rx_value" and plot it.

That's it! Your project should be working fine if your logic for parsing the data is correct. I have left that because it can be implemented in many ways. As I have stated before, you can abstract serial port setup, reading data from serial port and parsing the data as three different classes and integrate them appropriately so that you have all the functions separated. This makes debugging more easier and also makes the project neat!

6 comments:

  1. Hi Narayanan, great tutorial from you!

    I am trying to follow your guide to achieve the graph plotting with reading from serial usb com port.
    However I stuck at :

    "3. processError( ) is just a function to display the text in its argument. Define it."

    I do not quite understand how to define it, i have tried to define it under private slots: in mainwindow.
    But no luck.
    I am just started using QT5 in less than 2 week.
    I have succeeded to read data and show the number in TextBrowser .ui file

    Here is the snippet for acquiring the serial
    void MainWindow::readTemp()
    {
    if( serial->bytesAvailable() >= 6 )
    {
    serial->read(databuff,5);
    ui->textBrowser_2->setText(databuff);

    }



    if( serial->bytesAvailable() >= 5 )
    {
    serial->read(databuff,5);
    ui->textBrowser->setText(databuff);

    }

    I need to plot the real time reading into 2 separate graphs, OR 2 readings plot on same graph.
    But do not know how to manipulate the incoming data.

    I hope to hear your opinion.
    thx alot!!

    ReplyDelete
    Replies
    1. You could define processError() as a function in the MainWindow class. The function could just have a cout statement or printf statement that just prints the string passed as argument to the function.

      To plot 2 separate graphs:
      - Create a new QWidget in your UI and promote it to the "qcustomplot" class.
      - Use this object's name in step 6 above when you create a pointer to it. Now, all properties are applied to this graph too!

      To plot 2 readings on same graph:
      - Repeat step 4 to create two more graphs 2 and 3, one for drawing the line and one for the dot both for the second curve that you wanted to plot. Set necessary properties according to your wish.
      - In step 6, when you add data to the graphs, add the data for the second curve in graphs 2 and 3 which should give the line and dot for that curve too.

      Hope this is clear. If you want to read data from multiple serial ports, you could spawn multiple Reader objects, one for each port. But, then, you need to make the whole program threaded to handle each port separately and simultaneously. The program should also be made thread-safe. It is not very difficult to do that; I myself have a few projects done that way and all work fine.

      Delete
    2. Hi Narayanan, thanks alot for your reply

      Is this what you mean "define processError() as a function in the MainWindow class" :

      MainWindow::MainWindow(QWidget *parent) :
      QMainWindow(parent),
      ui(new Ui::MainWindow)
      {
      ui->setupUi(this);
      }

      void MainWindow::processError()
      {
      if (serial->portName() != QString("COM8")) { // change the COM port!

      serial->close();
      serial->setPortName("COM8");
      if (!serial->open(QIODevice::ReadWrite))
      {
      processError(tr("Can't open %1, error code %2")
      .arg(serial->portName()).arg(serial->error()));
      return;
      }
      if (!serial->setBaudRate(QSerialPort::Baud57600)) {
      processError(tr("Can't set rate 9600 baud to port %1, error code %2")
      .arg(serial->portName()).arg(serial->error()));
      return;
      }
      if (!serial->setDataBits(QSerialPort::Data8)) {
      processError(tr("Can't set 8 data bits to port %1, error code %2")
      .arg(serial->portName()).arg(serial->error()));
      return;
      }
      if (!serial->setParity(QSerialPort::NoParity)) {
      processError(tr("Can't set no patity to port %1, error code %2")
      .arg(serial->portName()).arg(serial->error()));
      return;
      }
      if (!serial->setStopBits(QSerialPort::OneStop)) {
      processError(tr("Can't set 1 stop bit to port %1, error code %2")
      .arg(serial->portName()).arg(serial->error()));
      return;
      }
      if (!serial->setFlowControl(QSerialPort::NoFlowControl)) {
      processError(tr("Can't set no flow control to port %1, error code %2")
      .arg(serial->portName()).arg(serial->error()));
      return;
      }
      }
      }


      I got error after defining it in this way :
      " no matching function for call to 'MainWindow::processError(QString)"


      Actually there are some errors like:
      CASE 1

      //mainwindow.h
      ...
      private:
      ...
      bool start = false;
      double rx_value, key_x, range_y_min = 0, range_y_max = 10

      error: ": non-static data member initializers only available with -std=c++11 or -std=gnu++11 [enabled by default]
      bool start = false;
      ^
      double rx_value, key_x, range_y_min=0 , range_y_max=10 ;
      ^ ^
      The errors gone after i remove the "=" sign and the values


      CASE 2

      error: prototype for 'void MainWindow::realtimeDataSlot()' does not match any in class 'MainWindow'
      void MainWindow::realtimeDataSlot()
      ^ ^

      Additional question:

      I know that

      "ui->textBrowser->setText(databuff);"

      transfer the serial data to textBrowser in the ui, and it shows the number very well.

      How can i assign the value to a variable so that i can manipulate or do calculation later..?

      is it possible to do like this :

      a=ui->textBrowser->setText(databuff);

      then i can use the "a" to do something like w= a+2..
      I am more familiar with matlab coding.

      QT is totally a new thing for me...

      Anyhow, thank you for your time to explain all this for me.
      I hope to see more of you project with detail procedure so that I can learn more from you.
      Thanks again!!

      Delete
    3. I think you definitely have to learn object-oriented programming and C++ to make sense of this tutorial I have put up. MATLAB knowledge is insufficient for this kind of applications. I can go ahead and try to clarify your queries, but it is only going to spawn new doubts to you because from your doubts above, I see that you do not know C++. In such case, honestly, it is not possible for me to make everything in this tutorial clear to you. I hope you understand.

      Do get back to this if you learn C++ and are still interested in these projects. I will be happy to clarify doubts (if any) at that point of time.

      Delete
  2. Hello Narayanan, I would like to ask you please. Can you help me. I make now one app and I need from you confirmation. My data come #2.00
    #1.00
    #3.00
    #7.00
    #3.00
    #3.00
    #7.00
    #6.00
    Is it correct? And it is possible, that I have in your code a mistake too. Can you me sen this code with the certain wording? Thanks a lot for your help

    ReplyDelete
  3. Thank you Narayanan... especially for your successive nation explanation! by the way I am a neurosurgeon and interested in biosignal processing as a hobbiest.. If you send your code, I will try to make own GUI by using QT.. I am beginner in QT and I like it day by day.. My email is erkutlu@gantep.edu.tr
    Dr.Ibrahim Erkutlu, M.D.

    ReplyDelete