Qt | 实现QCustomPlot的自定义图例复选框功能,控制曲线显示与隐藏

在项目中有一个数据展示需求,要求曲线和曲线对应的文字说明垂直对齐,且文字说明栏需要带有控制曲线显示/隐藏的复选框,并且复选框旁边需要显示对应曲线的颜色。

于是第一时间考虑到使用qcustomplot这个第三方库,因为本身qcustomplot就自带标签栏(legend)和在标签栏上显示的图例(legenditem)。

scss 复制代码
void Widget::demo1()
{
    QVBoxLayout* layout = new QVBoxLayout(this);
    QCustomPlot *customPlot = new QCustomPlot(this);
    layout->addWidget(customPlot);

    // 当传参为空时,addGraph会自动添加xAxis,yAxis,这里添加两条曲线
    customPlot->addGraph();
    customPlot->addGraph();

    auto graph1 = customPlot->graph(0);
    auto graph2 = customPlot->graph(1);
    graph1->setPen(QPen(Qt::blue));
    graph1->setName("曲线1");  // 默认的名称为graph + 1 + 索引
    graph2->setPen(QPen(Qt::red));
    graph2->setName("曲线2");

    // 准备数据
    QVector<double> x(101), y1(101), y2(101); // 创建数据点向量
    for (int i=0; i<101; ++i)
        {
            x[i] = i/50.0 - 1; // x 范围从 -1 到 1
            y1[i] = x[i]*x[i] + QRandomGenerator::global()->generateDouble() * 0.5; // y = x^2 + 随机偏移
            y2[i] = -x[i]*x[i] + QRandomGenerator::global()->generateDouble() * 0.5; // y = -x^2 + 随机偏移
        }

    // 为图形设置数据
    graph1->setData(x, y1);
    graph2->setData(x, y2);


    // 设置坐标轴标签
    customPlot->xAxis->setLabel("x");
    customPlot->yAxis->setLabel("y");


    // 设置轴的自适应
    customPlot->rescaleAxes();

    // 设置坐标轴范围
    // customPlot->xAxis->setRange(-1, 1);
    // customPlot->yAxis->setRange(0, 1);

    // 设置item可见
    customPlot->legend->setVisible(true);
    customPlot->legend->setFillOrder(QCPLegend::foColumnsFirst);

    customPlot->axisRect()->insetLayout()->setInsetAlignment(0, Qt::AlignBottom|Qt::AlignHCenter);
    // 移动图例到坐标系的下方

    // 重新绘制图表以显示曲线
    customPlot->replot();
}

基础方式实现效果如图

可以看到,即便调整了legend中item的排列方式,以及legend的位置,item还是处于坐标系中,这就导致会遮挡一部分的曲线。最终实现的效果应该是legend处于坐标系的最下方,且水平宽度与qcustomplot相同。

通过阅读源码,发现qcustomplot存在一个主布局QCPLayoutGrid(网格布局) ,默认的坐标系defaultAxisRect会被添加到这个主布局中,而legend会被添加到defaultAxisRect,所以接下来要做的就是,将原有的legend位置换一下。

在原有的代码中添加如下代码

rust 复制代码
// 设置item可见
customPlot->legend->setVisible(true);
// 关闭自动添加到图例中
customPlot->setAutoAddPlottableToLegend(false);
// 以列填充优先,一列满了后就填充下一列,默认是行优先
customPlot->legend->setFillOrder(QCPLegend::foColumnsFirst);
customPlot->plotLayout()->addElement(1,0,customPlot->legend);
// 设置legend的高度为20
customPlot->legend->setMaximumSize(QWIDGETSIZE_MAX, 20);
// 重新绘制图表以显示曲线

实现效果

到这里布局问题已经完成了,但是复选框图例项还没有实现,这里看了下源码,需要写一个自定义的图例类来替代原有的图例项,原有的图例类QCPPlottableLegendItem是继承自QCPAbstractLegendItem这个类,QCPPlottableLegendItem绘制图例项的源码如下:

可以看到原生item是根据初始化时,会传入自带的一个legend,和一个plottable,这个plottable在查看源码时,发现实际上是一个graph,所以当给qcustomplot->graph(idx)->setName(nameStr)后,自动生成的item会在draw中获取graph的name,并绘制出一个item。结合这个源码,实现一个带复选框的自定义item就很简单了,只需要自定义一个LegendItem,并重写对应的draw函数。

