Qt自定义画板-多点绘制平滑曲线或函数曲线详解

本文主要讲解根据多点,绘制平滑曲线。且xy坐标轴根据点的大小,自适应xy轴的刻度。

比如点是1~8.5范围内的,坐标轴范围可以为0~9;如果是0-35.8范围内,坐标轴可以为0~40。

即根据数据范围,确定坐标轴范围。

一、基础知识

好了下面进入正题,要搞懂几个东西。就是我们数学中的点(后面简称【数点】)与QT中的点(后面简称【QT点】)的对应关系。中间还涉及到XY轴【画图区域】坐标(后面简称【图点】)和QT坐标的关系。

要画好图,就是要知道怎么把【数点】转为【QT点】。这样你就知道点画在哪里。

1、【图点】与【QT点】转换关系

如图:我们自定义QWidget,QWidget的大小就是height()*width()。我们预留画图区域与QWidget内边距分别是左右xpadding,上下ypadding。这个边距可以自己定义,也可以上下不一样宽。留这个边距是为了画坐标轴的数值相关。

【QT点】:是以左上角为原点(Qt(0,0)),以水平向右为x递增,以竖直向下为y递增。所以最右下角就是QPointF(width(),height())。右上角就是QPointF(widht(),0);以此类推。

【画图区域】:就是我们选定一个区域作为画图区域,来画x轴,y轴。

【图点】:就是画图区域的点位置。

比如图点(x,y)是相对于画图区域的原点o(0,0)的相对位置。这样是不是和坐标轴有点像了。

那图点转为QT点怎么转呢,从图中不难看出这样的关系。

QT点的x坐标=xpadding+x;

QT点的y坐标=height()-ypadding-y;

const QPointF GraphWidget::screenPointMaptoQTPoint(const QPointF &scPoint)
{
    return QPointF(scPoint.x() + xpadding, height() - ypadding - scPoint.y());
}

2、【数点】与【图点】转换关系

其实就是比例尺的问题。就是根据所有【数点】的xy大小,来选择一个合适的xy轴的范围。然后这个xy轴上面多长对应【画图区】的多少像素。这样【数点】就能等比缩放到【画图区域】。

举个例子。【画图区域】尺寸是300px(宽)*200px(高)。而所有【数点】的范围,x范围为0-33.5,y范围为0-8.5。那么我们可以取x轴的范围0~40(即minX=0,maxX=40),y轴范围0~9(minY=0;maxY=9)。这样所有的点都能在这个X轴y轴的范围内。所以我们的数据等比缩放就是。

x轴范围大小maxX-minX=40;对应【画图区域】宽(300px);所以缩放比例为kx=300/40;

y轴范围大小maxY-minY=9;对应【画图区域】高(200px);所以缩放比例为ky=200/9;

所以【数点】(xm,ym)转为【图点】是如下关系。

const QPointF GraphWidget::mathMaptoScreenPoint(const double &xm, const double &ym)
{
    return QPointF(qRound64((xm - minX) * kx), qRound64((ym - minY) * ky));
}

 3、【数点】与【QT点】转换关系

从1与2可以很容易知道他们之间的转换关系了。

const QPointF GraphWidget::mathMaptoQTPoint(const double &xm, const double &ym)
{
    return screenPointMaptoQTPoint(mathMaptoScreenPoint(xm,ym));
}

二、绘图

原理搞明白了,画图就简单了。定义GraphWidget。直接上代码。

1、画坐标系

2、画曲线

3、画极点(可选)、坐标轴说明。

#ifndef GRAPHWIDGET_H
#define GRAPHWIDGET_H

#pragma once
#include <QPaintEvent>
#include <QPainter>
#include <QWidget>
// 坐标
struct CoordinateXY {
    CoordinateXY(const double &_x = -1, const double &_y = -1) : x(_x), y(_y) { }
    double x;
    double y;
};

