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)
- Open "mainwindow.ui" under the "Forms" folder in your Qt project's tree.
- Extend the "MainWindow" to whatever size is required at the minimum.
- Pick and place a "Grid Layout" to the MainWindow and maximize it to fit the whole window.
- Pick and place a "Widget" (found under the "Containers" category in the list) inside this grid. Right-click the widget and choose "Promote To".
- 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".
- 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.
- 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.
- You are ready for giving "intelligence" to these objects now!
"mainwindow.h":
- Make sure to include the Qt Serial Port library with: #include <QtSerialPort/QSerialPort>.
- Create a section as "private slots:" if it does not exist already. Under that define the following slots:
- void readResponse( ) - to read responses from the serial port
- void realtimeDataSlot(double value) - to receive a "value" from readResponse() & plot it
- void setupGraph(QCustomPlot *graphPlot) - to set up the "graph" object with properties
- void on_start_clicked( ) - to define what happens when you click the "start" button
- void on_stop_clicked( ) - to define what happens when you click the "stop" button
- In the section named "private:", define the following variables:
- QByteArray response; - to read ans store the data from the serial port
- QTimer timer; - to control how often the graph is refreshed with a new data point
- QSerialPort serial;
- bool start = false; - to control when the graph should be plotting
- double rx_value, key_x, range_y_min = 0, range_y_max = 10
"mainwindow.cpp":
- MainWindow::MainWindow(QWidget *parent) :QMainWindow(parent),
ui(new Ui::MainWindow) - ui->setupUi(this);
- 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;
}
}
- processError( ) is just a function to display the text in its argument. Define it.
- void MainWindow::on_start_clicked( )
- Disable the "start" button: ui->start->setEnabled(false);
- Set the bool variable "start" to true
- Connect the "timeout( )" signal of "timer" to the slot "readResponse( )":
- connect(&timer, SIGNAL(timeout( )), this, SLOT(readResponse( )));
- 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.
- void MainWindow::on_stop_clicked( )
- Set the bool variable "stop" to false
- Exit the program: exit(0);
- void MainWindow::setupGraph(QCustomPlot *graphPlot)
- graphPlot->addGraph(); // blue dot
- graphPlot->graph(0)->setPen(QPen(Qt::blue));
graphPlot->graph(0)->setLineStyle(QCPGraph::lsLine);
- graphPlot->addGraph(); // blue dot
- graphPlot->graph(1)->setPen(QPen(Qt::blue));
graphPlot->graph(1)->setLineStyle(QCPGraph::lsNone);
graphPlot->graph(1)->setScatterStyle(QCPScatterStyle::ssDisc); - graphPlot->xAxis->setLabel("<-- Time -->");
graphPlot->xAxis->setTickLabelType(QCPAxis::ltNumber);
graphPlot->xAxis->setNumberFormat("gb");
graphPlot->xAxis->setAutoTickStep(true); - graphPlot->yAxis->setTickLabelColor(Qt::black);
graphPlot->yAxis->setAutoTickStep(true); - graphPlot->axisRect()->setupFullAxesBox();
- connect(graphPlot->xAxis, SIGNAL(rangeChanged(QCPRange)), graphPlot->xAxis2, SLOT(setRange(QCPRange)));
connect(graphPlot->yAxis, SIGNAL(rangeChanged(QCPRange)), graphPlot->yAxis2, SLOT(setRange(QCPRange))); - void MainWindow::readResponse( )
- 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);
}
- 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.
- void MainWindow::realtimeDataSlot( )
- 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!