在重写时还发现一个问题,这个方法是QCPAbstractPlottable抽象类的一个接口,不同的plottable有着对应的实现,如果重写后需要调用这个方法,则需要对源码进行修改,实际上只需要添加一行代码:在QCPAbstractPlottable的头文件中将当前自定义item类声明为QCPAbstractPlottable的友元即可

scss 复制代码
mPlottable->drawLegendIcon(painter, iconRect);

qcustomplot的item是通过QPainter绘制出来的,所以说用不了QCheckBox,这里我也是使用的QPainter进行绘制

绘制的方式是画一个矩形(带圆角的),然后在矩形内部画三个点连接起来(可以找现有的复选框效果进行模仿绘制),这里发现开启抗锯齿后复选框会有边缘模糊的效果,不开则比较锐利。

实现的自定义item如下:

scss 复制代码
#ifndef CUSTOMLEGENDITEM_H
#define CUSTOMLEGENDITEM_H

#include "qcustomplot/qcustomplot.h"
class CustomLegendItem :public QCPPlottableLegendItem {
Q_OBJECT
public:
explicit CustomLegendItem(QCPAbstractPlottable *plottable, QCPLegend *parentLegend, const QString &text = "");
~CustomLegendItem();

protected:
virtual void draw(QCPPainter *painter) Q_DECL_OVERRIDE;
virtual void mousePressEvent(QMouseEvent *event, const QVariant &details) Q_DECL_OVERRIDE;
virtual void mouseDoubleClickEvent(QMouseEvent *event, const QVariant &details) Q_DECL_OVERRIDE;

signals:
void checkboxStateChanged(bool checked);

private:
void initConnection();

private:
bool mCheckBoxChecked;
int mCheckBoxSize;
QRect mCheckBoxRect;
QColor mCheckBoxBorderColor = Qt::black;
QColor mCheckBoxCheckedColor = Qt::blue;
int mIconTextSpacing;  // 图标与文字之间的间距
int mTotalHorizontalPadding;  // 整体水平方向的内边距
void drawCheckBox(QCPPainter *painter);
void handleMousePressEvent(QMouseEvent *event);

};
#endif // CUSTOMLEGENDITEM_H

#include "customlegenditem.h"
CustomLegendItem::CustomLegendItem(QCPAbstractPlottable *plottable, QCPLegend *parentLegend, const QString &text)
: QCPPlottableLegendItem(parentLegend, plottable)
{
    setSelectable(true);
    this->setAntialiased(true);
}

CustomLegendItem::~CustomLegendItem()
{

}

// 重写draw函数
void CustomLegendItem::draw(QCPPainter *painter)
{
    if (!mPlottable) return;

    mCheckBoxSize = 18;
    mIconTextSpacing = 5;  // 可以根据实际需求调整间距大小
    mTotalHorizontalPadding = 10;  // 整体水平方向预留的内边距,可按需调整

    int yCenter = mRect.y() + mRect.height() / 2;
    int startX = mRect.x() + mTotalHorizontalPadding;

    // 计算总宽度
    QSize iconSize = mParentLegend->iconSize();
    QFontMetrics fm(getFont());
    int textWidth = fm.horizontalAdvance(mPlottable->name());
    int totalWidth = mTotalHorizontalPadding * 2 + mCheckBoxSize + mIconTextSpacing * 2 + iconSize.width() + textWidth;

    // 更新mRect以适应新内容
    mRect.setWidth(totalWidth);
    mRect.setHeight(qMax(fm.height(), mCheckBoxSize));

    // 绘制复选框
    mCheckBoxRect = QRect(startX, yCenter - mCheckBoxSize / 2, mCheckBoxSize, mCheckBoxSize);
    drawCheckBox(painter);
    startX += mCheckBoxSize + mIconTextSpacing;

    // 绘制图标
    QRect iconRect(startX, yCenter - iconSize.height() / 2, iconSize.width(), iconSize.height());
    painter->save();
    painter->setClipRect(iconRect, Qt::IntersectClip);
    mPlottable->drawLegendIcon(painter, iconRect);
    painter->restore();
    startX += iconSize.width() + mIconTextSpacing;

    // 绘制文字
    painter->setFont(getFont());
    painter->setPen(QPen(getTextColor()));
    QRect textRect = painter->fontMetrics().boundingRect(mPlottable->name());
    int textHeight = qMax(textRect.height(),mCheckBoxRect.height());
    int textY = yCenter - textHeight / 2; // 根据文本高度居中
    painter->drawText(startX, textY, textRect.width(), textHeight, Qt::TextDontClip, mPlottable->name());

    // draw icon border:
    if (getIconBorderPen().style()!= Qt::NoPen)
    {
        painter->setPen(getIconBorderPen());
        painter->setBrush(Qt::NoBrush);
        int halfPen = qCeil(painter->pen().widthF()*0.5)+1;
        painter->setClipRect(mOuterRect.adjusted(-halfPen, -halfPen, halfPen, halfPen));
        painter->drawRect(iconRect);
    }
}

