Визуализация данных с помощью QtCharts

Рязанов Илья, ryasanov@gmail.com

Совсем недавно появилась новость, что компания Digia открывает исходный код своей проприетарной библиотеки QtCharts (QtCharts). Библиотека QtCharts, является дополнением к Qt и представляет, мощное средство для рисования всяких красивых графиков. Теперь последняя версия этой замечательной библиотеки, доступна под лицензией GPL. Компания Digia обещает включить QtCharts в следующий релиз Qt. Пока она доступна в виде исходных текстов. 

В этой статье мы соберем эту библиотеку из исходных кодов и напишем простой виджет для рисования осциллограмм с нашего прибора LESO4

Сборку библиотеки, я буду делать в Windows, хотя для Linux она практически ничем не отличается.

На компьютер нужно будет установить:

Собирать библиотеку будем в директории где установлен Qt. У меня это C:\Qt.

И так, запускаем консоль сборки Qt (Пуск -> Все программы -> Qt -> 5.4 -> MinGW 4.9(32-bit) -> Qt 5.4 for Desktop MinGW 4.9(32-bit)) и перейдем в директорию где соберем библиотеку:

cd c:\Qt

Получаем исходный код библиотеки с github и переходим в директорию с исходниками:

git clone https://github.com/qtproject/qtcharts.git
cd ./qtcharts

Чтобы git был доступен из консоли сборки Qt в винде, его следует добавить в системную переменную PATH.

Теперь можно приступить к сборке библиотеки:

qmake CONFIG+=release
mingw32-make

В процессе сборки запустятся скрипты perl, которые сгенерируют нужные файлы mkspec, чтобы библиотеку подключать к проекту как модуль Qt.

Компиляция может занять определенное время, так что можно пойти выпить чашку кофе.

После того как процесс завершен можно приступить к написанию нашего виджета.

Запускаем QtCreator и создаем новый проект (Файл -> Создать файл или проект и в появившемся меню Приложение -> Приложение Qt Widgets).  

В следующем окне задаем название и местоположение проекта, например я назвал проект LESO4Plot:

Выбираем все доступные комплекты:

Класс я назвал LESOPlotWidget, а базовый класс выбрал QWidget. Также не забываем снять галочку - Создать форму:

Теперь для подключения к проекту, библиотеки QtCharts, в файле проекта LESO4PlotWidget.pro нужно добавить следующие строки

include(C:\Qt\qtcharts\mkspecs\modules\qt_lib_charts.pri)
Qt += charts

В файле qt_lib_charts.pri находятся определения модуля Qt с названием charts. Модуль добавляется к проекту, Qt += charts в файле .pro 

Для взаимодействием с устройством LESO4 понадобится библиотека libLESO4, ее тоже необходимо собрать и подключить к проекту. В консоле Qt переходим в директорию проекта и клонируем репозиторий с исходный кодом библиотеки:

cd C:\LESO4Plot
git clone https://iRyaz@bitbucket.org/iRyaz/leso4_api.git

Теперь компилим библиотеку:

cd leso4_api
mingw32-make

В файл LESO4PlotWidget.pro добавляем путь к заголовочному файлу leso4.h и файлу собранной библиотеки libLESO4.dll:

INCLUDEPATH += ./leso4_api/
LIBS += -L./leso4_api/ -lLESO4

Одна тонкость, чтобы линковщик нашел libLESO4.dll, по заданному пути, нужно в QtCreator отключить теневую сборку, снять галочку в настройках

Окей проект настроили, теперь можно приступить к кодингу.

Виджет работает следующим образом.

В конструкторе виджет, открывается и настраивается устройтство LESO4 с помощью функций библиотеки libLESO4. Если устройство не получилось открыть, то выводится сообщение об ошибки, и программа завершается. Если все хорошо, то инициализируется основной компонент QChartView, для отображения графиков. Затем сигнал программного таймера QTimer связывается со слотом readDataDevice() и запускается. Это значит, что когда таймер досчитывает до определенного интервала, будет вызываться функция readDataDevice(). Сигналы и слоты - относяться чисто к Qt, о них можно почитать в официальной документации. В функции readDataDevice() данные считываются с устройства и выводятся на график.  

Настройка устройства производится в конструкторе виджета. Первым шагом открывается устройство, и если устройство не открылось, то выводится сообщение об ошибки и программа закрывается. Если устройство успешно открылось, то устанавливается количество точек, частота дискретизации, включаются все каналы и на каналах устанавливается максимальное напряжение. Ниже в листинге показан отрывок из файла lesoplotwidget.cpp

