Memory using Qcharts

Our second SysInfoWidget is a MemoryWidget class. This widget will display a history of the data so that we can see how the memory consumption evolves over time. To display this data, we will use a QLineSeries class from the Qt Chart module. Create the MemoryWidget class and follow the same pattern we used for CpuWidget:

#include <QtCharts/QLineSeries> 
 
#include "SysInfoWidget.h" 
 
class MemoryWidget : public SysInfoWidget 
{ 
    Q_OBJECT 
public: 
    explicit MemoryWidget(QWidget *parent = 0); 
 
protected slots: 
    void updateSeries() override; 
 
private: 
    QtCharts::QLineSeries* mSeries; 
    qint64 mPointPositionX; 
}; 

Instead of a being a QPieSeries*mSeries is a type of QLineSeries* which will be linked to the chart object in a very similar fashion to MemoryWidget.cpp:

#include "MemoryWidget.h" 
#include <QtCharts/QAreaSeries> 
 
using namespace QtCharts; 
 
const int CHART_X_RANGE_COUNT = 50; 
const int CHART_X_RANGE_MAX = CHART_X_RANGE_COUNT - 1; 
 
MemoryWidget::MemoryWidget(QWidget *parent) : 
    SysInfoWidget(parent), 
    mSeries(new QlineSeries(this)), 
    mPointPositionX(0) 
{ 
    QAreaSeries* areaSeries = new QAreaSeries(mSeries); 
 
    QChart* chart = chartView().chart(); 
    chart->addSeries(areaSeries); 
    chart->setTitle("Memory used"); 
    chart->createDefaultAxes(); 
    chart->axisX()->setVisible(false); 
    chart->axisX()->setRange(0, CHART_X_RANGE_MAX); 
    chart->axisY()->setRange(0, 100); 
} 
 
void MemoryWidget::updateSeries() 
{ 
} 

The mSeries data is, as usual, initialized in the initializer list. The mPointPositionX is an unsigned long long (using the Qt notation qint64) variable that will track the last X position of our data set. This huge value is used to make sure that mPointPositionX never overflows.

We then use an intermediate areaSeries that takes ownership of mSeries upon its initialization in QAreaSeries* areaSeries = new QareaSeries(mSeries)areaSeries is then added to the chart object at chart->addSeries(areaSeries). We do not want to display a single line in our QChart; instead we want to display an area that represents the used memory percentage. That is why we use an areaSeries type. Nonetheless, we will still update the mSeries data when adding new points to the dataset in the updateSeries() function. The areaSeries type will automatically handle them and deliver them to the chart object.

After chart->addSeries(areaSeries), we configure the chart display:

  • The chart->createDefaultAxes() function creates an X and Y axis based on the areaSeries type. If we used a 3D series, the createDefaultAxes() function would have added a Z axis.
  • Hide the X axis tick values with chart->axisX()->setVisible(false) (intermediate values displayed at the bottom of the axis). In our MemoryWidget class, this information is not relevant.
  • To define the number of points we want to display--the size of the display history--we call chart->axisX()->setRange(0, CHART_X_RANGE_MAX). Here we use a constant to make it easier to modify this value afterwards. Seeing the value at the top of the file, we avoid having to skim through MemoryWidget.cpp, searching where this value is used to update it.
  • chart->axisY()->setRange(0, 100) defines the maximum range of the Y axis, which is a percentage, based on the value returned by the SysInfo::memoryUsed() function.

The chart is now properly configured. We now have to feed it by filling the updateSeries() body:

void MemoryWidget::updateSeries() 
{ 
    double memoryUsed = SysInfo::instance().memoryUsed(); 
    mSeries->append(mPointPositionX++, memoryUsed); 
    if (mSeries->count() > CHART_X_RANGE_COUNT) { 
        QChart* chart = chartView().chart(); 
        chart->scroll(chart->plotArea().width()  
                      / CHART_X_RANGE_MAX, 0); 
        mSeries->remove(0); 
    } 
} 

We first retrieve the latest memory percentage used and append it to mSeries at the X coordinate mPointPositionX (we post-increment it for the next updateSeries() call) and Y coordinate memoryUsed. As we want to keep a history of mSeriesmSeries->clear() is never called. However, what will happen when we add more than CHART_X_RANGE_COUNT points? The visible "window" on the chart is static and the points will be added outside. This means that we will see the memory usage only for the first CHART_X_RANGE_MAX points and then, nothing.

