Qt绘图:坐标轴

0.前言

绘制图表很重要的一步就是确立坐标轴,有了标尺,才能找准自己的定位。每一个数据点都需要根据坐标轴来计算数据值对应的屏幕像素位置。

本文代码源码链接及实现效果如下:

github 链接(XYView类):https://github.com/gongjianbo/EasyQPainter

1.实现细节

对于固定的坐标范围,刻度可以直接根据最大最小值进行均分。但是加入缩放、移动等交互后,就需要保存一些额外的信息,如最小缩放范围、最大最小限定范围等。

确定好范围后,就是刻度的位置计算。一些常见的图表库都会根据当前范围动态的计算刻度间隔和刻度值,且尽量取整。如 [0,10] 分三份,肯定不会是 3.33 和 6.66 ,而是以 3 或 4 为间隔进行取值,变成 (0,3,6,9) 四个刻度值。我这里采用递归的方式,每次乘以或除以 10,然后同这一数级的特殊值进行比较来获取最终的间隔。

double XYAxis::calcValueSpaceHelper(double valueRefRange, int dividend) const
{
    //分段找合适的间隔,分割倍数dividend每次递归乘以10
    if (valueRefRange > 8 * dividend){
        //if(dividend>10000*100)return dividend;
        return calcValueSpaceHelper(valueRefRange, dividend * 10);
    }else if (valueRefRange > 4.5 * dividend){
        return 5 * dividend;
    }else if (valueRefRange > 3 * dividend){
        return 4 * dividend;
    }else if (valueRefRange > 1.5 * dividend){
        return 2 * dividend;
    }else{
        return dividend;
    }
}

有了刻度的间隔,还得计算从哪里开始才是间隔的整倍数,这个点就是刻度绘制的第一个起始点,后面的点直接以间隔步进即可。我主要是拿间隔值来取模运算。

通过刻度值和数据值之间的转换,才能使数据正确的渲染在某个位置。两者换算也很简单,通过等比法就能列出算式。

//像素位置转数据值,unit1PxToValue为每像素代表的数据值大小
double XYAxis::pxToValue(double px) const
{
    return px * unit1PxToValue + minValue;
}

//数据值转像素位置,unit1ValueToPx为每单位数据值表示的像素大小
double XYAxis::valueToPx(double value) const
{
    return (value - minValue) * unit1ValueToPx;
}

对于缩放,如果是鼠标在图表区域内滚轮缩放,需要根据当前鼠标位置分别计算上下或左右两侧的增减值,保持缩放前后该点的刻度值不变(根据当前位置进行聚焦)。

//先计算鼠标在图中左右百分比zoom_proportion
//将单次步进乘以百分比就能分别得到左侧和右侧应该乘以的值了
minValue += zoom_step * zoom_proportion;
maxValue -= zoom_step * (1 - zoom_proportion);

此外,x 轴和 y 轴的交点最好是重合 1 像素,这样两个轴的交叠处正好呈十字,效果好一点。

2.主要实现代码

#pragma once
#include <QObject>
#include <QPainter>

//笛卡尔坐标系(直角坐标系)的坐标轴
class XYAxis : public QObject
{
    Q_OBJECT
public:
    //刻度线所在方位,上下左右
    enum AxisPosition
    {
        AtLeft,
        AtRight,
        AtTop,
        AtBottom
    };
    //刻度线的间隔计算方式
    enum TickMode
    {
        //固定值间隔
        FixedValue,
        //根据参考像素间隔
        RefPixel
    };

public:
    explicit XYAxis(QObject *parent = nullptr);
    //初始化,构造后在渲染前调用
    void init(AxisPosition position, double minLimit, double maxLimit,
              double minRange, double minValue, double maxValue);

    //刻度线所在方位,上下左右
    AxisPosition getAxisPosition() const;
    void setAxisPosition(AxisPosition position);

    //刻度线的间隔计算方式
    TickMode getTickMode() const;
    void setTickMode(TickMode mode);

    //坐标区域
    QRect getRect() const;
    void setRect(const QRect &rect);

    //小数精度
    int getDecimalPrecision() const;
    void setDecimalPrecision(int precison);

    //固定值的间隔
    double getFixedValueSpace() const;
    void setFixedValueSpace(double value);

    //参考像素范围的间隔
    int getRefPixelSpace() const;
    void setRefPixelSpace(int pixel);

