0.前言
绘制图表很重要的一步就是确立坐标轴,有了标尺,才能找准自己的定位。每一个数据点都需要根据坐标轴来计算数据值对应的屏幕像素位置。
本文代码源码链接及实现效果如下:
github 链接(XYView类):https://github.com/gongjianbo/EasyQPainter
对于固定的坐标范围,刻度可以直接根据最大最小值进行均分。但是加入缩放、移动等交互后,就需要保存一些额外的信息,如最小缩放范围、最大最小限定范围等。
确定好范围后,就是刻度的位置计算。一些常见的图表库都会根据当前范围动态的计算刻度间隔和刻度值,且尽量取整。如 [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
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?