您当前的位置: 首页 >  qt

龚建波

暂无认证

  • 5浏览

    0关注

    312博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Qt绘图:坐标轴

龚建波 发布时间:2021-12-04 02:28:57 ,浏览量:5

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 
#include 

//笛卡尔坐标系(直角坐标系)的坐标轴
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 getTickPos() const;
    //刻度值文本
    QVector 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 tickPos;
    //刻度值文本
    QVector 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 
#include 
#include 

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 XYAxis::getTickPos() const
{
    return tickPos;
}

QVector 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 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 = 1.0)
        return 1.0;
    return zoom_proportion;
}

void XYAxis::addMinValue()
{
    //不能小于最小范围
    if (maxValue - minValue  maxLimit)
    {
        maxValue = maxLimit;
    }
    calcAxis();
}

void XYAxis::subMaxValue()
{
    //不能小于最小范围
    if (maxValue - minValue             
关注
打赏
1655829268
查看更多评论
0.3808s