    //刻度位置
    QVector<int> getTickPos() const;
    //刻度值文本
    QVector<QString> getTickLabel() const;

    //最小值限制
    double getMinLimit() const;
    //最大值限制
    double getMaxLimit() const;
    //最小范围限制
    double getMinRange() const;
    //当前显示的最小刻度
    double getMinValue() const;
    //当前显示的最大刻度
    double getMaxValue() const;

    //像素与值的换算
    double getUnit1PxToValue() const;
    double getUnit1ValueToPx() const;

    /**
    * @brief 坐标轴像素值转数值
    * @details 暂时只有2方向,
    * Qt绘制起点为左上角,往右下角取正.
    * @param px 鼠标pos
    * 该函数只负责计算对应的刻度数值,横向时可能参数要减去left,
    * 竖向时可能参数先被bottom+1减一下
    * @return 对应的刻度数值
    */
    double pxToValue(double px) const;

    /**
    * @brief 数值转坐标轴像素值
    * @details 暂时只有2方向,
    * Qt绘制起点为左上角,往右下角取正.
    * @param value 对应的刻度数值
    * @return 鼠标所在point对应的px长度,
    * 该函数只负责计算距离,横向时可能要拿得到的px加上left,
    * 竖向时可能需要拿bottom+1来减去得到的px.
    */
    double valueToPx(double value) const;

    //绘制
    void draw(QPainter *painter);

private:
    //坐标轴在上下左右不同位置时,绘制不同的效果,本demo只写部分
    void drawLeft(QPainter *painter);
    void drawBottom(QPainter *painter);
    //大小or范围等变动后重新计算刻度信息
    void calcAxis();
    //计算间隔和起点
    void calcSpace(double axisLength);
    //计算刻度像素间隔
    double calcPxSpace(double unitP2V, double valueSpace) const;
    //计算刻度像素起点
    double calcPxStart(double unitP2V, double valueSpace, double valueMin, double valueMax) const;
    //计算值间隔
    double calcValueSpace(double unitP2V, int pxRefSpace) const;
    //辅助计算值间隔
    double calcValueSpaceHelper(double valueRefRange, int dividend) const;
    //刻度值的小数位数
    int getTickPrecision() const;
    int getTickPrecisionHelper(double valueSpace, double compare, int precision) const;
    //步进
    double valueCalcStep() const;
    double valueZoomInStep() const;
    double valueZoomOutStep() const;
    //根据pos计算zoom的左右/上下百分比
    double calcZoomProportionWithPos(const QPoint &pos) const;

signals:
    void axisChanged();

public slots:
    //移动
    void addMinValue();
    void subMinValue();
    void addMaxValue();
    void subMaxValue();
    bool moveValueWidthPx(int px);
    //放大缩小
    void zoomValueIn();
    void zoomValueOut();
    void zoomValueInPos(const QPoint &pos);
    void zoomValueOutPos(const QPoint &pos);
    //全览,value设置为limit
    void overallView();
    //设置刻度limit范围
    void setLimitRange(double min, double max, double range);
    //设置刻度当前value显示范围
    void setValueRange(double min, double max);

private:
    //刻度线所在方位,上下左右
    AxisPosition thePosition{AtLeft};
    //刻度线的间隔计算方式
    TickMode theMode{RefPixel};
    //坐标区域
    QRect theRect;
    //显示的小数位数
    int decimalPrecision{3};
    //刻度根据固定值间隔时的参考,一般用于等分
    double fixedValueSpace{100.0};
    //刻度根据像素间隔的参考,一般用于自适应
    //通过参考像素间隔计算得到值间隔,再取整后转换为像素间隔
    double refPixelSpace{35.0};
    //刻度位置
    QVector<int> tickPos;
    //刻度值文本
    QVector<QString> tickLabel;

    //刻度值限定范围
    double minLimit{0.0};
    double maxLimit{1000.0};
    //最小缩放范围
    double minRange{10.0};
    //当前显示范围
    double minValue{0.0};
    double maxValue{1000.0};

    //1像素表示的值
    double unit1PxToValue{1.0};
    //1单位值表示的像素
    double unit1ValueToPx{1.0};
    //刻度绘制像素起点
    //横向以左侧开始,竖向以底部开始
    double pxStart{0.0};
    //刻度像素间隔
    double pxSpace{30.0};
    //刻度值间隔
    double valueSpace{1.0};
};
#include "XYAxis.h"
#include <cmath>
#include <QtMath>
#include <QDebug>

