【QCustomPlot教程07】QCustomPlot 交互功能与用户操作

【QCustomPlot教程07】QCustomPlot 交互功能与用户操作

原创作者:郑同学的笔记

原文链接:https://zhengjunxue.blog.csdn.net/article/details/155229882

一、内置交互功能

QCustomPlot通过QCP::Interaction枚举定义了一系列内置交互功能,我们可以通过setInteractions()方法启用它们。

交互枚举详解

QCP::iRangeDrag - 允许用户通过鼠标拖拽来平移图表

QCP::iRangeZoom - 允许用户通过鼠标滚轮或拖拽选择区域来缩放图表

QCP::iSelectPlottables - 允许用户选择图表中的曲线、散点等绘图元素

QCP::iSelectAxes - 允许用户选择坐标轴

QCP::iSelectLegend - 允许用户选择图例

QCP::iSelectItems - 允许用户选择自定义添加的项目(如文本、箭头等)

Demo

cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QTimer>

void InteractionsDemo(QCustomPlot *customPlot)
{
    // 添加一条正弦曲线
       QVector<double> x(100), y(100);
       for (int i = 0; i < 100; ++i) {
           x[i] = i / 10.0;
           y[i] = qSin(x[i]);
       }
       customPlot->addGraph();
       customPlot->graph(0)->setData(x, y);
       customPlot->xAxis->setLabel("X");
       customPlot->yAxis->setLabel("Y");
       customPlot->legend->setVisible(true);
       customPlot->graph(0)->setName("sin(x)");

       // 启用交互功能
       customPlot->setInteractions(
           QCP::iRangeDrag |          // 拖动平移
           QCP::iRangeZoom |          // 滚轮缩放
           QCP::iSelectPlottables |   // 选择图线
           QCP::iSelectAxes |         // 选择坐标轴
           QCP::iSelectLegend         // 选择图例
       );

       customPlot->rescaleAxes();
       customPlot->replot();
}

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 创建 QCustomPlot 实例
   QCustomPlot *customPlot = new QCustomPlot(this);
   setCentralWidget(customPlot);

   InteractionsDemo(customPlot);

//    LegendDemo *demo = new LegendDemo(this);
//    setCentralWidget(demo);
}

MainWindow::~MainWindow()
{
    delete ui;
}

二、自定义交互功能

除了内置交互,QCustomPlot 还支持高度自定义的交互逻辑,常见包括:

  • 数据追踪器(QCPItemTracer)
    十字光标(十字坐标线)
    自定义鼠标事件处理

1、数据追踪器:QCPItemTracer

用于在曲线上动态显示某个数据点的位置信息。

cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QTimer>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 创建 QCustomPlot 实例
   QCustomPlot *customPlot = new QCustomPlot(this);
   setCentralWidget(customPlot);

   // 添加正弦曲线
       QVector<double> x(100), y(100);
       for (int i = 0; i < 100; ++i) {
           x[i] = i / 10.0;
           y[i] = std::sin(x[i]);
       }
       customPlot->addGraph();
       customPlot->graph(0)->setData(x, y);
       customPlot->xAxis->setLabel("X");
       customPlot->yAxis->setLabel("Y");
       customPlot->replot();

       // 创建追踪器(作为成员变量或局部变量,但需保证生命周期)
       QCPItemTracer *tracer = new QCPItemTracer(customPlot);
       tracer->setStyle(QCPItemTracer::tsCircle);
       tracer->setSize(8);
       tracer->setPen(QPen(Qt::red));
       tracer->setBrush(Qt::red);
       tracer->setVisible(false); // 初始隐藏

       // 关键:将追踪器关联到图形
        tracer->setGraph(customPlot->graph(0));

       // 连接鼠标移动事件:现在在 MainWindow 成员函数中,this 有效!
       connect(customPlot, &QCustomPlot::mouseMove, this, [=](QMouseEvent *event) {
           double key = customPlot->xAxis->pixelToCoord(event->pos().x());
           auto graph = customPlot->graph(0);
           double minDist = std::numeric_limits<double>::max();
           int bestIndex = -1;

           // 遍历数据点找最近的
           for (int i = 0; i < graph->data()->size(); ++i) {
               double dist = std::abs(graph->data()->at(i)->key - key);
               if (dist < minDist) {
                   minDist = dist;
                   bestIndex = i;
               }
           }

           if (bestIndex >= 0) {
               tracer->setGraphKey(graph->data()->at(bestIndex)->key);
               tracer->setVisible(true);
           } else {
               tracer->setVisible(false);
           }
           customPlot->replot();
       });

       // 启用交互(可选)
       customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom);