struct GraphDataInfo {
    GraphDataInfo() : min(0), max(0), space(5), k(1), unit("") { }
    inline double range() { return max - min; }
    double min;   // 坐标轴最小刻度值
    double max;   // 坐标轴最大刻度值
    int space;    // 刻度分割多少列(行)
    double k;     // xy放大倍数=屏幕大小/范围
    QString unit; // x轴,y轴显示内容
};

class GraphWidget : public QWidget
{
    Q_OBJECT
public:
    GraphWidget(QWidget *parent = nullptr);
    ~GraphWidget();
    /// @brief 设置xy坐标点列表数据
    /// @param xyListy 数学中的坐标点list
    /// @param polexy 极点,可画可不画,看需要
    /// @param xUnit x轴显示内容包括单位
    /// @param yUnit y轴显示内容包括单位
    void setXYDoubleData(const QList<CoordinateXY> &xyList, const QString &xUnit = "", const QString &yUnit = "",
                         const CoordinateXY &polexy = CoordinateXY());
    void setDrawPole(bool draw);

protected:
    virtual void paintEvent(QPaintEvent *event) override;

private:
    /// @brief 数点转换为QT点位置。
    /// @param xm 数据x
    /// @param ym 数据y
    /// @return
    const QPointF mathMaptoQTPoint(const CoordinateXY &mathxy);
    /// @brief 画图区域点换为QT点位置
    /// @param scPoint
    /// @return
    const QPointF screenPointMaptoQTPoint(const QPointF &scPoint);
    /// @brief 数点转换为画图区域点位置
    /// @param scPoint
    /// @return
    const QPointF mathMaptoscreenPoint(const CoordinateXY &mathxy);
    /// @brief 获取列表中最大或最小值
    /// @param list
    /// @param xvalue
    /// @param yvalue
    /// @param bmax
    void MinMaxValue(const QList<CoordinateXY> &list, double &xvalue, double &yvalue, bool bmax);
    /// @brief 获取图形转换相关信息
    /// @param maxMath 数学最大值
    /// @param minMath 数学最小值
    /// @param screenpix 屏幕的宽度或者高度
    /// @param unitstr 单位
    /// @return
    GraphDataInfo getGraphDataInfo(const double maxMath, const double &minMath, const int &screenpix,
                                   const QString &unitstr);
    bool isEqualO(const double &value);

    /// @brief 画垂直x轴网格线
    /// @param painter
    /// @param lineNo
    void DrawGridX(QPainter *painter, const int lineNo);
    /// @brief 画垂直y轴网格线
    /// @param painter
    /// @param lineNo
    void DrawGridY(QPainter *painter, const int lineNo);
    /// @brief 画xy曲线
    /// @param painter
    void DrawXYPoints(QPainter *painter);
    /// @brief 画极点
    /// @param painter
    void DrawPole(QPainter *painter);
    /// @brief 画单位
    /// @param painter
    void DrawUnit(QPainter *painter);

private:
    GraphDataInfo m_xAxisInfo;    // x轴信息
    GraphDataInfo m_yAxisInfo;    // y轴信息
    QList<CoordinateXY> m_xyList; // x点列表实际坐标
    CoordinateXY m_pole;          // 极点坐标
    bool m_drawPole;
};

#endif
#include "GraphWidget.h"

#include <QFontMetrics>
#include <QPainterPath>
#include <QtMath>

static const int xpadding = 40;                           // xy轴屏幕距离窗口的内边距
static const int ypadding = 40;                           // xy轴屏幕距离窗口的内边距
static const int widgetWidth = 300 + 2 * xpadding;        // 宽度
static const int widgetHeight = 240 + 2 * ypadding;       // 高度
static const QColor TEXTCOLOR_NORMAL = QColor("#252d3f"); // 文字颜色