XYAxis::XYAxis(QObject *parent)
    : QObject(parent)
{
}

void XYAxis::init(AxisPosition position, double minLimit, double maxLimit,
                  double minRange, double minValue, double maxValue)
{
    this->thePosition = position;
    this->minLimit = minLimit;
    this->maxLimit = maxLimit;
    this->minRange = minRange;
    this->minValue = minValue;
    this->maxValue = maxValue;
}

XYAxis::AxisPosition XYAxis::getAxisPosition() const
{
    return thePosition;
}

void XYAxis::setAxisPosition(AxisPosition position)
{
    if (thePosition != position)
    {
        thePosition = position;
        emit axisChanged();
    }
}

XYAxis::TickMode XYAxis::getTickMode() const
{
    return theMode;
}

void XYAxis::setTickMode(TickMode mode)
{
    if (theMode != mode)
    {
        theMode = mode;
        calcAxis();
    }
}

QRect XYAxis::getRect() const
{
    return theRect;
}

void XYAxis::setRect(const QRect &rect)
{
    if (theRect != rect && rect.isValid())
    {
        theRect = rect;
        calcAxis();
    }
}

int XYAxis::getDecimalPrecision() const
{
    return decimalPrecision;
}

void XYAxis::setDecimalPrecision(int precison)
{
    if (decimalPrecision != precison)
    {
        decimalPrecision = precison;
        emit axisChanged();
    }
}

double XYAxis::getFixedValueSpace() const
{
    return fixedValueSpace;
}

void XYAxis::setFixedValueSpace(double value)
{
    fixedValueSpace = value;
    calcAxis();
}

int XYAxis::getRefPixelSpace() const
{
    return refPixelSpace;
}

void XYAxis::setRefPixelSpace(int pixel)
{
    refPixelSpace = pixel;
    calcAxis();
}

QVector<int> XYAxis::getTickPos() const
{
    return tickPos;
}

QVector<QString> XYAxis::getTickLabel() const
{
    return tickLabel;
}

double XYAxis::getMinLimit() const
{
    return minLimit;
}

double XYAxis::getMaxLimit() const
{
    return maxLimit;
}

double XYAxis::getMinRange() const
{
    return minRange;
}

double XYAxis::getMinValue() const
{
    return minValue;
}

double XYAxis::getMaxValue() const
{
    return maxValue;
}

double XYAxis::getUnit1PxToValue() const
{
    return unit1PxToValue;
}

double XYAxis::getUnit1ValueToPx() const
{
    return unit1ValueToPx;
}

double XYAxis::pxToValue(double px) const
{
    return px * unit1PxToValue + minValue;
}

double XYAxis::valueToPx(double value) const
{
    return (value - minValue) * unit1ValueToPx;
}

void XYAxis::draw(QPainter *painter)
{
    painter->fillRect(theRect, QColor(0, 180, 200));
    switch (this->getAxisPosition())
    {
    case AtRight:
        //drawRight(painter);
        break;
    case AtLeft:
        drawLeft(painter);
        break;
    case AtTop:
        //drawTop(painter);
        break;
    case AtBottom:
        drawBottom(painter);
        break;
    default:
        break;
    }
}

void XYAxis::drawLeft(QPainter *painter)
{
    painter->save();
    painter->drawLine(theRect.topRight(), theRect.bottomRight());

    const int right_pos = theRect.right();
    for (int i = 0; i < tickPos.count(); i++)
    {
        const int y_pos = tickPos.at(i);
        painter->drawLine(QPoint(right_pos, y_pos),
                          QPoint(right_pos - 5, y_pos));
        painter->drawText(right_pos - 5 - painter->fontMetrics().width(tickLabel.at(i)),
                          y_pos + painter->fontMetrics().height() / 2,
                          tickLabel.at(i));
    }

    painter->restore();
}

