QT 图表的使用(三) -- 图表交互操作

在绘制图表时,我们有时需要实现一些交互式操作,例如图表缩放、显示鼠标光标处的数据,坐标、选择曲线上的数据点、显示或隐藏序列等。

在这里插入图片描述

具体功能

  • 图表中包含一个 QLineSeries 序列和一个QSplineSeries 序列,且显示了数据点。
  • 在图表上拖动鼠标时可以放大图表,或拖动曲线移动。使用鼠标滚轮可以放大或缩小图表。
  • 鼠标光标在图表上移动时,会在状态栏上实时显示鼠标光标处的数据坐标。
  • 鼠标光标移动到一个序列上时,序列颜色会变为红色,并显示曲线上的坐标;鼠标光标移出序列,时序列颜色恢复为黑色。
  • 点击序列上的数据点,数据点的选中状态就会改变。选中的数据点会用专门的颜色显示,并且数据点标记要大于正常数据点的标记,如图 12-9 中所示的几个大一些的数据点。
  • 图例具有类似于复选框的功能,点击图例某一项就可以显示或隐藏对应的序列。
图表交互操作概述
QChart类的功能函数

QChart 的上层父类是 QGraphicsItem,其功能类似于图形/视图架构中的图形项的功能。QChart有一些接口函数可实现图表的移动、缩放等操作

QPointF mapToPosition(const QPointF &value, QAbstractSeries *series = nullptr)

QPointF mapToValue(const QPointF &position, QAbstractSeries *series = nullptr)

void zoom(qreal factor) //缩放图表,factor 值大于 1 表示放大,factor 值为 0~1 表示缩小

void zoomIn() //放大 2 倍

void zoomIn(const QRectF &rect) //放大到最大,使得 rect 表示的矩形范围依然能被显示

void zoomOut() //缩小到原来的一半

void zoomReset() //恢复原始大小

void scroll(qreal dx, qreal dy) //移动图表的可视区域,参数单位是像素

QChartView的自动放大功能

QChartView 有一个函数 setRubberBand()可以设置在视图上用鼠标框选时的放大模式:

void QChartView::setRubberBand(const QChartView::RubberBands &rubberBand)

枚举类型 QChartView::RubberBand 有以下几种枚举值。

• QChartView::NoRubberBand:无任何动作,不自动放大。

• QChartView::VerticalRubberBand:拖动鼠标时,自动绘制一个矩形框,宽度等于整个图的宽度,高度等于鼠标拖动的范围的高度。释放鼠标后,放大显示此矩形框内的内容。

• QChartView::HorizontalRubberBand:拖动鼠标时,自动绘制一个矩形框,高度等于整个图的高度,宽度等于鼠标拖动的范围的宽度。释放鼠标后,放大显示此矩形框内的内容。

• QChartView::RectangleRubberBand:拖动鼠标时,自动绘制一个矩形框,宽度和高度分别等于鼠标拖动的范围的宽度和高度。释放鼠标后,显示效果与 VerticalRubberBand 模式的基本相同,只是垂直方向放大,没有放大显示框选的矩形框的内容。这应该是 Qt 6.2 的一个 bug。

• QChartView::ClickThroughRubberBand:这是一个额外的选项,需要与其他选项进行或运算,再作为函数 setRubberBand()的参数。使用这个选项后,鼠标的 clicked()信号才会被传递给图表中的序列对象,否则,在自动框选放大模式下,序列接收不到 clicked()信号。

在 QChartView 的父类 QGraphicsView 中有一个函数 setDragMode(),用于设置鼠标拖动模式,

它的函数原型定义如下:

void QGraphicsView::setDragMode(QGraphicsView::DragMode mode)

参数 mode 是枚举类型 QGraphicsView::DragMode,其各种枚举值的作用如下。

• QGraphicsView::NoDrag:无动作。

• QGraphicsView::ScrollHandDrag:鼠标光标变成手形,拖动鼠标时会拖动图中的曲线。

• QGraphicsView::RubberBandDrag:鼠标光标变成十字形,拖动鼠标时会自动绘制一个矩形框。