//    Crosshair *demo = new Crosshair(customPlot);
//    setCentralWidget(demo);
}

MainWindow::~MainWindow()
{
    delete ui;
}

2、十字坐标线(Crosshair)

使用两条 QCPItemStraightLine 实现十字光标。

Demo:添加十字光标

cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QTimer>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 创建 QCustomPlot 实例
   QCustomPlot *customPlot = new QCustomPlot(this);
   setCentralWidget(customPlot);

   // 添加十字线
   QCPItemStraightLine *vLine = new QCPItemStraightLine(customPlot);
   QCPItemStraightLine *hLine = new QCPItemStraightLine(customPlot);
   vLine->setPen(QPen(Qt::gray, 1, Qt::DashLine));
   hLine->setPen(QPen(Qt::gray, 1, Qt::DashLine));

   connect(customPlot, &QCustomPlot::mouseMove, this, [=](QMouseEvent *event) {
       double x = customPlot->xAxis->pixelToCoord(event->pos().x());
       double y = customPlot->yAxis->pixelToCoord(event->pos().y());

       vLine->point1->setCoords(x, customPlot->yAxis->range().lower);
       vLine->point2->setCoords(x, customPlot->yAxis->range().upper);
       hLine->point1->setCoords(customPlot->xAxis->range().lower, y);
       hLine->point2->setCoords(customPlot->xAxis->range().upper, y);

       customPlot->replot();
   });
}

MainWindow::~MainWindow()
{
    delete ui;
}

3、QCustomPlot 鼠标事件重写的两种方式

在 QCustomPlot 中处理鼠标事件有两种主要方式:继承方式和组合方式。下面详细讲解这两种方法的具体实现。

  • 方式一:继承 QCustomPlot(继承方式)
    通过创建 QCustomPlot 的子类来直接重写鼠标事件。
cpp 复制代码
// mycustomplot.h
#ifndef MYCUSTOMPLOT_H
#define MYCUSTOMPLOT_H

#include "qcustomplot/qcustomplot.h"

class MyCustomPlot : public QCustomPlot
{
    Q_OBJECT
public:
    explicit MyCustomPlot(QWidget *parent = nullptr);

protected:
    // 重写鼠标事件
    virtual void mousePressEvent(QMouseEvent *event) override;
    virtual void mouseMoveEvent(QMouseEvent *event) override;
    virtual void mouseReleaseEvent(QMouseEvent *event) override;
    virtual void wheelEvent(QWheelEvent *event) override;

signals:
    // 可以定义一些信号,用于与外部通信
    void mouseMoved(QMouseEvent *event);
};

#endif // MYCUSTOMPLOT_H
cpp 复制代码
// mycustomplot.cpp
#include "mycustomplot.h"
#include <QMouseEvent>
#include <QWheelEvent>

MyCustomPlot::MyCustomPlot(QWidget *parent) : QCustomPlot(parent)
{
    // 初始化代码...
    this->xAxis->setRange(0,10);
    this->yAxis->setRange(0,10);
}

void MyCustomPlot::mousePressEvent(QMouseEvent *event)
{
    // 先调用基类处理,以确保QCustomPlot的内置交互功能(如拖动、缩放)仍然有效
    QCustomPlot::mousePressEvent(event);

    // 自定义处理
    if (event->button() == Qt::LeftButton) {
        // 左键按下处理
        qDebug() << "Left button pressed at" << event->pos();
    }
}

