在绘制图表时,我们有时需要实现一些交互式操作,例如图表缩放、显示鼠标光标处的数据,坐标、选择曲线上的数据点、显示或隐藏序列等。
具体功能
- 图表中包含一个 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);
}
版权归原作者 李墨鱼 所有, 如有侵权,请联系我们删除。