Qt—坐标系统

x33g5p2x  于2022-04-25 转载在 其他  
字(7.4k)|赞(0)|评价(0)|浏览(373)

坐标系统

Qt的坐标系统是由QPainter类控制的,而 QPainter是在绘图设备上进行绘制的。

一个绘图设备的默认坐标系统中,原点(0,0)在其左上角,x坐标向右增长,y坐标向下增长。

在基于像素的设备上,默认的单位是一个像素,而在打印机上默认的单位是一个点(1/72英寸)。

QPainter的逻辑坐标与绘图设备的物理坐标之间的映射由QPainter的变换矩阵、视口和窗口处理。逻辑坐标和物理坐标默认是一致的。

QPainter也支持坐标变换(比如旋转和缩放)。

本节的内容可以在帮助中通过Coordinate System关键字查看。

抗锯齿渲染

1 逻辑表示

一个图形的大小(宽和高)总与其数学模型相对应,图10-11示意了忽略其渲染时使用的画笔的宽度时的样子。

2 抗锯齿绘画

抗锯齿(Anti-aliased)又称为反锯齿或者反走样,就是对图像的边缘进行平滑处理,使其看起来更加柔和流畅的一种技术

QPainter进行绘制时可以使用QPainter: :RenderHint渲染提示来指定是否要使用抗锯齿功能,渲染提示的取值如表10–2所列。

在默认的情况下,绘制会产生锯齿,并且使用这样的规则进行绘制:当使用宽度为一个像素的画笔进行渲染时,像素会在数学定义的点的右边和下边进行渲染,如图10-12所示。

  1. 当使用一个拥有偶数像素的画笔进行渲染时,像素会在数学定义的点的周围对称渲染;
  2. 而当使用一个拥有奇数像素的画笔进行渲染时,像素会被渲染到数学定义的点的右边和下边,如图10-13所示。

矩形可以用QRect类来表示,但是由于历史的原因,QRect::right()和 QRect::bottom()函数的返回值会偏离矩形真实的右下角。
使用QRect的 right()函数返回left() + width()-1,而 bottom()函数返回top() + height ()-1。

建议使用QRectF来代替QRect.

! QRectF类在一个使用了浮点数精度的坐标平面中定义了一个矩形,QRectF::right()和 QRectF::bottom()会返回真实的右下角坐标。

当然,也可以使用QRect类,应用x()+ width()和y()+ height()来确定右下角的坐标,而不要使用right()和 bottom()函数。

如果在绘制时使用了抗锯齿渲染提示,即使用QPainter : : setRenderHint ( RenderHint hint,bool on = true )函数,将参数 hint设置为了QPainter::Antialiasing。那么像素就会在数学定义的点的两侧对称地进行渲染,如图所示。

上面是渲染的事情,下面讲解下坐标变化,就是移动之类的

坐标变换

基本变换

默认的,QPainter在相关设备的坐标系统上进行操作,但是它也完全支持仿射( affine)坐标变换(仿射变换的具体概念可以查看其他资料)。

绘图时可以使用QPainter::scale()函数缩放坐标系统,使用QPainter::rotate()函数顺时针旋转坐标系统,使用QPainter::translate()函数平移坐标系统,还可以使用QPainter::shear()围绕原点来扭曲坐标系统。

坐标系统的2D变换由QTransform类实现,可以使用前面提到的那些便捷函数进行坐标系统变换,当然也可以通过QTransform类实现,而且 QTransform类对象可以存储多个变换操作;当同样的变换要多次使用时,建议使用QTransform类对象。

坐标系统的变换是通过变换矩阵实现的,可以在平面上变换一个点到另一个点。进行所有变换操作的变换矩阵都可以使用QPainter::worldTransform()函数获得;如果要设置一个变换矩阵,可以使用QPainter::setWorldTransform()函数。
这两个函数也可以分别使用QPainter :: transform()和 QPainter ::setTransform()函数来代替。