void MyCustomPlot::mouseMoveEvent(QMouseEvent *event)
{
    // 先调用基类处理
    QCustomPlot::mouseMoveEvent(event);

    // 自定义处理
    qDebug() << "Mouse moved at" << event->pos();
    emit mouseMoved(event); // 发出信号
}

void MyCustomPlot::mouseReleaseEvent(QMouseEvent *event)
{
    // 先调用基类处理
    QCustomPlot::mouseReleaseEvent(event);

    // 自定义处理
    if (event->button() == Qt::LeftButton) {
        qDebug() << "Left button released at" << event->pos();
    }
}

void MyCustomPlot::wheelEvent(QWheelEvent *event)
{
    // 先调用基类处理,以实现滚轮缩放
    QCustomPlot::wheelEvent(event);

    // 自定义处理
    qDebug() << "Wheel event";
}

使用

cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QTimer>
#include "mycustomplot.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 创建 QCustomPlot 实例
//   QCustomPlot *customPlot = new QCustomPlot(this);
//   setCentralWidget(customPlot);

//   customPlot->installEventFilter()

    MyCustomPlot *demo = new MyCustomPlot(this);
    setCentralWidget(demo);
}

MainWindow::~MainWindow()
{
    delete ui;
}
  • 方式二:使用 QCustomPlot 作为成员(组合方式)
    通过将 QCustomPlot 作为成员变量,并使用事件过滤器来处理鼠标事件。
cpp 复制代码
// mywidget.h
#ifndef MYWIDGET_H
#define MYWIDGET_H

#include <QWidget>
#include "qcustomplot/qcustomplot.h"

class MyWidget : public QWidget
{
    Q_OBJECT
public:
    explicit MyWidget(QWidget *parent = nullptr);

protected:
    // 重写事件过滤器
    virtual bool eventFilter(QObject *obj, QEvent *event) override;

private:
    QCustomPlot *m_customPlot;
};

#endif // MYWIDGET_H
cpp 复制代码
// mywidget.cpp
#include "mywidget.h"
#include <QMouseEvent>
#include <QVBoxLayout>

MyWidget::MyWidget(QWidget *parent) : QWidget(parent)
{
    m_customPlot = new QCustomPlot(this);
    QVBoxLayout *layout = new QVBoxLayout(this);
    layout->addWidget(m_customPlot);

    // 安装事件过滤器
    m_customPlot->installEventFilter(this);

    // 初始化m_customPlot...
}

bool MyWidget::eventFilter(QObject *obj, QEvent *event)
{
    if (obj == m_customPlot) {
        QMouseEvent *mouseEvent;
        switch (event->type()) {
        case QEvent::MouseButtonPress:
            mouseEvent = static_cast<QMouseEvent*>(event);
            if (mouseEvent->button() == Qt::LeftButton) {
                qDebug() << "Left button pressed at" << mouseEvent->pos();
                // 处理事件,如果需要阻止进一步处理(例如QCustomPlot的内置处理),则返回true
                // 否则返回false,让事件继续传递
            }
            break;
        case QEvent::MouseMove:
            mouseEvent = static_cast<QMouseEvent*>(event);
            qDebug() << "Mouse moved at" << mouseEvent->pos();
            break;
        case QEvent::MouseButtonRelease:
            mouseEvent = static_cast<QMouseEvent*>(event);
            if (mouseEvent->button() == Qt::LeftButton) {
                qDebug() << "Left button released at" << mouseEvent->pos();
            }
            break;
        case QEvent::Wheel:
            qDebug() << "Wheel event";
            break;
        default:
            break;
        }
    }

    // 将事件传递给基类事件过滤器
    return QWidget::eventFilter(obj, event);
}

如何使用

cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QTimer>
#include "mywidget.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 创建 QCustomPlot 实例
//   QCustomPlot *customPlot = new QCustomPlot(this);
//   setCentralWidget(customPlot);

//   customPlot->installEventFilter()

    MyWidget *demo = new MyWidget(this);
    setCentralWidget(demo);
}

MainWindow::~MainWindow()
{
    delete ui;
}

三、信号与槽机制

