Qt之QCustomPlot的二次封装:实现支持多数据线条的时间轴实时动态曲线显示的widget类

Qt之QCustomPlot的二次封装:实现支持多数据线条的时间轴实时动态曲线显示的widget类

  • 前言
  • 开发环境的搭建
  • 关键步骤介绍
    • 新建一个Qt 设计师界面类
    • InitTimeAxis函数:初始化横坐标,让其作为时间轴
    • SetValueState函数:设置线条的个数、标题等
    • SetCurrentTime_T函数:实时设置时间,让时间轴动起来
    • UpdateValues()和SetOneValue()函数:让曲线飞起来
  • 总结

前言

在这里读了不少关于Qt图表的文章,遇到了不少大神的文章让我受益匪浅,但遇到更多的是不接地气的事例。一看文章便知道作者肯定没干过工程的,写Qt代码还是基于“玩票儿”或者学习,和真正用于工程实践的东西严重脱节的。
所以从今天开始,我会陆续对Qt的一些关于图表方面的知识一边学习,一边进行介绍,和很多博主不同的是,文章产生的相关代码,是一定可以直接拿到工程实践中方便使用的。
废话不多说,今天就介绍一个基于QCustomPlot第三方图表类而实现的时间轴实时曲线图表。

开发环境的搭建

我使用的Qt版本是:Qt 5.8.0(MSVC 2015,32bit),Qt Creator 4.2.1。如何安装就不多介绍了。
第三方控件QCustomPlot是我从官方网站上下载的,大家可以去下载,也可以从本文的附件里获得,下面就简单介绍一下如何让你的工程支持QCustomPlot。
新建一个Qt Widget Application工程。

现在我们把QCustomPlot.h和QCustomPlot.cpp拷贝到工程的目录里面,记住,是你的开发目录,不是build开头的那个编译生成的文件夹,然后将其加入到工程中。
然后在pro文件中的greaterThan(QT_MAJOR_VERSION, 4): QT += widgets后面加一个词:printsupport
到这里,你的工程就支持QCustomPlot类了,是不是很简单,接下来我就对如何封装一个支持多曲线实时显示的图表控件做下介绍。

关键步骤介绍

新建一个Qt 设计师界面类

这个类就是我们要进行二次封装的载体,很多人直接拿代码实现界面。个人认为,除非你是大牛,否则作为入门不深的开发者,还是老老实实的直接靠拖拽来实现自己的Widget吧,提升复用啥的也方便,笨人有笨办法,先学会走再想着飞,否则你就算实现了,也不知道怎么在工程里使用。
Qt设计师界面类是入门不深的开发者的福音

我给我新建的Widget类取名叫TimeDynamicListForm,然后在他的上面拖入一个Widget,我给它起个名字叫custom_plot_widget,然后栅格化,让它铺满整个Form。
在这里插入图片描述
选中custom_plot_widget,右键“提升为”,将其提升成QCustomPlot,头文件默认即可,然后点击“添加”,然后再点“提升”,编译一下,如果没有错,说明你搭的环境成功了。
在这里插入图片描述
为了进一步验证,在mainwindow里面拽出来一个Widget,然后把它提升为咱们自定义的TimeDynamicListForm,方法和刚才提升QCustomPlotL类似,这也是以后我们复用自定义widget的方法,再次编译,如果你得到了这样界面,说明准备工作已经完成了。
在这里插入图片描述
好了,准备工作就绪,下面我对实现动态实时显示的关键代码进行介绍。

InitTimeAxis函数:初始化横坐标,让其作为时间轴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void TimeDynamicListForm::InitTimeAxis(int temp_tick_count,double max_second)
{
    QSharedPointer<QCPAxisTickerDateTime> date_time_ticker(new QCPAxisTickerDateTime);

    date_time_ticker->setDateTimeFormat("hh:mm:ss");//显示格式

    tick_count=temp_tick_count;
    date_time_ticker->setTickCount(tick_count);//刻度数设置
    date_time_ticker->setTickStepStrategy(QCPAxisTicker::tssMeetTickCount);

    ui->custom_plot_widget->xAxis->setTickLabelRotation(1);//刻度线稍微倾斜,节省空间,参数是角度

    ui->custom_plot_widget->xAxis->setTicker(date_time_ticker);//将其作用于x坐标轴

    //1作为最小单位刻度
    ui->custom_plot_widget->xAxis->setSubTickLength(1);

    max_second_count=max_second;//max_second_count是类成员变量,这里只赋值,在其他函数中使用
}

