QCustomPlot之平滑曲线下(九)

接上篇QCustomPlot之平滑曲线上(八),上篇只是实现了平滑曲线的绘制,但是并没有实现平滑曲线与0点线之间的填充区域以及两个QCPGraph之间的填充区域,我们将在这里实现它们

drawFill函数的修改

void QCPGraph::drawFill(QCPPainter *painter, QVector<QPointF> *lines) const
{
    if (mLineStyle == lsImpulse) return; // fill doesn't make sense for impulse plot
    if (painter->brush().style() == Qt::NoBrush || painter->brush().color().alpha() == 0) return;
    
    applyFillAntialiasingHint(painter);
    QVector<QCPDataRange> segments = getNonNanSegments(lines, keyAxis()->orientation());
    if (!mChannelFillGraph)  // 与0点线围成的区域
    {
        // draw base fill under graph, fill goes all the way to the zero-value-line:
        for (int i=0; i<segments.size(); ++i)
            if (mSmooth && mLineStyle == lsLine) painter->drawPath(getSmoothFillPath(lines, segments.at(i)));   // 平滑曲线
            else painter->drawPolygon(getFillPolygon(lines, segments.at(i)));   // 折线
    } else   // 与其它QCPGraph围成的区域
    {
        // draw fill between this graph and mChannelFillGraph:
        QVector<QPointF> otherLines;
        mChannelFillGraph->getLines(&otherLines, QCPDataRange(0, mChannelFillGraph->dataCount()));
        if (!otherLines.isEmpty())
        {
            QVector<QCPDataRange> otherSegments = getNonNanSegments(&otherLines, mChannelFillGraph->keyAxis()->orientation());
            QVector<QPair<QCPDataRange, QCPDataRange> > segmentPairs = getOverlappingSegments(segments, lines, otherSegments, &otherLines);
            for (int i=0; i<segmentPairs.size(); ++i) {
                if (mSmooth && mLineStyle == lsLine && mChannelFillGraph->mLineStyle == lsLine) // 稍后会解释为什么要限制线风格为lsLine
                    painter->drawPath(getSmoothChannelFillPath(lines, segmentPairs.at(i).first, &otherLines, segmentPairs.at(i).second));  // 平滑曲线
                else 
                    painter->drawPolygon(getChannelFillPolygon(lines, segmentPairs.at(i).first, &otherLines, segmentPairs.at(i).second));   // 折线
            }
        }
    }
}

与0点线之间的填充区域

const QPainterPath QCPGraph::getSmoothFillPath(const QVector<QPointF> *lineData, QCPDataRange segment) const
{
    // 只有一个点构不成填充区域
    if (segment.size() < 2)
        return QPainterPath();

    // 起点,终点对应在轴上的位置
    QPointF start = getFillBasePoint(lineData->at(segment.begin()));
    QPointF end = getFillBasePoint(lineData->at(segment.end() - 1));

    // 将平滑曲线连成一个封闭区域
    QPainterPath path = SmoothCurveGenerator::generateSmoothCurve(*lineData);
    path.lineTo(end);
    path.lineTo(start);
    path.lineTo(lineData->at(segment.begin()));
    return path;
}
与0点线之间的填充区域

与其它QCPGraph围成的区域

getSmoothChannelFillPath基本是从getChannelFillPolygon复制过来的,我们在这上面进行修改,修改的内容我都有注释