QCustomPlot 提供丰富的信号,便于响应用户操作。

  • 1 轴范围变化:rangeChanged()
    当用户拖动或缩放导致坐标轴范围改变时触发。
交互类型 信号 说明
轴范围变化 QCPAxis::rangeChanged(const QCPRange &newRange) 当坐标轴范围改变时发出。
轴范围变化(含旧值) QCPAxis::rangeChanged(const QCPRange &newRange, const QCPRange &oldRange) 重载信号,同时提供改变前的旧范围。
cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QTimer>
#include "mywidget.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 创建图表
    QCustomPlot *plot = new QCustomPlot;
    setCentralWidget(plot);

    // 添加数据
    QVector<double> x(50), y(50);
    for (int i = 0; i < 50; ++i) {
        x[i] = i / 5.0 - 5;
        y[i] = x[i] * x[i] + qSin(x[i] * 2);
    }
    plot->addGraph();
    plot->graph(0)->setData(x, y);
    plot->graph(0)->setScatterStyle(QCPScatterStyle::ssDisc);

    // 设置可交互部分:允许选择数据点
//    plot->setInteractions(QCP::iSelectPlottables);
    // 启用交互(可选)
    plot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom);

    // 连接选择信号
    // 关键:监听范围变化
    connect(plot->xAxis, SIGNAL(rangeChanged(QCPRange)), this, SLOT(xAxisChanged(QCPRange)));
    connect(plot->yAxis, SIGNAL(rangeChanged(QCPRange)), this, SLOT(yAxisChanged(QCPRange)));

    // 设置坐标轴范围
    plot->xAxis->setRange(-10, 10);
    plot->yAxis->setRange(-10, 10);

    plot->xAxis->setLabel("X轴");
    plot->yAxis->setLabel("Y轴");
    // 为选择的点设置样式
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::xAxisChanged(const QCPRange &range)
{
        // 更新水平滚动条
        qDebug() << "X轴范围变化:" << range.lower << "~" << range.upper;
}

void MainWindow::yAxisChanged(const QCPRange &range)
{
        // 更新水平滚动条
        qDebug() << "y轴范围变化:" << range.lower << "~" << range.upper;
}
cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "qcustomplot/qcustomplot.h"
namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void xAxisChanged(const QCPRange &range);
    void yAxisChanged(const QCPRange &range);

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H
  • 2、主要交互信号分类
信号类型 信号名称 说明
鼠标事件 mouseDoubleClick, mousePress, mouseMove, mouseRelease, mouseWheel 基础鼠标交互
元素点击 plottableClick, itemClick, axisClick, legendClick 特定图表元素点击
选择变化 selectionChangedByUser 用户交互引起的选择变化
重绘事件 beforeReplot, afterLayout, afterReplot 图表重绘过程
cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QTimer>
#include "mywidget.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    QCustomPlot *customPlot = new QCustomPlot(this);
   setCentralWidget(customPlot);

   // 添加一个 Graph
   customPlot->addGraph();
   QVector<double> x(10), y(10);
   for (int i = 0; i < 10; ++i) {
       x[i] = i;
       y[i] = qSin(i);
   }
   customPlot->graph(0)->setData(x, y);
   customPlot->graph(0)->setLineStyle(QCPGraph::lsLine);

   // 启用选择功能:允许选择整个 plottable(包括数据点)
   customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
   customPlot->graph(0)->setSelectable(QCP::stWhole); // 或 stWhole

   // 连接信号
   connect(customPlot, &QCustomPlot::selectionChangedByUser,
           this, &MainWindow::onSelectionChangedByUser);

   customPlot->rescaleAxes();
   customPlot->replot();
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::onSelectionChangedByUser()
{
    QCustomPlot *plot = qobject_cast<QCustomPlot*>(sender());
   if (!plot) return;

   qDebug() << "Selection changed by user.";

   // 遍历所有 graph,检查是否有被选中的数据点
   for (int i = 0; i < plot->graphCount(); ++i)
   {
       QCPGraph *graph = plot->graph(i);
       if (graph->selected())
       {
           qDebug() << "Graph" << i << "is selected as a whole.";
       }
    }
}