本文大多转载自《快速掌握PyQt5》第三十四章 图形视图框架
一、Graphicsview 是什么
1、Graphics View提供了一个平面,用于管理和交互大量自定义的2D图形图元,以及一个用于可视化图元的视图窗口小部件,支持缩放和旋转,包含了一套完整的事件体系,包括鼠标(Hover等)、键盘事件。
2、Graphics View使用BSP(二进制空间分区)树来提供非常快速的图元发现,因此,即使有数百万个图元,它也可以实时显示大型场景。
3、创建简单的基本流程
from PySide2 import QtCore, QtGui
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
import sys
class SingleNote(QGraphicsTextItem):
def __init__(self, parent=None, scene=None):
super(SingleNote, self).__init__(parent, scene)
self.setHtml("<strong>Hello</strong>")
#鼠标进入时,设置大小为2.0
def hoverEnterEvent(self, QGraphicsSceneHoverEvent):
self.setScale(2.0)
#鼠标离开时,设置大小为0.5
def hoverLeaveEvent(self, QGraphicsSceneHoverEvent):
self.setScale(0.5)
class NoteScene(QGraphicsScene):
def __init__(self, parent=None):
super(NoteScene, self).__init__(parent)
class MainView(QGraphicsView):
def __init__(self, parent=None):
super(MainView, self).__init__(parent)
if __name__ == "__main__":
app = QApplication(sys.argv)
# 创建一个QGraphicsView对象
mv = MainView()
#创建一个QGraphicsScence对象
scene = NoteScene()
#创建一个QGraphicsTextItem对象
item = SingleNote()
#为场景添加item
scene.addItem(item)
#为view设置场景
mv.setScene(scene)
mv.show()
app.exec_()
二、三个主要类
1、QGraphicsItem图元类
图元可以是文本、图片,规则几何图形或者任意自定义图形。该类已经提供了一些标准的图元,比如:
- 直线图元QGraphicsLineItem
- 矩形图元QGraphicsRectItem
- 椭圆图元QGraphicsEllipseItem
- 图片图元QGraphicsPixmapItem
- 文本图元QGraphicsTextItem
- 路径图元QGraphicsPathItem
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap, QColor, QPainterPath
from PyQt5.QtWidgets import QApplication, QGraphicsItem, QGraphicsLineItem, QGraphicsRectItem, QGraphicsEllipseItem, \
QGraphicsPixmapItem, QGraphicsTextItem, QGraphicsPathItem, QGraphicsScene, QGraphicsView
class Demo(QGraphicsView):
def __init__(self):
super(Demo, self).__init__()
self.resize(300, 300)
# 设置场景
self.scene = QGraphicsScene()
self.scene.setSceneRect(0, 0, 300, 300)
# 直线图元
self.line = QGraphicsLineItem()
self.line.setLine(100, 10, 200, 10)
# self.line.setLine(QLineF(100, 10, 200, 10))
# 矩形图元
self.rect = QGraphicsRectItem()
self.rect.setRect(100, 30, 100, 30)
# self.rect.setRect(QRectF(100, 30, 100, 30))
# 椭圆图元
self.ellipse = QGraphicsEllipseItem()
self.ellipse.setRect(100, 80, 100, 20)
# self.ellipse.setRect(QRectF(100, 80, 100, 20))
# 图片图元
self.pic = QGraphicsPixmapItem()
self.pic.setPixmap(QPixmap('img1.jpg').scaled(60, 60))
self.pic.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable) #可选择,可移动、
self.pic.setOffset(100, 120)
# self.pic.setOffset(QPointF(100, 120))
# 文本图元
self.text1 = QGraphicsTextItem()
self.text1.setPlainText('Hello PyQt5')
self.text1.setDefaultTextColor(QColor(66, 222, 88))
self.text1.setPos(100, 180)
self.text2 = QGraphicsTextItem()
self.text2.setPlainText('Hello World')
self.text2.setTextInteractionFlags(Qt.TextEditorInteraction) #可编辑
self.text2.setPos(100, 200)
self.text3 = QGraphicsTextItem()
self.text3.setHtml('<a href="https://baidu.com">百度</a>')
self.text3.setOpenExternalLinks(True)
self.text3.setTextInteractionFlags(Qt.TextBrowserInteraction) #可跳转到其他网页
self.text3.setPos(100, 220)
# 路径图元
self.path = QGraphicsPathItem()
self.tri_path = QPainterPath()
self.tri_path.moveTo(100, 250)
self.tri_path.lineTo(130, 290)
self.tri_path.lineTo(100, 290)
self.tri_path.lineTo(100, 250)
self.tri_path.closeSubpath()
self.path.setPath(self.tri_path)
# 添加图元到场景中
self.scene.addItem(self.line)
self.scene.addItem(self.rect)
self.scene.addItem(self.ellipse)
self.scene.addItem(self.pic)
self.scene.addItem(self.text1)
self.scene.addItem(self.text2)
self.scene.addItem(self.text3)
self.scene.addItem(self.path)
# 设置场景
self.setScene(self.scene)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
运行结果:
QGraphicsItem支持以下特性:
- 鼠标按下、移动、释放和双击事件,以及鼠标悬浮事件、滚轮事件和右键
- 菜单事件
- 键盘输入事件
- 拖放事件
- 分组
- 碰撞检测
事件的传递顺序
视图->场景->图元
import sys
from PyQt5.QtWidgets import QApplication, QGraphicsRectItem, QGraphicsScene, QGraphicsView
class CustomItem(QGraphicsRectItem):
def __init__(self):
super(CustomItem, self).__init__()
self.setRect(100, 30, 100, 30)
def mousePressEvent(self, event):
print('event from QGraphicsItem')
super().mousePressEvent(event)
class CustomScene(QGraphicsScene):
def __init__(self):
super(CustomScene, self).__init__()
self.setSceneRect(0, 0, 300, 300)
def mousePressEvent(self, event):
print('event from QGraphicsScene')
super().mousePressEvent(event)
class CustomView(QGraphicsView):
def __init__(self):
super(CustomView, self).__init__()
self.resize(300, 300)
def mousePressEvent(self, event):
print('event from QGraphicsView')
super().mousePressEvent(event)
if __name__ == '__main__':
app = QApplication(sys.argv)
view = CustomView()
scene = CustomScene()
item = CustomItem()
scene.addItem(item)
view.setScene(scene)
view.show()
sys.exit(app.exec_())
输出结果:
event from QGraphicsView
event from QGraphicsScene
event from QGraphicsItem
子图元传递到父图元
import sys
from PyQt5.QtWidgets import QApplication, QGraphicsRectItem, QGraphicsScene, QGraphicsView
class CustomItem(QGraphicsRectItem):
def __init__(self, num):
super(CustomItem, self).__init__()
self.setRect(100, 30, 100, 30)
self.num = num
def mousePressEvent(self, event):
print('event from QGraphicsItem{}'.format(self.num))
super().mousePressEvent(event)
if __name__ == '__main__':
app = QApplication(sys.argv)
view = QGraphicsView()
scene = QGraphicsScene()
item1 = CustomItem(1)
item2 = CustomItem(2)
item2.setParentItem(item1)
scene.addItem(item1)
view.setScene(scene)
view.show()
sys.exit(app.exec_())
输出结果:
event from QGraphicsItem2
event from QGraphicsItem1
图元分组
所谓分组也就是将各个图元进行分类,分到一起的图元就会共同行动(选中、移动以及复制等)。我们通过下面的代码来演示下:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPen, QBrush
from PyQt5.QtWidgets import QApplication, QGraphicsItem, QGraphicsRectItem, QGraphicsEllipseItem, QGraphicsScene, \
QGraphicsView, QGraphicsItemGroup
class Demo(QGraphicsView):
def __init__(self):
super(Demo, self).__init__()
self.resize(300, 300)
self.scene = QGraphicsScene()
self.scene.setSceneRect(0, 0, 300, 300)
# 1
self.rect1 = QGraphicsRectItem()
self.rect2 = QGraphicsRectItem()
self.ellipse1 = QGraphicsEllipseItem()
self.ellipse2 = QGraphicsEllipseItem()
self.rect1.setRect(100, 30, 100, 30)
self.rect2.setRect(100, 80, 100, 30)
self.ellipse1.setRect(100, 140, 100, 20)
self.ellipse2.setRect(100, 180, 100, 50)
# 2
pen1 = QPen(Qt.SolidLine)
pen1.setColor(Qt.blue)
pen1.setWidth(3)
pen2 = QPen(Qt.DashLine)
pen2.setColor(Qt.red)
pen2.setWidth(2)
brush1 = QBrush(Qt.SolidPattern)
brush1.setColor(Qt.blue)
brush2 = QBrush(Qt.SolidPattern)
brush2.setColor(Qt.red)
self.rect1.setPen(pen1)
self.rect1.setBrush(brush1)
self.rect2.setPen(pen2)
self.rect2.setBrush(brush2)
self.ellipse1.setPen(pen1)
self.ellipse1.setBrush(brush1)
self.ellipse2.setPen(pen2)
self.ellipse2.setBrush(brush2)
# 3
self.group1 = QGraphicsItemGroup()
self.group2 = QGraphicsItemGroup()
self.group1.addToGroup(self.rect1)
self.group1.addToGroup(self.ellipse1)
self.group2.addToGroup(self.rect2)
self.group2.addToGroup(self.ellipse2)
self.group1.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable)
self.group2.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable)
print(self.group1.boundingRect())
print(self.group2.boundingRect())
# 4
self.scene.addItem(self.group1)
self.scene.addItem(self.group2)
self.setScene(self.scene)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
输出结果:
碰撞检测
碰撞检测可以以边界为范围或者以形状为范围,如图,一个椭圆图元,边界为虚线,形状为实线
Qt.ContainsItemBoundingRect
0x2
以边界为范围,当前图元被其他图元完全包含住
Qt.IntersectsItemBoundingRect
0x3
以边界为范围,当前图元被完全包含或者与其他图元有交集
几种具体的检测方式:
常量 | 值 | 触发条件 |
---|---|---|
Qt.ContainsItemShape | 0x0 | 以形状为范围,当前图元被其他图元完全包含住 |
Qt.IntersectsItemShape | 0x1 | 以形状为范围,当前图元被完全包含或者与其他图元有交集 |
Qt.ContainsItemBoundingRect | 0x2 | 以边界为范围,当前图元被其他图元完全包含住 |
Qt.IntersectsItemBoundingRect | 0x3 | 以边界为范围,当前图元被完全包含或者与其他图元有交集 |
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QGraphicsItem, QGraphicsRectItem, QGraphicsEllipseItem, QGraphicsScene, \
QGraphicsView
class Demo(QGraphicsView):
def __init__(self):
super(Demo, self).__init__()
self.resize(300, 300)
self.scene = QGraphicsScene()
self.scene.setSceneRect(0, 0, 300, 300)
self.rect = QGraphicsRectItem()
self.ellipse = QGraphicsEllipseItem()
self.rect.setRect(120, 30, 50, 30)
self.ellipse.setRect(100, 180, 100, 50)
self.rect.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)
self.ellipse.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)
self.scene.addItem(self.rect)
self.scene.addItem(self.ellipse)
self.setScene(self.scene)
def mouseMoveEvent(self, event):
if self.ellipse.collidesWithItem(self.rect, Qt.IntersectsItemBoundingRect):
print(self.ellipse.collidingItems(Qt.IntersectsItemShape))
super().mouseMoveEvent(event)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
信号与槽
QGraphicsItem不继承自QObject,所以本身并不能使用信号和槽机制,我们也无法给它添加动画。不过我们可以自定义一个类,并让该类继承自QGraphicsObject。
import sys
from PyQt5.QtCore import QPropertyAnimation, QPointF, QRectF, pyqtSignal
from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsView, QGraphicsObject
class CustomRect(QGraphicsObject):
# 1
my_signal = pyqtSignal()
def __init__(self):
super(CustomRect, self).__init__()
# 2
def boundingRect(self):
return QRectF(0, 0, 100, 30)
# 3
def paint(self, painter, styles, widget=None):
painter.drawRect(self.boundingRect())
class Demo(QGraphicsView):
def __init__(self):
super(Demo, self).__init__()
self.resize(300, 300)
# 4
self.rect = CustomRect()
self.rect.my_signal.connect(lambda: print('signal and slot'))
self.rect.my_signal.emit()
self.scene = QGraphicsScene()
self.scene.setSceneRect(0, 0, 300, 300)
self.scene.addItem(self.rect)
self.setScene(self.scene)
# 5
self.animation = QPropertyAnimation(self.rect, b'pos')
self.animation.setDuration(3000)
self.animation.setStartValue(QPointF(100, 30))
self.animation.setEndValue(QPointF(100, 200))
self.animation.setLoopCount(-1)
self.animation.start()
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
2、QGraphicsScene场景类
场景提供了很多用于管理图元的方法
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap, QTransform
from PyQt5.QtWidgets import QApplication, QGraphicsItem, QGraphicsScene, QGraphicsView
class Demo(QGraphicsView):
def __init__(self):
super(Demo, self).__init__()
self.resize(300, 300)
self.scene = QGraphicsScene()
self.scene.setSceneRect(0, 0, 300, 300)
# 1
self.rect = self.scene.addRect(100, 30, 100, 30)
self.ellipse = self.scene.addEllipse(100, 80, 50, 40)
self.pic = self.scene.addPixmap(QPixmap('pic.png').scaled(60, 60))
self.pic.setOffset(100, 130)
self.rect.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsFocusable)
self.ellipse.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsFocusable)
self.pic.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsFocusable)
self.setScene(self.scene)
# 2
print(self.scene.items())
print(self.scene.items(order=Qt.AscendingOrder))
print(self.scene.itemsBoundingRect())
print(self.scene.itemAt(110, 40, QTransform()))
# 3
self.scene.focusItemChanged.connect(self.my_slot)
def my_slot(self, new_item, old_item):
print('new item: {}\nold item: {}'.format(new_item, old_item))
# 4
def mouseMoveEvent(self, event):
print(self.scene.collidingItems(self.ellipse, Qt.IntersectsItemShape))
super().mouseMoveEvent(event)
# 5 还需要修改
def mouseDoubleClickEvent(self, event):
item = self.scene.itemAt(event.pos(), QTransform())
self.scene.removeItem(item)
super().mouseDoubleClickEvent(event)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
3 QGraphicsView视图类
视图其实是一个滚动区域,用于显示场景
import sys
from PyQt5.QtCore import Qt, QRectF
from PyQt5.QtGui import QColor, QBrush
from PyQt5.QtWidgets import QApplication, QGraphicsItem, QGraphicsScene, QGraphicsView
class Demo(QGraphicsView):
def __init__(self):
super(Demo, self).__init__()
self.resize(300, 300)
self.scene = QGraphicsScene()
self.scene.setSceneRect(0, 0, 500, 500)
self.ellipse = self.scene.addEllipse(QRectF(200, 200, 50, 50), brush=QBrush(QColor(Qt.blue)))
self.rect = self.scene.addRect(QRectF(300, 300, 50, 50), brush=QBrush(QColor(Qt.red)))
self.ellipse.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable)
self.rect.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable)
self.setScene(self.scene)
self.press_x = None
# 1
def wheelEvent(self, event):
if event.angleDelta().y() < 0:
self.scale(0.9, 0.9)
else:
self.scale(1.1, 1.1)
# super().wheelEvent(event)
# 2
def mousePressEvent(self, event):
self.press_x = event.x()
# super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if event.x() > self.press_x:
self.rotate(10)
else:
self.rotate(-10)
# super().mouseMoveEvent(event)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
三、图形视图的坐标体系
场景坐标
视图坐标
图元坐标
图形视图提供了三种坐标系之间相互转换的函数,以及图元与图元之间的转换函数
- QGraphicsView.mapToScene() 视图到场景
- QGraphicsView.mapFromScene() 场景到视图
- QGraphicsItem.mapFromScene() 场景到图元
- QGraphicsItem.mapToScene() 图元到场景
- QGraphicsItem.mapToParent() 子图元到父图元
- QGraphicsItem.mapFromParent() 父图元到子图元
- QGraphicsItem.mapToItem() 当前图元到其他图元
- QGraphicsItem.mapFromItem() 其他图元到当前图元
import sys
from PyQt5.QtGui import QPixmap, QTransform
from PyQt5.QtWidgets import QApplication, QGraphicsItem, QGraphicsScene, QGraphicsView
class Demo(QGraphicsView):
def __init__(self):
super(Demo, self).__init__()
self.resize(600, 600)
self.scene = QGraphicsScene()
self.scene.setSceneRect(0, 0, 300, 300)
self.rect = self.scene.addRect(100, 30, 100, 30)
self.ellipse = self.scene.addEllipse(100, 80, 50, 40)
self.pic = self.scene.addPixmap(QPixmap('pic.png').scaled(60, 60))
self.pic.setOffset(100, 130)
self.rect.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable)
self.ellipse.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable)
self.pic.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable)
self.setScene(self.scene)
def mouseDoubleClickEvent(self, event):
item = self.scene.itemAt(event.pos(), QTransform())
self.scene.removeItem(item)
super().mouseDoubleClickEvent(event)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())