QTransform的使用

引言

A transformation specifies how to translate, scale, shear, rotate or project the coordinate system, and is typically used when rendering graphics.

A QTransform object can be built using the setMatrix(), scale(), rotate(), translate() and shear() functions. Alternatively, it can be built by applying basic matrix operations. The matrix can also be defined when constructed, and it can be reset to the identity matrix (the default) using the reset() function.

如上所述,QTransform的作用是平移、缩放、裁切、旋转或投影坐标系,通常在渲染图形时使用。QTransform本质是对矩阵的封装,将我们熟知的旋转矩阵、平移矩阵等,封装为清晰明了的函数,如rotate()、translate()等,方便程序调用。

既然是对矩阵的封装,自然就是应用在坐标的转换,从坐标系A转换到坐标系B,不过QTransform只支持2D场景下使用。实际使用场景多种多样,可以用于实现图元的放大缩小、图形的不规则展示、重复图形但有规律的绘制等等,能很好的满足QPainter、QOpenGLWidget中绘图的坐标系转换的使用。

基础知识

在实际使用之前,我们需要先了解矩阵的相关性质,这样才能够方便后续在程序中的使用。如果不彻底理解底层的实现,在实际调用的过程中很容易就被各种复杂的转换绕晕。虽然QTransform只支持2D,但为了更好的理解矩阵,以下例子中都是基于3D坐标举例,2D场景下将z值视为0即可。

缩放矩阵

[ S 1 0 0 0 0 S 2 0 0 0 0 S 3 0 0 0 0 1 ] × [ x y z 1 ] = [ S 1 × x S 2 × y S 3 × z 1 ] \left[\begin{array}{cccc} S_1&0&0&0\\ 0&S_2&0&0\\ 0&0&S_3&0\\ 0&0&0&1 \end{array}\right] \times \left[\begin{array}{c}x\\y\\z\\1 \end{array}\right] = \left[\begin{array}{c} S_1\times x\\ S_2\times y\\ S_3\times z\\ 1 \end{array}\right] S10000S20000S300001 × xyz1 = S1×xS2×yS3×z1
如上所示,原坐标点P1(x,y,z),通过旋转矩阵S转换为新坐标P2,其中S1、S2、S3代表不同坐标轴的缩放倍率。放在QTransform::scale中,sx和sy就分别对应S1和S2。

QTransform &QTransform::scale(qreal sx, qreal sy)

平移矩阵

[ 1 0 0 T x 0 1 0 T y 0 0 1 T z 0 0 0 1 ] × [ x y z 1 ] = [ T x + x T y + y T z + z 1 ] \left[\begin{array}{cccc} 1&0&0&T_x\\ 0&1&0&T_y\\ 0&0&1&T_z\\ 0&0&0&1 \end{array}\right] \times \left[\begin{array}{c}x\\y\\z\\1 \end{array}\right] = \left[\begin{array}{c} T_x + x\\ T_y + y\\ T_z + z\\ 1 \end{array}\right] 100001000010TxTyTz1 × xyz1 = Tx+xTy+yTz+z1
如上所示,原坐标点P1(x,y,z),通过旋转矩阵T转换为新坐标P2,其中Tx、Ty、Tz代表不同坐标轴的平移距离。放在QTransform::translate中,dx和dy就分别对应Tx和Ty。

QTransform &QTransform::translate(qreal dx, qreal dy)

旋转矩阵