函数 setDragMode()设置的值不会影响 QChartView 的自动放大功能,即不管 setDragMode()设置的是什么鼠标拖动模式,只要函数 setRubberBand()设置的是某种自动放大模式,在拖动鼠标时图表就会放大

QXYSeries类的信号

QLineSeries 的父类 QXYSeries 中定义了很多信号,其中对于交互式操作比较有用

的是如下几个信号。

  • void clicked(const QPointF &point) //点击了曲线
  • void doubleClicked(const QPointF &point) //双击了曲线
  • void hovered(const QPointF &point, bool state) //鼠标光标移入或移出了曲线
  • void pressed(const QPointF &point) //鼠标光标在曲线上,按下了某个鼠标键
  • void released(const QPointF &point) //鼠标光标在曲线上,释放了某个鼠标键
自定义图表视图类TChartView
TChartView的定义

需要在 QChartView 组件里对鼠标事件和按键事件进行处理,这就需要自定义一个从

QChartView 继承的类。

TChartView 类的定义如下:

class TChartView : public QChartView 
{ 
 Q_OBJECT 
private: 
 QPoint beginPoint; //选择矩形区域的起点
 QPoint endPoint; //选择矩形区域的终点
 bool m_customZoom= false; //是否使用自定义矩形放大模式
protected: 
 void mousePressEvent(QMouseEvent *event); //鼠标左键被按下
 void mouseReleaseEvent(QMouseEvent *event); //鼠标左键被释放
 void mouseMoveEvent(QMouseEvent *event); //鼠标移动
 void keyPressEvent(QKeyEvent *event); //按键事件
 void wheelEvent(QWheelEvent *event); //鼠标滚轮事件,缩放
public: 
 TChartView(QWidget *parent = nullptr); 
 ~TChartView(); 
 void setCustomZoomRect(bool custom); //设置是否使用自定义矩形放大模式
signals: 
 void mouseMovePoint(QPoint point); //鼠标移动信号
};

下面是 TChartView 类构造函数和公有函数 setCustomZoomRect()的代码:

TChartView::TChartView(QWidget *parent):QChartView(parent) 
{ 
     this->setMouseTracking(true); //必须设置为 true,这样才会实时产生 	mouseMoveEvent 事件
     this->setDragMode(QGraphicsView::NoDrag); //设置拖动模式 
     this->setRubberBand(QChartView::NoRubberBand); //设置自动放大模式
} 

void TChartView::setCustomZoomRect(bool custom) 
{ 
 	m_customZoom= custom; 
}
对鼠标框选的处理

拖动鼠标框选范围时,会触发 mousePressEvent()和 mouseReleaseEvent()事件处理函数,这两个函数的代码如下:

void TChartView::mousePressEvent(QMouseEvent *event) 
{//鼠标左键被按下,记录 beginPoint 
     if (event->button() == Qt::LeftButton) 
     beginPoint= event->pos(); 
     QChartView::mousePressEvent(event); //父类继续处理事件,必须如此调用
} 
void TChartView::mouseReleaseEvent(QMouseEvent *event) 
{ 
     if (event->button() == Qt::LeftButton) 
     { 
         endPoint= event->pos(); 
         if ((this->dragMode() == QGraphicsView::ScrollHandDrag) 
         &&(this->rubberBand() == QChartView::NoRubberBand)) //移动
             chart()->scroll(beginPoint.x()-endPoint.x(), endPoint.y() - beginPoint.y()); 
         else if (m_customZoom && this->dragMode() == QGraphicsView::RubberBandDrag) 
         {//放大
             QRectF rectF; 
             rectF.setTopLeft(beginPoint); 
             rectF.setBottomRight(endPoint); 
             this->chart()->zoomIn(rectF); //按矩形区域放大
         } 
     } 
     QChartView::mouseReleaseEvent(event); //父类继续处理事件,必须如此调用
}
其他事件的处理
void TChartView::mouseMoveEvent(QMouseEvent *event) 
{//鼠标移动事件
     QPoint point= event->pos(); 
     emit mouseMovePoint(point); //发射信号
     QChartView::mouseMoveEvent(event); //父类继续处理事件
} 
void TChartView::keyPressEvent(QKeyEvent *event) 
{//按键控制
 switch (event->key()) 
 { 
 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; 
 case Qt::Key_PageUp: 
 	chart()->scroll(0, -50); break; 
 case Qt::Key_PageDown: 
 	chart()->scroll(0, 50); break; 
 case Qt::Key_Escape: 
 	chart()->zoomReset(); break; 
 default: 
 	QGraphicsView::keyPressEvent(event); 
 } 
} 
void TChartView::wheelEvent(QWheelEvent *event) 
{//鼠标滚轮事件处理,缩放
     QPoint numDegrees = event->angleDelta()/8; 
     if (!numDegrees.isNull()) 
     { 
         QPoint numSteps = numDegrees/15; //步数
         int stepY=numSteps.y(); //垂直方向上滚轮的滚动步数
         if (stepY >0) //大于 0,前向滚动,放大
         	chart()->zoom(1.1*stepY); 
         else 
         	chart()->zoom(-0.9*stepY); 
     } 
     event->accept(); 
}
主窗口设计和初始化

