analogClock画图解析
analogClock.h文件重载画图函数
paintEvent(QPaintEvent*)函数是QWidget类中的虚函数,用于ui的绘制。
由于定义的analogClock类的基类是QWidget类,因此,我们必须重载该函数。
首先,我们必须在analogClock.h文件中,添加重载函数,代码为:
protected:
void paintEvent(QPaintEvent *event) override;
Qt creator非常人性化,你只要输入protected:,然后输入void paintE,还没有等到你输入完,光标所在位置自动会显示出整个函数,选择paintEvent函数,回车即可。
analogClock.h的完整代码为:
#ifndef ANALOGCLOCK_H
#define ANALOGCLOCK_H
#include <QWidget>
class analogClock : public QWidget
{
Q_OBJECT
public:
analogClock(QWidget *parent = nullptr);
~analogClock();
protected:
void paintEvent(QPaintEvent *event) override;
};
#endif // ANALOGCLOCK_H
ananlogClock类的绘图所有语句,必须在paintEvent函数中给出。
当窗口有重绘事件发生时,Qt自动重绘窗口。大部分重绘事件为:
(1)repaint()函数或者update()函数被调用
(2)被隐藏的部件被重新显示
(3)初始化窗口
注意:
(1)Qt会通过合并多个重绘事件为一个事件,目的是用来加速绘制。例如,当update()函数被多次调用时,或者窗口系统发送了多个重绘事件,那么Qt会合并这些事件为一个事件。update()函数不会立即进行重绘。update()函数允许Qt优化速度和减少闪烁。
(2)repaint()函数调用时,会立即调用paintEvent()函数。
(3)当重绘事件发生时,要更新的区域,一般会被擦除,然后在部件的背景上进行重新绘制。
analogClock.cpp代码
在analogClock.h文档中,把光标移到paintEvent函数所在的行,按alt+Enter,Qt Creator自动在analogClock.cpp文件中添加paintEvent函数的定义。
构造函数添加代码
在analogClock类的构造函数中,我们添加如下代码:
analogClock::analogClock(QWidget *parent)
: QWidget(parent)
{
// 定义一个定时器,这里用指针
QTimer *timer = new QTimer(this);
// 利用connect将定时器的timeout()信号与对应槽函数连接
// 此处的槽函数为:QOverload<>of(&AnalogClock::update))
connect(timer,
&QTimer::timeout,
this,
QOverload<>::of(&analogClock::update));
// 1000 ms 也就是1s发送一个信号
timer->start(100);
// 程序标题
setWindowTitle(tr("师老师设计的时钟 2020a"));
// 初始化应用程序的大小400像素*400像素
resize(400,400);
}
用于这里用到了定时器,因此,必须在程序前面添加包含头文件,即
#include <QTimer>
代码解析:
(1)创建一个定时器类的对象
代码为:
QTimer *timer = new QTimer(this);
注意这里采用定时器类指针 QTimer *timer,也采用new操作符。注意这里虽然用new操作符新建QTimer类的指针,不用在析构函数中用delete操作符删除指针,Qt在程序退出的时候,自动销毁内存。这是Qt的特性。
(2)建立信号与槽函数
timer超时后,会发出信号timeout(),因此,必须在创建好的定时器对象后,给其建立信号与槽函数。
Qt传统方法为:
connect(timer,
SIGNAL(timeout()),
this,
SLOT(onTimeout()));
connect()函数有4个参数,第1个参数是timer对象指针;第3个参数是this指针;第2个参数是timer对象发出的信号,SIGNAL表示信号;第4个参数是槽函数,SLOT表示槽。
Qt 5.14.0中的方法有所不同,代码如下:
connect(timer,
&QTimer::timeout,
this,
QOverload<>::of(&analogClock::update));
首先,省略了SIGNAL和SLOT。
其次,用&QTimer::timeout替代原来的timeout(),语法上更为严谨,代码可读性更强。
最后,槽函数用&analogClock::update,前面还用了QOverload重载函数。该函数返回一个指向重载函数的指针。
qOverload()需要C++14的支持。在C++11风格的代码中,QOverload的用法为:
... QOverload<>::of(&Foo::overloadedFunction)
... QOverload<int, QString>::of(&Foo::overloadedFunction)
要通过函数指针的语法连接该信号,就可以借助qOverload类获取函数指针,这也是Qt官方文档推荐的连接方式。
(3)开启定时器
在需要的地方,开启定时器,采用现在代码:
timer->start(100);
start()函数参数是毫秒,这里是100ms发送一次timeout信号。
(4)修改程序标题
程序标题的修改非常简单,代码为:
setWindowTitle(tr("师老师设计的时钟 2020a"));
注意,这里采用setWindowTitle()函数,参数用tr()函数,里面是字符串,可以中英文混合。
(5)初始化analogClock窗口大小
程序一运行,我们希望初始化analogClock窗口的大小,代码为:
resize(400,400);
这里采用resize()函数,设置宽和高均为400个像素。
paintEvent函数代码
在paintEvent函数中添加如下代码:
void analogClock::paintEvent(QPaintEvent *event)
{
// 定义一个QPoint常量数组
static const QPoint hourHand[3] = {
QPoint(7,8),
QPoint(-7,8),
QPoint(0,-40)
};
static const QPoint minuteHand[3] = {
QPoint(5,8),
QPoint(-5,8),
QPoint(0,-70)
};
static const QPoint secondHand[3] = {
QPoint(3,8),
QPoint(-3,8),
QPoint(0,-88)
};
// 定义时针,分针,秒针的颜色
QColor hourColor(127,0,127);
QColor minuteColor(0,127,127,191);
QColor secondColor(0,0,0,127);
// 求应用窗口高度和宽度的最小值
int side = qMin(width(),height());
//qDebug() << side;
// 定义一个时间类,捕捉当前时间
QTime time = QTime::currentTime();
// 定义一个日期类,捕捉当前日期
QDate date = QDate::currentDate();
// 开始画图
QPainter painter(this);
// 去锯齿
painter.setRenderHint(QPainter::Antialiasing);
// 将坐标原点(0,0)平移到窗口中心
painter.translate(width()/2,height()/2);
// 比例因子
painter.scale(side/200.0,side/200.0);
// 画时针 Qt::NoPen表示没有轮廓
painter.setPen(Qt::NoPen);
painter.setBrush(hourColor);
painter.save();
// 每小时是30°,
// paintr.rotate(qreal angle),其中angle是以度为单位
// 顺时针旋转
painter.rotate(30.0 *((time.hour()+time.minute()/60.0)));
// 画多边形,就画出了时针
painter.drawConvexPolygon(hourHand,3);
painter.restore();
// 画表盘上的小时
painter.setPen(hourColor);
for(int i=0;i<12;++i){
painter.drawLine(88,0,96,0);
painter.rotate(30.0);
}
// 画分钟
painter.setPen(Qt::NoPen);
painter.setBrush(minuteColor);
painter.save();
// 每分钟是6°吗?360/60 = 6度
painter.rotate(6.0*(time.minute()+ time.second()/60.0));
painter.drawConvexPolygon(minuteHand,3);
painter.restore();
// 画表盘上的分钟
painter.setPen(minuteColor);
for(int j=0;j<60;++j){
if((j%5) != 0) {
painter.drawLine(92,0,96,0);
}
painter.rotate(6.0);
}
// 我添加的画秒钟
painter.setPen(Qt::NoPen);
painter.setBrush(secondColor);
painter.save();
// 每秒钟是6°吗?360/60 = 6度
painter.rotate(6.0*(time.second()+time.msec()/1000.0));
painter.drawConvexPolygon(secondHand,3);
painter.restore();
painter.setPen(QPen(Qt::black,2,Qt::SolidLine,Qt::RoundCap));
painter.setBrush(Qt::green);
painter.drawEllipse(QRect(-2,-2,5,5));
painter.setPen(QPen(Qt::black,3,Qt::SolidLine,Qt::RoundCap));
painter.setBrush(Qt::NoBrush);
painter.drawEllipse(QRect(-96,-96,192,192));
//设置字体的类型,大小,加粗
QFont font("Arial",8,QFont::Bold);
//设置间距
font.setLetterSpacing(QFont::AbsoluteSpacing,1);
// 添加字体
painter.setFont(font);
QFontMetricsF fontMetrics(font);
// 写上时间
QString stemp;
stemp = QString::asprintf("%02d:%02d:%02d",
time.hour(),
time.minute(),
time.second());
qreal stempwid = fontMetrics.width(stemp);
qreal stempheight = fontMetrics.height();
qint32 nstemp = stemp.length();
//qDebug() << stempwid << stempheight << nstemp;
// 显示时间字符串
painter.drawText(-stempwid/2,3*stempheight,stemp);
// 显示日期字符串
stemp = QString::asprintf("%4d年%02d月%02d日",
date.year(),
date.month(),
date.day());
stempwid = fontMetrics.width(stemp);
painter.drawText(-stempwid/2,2*stempheight,stemp);
// 显示日期字符串
switch(date.dayOfWeek())
{
case 1:
stemp = QString::asprintf("星期一");
break;
case 2:
stemp = QString::asprintf("星期二");
break;
case 3:
stemp = QString::asprintf("星期三");
break;
case 4:
stemp = QString::asprintf("星期四");
break;
case 5:
stemp = QString::asprintf("星期五");
break;
case 6:
stemp = QString::asprintf("星期六");
break;
case 7:
stemp = QString::asprintf("星期日");
break;
}
stempwid = fontMetrics.width(stemp);
painter.drawText(20,5,stemp);
}
pantEvent()函数代码解析
(1)定义三个static const 局部变量数组hourHand[3],minuteHand[3],secondHand[3]
为时针、分针、表针定义3个QPoint常量,代码为:
static const QPoint hourHand[3] = {
QPoint(7,8),
QPoint(-7,8),
QPoint(0,-40)
};
const简介
C++中用const修饰过后,就变成常量。
C++中的const关键字的用法非常灵活,使用const将大大改善程序的健壮性。
const 是C++中常用的类型修饰符,常类型是指使用类型修饰符const说明的类型,常类型的变量或对象的值是不能被更新的。
const可以节省空间,避免不必要的内存分配。const定义的常量在程序运行过程中只有一份拷贝。
const提高了效率,编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
对于const变量,在类内声明必须初始化。
const int SIZE2 = 9; // const类型,声明时必须设定初始值
const double pii = 2.6; // const类型,声明时必须设定初始值
const直观的理解就是常量且不可修改。但是const的意义远不止如此:
const int a=30;
常量:a必须赋值,不赋值是不被允许的。
试想如果不赋值,又因为const不可修改性,那么a定义有意义吗?这也是const变量必须赋予初始值得直接原因。
static简介
static:
不考虑类,static的作用:
(1)第一个作用:隐藏。使得全局变量和函数对其它文件不可见,同时避免了不同文件的命名冲突。
(2)第二个作用:默认初始化为0。未初始化的全局静态变量和局部静态变量都保存在BBS段,BBS段的特点是,程序运行之前会自动清零。
(3)第三个作用:保持局部变量内容的持久性。此变量声明周期是整个程序的声明周期,但是作用域只在声明它的函数中。
静态static局部变量
在局部变量前,加上关键字static,该变量就被定义成为一个静态局部变量。
通常,在函数体内定义了一个变量,每当程序运行到该语句时都会给该局部变量分配栈内存。但随着程序退出函数体,系统就会收回栈内存,局部变量也相应失效。
但有时候我们需要在两次调用之间对变量的值进行保存。通常的想法是定义一个全局变量来实现。但这样一来,变量已经不再属于函数本身了,不再仅受函数的控制,给程序的维护带来不便。
静态局部变量正好可以解决这个问题。静态局部变量保存在全局数据区,而不是保存在栈中,每次的值保持到下一次调用,直到下次赋新值。
静态局部变量有以下特点:
该变量在全局数据区分配内存;
静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;
静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0;
它始终驻留在全局数据区,直到程序运行结束。但其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束;
static const 变量简介
(1)static const 与const static是一样的,没有区别。
(2)static const存储在静态存储区域,不可修改其值的常量,可用来替代define或enum
(3)所有类型的变量都可以声明为static const
(4)将变量声明为static const,是为了实际需要,为了设定一个在整个class范围内都不变的常量。
C++11标准中引进了新的初始化方式:用元素初始化器的花括号列表:
int *p = new int[10]{0,1,2,3,4,5,6,7,8,9};
string *p2 = new string[10]{"a","an","the",string(3,'x')};
综上分析,时针数组hourHand[3]数组,被定义为static const数组,并进行了初始化。
const static常量存在于内存的常量区,有操作系统加载程序时,加载到内存的常量区。后面不再修改,加快了程序的执行速度和效率,也减小了内存开销。
minuteHand[3],secondHand[3]数组的代码同上解释。
注意,数组保存的是多变形的3个点的坐标,单位是像素。
定义时针分针秒针颜色
时针,分针,秒针的颜色,采用QColor类变量,代码为:
QColor hourColor(127,0,127);
QColor minuteColor(0,127,127,191);
QColor secondColor(0,0,0,127);
这里,采用Qt的QColor类来定义三个对象(变量)。
QColor类提供基于RGB、HSV或CMYK值的颜色。颜色通常用RGB(红色、绿色和蓝色)组件指定,但也可以用HSV(色相、饱和度和值)和CMYK(青色、品红、黄色和黑色)组件指定。此外,可以使用颜色名称指定颜色。颜色名称可以是SVG 1.0的任何颜色名称。
这里采用QColor的这种用法
QColor ( int r, int g, int b, int a = 255 )
前3个整数分别是红,绿,蓝的颜色值,范围是0-255;
最后1个整数是透明度,范围是0-255,数值0表示完全透明,数值255表示不透明。缺省值是255。
窗口的宽度和高度最小值
由于想设计的钟是一个圆形,因此,我们必须求取当前窗口宽度和高度的最小值,以像素为单位,代码为:
int side = qMin(width(),height());
这里,用到width()和height()函数,还用到qMin()函数来求最小值。
程序刚开始运行时,由于设置窗口是400*400像素,因此,其值开始是side = 400。
但是,用户如果鼠标改变窗口大小,其值也在变化。
定义时间和日期类变量
利用Qt的QTime类和QDate类,定义一个时间变量和一个日期变量。同时,获取当前时间和日期。代码为:
QTime time = QTime::currentTime();
QDate date = QDate::currentDate();
currentTime()函数和currentDate()函数,可以获取当前时间和日期。
QPainter类开始画图
paintEvent()函数的画图,采用QPainter类变量开始,用法为:
QPainter painter(this);
也就是定义一个QPainter对象painter,注意里面初始化指针为this。
后面所有画图都在painter基础上进行绘制。
去锯齿
为使画的线条和图形圆滑,采用Qt的去锯齿函数,代码为:
painter.setRenderHint(QPainter::Antialiasing);
setRenderHint()函数,里面的参数为QPainter::Antialiasing,就可以自动去锯齿。
坐标原点平移
我们的时钟,原点在窗口中心,因此,采用平移函数,将原点平移到窗口中心,代码为:
painter.translate(width()/2,height()/2);
translate()函数,将原本原点在窗口左上角为(0,0),平移到窗口的中心(width()/2,height()/2)。
注意,这里是以像素为单位的。
窗口比例
Qt中的绘图函数,既可以用像素,也可以用实数。
我们的时针,分针,秒针的多边形数据,是以像素为单位的。因此,我们在绘图的时候,需要将这个数据,进行比例运算,使得窗口在放大或缩小的时候,我们绘制的时针,分针,秒针同步放大和缩小。
因此,采用下面的代码:
painter.scale(side/200.0,side/200.0);
scale()函数,实时设置绘图的比例。因为side变量,是根据窗口的大小变化而变化的,所以scale()函数里面的比例,也是随着窗口的缩放而缩放。
这样做的好处是:我们后面的绘图函数,只需要按照初始设置的像素值进行绘制,而不需要考虑窗口的大小。当窗口大小发生改变时,scale()函数负责进行比例缩放。
设置画笔
Qt的设置画笔,采用setPen()函数,代码为:
painter.setPen(Qt::NoPen);
这里采用Qt::NoPen,表示无笔,即不画轮廓线。
设置画刷
Qt的设置画刷,采用setBrush()函数,代码为:
painter.setBrush(hourColor);
这里,采用setBrush(hourColor),即后面画图,若是多边形,则采用hourColor颜色进行填充。
保存画图设置
Qt绘图的时候,可以保存当前的画图设置,即笔的颜色,画刷颜色等等。代码为:
painter.save();
save()函数,把当前画图的各种参数,压入堆栈中。后面可以通过restore()函数来回复画图的各种参数。
旋转角度
旋转角度,通过rotate()函数来实现,代码为:
painter.rotate(30.0 *((time.hour()+time.minute()/60.0)));
rotate()函数的参数是以度为单位,整数表示顺时针方向的角度。
这里,通过time.hour()函数获取当前时间的小时值(0-23小时),通过time.minute()函数获取当前时间的分钟值(0-59分钟),分钟除以60就转换为小时值。每个小时在表盘上是30度。
当前代码,就是把画布方向顺时针旋转到当前时针的方向。
画时针
画时针,通过画多边形函数drawConvexPolygon()函数来完成,代码为:
painter.drawConvexPolygon(hourHand,3);
代码非常容易理解。
drawConvexPolygon()是画凸多变形,
drawPolygon()是画多变形。
恢复画图参数
刚才画时针之前,我们保存了画图现场参数。在画时针的时候,我们根据当前的时间,把画布顺时针旋转了一个角度。现在如果不恢复到画时针之前的画图参数,后面画图函数,都是在当前旋转角度的基础上进行绘制。这样绘制的图形就可能失真甚至是出错。
因此,我们可以用Qt的restore()函数,来恢复原来的画图现场。代码为
painter.restore();
画时针刻度
时针虽然画出来了。但是钟表盘的圆圈上,应该有12个时针的刻度,采用下面代码来完成。
painter.setPen(hourColor);
for(int i=0;i<12;++i){
painter.drawLine(88,0,96,0);
painter.rotate(30.0);
}
这里,首先设置颜色为时针颜色setPen(hourColor)。
然后,采用1个循环,共有12个时针刻度,每个刻度,采用画线函数drawLine(88,0,96,0)来画一个小线段。drawLine()函数的参数是两个点的x,y坐标。因此,线段长度为6。如果用户没有改变窗口大小,那么时针刻度长度就是6个像素。如果缩小了表盘,则时针刻度小于6个像素;如果放大了表盘,则时针刻度大于6个像素。这个工作是painter.scale()函数来自动完成的。
每画一个时针刻度,采用painter.rotate(30.0)函数顺时针转30度。
最后,旋转的角度,正好为360度。360度等于0度。
分针和分针刻度画图
分针和分针刻度的画图程序代码解析,基本与时针相同,代码为:
// 画分钟
painter.setPen(Qt::NoPen);
painter.setBrush(minuteColor);
painter.save();
// 每分钟是6°吗?360/60 = 6度
painter.rotate(6.0*(time.minute()+ time.second()/60.0));
painter.drawConvexPolygon(minuteHand,3);
painter.restore();
// 画表盘上的分钟
painter.setPen(minuteColor);
for(int j=0;j<60;++j){
if((j%5) != 0) {
painter.drawLine(92,0,96,0);
}
painter.rotate(6.0);
}
注意:
(1)画分钟时,要提取分和秒的时间信息;
(2)分钟刻度有60个,每分钟6度。
(3)分钟刻度不要覆盖了时钟刻度。因此程序中有一个求余操作。
秒钟画图
秒钟的画图,与时钟和分钟基本相同,代码为:
painter.setPen(Qt::NoPen);
painter.setBrush(secondColor);
painter.save();
// 每秒钟是6°吗?360/60 = 6度 painter.rotate(6.0*(time.second()+time.msec()/1000.0));
painter.drawConvexPolygon(secondHand,3);
painter.restore();
秒钟的刻度同分钟刻度,因此,不用画。
钟表上写上日期时间和星期
电子钟的表盘上,我们希望写上日期,时间和星期。具体代码为:
painter.setPen(QPen(Qt::black,2,Qt::SolidLine,Qt::RoundCap));
painter.setBrush(Qt::green);
painter.drawEllipse(QRect(-2,-2,5,5));
painter.setPen(QPen(Qt::black,3,Qt::SolidLine,Qt::RoundCap));
painter.setBrush(Qt::NoBrush);
painter.drawEllipse(QRect(-96,-96,192,192));
//设置字体的类型,大小,加粗
QFont font("Arial",8,QFont::Bold);
//设置间距
font.setLetterSpacing(QFont::AbsoluteSpacing,1);
// 添加字体
painter.setFont(font);
QFontMetricsF fontMetrics(font);
// 写上时间
QString stemp;
stemp = QString::asprintf("%02d:%02d:%02d",
time.hour(),
time.minute(),
time.second());
qreal stempwid = fontMetrics.width(stemp);
qreal stempheight = fontMetrics.height();
qint32 nstemp = stemp.length();
//qDebug() << stempwid << stempheight << nstemp;
// 显示时间字符串
painter.drawText(-stempwid/2,3*stempheight,stemp);
// 显示日期字符串
stemp = QString::asprintf("%4d年%02d月%02d日",
date.year(),
date.month(),
date.day());
stempwid = fontMetrics.width(stemp);
painter.drawText(-stempwid/2,2*stempheight,stemp);
// 显示星期字符串
switch(date.dayOfWeek())
{
case 1:
stemp = QString::asprintf("星期一");
break;
case 2:
stemp = QString::asprintf("星期二");
break;
case 3:
stemp = QString::asprintf("星期三");
break;
case 4:
stemp = QString::asprintf("星期四");
break;
case 5:
stemp = QString::asprintf("星期五");
break;
case 6:
stemp = QString::asprintf("星期六");
break;
case 7:
stemp = QString::asprintf("星期日");
break;
}
stempwid = fontMetrics.width(stemp);
painter.drawText(20,5,stemp);