在进行变换操作时,可能需要多次改变坐标系统,然后再恢复,这样编码会很乱,而且很容易出现操作错误。这时可以使用QPainter:: save()函数来保存QPainter的变换矩阵,它会把变换矩阵保存到一个内部栈中,需要恢复变换矩阵时再使用QPainter: : restore()函数将其弹出。

窗口-视口转换

使用QPainter进行绘制时,会使用逻辑坐标进行绘制,然后再转换为绘图设备的物理坐标。

逻辑坐标到物理坐标的映射由QPainter的worldTransform()函数QPainter 的viewport()以及 window()函数进行处理。其中,视口( viewport)表示物理坐标下指定的一个任意矩形,而窗口( window,与以前讲的窗口部件的概念不同)表示逻辑坐标下的相同矩形。默认的,逻辑坐标和物理坐标是重合的,它们都相当于绘图设备上的矩形。

使用窗口-视口转换可以使逻辑坐标系统适合应用要求,这个机制也可以用来让绘图代码独立于绘图设备。

举个例子:
例如,可以使用下面的代码来使逻辑坐标以(-50,-50)为原点,宽为100,高为100,(0,0)点为中心:

QPainter painter(this); //定义绘画对象
painter.setWindow(QRect(-50,-50,100,100));

现在逻辑坐标的(-50,-50)对应绘图设备的物理坐标的(0,0)点。

这样就可以独立于绘图设备,使绘图代码在指定的逻辑坐标上进行操作了。

当设置窗口或者视口矩形时,实际上是执行了坐标的一个线性变换,窗口的4个角会映射到视口对应的4个角,反之亦然。因此,一个很好的办法是让视口和窗口维持相同的宽高比来防止变形:

int side=qMin(width(),height());
int x=(width()-side/2);
int y=(height()-side/2);
painter.setViewport(x,y,side,side);

使窗口window与视图viewport相同的宽高比来防止变形

如果设置了逻辑坐标系统为一个正方形,那么也需要使用QPainter :: setViewport()函数设置视口为正方形;
例如,这里将视口设置为适合绘图设备矩形的最大矩形。
在设置窗口或视口时考虑到绘图设备的大小,就可以使绘图代码独立于绘图设备。

窗口-视口转换仅仅是线性变换,不会执行裁减操作。这就意昧着如果绘制范围超出了当前设置的窗口,那么仍然会使用相同的线性代数方法将绘制变换到视口上。
绘制过程中先使用坐标矩阵进行变换,再使用窗口-视口转换。

例子 坐标扭曲,平移,缩放

上面的内容不容易理解,我们写个例子:
新建 qtwidget项目,名字为mytransformation

在widget.h中声明重绘事件处理函数:

protected:
    void paintEvent(QPaintEvent *);

然后到 widget.cpp文件中添加头文件#include< QPainter>。下面添加paintEvent()函数的定义:

void Widget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    // 填充界面背景为白色
    painter.fillRect(rect(), Qt::white);
    painter.setPen(QPen(Qt::red, 11));
    // 绘制一条线段
    painter.drawLine(QPoint(5, 6), QPoint(100, 99));
    // 将坐标系统进行平移,使(200, 150)点作为原点
    painter.translate(200, 150);
    // 开启抗锯齿
    painter.setRenderHint(QPainter::Antialiasing);
    // 重新绘制相同的线段
    painter.drawLine(QPoint(5, 6), QPoint(100, 99));
}

设置画笔红色,11宽,然后绘制一条以5,6为原点,100,99终点的线段,然后平移到以200,150为原点,
最后开启抗锯齿,最后重新绘制一个原来的线段

这里说下translate(x,y)
里面的xy是水平和垂直方向的偏移量,也就是说,从原来的位置,向x(右)移动x,向y(下)移动y

第二条线段使用了抗锯齿所以更加平滑
如果想把线段还原回去,那么反向平移就可以了
translate(-200,-150)

下面继续写:

// 保存painter的状态
    painter.save();
    // 将坐标系统旋转90度
    painter.rotate(90);
    painter.setPen(Qt::cyan);
    // 重新绘制相同的线段
    painter.drawLine(QPoint(5, 6), QPoint(100, 99));
    // 恢复painter的状态
    painter.restore();

这里先使用save()函数保存了painter的当前状态﹐然后将坐标系统进行旋转并绘制了同以前一样的线段;不过,因为坐标系统已经旋转了,所以这条线段也不会和前面的线段重合。

这里的rotate()函数会以原点为中心进行旋转,其参数为旋转的角度﹐正数为顺时针旋转,负数为逆时针旋转。最后使用restore()函数恢复了painter 以前的状态,就是恢复到了旋转以前的坐标系统和画笔颜色。可以运行程序查看效果。

下面继续写:

painter.setBrush(Qt::darkGreen);
    // 绘制一个矩形
    painter.drawRect(-50, -50, 100, 50);
    painter.save();
    // 将坐标系统进行缩放
    painter.scale(0.5, 0.4);
    painter.setBrush(Qt::yellow);
    // 重新绘制相同的矩形
    painter.drawRect(-50, -50, 100, 50);
    painter.restore();

这里绘制的矩形是这个样子的

它的位置是这样的

说明了现在是以200,150作为原点,就是相当于0,0
painter上面定义的,然后绘制一个矩形,以200-50,150-50为原点开始绘制

然后将坐标系统缩放

// 将坐标系统进行缩放
    painter.scale(0.5, 0.4);//横向和纵向的缩放比例,大于1放大,小于1缩小

这里的参数是xy的缩放倍数

相当于把原来的坐标框架缩小,x缩小一半,y缩小0.4

下面继续写:

painter.setPen(Qt::blue);
    painter.setBrush(Qt::darkYellow);
    // 绘制一个椭圆
    painter.drawEllipse(QRect(60, -100, 50, 50));
    // 将坐标系统进行扭曲
    painter.shear(1.5, -0.7);
    painter.setBrush(Qt::darkGray);
    // 重新绘制相同的椭圆
    painter.drawEllipse(QRect(60, -100, 50, 50));

进行扭曲
参数分别是水平和垂直的扭曲值
这里横向扭曲1.5

横向扭曲1

纵向扭曲1

那么-1会咋样
横向-1

纵向-0.6

方向不同罢了

下面看一下

例子:窗口-视图转换

将上面函数里面的注释掉
改成

void Widget::paintEvent(QPaintEvent *)
{
        QPainter painter(this);
        painter.setWindow(-50, -50, 100, 100);
        painter.setBrush(Qt::green);
        painter.drawRect(0, 0, 20, 20);
}

这里先使用set Window()函数将逻辑坐标矩形设置为以(一50,一50)为起点﹐宽100,高100。这样逻辑坐标的(一50,一50)点就会对应物理坐标的(0,0)点,因为这里是在this(即 Widget部件上)进行绘图,所以 Widget就是绘图设备。也就是说,现在逻辑坐标的(一50,一50)点对应界面左上角的(0,0)点。

而且,因为逻辑坐标矩形宽为100 ,高为100,所以界面的宽度和高度都会被100 等分。

等于上面设置了这个窗口的坐标表示

下面在界面上显示出物理坐标,从而帮助读者理解。在 widget.h文件的 protected域中声明鼠标移动事件处理函数:

protected:
    void paintEvent(QPaintEvent *);
    void mouseMoveEvent(QMouseEvent *event);

widget.cpp中
头文件

#include <QToolTip>
#include <QMouseEvent>

再在构造函数中添加如下一行代码,保证不用按下鼠标按键也能触发鼠标移动事件:

setMouseTracking(true);

Qt的setMouseTracking使用

不用按下也能触发事件

添加处理函数

void Widget::mouseMoveEvent(QMouseEvent *event)
{
    QString pos = QString("%1,%2").arg(event->pos().x()).arg(event->pos().y());
    QToolTip::showText(event->globalPos(), pos, this);
}