第一个参数是设置刻度的数量的;第二个参数是设置最大要显示的秒数,这个函数只是赋了值,会在其他函数中使用。
在mainwindow中调用完后编译,效果是这样的:
在这里插入图片描述

SetValueState函数:设置线条的个数、标题等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void TimeDynamicListForm::SetValueState(QStringList temp_title_list, QList<QColor> color_list)
{
    cur_line_count=temp_title_list.count();

    int count=color_list.count();

    if(cur_line_count!=count)
    {
        return;
    }

    if(cur_line_count>MAX_TIME_DYNAMIC_LINES_COUNT)
    {
        cur_line_count=MAX_TIME_DYNAMIC_LINES_COUNT;
    }

    title_list.clear();

    for(int i=0;i<cur_line_count;i++)
    {
        value_list[i].value_vector.clear();
        value_list[i].time_t_vector.clear();

        value_list[i].max_value=0;
        value_list[i].min_value=0;

        ui->custom_plot_widget->addGraph();
        ui->custom_plot_widget->graph(i)->setName(temp_title_list.at(i)+":0");

        title_list.append(temp_title_list.at(i));

        QPen pen;
        pen.setColor(color_list.at(i));

        ui->custom_plot_widget->graph(i)->setPen(pen);
    }

    setLegendPosition(ui->custom_plot_widget);
}

这里使用了若干自定义的宏、结构体和成员变量,在下面简要介绍,还不明白的可用看文章附件里的头文件。
最后一句话调用的函数是用来将线条的标识显示到图表下面的,我是直接用的大神@缘如风的文章:QCustomPlot实现图例置于底部,具体内容还是不贴出来了,如果想要看,附件的代码中有。

1
2
3
4
5
6
7
8
9
10
11
12
13
//宏定义
#define MAX_TIME_DYNAMIC_LINES_COUNT 10 //支持的线条最大数
//自定义结构体
typedef struct _TimeDynamicValueList{
    QVector<double> value_vector;
    QVector<double> time_t_vector;
    double max_value;
    double min_value;
}TimeDynamicValueList,*PTimeDynamicValueList;
//成员变量
    TimeDynamicValueList value_list[MAX_TIME_DYNAMIC_LINES_COUNT];//值列表
    int cur_line_count;//当前的线条数目
    QStringList title_list;//线条的标题列表

在mianwidow的构造函数对其进行调用,我是使用了6条线,调用的函数如下:

1
2
3
4
5
6
7
8
9
    //2.设置线条
    QStringList temp_title_list;
    QList<QColor> temp_color_list;

    temp_title_list.clear();
    temp_title_list<<"电机1#"<<"电机2#"<<"电机3#"<<"电机4#"<<"电机5#"<<"电机6#";
    temp_color_list.clear();
    temp_color_list<<QColor(211,231,47)<<QColor(28,111,249)<<QColor(245,138,43)<<QColor(28,249,147)<<QColor(28,241,249)<<QColor(255,190,0);
    ui->widget->SetValueState(temp_title_list,temp_color_list);

SetCurrentTime_T函数:实时设置时间,让时间轴动起来

1
2
3
4
5
6
7
8
9
void TimeDynamicListForm::SetCurrentTime_T(unsigned int time_t)
{
    cur_time_t=(double)time_t;

    ui->custom_plot_widget->xAxis->setRange(cur_time_t-max_second_count,cur_time_t);

    //仅靠时间来驱动刷新
    ui->custom_plot_widget->replot();
}

这个函数是靠外部调用,把1970年1月1日算起的积秒值作为参数调用的,我是给mainwindow设置了一个timer,在timeEvent函数里进行实时调用,你如果问我为啥不在Form本身用timer触发。能问这问题就能证明你没写过工程软件,简单解释一句:**一个大系统是需要统一的时统的。**我为控件做了这么一个函数,正是为了让它能在大系统的时间节拍里刷新,避免各自为战。
在mainwindow的timeEvent中的调用如下,至此以后,你会发现时间轴动起来了,接下来我们就可以把曲线加入了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void MainWindow::timerEvent(QTimerEvent *event)
{
    if(event->timerId()==cycle_id)
    {
        static unsigned int pre_time_t=0;

        unsigned int cur_time_t=QDateTime::currentDateTime().toTime_t();

        if(pre_time_t!=cur_time_t)
        {
            ui->widget->SetCurrentTime_T(cur_time_t);
            pre_time_t=cur_time_t;
        }
    }
}

现在启动,效果是这样的,还没曲线显示?别急,马上就会有了。在这里插入图片描述

UpdateValues()和SetOneValue()函数:让曲线飞起来