if(leso4Open(DEVICE_DESCRIPTOR) != LESO4_OK)       // Открытие устройства
        DEVICE_NOT_CONNECT_MESSAGE  // Если устройство не удалось открыть, то выдаем сообщение и завершаем программу
 
leso4SetSamplesNum(SAMPLE_NUM);                     // Количество точек для одного канала
leso4SetSamplFreq(sampe_frequency_10MHz);           // Частота дискретизации 10 МГц
 
leso4EnableChannel(CHANNEL_A);                      // Включить канал A
leso4SetAmplitudeScan(CHANNEL_A, max_voltage_20V);  // Максимальная амплитуда для канала A 20 В
 
leso4EnableChannel(CHANNEL_B);                      // Включить канал B
leso4SetAmplitudeScan(CHANNEL_B, max_voltage_20V);  // Максимальная амплитуда для канала B 20 В
 
leso4EnableChannel(CHANNEL_C);                      // Включить канал C
leso4SetAmplitudeScan(CHANNEL_C, max_voltage_20V);  // Максимальная амплитуда для канала C 20 В
 
leso4EnableChannel(CHANNEL_D);                      // Включить канал D
leso4SetAmplitudeScan(CHANNEL_D, max_voltage_20V);  // Максимальная амплитуда для канала D 20 В

Рисование графика в библиотеке QtCharts приведено в документации и сводится к заполнению некоторого класса производного от QXYSeries, координатами точек и передачей указателя функции addSeries класса QChart. Ниже приведен пример как построить график из двух точек

QLineSeries* series = new QLineSeries();   // Создать класс QLineSeries производный от QXYSeries
series->add(0, 6);                         // Добавить точку с координатами X = 0, Y = 6
series->add(2, 4);                         // Добавить точку с координатами X = 2, Y = 4
chartView->chart()->addSeries(series);     // Передать указатель на функции addSeries класса QChart
chartView->chart()->createDefaultAxes();   // Автоматический создать оси на графике

Заметим, что функции addSeries передается только указатель на структуру в которой хранятся координаты точек, поэтому его можно передать только один раз, и дальше обновлять саму структуру Series, а отображение виджета будет обновляться автоматически.

Функция createDefaultAxes() создает на графике оси и автоматически задает их диапазон в зависимости от точек на графике.  

Помимо QLineSeries в библиотеке есть еще и другие производные классы для предназначенные для разных графиков и имеющие разные свойства. Например QSplineSeries при добавлении новых точек автоматически производится сплайн интерполяция на графике. В своем виджете я использую QLineSeries, ибо он не использует интерполяци и отрисовка графика происходит быстрее, использование QSplineSeries сильно тормозило бы программу.

Виджет будет показывать осциллограммы для четырех каналов. Из объявления в заголовочном файле lesoplotwidget.h

QValueAxis *xAxis;  // Ось X
QValueAxis *yAxis;  // Ось Y
 
QLineSeries *diagramA;  // Точки для осциллограммы канала A
QLineSeries *diagramB;  // Точки для осциллограммы канала B
QLineSeries *diagramC;  // Точки для осциллограммы канала C
QLineSeries *diagramD;  // Точки для осциллограммы канала D

Эти указатели инициализируются в конструкторе нашего виджета в файле lesoplotwidget.cpp.

Ниже в листинге показана инициализация  для канала А, для всех остальных каналов все точно также

xAxis = new QValueAxis;                      // Ось X
xAxis->setRange(0, SAMPLE_NUM*TIME_STEP);    // Диапазон от 0 до времени которое соответстует SAMPLE_NUM точек
xAxis->setTitleText(tr("Time"));             // Название оси X
xAxis->setTitleBrush(Qt::magenta);           // Цвет названия
xAxis->setLabelsColor(Qt::magenta);          // Цвет элементов оси
 
yAxis = new QValueAxis;                      // Ось Y
yAxis->setRange(-20, 20);                    // Диапазон от -20 до +20 Вольт
yAxis->setTitleText(tr("Amplitude"));             // Название оси Y
yAxis->setTitleBrush(Qt::yellow);            // Цвет названия
yAxis->setLabelsColor(Qt::yellow);           // Цвет элементов оси
 
