Qt 5.14.1 analogClock程序画图解析 part03

本文详细解析了Qt 5.14.1中analogClock程序的实现,包括如何在`analogClock.h`和`analogClock.cpp`中重载`paintEvent`函数,构造函数中的定时器设置,以及如何利用`QPainter`进行图形绘制。文章涵盖了创建定时器、信号与槽的连接、窗口大小初始化、时针、分针和秒针的绘制等关键步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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);
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值