Qt示例-AnalogClock-自定义窗体-使用QPainter的转换和缩放特性简化绘图

摘要:
本示例是使用Qt的QPainter的转换和缩放特性简化绘图,绘制一个时钟,里面包含时针、分针、秒针、钟表刻度的绘制。
也包含计时器的使用,以及创建带有栅格表面的自定义窗口。

实现效果如图:

Clock.gif

源码位置:https://gitee.com/mao_zg/Analog_Clock

1、AnalogClock定义

首先,需要一个继承自QWindow的子类,来自定义一个窗口,当做一个画布,作为绘制的载体。

class AnalogClock : public QWindow
{
    Q_OBJECT

public:
    explicit AnalogClock(QWindow *parent = Q_NULLPTR);  

接着需要在这个自定义的窗体上面创建一个栅格。
QBackingStore允许使用QPainter在带有栅格表面的QWindow上进行绘制。另一种呈现QWindow的方法是使用OpenGLQOpenGLContext
QBackingStore包含窗口内容的缓冲表示,因此通过使用QPainter只更新窗口内容的一个子区域来支持部分更新。
QBackingStore也可以给想要使用QPainter,而不想使用OpenGL来绘制图形的应用程序使用。
而这个示例是要使用QPainter来进行绘图,所以我们需要一个QBackingStore的成员。

private:
QBackingStore* m_pBackingStore = nullptr;

钟表是需要动态去刷新和渲染的(因为时间是在变化的),所以需要重写QObject的一些事件处理函数。
注意:event事件处理函数,它会处理窗口所有的事件,所以当处理完自己需要的事件后,务必要调用基类的event函数,否则,窗口的其余事件都无法得到有效的执行

protected:
    bool event(QEvent* event) override;

在窗口改变大小的时候,也需要将绘制的图形重新按照新的窗体大小进行渲染,以保持随窗体变化。所以需要重写resizeEvent函数。
每当窗口在窗口系统中调整大小时,都会调用resize事件,
可以直接通过窗口系统确认setGeometry()resize()请求,也可以通过用户手动调整窗口大小来间接调用该事件。

void resizeEvent(QResizeEvent* event) override;

窗口还有一种需要渲染的事件,一种简单的情况就是被其他窗体遮挡后,又重新被启用或者是显示、激活等操作。
所以需要重写exposeEvent函数来处理类似这种情况的渲染操作。
每当窗口的某个区域失效时,窗口系统就会发送expose事件,例如由于窗口系统中的expose发生变化。
一旦获得一个如isexpose()为真的显现事件,应用程序就可以开始使用QBackingStoreQOpenGLContext将其呈现到窗口中。
如果将窗口移出屏幕,使其完全被另一个窗口遮挡,或被最小化,或类似的动作,则可能调用此函数,
isexpose()的值可能变为false。当这种情况发生时,应用程序应该停止显现,因为它对用户不再可见。

注意:在第一次显示窗口时,resize事件总是在expose事件之前发送。
与其关联使用的函数:QWindow::isExposed()

void exposeEvent(QExposeEvent* event) override;

因为时钟每秒都需要进行刷新渲染,所以还需要重写一个计时器,让它每隔1秒发一次事件,然后通过这个事件来渲染时钟的最新状态。

void timerEvent(QTimerEvent*) override;

在创建计时器时,还需要记录一个计时器标识,避免与其他的计时器事件产生混乱,但是本示例中的窗口只有一个活动的计时器事件,不需要进行区分的,不过这么做是一个好习惯。

int m_nTimerId = 0;

最后是其它的函数,主要是绘制功能的实现函数