void XYAxis::drawBottom(QPainter *painter)
{
    painter->save();
    painter->drawLine(theRect.topLeft(), theRect.topRight());

    const int top_pos = theRect.top();
    for (int i = 0; i < tickPos.count(); i++)
    {
        const int x_pos = tickPos.at(i);
        painter->drawLine(QPoint(x_pos, top_pos),
                          QPoint(x_pos, top_pos + 5));
        painter->drawText(x_pos - painter->fontMetrics().width(tickLabel.at(i)) / 2,
                          top_pos + 5 + painter->fontMetrics().height(),
                          tickLabel.at(i));
    }
    painter->restore();
}

void XYAxis::calcAxis()
{
    if (minLimit >= maxLimit || theRect.isNull())
        return;
    if (minValue > maxValue)
    {
        std::swap(minValue, maxValue);
    }
    if (minLimit > minValue)
    {
        minValue = minLimit;
    }
    if (maxLimit < maxValue)
    {
        maxValue = maxLimit;
    }
    switch (this->getAxisPosition())
    {
    case AtBottom:
    {
        //横向x轴
        calcSpace(theRect.width() - 1);
        //计算刻度线
        const double right_pos = theRect.right();
        tickPos.clear();
        tickLabel.clear();
        const int precision = getTickPrecision();
        //i是用刻度px算坐标位置;j是用刻度px算i对应的value
        //条件i>pos-N是为了显示最大值那个刻度
        for (double i = theRect.left() + pxStart, j = pxStart; i < right_pos + 2; i += pxSpace, j += pxSpace)
        {
            tickPos.push_back(std::round(i));
            const double label_value = (minValue + (j)*unit1PxToValue);
            QString label_text = QString::number(label_value, 'f', precision);
            if (label_text == "-0")
            { //会有-0
                label_text = "0";
            }
            tickLabel.push_back(label_text);
        }
    }
    break;
    case AtLeft:
    {
        //竖向y轴
        calcSpace(theRect.height() - 1);
        //计算刻度线
        const double top_pos = theRect.top();
        tickPos.clear();
        tickLabel.clear();
        const int precision = getTickPrecision();
        //i是用刻度px算坐标位置;j是用刻度px算i对应的value
        //条件i>pos-N是为了显示最大值那个刻度
        for (double i = theRect.bottom() - pxStart, j = pxStart; i > top_pos - 2; i -= pxSpace, j += pxSpace)
        {
            tickPos.push_back(std::round(i));
            const double label_value = (minValue + (j)*unit1PxToValue);
            QString label_text = QString::number(label_value, 'f', precision);
            if (label_text == "-0")
            { //会有-0
                label_text = "0";
            }
            tickLabel.push_back(label_text);
        }
    }
    break;
    default:
        break;
    }
    emit axisChanged();
}

void XYAxis::calcSpace(double axisLength)
{
    //计算每单位值
    //为什么算了两个互为倒数的数呢?因为浮点数精度问题
    unit1PxToValue = (maxValue - minValue) / (axisLength);
    unit1ValueToPx = (axisLength) / (maxValue - minValue);
    //计算间隔和起点
    //计算刻度间隔及刻度起点
    switch (theMode)
    {
    case FixedValue:
        //该模式ValueSpace固定不变;
        valueSpace = fixedValueSpace;
        pxSpace = calcPxSpace(unit1PxToValue, valueSpace);
        pxStart = calcPxStart(unit1PxToValue, valueSpace, minValue, maxValue);
        break;
    case RefPixel:
        valueSpace = calcValueSpace(unit1PxToValue, refPixelSpace);
        pxSpace = calcPxSpace(unit1PxToValue, valueSpace);
        pxStart = calcPxStart(unit1PxToValue, valueSpace, minValue, maxValue);
        break;
    default:
        break;
    }
}

double XYAxis::calcPxSpace(double unitP2V, double valueSpace) const
{
    //这里与真0.0比较
    if (unitP2V <= 0.0)
    {
        qWarning() << __FUNCTION__ << "unitP2V is too min" << unitP2V;
        return 30.0;
    }
    return valueSpace / unitP2V;
}