GraphWidget::GraphWidget(QWidget *parent)
    : QWidget(parent),
      m_xAxisInfo(GraphDataInfo()),
      m_yAxisInfo(GraphDataInfo()),
      m_xyList(QList<CoordinateXY>()),
      m_pole(0, 0),
      m_drawPole(false)
{
    this->setFixedSize(widgetWidth, widgetHeight);
    QList<CoordinateXY> list;
    for (int x = 0; x < 100; ++x) {
        list.append(CoordinateXY(x * 0.1, 1.5 * 0.01 * x * x));
    }
    setXYDoubleData(list, "x轴(嘻嘻)", "y轴(哈哈)", CoordinateXY(6, 1.5 * 36));
    setDrawPole(true);
}

GraphWidget::~GraphWidget() { }

void GraphWidget::setXYDoubleData(const QList<CoordinateXY> &xyList, const QString &xUnit, const QString &yUnit,
                                  const CoordinateXY &polexy)
{
    this->setFixedSize(widgetWidth, widgetHeight);
    double minX = 0.0;
    double maxX = 0.0;
    double minY = 0.0;
    double maxY = 0.0;
    MinMaxValue(xyList, maxX, maxY, true);
    MinMaxValue(xyList, minX, minY, false);
    m_xAxisInfo = getGraphDataInfo(maxX, minX, (this->width() - 2 * xpadding), xUnit);
    m_yAxisInfo = getGraphDataInfo(maxY, minY, (this->height() - 2 * ypadding), yUnit);
    m_xyList = xyList;
    m_pole = polexy;
    update();
}

void GraphWidget::setDrawPole(bool draw)
{
    m_drawPole = draw;
}

void GraphWidget::paintEvent(QPaintEvent *event)
{
    if (isEqualO(m_xAxisInfo.range()) && isEqualO(m_yAxisInfo.range()) || m_xyList.size() <= 0) {
        QWidget::paintEvent(event);
        return;
    }

    QPainter *painter = new QPainter(this);
    for (int n = 0; n <= m_xAxisInfo.space; ++n) {
        DrawGridX(painter, n);
    }
    for (int k = 0; k <= m_yAxisInfo.space; ++k) {
        DrawGridY(painter, k);
    }
    DrawXYPoints(painter);
    DrawPole(painter);
    DrawUnit(painter);
}

const QPointF GraphWidget::mathMaptoQTPoint(const CoordinateXY &mathxy)
{
    return screenPointMaptoQTPoint(mathMaptoscreenPoint(mathxy));
}

const QPointF GraphWidget::screenPointMaptoQTPoint(const QPointF &scPoint)
{
    return QPointF(scPoint.x() + xpadding, height() - ypadding - scPoint.y());
}

const QPointF GraphWidget::mathMaptoscreenPoint(const CoordinateXY &mathxy)
{
    return QPointF(qRound64((mathxy.x - m_xAxisInfo.min) * m_xAxisInfo.k),
                   qRound64((mathxy.y - m_yAxisInfo.min) * m_yAxisInfo.k));
}

void GraphWidget::MinMaxValue(const QList<CoordinateXY> &list, double &xvalue, double &yvalue, bool bmax)
{
    xvalue = 0.0;
    yvalue = 0.0;
    for (CoordinateXY dvlue : list) {
        if (bmax) {
            if (xvalue < dvlue.x) {
                xvalue = dvlue.x;
            }
            if (yvalue < dvlue.y) {
                yvalue = dvlue.y;
            }
        } else {
            if (xvalue > dvlue.x) {
                xvalue = dvlue.x;
            }
            if (yvalue > dvlue.y) {
                yvalue = dvlue.y;
            }
        }
    }
}