    //渲染钟表函数
    void render(QPainter* pPainter);
    //renderLater函数会发送更新请求的事件,这个事件会触发绘制
    void renderLater();
    //绘制的执行函数
    void renderNow();
    //绘制时钟刻度
    void drawClockScale(QPainter* pPainter);

2、AnalogClock 实现

先是构造函数的实现。
主要动作:创建QBackingStore实例,设置窗口的初始位置以及宽度、高度
并且启动一个计时器事件,让其每隔1000毫秒(1秒)发出一次事件

AnalogClock::AnalogClock(QWindow *parent)
    :QWindow(parent),
    m_pBackingStore(new QBackingStore(this))
{
    setGeometry(200, 200, 400, 300); //设置窗口初始大小
    //启动计时器并返回计时器标识符,如果无法启动计时器则返回零。
    //每隔几毫秒就会发生一个计时器事件,直到调用killTimer()
    m_nTimerId = startTimer(1000);//每隔1秒发出计时器事件
}

接着实现重写的事件处理函数。
注意:event函数处理完以后,一定要调用基类的event函数

bool AnalogClock::event(QEvent* event)
{
    if (event->type() == QEvent::UpdateRequest) //事件类型为更新请求
    {
        //调用渲染函数
        renderNow();
        return true;
    }
    return QWindow::event(event);
}

void AnalogClock::resizeEvent(QResizeEvent* event)
{
    m_pBackingStore->resize(event->size());
}

void AnalogClock::exposeEvent(QExposeEvent* event)
{
    if (isExposed())
    {
        renderNow();
    }
}

void AnalogClock::timerEvent(QTimerEvent* event)
{
    if (m_nTimerId == event->timerId())
    {
        renderLater();
    }
}

AnalogClock::renderLater()函数主要调用requestUpdate
触发要传递到此窗口的QEvent::UpdateRequest事件。
在某些平台上,事件与显示同步发送。否则,事件将在延迟5毫秒后发送。
额外的时间用于为事件循环提供一些空闲时间来收集系统事件,可以使用QT_QPA_UPDATE_IDLE_TIME环境变量覆盖这些时间。

void AnalogClock::renderLater()
{
    requestUpdate();
}

AnalogClock::renderNow()函数为绘制的入口函数,
主要是绘制前的初始化动作,设置绘制区域,设置绘制区域的填充颜色,调用绘制钟表的函数render

paintDevice函数返回指定绘制表面的绘制设备。
警告:该设备只在调用beginPaint()和endPaint()之间有效。不要缓存返回的值。

把这个绘制设备实例,传给QPainter,用来创建它的实例
这个绘制设备的填充色是一个QGradient::Preset,此枚举定义了一组渐变色预设值,这个是在Qt5.12加入进来的
关于此枚举的详细说明,请参见这篇文章:https://www.jianshu.com/p/239b28e36e74

void AnalogClock::renderNow()
{
    if (!isExposed())
    {
        return;
    }

    QRect rect(0, 0, width(), height());
    m_pBackingStore->beginPaint(rect);

    QPaintDevice* pDevice = m_pBackingStore->paintDevice();
    QPainter oPainter(pDevice);

    /*
    用指定的画笔填充给定的矩形。
    也可以指定QColor而不是QBrush;QBrush构造函数(使用QColor参数)将自动创建一个实体模式笔刷。
    第二个参数为颜色的预设值,调好的颜色
    */
    oPainter.fillRect(rect, QGradient::KindSteel);
    //使用该画笔进行渲染
    render(&oPainter);
    oPainter.end();

    m_pBackingStore->endPaint();
    m_pBackingStore->flush(rect);
}

最后来看下绘制的实现。
首先设置一下渲染的样式或者是提示,使用函数setRenderHint
样式为:QPainter::Antialiasing,指示引擎应尽可能消除原语的边缘,这使得绘制对角线更加平滑
其他类型:

1. TextAntialiasing = 0x02
指示文本抗锯齿,使文本更平滑。若要强制禁用文本的抗锯齿,请不要使用此提示。相反,在字体的样式策略上设置QFont::NoAntialias
2. SmoothPixmapTransform = 0x04
指示引擎应该使用平滑的像素映射转换算法(如双线性)而不是最近邻。
3. HighQualityAntialiasing = 0x08
表示高质量的抗锯齿,不过此值已过时,将会被忽略,可以使用Antialiasing替换
4. NonCosmeticDefaultPen = 0x10
表示画笔默认是无修饰的,此值已过时,QPen的默认值现在就是非修饰性的。
5. Qt4CompatiblePainting = 0x20
兼容性提示,告诉引擎使用与Qt 4中相同的基于X11的填充规则,在Qt 4中,抗锯齿呈现被偏移了不到半个像素。也将默认构建的QPen作为修饰的。
在将Qt 4应用程序移植到Qt 5时可能非常有用。
6. LosslessImageRendering = 0x40
尽可能使用无损图像渲染。目前,这个指示只在使用QPainter通过QPrinterQPdfWriter输出PDF文件时使用,其中drawImage()/drawPixmap()调用将使用无损压缩算法对图像进行编码,而不是有损的JPEG压缩。这个值是在Qt 5.13中添加的。

pPainter->setRenderHint(QPainter::Antialiasing);

接着要用到QPainter的转换和缩放特性了。
translate()平移将原点移动到窗口的中心,缩放操作确保将接下来的绘图操作缩放到适合窗口的大小。
这里使用一个比例因子,使用x和y坐标在-100和100之间,保证绘制的图形在窗口最短边的范围内。

image.png

//通过向量(dx, dy)转换坐标系。
pPainter->translate(width() / 2.0, height() / 2.0);
int nSide = qMin(width(), height());
//缩放坐标系
pPainter->scale(nSide / 200.0, nSide / 200.0);

接着先实现时钟刻度线的绘制,主要包含小时、分钟(秒钟)的刻度线
时钟是一个圆形,小时为12,所以小时的每一个刻度线间隔30°,同理,分钟的每一个刻度线间隔为6°。
然后绘制分钟的刻度线的时候,要跳过5的倍数,因为这里是小时的刻度线,否则就会覆盖掉小时的刻度线

void AnalogClock::drawClockScale(QPainter* pPainter)
{
    QColor oHourColor(127, 0, 127);
    QColor oMinuteColor(0, 127, 127, 191);

    pPainter->setPen(oHourColor);
    for (int i = 0; i < 12; ++i)
    {
        pPainter->drawLine(88, 0, 96, 0);
        pPainter->rotate(30.0);
    }

    pPainter->setPen(oMinuteColor);
    for (int j = 0; j < 60; ++j)
    {
        //当绘制到5的倍数的时候,需要跳过去,避免覆盖到了时针刻度上
        if ((j % 5) != 0)
        {
            pPainter->drawLine(92, 0, 96, 0);
        }

        pPainter->rotate(6.0);
    }
}

最后就是时针、分针、秒针的绘制了。
setPen()Qt::NoPen,是为了绘制的时候不需要带有任何轮廓。
并使用了一个颜色适合显示小时的实体笔刷。画笔用于填充多边形和其他几何形状。

这里使用了一个公式,该公式将坐标系统逆时针旋转若干度,这些度由当前的小时和分钟决定
saverestore 为保存当前绘制工具的状态和恢复绘制工具保存前的状态。
目的是为了在绘制分针、秒针的时候,不需要考虑上一次的旋转矩阵的状态。

//绘制小时指针
static const QPoint oHourHand[3] = {
    QPoint(5,8),
    QPoint(-5,8),
    QPoint(0,-40)
};
QColor oHourColor(127, 0, 127);
pPainter->setPen(Qt::NoPen);
pPainter->setBrush(oHourColor);

QTime oTime = QTime::currentTime();
pPainter->save();//保存当前绘制工具的状态
double dRotate = 30.0 * (oTime.hour() + oTime.minute() / 60);
pPainter->rotate(dRotate);
pPainter->drawConvexPolygon(oHourHand, 3);
pPainter->restore(); //恢复绘制工具保存前的状态

//绘制分钟指针
static const QPoint oMinuteHand[3] = {
    QPoint(5,6),
    QPoint(-5,6),
    QPoint(0, -60)
};
QColor oMinuteColor(0, 127, 127, 191);
pPainter->setPen(Qt::NoPen);
pPainter->setBrush(oMinuteColor);
pPainter->save();
double dRotateMinute = 6.0 * (oTime.minute() + oTime.second() / 60);
pPainter->rotate(dRotateMinute);
pPainter->drawConvexPolygon(oMinuteHand, 3);
pPainter->restore();

//绘制秒针
static const QPoint oSecondHand[3] = {
    QPoint(3,3),
    QPoint(-3,3),
    QPoint(0,-80)
};

QColor oSecondColor(0, 0, 127, 210);
pPainter->setPen(Qt::NoPen);
pPainter->setBrush(oSecondColor);
pPainter->save();
double dRotateSecond = 6.0 * (oTime.second() + oTime.second() / 60);
pPainter->rotate(dRotateSecond);
pPainter->drawConvexPolygon(oSecondHand, 3);
pPainter->restore();

你可能感兴趣的:(Qt示例-AnalogClock-自定义窗体-使用QPainter的转换和缩放特性简化绘图)