深入了解PyQt5中的图形视图框架

目录
  • 1.QGraphicsItem图元类
  • 2.QGraphicsScene场景类
  • 3.QGraphicsView视图类
  • 4.图形视图的坐标体系
  • 5.小结

在之前的章节中,笔者一般使用QLabel控件来显示图片。但是,如果要使用很多图片怎么办?难道要实例化很多个QLabel控件来一一显示?那如何管理呢?当然,我们不可能会用QLabel控件来做这样的事,否则会非常麻烦和混乱。PyQt5中的图形视图可以让我们管理大量的自定义2D图元并与之交互。该框架使用BSP(Binary Space Partitioning - 二叉空间分割)树,以快速查找图形元素。所以就算一个视图场景中包含数百万的图元,它也可以实时进行显示。如果要用PyQt5来制作稍微复杂点的游戏的话,图形视图是必定要用到的。

图形视图框架主要包含三个类:QGraphicsItem图元类、QGraphicsScene场景类和QGraphicsView视图类。简单一句话来概括下三者的关系就是:图元放在场景上,场景内容通过视图来显示。下面我们来一一进行讲解。

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__()
        # 1
        self.resize(300, 300)

        # 2
        self.scene = QGraphicsScene()
        self.scene.setSceneRect(0, 0, 300, 300)

        # 3
        self.line = QGraphicsLineItem()
        self.line.setLine(100, 10, 200, 10)
        # self.line.setLine(QLineF(100, 10, 200, 10))

        # 4
        self.rect = QGraphicsRectItem()
        self.rect.setRect(100, 30, 100, 30)
        # self.rect.setRect(QRectF(100, 30, 100, 30))

        # 5
        self.ellipse = QGraphicsEllipseItem()
        self.ellipse.setRect(100, 80, 100, 20)
        # self.ellipse.setRect(QRectF(100, 80, 100, 20))

        # 6
        self.pic = QGraphicsPixmapItem()
        self.pic.setPixmap(QPixmap('pic.png').scaled(60, 60))
        self.pic.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable)
        self.pic.setOffset(100, 120)
        # self.pic.setOffset(QPointF(100, 120))

        # 7
        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" rel="external nofollow" >百度</a>')
        self.text3.setOpenExternalLinks(True)
        self.text3.setTextInteractionFlags(Qt.TextBrowserInteraction)
        self.text3.setPos(100, 220)

        # 8
        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)

        # 9
        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)

        # 10
        self.setScene(self.scene)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    demo = Demo()
    demo.show()
    sys.exit(app.exec_())

1. 该类直接继承QGraphicsView,那么窗口就是视图,且大小为300x300;

2. 实例化一个QGraphicsScene场景,并调用setSceneRect(x, y, w, h)方法来设置场景坐标原点和大小。从代码中我们得知坐标原点为(0, 0),之后往场景中添加的图元就会都根据该坐标来设置位置(关于坐标的更多内容,笔者会在34.4小节中进行讲解)。场景的大小为300x300,跟视图大小一样;

3. 实例化一个QGraphicsLineItem直线图元,并调用setLine()方法设置直线两端的坐标。该方法既可以直接传入四个数值,也可以传入一个QLineF对象。文档里写的非常清楚:

4-5. 跟直线图元类似,这里分别实例化矩形图元和椭圆图元,并调用相应的方法来设置位置和大小;

6. 实例化一个图片图元,并调用setPixmap()方法设置图片,QPixmap对象有个scaled()方法可以设置图片的大小(当然我们也可以使用QGraphicsItem的setScale()方法来设置),接着我们设置该图元的Flag属性,让他可以被选中以及移动,这是所有图元共有的方法。最后调用setOffset()方法来设置图片相对于场景坐标原点的偏移量;

7. 这里实例化了三个文本图元,分别显示普通绿色文本,可编辑文本以及超链接文本(HTML)。setDefaultColor()方法可以用来设置文本的颜色,setPos()用来设置文本图元相对于场景坐标原点的位置(该方法是所有图元共有的方法,我们当然也可以使用在其他类型的图元上)。