double XYAxis::calcPxStart(double unitP2V, double valueSpace, double valueMin, double valueMax) const
{
    Q_UNUSED(valueMax)
    if (unitP2V <= 0.0 || valueSpace <= 0.0)
    {
        qWarning() << __FUNCTION__ << "unitP2V or valueSpace is too min" << unitP2V << valueSpace;
        return 0.0;
    }
    //min有正负,而unit和space只有正
    //如果最小值为正数or零
    //从最小值往上找第一个能被value_space整除的数
    //如果最小值为负数
    //从0往下找最后一个能被value_space整除的数
    //(如果min绝对值小于value_space则起点为0)
    //即起点值应该是value_space的整倍数
    const double begin_precision = std::pow(10, decimalPrecision);
    const double begin_cut = (decimalPrecision <= 0)
                                 ? 0
                                 : qRound(std::abs(valueMin) * begin_precision) % qRound(valueSpace * begin_precision) / begin_precision;
    //因为cut是value_space模出来的,且该分支min和value_space都为正,
    //所以起始值(value_space-cut)不会为负。
    //起点px就为起始值*单位值表示的像素;或者为起始值/单位像素表示的值
    //(注意:起始值是距离起点的间隔值)
    const double begin_val = qFuzzyIsNull(begin_cut) ? 0.0 : (valueMin >= 0.0) ? (valueSpace - begin_cut)
                                                                               : begin_cut;
    return begin_val / unitP2V;

    //之前以左上角为起始计算的逻辑,会导致左下角xy的零点相交误差大,现在改为左下角开始算
    //if(getAxisPosition()==AtTop||getAxisPosition()==AtBottom){
    //    //横向刻度值的是从左至右,和坐标x值增长方向一样
    //    return begin_val/unitP2V;
    //}else if(getAxisPosition()==AtLeft||getAxisPosition()==AtRight){
    //    //竖向如果从上往下开始计算,则刻度值和坐标y值增长方向相反
    //    const double end_val=(valueMax-valueMin-begin_val)-valueSpace*(int)((valueMax-valueMin-begin_val)/valueSpace);
    //    return end_val/unitP2V;
    //}
    //return 0;
}

double XYAxis::calcValueSpace(double unitP2V, int pxRefSpace) const
{
    //尽量为整除
    const double space_ref = unitP2V * pxRefSpace;
    double space_temp = space_ref;
    if (space_ref > 1)
        space_temp = calcValueSpaceHelper(space_ref, 1);
    else
        space_temp = calcValueSpaceHelper(space_ref * std::pow(10, decimalPrecision), 1) * std::pow(10, -decimalPrecision);
    //避免过大过小
    /*if(space_temp<=std::pow(10,-_decimalPrecision)){
        return std::pow(10,-_decimalPrecision);
    }else if(space_temp<space_ref*0.7){
        return space_temp*2;
    }else if(space_temp>space_ref*1.8){
        return space_temp/2;
    }*/
    return space_temp;
}

double XYAxis::calcValueSpaceHelper(double valueRefRange, int dividend) const
{
    //分段找合适的间隔,分割倍数dividend每次递归乘以10
    //考虑到当前应用场景,没有处理太大or太小的数
    //其实这个递归也不是很好,如果数值较大比较费时间,但是统计数值位数也需要去递归
    if (valueRefRange > 8 * dividend)
    {
        //if(dividend>10000*100)return dividend;
        return calcValueSpaceHelper(valueRefRange, dividend * 10);
    }
    else if (valueRefRange > 4.5 * dividend)
    {
        return 5 * dividend;
    }
    else if (valueRefRange > 3 * dividend)
    {
        return 4 * dividend;
    }
    else if (valueRefRange > 1.5 * dividend)
    {
        return 2 * dividend;
    }
    else
    {
        return dividend;
    }

    //递归思路
    /*if(temp_value>8*x){//x=1,>8--loop
        if(temp_value>8*x(10)){ //x=10,>80--loop
        }if(temp_value>4*x(10)){ //x=10,50
        }else if(temp_value>1.5*x(10)){ //x=10,20
        }else{ //x=10,10
        }
    }else if(temp_value>4*x){ //x=1,5
    }else if(temp_value>1.5*x){ //x=1,2
    }else{ //x=1,1
        //...
    }*/
}

int XYAxis::getTickPrecision() const
{
    //刻度的小数位数
    return getTickPrecisionHelper(valueSpace, 1, 0);
}

int XYAxis::getTickPrecisionHelper(double valueSpace, double compare, int precision) const
{
    //第二个参数为小数参照,每次递归除以10再和传入的参数一间隔值比较
    //如果valueSpace大于compare,那么小数精度就是当前precision
    if (valueSpace >= compare)
    {
        return precision;
    }
    return getTickPrecisionHelper(valueSpace, compare / 10, precision + 1);
}