// 自绘复选框的函数实现
void CustomLegendItem::drawCheckBox(QCPPainter *painter)
{
    painter->setPen(Qt::black);  // 设置画笔颜色,可调整
    painter->setBrush(Qt::NoBrush);

    QPen checkBoxBorderPen(Qt::black,2);
    QPen checkBoxCheckedPen(QColor(64,65,66),2);
    QPen checkBoxUnCheckedPen(QColor(216,232,232),2);

    // 绘制对号
    int padding = 3; // 内边距,可根据实际大小调整
    QPoint topLeft(mCheckBoxRect.topLeft() + QPoint(padding, padding));
    QPoint bottomRight(mCheckBoxRect.bottomRight() - QPoint(padding, padding));
    QRect checkBoxArea(topLeft, bottomRight);

    auto w = checkBoxArea.width();
    auto h = checkBoxArea.height();
    auto x = checkBoxArea.x();
    auto y = checkBoxArea.y();

    // √从左至右分解为3个点,分别是ABC三点
    // A
    QPoint aPos(x + w/10,y + h/2);
    QPoint bPos(x + w/3, y + 4*h/5);
    QPoint cPos(x + 9 * w/10,y + h/3);

    if (mCheckBoxChecked)
    {
        painter->setPen(checkBoxCheckedPen);
    }
    else
    {
        painter->setPen(checkBoxUnCheckedPen);
    }
    painter->drawLine(aPos,bPos);
    painter->drawLine(bPos,cPos);

    painter->setPen(checkBoxBorderPen);
    painter->drawRoundedRect(mCheckBoxRect,5,5);
}

void CustomLegendItem::initConnection()
{

}

// 处理鼠标点击事件的函数实现
void CustomLegendItem::handleMousePressEvent(QMouseEvent *event)
{
    if (mCheckBoxRect.contains(event->pos()))
    {
        mCheckBoxChecked =!mCheckBoxChecked;  // 切换复选框状态
        mPlottable->setVisible(mCheckBoxChecked);
        emit checkboxStateChanged(mCheckBoxChecked);  // 发出状态改变的信号
        // 更新绘制,使复选框显示最新状态
        mPlottable->parentPlot()->replot();
    }
}

void CustomLegendItem::mousePressEvent(QMouseEvent *event, const QVariant &details)
{
    handleMousePressEvent(event);
}

void CustomLegendItem::mouseDoubleClickEvent(QMouseEvent *event, const QVariant &details)
{
    handleMousePressEvent(event);
}

最终使用这个item时,需要禁止自动添加图例项

kotlin 复制代码
this->setAutoAddPlottableToLegend(false);

最终在项目中使用时,我是重新封装了一个CheckBoxItemGraphPlot类,继承自qcustomplot,并重写addGraph,在addGraph方法中进行item项的添加,最终实现效果(封装的这个类还添加了鼠标追踪功能,鼠标追踪的标签会自动计算重合点,避免标签重合,相关的资料可以在网上找到)

相关推荐
Antonio9151 小时前
【Q&A】Qt中直接渲染和离屏渲染效率哪个高?
开发语言·qt·信息可视化
七夕先生1 小时前
【Qt】自定义标题栏 Title Bar的两种方案
开发语言·qt
m0_555762905 小时前
qt图表背景问题
开发语言·数据库·qt
laimaxgg13 小时前
Qt窗口控件之颜色对话框QColorDialog
开发语言·前端·c++·qt·命令模式·qt6.3
wkm95613 小时前
Ubuntu Qt: no service found for - “org.qt-project.qt.mediaplayer“
开发语言·qt·ubuntu
那里有颗树13 小时前
Qt程序增加Dump文件保存
qt·dump·未捕获异常
二进制人工智能13 小时前
【QT 多线程示例】两种多线程实现方式
开发语言·qt
梁山1号14 小时前
【QT】】qcustomplot的初步使用二
c++·单片机·qt
孤独得猿14 小时前
Qt带参数的信号和槽,以及信号与槽的连接方式
开发语言·qt
任小七15 小时前
VTK-8.2.0源码编译和初步使用(Cmake+VS2015+Qt5.14.2)
qt·vtk·vs