这里先获取了鼠标指针在 Widget 上的坐标(即物理坐标),然后在工具提示中进行显示。

现在运行程序可以看到,在(0,0)点绘制的矩形实际在(200,150)点,而矩形的宽和高也不再是20,而变为了80和 60,效果如图所示。

为什么会出现这样的问题呢?前面已经讲过,更改逻辑坐标或者物理坐标的矩形就是进行坐标的一个线性变换,逻辑坐标矩形的4个角会映射到对应物理坐标矩形的4个角。

而现在 Widget部件的大小为宽400、高300,所以物理坐标对应的矩形就是(0,0,400,300)。
坐标要求(-50,-50,100,100)

这样按比例对应,就是在水平方向,逻辑坐标的一个单位对应物理坐标的4个单位;在垂直方向,逻辑坐标的一个单位对应物理坐标的3个单位

如图10-17所示。所以,逻辑坐标中的宽20 ,高20的矩形在物理坐标中就是宽80,高60的矩形。

坐标原点设置为-50,-50,对应原点0,0。x按比例缩放4倍,y按比例缩放3倍数

可以看到,设置的矩形已经发生了变形,由设置的正方形变成了一个长方形。

为了防止变形,需要将视口的宽和高的对应比例设置为相同值,因为逻辑坐标的矩形设置为了一个正方形,所以视口(即物理坐标矩形)也应该设置为一个正方形,更改 paint-Event)函数如下:

QPainter painter(this);
    int side = qMin(width(), height());
    int x = (width() / 2);
    int y = (height() / 2);
    // 设置视口
    painter.setViewport(x, y, side, side);
    painter.setWindow(0, 0, 100, 100);
    painter.setBrush(Qt::green);
    painter.drawRect(0, 0, 20, 20);

这样就是正确的绘制了,不会变形了

例子:坐标变换与定时器结合的简单动画

还在上面项目上修改

widget.h上添加前置声明

class QTimer;

再添加二个私有变量

private:
    Ui::Widget *ui;
    QTimer *timer;
    int angle;

进入widget.cpp中,添加头文件QTime

构造函数中,添加

ui->setupUi(this);
    setMouseTracking(true);

    QTimer *timer = new QTimer(this);
    connect(timer, SIGNAL(timeout()), this, SLOT(update()));
    timer->start(1000);
    angle = 0;

创建了定时器,连接一个槽,设置定时事件1000ms
这样1000ms会发生一次update
update是widget部件上的更新函数,每次都会重新绘画
,重新执行painteEvent函数

下面将paintEvent函数更改

void Widget::paintEvent(QPaintEvent *)
{
    angle += 10;
    if(angle == 360)
        angle = 0;
   //抗锯齿     
    int side = qMin(width(), height());
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    //防止变形
    QTransform transform;
    transform.translate(width()/2, height()/2);
    //缩放
    transform.scale(side/300.0, side/300.0);
    //旋转 angle角度
    transform.rotate(angle);
    //绘制线段(表的指针)和椭圆(表)
    painter.setWorldTransform(transform);
    painter.drawEllipse(-120, -120, 240, 240);
    painter.drawLine(0, 0, 100, 0);
}

因为这里连续进行了多个坐标转换,所以使用了QTransform类对象,当连续进行多个坐标转换时使用这个类更高效。这里根据部件的大小使用scale()函数进行了缩放,这样当窗口改变大小时,绘制的内容也会跟着变换大小。然后在rotate()函数中使用了变量angle作为参数,每次执行paintEvent()函数时angle都增加10度,这样就会旋转一个不同的角度﹐当其值为360时将它重置为0。运行程序可以看到一个每隔1秒走动一下的表针动画。

关于坐标系统的应用,Qt中提供了一个Analog Clock Example程序、一个 Trans-formations Example程序,还有一个Affine Transformations演示程序,可以参考一下。

相关文章