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θ00−sinθcosθ00001 × xyz1 = xcosθ×y−sinθ×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θ0−sinθ00100sinθ0cosθ00001 × xyz1 = cosθ×x+sinθ×zy−sinθ×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θ00−sinθcosθ0000100001 × xyz1 = cosθ×x−sinθ×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°,):
有了矩阵的相关基础知识,可以开始实际代码的编写。具体功能如下:
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
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
#include
#include
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文件
<ui version="4.0">
<class>MainWindowclass>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0x>
<y>0y>
<width>721width>
<height>600height>
rect>
property>
<property name="windowTitle">
<string>MainWindowstring>
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>500width>
<height>0height>
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>Anglestring>
property>
widget>
item>
<item>
<widget class="QSpinBox" name="spinBox">
<property name="suffix">
<string>°string>
property>
<property name="maximum">
<number>360number>
property>
<property name="singleStep">
<number>10number>
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>-250number>
property>
<property name="maximum">
<number>250number>
property>
<property name="singleStep">
<number>10number>
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>-250number>
property>
<property name="maximum">
<number>250number>
property>
<property name="singleStep">
<number>10number>
property>
widget>
item>
layout>
item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Verticalenum>
property>
<property name="sizeHint" stdset="0">
<size>
<width>20width>
<height>40height>
size>
property>
spacer>
item>
layout>
item>
layout>
widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0x>
<y>0y>
<width>721width>
<height>22height>
rect>
property>
widget>
<widget class="QStatusBar" name="statusbar"/>
widget>
<customwidgets>
<customwidget>
<class>TestWidgetclass>
<extends>QWidgetextends>
<header>mainwindow.hheader>
<container>1container>
customwidget>
customwidgets>
<resources/>
<connections/>
ui>