QPainter - 使用一个时钟项目从头开始看QPainter
之前一直在说绘制,但是没有从头详细的去了解绘制这块的写法,因此我们来使用一个时钟的项目来学习一下绘制
先上个图:
绘制的原理
clockpainter
#ifndef PAINTERTEST_CLOCKPAINTER_H
#define PAINTERTEST_CLOCKPAINTER_H
#include <QTimer>
#include <QWidget>
#include <QColor>
class ClockPainter : public QWidget
{
Q_OBJECT
public:
explicit ClockPainter(QWidget *parent = nullptr);
~ClockPainter() override;
protected:
void paintEvent(QPaintEvent *event) override;
private:
};
#endif //PAINTERTEST_CLOCKPAINTER_H
#include "clockpainter.h"
#include <QPainter>
ClockPainter::ClockPainter(QWidget *parent)
: QWidget(parent)
{
}
ClockPainter::~ClockPainter()
{
}
void ClockPainter::paintEvent(QPaintEvent *event)
{
int width = this->width();
int height = this->height();
int side = qMin(width, height);
QPainter painter(this);
// 抗锯齿
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
// 将绘制原点移动到界面的中心点
painter.translate(width / 2, height / 2);
// 用(sx, sy)对坐标系进行缩放。(计算的比例使得缩放在一个合适的范围)
painter.scale(side / 200.0, side / 200.0);
// 保存当前的painter状态(将状态推入堆栈)。save()之后必须跟着相应的restore();end()函数的作用是展开堆栈。
// save 和restore 是为了保证在绘制复杂图形的时候绘制的准确性的。因为需要经过多次的变换操作,为了多次变换间产生的影响,因此需要使用这两个函数
painter.save();
QPen pen;
pen.setColor(Qt::red);
pen.setWidth(5);
painter.setPen(pen);
// 顺时针旋转坐标系。给定的角度参数以度为单位。
painter.translate(50, 50);
// 顺时针旋转坐标系。给定的角度参数以度为单位。
painter.rotate(90);
// 绘制直线
painter.drawLine(QPoint(0, 0), QPoint(0, 50));
// 恢复当前painter状态(从堆栈中弹出已保存的状态)。
painter.restore();
QWidget::paintEvent(event);
}
从上面的例子我们可以看到QT整体的坐标系是怎么样的, (0,0)在左上角,+x轴是向左,+y轴是向下。
我们可以通过变换去改变这些位置。这个需要对线性代数有基本的了解,具体的可以参考下面这篇:
QT+ OpenGL 变换_turbolove的博客-CSDN博客
你可以尝试修改对应的顺序看看效果
备注:painter操作的是坐标系,这个我自己看感觉出来的
按照上面的思路,我们加一个计时器,并且使得每秒旋转6度,这样的话,我们的这个线一分钟可以转一圈
绘制时分秒指针
我们需要删除现有的绘制函数,然后添加新的函数来绘制对应的指针
void ClockPainter::drawHands(QPainter &painter)
{
// 保存当前的painter状态(将状态推入堆栈)。save()之后必须跟着相应的restore();end()函数的作用是展开堆栈。
// save 和restore 是为了保证在绘制复杂图形的时候绘制的准确性的。因为需要经过多次的变换操作,为了多次变换间产生的影响,因此需要使用这两个函数
int secondHandR = radius_ - 20;
int minuteHandR = radius_ - 40;
int HourHandR = radius_ - 50;
QTime current_time =QTime::currentTime();
int hour = current_time.hour(); //当前的小时
int minute = current_time.minute(); //当前的分
int second = current_time.second(); //当前的秒
QPolygon pts;
painter.save();
pts.setPoints(4, -3, 0, 0, 5, 3, 0, 0, -HourHandR);
painter.setBrush(Qt::red);
painter.setPen(Qt::NoPen);
painter.rotate(hour * 30 + float(minute / 60.0) * 30.0);
painter.drawPolygon(pts);
painter.restore();
painter.save();
pts.setPoints(4, -2, 0, 0, 7, 2, 0, 0, -minuteHandR);
painter.setBrush(Qt::blue);
painter.setPen(Qt::NoPen);
painter.rotate(minute * 6 + float(second / 60.0) * 6.0);
painter.drawPolygon(pts);
painter.restore();
painter.save();
pts.setPoints(4, -1, 0, 0, 9, 1, 0, 0, -secondHandR);
painter.setBrush(Qt::black);
painter.setPen(Qt::NoPen);
painter.rotate(second * 6);
painter.drawPolygon(pts);
painter.restore();
}
解释一下主要的函数
-
QPolygon pts; 定义的是多边形的数据结构;
-
pts.setPoints(4, -3, 0, 0, 5, 3, 0, 0, -HourHandR); 第一个是点的个数,后面依次是对应点的xy值,使用这个可以绘制出自己想要的指针样式;
-
painter.setBrush(Qt::blue); 这里是设置的填充色, setPen设置的是边框颜色。
-
painter.rotate 上面介绍过了,这里是获取的旋转角度。
绘制背景
这里本来应该先绘制背景的,我们调用的时候也是绘制的背景先。但是我在写代码的时候是先写的指针看效果,然后给对应的指针加背景的,我这里是按照这个顺序来写的博客。
我们新增函数来绘制背景
void ClockPainter::drawBackground(QPainter &painter)
{
// 3.绘制外部的黑色的圈
int r = radius_ * 0.9;
painter.save();
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::black);//外围
painter.drawEllipse(-r, -r, r * 2, r * 2);
painter.restore();
// 2.绘制外部的颜色
r = radius_*0.88;
painter.save();
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::white);//外围
painter.drawEllipse(-r, -r, r * 2, r * 2);
painter.restore();
// 1.绘制指针背景色
r = radius_*0.1;
painter.save();
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(200, 200, 200));//外围
painter.drawEllipse(-r, -r, r * 2, r * 2);
painter.restore();
}
这里我们绘制一个圆和2个圆环,圆是给指针中心做背景的,第一个圆环覆盖所有的外框,第二个圆环显示时间值,思路是这样,但是绘制的时候需要反着绘制,先绘制大的,然后使用小的覆盖大的
绘制刻度线
接下来我们来绘制刻度线,刻度线没啥,就直接循环绘制即可
void ClockPainter::drawScale(QPainter &painter)
{
double radius = radius_*0.88;
painter.save();
int h = 0;
for(int i = 0; i < 60; i++)
{
if(i % 5 == 0)
{
QPen pen;
pen.setWidthF(1.5);
painter.setPen(pen);
painter.drawLine(0, - radius + 8, 0, -radius);
QFontMetrics fm(painter.font());
QString text;
h == 0? text = "12":text = QString::number(h);
h++;
int width = fm.width(text);
int height = fm.height();
painter.drawText(-width / 2.0, - radius + 8 + height, text);
}
else
{
QPen pen;
pen.setWidthF(0.8);
painter.setPen(pen);
painter.drawLine(0, - radius + 3, 0, -radius);
}
painter.rotate(6);
}
painter.restore();
}
我这个是效果简单的,但是万丈高楼平地起,如果你想写复杂的,需要先学会简单的。
完整代码
#ifndef PAINTERTEST_CLOCKPAINTER_H
#define PAINTERTEST_CLOCKPAINTER_H
#include <QTimer>
#include <QWidget>
#include <QColor>
class ClockPainter : public QWidget
{
Q_OBJECT
public:
explicit ClockPainter(QWidget *parent = nullptr);
~ClockPainter() override;
protected:
void paintEvent(QPaintEvent *event) override;
void drawHands(QPainter &painter);
void drawBackground(QPainter &painter);
void drawScale(QPainter &painter);
private:
int radius_{100};
QTimer timer_;
};
#endif //PAINTERTEST_CLOCKPAINTER_H
#include "clockpainter.h"
#include <QPainter>
#include <QTimer>
#include <QTime>
int degree = 0;
ClockPainter::ClockPainter(QWidget *parent)
: QWidget(parent)
{
connect(&timer_, &QTimer::timeout, [&](){
update();
});
timer_.start(1000);
}
ClockPainter::~ClockPainter()
{
}
void ClockPainter::paintEvent(QPaintEvent *event)
{
int width = this->width();
int height = this->height();
int side = qMin(width, height);
QPainter painter(this);
// 抗锯齿
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
// 将绘制原点移动到界面的中心点
painter.translate(width / 2, height / 2);
// 用(sx, sy)对坐标系进行缩放。(计算的比例使得缩放在一个合适的范围)
painter.scale(side / 200.0, side / 200.0);
drawBackground(painter);
drawScale(painter);
drawHands(painter);
}
void ClockPainter::drawHands(QPainter &painter)
{
// 保存当前的painter状态(将状态推入堆栈)。save()之后必须跟着相应的restore();end()函数的作用是展开堆栈。
// save 和restore 是为了保证在绘制复杂图形的时候绘制的准确性的。因为需要经过多次的变换操作,为了多次变换间产生的影响,因此需要使用这两个函数
int secondHandR = radius_ - 20;
int minuteHandR = radius_ - 40;
int HourHandR = radius_ - 50;
QTime current_time =QTime::currentTime();
int hour = current_time.hour(); //当前的小时
int minute = current_time.minute(); //当前的分
int second = current_time.second(); //当前的秒
QPolygon pts;
painter.save();
pts.setPoints(4, -3, 0, 0, 5, 3, 0, 0, -HourHandR);
painter.setBrush(Qt::red);
painter.setPen(Qt::NoPen);
painter.rotate(hour * 30 + float(minute / 60.0) * 30.0);
painter.drawPolygon(pts);
painter.restore();
painter.save();
pts.setPoints(4, -2, 0, 0, 7, 2, 0, 0, -minuteHandR);
painter.setBrush(Qt::blue);
painter.setPen(Qt::NoPen);
painter.rotate(minute * 6 + float(second / 60.0) * 6.0);
painter.drawPolygon(pts);
painter.restore();
painter.save();
pts.setPoints(4, -1, 0, 0, 9, 1, 0, 0, -secondHandR);
painter.setBrush(Qt::black);
painter.setPen(Qt::NoPen);
painter.rotate(second * 6);
painter.drawPolygon(pts);
painter.restore();
}
void ClockPainter::drawBackground(QPainter &painter)
{
// 这里我们绘制一个圆和2个圆环,圆是给指针中心做背景的,第一个圆环覆盖所有的外框,第二个圆环显示时间值,思路是这样,但是绘制的时候需要反着绘制,先绘制大的,然后使用小的覆盖大的
// 3.绘制外部的黑色的圈
int r = radius_ * 0.9;
painter.save();
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::black);//外围
painter.drawEllipse(-r, -r, r * 2, r * 2);
painter.restore();
// 2.绘制外部的颜色
r = radius_*0.88;
painter.save();
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::white);//外围
painter.drawEllipse(-r, -r, r * 2, r * 2);
painter.restore();
// 1.绘制指针背景色
r = radius_*0.1;
painter.save();
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(200, 200, 200));//外围
painter.drawEllipse(-r, -r, r * 2, r * 2);
painter.restore();
}
void ClockPainter::drawScale(QPainter &painter)
{
double radius = radius_*0.88;
painter.save();
int h = 0;
for(int i = 0; i < 60; i++)
{
if(i % 5 == 0)
{
QPen pen;
pen.setWidthF(1.5);
painter.setPen(pen);
painter.drawLine(0, - radius + 8, 0, -radius);
QFontMetrics fm(painter.font());
QString text;
h == 0? text = "12":text = QString::number(h);
h++;
int width = fm.width(text);
int height = fm.height();
painter.drawText(-width / 2.0, - radius + 8 + height, text);
}
else
{
QPen pen;
pen.setWidthF(0.8);
painter.setPen(pen);
painter.drawLine(0, - radius + 3, 0, -radius);
}
painter.rotate(6);
}
painter.restore();
}