继承QWidget使用QPainter自定义二维图形控件【Qt学习】

继承QWidget使用QPainter自定义二维图形控件【Qt学习】

通过阅读该文章,将了解本文所说的二维图形控件的基本概念、为何要自定义二维图形控件、如何自定义二维图形控件。

该文章将首先进行一些书面化的描述,然后再通过继承QWidget,使用QPainter自定义折线绘制控件进行演示,控件效果如下图:
折线绘制控件
(注:该例子仅仅是为了说明如何实现简单的自定义绘制,其实折线绘制我们一般是不会选择自定义实现,完全可以使用更加优秀的第三方库进行实现,如:QCustomPlot)

一、理论部分

1. 二维图形控件基本概念

控件:控件是指对数据和方法的封装,此处所指的“二维图形控件”特指通过Qt-QPainter绘制的以二维图形的形式展示给用户的交互控件

2. 为什么要自定义二维图形控件

Qt中有许多自定义的控件,比如下拉框QComboBox、按钮控件QPushButton、复选框QCheckBox等等,基本能满足我们大多数的界面开发需求,如下图所示:
Qt自带的图形控件

但是,有时候我们会面临一些新的需求,比如说:”我需要个仪表盘“、”我需要个折线图“、”我需要个进度提示控件“…

然后你就会发现:
a. 现存的Qt控件无法满足需求
b. 没有找到合适的第三方开源控件

这个时候就需要自定义控件啦!!!!

3. 自定义窗口部件的常见思路

我整理了下就我理解的自定义部件的常见思路,如下图所示,主要列出了三种可行的方式:
a. 对现存的窗口部件进行组合,即最常规的方式,比如用Designer拖动现成控件设计对话框,不需要自己写控件的实现方案,只需要处理交互逻辑;
b. 子类化相关窗口部件,就是继承现成的一些控件,修改或是扩展相关的功能以满足需求,如重写QComboBox;
c. 子类化QWidget, 其实和b所说的一致,只是灵活性更高,我们要完全根据我们的需求去定义数据、外观、行为等。

该文章例子演示部分主要说明子类化QWidget如何进行。
在这里插入图片描述

二、例子演示

目前我暂时不一步一步的去进行代码说明,我直接将折线绘制的代码附上,现在仅仅是进行简单的实现绘制,后期有时间再实现一些交互。

一般的 ,实现这么一个功能较单一的控件, 将数据和行为都定义在控件内即可,一般步骤是:
a . 根据需要思考我们的数据结构,由于绘制的是折线,我们就可使用QVector m_polylineData来装载折线数据即可【即输入数据】
b. 下一步就是考虑如何将a中的数据绘制到界面,这一步需要我们根据窗体和数据信息,作一些数据计算和映射,最后使用画笔QPainiter在paintEvent中绘制即可
c. 绘制好后,我们需要考虑的就是用户看见这些数据,可能会进行的操作,如拖动、缩放等【TODO该例子暂未实现】

PolylineWidget.h:

/**
 * 名称:折线绘制控件
 * 日期:2021.02.08
 * 作者:zl
 *
 * 说明:
 *      该控件仅仅是对如何继承QWidget并根据需求定义外观、行为进行一个简单的说明
 *
 * 整体布局:
 * 其中:
 *     m_viewRect主要用于绘制折线
 *     PolylineWidget和m_viewRect之间就是边矩,我们将在这个区域绘制标题信息和标尺信息
 *
 *                  PolylineWidget
 * ——————————————————————————————————————————————
 *|                     主标题                 |
 *|           ----------------------------     |
 *|          :                          :    |
 *|          : 折线绘制区域(m_viewRect) :    |
 *| 垂直标题 :                          :    |
 *|          :                          :    |
 *|          :---------------------------     |
 *|                 水平标题                   |
 * —————————————————————————————————————————————
 * */


#ifndef POLYLINEWIDGET_H
#define POLYLINEWIDGET_H


#include <QWidget>
class PolylineWidget:public QWidget
{
    Q_OBJECT

public:
    explicit PolylineWidget(QWidget *parent = 0);
    ~PolylineWidget();

    /**
     * @brief 设置折线图数据
     * @param polyline 输入折线数据点集
     */
    void setPolylineData(const QVector<QPointF>& polyline);

    /**
     * @brief 设置主标题
     * @param title 输入 主标题
     */
    void setMainTitleText(const QString& title);

    /**
     * @brief 设置横坐标显示文字
     * @param HText 输入 横坐标显示文字
     */
    void setHCoordinateText(const QString& HText);

    /**
     * @brief 设置纵坐标显示文字
     * @param VText 输入 纵坐标显示文字
     */
    void setVCoordinateText(const QString& VText);

