在QPainter的初始坐标系统中,点(0,0)位于绘图设备的左上角。X轴坐标向右递增,y轴向下递增,一个象素占据1×1的面积。
需要说明的一点是一个象素的中心位于坐标的一半处。例如,左上角位于点(0,0)和点(1,1)之间区域的象素,它的中心位于(0.5,0.5)。如果我们使用QPainter绘制一个位置在(100,100)的象素,QPainter会在每个坐标值上增加0.5,以坐标(100.5,100.5)为中心绘制这个象素。
一个需要注意的事情是,一个象素的中心位于象素坐标的“半象素”坐标。例如,窗口左上角象素占据从点(0,0)到(1,1)的位置,它的中心位于(0.5,0.5)。如果我们需要QPainter在点(100,100)的坐标处绘制另一个象素,QPainter将会在两个坐标轴方向偏移0.5个坐标点,即象素的中心点将会位于(100.5,100.5)。
这个偏移看起来有些教条,但是实际上有这重要的作用。首先,在禁止消除锯齿功能(缺省设置)时才进行0.5的偏移。如果许可了消除锯齿功能,QPainter会在(100,100)的位置绘制一个黑色的象素。事实是QPainter在(99.5,99.5),(99.5,100.5),(100.5,99.0),(100.5,100.5)绘制亮灰色象素,这样产生的效果就是一个黑色象素位于四个象素的焦点(100,100)处。如果我们不需要这个功能,可以把坐标偏移半个象素。
在绘制直线,矩形,椭圆时,上述规则都是适用的。图8.7表明了在不用消除锯齿功能时,用不同的笔宽度绘制矩形drawRect(2,2,6,5)的不同结果。需要特别注意用1象素的笔宽绘制6*5的矩形时实际的矩形面积为7*6。这和以前的Qt版本不同,但是这个功能对绘制看缩放的,独立于分辨率的矢量图形很有帮助。
Figure 8.7. Drawing a 6 x 5 rectangle with no antialiasing
现在我们已经理解了Qt的默认坐标系同,现在再来了解QPainter的视口(viewport),窗口(window)和世界坐标系矩阵(world matrix)的变化。(在这一节中,窗口(window)不是控件的窗口,视口(viewport)也和QScrollArea的视口也没有联系)
窗口和视口是紧密联系在一起的。视口是由物理坐标确定的任意矩形。窗口是由逻辑坐标表示的视口大小。QPainter在进行绘制时,我们给QPainter的是逻辑坐标,根据视口和窗口的设置,这些逻辑坐标通过线形变换,转换为物理坐标。
通常,窗口和视口的大小和绘图设备是一致的。例如,一个320*200的控件,视口和窗口都是一个320*200的矩形,起始点(0,0)位于左上角。这时,逻辑坐标和物理坐标是相同的。
视口窗口机制是为了绘制与绘图设备的大小和分辨率无关的图形。如果我们的逻辑坐标设置为从(-50,-50)到(+50,+50)的矩形,(0,0)点在中心。如下这样设置窗口:
painter.setWindow(-50, -50, 100, 100);
(-50,-50)
确定了原点,(100,100)确定矩形的宽和高。在窗口中,逻辑坐标(-50,-50)相当于物理坐标中的原点(0,0),(+50,+50)相当于物理坐标的点(320,320)。视口的设置没有改变。
Figure 8.8. Converting logical coordinates into physical coordinates
现在来说明世界坐标系矩阵。窗口视口可以转换变形,世界坐标系矩阵也是一个用来图形变换的转换矩阵。用来平移,缩放,旋转,剪切图形。例如,如果要绘制一行倾斜45°的文字,代码如下:
QMatrix matrix;
matrix.rotate(45.0);
painter.setMatrix(matrix);
painter.drawText(rect, Qt::AlignCenter, tr("Revenue"));
传给drawText()函数的逻辑坐标由世界矩阵进行旋转,然后根据窗口视口设置映射到物理坐标。
如果我们指定了多个坐标变换,按照设置顺序应用。例如,以(10,20)做为中心旋转45°,可以把原点移动到(10,20),然后旋转,再把窗口原点平移到原来的位置:
QMatrix matrix;
matrix.translate(-10.0, -20.0);
matrix.rotate(45.0);
matrix.translate(+10.0, +20.0);
painter.setMatrix(matrix);
painter.drawText(rect, Qt::AlignCenter, tr("Revenue"));
一个简单的方法是使用QPianter的转换函数translate(),scale(),rotate()和shear()。
painter.translate(-10.0, -20.0);
painter.rotate(45.0);
painter.translate(+10.0, +20.0);
painter.drawText(rect, Qt::AlignCenter, tr("Revenue"));
但是,如果我们反复需要同一个矩阵,最好还是把它保存到QMatrix中,在需要的时候给QPainter设置。
为了更好的解释绘图的坐标变换,我们看一下图8.9所示OvenTimer控件的代码。OvenTimer以厨房计时器为模型,在烤炉没有自带的计时器之前,这种定时器使用很广泛。用户点击定时器上面的一个刻度值,指针就从这个刻度值开始,自动逆时针旋转,到达刻度0的位置,这时,OvenTimer发出timeout()信号。
Figure 8.9. The OvenTimer widget
头文件oventimer.h,从QWidget继承,重写了paintEvent()和mousePressEvent()函数。
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;
}
;
源文件oventimer.cpp,首先是一些常量的定义,确定定时器的外观。
在构造函数中,我们创建了两个QTimer对象:updateTimer每一秒中更新控件的外观,finishTimer在定时器到达0点时发出timeOut信号。finishTimer只需要一次timeOut,所以调用了setSingleShot(true)。通常计时器QTimer自创建开始就计时,直到它们停止或者销毁。最后一个connect语句用来定时结束时停止计时器。
函数setDuration()设置计时器的时间周期,以秒为单位。结束时间由当前时间(由QDateTime::currentDateTime()得到)加上定时周期得到,保存在finishTime中。最后调用update()用新的计时周期重新绘制控件。
finishTime
变量为QDateTime类型,因此变量中包含当前的日期和时间。我们需要避免一个循环错误,例如当前时间为午夜以前而结束时间为午夜以后。
函数duration()函数返回在定时结束之前还剩下的时间。如果计时器没有启动,则返回0。
如果用户点击了控件,我们就找到距离点击点最近的一个刻度值(当然有细微的误差)我们使用得到的刻度值设置新的定时周期。然后开始重新绘制控件。指针开始逆时针移动直到计时结束。
在paintEvent()中,设置视口与控件的尺寸一致,设置窗口为(50,50,100,100),即有点(-50,-50)到(50,50)的矩形。qMin()模板函数得到两个参数中的最小值,调用draw()函数绘制。
Figure 8.10. The
OvenTimer
widget at three different sizes
现在我们看一下draw()函数,首先我们绘制一个小的倒三角形表示控件的0位置。这个三角形由三个坐标指定,使用函数drawPolygon()绘制它。
static const int triangle[3][2] = {
{ -2, -49 }, { +2, -49 }, { 0, -47 }
};
QPen thickPen(palette().foreground(), 1.5);
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]));
视口窗口机制的好处就在于我们可以直接在绘图函数中指定坐标值,根据自动坐标变换能适应控件的各种大小。
在绘制最外面的一个圆形我们使用了圆锥渐变。渐变的中心点位于(0,0),角度为-90°。
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);
绘制里面的圆形时使用了圆形渐变。圆心和渐变的中心点位于(0,0),渐进半径为20。
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);
在绘制刻度时,我们旋转控件的坐标系。在原来的坐标系中,0分钟刻度在最上面,现在0刻度被移动到相当于剩余时间的位置。坐标旋转后我们绘制矩形的突起手柄,它的旋转角度和坐标旋转角度相同。
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, 150, 50);
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, 25,
Qt::AlignHCenter | Qt::AlignTop,
QString::number(i));
} else {
painter->setPen(thinPen);
painter->drawLine(0, -42, 0, -44);
}
painter->rotate(-DegreesPerMinute);
}
在for循环中,我们沿着最外层圆形的边绘制时间记号,每隔5分钟一次。记号值画在刻度的下面。在每一次循环结束,坐标旋转7°,相当于1分钟。这样再次绘制标记时,虽然我们传给drawLine()和drawText()坐标值没有变,但是却能绘制在不同的地方。
这个代码中的for循环有一个小的缺陷,如果我们执行更多的循环就能很明显出现。我们每次调用rotate(),当前世界坐标系矩阵乘以一个旋转矩阵,得到一个新的世界坐标系矩阵。由于浮点数运算时产生的四舍五入误差就会累加,世界坐标系矩阵就越发不准确。我们可以重新设计for循环避免这个问题,在每一次循环中,使用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, 25,
Qt::AlignHCenter | Qt::AlignTop,
QString::number(i));
} else {
painter->setPen(thinPen);
painter->drawLine(0, -42, 0, -44);
}
painter->restore();
}
另一种实现计时器的方法是不进行坐标变换,使用算术函数sin()和cos()计算刻度位置。但是如果想绘制文本,还是需要旋转坐标系。
const
double
DegreesPerMinute
=
7.0
;
const
double
DegreesPerSecond
=
DegreesPerMinute
/
60
;
const
int
MaxMinutes
=
45
;
const
int
MaxSeconds
=
MaxMinutes
*
60
;
const
int
UpdateInterval
=
1
;
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()));
}
void
OvenTimer::setDuration(
int
secs)
{
if
(secs
>
MaxSeconds) {
secs
=
MaxSeconds;
}
else
if
(secs
<=
0
) {
secs
=
0
;
}
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
=
atan2(
-
point.x(),
-
point.y())
*
180
/
3.14159265359
;
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
);
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
,
150
,
50
);
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
,
25
,
Qt::AlignHCenter
|
Qt::AlignTop,
QString::number(i));
}
else
{
painter
->
setPen(thinPen);
painter
->
drawLine(
0
,
-
42
,
0
,
-
44
);
}
painter
->
rotate(
-
DegreesPerMinute);
}
}