setTextInteractionFlags()用来设置文本属性,这里的Qt.TextEditorInteraction参数表示为可编辑属性(相当于在QTextEdit上编辑文本),最后的Qt.TextBrowserInteraction表明该文本用于浏览(相当于在QTextBrowser上的文本)。有关更多的属性,大家可以在文档里搜索Qt::TextInteractionFlags来了解。

当然如果要让超链接文本能够被打开,我们还需要使用setOpenExternalLinks()方法,传入一个True参数即可。

8. 路径图元可以用于显示任意形状的图形,setPath()方法需要传入一个QPainterPath对象,而我们就是用该对象来进行绘画操作的。moveTo()方法表示将画笔移动到相应位置上,lineTo()表示画一条直线,closeSubpath()方法表示当前作画结束 (查阅文档来了解更多有关QPaintPath对象的方法),这里我们画了一个直角三角形;

9. 调用场景的addItem()方法将所有图元添加进来;

10. 调用setScene()方法来让场景居中显示在视图中。

运行截图如下:

图片可以被选中和移动:

Hello World文本可以被编辑:

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_())

图元,场景和视图其实都有各自的事件函数,我们在上面分别继承了QGraphicsRectItem, QGraphicsScene以及QGraphicsView并重新实现了各自的mousePressEvent()事件函数,在其中我们都打印一句话来让用户知道是哪个函数被执行了。

运行截图如下:

我们在矩形框内点击之后,发现控制台输入如下信息:

由此可见,事件的传递顺序为视图->场景->图元。有一点大家需要注意,重新实现事件函数的话我们必须要调用相应的父类事件函数,否则事件无法顺利传递下去。假如我把CustomView类中事件函数下的super().mousePressEvent(event)这行代码删除掉,那么控制台只会输出"event from QGraphicsView":

一个图元中可以添加另一个图元(一个图元可以是另一个图元的父类),那此时图元之间的事件传递顺序又是如何的呢?请看下面代码:

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_())

因为实例化的是两个一样的矩形图源,为了进行区分,我们在CustomItem的初始化函数中加入一个num参数,然后在事件函数中打印出实例化时所传入的数字即可。

调用setParentItem()方法将item1设置为item2的父类,然后将item1添加到场景中(item2自然也被加入)。

运行截图如下:

在矩形框中点击,控制台打印如下:

由此可见,事件是由子图元传递到父图元的。同理,如果不加super().mousePressEvent(event),那么事件就会停止传递,最后也就只会显示"event from QGraphicsItem2":

请大家一定要搞清楚事件的传递顺序,这样才能更好地使用图形视图框架。

所谓分组也就是将各个图元进行分类,分到一起的图元就会共同行动(选中、移动以及复制等)。我们通过下面的代码来演示下:

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_())

1. 实例化四个图元,两个为矩形,两个为椭圆,并调用setRect()方法设置坐标和大小;

2. 实例化两种画笔和两种画刷,用于图元的样式设置;

3. 实例化两个QGraphicsGroup分组对象,并将矩形和椭圆都添加进来。rect1和ellipse1在group1里,而rect2和ellipse2在group2里。接着调用setFlags()方法设置属性,让分组可以选中和移动。boundRect()方法放回一个QRectF值,该值可以显示出分组的边界位置和大小;

4. 将分组添加到场景当中。

运行截图如下:

蓝色的矩形和椭圆为一组,可同时选中和移动,红色的同理。黑色边框即为边界,其位置和大小可用boundRect()方法来获取。通过下面的截图我们可以发现QGraphicsItemGroup的边界的位置和大小由其中的图元整体所决定:

碰撞检测在游戏中的用处非常大,比如在飞机大战游戏中,如果子弹没有和敌机做碰撞检测处理的话,那敌机就不会被消灭,奖励也不会增加,游戏也就没有什么意思。我们通过下面这个例子来带大家了解如何对图元进行碰撞检测:

界面上有一个矩形图元和一个椭圆图元,两者都可以选中和移动。我们就对两者进行碰撞检测。在此之前我们先了解下boundingRect()边界和shape()形状的区别。请看下方的椭圆图元:

当选中这个图元时,虚线部分显示的就是该图元的边界,而形状就指的是图元本身,也就是黑色实线部分。碰撞检测可以以边界为范围或者以形状为范围。假如我们在代码中以边界为范围,那椭圆的虚线跟矩形图元一碰到,就会触发碰撞检测;如果以形状为范围,那只有在椭圆的黑色实线跟矩形碰到的情况下,碰撞检测才会触发。

下面是几种具体的检测方式:

下面请看代码示例:

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_())

初始化函数中的代码想必大家都懂了,这里就不再讲述,我们重点来看mouseMoveEvent()事件函数。

我们调用椭圆图元的collidesWithItem()方法来指定要与之进行碰撞检测的其他图元以及检测方式。其他图元指的就是矩形图元,而且我们可以看到这里是以椭圆的边界为范围,而且只要两个图元有交集就会触发检测。如果碰撞条件成立,那么collidesWithItem()就会返回一个True,那么此时if条件判断也就成立。

collidingItems()方法在指定检测方式后可以返回所有符合碰撞条件的其他图元,返回值类型为列表。这里的检测方式是以形状为范围的,同样有交集即可。

那mouseMoveEvent()事件函数所要表达的意思就是:当椭圆的边界和矩形接触,那么if条件判断就成立,不过此时打印的还只是空列表,因为椭圆本身(黑色实线)并还没有跟矩形有所接触。不过当接触了之后控制台就会输出包含矩形图元的列表了。

请大家调用矩形图元的collidesWithItem()和collidingItems()方法来尝试下,看看有什么不同。也就是把mouseMoveEvent()事件函数修改如下:

def mouseMoveEvent(self, event):
    if self.rect.collidesWithItem(self.ellipse, Qt.IntersectsItemBoundingRect):
        print(self.rect.collidingItems(Qt.IntersectsItemShape))
    super().mouseMoveEvent(event)

出于性能考虑,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_())

1. 自定义一个信号;

2-3. 继承QGraphicsObject的话,我们最好把boundingRect()和paint()方法重新实现下。在boundingRect()中我们返回一个QRectF类型值来确定CustomRect的默认位置和大小。在paint()中调用drawRect()方法将矩形画到界面上;

4. 将自定义的信号和槽函数连接,槽函数中打印“signal and slot”字符串。接着调用信号的emit()方法来发射信号,那么槽函数也就会启动了;

5. 加上QPropertyAnimation属性动画,将矩形从(100, 30)移动到(100, 200),时间为3秒,动画无限循环。

运行截图如下,矩形图元从上而下缓缓移动:

控制台打印内容:

2.QGraphicsScene场景类

在之前的小节中,我们要往场景中添加图元的话都是先把图元实例化好,再调用场景的addItem()方法进行添加。不过场景其实还提供了以下方法让我们可以快速添加图元:

当然场景还提供了很多用于管理图元的方法。我们通过下面的代码来学习下:

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_())

1. 直接调用场景的addRect(), addEllipse()和addPixmap()方法来添加图元。这里需要大家了解一个知识点:先添加的图元处于后添加的图元下方(Z轴方向),大家可以自己运行下代码然后移动下图元,之后就会发现该程序中图片图元处于最上方,椭圆其次,而矩形处于最下方。不过我们可以通过调用图元的setZValue()方法来改变上下位置(请查阅文档来了解,这里不详细解释)。

接着设置图元的Flag属性。这里多出来的一个ItemIsFocusable表示让图元可以聚焦(默认是无法聚焦的),该属性跟下面第3小点中要讲的foucsItemChanged信号有关;

2. 调用items()方法可以返回场景中的所有图元,返回值类型为列表。返回的元素默认以降序方式(Qt.DescendingOrder),也就是从上到下进行排列(QPixmapItem, QEllipseItem, QRectItem)。可修改order参数的值,让列表中返回的元素按照升序方式排列。

