在项目中有一个数据展示需求,要求曲线和曲线对应的文字说明垂直对齐,且文字说明栏需要带有控制曲线显示/隐藏的复选框,并且复选框旁边需要显示对应曲线的颜色。
于是第一时间考虑到使用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项的添加,最终实现效果(封装的这个类还添加了鼠标追踪功能,鼠标追踪的标签会自动计算重合点,避免标签重合,相关的资料可以在网上找到)