    /**
     * @brief 设置折线绘制区与窗体边界的间隔
     * @param left 输入 左间距
     * @param top 输入 上间距
     * @param right 输入 右间距
     * @param bottom 输入 下间距
     */
    void setMargin(int left, int top, int right, int bottom);

    /**
     * @brief 【未实现】刷新重绘,用于用户手动刷新
     */
    void refresh();

protected:

    /**
    * @brief 窗体大小改变,每次改变窗体大小后将重新计算边矩
    */
    virtual void resizeEvent(QResizeEvent* event);

    /**
    * @brief 重绘事件,控制主要的绘制流程
    */
    virtual void paintEvent(QPaintEvent *event);

    /**
     * @brief 绘制折线
     * @param p 输入 画笔
     */
    void drawPolyline(QPainter *p);

    /**
     * @brief 绘制横坐标标尺
     * @param p 输入 画笔
     */
    void drawHCoordinate(QPainter *p);

    /**
     * @brief 绘制纵坐标标尺
     * @param p 输入 画笔
     */
    void drawVCoordinate(QPainter *p);

    /**
     * @brief 绘制横纵坐标显示文字
     * @param p 输入 画笔
     */
    void drawCoordinateText(QPainter *p);

private:
    /**
     * @brief 逻辑数值X转桌面像素坐标X
     * @param lpx 输入 逻辑坐标X
     * @return
     */
    int lpxTodpx(double lpx);

    /**
     * @brief 桌面像素X坐标转逻辑坐标X
     * @param dpx 输入 桌面像素坐标
     * @return
     */
    double dpxTolpx(int dpx);

    /**
     * @brief 逻辑坐标Y转桌面坐标
     * @param lpy 输入 逻辑y值
     * @return
     */
    int lpyTodpy(double lpy);

    /**
     * @brief 桌面y坐标转逻辑y坐标
     * @param dpy 输入 桌面y坐标
     * @return
     */
    double dpyTolpy(int dpy);

    /**
     * @brief 设置绘制的折线数据后需要计算逻辑数值范围,且对数据进行排序
     */
    void initData();

private:
    ///< 折线图数据,即需要绘制的数据
    QVector<QPointF> m_polylineData;
    ///< 输入数值的最值范围 横X 纵Y
    double m_maxX;
    double m_minX;
    double m_maxY;
    double m_minY;
    ///< 折线图横坐标显示文本
    QString m_HCoordinateText;
    ///< 折线图纵坐标显示文本
    QString m_VCoordinateText;
    ///< 主标题,即顶部的标图
    QString m_mainTitle;



    ///< 绘图区域,用于绘制折线图形的区域,
    QRect m_viewRect;
    ///< 绘制区域与窗体边界的间距
    QRect m_marginRect;
};

#endif // POLYLINEWIDGET_H

PolylineWidget.cpp:

#include "polylinewidget.h"
#include"qpainter.h"

#include <math.h>
#include <QDebug>

/*********工具方法**************/


/// 比较函数,升序排列
bool compare(const QPointF& p1, const QPointF& p2){
    return p1.x() < p2.x();
}

/// 调整步数
double gAdaptStep(double step)
{
    double exp;

    exp = 1.0;
    while(step < 10)
    {
        step *= 10;
        exp /= 10;
    }

    while(step > 100)
    {
        step /= 10;
        exp *= 10;
    }

    step = qRound(step/5)*5*exp;
    return step;
}





PolylineWidget::PolylineWidget(QWidget *parent):
    QWidget(parent)
{
   // 最值初始化
   m_maxX = m_maxY = -HUGE_VAL;
   m_minX = m_minY = HUGE_VAL;
   // 坐标显示文字
   m_HCoordinateText = m_VCoordinateText ="";

   // 默认边距
   m_marginRect.setLeft(20);
   m_marginRect.setTop(20);
   m_marginRect.setRight(20);
   m_marginRect.setBottom(20);

}

PolylineWidget::~PolylineWidget()
{

}

void PolylineWidget::setPolylineData(const QVector<QPointF> &polyline)
{
    m_polylineData = polyline;
    initData();
}

void PolylineWidget::setMainTitleText(const QString &title)
{
    m_mainTitle = title;
}

void PolylineWidget::setHCoordinateText(const QString &HText)
{
    m_HCoordinateText = HText;

}

void PolylineWidget::setVCoordinateText(const QString &VText)
{
    m_VCoordinateText = VText;
}

void PolylineWidget::setMargin(int left, int top, int right, int bottom)
{
    // 将边矩保存到成员变量
    m_marginRect.setLeft(left);
    m_marginRect.setRight(right);
    m_marginRect.setTop(top);
    m_marginRect.setBottom(bottom);

    // 更新绘图区
    int w = width() - m_marginRect.left() - m_marginRect.right();
    int h = height() - m_marginRect.top() - m_marginRect.bottom();
    m_viewRect.setRect(m_marginRect.left(), m_marginRect.top(), w,
                      h);
}