GraphDataInfo GraphWidget::getGraphDataInfo(const double maxMath, const double &minMath, const int &screenpix,
                                            const QString &unitstr)
{
    GraphDataInfo graphInfo;
    double mathRange = maxMath - minMath;
    // 倍数,放大或缩小
    double multiple = 1.0;
    if (mathRange > 10.0) {
        while (mathRange > 10.0) {
            multiple = multiple / 10.0;
            mathRange = mathRange / 10.0;
        }
    } else if (mathRange < 1.0 && mathRange > 0) {
        while (mathRange < 1.0) {
            multiple = multiple * 10.0;
            mathRange = mathRange * 10.0;
        }
    } else if (isEqualO(mathRange)) {
        graphInfo.min = qFloor(minMath);
        graphInfo.max = graphInfo.min + multiple * 1;
        return graphInfo;
    }
    graphInfo.space = qCeil(mathRange);
    double range = graphInfo.space / multiple;
    graphInfo.min = qFloor(minMath);
    graphInfo.max = range - graphInfo.min;
    graphInfo.k = screenpix / range;
    if (graphInfo.space < 5) {
        graphInfo.space = graphInfo.space * 2;
    }
    graphInfo.unit = unitstr;
    return graphInfo;
}

bool GraphWidget::isEqualO(const double &value)
{
    if ((value < 0.0 && value > -1e-16) || (value > 0.0 && value < 1e-16)) {
        return true;
    }
    return false;
}

void GraphWidget::DrawGridX(QPainter *painter, const int lineNo)
{
    if (painter == nullptr || m_xAxisInfo.space == 0) {
        return;
    }
    painter->setPen(QPen(TEXTCOLOR_NORMAL, 1));
    painter->setFont(QFont("Arial", 8));
    int xPixel = ((this->width() - 2 * xpadding) * lineNo / m_xAxisInfo.space);
    // 刻度线上两点
    QPointF xGridPoint(xPixel, 0);
    QPointF xGridTopPoint(xPixel, (this->height() - 2 * ypadding));
    // 屏幕坐标转换成窗口坐标
    QPointF xGridPointQt = screenPointMaptoQTPoint(xGridPoint);
    QPointF xGridTopPointQt = screenPointMaptoQTPoint(xGridTopPoint);
    // 绘制刻度线
    painter->drawLine(xGridPointQt, xGridTopPointQt);

    // 写上刻度
    QPointF textPoint = xGridPoint + QPointF(-5, -ypadding / 3);
    QPointF textPointQt = screenPointMaptoQTPoint(textPoint);
    painter->drawText(textPointQt, QString("%1").arg(m_xAxisInfo.range() / m_xAxisInfo.space * lineNo));
}

void GraphWidget::DrawGridY(QPainter *painter, const int lineNo)
{
    if (painter == nullptr || m_yAxisInfo.space == 0) {
        return;
    }
    painter->setPen(QPen(TEXTCOLOR_NORMAL, 1));
    painter->setFont(QFont("Arial", 8));
    int yPixel = (this->height() - 2 * ypadding) * lineNo / m_yAxisInfo.space;

    // 刻度线上两点
    QPointF yGridPoint(0, yPixel);
    QPointF yGridRightPoint((this->width() - 2 * xpadding), yPixel);
    // 屏幕坐标转换成窗口坐标
    QPointF yGridPointQt = screenPointMaptoQTPoint(yGridPoint);
    QPointF yGridRightPointQt = screenPointMaptoQTPoint(yGridRightPoint);
    // 绘制刻度线
    painter->drawLine(yGridPointQt, yGridRightPointQt);

    // 写上刻度
    QPointF textPoint = yGridPoint + QPointF(-xpadding / 2, -5);
    QPointF textPointQt = screenPointMaptoQTPoint(textPoint);
    painter->drawText(textPointQt, QString("%1").arg(m_yAxisInfo.range() / m_yAxisInfo.space * lineNo));
}

