现在的位置: 首页 > 综合 > 正文

8-2 坐标变换(Painter Transformations)

2013年01月27日 ⁄ 综合 ⁄ 共 10025字 ⁄ 字号 评论关闭
 

在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-50100100);
    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(
150150200);
    painter
->setPen(thinPen);
    painter
->setBrush(palette().foreground());
    painter
->drawPolygon(QPolygon(3&triangle[0][0]));
   QConicalGradient coneGradient(
00-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-469292);
   QRadialGradient haloGradient(
002000);
   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-204040);
   QLinearGradient knobGradient(
-7-257-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-25145015050);
    
for (int i = 0; i <= MaxMinutes; ++i) {
        
if (i % 5 == 0) {
            painter
->setPen(thickPen);
            painter
->drawLine(0-410-44);
            painter
->drawText(-15-413025,
                              Qt::AlignHCenter 
| Qt::AlignTop,
                              QString::number(i));
        } 
else {
            painter
->setPen(thinPen);
            painter
->drawLine(0-420-44);
        }
        painter
->rotate(-DegreesPerMinute);
    }
}

抱歉!评论已关闭.