绕X轴旋转:
[ 1 0 0 0 0 cos ⁡ θ − sin ⁡ θ 0 0 sin ⁡ θ cos ⁡ θ 0 0 0 0 1 ] × [ x y z 1 ] = [ x cos ⁡ θ × y − sin ⁡ θ × z sin ⁡ θ × y + cos ⁡ θ × z 1 ] \left[\begin{array}{cccc} 1&0&0&0\\ 0&\cos\theta&-\sin\theta&0\\ 0&\sin\theta&\cos\theta&0\\ 0&0&0&1 \end{array}\right] \times \left[\begin{array}{c}x\\y\\z\\1 \end{array}\right] = \left[\begin{array}{c} x\\ \cos\theta \times y -\sin\theta \times z\\ \sin\theta \times y+ \cos\theta \times z\\ 1 \end{array}\right] 10000cosθsinθ00sinθcosθ00001 × xyz1 = xcosθ×ysinθ×zsinθ×y+cosθ×z1
绕Y轴旋转:
[ cos ⁡ θ 0 sin ⁡ θ 0 0 1 0 0 − sin ⁡ θ 0 cos ⁡ θ 0 0 0 0 1 ] × [ x y z 1 ] = [ cos ⁡ θ × x + sin ⁡ θ × z y − sin ⁡ θ × x + cos ⁡ θ × z 1 ] \left[\begin{array}{cccc} \cos\theta&0&\sin\theta&0\\ 0&1&0&0\\ -\sin\theta&0&\cos\theta&0\\ 0&0&0&1 \end{array}\right] \times \left[\begin{array}{c}x\\y\\z\\1 \end{array}\right] = \left[\begin{array}{c} \cos\theta \times x + \sin\theta \times z\\ y\\ -\sin\theta \times x + \cos\theta \times z\\ 1 \end{array}\right] cosθ0sinθ00100sinθ0cosθ00001 × xyz1 = cosθ×x+sinθ×zysinθ×x+cosθ×z1
绕Z轴旋转:
[ cos ⁡ θ − sin ⁡ θ 0 0 sin ⁡ θ cos ⁡ θ 0 0 0 0 1 0 0 0 0 1 ] × [ x y z 1 ] = [ cos ⁡ θ × x − sin ⁡ θ × y sin ⁡ θ × x + cos ⁡ θ × y z 1 ] \left[\begin{array}{cccc} \cos\theta&-\sin\theta&0&0\\ \sin\theta&\cos\theta&0&0\\ 0&0&1&0\\ 0&0&0&1 \end{array}\right] \times \left[\begin{array}{c}x\\y\\z\\1 \end{array}\right] = \left[\begin{array}{c} \cos\theta \times x -\sin\theta \times y\\ \sin\theta \times x + \cos\theta \times y\\ z\\ 1 \end{array}\right] cosθsinθ00sinθcosθ0000100001 × xyz1 = cosθ×xsinθ×ysinθ×x+cosθ×yz1
由于QTransfrom只支持2D,因此这里我们只需要了解绕Z轴旋转即可。原坐标点P1(x,y,z),通过旋转矩阵R转换为新坐标P2。放在QTransform::translate中,angle就是矩阵中的θ。这里可以看到还有一个参数axis,使用其他轴进行旋转时得到的新坐标是其在XY平面的投影。

QTransform &QTransform::rotate(qreal angle, Qt::Axis axis = Qt::ZAxis)

矩阵乘法

同时大量使用到矩阵的叉乘,运算规则可以简单理解为一行乘以一列,详细描述如下:

新矩阵的第i行第j列的元素的值,为第一个矩阵的第i行的每个元素分别乘上第二个矩阵第j列的每个元素然后进项相加

矩阵乘法一般不满足交换律(除了有些特殊的方阵之间的乘法),缩放矩阵属于该特殊矩阵,因此满足交换律,相乘时没有顺序要求,但是平移矩阵和旋转矩阵并不不满足交换律的,也就是
[ T ] × [ R ] × [ x y z 1 ] ≠ [ R ] × [ T ] × [ x y z 1 ] \left[\begin{array}{c} T \end{array}\right] \times \left[\begin{array}{c} R \end{array}\right] \times \left[\begin{array}{c}x\\y\\z\\1 \end{array}\right] \neq \left[\begin{array}{c} R \end{array}\right] \times \left[\begin{array}{c} T \end{array}\right] \times \left[\begin{array}{c}x\\y\\z\\1 \end{array}\right] [T]×[R]× xyz1 =[R]×[T]× xyz1
同时乘法又是右结合律的,如果你希望先旋转再平移,那么你需要将旋转放在后面,代码如下:

    QTransform transform;
    transform.translate(translate_x_, translate_y_);
    transform.rotate(rotate_);

