前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Qt示例-AnalogClock-自定义窗体-使用QPainter的转换和缩放特性简化绘图

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

作者头像
Sky_Mao
发布2020-07-23 21:52:07
2K0
发布2020-07-23 21:52:07
举报

摘要:

本示例是使用Qt的QPainter的转换和缩放特性简化绘图,绘制一个时钟,里面包含时针、分针、秒针、钟表刻度的绘制。

也包含计时器的使用,以及创建带有栅格表面的自定义窗口。

实现效果如图:

Clock.gif

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

1、AnalogClock定义

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

代码语言:javascript
复制
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的成员。

代码语言:javascript
复制
private:
QBackingStore* m_pBackingStore = nullptr;

钟表是需要动态去刷新和渲染的(因为时间是在变化的),所以需要重写QObject的一些事件处理函数。

注意:event事件处理函数,它会处理窗口所有的事件,所以当处理完自己需要的事件后,务必要调用基类的event函数,否则,窗口的其余事件都无法得到有效的执行

代码语言:javascript
复制
protected:
    bool event(QEvent* event) override;

在窗口改变大小的时候,也需要将绘制的图形重新按照新的窗体大小进行渲染,以保持随窗体变化。所以需要重写resizeEvent函数。

每当窗口在窗口系统中调整大小时,都会调用resize事件,

可以直接通过窗口系统确认setGeometry()resize()请求,也可以通过用户手动调整窗口大小来间接调用该事件。

代码语言:javascript
复制
void resizeEvent(QResizeEvent* event) override;

窗口还有一种需要渲染的事件,一种简单的情况就是被其他窗体遮挡后,又重新被启用或者是显示、激活等操作。

所以需要重写exposeEvent函数来处理类似这种情况的渲染操作。

每当窗口的某个区域失效时,窗口系统就会发送expose事件,例如由于窗口系统中的expose发生变化。

一旦获得一个如isexpose()为真的显现事件,应用程序就可以开始使用QBackingStoreQOpenGLContext将其呈现到窗口中。

如果将窗口移出屏幕,使其完全被另一个窗口遮挡,或被最小化,或类似的动作,则可能调用此函数,

isexpose()的值可能变为false。当这种情况发生时,应用程序应该停止显现,因为它对用户不再可见。

注意:在第一次显示窗口时,resize事件总是在expose事件之前发送。

与其关联使用的函数:QWindow::isExposed()

代码语言:javascript
复制
void exposeEvent(QExposeEvent* event) override;

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

代码语言:javascript
复制
void timerEvent(QTimerEvent*) override;

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

代码语言:javascript
复制
int m_nTimerId = 0;

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

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

2、AnalogClock 实现

先是构造函数的实现。

主要动作:创建QBackingStore实例,设置窗口的初始位置以及宽度、高度

并且启动一个计时器事件,让其每隔1000毫秒(1秒)发出一次事件

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

接着实现重写的事件处理函数。

注意:event函数处理完以后,一定要调用基类的event函数

代码语言:javascript
复制
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环境变量覆盖这些时间。

代码语言:javascript
复制
void AnalogClock::renderLater()
{
    requestUpdate();
}

AnalogClock::renderNow()函数为绘制的入口函数,

主要是绘制前的初始化动作,设置绘制区域,设置绘制区域的填充颜色,调用绘制钟表的函数render

paintDevice函数返回指定绘制表面的绘制设备。

警告:该设备只在调用beginPaint()和endPaint()之间有效。不要缓存返回的值。

把这个绘制设备实例,传给QPainter,用来创建它的实例

这个绘制设备的填充色是一个QGradient::Preset,此枚举定义了一组渐变色预设值,这个是在Qt5.12加入进来的

关于此枚举的详细说明,请参见这篇文章:https://cloud.tencent.com/developer/article/1667881

代码语言:javascript
复制
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::NoAntialias2. 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中添加的。

代码语言:javascript
复制
pPainter->setRenderHint(QPainter::Antialiasing);

接着要用到QPainter的转换和缩放特性了。

translate()平移将原点移动到窗口的中心,缩放操作确保将接下来的绘图操作缩放到适合窗口的大小。

这里使用一个比例因子,使用x和y坐标在-100和100之间,保证绘制的图形在窗口最短边的范围内。

image.png

代码语言:javascript
复制
//通过向量(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的倍数,因为这里是小时的刻度线,否则就会覆盖掉小时的刻度线

代码语言:javascript
复制
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 为保存当前绘制工具的状态和恢复绘制工具保存前的状态。

目的是为了在绘制分针、秒针的时候,不需要考虑上一次的旋转矩阵的状态。

代码语言:javascript
复制
//绘制小时指针
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();
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、AnalogClock定义
  • 2、AnalogClock 实现
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档