从名字应该可以看出,这两个函数是有一定关系的。前者通过调用后者对每一个曲线批量循环赋值刷新,不多说,上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
void TimeDynamicListForm::UpdateValues(QList<double> values)
{
    bool max_value_change_flag=false;
    bool min_value_change_flag=false;

    int count=values.count();

    if(count!=cur_line_count)
    {
        return;
    }

    max_value=value_list[0].max_value;
    min_value=value_list[0].min_value;

    for(int i=0;i<count;i++)
    {
        SetOneValue(i,values.at(i));

        if(max_value<value_list[i].max_value)
        {
            max_value=value_list[i].max_value;
            max_value_change_flag=true;
        }

        if(min_value>value_list[i].min_value)
        {
            min_value=value_list[i].min_value;
            min_value_change_flag=true;
        }
    }

    if(max_value_change_flag==true)
    {
        ui->custom_plot_widget->yAxis->setRangeUpper(max_value+1);
    }

    if(min_value_change_flag==true)
    {
        ui->custom_plot_widget->yAxis->setRangeLower(min_value-1);
    }
}

void TimeDynamicListForm::SetOneValue(int number,double value)
{
    value_list[number].value_vector.push_back(value);

    double cur_time=cur_time_t;

    value_list[number].time_t_vector.push_back(cur_time);

    int count_1=value_list[number].value_vector.count();
    int count_2=value_list[number].time_t_vector.count();

    if(count_1!=count_2)
    {
        value_list[number].value_vector.clear();
        value_list[number].time_t_vector.clear();
        return;
    }

    //一根线上的最大点数目(暂时用秒为单位,毫秒级的需要进一步处理)

    if(count_1>max_second_count)//超过最大值,移除第一个点
    {
        value_list[number].value_vector.removeFirst();
        value_list[number].time_t_vector.removeFirst();
    }

    count_1=value_list[number].value_vector.count();

    value_list[number].max_value=value_list[number].value_vector.at(0);
    value_list[number].min_value=value_list[number].value_vector.at(0);

    //大小值
    for(int i=0;i<count_1;i++)
    {
        if(value_list[number].max_value<value_list[number].value_vector.at(i))
        {
            value_list[number].max_value=value_list[number].value_vector.at(i);
        }

        if(value_list[number].min_value>value_list[number].value_vector.at(i))
        {
            value_list[number].min_value=value_list[number].value_vector.at(i);
        }
    }

    ui->custom_plot_widget->graph(number)->setData(value_list[number].time_t_vector,value_list[number].value_vector);

    //把值刷在
    QString temp_value_str=QString::number(value,'f',2);
    ui->custom_plot_widget->graph(number)->setName(title_list.at(number)+":"+temp_value_str);
}

在mainwindow的tiemEvent()函数中,我写了若干随机数,然后进行调用,调用的事例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//若干随机数
            double  temp_voltage;
            temp_voltage=(double)(qrand()%200);
            double motor_1=temp_voltage/10-10;

            temp_voltage=(double)(qrand()%200);
            double motor_2=temp_voltage/10-10;

            temp_voltage=(double)(qrand()%200);
            double motor_3=temp_voltage/10-10;

            temp_voltage=(double)(qrand()%200);
            double motor_4=temp_voltage/10-10;

            temp_voltage=(double)(qrand()%200);
            double motor_5=temp_voltage/10-10;

            temp_voltage=(double)(qrand()%200);
            double motor_6=temp_voltage/10-10;

            //刷新数值
            QList<double> temp_double_list;
            temp_double_list.clear();

            temp_double_list.append(motor_1);
            temp_double_list.append(motor_2);
            temp_double_list.append(motor_3);
            temp_double_list.append(motor_4);
            temp_double_list.append(motor_5);
            temp_double_list.append(motor_6);

            ui->widget->UpdateValues(temp_double_list);

调用后的效果,6条曲线飞起来了。
在这里插入图片描述

总结

至此,对基于QCustomPlot支持多曲线实时显示的图表二次封装就完成了,我想应该能满足大部分对多数据动态显示的需求了。使用也很方便,在你任何想要使用的地方拽一个widget,然后把它提升为咱们封装好的界面类,简单设置一下就可以投入使用,CPU占用率非常小,很实用。

当然,改进的地方有很多,比如如果觉得1秒的精度不够,可以考虑进一步加入毫秒值;;再比如想要对背景字体进行设置…这些我在正式使用时已经实现了,有兴趣或者有需要的,可以留言或者加我VX:ConciseRabbit进行交流。

文章为原创,转载请注明出处。