使用QCustomPlot绘图时,相信大多数童鞋们都会有类似的诉求:希望鼠标移动到节点时,可以显示该节点的数据。这里转载了一篇关于 鼠标悬浮提示 的一篇文章,并对该文章涉及的代码经过了整理,经实践证明是可行的。
鼠标悬浮提示效果图
头文件
cpp
QCPToolTip
class QCPToolTip : public QCPAbstractItem
{
Q_OBJECT
public:
explicit QCPToolTip(QCustomPlot *parentPlot);
void setText(const QString &text);
void setFont(const QFont &font);
void setTextColor(const QColor &color);
void setBorderPen(const QPen &pen);
void setBrush(const QBrush &brush);
void setRadius(double xRadius, double yRadius, Qt::SizeMode mode = Qt::AbsoluteSize);
void setOffset(double xOffset, double yOffset);
void setPadding(const QMargins &paddings);
Q_SLOT void handleTriggerEvent(QMouseEvent *event);
void updatePosition(const QPointF &newPos, bool replot = false);
void update();
virtual double selectTest(const QPointF &pos, bool onlySelectable, QVariant *details) const Q_DECL_OVERRIDE;
QCPItemPosition * const position;
protected:
bool mPlotReplot; // 表明是由QCustomPlot刷新的,需要更新位置
QString mText;
Qt::Alignment mTextAlignment;
QFont mFont;
QColor mTextColor;
QPen mBorderPen;
QBrush mBrush;
QPointF mRadius;
Qt::SizeMode mSizeMode;
QPointF mOffset; // 偏移鼠标的距离
QMargins mPadding;
QCPGraph *mHighlightGraph;
QPointF mGraphDataPos;
virtual void draw(QCPPainter *painter) Q_DECL_OVERRIDE;
virtual void drawGraphScatterPlot(QCPPainter *painter, QCPGraph *graph, const QPointF &pos);
int pickClosest(double target, const QVector<double> &vector);
};
源文件
cpp
QCPToolTip
QCPToolTip::QCPToolTip(QCustomPlot *parentPlot)
: QCPAbstractItem(parentPlot),
position(createPosition(QLatin1String("position"))),
mPlotReplot(true),
mTextAlignment(Qt::AlignLeft | Qt::AlignVCenter),
mRadius(6, 6),
mSizeMode(Qt::AbsoluteSize),
mHighlightGraph(nullptr)
{
position->setType(QCPItemPosition::ptAbsolute);
setSelectable(false);
setLayer("overlay");
setBorderPen(Qt::NoPen);
setBrush(QColor(87, 98, 93, 180));
setTextColor(Qt::white);
setOffset(20, 20);
setPadding(QMargins(6, 6, 6, 6));
connect(mParentPlot, SIGNAL(mouseMove(QMouseEvent *)), this, SLOT(handleTriggerEvent(QMouseEvent *)));
}
void QCPToolTip::setText(const QString &text)
{
mText = text;
};
void QCPToolTip::setFont(const QFont &font)
{
mFont = font;
};
void QCPToolTip::setTextColor(const QColor &color)
{
mTextColor = color;
};
void QCPToolTip::setBorderPen(const QPen &pen)
{
mBorderPen = pen;
};
void QCPToolTip::setBrush(const QBrush &brush)
{
mBrush = brush;
};
void QCPToolTip::setRadius(double xRadius, double yRadius, Qt::SizeMode mode /*= Qt::AbsoluteSize*/)
{
mRadius = QPointF(xRadius,yRadius);
mSizeMode = mode;
};
void QCPToolTip::setOffset(double xOffset, double yOffset)
{
mOffset = QPointF(xOffset,yOffset);
};
void QCPToolTip::setPadding(const QMargins &paddings)
{
mPadding = paddings;
};
void QCPToolTip::handleTriggerEvent(QMouseEvent *event)
{
updatePosition(event->pos(), true); // true 表示需要单独刷新,将调用update函数
}
void QCPToolTip::update()
{
mPlotReplot = false; // 表明单独刷新
layer()->replot();
mPlotReplot = true; // 单独刷新完毕
}
// 不需要鼠标点击测试,因为ToolTip是跟随鼠标的,鼠标点击不到
double QCPToolTip::selectTest(const QPointF &pos, bool onlySelectable, QVariant *details) const
{
Q_UNUSED(pos)
Q_UNUSED(onlySelectable)
Q_UNUSED(details)
return -1;
}
int QCPToolTip::pickClosest(double target, const QVector<double> &vector)
{
if (vector.size() < 2)
return 0;
// 查找第一个大于或等于target的位置
auto it = std::lower_bound(vector.constBegin(), vector.constEnd(), target);
if (it == vector.constEnd()) return vector.size() - 1;
else if (it == vector.constBegin()) return 0;
else return target - *(it - 1) < *it - target ? (it - vector.constBegin() - 1): (it - vector.constBegin());
}
void QCPToolTip::updatePosition(const QPointF &newPos, bool replot)
{
mHighlightGraph = nullptr;
double tolerance = mParentPlot->selectionTolerance();
for (int i = mParentPlot->graphCount() - 1; i >= 0; --i)
{
QCPGraph *graph = mParentPlot->graph(i);
// graph不可见或者scatter style 为空的时候,不显示ToolTip
if (!graph->realVisibility() || graph->scatterStyle().isNone())
continue;
double limitDistance = tolerance; // limitDistance 用于选择的范围
double penWidth = graph->pen().widthF();
QCPScatterStyle scatterStyle = graph->scatterStyle();
limitDistance = qMax(scatterStyle.size(), tolerance);
penWidth = scatterStyle.isPenDefined() ? scatterStyle.pen().widthF() : penWidth;
// details会返回最接近的一个数据点,selectTest是不精确的,所以后面还要判断
QVariant details;
double currentDistance = graph->selectTest(newPos, false, &details);
QCPDataSelection selection = details.value<QCPDataSelection>();
if (currentDistance >= 0 && currentDistance < limitDistance + penWidth && !selection.isEmpty())
{
// 取出当前key和value值,并且转换为像素位置
double key = graph->dataMainKey(selection.dataRange().begin());
double value = graph->dataMainValue(selection.dataRange().begin());
QPointF pos = graph->coordsToPixels(key, value);
QRectF rect(pos.x() - limitDistance * 0.5, pos.y() - limitDistance * 0.5, limitDistance, limitDistance);
rect = rect.adjusted(-penWidth, -penWidth, penWidth, penWidth);
// 判断鼠标位置是否在数据点上
if (rect.contains(newPos))
{
/*
* 解开以下注释,可以使得我们的文字跟轴标签的文字是一样的(但跟轴标签实际的显示效果可能是不一样的,
* 这里要注意,例如对于科学计数法,轴可能会使用美化),同时要注意当轴标签不显示的时候tickVectorLabels
* 返回的是空的,所以我们要做一下判断
*/
// 注意这里的方式是不精确的,适用于文字轴这种类型的
/*int keyIndex = pickClosest(key, graph->keyAxis()->tickVector());
setText(QString("%1:%2").arg(graph->keyAxis()->tickVectorLabels().at(keyIndex),
QString::number(value)));*/
setText(QString("%1:%2").arg(QString::number(key), QString::number(value)));
mHighlightGraph = graph;
mGraphDataPos = pos;
mParentPlot->setCursor(Qt::PointingHandCursor);
position->setPixelPosition(newPos); // 更新位置
setVisible(true);
if (replot) {
update();
}
break;
}
}
}
if (!mHighlightGraph && visible())
{
mParentPlot->setCursor(Qt::ArrowCursor);
setVisible(false);
if (replot) {
update();
}
}
}
void QCPToolTip::draw(QCPPainter *painter)
{
if (mPlotReplot) { // 当前是由QCustomPlot的replot函数刷新的,所以要更新位置
updatePosition(position->pixelPosition(), false); // false表明不刷新
if (!visible()) {
// 由于位置更新之后,ToolTip可能会隐藏掉了,所以此处直接返回
return;
}
}
drawGraphScatterPlot(painter, mHighlightGraph, mGraphDataPos);
QPointF pos = position->pixelPosition() + mOffset;
painter->translate(pos); // 移动painter的绘制原点位置
QFontMetrics fontMetrics(mFont);
QRect textRect = fontMetrics.boundingRect(0, 0, 0, 0, Qt::TextDontClip | mTextAlignment, mText);
textRect.moveTopLeft(QPoint(mPadding.left(), mPadding.top()));
QRect textBoxRect = textRect.adjusted(-mPadding.left(), -mPadding.top(), mPadding.right(), mPadding.bottom());
textBoxRect.moveTopLeft(QPoint());
// 限制ToolTip不超过QCustomPlot的范围
if (pos.x() + textBoxRect.width() >= mParentPlot->viewport().right())
painter->translate(-mOffset.x() * 2 - textBoxRect.width(), 0);
if (pos.y() + textBoxRect.height() * 2 >= mParentPlot->viewport().bottom())
painter->translate(0, -mOffset.y() * 2 - textBoxRect.height());
// 绘制背景和边框
if ((mBrush != Qt::NoBrush && mBrush.color().alpha() != 0) ||
(mBorderPen != Qt::NoPen && mBorderPen.color().alpha() != 0))
{
double clipPad = mBorderPen.widthF();
QRect boundingRect = textBoxRect.adjusted(-clipPad, -clipPad, clipPad, clipPad);
painter->setPen(mBorderPen);
painter->setBrush(mBrush);
painter->drawRoundedRect(boundingRect, mRadius.x(), mRadius.y(), mSizeMode);
}
// 绘制文字
painter->setFont(mFont);
painter->setPen(mTextColor);
painter->setBrush(Qt::NoBrush);
painter->drawText(textRect, Qt::TextDontClip | mTextAlignment, mText);
}
void QCPToolTip::drawGraphScatterPlot(QCPPainter *painter, QCPGraph *graph, const QPointF &pos)
{
if (!graph) return;
QCPScatterStyle style = graph->scatterStyle();
if (style.isNone()) return;
if (graph->selectionDecorator()) // 如果有select decorator,则使用修饰器的风格
style = graph->selectionDecorator()->getFinalScatterStyle(style);
style.applyTo(painter, graph->pen());
style.setSize(style.size() * 1.2); // 放大一点
style.drawShape(painter, pos);
}
使用方法
cpp
if(m_tip == nullptr){
m_tip = new QCPToolTip(m_plotLine);//m_plotLine is QCustomPlot
}