采用可视化方法设计主窗口界面,在工作区放置一个 QGraphicsView 组件,然后将其提升为TChartView 类,将其对象名称设置为 chartView。主窗口类 MainWindow 的定义如下:

class MainWindow : public QMainWindow 
{ 
 Q_OBJECT 
private: 
 QChart *chart; //图表对象
 QLabel *lab_chartXY; //状态栏上的标签
 QLabel *lab_hoverXY; 
 QLabel *lab_clickXY; 
 void createChart(); //创建图表
 void prepareData(); //准备数据
 int getIndexFromX(QXYSeries *series, qreal xValue, qreal tol=0.05); 
 //返回数据点的序号
public: 
 MainWindow(QWidget *parent = nullptr); 
private slots: 
 void do_legendMarkerClicked(); //图例被点击
 void do_mouseMovePoint(QPoint point); //鼠标移动
 void do_series_clicked(const QPointF &point); //序列被点击
 void do_series_hovered(const QPointF &point, bool state); //移入或移出序列
private: 
 Ui::MainWindow *ui; 
}; 

MainWindow 类中有几个自定义槽函数,用于与一些信号关联并进行处理。函数 getIndexFromX()用于在一个序列中根据参数xValue的值确定数据点的序号,在用鼠标选择数据点时会用到这个函数。

MainWindow 类的构造函数代码如下:

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) 
{ 
     ui->setupUi(this); 
     this->setCentralWidget(ui->chartView); 
     lab_chartXY = new QLabel("Chart X=, Y= "); //用于添加到状态栏的 QLabel 组件
     lab_chartXY->setMinimumWidth(200); 
     ui->statusBar->addWidget(lab_chartXY); 
     lab_hoverXY = new QLabel("Hovered X=, Y= "); 
     lab_hoverXY->setMinimumWidth(200); 
     ui->statusBar->addWidget(lab_hoverXY); 
     lab_clickXY = new QLabel("Clicked X=, Y= "); 
     lab_clickXY->setMinimumWidth(200); 
     ui->statusBar->addWidget(lab_clickXY); 
     createChart(); //创建图表
     prepareData(); //生成数据
     connect(ui->chartView,SIGNAL(mouseMovePoint(QPoint)), 
     this, SLOT(do_mouseMovePoint(QPoint))); //鼠标移动事件
}

构造函数里创建了图表,还将 chartView 的 mouseMovePoint()信号与槽函数 do_mouseMovePoint()关联。创建图表的代码如下:

