上篇文章介绍了串口的设置,以下介绍初步画图。
感觉绘图才是QT等面向对象编程的最好特例。做完之后能感觉到这点是和在单片机或者ARM上逻辑编程最大的区别。
绘图步骤:
(1)采用QT自带chart函数绘图。优点:基础,易懂;缺点:麻烦。
注意:在画图过程中,由于绘图数据包含:三相电压波形(一个坐标系,也就是一张图),三相电流波形,电压电流相位等等,计算下来一共6个图,14条曲线。绘图时间1s间隔,1条曲线100个点。10ms接收40个8位数据进行处理,全部流程走下来,会出现丢点。分析原因:串口接收出现问题和绘图出现问题。经试验确定减小图可以将丢点减小后,所以利用按钮,把6个图减小为4个图,也就是电压显示的时候电流不显示(利用按钮槽函数,很容易实现,代码下面会有显示),丢点情况消失,但是也会有个别点丢失,但是情况出现很少。分析原因:QT中介绍chart函数时,强调能绘出很多点而不卡顿,这点是可以肯定的,利用append也好,利用replace也行,点多是对绘图性能没有影响,或者影响很小。但是图多(不是曲线,不是点数)的情况下,是有影响的,并且影响还不小。
(2)考虑到绘图数,点数,曲线数的扩展性,采用customplot文件。优点:代码简单;缺点:顶层操作,步骤多,代码没见过就不会用。
注意:在画图中,参考了网上和customplot自带例程,将坐标系,图和点放入《QT串口大量数据接收并绘图(一)》中的数据处理函数,即1s处理一次(同时串口大概10ms接收一次),此时绘图出现大面积丢点(依旧是6个图,那么多曲线,那么多点)。所以将坐标系,图和点的初始化函数放入ui->setupUi(this)位置下面(差不多是程序的初始化),丢点情况消失。应该是初始化坐标轴,初始化点,和初始化图的程序占用大量时间。改正在数据处理函数中仅仅绘点部分,即可解决点丢失现象。
(3)此篇中仅仅介绍chart函数绘图,下节介绍customplot函数绘图。
(1)pro文件中加入以下程序:
QT += charts core gui serialport widgets
其中:core 和 gui 系统文件,和debug还是其他功能有关,具体参看QT的help文件。charts是绘图部分,必须添加。serialport是串口部分,《QT串口大量数据接收并绘图(一)》中利用的串口程序都是基于此。widgets是插件函数,自己编写按钮还有一些其他功能需要用到,加上就对了。
(2)h文件中加入的include程序
#include //主文件头文件
#include //串口头文件
#include //画点状曲线头文件
#include //画线曲线头文件
#include //画坐标轴头文件
#include //画绘图区域头文件
#include //时间定时器头文件
#include //手势头文件(鼠标动作)
#include //绘图屏幕参数头文件
#include //绘图区域显示头文件
#include //鼠标动作时间头文件(产生槽函数)
加入的头文件具体作用已经标出,最好都加上,如果感觉没用可以去掉,如果需要加,最好去查下help文件,看看需要不需要加。
(3)c文件中加入的include程序
#include
#include //端口号索引(用在串口号接收上)
#include //链表(用在串口号接收上)
#include //标签
#include //图表
#include //按钮
#include //
#include //留言板
#include //画图
#include //点
#include //线
#include //坐标系
#include //绘图区域大小变化事件
#include //打印参数头文件,最好加上,最好用上,很方便
using namespace QtCharts;
namespace Ui { class MainWindow; }
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
说明:第一句必须加上,关系到点,线,坐标,图参数的全局化。如果没有这句,只能在某个函数里命名和使用点,线,坐标,图参数,加上子函数,就TM不认识了。这个被搞了好久。在KEIL,MDK或者CCS里面只要声明一个外部全局变量,就可以全部子函数使用此变量,方便又快捷。但是这个必须加上这句,并且第二句也得采用这个形式。自动生成的程序的第二句是有其他东西的,必须删掉才行。开始我采用的是QT自带的例程,结果只能在main函数里面用点,线,坐标,图参数变量,不能放到ui界面,或者其他子函数里面。这个方法就可以定义以下变量了:
public:
//绘图定义变量
QChartView *chartView_Vs;
QChart *chart_Vs;
QLineSeries *m_series_line_Vsua;
QLineSeries *m_series_line_Vsub;
QLineSeries *m_series_line_Vsuc;
QScatterSeries *m_series_point_Vsua;
QScatterSeries *m_series_point_Vsub;
QScatterSeries *m_series_point_Vsuc;
QValueAxis *m_axisX_Vs;
QValueAxis *m_axisY_Vs;
命名完之后,在所有子函数里面都能用点,线,坐标,图参数变量。
注意:可能有其他方法,时间紧迫,此方法可行,也就用此方法了,并且挺简单。
(1)点放在线上,后面参数可以自己添加,一个x,一个y。
m_series_point_Vsua->append(gfUTCmsBuffer[j], gfVsuAmpBuffer[j]);
m_series_point_Vsub->append(gfUTCmsBuffer[j], gfVsvAmpBuffer[j]);
m_series_point_Vsuc->append(gfUTCmsBuffer[j], gfVswAmpBuffer[j]);
(2)定义线,h文件里面已经定义,可以简单这样定义,如果h文件里没有定义,需要完整的定义,具体怎么弄网上挺多。
m_series_point_VsuaPha = new QScatterSeries();
m_series_point_VsubPha = new QScatterSeries();
m_series_point_VsucPha = new QScatterSeries();
(3)定义图,定义标题大小,曲线定义隐藏,最后一句也没查啥意思,别人这样用,我也就用了。
chart_Vs = new QChart;
chart_Vs->setTitle("电压三相幅值");
chart_Vs->setTitleFont(ft);
chart_Vs->legend()->hide();
chart_Vs->setAnimationOptions(QChart::AllAnimations);
(4)线放图上
chart_Vs->addSeries(m_series_point_Vsua);
chart_Vs->addSeries(m_series_point_Vsub);
chart_Vs->addSeries(m_series_point_Vsuc);
(5)定义绘图区域。第一句:定义绘图区域,并放在开始产生的ui界面上,ui->centralwidget这句话很重要,要不图不能在ui上显示。查了很多例程才找到的方法。当然如果程序在main函数里,就是另一个东西了:&window,也就是直接显示到window的界面里。至于window和ui界面的区别,可以网上找找,最好放ui界面里面,那样方便。第二句:不知道啥意思,也没查,有没有好像没影响。第三句:定义绘图区域的大小,这个要和ui界面的大小配合起来,反正看着好看就行,你喜欢就行。第四句:这句是鼠标动作,也就是放大波形和缩小波形用的,加上这句就能利用鼠标动作事件函数。
chartView_Vs = new QChartView(ui->centralwidget);
chartView_Vs->setRenderHint(QPainter::Antialiasing);
chartView_Vs->setFixedSize(1200,300);
chartView_Vs->setRubberBand(QChartView::RectangleRubberBand);
(6)定义坐标系。先定义坐标系。第三句第四句:将坐标系固定在图上。最后三句:将线和坐标系系在一起,可以一起动。
m_axisX_Vs = new QValueAxis;
m_axisY_Vs = new QValueAxis;
chartView_Vs->chart()->addAxis(m_axisX_Vs,Qt::AlignBottom);
chartView_Vs->chart()->addAxis(m_axisY_Vs,Qt::AlignLeft);
m_series_point_Vsua->attachAxis(m_axisX_Vs);
m_series_point_Vsua->attachAxis(m_axisY_Vs);
m_series_point_Vsub->attachAxis(m_axisX_Vs);
(7)其他辅助。前三句:设置点的大小,中间三句:设置线的颜色,后四句:设置坐标系的网格。具体效果可以自己试试,属于边边角角的东西,加不加都行。
m_series_point_Vsua->setMarkerSize(100);
m_series_point_Vsub->setMarkerSize(100);
m_series_point_Vsuc->setMarkerSize(100);
m_series_point_Vsua->setColor(600);
m_series_point_Vsub->setColor(600);
m_series_point_Vsuc->setColor(600);
m_axisX_Vs->setRange(0, 100);
m_axisX_Vs->setTickCount(11);
m_axisY_Vs->setRange(-100, 100);
m_axisY_Vs->setTickCount(5);
(1) 第三部分的(2)到(7)都是初始化图形。说简单点就是点放线上,线放坐标系上,坐标系放图上,图放开辟的图的区域里面。这个就是面向对象编程的基本逻辑吧。
(2) 第三部分的(1)即为改变点数据的函数,append之后就会画出新的点,点就会按照上述顺序放在图上,并显示到ui界面上。当然append仅仅是追加点,追加之后前面的点,前面的点不会变,一直加下去。
(3)坐标系的x和y都要随着时间的变化而变化,需要用到滚屏程序,也就是将坐标系往前挪:scroll(x, y)函数,这个函数其他有点问题,但是系统会告诉用哪个函数,直接改改就行了,好像是Qscroll函数。滚屏的时候需要注意滚动的距离,长了短了都不行,这就需要用到坐标系和屏的一些参数,添加相应头文件就好。
(4)项目中并没用到滚屏。10ms的40个8位数据中,有1个8位数据是表示时间的,即:10ms,20ms,一直到990ms。1s的时间画一个屏幕,所以我就利用表示时间的参量作为坐标轴的x轴了。每一屏都是10ms到990ms,循环往复。如果需要真实时间作为x轴的话,还是需要(3)中说的滚屏函数的。
(5)以下介绍ui界面
制作的ui界面如上图所示。其中电压,电流,频率,频率变化率按钮被屏蔽,串口号和波特率可选,灯为黄色,文字为解析停止。
工作界面如上图。其中:开始按钮变为停止。电压,电流,频率,频率变化率按钮变为可用。串口号和波特率不可用。文字显示为报头:《QT串口大量数据接收并绘图(一)》中的80007FFF。
工作数据发生故障时界面如上图。其中:数据等变为红灯。文字显示为“解析中......”
当按下电流和频率变化率按钮时,绘图界面显示按钮对应的实时数据。
(6)由电流和电压按钮对应的槽函数如下:作用为切换不同的坐标系、实时数据。其本质为隐藏对应绘图区域。
void MainWindow::on_SHOWVOL_clicked()
{
chartView_Vs->show();
chart_Vs->show();
chartView_Is->close();
chart_Is->close();
gnVolShowFlag = 1;
gnCurrShowFlag = 0;
}
void MainWindow::on_SHOWCURR_clicked()
{
chartView_Vs->close();
chart_Vs->close();
chartView_Is->show();
chart_Is->show();
gnVolShowFlag = 0;
gnCurrShowFlag = 1;
}
函数作用:电压按钮:将电压波形和坐标系展示,电流波形和坐标系关闭。同时加入对应事件FLAG。这个Flag变量最好加上,以便在其他子函数中使用。
(7)如果在一屏中显示,必须清屏,如果不清屏,所有的线都会在最后一个点、第二屏的第一个点产生回折线,挺难看。如果是滚屏的话,这句话就不用了。
m_series_point_Vsua->clear();
m_series_point_Vsub->clear();
m_series_point_Vsuc->clear();
(8)当ui界面发生大小变化时,其内部的表和按钮什么的也要相应的变化,这个需要用到大小变化的槽函数,需要自己编写
protected:
virtual void resizeEvent(QResizeEvent *event) override;
此段为自己编写虚函数,覆盖原有的ui界面大小改变槽函数(中断函数)。加入到h文件里。
void MainWindow::resizeEvent(QResizeEvent *event)
{
qreal ratioW;
qreal ratioH;
ratioW = event->size().width()/1200.0;
ratioH = event->size().height()/900.0;
chartView_Vs->setFixedWidth(1200*ratioW);
chartView_Vs->setFixedHeight(300*ratioH);
}
加入这个槽函数,当你ui界面大小变化时,自然就会进入此函数,就会执行里面的程序。
项目设计的初始界面1200乘以900,以此为基准,当大小变化时,其实时的界面大小数据会采集,计算得到比例系数。根据比例系数重新定义图表的大小,就能改变其占用的比例,达到ui界面大小变化,内部图表大小变化的目的。当然如果图表的位置不在原点,还需要将其位置也按照一定的比例进行挪动。使用move语句即可。
(9)波形观察函数,也就是看波形的时候需要放大或者缩小。具体函数没有看啥意思,关键是脑子不好使,没看懂。但是加上就能用。
注意:需要加上(5)中的最后一句话。
bool Chart::sceneEvent(QEvent *event)
{
if (event->type() == QEvent::Gesture)
return gestureEvent(static_cast(event));
return QChart::event(event);
}
bool Chart::gestureEvent(QGestureEvent *event)
{
if (QGesture *gesture = event->gesture(Qt::PanGesture)) {
QPanGesture *pan = static_cast(gesture);
QChart::scroll(-(pan->delta().x()), pan->delta().y());
}
if (QGesture *gesture = event->gesture(Qt::PinchGesture)) {
QPinchGesture *pinch = static_cast(gesture);
if (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged)
QChart::zoom(pinch->scaleFactor());
}
return true;
}
bool ChartView::viewportEvent(QEvent *event)
{
if (event->type() == QEvent::TouchBegin) {
// By default touch events are converted to mouse events. So
// after this event we will get a mouse event also but we want
// to handle touch events as gestures only. So we need this safeguard
// to block mouse events that are actually generated from touch.
m_isTouching = true;
// Turn off animations when handling gestures they
// will only slow us down.
chart()->setAnimationOptions(QChart::NoAnimation);
}
return QChartView::viewportEvent(event);
}
void ChartView::mousePressEvent(QMouseEvent *event)
{
if (m_isTouching)
return;
QChartView::mousePressEvent(event);
}
void ChartView::mouseMoveEvent(QMouseEvent *event)
{
if (m_isTouching)
return;
QChartView::mouseMoveEvent(event);
}
void ChartView::mouseReleaseEvent(QMouseEvent *event)
{
if (m_isTouching)
m_isTouching = false;
// Because we disabled animations when touch event was detected
// we must put them back on.
chart()->setAnimationOptions(QChart::SeriesAnimations);
QChartView::mouseReleaseEvent(event);
}
void ChartView::keyPressEvent(QKeyEvent *event)
{
switch (event->key()) {
case Qt::Key_Plus:
chart()->zoomIn();
break;
case Qt::Key_Minus:
chart()->zoomOut();
break;
//![1]
case Qt::Key_Left:
chart()->scroll(-10, 0);
break;
case Qt::Key_Right:
chart()->scroll(10, 0);
break;
case Qt::Key_Up:
chart()->scroll(0, 10);
break;
case Qt::Key_Down:
chart()->scroll(0, -10);
break;
default:
QGraphicsView::keyPressEvent(event);
break;
}
}
直接加上会出现好多错误,是因为h文件里面没有声明函数。这个需要额外加,至于为啥这样加,我就一次一次试验的,这样加好使,什么原因,说不上来,对class的c++编程还是不精通。下面程序加入到h文件中。
class Chart : public QChart
//![1]
{
public:
protected:
bool sceneEvent(QEvent *event);
private:
bool gestureEvent(QGestureEvent *event);
private:
};
class ChartView : public QChartView
//![1]
{
public:
ChartView(QChart *chart, QWidget *parent = 0);
//![2]
protected:
bool viewportEvent(QEvent *event);
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
void keyPressEvent(QKeyEvent *event);
//![2]
private:
bool m_isTouching;
};
这个部分添加的时候,直接加到原有的class后面,别加到里面,这个是自己独立的class。加上就能用随意观察波形了。
(1)面向对象的程序像搭积木,放在一起就行了(目前这样的理解,不知道以后会不会更深)。放在一起完成自己想要的功能就行。然后去验证对不对,只要对了,不出现bug,就说明可以了。
(2)刚接触看到的函数都TM不认识,这是啥,这又是啥。硬着头皮看看,跟着视频或者跟着教程做做,回头看看就感觉很简单了,我也就用一周的时间学习,就开始编写自己的程序了,现在看看还挺简单(或许是理解的太浅,造成的假象,本质的东西还没领悟到,没办法,时间紧迫)。最好能看例程,然后把别人的例程拿过来,直接用,别害怕改,改错了再改回来,总能试验正确的。出现错误的时候根本不知道错误在哪,硬着头皮好好研究研究,慢慢的就行了。
(3)好好利用里面的help。都是英文,硬着头皮看看,最后你会发现,我去,还能这样,我去,这也太看不懂了,我去,差不多就这样,能用就行。我去,算了,直接去CSDN去找别人写的吧。还是能看就看,基本的还是能看懂的。
注:由于小伙伴需要源代码的时间不同,登录邮箱界面太多麻烦,所以建立了一个订阅号,如果有问题或者需要源码,可添加订阅号,留言后会发送源代码或者有任何问题可留言,将积极解决提出的问题。