继承QWidget使用QPainter自定义二维图形控件【Qt学习】
通过阅读该文章,将了解本文所说的二维图形控件的基本概念、为何要自定义二维图形控件、如何自定义二维图形控件。
该文章将首先进行一些书面化的描述,然后再通过继承QWidget,使用QPainter自定义折线绘制控件进行演示,控件效果如下图:
(注:该例子仅仅是为了说明如何实现简单的自定义绘制,其实折线绘制我们一般是不会选择自定义实现,完全可以使用更加优秀的第三方库进行实现,如:QCustomPlot)
一、理论部分
1. 二维图形控件基本概念
控件:控件是指对数据和方法的封装,此处所指的“二维图形控件”特指通过Qt-QPainter绘制的以二维图形的形式展示给用户的交互控件
2. 为什么要自定义二维图形控件
Qt中有许多自定义的控件,比如下拉框QComboBox、按钮控件QPushButton、复选框QCheckBox等等,基本能满足我们大多数的界面开发需求,如下图所示:
但是,有时候我们会面临一些新的需求,比如说:”我需要个仪表盘“、”我需要个折线图“、”我需要个进度提示控件“…
然后你就会发现:
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)以上只是说明基础的绘制,未考虑其它过多的东西
【有时间且必要的话会持续更…】