itemsBoundingRect()返回所有图元所构成的整体的边界。

itemAt()可以返回指定位置上的图元,如果在这个位置上有两个重叠的图元的话,那就返回最上面的图元,传入的QTransform()跟图元的Flag属性ItemIgnoresTransformations有关,由于这里没有设置该属性我们直接传入QTransform()就行(这里不细讲,否则可能就会比较混乱了,大家可以先单纯记住,之后再深入研究);

3. 场景有个focusChangedItem信号,当我们选中不同的图元时,该信号就会发出,前提是图元设置了ItemIsFocusable属性。该信号可以传递两个值过来,第一个是新选中的图元,第二个是之前选中的图元;

4. 调用场景的collidingItems()可以打印出在指定碰撞触发条件下,所有和目标图元发生碰撞的其他图元;

5. 我们在图元上双击下,就可以调用removeItem()方法将其删除。注意这里其实直接给itemAt()传入event.pos()是不准确的,因为event.pos()其实是鼠标在视图上的坐标而不是场景上的坐标。大家可以把窗口放大,然后再双击试下,会发现图元并不会消失,这是因为视图大小跟场景大小不再一样,坐标也发生了改变。具体解决方案请看34.4小节。

运行截图如下:

控制台打印内容:

双击某个图元,将其删除:

我们还可以向场景中添加QLabel, QLineEdit, QPushButton, QTableWidget等简单或者复杂的控件,甚至可以直接添加一个主窗口。接下来通过完成以下界面来带大家进一步了解(就是第三章布局管理中的界面例子):

代码如下:

import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QGraphicsScene, QGraphicsView, QGraphicsWidget, QGraphicsGridLayout, \
                            QGraphicsLinearLayout, QLabel, QLineEdit, QPushButton

class Demo(QGraphicsView):
    def __init__(self):
        super(Demo, self).__init__()
        self.resize(220, 110)
        # 1
        self.user_label = QLabel('Username:')
        self.pwd_label = QLabel('Password:')
        self.user_line = QLineEdit()
        self.pwd_line = QLineEdit()
        self.login_btn = QPushButton('Log in')
        self.signin_btn = QPushButton('Sign in')

        # 2
        self.scene = QGraphicsScene()
        self.user_label_proxy = self.scene.addWidget(self.user_label)
        self.pwd_label_proxy = self.scene.addWidget(self.pwd_label)
        self.user_line_proxy = self.scene.addWidget(self.user_line)
        self.pwd_line_proxy = self.scene.addWidget(self.pwd_line)
        self.login_btn_proxy = self.scene.addWidget(self.login_btn)
        self.signin_btn_proxy = self.scene.addWidget(self.signin_btn)
        print(type(self.user_label_proxy))

        # 3
        self.g_layout = QGraphicsGridLayout()
        self.l_h_layout = QGraphicsLinearLayout()
        self.l_v_layout = QGraphicsLinearLayout(Qt.Vertical)
        self.g_layout.addItem(self.user_label_proxy, 0, 0, 1, 1)
        self.g_layout.addItem(self.user_line_proxy, 0, 1, 1, 1)
        self.g_layout.addItem(self.pwd_label_proxy, 1, 0, 1, 1)
        self.g_layout.addItem(self.pwd_line_proxy, 1, 1, 1, 1)
        self.l_h_layout.addItem(self.login_btn_proxy)
        self.l_h_layout.addItem(self.signin_btn_proxy)
        self.l_v_layout.addItem(self.g_layout)
        self.l_v_layout.addItem(self.l_h_layout)

        # 4
        self.widget = QGraphicsWidget()
        self.widget.setLayout(self.l_v_layout)

        # 5
        self.scene.addItem(self.widget)
        self.setScene(self.scene)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    demo = Demo()
    demo.show()
    sys.exit(app.exec_())

1. 实例化需要的控件,因为父类不是QGraphicsView,所以不加self;

