配套视频讲解:https://www.bilibili.com/video/BV1uf4y1U7aa?p=10
Analog Clock Window 示例
Analog Clock Window 示例展示了如何在一个自定义的window上绘制内容。
这个例子使用了QPainter的位移、缩放、旋转特性,这些功能都是对计算机图形学算法的封装,相关的矩阵对用户来说都是透明的,使用起来非常方便。
概述
本示例展示了如何使用QPainter在QWindows绘制一个时钟。时钟的大小会随着窗口大小的变化而自动匹配。本示例是基于另一个GUI 示例基础之上完成的。所以这里我们一共会讲两个示例程序。Analog Clock Window Example 和Raster Window Example ,如下图所示(注意颜色的匹配):
简单来说Raster Window 示例展示的是使用QPainter在QWindow是进行最基础的静态的绘制。而Analog Clock Window 示例主要展示的是图元的绘制,以及动态的更新。
如上图所示,蓝色部分都是一样的,是两个项目的公用部分。可以看出,需要了解Analog Clock Window示例,需要先了解Raster Window示例。那下面我们先介绍一下Raster Window 示例。
Raster Window 示例
该示例展示如何使用QPainter在QWindows上进行简单的绘制。
RasterWindow 声明
//土豪式头文件加入法。也可以细一点。
//还有一种更土豪的写法<QtWidgets>,包含Qt Core, Qt GUI 和 Qt Widgets模块,但需要记得在pro文件中加入支持,例如:QT += widgets
#include <QtGui>
class RasterWindow : public QWindow
{
Q_OBJECT
public:
explicit RasterWindow(QWindow *parent = 0);
//renderNow绘制流程的一个步骤
virtual void render(QPainter *painter);
public slots:
//在本示例中没有用到,放到Analog Clock Window 再讲
void renderLater();
//执行绘制流程
void renderNow();
protected:
//配合renderLater()使用,本例中也没有用到。
bool event(QEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void exposeEvent(QExposeEvent *event) override;
private:
//QBackingStore是QT5新加入的类。有了他,Qt就能像OpenGL一样在窗口上进行灵活的绘制。
//用于管理窗口的back buffer(前后缓冲,交替显示,以保证呈现的是整幅画面,而不是绘制过程)
//绘制中的缓冲就是back buffer,显示中的就是front buffer。枚帧进行一次swap
//QBackingStore使用的是栅格化方式(RasterSurface)
//栅格这个词在地图纹理中经常出现,可以百度一下细节。简单来说就是把图片按色块组织起来。
//栅格图又称位图,放大会失真(例如:地图纹理)。
//矢量图基于数据绘制,缩放后会根据数据重新绘制,不会失真(例如:字体,对的字体也是图)
QBackingStore *m_backingStore;
};
RasterWindow 实现
#include "rasterwindow.h"
//在构造函数中创建backingstore,并将它需要管理的对象实例传给它。
RasterWindow::RasterWindow(QWindow *parent)
: QWindow(parent)
, m_backingStore(new QBackingStore(this))
{
//(int posx, int posy, int w, int h)
setGeometry(100, 100, 300, 200);
}
//负责处理更新事件。
bool RasterWindow::event(QEvent *event)
{
if (event->type() == QEvent::UpdateRequest) {
renderNow();
return true;
}
return QWindow::event(event);
}
//有些情况我们并不需要立即绘制。
void RasterWindow::renderLater()
{
//QWindow::requestUpdate(),告诉系统,窗口需要更新,让系统自己决定何时更新。
//会产生一个QEvent::UpdateRequest事件
requestUpdate();
}
void RasterWindow::resizeEvent(QResizeEvent *resizeEvent)
{
//确保窗口的back buffer大小与窗口一致
m_backingStore->resize(resizeEvent->size());
}
//当窗口的显示状态发生变化的时候,更新绘制。
void RasterWindow::exposeEvent(QExposeEvent *)
{
//只有在没有遮挡的情况下才需要重新绘制。
if (isExposed())
renderNow();
}
void RasterWindow::renderNow()
{
//如果有遮挡则直接返回
if (!isExposed())
return;
//(int posx, int posy, int w, int h)
QRect rect(0, 0, width(), height());
m_backingStore->beginPaint(rect);
//QPainter是一个画家,QPaintDevice是画布。
QPaintDevice *device = m_backingStore->paintDevice();
QPainter painter(device);
//通过改变fillRect的值可以绘制不同的区域。
//fillRect(50, 50, width()-100, height()-100, QGradient::NightFade);外围的50不会被绘制。
painter.fillRect(0, 0, width(), height(), QGradient::NightFade);
//软件设计者的结构设计,RasterWindow只绘制背景,以及创建并设置一个QPainter对象
//具体业务的绘制,放到其他函数。这里render使用这里创建好的Painter绘制了一个文字
render(&painter);
painter.end();
m_backingStore->endPaint();
m_backingStore->flush(rect);
}
//render被renderNow调用,可以被子类重载,实现模板方法设计模式
//模板方法设计模式:在一个固定的流程里(renderNow是稳定的部分,render是变化的部分)。
//RasterWindow作为主类,并不知道子类需要绘制什么内容,所以使用虚函数render,让子类重载
void RasterWindow::render(QPainter *painter)
{
painter->drawText(QRectF(0, 0, width(), height()),
Qt::AlignCenter, QStringLiteral("QWindow"));//QStringLiteral更高效的QString宏
}
这里采用的是模板方法设计模式,RasterWindow::renderNow不但是完成里一些基础的工作,还负责了整个绘制工作的流程,也就是建立了骨架,并将一些具体的步骤延迟到了子类来实现(晚绑定)。
回到Analog Clock Window 示例
基于Raster Window 示例中的RasterWindow类,创建AnalogClockWindow类。重载 render 函数,绘制时钟表面。从这里就能看出软件设计者的用心了,render只负责具体业务的绘制,背景和准备工作在RasterWindow::renderNow中完成。我们重载的AnalogClockWindow;:render会被RasterWindow::renderNow函数调用。类图如下所示:
AnalogClockWindow 类声明
#include <QtGui>
#include "rasterwindow.h"
class AnalogClockWindow : public RasterWindow
{
public:
AnalogClockWindow();
protected:
void timerEvent(QTimerEvent *) override;
void render(QPainter *p) override;
private:
int m_timerId;
};
AnalogClockWindow 类实现
AnalogClockWindow::AnalogClockWindow()
{
setTitle("Analog Clock");
resize(200, 200);
//启动一个定时器,每秒钟发出一个timerEvent。m_timerId为int类型,是该定时器的ID
m_timerId = startTimer(1000);
}
void AnalogClockWindow::timerEvent(QTimerEvent *event)
{
//m_timerId定时器开启后,每秒会进入一次。
if (event->timerId() == m_timerId)
//renderLater调用QWindow::requestUpdate(),告诉系统,窗口需要更新,但无需立即更新。
renderLater();
}
//模板方法设计模式中的变化步骤。
void AnalogClockWindow::render(QPainter *p)
{
//三角形的三个点坐标数据,目前原点还在左上角。后面会移动到窗口中心。
//注意QWindow坐标里Y轴是反的,负数表示朝上。
static const QPoint hourHand[3] = {
QPoint(7, 8),
QPoint(-7, 8),
QPoint(0, -40)
};
static const QPoint minuteHand[3] = {
QPoint(7, 8),
QPoint(-7, 8),
QPoint(0, -70)
};
QColor hourColor(127, 0, 127);
QColor minuteColor(0, 127, 127, 191);
//开启抗锯齿。
p->setRenderHint(QPainter::Antialiasing);
//所有的点,都进行一次位移。也就是将整个图片的原点从左上角,移动到中心。
p->translate(width() / 2, height() / 2);
//根据窗口大小缩放。图形缩放都是对点的操作,背后都有矩阵参与计算。
int side = qMin(width(), height());
p->scale(side / 200.0, side / 200.0);
//不需要边框,只需要填充。
p->setPen(Qt::NoPen);
p->setBrush(hourColor);
QTime time = QTime::currentTime();
p->save();//保存当前上下文
//根据时间值,计算旋转的度数。
p->rotate(30.0 * ((time.hour() + time.minute() / 60.0)));
p->drawConvexPolygon(hourHand, 3);
p->restore();//回到保存的上下文
p->setPen(hourColor);
//小时的刻度线
for (int i = 0; i < 12; ++i) {
p->drawLine(88, 0, 96, 0);
p->rotate(30.0);
}
p->setPen(Qt::NoPen);
p->setBrush(minuteColor);
p->save();
p->rotate(6.0 * (time.minute() + time.second() / 60.0));
p->drawConvexPolygon(minuteHand, 3);
p->restore();
p->setPen(minuteColor);
//分钟的刻度线
for (int j = 0; j < 60; ++j) {
if ((j % 5) != 0)
p->drawLine(92, 0, 96, 0);
p->rotate(6.0);
}
}
加上秒针
在render函数中,按同样的方法设置顶点,颜色。添加旋转代码即可。