const QPainterPath QCPGraph::getSmoothChannelFillPath(const QVector<QPointF> *thisData, QCPDataRange thisSegment,
                                                      const QVector<QPointF> *otherData, QCPDataRange otherSegment) const
{
    QPainterPath result;
    if (!mChannelFillGraph)
        return result;

    QCPAxis *keyAxis = mKeyAxis.data();
    QCPAxis *valueAxis = mValueAxis.data();
    if (!keyAxis || !valueAxis) { qDebug() << Q_FUNC_INFO << "invalid key or value axis"; return result; }
    if (!mChannelFillGraph.data()->mKeyAxis) { qDebug() << Q_FUNC_INFO << "channel fill target key axis invalid"; return result; }

    if (mChannelFillGraph.data()->mKeyAxis.data()->orientation() != keyAxis->orientation())
        return result; // don't have same axis orientation, can't fill that (Note: if keyAxis fits, valueAxis will fit too, because it's always orthogonal to keyAxis)

    if (thisData->isEmpty()) return result;
    QVector<QPointF> thisSegmentData(thisSegment.size());
    QVector<QPointF> otherSegmentData(otherSegment.size());
    std::copy(thisData->constBegin()+thisSegment.begin(), thisData->constBegin()+thisSegment.end(), thisSegmentData.begin());
    std::copy(otherData->constBegin()+otherSegment.begin(), otherData->constBegin()+otherSegment.end(), otherSegmentData.begin());
    // pointers to be able to swap them, depending which data range needs cropping:
    QVector<QPointF> *staticData = &thisSegmentData;
    QVector<QPointF> *croppedData = &otherSegmentData;

    //! [1] 以下为添加的内容
    result = SmoothCurveGenerator::generateSmoothCurve(thisSegmentData);
    if (mChannelFillGraph->mSmooth && mChannelFillGraph->mLineStyle == lsLine) {  // mChannelFillGraph也是平滑曲线
        QVector<QPointF> otherSegmentDataReverse(otherSegmentData.size());
        for (int i = otherSegmentData.size() - 1; i >= 0; --i)
            otherSegmentDataReverse[otherSegmentData.size() - i - 1] = otherSegmentData.at(i);
        result = SmoothCurveGenerator::generateSmoothCurve(result, otherSegmentDataReverse);
    } else {  // mChannelFillGraph 是折线
        // mLineStyle != lsLine 会导致闪烁,目前还不知道什么原因造成
        for (int i = otherSegmentData.size() - 1; i >= 0; --i)
            result.lineTo(otherSegmentData.at(i));
    }
    //! [1]

    // crop both vectors to ranges in which the keys overlap (which coord is key, depends on axisType):
    if (keyAxis->orientation() == Qt::Horizontal)
    {
        // x is key
        // crop lower bound:
        if (staticData->first().x() < croppedData->first().x()) // other one must be cropped
            qSwap(staticData, croppedData);
        const int lowBound = findIndexBelowX(croppedData, staticData->first().x());
        if (lowBound == -1) return result; // key ranges have no overlap
        //! [2] 以下为添加的内容
        QPointF firstPoint = QPointF(croppedData->at(0).x(), valueAxis->coordToPixel(valueAxis->range().upper));  // 注意这里只裁剪到了轴矩形的可见区域
        //! [2]
        croppedData->remove(0, lowBound);
        // set lowest point of cropped data to fit exactly key position of first static data point via linear interpolation:
        if (croppedData->size() < 2) return result; // need at least two points for interpolation
        double slope;
        if (!qFuzzyCompare(croppedData->at(1).x(), croppedData->at(0).x()))
            slope = (croppedData->at(1).y()-croppedData->at(0).y())/(croppedData->at(1).x()-croppedData->at(0).x());
        else
            slope = 0;
        (*croppedData)[0].setY(croppedData->at(0).y()+slope*(staticData->first().x()-croppedData->at(0).x()));
        (*croppedData)[0].setX(staticData->first().x());

        //! [3] 以下为添加的内容
        QPointF lastPoint = QPointF(staticData->first().x(), valueAxis->coordToPixel(valueAxis->range().lower));  // 注意这里只裁剪到了轴矩形的可见区域
        QPainterPath droppedPath;
        droppedPath.addRect(QRectF(firstPoint, lastPoint).normalized());
        result -= droppedPath;  // 裁掉多余区域
        //! [3]

        // crop upper bound:
        if (staticData->last().x() > croppedData->last().x()) // other one must be cropped
            qSwap(staticData, croppedData);
        int highBound = findIndexAboveX(croppedData, staticData->last().x());
        if (highBound == -1) return result; // key ranges have no overlap
        //! [4] 以下为添加的内容
        firstPoint = QPointF(croppedData->last().x(), valueAxis->coordToPixel(valueAxis->range().lower));  // 注意这里只裁剪到了轴矩形的可见区域
        //! [4]
        croppedData->remove(highBound+1, croppedData->size()-(highBound+1));
        // set highest point of cropped data to fit exactly key position of last static data point via linear interpolation:
        if (croppedData->size() < 2) return result; // need at least two points for interpolation
        const int li = croppedData->size()-1; // last index
        if (!qFuzzyCompare(croppedData->at(li).x(), croppedData->at(li-1).x()))
            slope = (croppedData->at(li).y()-croppedData->at(li-1).y())/(croppedData->at(li).x()-croppedData->at(li-1).x());
        else
            slope = 0;
        (*croppedData)[li].setY(croppedData->at(li-1).y()+slope*(staticData->last().x()-croppedData->at(li-1).x()));
        (*croppedData)[li].setX(staticData->last().x());

        //! [5] 以下为添加的内容
        lastPoint = QPointF(staticData->last().x(), valueAxis->coordToPixel(valueAxis->range().upper));
        droppedPath = QPainterPath();
        droppedPath.addRect(QRectF(firstPoint, lastPoint).normalized());
        result -= droppedPath;  // 裁掉多余区域
        //! [5]
    } else // mKeyAxis->orientation() == Qt::Vertical
    {
        // y is key
        // crop lower bound:
        if (staticData->first().y() < croppedData->first().y()) // other one must be cropped
            qSwap(staticData, croppedData);
        int lowBound = findIndexBelowY(croppedData, staticData->first().y());
        if (lowBound == -1) return result; // key ranges have no overlap
        //! [6] 以下为添加的内容
        QPointF firstPoint = QPointF(valueAxis->coordToPixel(valueAxis->range().upper), croppedData->first().y());
        //! [6]
        croppedData->remove(0, lowBound);
        // set lowest point of cropped data to fit exactly key position of first static data point via linear interpolation:
        if (croppedData->size() < 2) return result; // need at least two points for interpolation
        double slope;
        if (!qFuzzyCompare(croppedData->at(1).y(), croppedData->at(0).y())) // avoid division by zero in step plots
            slope = (croppedData->at(1).x()-croppedData->at(0).x())/(croppedData->at(1).y()-croppedData->at(0).y());
        else
            slope = 0;
        (*croppedData)[0].setX(croppedData->at(0).x()+slope*(staticData->first().y()-croppedData->at(0).y()));
        (*croppedData)[0].setY(staticData->first().y());

        //! [7] 以下为添加的内容
        QPointF lastPoint = QPointF(valueAxis->coordToPixel(valueAxis->range().lower), staticData->first().y());
        QPainterPath droppedPath;
        droppedPath.addRect(QRectF(firstPoint, lastPoint).normalized());
        result -= droppedPath;
        //! [7]

        // crop upper bound:
        if (staticData->last().y() > croppedData->last().y()) // other one must be cropped
            qSwap(staticData, croppedData);
        int highBound = findIndexAboveY(croppedData, staticData->last().y());
        if (highBound == -1) return result; // key ranges have no overlap
        //! [8] 以下为添加的内容
        firstPoint = QPointF(valueAxis->coordToPixel(valueAxis->range().lower), croppedData->last().y());
        //! [8]
        croppedData->remove(highBound+1, croppedData->size()-(highBound+1));
        // set highest point of cropped data to fit exactly key position of last static data point via linear interpolation:
        if (croppedData->size() < 2) return result; // need at least two points for interpolation
        int li = croppedData->size()-1; // last index
        if (!qFuzzyCompare(croppedData->at(li).y(), croppedData->at(li-1).y())) // avoid division by zero in step plots
            slope = (croppedData->at(li).x()-croppedData->at(li-1).x())/(croppedData->at(li).y()-croppedData->at(li-1).y());
        else
            slope = 0;
        (*croppedData)[li].setX(croppedData->at(li-1).x()+slope*(staticData->last().y()-croppedData->at(li-1).y()));
        (*croppedData)[li].setY(staticData->last().y());

        //! [9] 以下为添加的内容
        lastPoint = QPointF(valueAxis->coordToPixel(valueAxis->range().upper), staticData->last().y());
        droppedPath = QPainterPath();
        droppedPath.addRect(QRectF(firstPoint, lastPoint).normalized());
        result -= droppedPath;
        //! [9]
    }
    return result;
}
与其它QCPGraph围成的区域

已知问题

  • 限制线风格为lsLine的原因是因为其它风格可能会导致围成的区域在拖动/缩放的时候可能导致闪烁,而且其它风格对于平滑曲线来说并没有意义,所以干脆将风格限制为了lsLine
  • 由于QPainterPath的原因,它会在某些情况下导致贝塞尔曲线变直,而我们的平滑曲线是通过贝塞尔曲线绘制的,所以在某些情况下(比如放大的时候)会发现曲线变直了,Qt的原文如下:Bezier curves may be flattened to line segments due to numerical instability of doing bezier curve intersections.
  • 经测试,当显示区域的点个数为两个的时候,曲线会变直,不知道是因为曲线算法的原因还是Qt的原因

虽然有以上的问题,但已经可以满足我们的使用了

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

推荐阅读更多精彩内容