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项的添加,最终实现效果(封装的这个类还添加了鼠标追踪功能,鼠标追踪的标签会自动计算重合点,避免标签重合,相关的资料可以在网上找到)

相关推荐
A.A呐3 小时前
【QT第三章】常用控件2
开发语言·qt
笨笨马甲3 小时前
Qt 实现三维坐标系的方法
开发语言·qt
谁动了我的代码?4 小时前
VNC中使用QT的GDB调试,触发断点时与界面窗口交互导致整个VNC冻结
开发语言·qt·svn
肖恭伟5 小时前
QtCreator Linux ubuntu24.04问题集合
linux·windows·qt
vegetablesssss6 小时前
QT国际化翻译
qt
困死,根本不会6 小时前
Qt Designer 基础操作学习笔记
开发语言·笔记·qt·学习·microsoft
喜欢喝果茶.6 小时前
Qt MQTT部署
开发语言·qt
浅碎时光8076 小时前
Qt 窗口 (菜单 工具栏 状态栏 浮动窗口 对话框)
qt
GIS阵地7 小时前
一场由Qt5 painter的drawRect引起的血雨腥风
开发语言·qt·gis·qgis
娇娇yyyyyy7 小时前
QT编程(8): qt自定义菜单项
qt·microsoft