坐标变换QTransform
前面介绍的绘图都是在窗口坐标系下进行的,窗口坐标系的原点在屏幕的左上角,x轴水平向右,y轴竖直向下。使用窗口坐标系经常会不太方便,例如绘制一个对称的多边形时,需要计算出多边形的顶点坐标,这样比较麻烦,如果能把坐标系的原点移到对称多边形的中心,在移动后的坐标系中计算顶点坐标就比较简单了。
用QPainter 提供的变换坐标系方法进行坐标系变换
PySide6 提供了两种变换坐标系的方法
- 一种方法是使用QPainter 提供的变换坐标系的方法
- 另外一种方法是使用QTransform类。
QPainter 提供的变换坐标系的方法如表所示,可以对坐标系进行平移、缩放、旋转和错切。
对于错切 shear(sx,sy)
方法的理解为,设(x0,y0)是变换前的一个点的坐标,则错切后的坐标是(sx*yo+x0,sy* x+y0)
。
变换坐标系的方法 | 说 明 |
---|---|
translate(Union[QPointF,QPoint]) | 平移坐标系 |
translate(dx: float,dy: float) | 平移坐标系 |
rotate(float) | 旋转坐标系 |
scale(sx:float,sy: float) | 缩放坐标系 |
shear(sh: float,sv: float) | 错切坐标系 |
resetTransform() | 重置坐标系 |
save() | 保存当前绘图状态 |
restore() | 恢复绘图状态 |
下面的程序首先建立一个myPainterTransform类它继承自QWidget在该类中采用坐标变换的方法,重绘前面用到的太极图,并通过参数控制是否对太极图进行旋转、缩放和平移,这个 myPainterTransform 类相当于自定义的控件。在主程序类中,建立了四个myPainterTransform类的实例对象第一个能够旋转第二个能够缩放第三个能够平动第四个错切不动。
# -*- coding: UTF-8 -*-
# File date: Hi_2023/3/8 16:18
# File_name: 10-用QPainter 提供的变换坐标系方法进行坐标系变换.py
from PySide6.QtWidgets import QApplication,QWidget,QSplitter,QHBoxLayout
from PySide6.QtGui import QPen,QPainter,QPainterPath,QBrush,QPalette,QTransform
from PySide6.QtCore import QPointF,Qt.QTimer
import sys
class myPainterTransform(QWidget): # 用坐标变换的方法创建太极图像
def __init__(self,rotational=False,scaled=False,translational=False,sheared=False,parent=None):
super().__init__(parent)
palette = self.palette()
palette.setColor(QPalette.Window,Qt.darkYellow)
self.setPalette(palette)# 设置窗口背景
self.setAutoFillBackground(True)
self.__rotational = rotational # 获取输人的参数值
self.__scaled = scaled # 获取输人的参数值
self.__translational = translational # 获取输人的参数值
self.__sheared = sheared # 获取输人的参数值
self.__rotation = 0 # 旋转角度
self.__scale = 1 # 缩放系数
self.__translation = 0 # 平移量
self.__sx = 0 # 错切系数
self.__sy = 0 # 错切系数
self.timer = QTimer(self)
self.timer.timeout.connect(self.timeout)
self.timer.setInterval(10)
self.timer.start()# 定时器
def paintEvent(self,event):
self.center = QPointF(self.width()/ 2,self.height()/ 2)
painter = QPainter(self)
painter.translate(self.center)# 将坐标系移动到中心位置
pen = QPen()
pen.setWidth(3)
pen.setColor(Qt.black)
painter.setPen(pen)
path = QPainterPath()# 路径
r = min(self.width(),self.height())/ 3 # 外部大圆的半径
r1 = r / 7 # 内部小圆的半径
path.moveTo(0,-r)
path.arcTo(-r,-r,2 * r,2 * r,90,360)# 外部大圆
path.arcTo(-r,-r,2 * r,2 * r,90,-180)# 反向半圆
path.moveTo(0,r)
path.arcTo(-r / 2,0,r,r,-90,180)# 内部半圆
path.arcTo(-r / 2,-r,r,r,270,-180)# 内部半圆
path.moveTo(r1,-r / 2)# 内部小圆
path.arcTo(-r1,-r / 2 - r1,2 * r1,2 * r1,0,360)
path.moveTo(r1,r / 2)# 内部小圆
path.arcTo(-r1,r / 2 - r1,2 * r1,2 * r1,0,-360)
path.setFillRule(Qt.WindingFill)# 填充方式
brush = QBrush(Qt.SolidPattern)
painter.setBrush(brush)# 设置画刷
painter.rotate(self.__rotation)# 坐标系旋转
painter.scale(self.__scale,self.__scale)# 坐标系缩放
painter.translate(self.__translation,0)# 坐标系平移
if self.__sheared:
painter.shear(self.__sx,self.__sy)
painter.drawPath(path)# 绘制路径
super().paintEvent(event)
def timeout(self):
if self.__rotational: # 设置坐标系的旋转角度值参数
if self.__rotation < - 360:
self.__rotation = 0
self.__rotation = self.__rotation - 1
if self.__scaled: # 设置坐标系的缩放比例参数
if self.__scale > 2:
self.__scale = 0.2
self.__scale = self.__scale + 0.005
if self.__translational: # 设置坐标系的平移量参数
if self.__translation > self.width()/ 2 + min(self.width(),self.height())/ 3:
self.translation = - self.width()/ 2 - min(self.width(),self.height())/ 3
self.translation = self.__translation + 1
self.update()
def setShearFactor(self,sx=0,sy=0):
self.__sx = sx
self.__sy = sy
class MyWindow(QWidget):
def __init__(self,parent=None):
super().__init__(parent)
self.setupUi()
self.resize(800,600)
def setupUi(self):
h = QHBoxLayout(self)# 布局
splitter_1 = QSplitter(Qt.Horizontal)
splitter_2 = QSplitter(Qt.Vertical)
splitter_3 = QSplitter(Qt.Vertical)
h.addWidget(splitter_1)
splitter_1.addWidget(splitter_2)
splitter_1.addWidget(splitter_3)
taiji_1 = myPainterTransform(rotational=True)# 第一个太极图,能够旋转
taiji_2 = myPainterTransform(scaled=True)# 第二个太极图,能够缩放
taiji_3 = myPainterTransform(translational=True)# 第三个太极图,能够平动
taiji_4 = myPainterTransform(sheared=True)# 第四个太极图,错切
taiji_4.setShearFactor(0.4,0.2)# 设置错切系数
splitter_2.addWidget(taiji_1)
splitter_2.addWidget(taiji_2)
splitter_3.addWidget(taiji_3)
splitter_3.addWidget(taiji_4)
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MyWindow()
win.show()
sys.exit(app.exec())
用QTransform方法进行坐标系变换
采用坐标变换 QTransform可以进行更复杂的变换。
QTransform 是一个3X3的阵,用QTransform类创建变换矩阵的方法如下所示其中 hij参数是矩阵的元素值,类型都是 float。
from PySide6.QtGui import QTransform
QTransform(self)-> None
QTransform(h11: float,h12: float,h13: float,h21: float,h22: float,h23: float,h31: float,h32: float,h33: float)-> None
QTransform(h11: float,h12: float,h21: float,h22: float,dx: float,dy: float)-> None
QTransform(Other: PySide6.QtGui.QTransform)-> None
- 参数h11和h22是沿x轴和y轴方向的缩放比例;
- h31和32是沿x轴和y轴方向的位移dx和d;
- h21和h12是沿x轴和轴方的错切:
- h13和h23是x轴和y轴方向的投影;
- h33 是附加投影系数,通常取 1
对于二维空间中的一个坐标(x,y),可用(x,y,k)表示,其中k是一个不为0的缩放比例系数。当k-1时,坐标可以表示成(x,y,1),通过变换矩阵,可以得到新的坐标(x,y,1),
-
用变换矩阵可以表示成
-
对于沿着x和y方向的平移可以表示成
-
对于沿着x和y方向的缩放可以表示成
-
对于绕z轴旋转θ角可以表示成
-
对于错切可以表示成
如果要进行多次不同的变换,可以将以上变换矩阵依次相乘,得到总的变换矩阵。以上是针对二维图形的变换,这些方法也可推广到三维变换
QTransform的常用方法如表所示
QTransform的方法及参数类型 | 返回值的类型 | 说明 |
---|---|---|
rotate(a:float,axis: Qt.Axis = Qt.ZAxis) | QTransform | 获取以度表示的旋转矩阵,axis可取 Qt.XAxis,Qt.YAxis 或 Qt.ZAxis |
rotateRadians(a: float,axis:Qt.Axis= Qt.ZAxis) | QTransform | 获取以弧度表示的旋转矩阵 |
scale(sx: float,sy: float) | QTransform | 获取缩放矩阵 |
shear(sh:float,sv: float) | QTransform | 获取错切矩阵 |
translate(dx: float,dy: float) | QTransform | 获取平移矩阵 |
dx()、dy() | float | 获取平移量 |
setMatrix(m11: float,m12: float,m13: float,m21: float,m22: float,m23: float,m31: float,m32: float,m33:float) | None | 设置矩阵的各个值 |
m11()、m12()、m13()、m21()、m22()、 m23()、m31()、m32()、m33() | float | 获取矩阵的各个值 |
transposed() | QTransform | 获取转置矩阵 |
isInvertible() | bool | 获取是否可逆 |
inverted() | Tuple[Tuple,bool] | 获取逆矩阵 |
isIdentity() | bool | 获取是否是单位矩阵 |
isAffine() | bool | 获取是否是放射变换 |
isRotating() | bool | 获取是否只是旋转变换 |
isScaling() | bool | 获取是否只是缩放变换 |
is Translating() | bool | 获取是否只是平移变换 |
adjoint() | QTransform | 获取共轭矩阵 |
determinant() | float | 获取矩阵的秩 |
reset() | None | 重置矩阵,对角线值为1,其他全部 为0 |
map(x:float,y: float) | Tuple[float,float] | 变换坐标值,即坐标值与变换矩阵 相乘 |
map(Union[QPointF,QPoint]) | QPointF | 变换点 |
map(Union[QLineF,QLine]) | QLineF | 变换线 |
map(Union[QPolygon,Sequence[QPoint],QRect]) | QPolygon | 变换多点到多边形 |
map(Union[QPolygonF,Sequence[QPointF],QPolygon.QRectF]) | QPolygonF | 变换多点到多边形 |
map(Union[QRegion,QBitmap,QPolygon,QRect]) | QRegion | 变换区域 |
map(p: QPainterPath) | QPainterPath | 变换路径 |
mapRect(Union[QRectF,QRect]) | QRectF | 变换矩形 |
mapToPolygon(QRect) | QPolygon | 将矩形变换到多边形 |
[static]fromScale(sx:float,sy: float) | QTransform | 由缩放量获取变换矩阵 |
[static]fromTranslate(dx: float,dy: float) | QTransform | 由平移量获取变换矩阵 |
-
QPainter 用setTransform(QTransform,combine = False)或 setWorldTransform(QTransform;combine=False)方法设置变换矩阵
- 参数 combine表示是否在现有的变换上叠加新的变换矩阵,如果是 False 则设置新的变换矩阵;
-
用transform()方法获取变换矩阵;
-
用resetTransform()方法重置变换矩阵包括 window 和 viewport 的设置;
-
用combinedTransform()方法获取 window、viewport 和 world 的组合变换矩阵;
-
用deviceTransform(方法获取从逻辑坐标到设备坐标的变换矩阵。
下面的程序在新坐标系中绘制一个五角星。
窗口坐标系的原点在屏幕的左上角,从左到右是x轴,从上到下是轴。在屏幕坐标系下绘制几何图形时通常不方便。
下面的程序先利用translate()方法将坐标系的原点平移到屏幕的中心位置,再利用rotate()方法将坐标系沿着x轴旋转180,此时坐标系的y轴竖直向上,这就是我们通常意义上的坐标系的方向。
在绘制图形时,几何点位置直接在新坐标系中计算即可。
# -*- coding: UTF-8 -*-
# File date: Hi_2023/3/8 17:21
# File_name: 11-用QTransform方法进行坐标系变换.py
import sys,math
from PySide6.QtWidgets import QApplication,QWidget
from PySide6.QtGui import QPainter,QTransform
from PySide6.QtCore import QPointF,Qt
class MyWindow(QWidget):
def __init__(self,parent=None):
super().__init__(parent)
self.resize(600,500)
def paintEvent(self,event):
transform = QTransform()
transform.translate(self.width()/ 2,self.height()/ 2)# 沿着x轴旋转 180,轴向上
transform.rotate(180,Qt.Axis.XAxis)# 原点平移到窗口中心位置
painter = QPainter(self)
painter.setTransform(transform)# 设置变换
r = 100 # 五角星的外接圆半径
points = list()
for i in range(5):
x = r * math.cos((90 + 144 * i)* math.pi / 180)
y = r * math.sin((90 + 144 * i)* math.pi / 180)
points.append(QPointF(x,y))
painter.drawPolygon(points)# 绘制多边形
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MyWindow()
win.show()
sys.exit(app.exec())