效果如下(黄色虚线为原始图形,红色实线为先旋转45°再平移):
在这里插入图片描述

而如果需要先平移再旋转,则需要将平移放在后面,代码如下:

    QTransform transform;
    transform.translate(translate_x_, translate_y_);
    transform.rotate(rotate_);

效果如下(黄色虚线为原始图形,红色实线为先再平移再旋转45°,):
在这里插入图片描述

实际使用

有了矩阵的相关基础知识,可以开始实际代码的编写。具体功能如下:

  1. 默认矩形QRect(0, 0, 100, 100)
  2. 支持平移
  3. 支持绕矩形中心旋转

Demo实现效果如下:
在这里插入图片描述

实现思路

	QTransform transform;
    transform.translate(translate_x_, translate_y_);
    transform.rotate(rotate_);
    transform.translate(-rect_.width() / 2, -rect_.height() / 2);
    painter.setPen(QColor(244,67,54));
    QPolygon polygon = transform.mapToPolygon(rect_);
    painter.drawPolygon(polygon);

核心代码只有上述部分,通过QTransform 将矩形rect_转换为新的多边形polygon ,再将polygon 绘制出来。

按照之前说的右结合律的顺序,需要先将矩形rect_移动至其中心,再进行旋转界面设置的数值rotate_,再将其移动界面界面设置的数值translate_x_、translate_y_。

完整代码

自绘控件

class TestWidget : public QWidget
{
    Q_OBJECT

public:
    TestWidget(QWidget *parent = nullptr);

    void setRotate(int rotate);
    void setTranslateX(qreal translate_x);
    void setTranslateY(qreal translate_y);

protected:
    void paintEvent(QPaintEvent* event) override;

private:
    int rotate_;
    qreal translate_x_;
    qreal translate_y_;

    QRect rect_;
};
TestWidget::TestWidget(QWidget *parent)
    : QWidget(parent)
    , rotate_(0)
    , translate_x_(0)
    , translate_y_(0)
    , rect_(0, 0, 100, 100)
{

}

void TestWidget::setRotate(int rotate)
{
    rotate_ = rotate;
    update();
}

void TestWidget::setTranslateX(qreal translate_x)
{
    translate_x_ = translate_x;
    update();
}

void TestWidget::setTranslateY(qreal translate_y)
{
    translate_y_ = translate_y;
    update();
}


void TestWidget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    painter.save();
    painter.setPen(Qt::NoPen);
    painter.setBrush(Qt::white);
    painter.drawRect(event->rect());
    painter.restore();

    // 坐标轴
    painter.save();
    painter.setPen(QColor(32, 32, 32, 255 * 0.5));
    QPoint p1(0, height() / 2);
    QPoint p2(width(), height() / 2);
    QPoint p3(width() / 2, 0);
    QPoint p4(width() / 2, height());
    painter.drawLine(p1, p2);
    painter.drawLine(p3, p4);
    // 箭头
    int interval = 6;
    QPainterPath arrows;
    arrows.lineTo(0, interval / 2);
    arrows.lineTo(interval, 0);
    arrows.lineTo(0, -interval / 2);
    {
        painter.save();
        painter.translate(width() - interval, height() / 2);
        painter.fillPath(arrows, QColor(128, 128, 128));
        painter.restore();
    }
    {
        painter.save();
        painter.translate(width() / 2, height() - interval);
        painter.rotate(90);
        painter.fillPath(arrows, QColor(128, 128, 128));
        painter.restore();
    }
    painter.restore();

    painter.translate(width() / 2, height() / 2);

    // 原矩形
    QPen pen(QColor(255,193,7));
    pen.setStyle(Qt::DashLine);
    painter.setPen(pen);
    painter.drawRect(rect_);

    // 新矩形
    QTransform transform;
    transform.translate(translate_x_, translate_y_);
    transform.rotate(rotate_);
    transform.translate(-rect_.width() / 2, -rect_.height() / 2);
    painter.setPen(QColor(244,67,54));
    QPolygon polygon = transform.mapToPolygon(rect_);
    painter.drawPolygon(polygon);
}

