坐标系统变换
在QPainter的默认坐标系中,点(0,0)位于绘图设备的左上角,x坐标向右增长,y坐标向下增长。默认坐标系的每个像素占1x1大小的区域。
理论上,像素的中心取决于半像素坐标。例如,窗口部件的左上角像素覆盖了点 (0, 0) 到点(1, 1)的区域,它的中心在 (0.5,0.5) 位置。如果告诉 QPainter 绘制一个像素,例如 (100,100) ,它会相应地在两个方向做+0.5 的偏移,使得像素点的中心位置在(100.5, 100.5)。
这一差别初看起来理论性很强,但它在实践中却很重要。首先,只有当反走样无效时(默认情况)才偏移+0.5;如果反走样有效,并且我们试图在 (100, 100)的位置绘制一个黑色的像素,实际上QPainter 会为 (99.5, 99.5) (99.5, 100.5) (100.5, 99.5) 和(100.5, 100.5) 四个像素点着浅灰色,给人的印象是一个像素正好位于四个像素的重合处。如果不需要这种效果,可以通过指定半像素坐标或者通过偏移 QPainter( +0.5, +0.5) 来避免这种效果的出现。
当绘制图形时,例如线、矩形和椭圆,可以使用相似的规则。图 8.7 表明当反走样关闭时调用由drawRect(2, 2, 6, 5) 时不同画笔宽度产生的变化。值得特别注意的是,6x5宽度为1的矩形可以有效地覆盖 7x6 的区域。这不同于旧的工具,包括早期的 Qt 版本,其本质是要支持真正可缩放的、与分辨率无关的矢量图。图 8.8 表明当反走样打开时调用 drawRect(2, 2, 6, 5) 产生的结果,图 8.9表明当指定半像素坐标时产生的结果。
既然已经了解了默认的坐标系统,就可以进一步看一下怎样使用 QPainter视口、窗口、世界矩阵。(在上下文中,术语"窗口"说的不是顶层窗口部件窗口,"视口"跟 ScrollArea的视口也没有关系。)
视口和窗口密不可分。视口是物理坐标系下指定的任意矩形。窗口也是指同一矩形,只不过是在逻辑坐标系下。当绘制图形时,在逻辑坐标系下指定的点,这些坐标都是基于当前的窗口-视口设定并以线性代数的方式转换为物理坐标的。
默认情况下,视口和窗口都被设置成设备的矩形。例如,如果设备是 320 x 200 的矩形,视口和窗口都是左上角为 (0, 0) 的320 x 200 的相同矩形。这种情况下,逻辑坐标系和物理坐标系是一致的。
这种窗口-视口机制对于编写独立于绘制设备大小和分辨率的绘制代码是很有用的。例如,如果想让逻辑坐标从(-50, -50)到(+50, +50),并且(0, 0)在中间,可以这样设置窗口:
painter.setWindow(-50, -50, 100, 100);
(-50, -50)指定了原点,(100, 100)指定了宽和高。这意味着逻辑坐标(-50, -50)对应物理坐标(0, 0),而逻辑坐标(+50, +50)对应物理坐标(320, 200),如图所示,这个例子没有改变视口。
世界变换
世界变换:是在窗口-视口转换之外使用的变换矩阵。它允许移动、缩放、旋转或者拉伸绘制的项。
PaintedWidget.h
#ifndef PAINTEDWIDGET_H
#define PAINTEDWIDGET_H
#include <QWidget>
#include <QPainter>
QT_BEGIN_NAMESPACE
namespace Ui { class PaintedWidget; }
QT_END_NAMESPACE
class PaintedWidget : public QWidget
{
Q_OBJECT
public:
PaintedWidget(QWidget *parent = nullptr);
~PaintedWidget();
protected:
void paintEvent(QPaintEvent *event);
private:
Ui::PaintedWidget *ui;
};
#endif // PAINTEDWIDGET_H
PaintedWidget.cpp
#include "paintedwidget.h"
#include "ui_paintedwidget.h"
PaintedWidget::PaintedWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::PaintedWidget)
{
ui->setupUi(this);
}
PaintedWidget::~PaintedWidget()
{
delete ui;
}
void PaintedWidget::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
QFont font("Courier", 24); //font用于设置字体,显示更加方便
painter.setFont(font);
painter.drawText(50, 50, "Hello, world!"); //drawText()用于绘制文本,50,50指代文本左下角的坐标
QTransform transform;
transform.rotate(+45.0); //rotate用于将文本顺时针旋转,将文本顺时针旋转45度
painter.setWorldTransform(transform);
painter.drawText(60, 60, "Hello, world!");
}
main.cpp
#include "paintedwidget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
PaintedWidget w;
w.show();
return a.exec();
}
模仿烤箱的定时器
为了描述绘制器的变换,我们写一个OvenTimer模仿烤箱的定时器,它是烤箱内置的钟表。用户可以单击刻度来设置持续时间。转轮会自动地逆时针转到0,OverTimer在这一点发射timeout()信号。
OvenTimer.h
#ifndef OVENTIMER_H
#define OVENTIMER_H
#include <QDateTime>
#include <QWidget>
class QTimer;
class OvenTimer : public QWidget
{
Q_OBJECT
public:
OvenTimer(QWidget *parent = 0);
void setDuration(int secs);
int duration() const;
void draw(QPainter *painter);
signals:
void timeout();
protected:
void paintEvent(QPaintEvent *event);
void mousePressEvent(QMouseEvent *event);
private:
QDateTime finishTime;
QTimer *updateTimer;
QTimer *finishTimer;
};
#endif
OvenTimer.cpp
#include <QtGui>
#include <cmath>
#ifndef M_PI
#define M_PI 3.14159265359
#endif
#include "oventimer.h"
const double DegreesPerMinute = 7.0;
const double DegreesPerSecond = DegreesPerMinute / 60;
const int MaxMinutes = 45;
const int MaxSeconds = MaxMinutes * 60;
const int UpdateInterval = 5;
OvenTimer::OvenTimer(QWidget *parent)
: QWidget(parent)
{
finishTime = QDateTime::currentDateTime(); //获取当前时间
updateTimer = new QTimer(this);
connect(updateTimer, SIGNAL(timeout()), this, SLOT(update()));
finishTimer = new QTimer(this);
finishTimer->setSingleShot(true);
connect(finishTimer, SIGNAL(timeout()), this, SIGNAL(timeout()));
connect(finishTimer, SIGNAL(timeout()), updateTimer, SLOT(stop()));
QFont font;
font.setPointSize(8); //设置窗口字体的大小
setFont(font);
}
void OvenTimer::setDuration(int secs) //设置了烤箱定时器的持续时间为给定的秒数。
{
secs = qBound(0, secs, MaxSeconds); //防止越界0~MaxSeconds
finishTime = QDateTime::currentDateTime().addSecs(secs);
if (secs > 0) {
updateTimer->start(UpdateInterval * 1000);
finishTimer->start(secs * 1000);
} else {
updateTimer->stop();
finishTimer->stop();
}
update();
}
int OvenTimer::duration() const //获取定时器完成前剩余的秒数。
{
int secs = QDateTime::currentDateTime().secsTo(finishTime);
if (secs < 0)
secs = 0;
return secs;
}
void OvenTimer::mousePressEvent(QMouseEvent *event)
{
QPointF point = event->pos() - rect().center();
double theta = std::atan2(-point.x(), -point.y()) * 180.0 / M_PI;
setDuration(duration() + int(theta / DegreesPerSecond));
update();
}
void OvenTimer::paintEvent(QPaintEvent * /* event */)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
int side = qMin(width(), height()); //返回两个参数中较小的一个
painter.setViewport((width() - side) / 2, (height() - side) / 2,
side, side);
painter.setWindow(-50, -50, 100, 100);
draw(&painter);
}
void OvenTimer::draw(QPainter *painter)
{
static const int triangle[3][2] = {
{ -2, -49 }, { +2, -49 }, { 0, -47 }
};
QPen thickPen(palette().foreground(), 1.5); //palette().foreground()得到笔刷
QPen thinPen(palette().foreground(), 0.5);
QColor niceBlue(150, 150, 200);
painter->setPen(thinPen);
painter->setBrush(palette().foreground());
painter->drawPolygon(QPolygon(3, &triangle[0][0]));
QConicalGradient coneGradient(0, 0, -90.0);
coneGradient.setColorAt(0.0, Qt::darkGray);
coneGradient.setColorAt(0.2, niceBlue);
coneGradient.setColorAt(0.5, Qt::white);
coneGradient.setColorAt(1.0, Qt::darkGray);
painter->setBrush(coneGradient);
painter->drawEllipse(-46, -46, 92, 92);
QRadialGradient haloGradient(0, 0, 20, 0, 0);
haloGradient.setColorAt(0.0, Qt::lightGray);
haloGradient.setColorAt(0.8, Qt::darkGray);
haloGradient.setColorAt(0.9, Qt::white);
haloGradient.setColorAt(1.0, Qt::black);
painter->setPen(Qt::NoPen);
painter->setBrush(haloGradient);
painter->drawEllipse(-20, -20, 40, 40);
QLinearGradient knobGradient(-7, -25, 7, -25);
knobGradient.setColorAt(0.0, Qt::black);
knobGradient.setColorAt(0.2, niceBlue);
knobGradient.setColorAt(0.3, Qt::lightGray);
knobGradient.setColorAt(0.8, Qt::white);
knobGradient.setColorAt(1.0, Qt::black);
painter->rotate(duration() * DegreesPerSecond);
painter->setBrush(knobGradient);
painter->setPen(thinPen);
painter->drawRoundRect(-7, -25, 14, 50, 99, 49);
for (int i = 0; i <= MaxMinutes; ++i) {
if (i % 5 == 0) {
painter->setPen(thickPen);
painter->drawLine(0, -41, 0, -44);
painter->drawText(-15, -41, 30, 30,
Qt::AlignHCenter | Qt::AlignTop,
QString::number(i));
} else {
painter->setPen(thinPen);
painter->drawLine(0, -42, 0, -44);
}
painter->rotate(-DegreesPerMinute);
}
}
在构造函数中,创建了两个 QTimer对象:updateTimer用来每隔5秒刷新窗口部件的外观, finishTimer 在定时器达到0时发射 timeout()信号。 finishTìmer 只需要执行一次,因此我们调用setSingleShot(true)时。默认情况下,定时器会重复触发直到它们被停止或销毁。最后一个 connect()调用是一个优化,目的是当定时器停止运行时停止更新窗口部件。
在构造函数的末尾,我们把用来绘制窗口部件的字体的磅值设置为8磅。这保证了显示的数字大小大体一致。
setDuration()
setDuration()函数设置了烤箱定时器的持续时间为给定的秒数。使用Qt的qBound()函数意味着可以不必写这样的代码:
if(secs < 0)
{
secs = 0;
}
else if(secs > MaxSeconds)
{
secs = MaxSeconds;
}
我们通过在当前时间[从QDataTime::currentDateTime()获得]加上间隔时间得到结束时间,并且把它保存到finishTime私有变量中。在结束的地方,我们调用update(),用新的间隔时间重绘窗口部件。
finishTime变量的类型是QDataTime。这个变量保存日期和时间,避免了由于当前时间是在午夜之前并且完成时间是在午夜之后而产生的折回缺陷。
duration()
duration()返回定时器完成前剩余的秒数。如果定时器未激活,返回0。
mousePressEvent()
当用户点击窗口部件时,我们使用一个巧妙且有效的数学公式找到最近的刻度,并且使用这个结果来设置新的持续时间,然后安排一个重绘。用户点过的刻度移到了顶点,然后随着时间逆时针移动到0。
paintEvent()
在paintEvent()函数中,我们把视口设置成窗口部件中最大的正方形,把窗口设置成(-50, -50, 100, 100)的矩形,也就是说,100x100的矩形从(-50, -50)扩展到(+50, +50)。qMIn()模板函数返回两个参数中最小的一个。然后调用draw()函数实际执行绘图。
如果没有设置视口为正方形,当窗口部件被缩放为非正方形的矩形时,烤箱定时器就会变为椭圆。为了避免这种变形,必须把视口和窗口设置成具有相同纵横比的矩形。
draw()
首先,在窗口部件顶部的0位置绘制一个小三角形。这个三角形的三个坐标都是由代码直接给定的,我们用drawPolygon()绘制它。
窗口-视口的方便之处就在于可以在绘制命令中使用硬编码,却能得到很好的缩放效果。
我们绘制外面的圆,用锥形渐变填充。锥形的中心点位于(0,0),角度是-90度。
我们用辐射渐变填充内部圆。渐变的中心点和焦点都在(0,0)。渐变的半径是20。
我们调用rotate()来旋转绘图器的坐标系统。在旧的坐标系统中,0-minute的标记在最上面;现在这个标记移到了对应着剩余时间的位置。在这个旋转之后,我们绘制了矩形把手,因为它的方位依赖于旋转的角度。
在for循环中,我们在外面圆的边缘绘制了标记,并且绘制的数字都是5分钟的倍数。文本被放置在标记下边一个不可见的矩形里。在每个迭代的最后,顺时针旋转绘图器7度,这相当于1分钟。下次将把标记绘制在圆的其他位置,尽管传给drawLine()和drawText()的坐标总是一样的。
for循环中的代码有一个小缺陷,如果执行了更多的迭代,这一问题会变得很明显。每次调用rotate(),就高效地用一个旋转矩阵去乘当前的世界变换,从而创建一个新的世界变换。浮点数的舍入误差不断累积,得到了越来越不准确的世界变换。用下面的办法重写这段代码可以避免这个问题,使用save()和restore()函数为每次迭代保存和加载原始的矩阵:
for(int i = 0; i <= MaxMinutes; ++i)
{
painter->save();
painter->rotate(-i*DegreesPerMinute);
if(i%5 == 0)
{
painter->setPen(thickPen);
painter->drawLine(0, -41, 0, -44);
painter->drawText(-15, -41, 30, 30, Qt::AlignHCenter | Qt::AlignTop, QString::number(i));
}
else
{
painter->setPen(thinPen);
painter->drawLine(0, -42, 0, -44);
}
painter->restore();
}
实现烤箱定时器的另外一个办法是自己计算(x,y)位置,使用sin()和cos()函数找到圆上的位置。但之后将仍然需要利用移动和旋转并以一定的角度来绘制文本。
main.cpp
#include "oventimer.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
OvenTimer w;
w.show();
return a.exec();
}