void PolylineWidget::refresh()
{

}

void PolylineWidget::resizeEvent(QResizeEvent *event)
{
    // 更新绘图区
    int w = width() - m_marginRect.left() - m_marginRect.right();
    int h = height() - m_marginRect.top() - m_marginRect.bottom();
    m_viewRect.setRect(m_marginRect.left(), m_marginRect.top(), w,
                      h);
}

void PolylineWidget::paintEvent(QPaintEvent *event)
{
    if(m_maxX <= m_minX || m_maxY <= m_minY){
        return;
    }
    if(m_viewRect.width() < 5 || m_viewRect.height() < 5){
        return;
    }

    QPainter p(this);
    p.setBrush(QBrush(Qt::white));
    p.drawRect(m_viewRect);
    drawPolyline(&p);
    drawHCoordinate(&p);
    drawVCoordinate(&p);
    drawCoordinateText(&p);
}

void PolylineWidget::drawPolyline(QPainter *p)
{
    if(m_polylineData.isEmpty()) return;
    p->save();

    p->setBrush(QBrush(Qt::black));

    QPoint p1, p2;
    p1.setX(lpxTodpx(m_polylineData.first().x()));
    p1.setY(lpyTodpy(m_polylineData.first().y()));

    // 给点画一个小圆
    p->drawEllipse(p1, 2, 2);
    for(int i=1; i<m_polylineData.count(); ++i){
        p2.setX(lpxTodpx(m_polylineData[i].x()));
        p2.setY(lpyTodpy(m_polylineData[i].y()));
        // 给点画一个小圆
        p->drawEllipse(p2, 2, 2);

        // 画折线
        p->drawLine(p1, p2);
        p1 = p2;
    }

    p->restore();
}

void PolylineWidget::drawHCoordinate(QPainter *p)
{
    // 坐标标尺总数
    int rulerCount = 5;

    // 1)求出横标尺的逻辑间隔
    double deltx;
    int viewLeft, viewRight;
    viewLeft = m_viewRect.left();
    viewRight = m_viewRect.right();
    deltx = (dpxTolpx(viewRight) - dpxTolpx(viewLeft)) / rulerCount;
    deltx = gAdaptStep(fabs(deltx));// 调整步数

    // 2)得到开始绘制标尺的逻辑起点,和像素起点
    double lx; // 逻辑起点
    double dx; // 像素起点
    lx = dpxTolpx(viewLeft);
    lx = ceil(lx / deltx) * deltx; // 调整逻辑起点位置
    dx = lpxTodpx(lx); // 像素起点
    while(dx < viewLeft){
        // 当起点在视图区左侧之外时再向前移动一个步数
        lx += deltx;
        dx = lpxTodpx(lx);
    }

    // 3)开始绘制标尺及其文字
    int x, y;
    x = dx;
    y = m_viewRect.bottom();
    QFontMetrics fm = p->fontMetrics();
    while(x <= viewRight){
        // 得到显示数值
        QString str = QString::number(lx, 'f', 1);
        int strW = fm.width(str);

        // 绘制数值
        p->drawText(x - strW/2, y + 18, str);
        // 绘制标尺短线
        p->drawLine(x, y+5, x, y);

        // 修改步进
        lx += deltx;
        x = lpxTodpx(lx);
    }
}

void PolylineWidget::drawVCoordinate(QPainter *p)
{
    // 坐标标尺总数
    int rulerCount = 5;

    // 1)求出纵标尺的逻辑间隔
    double delty;
    int viewBottom, viewTop;
    viewBottom = m_viewRect.bottom();
    viewTop = m_viewRect.top();
    delty = (dpyTolpy(viewTop) - dpyTolpy(viewBottom)) / rulerCount;
    delty = gAdaptStep(fabs(delty));

    // 2)得到开始绘制标尺的逻辑起点,和像素起点
    double ly; // 逻辑起点
    double dy; // 像素起点
    ly = dpyTolpy(viewBottom);
    ly = floor(ly / delty) * delty; // 调整起点位置
    dy = lpyTodpy(ly);
    while(dy > viewBottom){
        ly += delty;
        dy = lpyTodpy(ly);
    }

    // 3)开始绘制标尺及其文字
    int x, y;
    x = m_viewRect.left();
    y = dy;
    QFontMetrics fm = p->fontMetrics();
    int strH = fm.height();
    while(y >= viewTop){
        // 得到显示数值
        QString str = QString::number(ly, 'f', 1);
        int strW = fm.width(str);

        // 绘制数值
        p->drawText(x - strW - 10, y - strH/2,
                    strW, strH, Qt::AlignCenter, str);
        // 绘制标尺短线
        p->drawLine(x, y, x-5, y);

        // 修改步进
        ly += delty;
        y = lpyTodpy(ly);

    }

}