double XYAxis::valueCalcStep() const
{
    // add sub的步进,根据需求自定义
    switch (theMode)
    {
    case RefPixel:
        return valueSpace;
        break;
    case FixedValue:
        return (maxValue - minValue) / 5;
        break;
    default:
        break;
    }
    return valueSpace;
}

double XYAxis::valueZoomInStep() const
{
    //zoomin 步进,根据需求自定义
    return (maxValue - minValue) / 4;
}

double XYAxis::valueZoomOutStep() const
{
    //zoomout 步进,根据需求自动逸
    return (maxValue - minValue) / 2;
}

double XYAxis::calcZoomProportionWithPos(const QPoint &pos) const
{
    //根据点在rect的位置计算百分比,通过百分比来计算左右缩放的值
    double zoom_proportion = 0.5;
    switch (this->getAxisPosition())
    {
    case AtTop:
    case AtBottom:
    {
        const int pos_x = pos.x();
        const int rect_left = theRect.left();
        const int rect_right = theRect.right();
        zoom_proportion = (pos_x - rect_left) / (double)(rect_right - rect_left);
    }
    break;
    case AtRight:
    case AtLeft:
    {
        const int pos_y = pos.y();
        const int rect_top = theRect.top();
        const int rect_bottom = theRect.bottom();
        zoom_proportion = (rect_bottom - pos_y) / (double)(rect_bottom - rect_top);
    }
    break;
    default:
        break;
    }
    if (zoom_proportion <= 0.0)
        return 0.0;
    if (zoom_proportion >= 1.0)
        return 1.0;
    return zoom_proportion;
}

void XYAxis::addMinValue()
{
    //不能小于最小范围
    if (maxValue - minValue <= minRange)
        return;
    minValue += valueCalcStep();
    if (maxValue - minValue < minRange)
    {
        minValue = maxValue - minRange;
    }
    calcAxis();
}

void XYAxis::subMinValue()
{
    //不能小于最小值的limit
    if (minValue <= minLimit)
        return;
    minValue -= valueCalcStep();
    if (minValue < minLimit)
    {
        minValue = minLimit;
    }
    calcAxis();
}

void XYAxis::addMaxValue()
{
    //不能大于最大值的limit
    if (maxValue > maxLimit)
        return;
    maxValue += valueCalcStep();
    if (maxValue > maxLimit)
    {
        maxValue = maxLimit;
    }
    calcAxis();
}

void XYAxis::subMaxValue()
{
    //不能小于最小范围
    if (maxValue - minValue <= minRange)
        return;
    maxValue -= valueCalcStep();
    if (maxValue - minValue < minRange)
    {
        maxValue = minValue + minRange;
    }
    calcAxis();
}

bool XYAxis::moveValueWidthPx(int px)
{
    double move_step = qAbs(px) * unit1PxToValue;
    if (move_step <= 0)
        return false;
    // <0 就是往min端移动,>0 就是往max端移动
    if (px < 0)
    {
        if (minValue <= minLimit)
            return false;
        if (minValue - move_step < minLimit)
        {
            move_step = minValue - minLimit;
        }
        minValue -= move_step;
        maxValue -= move_step;
    }
    else
    {
        if (maxValue > maxLimit)
            return false;
        if (maxValue + move_step > maxLimit)
        {
            move_step = maxLimit - maxValue;
        }
        minValue += move_step;
        maxValue += move_step;
    }
    calcAxis();
    return true;
}

void XYAxis::zoomValueIn()
{
    const double val_range = maxValue - minValue;
    if (val_range <= minRange)
        return;
    const double zoom_step = valueZoomInStep();
    if (zoom_step <= 0)
        return;
    if (val_range - zoom_step < minRange)
    {
        const double zoom_real_step = val_range - minRange;
        minValue += zoom_real_step / 2;
        maxValue = minValue + minRange;
    }
    else
    {
        minValue += zoom_step / 2;
        maxValue -= zoom_step / 2;
    }

    calcAxis();
}