2. 实例化一个场景对象,然后调用addWidget()方法来添加控件。addWidget()方法返回的值其实是一个QGraphicsProxyWidget代理对象,控件就是嵌入到该对象所提供的代理层中。user_label_proxy跟user_label的状态保持一致,如果我们禁用或者隐藏了user_label_proxy,那么相应的user_label也会被禁用或者隐藏掉,那我们就可以在场景中通过控制代理对象来操作控件(不过信号和槽还是要直接应用到控件上,代理对象不提供)。

3. 进行布局,注意这里用的是图形视图框架中的布局管理器:QGraphicsGridLayout网格布局和QGraphicsLinearLayout线形布局(水平和垂直布局结合)。不过用法其实差不多,只不过调用的方法是addItem()而不是addWidget()或者addLayout()了。线形布局默认是水平的,我们可以在实例化的时候传入Qt.Vertical来进行垂直布局(图形视图还有个锚布局QGraphicsAnchorLayout,这里不再讲解,相信大家文档也可以看的明白);

4. 实例化一个QGraphicsWidget,这个跟QWidget类似,只不过是用在图形视图框架这边,调用setLayout()方法来设置整体布局;

5. 将QGraphicsWidget对象添加到场景中,QGraphicsProxyWidget中嵌入的控件自然也就在场景上了,最后将场景显示在视图中就可以了。

3.QGraphicsView视图类

视图其实是一个滚动区域,如果视图小于场景大小的话,那窗口就会显示滚动条好让用户可以观察到全部场景(在Linux和Windows系统上,如果视图和场景大小一样,滚动条也会显示出来)。在下面的代码中,笔者让场景大于视图:

import sys
from PyQt5.QtCore import QRectF
from PyQt5.QtWidgets import QApplication, 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.scene.addEllipse(QRectF(200, 200, 50, 50))

        self.setScene(self.scene)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    demo = Demo()
    demo.show()
    sys.exit(app.exec_())

视图大小为300x300,场景大小为500x500。

运行截图如下:

MacOS

Linux(Ubuntu)

Windows

既然图元已经添加好,场景也已经设置好,那我们通常就可以调用视图的一些方法来对图元做一些变换,比如放大、缩小和旋转等。请看下方代码:

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_())

1. 在鼠标滚轮事件中,调用scale()方法来来放大和缩小视图。这里并没有必要调用父类的事件函数,因为我们不需要将事件传递给场景以及图元;

2. 重新实现鼠标按下和移动事件函数,首先获取鼠标按下时的坐标,然后判断鼠标是向左移动还是向右。如果向右的话,则视图顺时针旋转10度,否则逆时针旋转10度。

运行截图如下:

放大和缩小

旋转