void MainWindow::createChart() 
{ //创建图表
 chart = new QChart(); 
 ui->chartView->setChart(chart); 
 ui->chartView->setRenderHint(QPainter::Antialiasing); 
 ui->chartView->setCursor(Qt::CrossCursor); //设置鼠标光标为十字形
 QLineSeries *series0 = new QLineSeries(); 
 series0->setName("LineSeries 曲线"); 
 series0->setPointsVisible(true); //显示数据点
 series0->setMarkerSize(5); //数据点大小
 series0->setSelectedColor(Qt::blue); //选中点的颜色
 connect(series0,&QLineSeries::clicked, this, &MainWindow::do_series_clicked); 
 connect(series0,&QLineSeries::hovered, this, &MainWindow::do_series_hovered); 
 QSplineSeries *series1 = new QSplineSeries(); 
 series1->setName("SplineSeries 曲线"); 
 series1->setPointsVisible(true); 
 series1->setMarkerSize(5); 
 series1->setSelectedColor(Qt::blue); //选中点的颜色
 connect(series1,&QSplineSeries::clicked, this, &MainWindow::do_series_clicked); 
 connect(series1,&QSplineSeries::hovered, this, &MainWindow::do_series_hovered); 
 QPen pen(Qt::black); 
 pen.setStyle(Qt::DotLine); //虚线
 pen.setWidth(2); 
 series0->setPen(pen); 
 pen.setStyle(Qt::SolidLine); //实线
 series1->setPen(pen); 
 chart->addSeries(series0); 
 chart->addSeries(series1); 
 QValueAxis *axisX = new QValueAxis; 
 axisX->setRange(0, 10); 
 axisX->setLabelFormat("%.1f"); //标签格式
 axisX->setTickCount(11); //主刻度个数
 axisX->setMinorTickCount(2); 
 axisX->setTitleText("time(secs)"); 
 QValueAxis *axisY = new QValueAxis; 
 axisY->setRange(-2, 2); 
 axisY->setLabelFormat("%.2f"); //标签格式
 axisY->setTickCount(5); 
 axisY->setMinorTickCount(2); 
 axisY->setTitleText("value"); 
 chart->addAxis(axisX,Qt::AlignBottom); //坐标轴添加到图表中,并指定方向
 chart->addAxis(axisY,Qt::AlignLeft); 
 series0->attachAxis(axisX); //序列 series0 附加坐标轴
 series0->attachAxis(axisY); 
 series1->attachAxis(axisX); //序列 series1 附加坐标轴
 series1->attachAxis(axisY); 
 foreach (QLegendMarker* marker, chart->legend()->markers()) 
 connect(marker, SIGNAL(clicked()), this, SLOT(do_legendMarkerClicked())); 
} 
void MainWindow::prepareData() 
{//为序列生成数据
 QLineSeries *series0= (QLineSeries *)chart->series().at(0); 
 QSplineSeries *series1= (QSplineSeries *)chart->series().at(1); 
 qreal t=0, y1,y2, intv=0.5; 
 int cnt= 21; 
 for(int i=0; i<cnt; i++) 
 { 
 int rd= QRandomGenerator::global()->bounded(-5,6); //随机整数,[-5,5] 
 y1= qSin(2*t)+rd/50; 
 series0->append(t,y1); 
 rd= QRandomGenerator::global()->bounded(-5,6); //随机整数,[-5,5] 
 y2= qSin(2*t+20)+rd/50; 
 series1->append(t,y2); 
 t += intv; 
 } 
}
交互操作功能的实现
鼠标移动时显示光标处的坐标

自定义槽函数 do_mouseMovePoint()与界面组件 chartView 的 mouseMovePoint()信号关联,该函数代码如下:

void MainWindow::do_mouseMovePoint(QPoint point) 
{ 
 QPointF pt= chart->mapToValue(point); //变换为图表的坐标
 QString str= QString::asprintf("Chart X=%.1f,Y=%.2f",pt.x(),pt.y()); 
 lab_chartXY->setText(str); //状态栏上显示
}
QLegendMarker 的使用

主要是利用QLegendMarker 的点击信号关联自定义槽函数,而QLegendMarker的成员函数type可以返回图例标记类型,下面是这个自定义槽函数do_legendMarkerClicked()的代码:

