摘要:
本示例是使用Qt的QPainter的转换和缩放特性简化绘图,绘制一个时钟,里面包含时针、分针、秒针、钟表刻度的绘制。
也包含计时器的使用,以及创建带有栅格表面的自定义窗口。
实现效果如图:
源码位置: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的方法是使用OpenGL和QOpenGLContext。
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()为真的显现事件,应用程序就可以开始使用QBackingStore和QOpenGLContext将其呈现到窗口中。
如果将窗口移出屏幕,使其完全被另一个窗口遮挡,或被最小化,或类似的动作,则可能调用此函数,
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通过QPrinter或QPdfWriter输出PDF文件时使用,其中drawImage()/drawPixmap()调用将使用无损压缩算法对图像进行编码,而不是有损的JPEG压缩。这个值是在Qt 5.13中添加的。
pPainter->setRenderHint(QPainter::Antialiasing);
接着要用到QPainter的转换和缩放特性了。
translate()平移将原点移动到窗口的中心,缩放操作确保将接下来的绘图操作缩放到适合窗口的大小。
这里使用一个比例因子,使用x和y坐标在-100和100之间,保证绘制的图形在窗口最短边的范围内。
//通过向量(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,是为了绘制的时候不需要带有任何轮廓。
并使用了一个颜色适合显示小时的实体笔刷。画笔用于填充多边形和其他几何形状。
这里使用了一个公式,该公式将坐标系统逆时针旋转若干度,这些度由当前的小时和分钟决定
save和restore 为保存当前绘制工具的状态和恢复绘制工具保存前的状态。
目的是为了在绘制分针、秒针的时候,不需要考虑上一次的旋转矩阵的状态。
//绘制小时指针
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();