当然视图还提供了很多方法,比如同样可以用items()和itemAt()来获取图元,也可以设置视图背景、视图图缓存模式和鼠标拖曳模式等等。大家可按需查阅(这里讲多了怕混乱(ー`´ー))。

4.图形视图的坐标体系

(更新) 图形视图基于笛卡尔坐标系,视图,场景和图元坐标系都一样——左上角为原点,向右为x正轴,向下为y正轴。

图形视图提供了三种坐标系之间相互转换的函数,以及图元与图元之间的转换函数:

好,我们现在来讲解下34.2小节中的那个问题,代码如下:

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_())

在上面这个程序中,视图大小为600x600,而场景大小只有300x300。此时运行程序,我们双击的话是删除不了图元的,原因就是我们所获取的event.pos()是视图上的坐标,但是self.scene.itemAt()需要的是场景坐标。把视图坐标传给场景的itemAt()方法是获取不到任何图元的,所以我们应该要进行转换!

把mouseDoubleClickEvent()事件函数修改如下即可:

def mouseDoubleClickEvent(self, event):
    point = self.mapToScene(event.pos())
    item = self.scene.itemAt(point, QTransform())
    self.scene.removeItem(item)
    super().mouseDoubleClickEvent(event)

调用视图的mapToScene()方法将视图坐标转换为场景坐标,这样图元就可以找到,也就自然而然可以删除掉了。

运行截图如下,椭圆被删除了:

5.小结

1. 事件的传递顺序为视图->场景->图元,如果是在图元父子类之间传递的话,那传递顺序是从子类到父类;

2. 碰撞检测的范围分为边界和形状两种,需要明白两者的不同;

3. 要给QGraphicsItem加上信号和槽机制以及动画的话,就自定义一个继承于QGraphicsObject的类;

4. 往场景中添加QLabel, QLineEdit, QPushButton等控件,我们需要用到QGraphicsProxyWidget;

5. 视图,场景和图元都有自己的坐标系,注意使用坐标转换函数进行转换;

6. 图形视图框架知识点太多,笔者写本章的目的只是尽量带大家入门,个别地方可能会没有解释详细,请各位谅解。关于更多细节,大家可以在Qt Assistant中搜索“Graphics View Framework”来进一步了解。

以上就是深入了解PyQt5中的图形视图框架的详细内容,更多关于PyQt5图形视图框架的资料请关注我们其它相关文章!

(0)

相关推荐

  • python3+PyQt5自定义视图详解

    pyqt提供的几个视图类都可以较好工作,包括QLisView,QTableView和QTreeView.但是对于一些难以用现有的方式来呈现数据,这时,可以创建我们自己的视图子类并将其用做模型数据的可视化来解决这一问题.本文通过Python3+pyqt5实现了python Qt GUI 快速编程的16章的例子. #!/usr/bin/env python3 import gzip import os import platform import sys from PyQt5.QtCore impo

  • Qt通过QGraphicsview实现简单缩放及还原效果

    本文主要介绍通过QGraphicsview实现简单的缩放,以及缩放后还原原始大小. 1,自定义一个drawview继承QGraphicsview,缩放主要实现的函数为 void scale(qreal sx,qreal sy); 通过scale可以对view进行放大或缩小. 则可以在drawview中定义缩放slots为 void drawview::zoomIn(qreal delta) { zoom(delta); } void drawview::zoomOut(qreal delta)

  • pyqt5教程QGraphicsScene及QGraphicsView使用基础

    效果图: from PyQt5.QtCore import Qt, QRectF from PyQt5.QtGui import QColor, QPen, QBrush, QFont from PyQt5.QtWidgets import (QGraphicsView, QGraphicsScene, QApplication) class MainWindow(QGraphicsView): def __init__(self, parent=None): super(MainWindow,

  • 深入了解PyQt5中的图形视图框架

    目录 1.QGraphicsItem图元类 2.QGraphicsScene场景类 3.QGraphicsView视图类 4.图形视图的坐标体系 5.小结 在之前的章节中,笔者一般使用QLabel控件来显示图片.但是,如果要使用很多图片怎么办?难道要实例化很多个QLabel控件来一一显示?那如何管理呢?当然,我们不可能会用QLabel控件来做这样的事,否则会非常麻烦和混乱.PyQt5中的图形视图可以让我们管理大量的自定义2D图元并与之交互.该框架使用BSP(Binary Space Partit

  • python GUI库图形界面开发之PyQt5中QMainWindow, QWidget以及QDialog的区别和选择

    PyQt中MainWindow, QWidget以及Dialog的区别和选择 1. Qt界面分类 在Qt Designer设计界面时,首先需要选择界面模板,主要分为三个类: Main Window Widget Dialog 2. 三种模板的区别(官方文档介绍) MainWindow QMainWindow类提供一个有菜单条.锚接窗口(例如工具条)和一个状态条的主应用程序窗口. 主窗口通常用在提供一个大的中央窗口部件(例如文本编辑或者绘制画布)以及周围菜单.工具条和一个状态条.QMainWind

  • CI(CodeIgniter)框架视图中加载视图的方法

    本文实例讲述了CI(CodeIgniter)框架视图中加载视图的方法.分享给大家供大家参考,具体如下: CI做为php的一个轻量级框架,其自身具备很多优点,在此我重点想说的是视图中加载视图. 1:在Application\config\database.php文件中设置好CodeIgniter 数据库变量之后,紧接着在Application\config\config.php文件中设置基础 URL.例如我的基础 URL 是:http://localhost/codeigniter/ 2:接下来创

  • Python 图形界面框架TkInter之在源码中找pack方法

    目录 一.HelloWorld看pack() 二.pack()方法分析 三.pack_configure()方法分析 四.Label的继承结构 五.查看pack()方法源码的收获 前言: tkinter提供了3种布局管理方式: 1.pack 2.grid 3.place 每种布局管理器都非常有用,根据不同的需求,选择对应的布局方式,每个控件都可以使用pack作为布局管理器,从源码中看下pack()在哪里? 一.HelloWorld看pack() import tkinter   my_windo

  • python GUI库图形界面开发之PyQt5中QWebEngineView内嵌网页与Python的数据交互传参详细方法实例

    这几天研究了下PyQt5中QWebEngineView内嵌网页与Python的数据交互,今天把实例方法与代码发布出来供大家参数 数据交互需要load进一个网页,这里我选择load进一个本地html网页:JSTest.html. 同时,QWebEngineView与外面的交互还需要Qt官方提供的一个js文件:qwebchannel.js,这个文件可以在网上下载. JSTest.html和qwebchannel.js两个文件放在同一个目录下,我这边都是放在Python工程目录下. qwebchann

  • Python GUI教程之在PyQt5中使用数据库的方法

    目录 PyQt5的SQL数据库支持 在PyQt5中简单使用数据库 创建一个UI界面 连接一个数据库 在UI界面查看和修改数据 添加和删除数据 在桌面图像化界面编程中,我们通常需要将一些数据或配置信息存储在本地.在本地进行数据的存储,我们可以直接使用文本文件,比如ini文件.csv文件.json文件等,或者是使用文件型的数据库(比如sqlit3)进行存储. PyQt5的SQL数据库支持 Qt平台对SQL编程有着良好的支持,PyQt5也一并继承了过来.在PyQt5中,QtSql子模块提供对SQL数据

  • Android开发中获取View视图宽与高的常用方法小结

    本文实例讲述了Android开发中获取View视图宽与高的常用方法.分享给大家供大家参考,具体如下: 一.根据WindowManager管理器获得 1)这两种方法在屏幕未显示的时候,还是处于0的状态,即要在setContentView调用之后才有效. 2)Activity必须如此设置才能获得view的宽高 //设置为无标题 requestWindowFeature(Window.FEATURE_NO_TITLE); //设置为全屏模式getWindow().setFlags(WindowMana

  • pyqt5中QThread在使用时出现重复emit的实例

    在PyQt5中使用QThread的时候,要注意把所有QThread的对象在主类中的init(或者放在所有类函数的外面)中进行实例化,不然可能在多个QThread互相调用的时候,emit重复的信号.一般比较正确的写法如下所示,基本照着这种框架搭建都是没问题的. # -*- coding: utf-8 -*- import sys import time from PyQt5.QtWidgets import * from PyQt5.QtCore import * class MainUi(QWi

  • Android Studio 3.6中新的视图绑定工具ViewBinding 用法详解

    前言 我们在Android开发的过程中总是需要获取XML布局中的ViewId,以便给其赋值进行显示,早期我们只能使用 findViewById 这个API,会导致很多的模版代码出现.2013年左右Android界大神 Jake Wharton开源了Butter Knife框架,通过Bind("viewid")方式方便开发者获取ViewId.近两年由于谷歌对Kotlin的支持,我们开始使用 Android Kotlin extensions. 在文件中导入布局文件直接引用viewId.无

  • IOS在SwiftUI中显示模态视图的实例代码

    简介 这里教大家如何弹出一个简单的模态视图.分别有两个页面,ContentView和GCPresentedView,以下对应简称为A和B.我们要做的是在A视图中点击按钮跳转到B视图,然后再从B视图点击按钮返回到A视图. 步骤 在A视图中创建按钮和模态视图代码 struct ContentView: View { @State var isPresented = false var body: some View { Button(action: { self.isPresented = true

随机推荐