首頁 > 軟體

深入瞭解PyQt5中的圖形檢視框架

2022-03-25 13:00:16

在之前的章節中,筆者一般使用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圖形檢視框架的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com