void GraphWidget::DrawXYPoints(QPainter *painter)
{
    if (painter == nullptr) {
        return;
    }
    if (m_xyList.size() <= 0) {
        return;
    }
    painter->setRenderHint(QPainter::Antialiasing, true);
    painter->setPen(QPen(Qt::blue, 2));
    QPainterPath path(mathMaptoQTPoint(m_xyList[0]));
    for (int i = 0; i < m_xyList.size() - 1; ++i) {
        QPointF startPoint(mathMaptoQTPoint(m_xyList[i]));
        QPointF endPoint(mathMaptoQTPoint(m_xyList[i + 1]));
        path.quadTo(startPoint, (startPoint + endPoint) * 0.5);
    }

    path.lineTo(mathMaptoQTPoint(m_xyList.last()));
    painter->drawPath(path);
}

void GraphWidget::DrawPole(QPainter *painter)
{
    if (painter == nullptr || !m_drawPole) {
        return;
    }
    painter->setPen(QPen(Qt::red, 1));
    QPointF poleQt = mathMaptoQTPoint(m_pole);
    painter->drawEllipse(poleQt, 4, 4);
    painter->setPen(QPen(TEXTCOLOR_NORMAL, 1));
    painter->setFont(QFont("Arial", 10));
    painter->drawText(poleQt + QPointF(-10, -10),
                      QString("(X:%1,Y:%2)").arg(QString::number(m_pole.x)).arg(QString::number(m_pole.y)));
}

void GraphWidget::DrawUnit(QPainter *painter)
{
    if (painter == nullptr) {
        return;
    }
    painter->setPen(QPen(TEXTCOLOR_NORMAL, 1));
    painter->setFont(QFont("Arial", 10));
    int xwidth = painter->fontMetrics().width(m_xAxisInfo.unit);
    int ywidth = painter->fontMetrics().width(m_yAxisInfo.unit);
    painter->drawText(QPointF((width() - xwidth) / 2, height() - ypadding / 3), m_xAxisInfo.unit);
    painter->translate(xpadding / 3, (height() + ywidth) / 2); // 第1步:变换旋转中心到所绘制文字左下角
    painter->rotate(-90);                                      // 第2步: 旋转一定角度
    painter->drawText(0, 0, m_yAxisInfo.unit);                 // 第3步: 绘制文字
    painter->resetMatrix();                                    // 第4步: 恢复坐标旋转
}

三、效果图

下图不带极点。如果不想要xy轴的说明,可以传空字符。

下图带极点(极点目前只支持自己算好传进来)

 原创码子不易,喜欢的话就点个赞吧~

转载请注明出处,谢谢~

 

  • 13
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Qt 中,可以使用鼠标事件和 QPainter 绘图类来实现鼠标连续绘制贝塞尔曲线。下面是一个简单的实现示例: ```cpp #include <QtWidgets> class BezierWidget : public QWidget { public: BezierWidget(QWidget *parent = 0); protected: void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void paintEvent(QPaintEvent *event); private: QList<QPointF> m_points; }; BezierWidget::BezierWidget(QWidget *parent) : QWidget(parent) { setMouseTracking(true); } void BezierWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { m_points.append(event->pos()); update(); } } void BezierWidget::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { m_points.append(event->pos()); update(); } } void BezierWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); if (m_points.size() < 2) { return; } QPainterPath path(m_points.first()); for (int i = 1; i < m_points.size() - 1; ++i) { QPointF p1 = m_points[i - 1]; QPointF p2 = m_points[i]; QPointF p3 = (m_points[i] + m_points[i + 1]) / 2; path.cubicTo(p1, p2, p3); } path.lineTo(m_points.last()); painter.drawPath(path); } int main(int argc, char *argv[]) { QApplication app(argc, argv); BezierWidget widget; widget.resize(400, 400); widget.show(); return app.exec(); } ``` 这个示例程序会在窗口中实时绘制鼠标经过的点和贝塞尔曲线,每次鼠标按下或移动时都会将当前点添加到点列表中,然后使用 QPainterPath 类的 cubicTo() 函数绘制贝塞尔曲线。注意在构造函数中调用 setMouseTracking(true) 可以让窗口接收鼠标移动事件,即使没有按下鼠标键。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值