QCustomPlot之鼠标悬浮显示值(十一)

接上篇QCustomPlot之Item(十),我们将在此篇讲解如何自定义我们的Item,其作用是一个ToolTip

效果图

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);
};

源文件

我们将ToolTip移入overlay层,原因是因为它可能需要频繁的刷新,这样我们可以不重新绘制图表,优化速度。同时将position的位置设置为像素的方式

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 *)));
}

我们通过handleTriggerEvent来接收来自QCustomPlot的鼠标移动信号,作用是使得ToolTip跟随鼠标移动

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;
}

位置更新

ToolTip的位置主要是遍历当前QCustomPlot的所有QCPGraph,找到鼠标下的对应的数据点

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);
        if (!graph->realVisibility() || graph->scatterStyle().isNone())   // graph不可见或者scatter style 为空的时候,不显示ToolTip
            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;

        QVariant details;
        double currentDistance = graph->selectTest(newPos, false, &details);   // details会返回最接近的一个数据点,selectTest是不精确的,所以后面还要判断

        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();
    }
}

绘制

绘制原理是在数据点上方重新绘制scatter style,并且稍微放大一点scatter style的大小,造成一种假象,此种方法适用于scatter style有一个背景画刷

void QCPToolTip::draw(QCPPainter *painter)
{
    if (mPlotReplot) {  // 当前是由QCustomPlot的replot函数刷新的,所以要更新位置
        updatePosition(position->pixelPosition(), false);  // 传入false表明不刷新
        if (!visible()) return;   // 由于位置更新之后,ToolTip可能会隐藏掉了,所以此处直接返回
    }

    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);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352

推荐阅读更多精彩内容