void MainWindow::do_legendMarkerClicked() 
{ 
 QLegendMarker* marker= qobject_cast<QLegendMarker*> (sender()); 
 marker->series()->setVisible(!marker->series()->isVisible()); //序列的可见性
 marker->setVisible(true); //图例标记总是可见的
 qreal alpha= 1.0; 
 if (!marker->series()->isVisible()) 
 alpha= 0.5; //设置为半透明表示序列不可见
 QBrush brush= marker->labelBrush(); 
 QColor color= brush.color(); 
 color.setAlphaF(alpha); 
 brush.setColor(color); 
 marker->setLabelBrush(brush); //设置文字的 brush 
 brush= marker->brush(); 
 color= brush.color(); 
 color.setAlphaF(alpha); 
 brush.setColor(color); 
 marker->setBrush(brush); //设置图例标记的 brush 
}
序列的 hovered()和 clicked()信号的处理

两个序列的 hovered()信号关联同一个槽函数 do_series_hovered(),这个函数的代码如下:

void MainWindow::do_series_hovered(const QPointF &point, bool state) 
{ 
 QString str= "Series X=, Y="; 
 if (state) 
 str= QString::asprintf("Hovered X=%.1f,Y=%.2f",point.x(),point.y()); 
 lab_hoverXY->setText(str); //状态栏显示
 QLineSeries *series= qobject_cast<QLineSeries*> (sender()); //获取信号发射者
 QPen pen= series->pen(); 
 if (state) 
 pen.setColor(Qt::red); //鼠标光标移入序列,序列变成红色
 else 
 pen.setColor(Qt::black); //鼠标光标移出序列,序列恢复为黑色
 series->setPen(pen); 
}
void MainWindow::do_series_clicked(const QPointF &point) 
{ 
 	QString str= QString::asprintf("Clicked X=%.1f,Y=%.2f",point.x(),point.y()); 
 	lab_clickXY->setText(str); //状态栏显示
 	QLineSeries *series= qobject_cast<QLineSeries*> (sender()); //获取信号发射者
     int index= getIndexFromX(series, point.x()); //获取数据点序号
     if (index<0) 
     return; 
     bool isSelected= series->isPointSelected(index); //数据点是否被选中
     series->setPointSelected(index,!isSelected); //设置状态,选中或取消选中
} 

int MainWindow::getIndexFromX(QXYSeries *series, qreal xValue, qreal tol) 
{ 
 	QList<QPointF> points= series->points(); //返回数据点的列表
 	int index= -1; 
 for (int i=0; i<points.count(); i++) 
 { 
     qreal dx= xValue - points.at(i).x(); 
     if (qAbs(dx) <= tol) 
     { 
         index= i; 
         break; 
     } 
  } 
     return index; //-1 表示没有找到
}
图表的缩放和移动

TChartView 类里对鼠标事件和按键事件进行了处理,通过鼠标操作和按键操作就可以进行图表的缩放和移动,操作方式还与 QChartView 的 dragMode()和 rubberBand()函数的值有关。窗口上方有两个下拉列表框用于设置拖动模式和框选模式,其代码如下:

void MainWindow::on_comboDragMode_currentIndexChanged(int index) 
{// 设置拖动模式,dragMode,有 3 种模式: NoDrag、ScrollHandDrag、RubberBandDrag 
 ui->chartView->setDragMode(QGraphicsView::DragMode(index)); 
} 
void MainWindow::on_comboRubberBand_currentIndexChanged(int index) 
{//设置框选模式, rubberBand 
 ui->chartView->setCustomZoomRect(index == 4); //是否自定义模式
//必须有 ClickThroughRubberBand,才能将 clicked()信号传递给序列
 QFlags<QChartView::RubberBand> flags= QChartView::ClickThroughRubberBand; 
 switch(index) 
 { 
 case 0: 
 ui->chartView->setRubberBand(QChartView::NoRubberBand); 
 return; 
 case 1: 
 flags |= QChartView::VerticalRubberBand; //垂直方向选择
 break; 
 case 2: 
 flags |= QChartView::HorizontalRubberBand; //水平方向选择
 break; 
 case 3: 
 case 4: 
 flags |= QChartView::RectangleRubberBand; //矩形框选
 } 
 ui->chartView->setRubberBand(flags); 
}
  • 14
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值