channelAPen.setWidthF(PEN_WIDTH);            // Установить ширину линии осциллограммы для канала A
channelAPen.setBrush(Qt::yellow);            // Цвет осциллограммы канала А
 
diagramA = new QLineSeries;
diagramA->setName(tr("Channel A"));          // Имя Осциллограммы
diagramA->setPen(channelAPen);
diagramA->setUseOpenGL(true);                // Включить поддержку OpenGL
view->chart()->addSeries(diagramA);          // Добавить осциллограмму на отображение
view->chart()->setAxisX(xAxis, diagramA);    // Назначить ось xAxis, осью X для diagramA
view->chart()->setAxisY(yAxis, diagramA);    // Назначить ось yAxis, осью Y для diagramA

Чтение и обработка данных с устройства задается макросом DEVICE_TIMEOUT в милисекундах. Через этот промежуток времени вызывается функция readDataDevice()

В  функции readDataDevice(), данные считываются с устройства и проверяется, было ли соединение с устройством. Затем производится удаление всех точек в структуре QLineSerial каждого канала. Затем QLineSerial заполняется новыми точками на основе отсчетов сигнала и осциллограмма на экране обновляется.

void LESOPlotWidget::readDataDevice()
{
    leso4ReadFIFO();    // Чтение отсчетов с устройства
    if(!leso4IsOpen())  // Есть ли связь с устройством?
        DEVICE_NOT_CONNECT_MESSAGE
 
    diagramA->clear();  // Удаление точек на осциллограмме канала A
    diagramB->clear();  // Удаление точек на осциллограмме канала B
    diagramC->clear();  // Удаление точек на осциллограмме канала C
    diagramD->clear();  // Удаление точек на осциллограмме канала D
 
    double *dataA = (double*)leso4GetData(NULL, CHANNEL_A); // Получение указателя на массив отсчетов с канала A
    double *dataB = (double*)leso4GetData(NULL, CHANNEL_B); // Получение указателя на массив отсчетов с канала B
    double *dataC = (double*)leso4GetData(NULL, CHANNEL_C); // Получение указателя на массив отсчетов с канала C
    double *dataD = (double*)leso4GetData(NULL, CHANNEL_D); // Получение указателя на массив отсчетов с канала D
 
    paintGraph(dataA, diagramA);    // Заполнение структуры QLineSerial для канала A отсчетами
    paintGraph(dataB, diagramB);    // Заполнение структуры QLineSerial для канала B отсчетами
    paintGraph(dataC, diagramC);    // Заполнение структуры QLineSerial для канала C отсчетами
    paintGraph(dataD, diagramD);    // Заполнение структуры QLineSerial для канала D отсчетами
}

Функция paintGraph производит заполнение точками переданной структуры QLineSerial. Принимает указатель на буфер отсчетов и структуру QLineSerial

void LESOPlotWidget::paintGraph(double *samplesPtr, QLineSeries *series)
{
    for(int i(0); i < SAMPLE_NUM; i++)
        series->append(i*TIME_STEP, samplesPtr[i]);
}

Макросом TIME_STEP задается шаг дискретизации по времени. Он обратно пропорционален частоте дискретизации.

#define TIME_STEP 1.0f/10000000.0f  // Период дискретизации

Из функции видно, что координата точки графика по X - это  шаг дискретизации умноженный на номер отсчета, а координата по Y это сам отсчет считанный с устройства.

Вот в общем-то основные моменты. Ниже привоже полные листинги файлов lesoplotwidget.h и lesoplotwidget.h

В итоге должен получиться вот такой виджет. (На канал B прибора LESO4, подается меандр с генератора сигналов)

Обновление графика происходит непрерывно, что сильно грузит систему. Если на виджет добавить еще и другие элементы управления, например ползунок регулирования частоты дискретизации, то графический интерфейс будет очень медленно работать. Более правильно было сделать обновления графика не по таймеру, а в отдельном потоке. Этот виджет написан только, чтобы разобраться с работой с библиотекой QtCharts, а также для демонстрации работы прибора.

С помощью библиотеки QtCharts можно строить и другие типы графиков, в статье описана лишь малая ее часть. Также в библиотеку можно применять вместе с QML. Об этом хорошо написано в официальной документации.    

09 февраля 2016
Орфографическая ошибка в тексте:
Чтобы сообщить об ошибке автору, нажмите кнопку "Отправить сообщение об ошибке". Вы также можете отправить свой комментарий.