主窗体

#include <QMainWindow>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_spinBox_valueChanged(int arg1);
    void on_spinBox_2_valueChanged(int arg1);
    void on_spinBox_3_valueChanged(int arg1);

private:
    Ui::MainWindow *ui;
};
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QPainter>
#include <QPaintEvent>
#include <QPainterPath>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    //auto tmpWidget = new TestWidget(this);
    //setCentralWidget(tmpWidget);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_spinBox_valueChanged(int arg1)
{
    ui->widget->setRotate(arg1);
}

void MainWindow::on_spinBox_2_valueChanged(int arg1)
{
    ui->widget->setTranslateX(arg1);
}

void MainWindow::on_spinBox_3_valueChanged(int arg1)
{
    ui->widget->setTranslateY(arg1);
}

.ui文件

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>721</width>
    <height>600</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QGridLayout" name="gridLayout">
    <item row="0" column="0" rowspan="2">
     <widget class="TestWidget" name="widget" native="true">
      <property name="minimumSize">
       <size>
        <width>500</width>
        <height>0</height>
       </size>
      </property>
      <property name="styleSheet">
       <string notr="true"/>
      </property>
     </widget>
    </item>
    <item row="0" column="1" rowspan="2">
     <layout class="QVBoxLayout" name="verticalLayout_2">
      <item>
       <layout class="QHBoxLayout" name="horizontalLayout">
        <item>
         <widget class="QLabel" name="label">
          <property name="text">
           <string>Angle</string>
          </property>
         </widget>
        </item>
        <item>
         <widget class="QSpinBox" name="spinBox">
          <property name="suffix">
           <string>°</string>
          </property>
          <property name="maximum">
           <number>360</number>
          </property>
          <property name="singleStep">
           <number>10</number>
          </property>
         </widget>
        </item>
       </layout>
      </item>
      <item>
       <layout class="QHBoxLayout" name="horizontalLayout_2">
        <item>
         <widget class="QLabel" name="label_2">
          <property name="text">
           <string>x轴</string>
          </property>
         </widget>
        </item>
        <item>
         <widget class="QSpinBox" name="spinBox_2">
          <property name="minimum">
           <number>-250</number>
          </property>
          <property name="maximum">
           <number>250</number>
          </property>
          <property name="singleStep">
           <number>10</number>
          </property>
         </widget>
        </item>
       </layout>
      </item>
      <item>
       <layout class="QHBoxLayout" name="horizontalLayout_3">
        <item>
         <widget class="QLabel" name="label_3">
          <property name="text">
           <string>y轴</string>
          </property>
         </widget>
        </item>
        <item>
         <widget class="QSpinBox" name="spinBox_3">
          <property name="minimum">
           <number>-250</number>
          </property>
          <property name="maximum">
           <number>250</number>
          </property>
          <property name="singleStep">
           <number>10</number>
          </property>
         </widget>
        </item>
       </layout>
      </item>
      <item>
       <spacer name="verticalSpacer">
        <property name="orientation">
         <enum>Qt::Vertical</enum>
        </property>
        <property name="sizeHint" stdset="0">
         <size>
          <width>20</width>
          <height>40</height>
         </size>
        </property>
       </spacer>
      </item>
     </layout>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>721</width>
     <height>22</height>
    </rect>
   </property>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <customwidgets>
  <customwidget>
   <class>TestWidget</class>
   <extends>QWidget</extends>
   <header>mainwindow.h</header>
   <container>1</container>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>

参考资料

  1. 数学基础–矩阵
  2. OpenGL,Qt实现:1入门篇(缩放,位移,旋转)
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Arui丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值