void XYAxis::zoomValueOut()
{
    if (minValue <= minLimit &&
        maxValue >= maxLimit)
        return;
    const double zoom_half = valueZoomOutStep() / 2;
    const double min_zoom = (minValue - zoom_half < minLimit)
                                ? (minValue - minLimit)
                                : (zoom_half);
    const double max_zoom = (maxValue + zoom_half > maxLimit)
                                ? (maxLimit - maxValue)
                                : (zoom_half);
    //先不考虑补上不足的部分
    minValue -= min_zoom;
    maxValue += max_zoom;

    calcAxis();
}

void XYAxis::zoomValueInPos(const QPoint &pos)
{
    const double val_range = maxValue - minValue;
    if (val_range <= minRange)
        return;
    const double zoom_step = valueZoomInStep();
    if (zoom_step <= 0)
        return;
    const double zoom_proportion = calcZoomProportionWithPos(pos);
    if (val_range - zoom_step < minRange)
    {
        const double zoom_real_step = val_range - minRange;
        minValue += zoom_real_step / 2;
        maxValue = minValue + minRange;
    }
    else
    {
        minValue += zoom_step * zoom_proportion;
        maxValue -= zoom_step * (1 - zoom_proportion);
    }

    calcAxis();
}

void XYAxis::zoomValueOutPos(const QPoint &pos)
{
    if (minValue <= minLimit &&
        maxValue >= maxLimit)
        return;
    const double zoom_proportion = calcZoomProportionWithPos(pos);
    const double zoom_step = valueZoomInStep();
    const double min_step = zoom_step * zoom_proportion;
    const double max_step = zoom_step * (1 - zoom_proportion);
    const double min_zoom = (minValue - min_step < minLimit)
                                ? (minValue - minLimit)
                                : (min_step);
    const double max_zoom = (maxValue + max_step > maxLimit)
                                ? (maxLimit - maxValue)
                                : (max_step);
    //先不考虑补上不足的部分
    minValue -= min_zoom;
    maxValue += max_zoom;

    calcAxis();
}

void XYAxis::overallView()
{
    if (minValue <= minLimit &&
        maxValue >= maxLimit)
        return;
    minValue = minLimit;
    maxValue = maxLimit;
    calcAxis();
}

void XYAxis::setLimitRange(double min, double max, double range)
{
    if (min >= max || max - min < range)
    {
        return;
    }
    minLimit = min;
    maxLimit = max;
    minRange = range;
    emit axisChanged();
}

void XYAxis::setValueRange(double min, double max)
{
    if (min >= max || max - min <= minRange)
    {
        return;
    }
    minValue = min;
    maxValue = max;
    calcAxis();
}

该控件继承QWidget, 实现了左右上下四种形式的坐标轴控件。 可以设置固定间隔或自动选择间隔 可以设置最小间隔 开放一个槽来动态调整坐标轴的范围 处理了边缘刻度的显示 /************************************************************************ * 版权所有 (C) 2012-2015, liang1057@yahoo.com.cn 类声明: 坐标轴控件 ************************************************************************/ /** @brief 坐标轴控件 * * @details 坐标轴控件 只有刻度数字,数字可以隐藏(用来显示其他需要显示的刻度值) */ class uiAxis : public QWidget { Q_OBJECT public: /** @brief 坐标轴类型 * * @details 坐标轴类型 */ enum AXISTYPE{ LEFT_AXIS = 0, TOP_AXIS, RIGHT_AXIS, BOTTOM_AXIS }; /** @brief 构造函数 */ uiAxis(AXISTYPE type = BOTTOM_AXIS, QWidget *parent = 0); /** @brief 析构函数 */ ~uiAxis(void); /** @brief 设置坐标轴的范围 */ void setScop(double minValue, double maxValue); /** @brief 获取坐标轴的范围 */ void getScop(double& minValue,double& maxValue); /** @brief 获取坐标轴的范围 */ double getMinValue(); double getMaxValue(); /** @brief 设置坐标轴的类型 */ void setAxisType(AXISTYPE type); /** @brief 坐标轴的类型 */ AXISTYPE getAxisType(); /** @brief 设置最小刻度(小刻度的最小间隔) */ void setMinInterval(double value); /** @brief 设置自动间隔 */ void setAutoScale(bool val=true); /** @brief 设置固定间隔 */ void setSettedScale(bool val=true); /** @brief 设置坐标轴绘制范围, 像素值 */ void setBoundary(int left, int right, int top, int bottom); void getBoundary(int& left, int& right, int& top, int& bottom);
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龚建波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值