Fortunately, QChart provides a function to scroll inside the view to move the visible window. We start to handle this case only when the dataset is bigger than the visible window, meaning if (mSeries->count() > CHART_X_RANGE_COUNT). We then remove the point at the index 0 with mSeries->remove(0) to ensure that the widget will not store an infinite dataset. A SysInfo application that monitors the memory usage and has itself a memory leak is a bit sad.

The syntax chart->scroll(chart->plotArea().width() / CHART_X_RANGE_MAX, 0) will then scroll to the latest point on the X axis and nothing on Y. The chart->scroll(dx, dy) expects coordinates expressed in our series coordinates. That is the reason why we have to retrieve the char->plotArea() divided by CHART_X_RANGE_MAX , the X axis unit.

We can now add the MemoryWidget class in MainWindow:

// In MainWindow.h 
#include "CpuWidget.h" 
#include "MemoryWidget.h" 
 
... 
 
private: 
    Ui::MainWindow *ui; 
    CpuWidget mCpuWidget; 
    MemoryWidget mMemoryWidget; 
}; 
 
// In MainWindow.cpp 
MainWindow::MainWindow(QWidget *parent) : 
    QMainWindow(parent), 
    ui(new Ui::MainWindow), 
    mCpuWidget(this), 
    mMemoryWidget(this) 
{ 
    ui->setupUi(this); 
    SysInfo::instance().init(); 
    ui->centralWidget->layout()->addWidget(&mCpuWidget); 
    ui->centralWidget->layout()->addWidget(&mMemoryWidget); 
} 

Just as we did for CPUWidget, add a new member named mMemoryWidget to MainWindow and add it to the centralWidget layout with the ui->centralWidget->layout()->addWidget(&mMemoryWidget) syntax.

Compile, run the application, and wait a few seconds. You should see something close to this:

The MemoryWidget class works fine, but it looks a bit dull. We can customize it very easily with Qt. The goal is to have a bold line at the top of the memory area and a nice gradient from top to bottom. We just have to modify the areaSeries class in the MemoryWidget.cpp file:

#include <QtCharts/QAreaSeries> 
#include <QLinearGradient> 
#include <QPen> 
 
#include "SysInfo.h" 
 
using namespace QtCharts; 
 
const int CHART_X_RANGE_MAX = 50; 
const int COLOR_DARK_BLUE = 0x209fdf; 
const int COLOR_LIGHT_BLUE = 0xbfdfef; 
const int PEN_WIDTH = 3; 
 
MemoryWidget::MemoryWidget(QWidget *parent) : 
    SysInfoWidget(parent), 
    mSeries(new QLineSeries(this)) 
{ 
    QPen pen(COLOR_DARK_BLUE); 
    pen.setWidth(PEN_WIDTH); 
 
    QLinearGradient gradient(QPointF(0, 0), QPointF(0, 1)); 
    gradient.setColorAt(1.0, COLOR_DARK_BLUE); 
    gradient.setColorAt(0.0, COLOR_LIGHT_BLUE); 
    gradient.setCoordinateMode(QGradient::ObjectBoundingMode); 
 
    QAreaSeries* areaSeries = new QAreaSeries(mSeries); 
    areaSeries->setPen(pen); 
    areaSeries->setBrush(gradient); 
 
    QChart* chart = chartView().chart(); 
    ... 
} 

The QPen pen function is a part of the QPainter API. It is the foundation on which Qt relies to do most of the GUI drawing. This includes the whole QWidget API (QLabelQPushButtonQLayout, and so on). For the pen, we just have to specify its color and width, and then apply it to the areaSeries class with areaSeries->setPen(pen).

The principle is the same for the gradient. We define the starting point (QPointF(0, 0)) and the final point (QPointF(0, 1)) before specifying the color at each end of the vertical gradient. The QGradient::ObjectBoundingMode parameter defines how the start/final coordinates are mapped to the object. With the QAreaSeries class, we want the gradient coordinates to match the whole QareaSeries class. These coordinates are normalized coordinates, meaning that 0 is the start and 1 is the end of the shape:

  • The [0.0] coordinates will point to the top left corner of the QAreaSeries class
  • The [1.0] coordinates will point to the bottom left corner of the QAreaSeries class

A last build and run, and the SysInfo application will look like this:

A memory leak or starting a virtual machine is a great way to make your memory go crazy

The SysInfo application is now finished, and we even added some visual polish. You can explore the QGradient classes and the QPainter API if you want to further customize the widget to your taste.