void PolylineWidget::drawCoordinateText(QPainter *p)
{

    // 绘制主标题、横坐标标题、纵坐标标题
    QFontMetrics fm = p->fontMetrics();
    p->save();

    p->setFont(QFont(p->font().family(), 16));
    // 1)绘制主标题
    int mainTitleX, mainTitleY;
    mainTitleX = m_viewRect.left() + m_viewRect.width() / 2
            - fm.width(m_mainTitle) / 2;
    mainTitleY = m_viewRect.top()-5;
    p->drawText(mainTitleX, mainTitleY, m_mainTitle);


    // 2)绘制横坐标标题
    int hx, hy;
    hx = m_viewRect.left() + m_viewRect.width() / 2
            - fm.width(m_HCoordinateText) / 2;
    hy = m_viewRect.bottom() + m_marginRect.bottom()
            /*+ fm.height()*/;
    p->drawText(hx, hy, m_HCoordinateText);

    // 3) 绘制纵坐标
    int vx, vy;
    vx = m_viewRect.left() - m_marginRect.left() + fm.height();
    vy = m_viewRect.top() + m_viewRect.height() / 2
            + fm.width(m_VCoordinateText) / 2;
    p->translate(vx, vy);
    p->rotate(-90);
    p->drawText(0, 0, m_VCoordinateText);
    p->resetTransform();


    p->restore();
}

int PolylineWidget::lpxTodpx(double lpx)
{
    return m_viewRect.left() +
            m_viewRect.width() * (lpx - m_minX) / (m_maxX - m_minX);
}

double PolylineWidget::dpxTolpx(int dpx)
{
    return (dpx -m_viewRect.left()) * (m_maxX - m_minX) / m_viewRect.width() + m_minX;
}

int PolylineWidget::lpyTodpy(double lpy)
{
    return m_viewRect.bottom() - m_viewRect.height() * (lpy - m_minY) / (m_maxY - m_minY);
}

double PolylineWidget::dpyTolpy(int dpy)
{
    return (m_viewRect.bottom() - dpy) * (m_maxY - m_minY) / m_viewRect.height() + m_minY;
}

void PolylineWidget::initData()
{
    if(m_polylineData.isEmpty()){
        return;
    }

    // 对折线数据进行排序
    qSort(m_polylineData.begin(), m_polylineData.end(), compare);

    // 计算最值
    double maxX,minX,maxY,minY;
    maxX = maxY = -HUGE_VAL;
    minX = minY = HUGE_VAL;
    foreach (QPointF point, m_polylineData) {
        if(maxX < point.x()){
            maxX = point.x();
        }
        if(minX > point.x()){
            minX = point.x();
        }

        if(maxY < point.y()){
            maxY = point.y();
        }
        if(minY > point.y()){
            minY = point.y();
        }
    }

    // 当获取到有效的数据后对最值做一些偏移,使其不会绘制在边界上
    if(maxX > minX && maxY > minY){
        m_maxX = maxX + 0.1 * (maxX - minX);
        m_minX = minX - 0.1 * (maxX - minX);
        m_maxY = maxY + 0.1 * (maxY - minY);
        m_minY = minY - 0.1 * (maxY - minY);
    }
}

使用定义的折线绘制控件方式【在需要的地方调用即可】:


    QVector<QPointF> datas;
    datas << QPointF(12, 2.5)
             << QPointF(13, 12)
                << QPointF(14, 3.5)
                   << QPointF(15, 14)
                      << QPointF(16, 4.5)
                         << QPointF(17, 8.5)
                            << QPointF(18, 3.5)
                               << QPointF(19, 20.5)
                                 << QPointF(23, 12.2)
                                    << QPointF(19, 20)
                                       << QPointF(52, 4.4);


    PolylineWidget *pPolyWidget = new PolylineWidget;
    pPolyWidget->setMainTitleText("PolyWidget");
    pPolyWidget->setHCoordinateText("HCoordinate"); // 横坐标标题
    pPolyWidget->setVCoordinateText("VCoordinate"); // 纵坐标标图
    pPolyWidget->setMargin(60, 50, 30, 35); // 边矩
    pPolyWidget->setPolylineData(datas); // 需要绘制的数据
    this->setCentralWidget(pPolyWidget);

效果图:
在这里插入图片描述

备注:
1) 以上仅仅对源码进行了粘贴,未进行具体说明,有问题请指正,有需要求留言
2)以上只是说明基础的绘制,未考虑其它过多的东西

【有时间且必要的话会持续更…】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值