深入了解PyQt5中的圖形視圖框架
在之前的章節(jié)中,筆者一般使用QLabel控件來(lái)顯示圖片。但是,如果要使用很多圖片怎么辦?難道要實(shí)例化很多個(gè)QLabel控件來(lái)一一顯示?那如何管理呢?當(dāng)然,我們不可能會(huì)用QLabel控件來(lái)做這樣的事,否則會(huì)非常麻煩和混亂。PyQt5中的圖形視圖可以讓我們管理大量的自定義2D圖元并與之交互。該框架使用BSP(Binary Space Partitioning - 二叉空間分割)樹(shù),以快速查找圖形元素。所以就算一個(gè)視圖場(chǎng)景中包含數(shù)百萬(wàn)的圖元,它也可以實(shí)時(shí)進(jìn)行顯示。如果要用PyQt5來(lái)制作稍微復(fù)雜點(diǎn)的游戲的話,圖形視圖是必定要用到的。
圖形視圖框架主要包含三個(gè)類:QGraphicsItem圖元類、QGraphicsScene場(chǎng)景類和QGraphicsView視圖類。簡(jiǎn)單一句話來(lái)概括下三者的關(guān)系就是:圖元放在場(chǎng)景上,場(chǎng)景內(nèi)容通過(guò)視圖來(lái)顯示。下面我們來(lái)一一進(jìn)行講解。
1.QGraphicsItem圖元類
圖元可以是文本、圖片,規(guī)則幾何圖形或者任意自定義圖形。該類已經(jīng)提供了一些標(biāo)準(zhǔn)的圖元,比如:
- 直線圖元QGraphicsLineItem
- 矩形圖元QGraphicsRectItem
- 橢圓圖元QGraphicsEllipseItem
- 圖片圖元QGraphicsPixmapItem
- 文本圖元QGraphicsTextItem
- 路徑圖元QGraphicsPathItem
想必通過(guò)名稱也可以知道這些圖元是用來(lái)干嘛的,我們通過(guò)以下代碼來(lái)演示如何使用:
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 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. 實(shí)例化一個(gè)QGraphicsScene場(chǎng)景,并調(diào)用setSceneRect(x, y, w, h)方法來(lái)設(shè)置場(chǎng)景坐標(biāo)原點(diǎn)和大小。從代碼中我們得知坐標(biāo)原點(diǎn)為(0, 0),之后往場(chǎng)景中添加的圖元就會(huì)都根據(jù)該坐標(biāo)來(lái)設(shè)置位置(關(guān)于坐標(biāo)的更多內(nèi)容,筆者會(huì)在34.4小節(jié)中進(jìn)行講解)。場(chǎng)景的大小為300x300,跟視圖大小一樣;
3. 實(shí)例化一個(gè)QGraphicsLineItem直線圖元,并調(diào)用setLine()方法設(shè)置直線兩端的坐標(biāo)。該方法既可以直接傳入四個(gè)數(shù)值,也可以傳入一個(gè)QLineF對(duì)象。文檔里寫(xiě)的非常清楚:

4-5. 跟直線圖元類似,這里分別實(shí)例化矩形圖元和橢圓圖元,并調(diào)用相應(yīng)的方法來(lái)設(shè)置位置和大??;
6. 實(shí)例化一個(gè)圖片圖元,并調(diào)用setPixmap()方法設(shè)置圖片,QPixmap對(duì)象有個(gè)scaled()方法可以設(shè)置圖片的大小(當(dāng)然我們也可以使用QGraphicsItem的setScale()方法來(lái)設(shè)置),接著我們?cè)O(shè)置該圖元的Flag屬性,讓他可以被選中以及移動(dòng),這是所有圖元共有的方法。最后調(diào)用setOffset()方法來(lái)設(shè)置圖片相對(duì)于場(chǎng)景坐標(biāo)原點(diǎn)的偏移量;
7. 這里實(shí)例化了三個(gè)文本圖元,分別顯示普通綠色文本,可編輯文本以及超鏈接文本(HTML)。setDefaultColor()方法可以用來(lái)設(shè)置文本的顏色,setPos()用來(lái)設(shè)置文本圖元相對(duì)于場(chǎng)景坐標(biāo)原點(diǎn)的位置(該方法是所有圖元共有的方法,我們當(dāng)然也可以使用在其他類型的圖元上)。
setTextInteractionFlags()用來(lái)設(shè)置文本屬性,這里的Qt.TextEditorInteraction參數(shù)表示為可編輯屬性(相當(dāng)于在QTextEdit上編輯文本),最后的Qt.TextBrowserInteraction表明該文本用于瀏覽(相當(dāng)于在QTextBrowser上的文本)。有關(guān)更多的屬性,大家可以在文檔里搜索Qt::TextInteractionFlags來(lái)了解。
當(dāng)然如果要讓超鏈接文本能夠被打開(kāi),我們還需要使用setOpenExternalLinks()方法,傳入一個(gè)True參數(shù)即可。
8. 路徑圖元可以用于顯示任意形狀的圖形,setPath()方法需要傳入一個(gè)QPainterPath對(duì)象,而我們就是用該對(duì)象來(lái)進(jìn)行繪畫(huà)操作的。moveTo()方法表示將畫(huà)筆移動(dòng)到相應(yīng)位置上,lineTo()表示畫(huà)一條直線,closeSubpath()方法表示當(dāng)前作畫(huà)結(jié)束 (查閱文檔來(lái)了解更多有關(guān)QPaintPath對(duì)象的方法),這里我們畫(huà)了一個(gè)直角三角形;
9. 調(diào)用場(chǎng)景的addItem()方法將所有圖元添加進(jìn)來(lái);
10. 調(diào)用setScene()方法來(lái)讓場(chǎng)景居中顯示在視圖中。
運(yùn)行截圖如下:

圖片可以被選中和移動(dòng):

Hello World文本可以被編輯:

QGraphicsItem還支持以下特性:
- 鼠標(biāo)按下、移動(dòng)、釋放和雙擊事件,以及鼠標(biāo)懸浮事件、滾輪事件和右鍵菜單事件
- 鍵盤(pán)輸入事件
- 拖放事件
- 分組
- 碰撞檢測(cè)
實(shí)現(xiàn)事件函數(shù)非常簡(jiǎn)單,這里就不細(xì)講,我們重點(diǎn)要來(lái)了解下它在圖形視圖框架中的是如何傳遞的。請(qǐng)看下面的代碼:
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_())圖元,場(chǎng)景和視圖其實(shí)都有各自的事件函數(shù),我們?cè)谏厦娣謩e繼承了QGraphicsRectItem, QGraphicsScene以及QGraphicsView并重新實(shí)現(xiàn)了各自的mousePressEvent()事件函數(shù),在其中我們都打印一句話來(lái)讓用戶知道是哪個(gè)函數(shù)被執(zhí)行了。
運(yùn)行截圖如下:

我們?cè)诰匦慰騼?nèi)點(diǎn)擊之后,發(fā)現(xiàn)控制臺(tái)輸入如下信息:

由此可見(jiàn),事件的傳遞順序?yàn)橐晥D->場(chǎng)景->圖元。有一點(diǎn)大家需要注意,重新實(shí)現(xiàn)事件函數(shù)的話我們必須要調(diào)用相應(yīng)的父類事件函數(shù),否則事件無(wú)法順利傳遞下去。假如我把CustomView類中事件函數(shù)下的super().mousePressEvent(event)這行代碼刪除掉,那么控制臺(tái)只會(huì)輸出"event from QGraphicsView":

一個(gè)圖元中可以添加另一個(gè)圖元(一個(gè)圖元可以是另一個(gè)圖元的父類),那此時(shí)圖元之間的事件傳遞順序又是如何的呢?請(qǐng)看下面代碼:
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_())因?yàn)閷?shí)例化的是兩個(gè)一樣的矩形圖源,為了進(jìn)行區(qū)分,我們?cè)贑ustomItem的初始化函數(shù)中加入一個(gè)num參數(shù),然后在事件函數(shù)中打印出實(shí)例化時(shí)所傳入的數(shù)字即可。
調(diào)用setParentItem()方法將item1設(shè)置為item2的父類,然后將item1添加到場(chǎng)景中(item2自然也被加入)。
運(yùn)行截圖如下:

在矩形框中點(diǎn)擊,控制臺(tái)打印如下:

由此可見(jiàn),事件是由子圖元傳遞到父圖元的。同理,如果不加super().mousePressEvent(event),那么事件就會(huì)停止傳遞,最后也就只會(huì)顯示"event from QGraphicsItem2":

請(qǐng)大家一定要搞清楚事件的傳遞順序,這樣才能更好地使用圖形視圖框架。
所謂分組也就是將各個(gè)圖元進(jìn)行分類,分到一起的圖元就會(huì)共同行動(dòng)(選中、移動(dòng)以及復(fù)制等)。我們通過(guò)下面的代碼來(lái)演示下:
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. 實(shí)例化四個(gè)圖元,兩個(gè)為矩形,兩個(gè)為橢圓,并調(diào)用setRect()方法設(shè)置坐標(biāo)和大?。?/p>
2. 實(shí)例化兩種畫(huà)筆和兩種畫(huà)刷,用于圖元的樣式設(shè)置;
3. 實(shí)例化兩個(gè)QGraphicsGroup分組對(duì)象,并將矩形和橢圓都添加進(jìn)來(lái)。rect1和ellipse1在group1里,而rect2和ellipse2在group2里。接著調(diào)用setFlags()方法設(shè)置屬性,讓分組可以選中和移動(dòng)。boundRect()方法放回一個(gè)QRectF值,該值可以顯示出分組的邊界位置和大??;
4. 將分組添加到場(chǎng)景當(dāng)中。
運(yùn)行截圖如下:

藍(lán)色的矩形和橢圓為一組,可同時(shí)選中和移動(dòng),紅色的同理。黑色邊框即為邊界,其位置和大小可用boundRect()方法來(lái)獲取。通過(guò)下面的截圖我們可以發(fā)現(xiàn)QGraphicsItemGroup的邊界的位置和大小由其中的圖元整體所決定:

碰撞檢測(cè)在游戲中的用處非常大,比如在飛機(jī)大戰(zhàn)游戲中,如果子彈沒(méi)有和敵機(jī)做碰撞檢測(cè)處理的話,那敵機(jī)就不會(huì)被消滅,獎(jiǎng)勵(lì)也不會(huì)增加,游戲也就沒(méi)有什么意思。我們通過(guò)下面這個(gè)例子來(lái)帶大家了解如何對(duì)圖元進(jìn)行碰撞檢測(cè):

界面上有一個(gè)矩形圖元和一個(gè)橢圓圖元,兩者都可以選中和移動(dòng)。我們就對(duì)兩者進(jìn)行碰撞檢測(cè)。在此之前我們先了解下boundingRect()邊界和shape()形狀的區(qū)別。請(qǐng)看下方的橢圓圖元:

當(dāng)選中這個(gè)圖元時(shí),虛線部分顯示的就是該圖元的邊界,而形狀就指的是圖元本身,也就是黑色實(shí)線部分。碰撞檢測(cè)可以以邊界為范圍或者以形狀為范圍。假如我們?cè)诖a中以邊界為范圍,那橢圓的虛線跟矩形圖元一碰到,就會(huì)觸發(fā)碰撞檢測(cè);如果以形狀為范圍,那只有在橢圓的黑色實(shí)線跟矩形碰到的情況下,碰撞檢測(cè)才會(huì)觸發(fā)。
下面是幾種具體的檢測(cè)方式:

下面請(qǐng)看代碼示例:
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_())初始化函數(shù)中的代碼想必大家都懂了,這里就不再講述,我們重點(diǎn)來(lái)看mouseMoveEvent()事件函數(shù)。
我們調(diào)用橢圓圖元的collidesWithItem()方法來(lái)指定要與之進(jìn)行碰撞檢測(cè)的其他圖元以及檢測(cè)方式。其他圖元指的就是矩形圖元,而且我們可以看到這里是以橢圓的邊界為范圍,而且只要兩個(gè)圖元有交集就會(huì)觸發(fā)檢測(cè)。如果碰撞條件成立,那么collidesWithItem()就會(huì)返回一個(gè)True,那么此時(shí)if條件判斷也就成立。
collidingItems()方法在指定檢測(cè)方式后可以返回所有符合碰撞條件的其他圖元,返回值類型為列表。這里的檢測(cè)方式是以形狀為范圍的,同樣有交集即可。
那mouseMoveEvent()事件函數(shù)所要表達(dá)的意思就是:當(dāng)橢圓的邊界和矩形接觸,那么if條件判斷就成立,不過(guò)此時(shí)打印的還只是空列表,因?yàn)闄E圓本身(黑色實(shí)線)并還沒(méi)有跟矩形有所接觸。不過(guò)當(dāng)接觸了之后控制臺(tái)就會(huì)輸出包含矩形圖元的列表了。


請(qǐng)大家調(diào)用矩形圖元的collidesWithItem()和collidingItems()方法來(lái)嘗試下,看看有什么不同。也就是把mouseMoveEvent()事件函數(shù)修改如下:
def mouseMoveEvent(self, event):
if self.rect.collidesWithItem(self.ellipse, Qt.IntersectsItemBoundingRect):
print(self.rect.collidingItems(Qt.IntersectsItemShape))
super().mouseMoveEvent(event)出于性能考慮,QGraphicsItem不繼承自QObject,所以本身并不能使用信號(hào)和槽機(jī)制,我們也無(wú)法給它添加動(dòng)畫(huà)。不過(guò)我們可以自定義一個(gè)類,并讓該類繼承自QGraphicsObject。請(qǐng)看下面的解決方案:
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. 自定義一個(gè)信號(hào);
2-3. 繼承QGraphicsObject的話,我們最好把boundingRect()和paint()方法重新實(shí)現(xiàn)下。在boundingRect()中我們返回一個(gè)QRectF類型值來(lái)確定CustomRect的默認(rèn)位置和大小。在paint()中調(diào)用drawRect()方法將矩形畫(huà)到界面上;
4. 將自定義的信號(hào)和槽函數(shù)連接,槽函數(shù)中打印“signal and slot”字符串。接著調(diào)用信號(hào)的emit()方法來(lái)發(fā)射信號(hào),那么槽函數(shù)也就會(huì)啟動(dòng)了;
5. 加上QPropertyAnimation屬性動(dòng)畫(huà),將矩形從(100, 30)移動(dòng)到(100, 200),時(shí)間為3秒,動(dòng)畫(huà)無(wú)限循環(huán)。
運(yùn)行截圖如下,矩形圖元從上而下緩緩移動(dòng):


控制臺(tái)打印內(nèi)容:

2.QGraphicsScene場(chǎng)景類
在之前的小節(jié)中,我們要往場(chǎng)景中添加圖元的話都是先把圖元實(shí)例化好,再調(diào)用場(chǎng)景的addItem()方法進(jìn)行添加。不過(guò)場(chǎng)景其實(shí)還提供了以下方法讓我們可以快速添加圖元:

當(dāng)然場(chǎng)景還提供了很多用于管理圖元的方法。我們通過(guò)下面的代碼來(lái)學(xué)習(xí)下:
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. 直接調(diào)用場(chǎng)景的addRect(), addEllipse()和addPixmap()方法來(lái)添加圖元。這里需要大家了解一個(gè)知識(shí)點(diǎn):先添加的圖元處于后添加的圖元下方(Z軸方向),大家可以自己運(yùn)行下代碼然后移動(dòng)下圖元,之后就會(huì)發(fā)現(xiàn)該程序中圖片圖元處于最上方,橢圓其次,而矩形處于最下方。不過(guò)我們可以通過(guò)調(diào)用圖元的setZValue()方法來(lái)改變上下位置(請(qǐng)查閱文檔來(lái)了解,這里不詳細(xì)解釋)。
接著設(shè)置圖元的Flag屬性。這里多出來(lái)的一個(gè)ItemIsFocusable表示讓圖元可以聚焦(默認(rèn)是無(wú)法聚焦的),該屬性跟下面第3小點(diǎn)中要講的foucsItemChanged信號(hào)有關(guān);
2. 調(diào)用items()方法可以返回場(chǎng)景中的所有圖元,返回值類型為列表。返回的元素默認(rèn)以降序方式(Qt.DescendingOrder),也就是從上到下進(jìn)行排列(QPixmapItem, QEllipseItem, QRectItem)。可修改order參數(shù)的值,讓列表中返回的元素按照升序方式排列。
itemsBoundingRect()返回所有圖元所構(gòu)成的整體的邊界。
itemAt()可以返回指定位置上的圖元,如果在這個(gè)位置上有兩個(gè)重疊的圖元的話,那就返回最上面的圖元,傳入的QTransform()跟圖元的Flag屬性ItemIgnoresTransformations有關(guān),由于這里沒(méi)有設(shè)置該屬性我們直接傳入QTransform()就行(這里不細(xì)講,否則可能就會(huì)比較混亂了,大家可以先單純記住,之后再深入研究);
3. 場(chǎng)景有個(gè)focusChangedItem信號(hào),當(dāng)我們選中不同的圖元時(shí),該信號(hào)就會(huì)發(fā)出,前提是圖元設(shè)置了ItemIsFocusable屬性。該信號(hào)可以傳遞兩個(gè)值過(guò)來(lái),第一個(gè)是新選中的圖元,第二個(gè)是之前選中的圖元;
4. 調(diào)用場(chǎng)景的collidingItems()可以打印出在指定碰撞觸發(fā)條件下,所有和目標(biāo)圖元發(fā)生碰撞的其他圖元;
5. 我們?cè)趫D元上雙擊下,就可以調(diào)用removeItem()方法將其刪除。注意這里其實(shí)直接給itemAt()傳入event.pos()是不準(zhǔn)確的,因?yàn)閑vent.pos()其實(shí)是鼠標(biāo)在視圖上的坐標(biāo)而不是場(chǎng)景上的坐標(biāo)。大家可以把窗口放大,然后再雙擊試下,會(huì)發(fā)現(xiàn)圖元并不會(huì)消失,這是因?yàn)橐晥D大小跟場(chǎng)景大小不再一樣,坐標(biāo)也發(fā)生了改變。具體解決方案請(qǐng)看34.4小節(jié)。
運(yùn)行截圖如下:

控制臺(tái)打印內(nèi)容:

雙擊某個(gè)圖元,將其刪除:

我們還可以向場(chǎng)景中添加QLabel, QLineEdit, QPushButton, QTableWidget等簡(jiǎn)單或者復(fù)雜的控件,甚至可以直接添加一個(gè)主窗口。接下來(lái)通過(guò)完成以下界面來(lái)帶大家進(jìn)一步了解(就是第三章布局管理中的界面例子):

代碼如下:
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. 實(shí)例化需要的控件,因?yàn)楦割惒皇荙GraphicsView,所以不加self;
2. 實(shí)例化一個(gè)場(chǎng)景對(duì)象,然后調(diào)用addWidget()方法來(lái)添加控件。addWidget()方法返回的值其實(shí)是一個(gè)QGraphicsProxyWidget代理對(duì)象,控件就是嵌入到該對(duì)象所提供的代理層中。user_label_proxy跟user_label的狀態(tài)保持一致,如果我們禁用或者隱藏了user_label_proxy,那么相應(yīng)的user_label也會(huì)被禁用或者隱藏掉,那我們就可以在場(chǎng)景中通過(guò)控制代理對(duì)象來(lái)操作控件(不過(guò)信號(hào)和槽還是要直接應(yīng)用到控件上,代理對(duì)象不提供)。
3. 進(jìn)行布局,注意這里用的是圖形視圖框架中的布局管理器:QGraphicsGridLayout網(wǎng)格布局和QGraphicsLinearLayout線形布局(水平和垂直布局結(jié)合)。不過(guò)用法其實(shí)差不多,只不過(guò)調(diào)用的方法是addItem()而不是addWidget()或者addLayout()了。線形布局默認(rèn)是水平的,我們可以在實(shí)例化的時(shí)候傳入Qt.Vertical來(lái)進(jìn)行垂直布局(圖形視圖還有個(gè)錨布局QGraphicsAnchorLayout,這里不再講解,相信大家文檔也可以看的明白);
4. 實(shí)例化一個(gè)QGraphicsWidget,這個(gè)跟QWidget類似,只不過(guò)是用在圖形視圖框架這邊,調(diào)用setLayout()方法來(lái)設(shè)置整體布局;
5. 將QGraphicsWidget對(duì)象添加到場(chǎng)景中,QGraphicsProxyWidget中嵌入的控件自然也就在場(chǎng)景上了,最后將場(chǎng)景顯示在視圖中就可以了。
3.QGraphicsView視圖類
視圖其實(shí)是一個(gè)滾動(dòng)區(qū)域,如果視圖小于場(chǎng)景大小的話,那窗口就會(huì)顯示滾動(dòng)條好讓用戶可以觀察到全部場(chǎng)景(在Linux和Windows系統(tǒng)上,如果視圖和場(chǎng)景大小一樣,滾動(dòng)條也會(huì)顯示出來(lái))。在下面的代碼中,筆者讓場(chǎng)景大于視圖:
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,場(chǎng)景大小為500x500。
運(yùn)行截圖如下:
MacOS

Linux(Ubuntu)

Windows

既然圖元已經(jīng)添加好,場(chǎng)景也已經(jīng)設(shè)置好,那我們通常就可以調(diào)用視圖的一些方法來(lái)對(duì)圖元做一些變換,比如放大、縮小和旋轉(zhuǎn)等。請(qǐng)看下方代碼:
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. 在鼠標(biāo)滾輪事件中,調(diào)用scale()方法來(lái)來(lái)放大和縮小視圖。這里并沒(méi)有必要調(diào)用父類的事件函數(shù),因?yàn)槲覀儾恍枰獙⑹录鬟f給場(chǎng)景以及圖元;
2. 重新實(shí)現(xiàn)鼠標(biāo)按下和移動(dòng)事件函數(shù),首先獲取鼠標(biāo)按下時(shí)的坐標(biāo),然后判斷鼠標(biāo)是向左移動(dòng)還是向右。如果向右的話,則視圖順時(shí)針旋轉(zhuǎn)10度,否則逆時(shí)針旋轉(zhuǎn)10度。
運(yùn)行截圖如下:

放大和縮小


旋轉(zhuǎn)

當(dāng)然視圖還提供了很多方法,比如同樣可以用items()和itemAt()來(lái)獲取圖元,也可以設(shè)置視圖背景、視圖圖緩存模式和鼠標(biāo)拖曳模式等等。大家可按需查閱(這里講多了怕混亂(ー`´ー))。
4.圖形視圖的坐標(biāo)體系
(更新) 圖形視圖基于笛卡爾坐標(biāo)系,視圖,場(chǎng)景和圖元坐標(biāo)系都一樣——左上角為原點(diǎn),向右為x正軸,向下為y正軸。

圖形視圖提供了三種坐標(biāo)系之間相互轉(zhuǎn)換的函數(shù),以及圖元與圖元之間的轉(zhuǎn)換函數(shù):

好,我們現(xiàn)在來(lái)講解下34.2小節(jié)中的那個(gè)問(wèn)題,代碼如下:
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_())在上面這個(gè)程序中,視圖大小為600x600,而場(chǎng)景大小只有300x300。此時(shí)運(yùn)行程序,我們雙擊的話是刪除不了圖元的,原因就是我們所獲取的event.pos()是視圖上的坐標(biāo),但是self.scene.itemAt()需要的是場(chǎng)景坐標(biāo)。把視圖坐標(biāo)傳給場(chǎng)景的itemAt()方法是獲取不到任何圖元的,所以我們應(yīng)該要進(jìn)行轉(zhuǎn)換!
把mouseDoubleClickEvent()事件函數(shù)修改如下即可:
def mouseDoubleClickEvent(self, event):
point = self.mapToScene(event.pos())
item = self.scene.itemAt(point, QTransform())
self.scene.removeItem(item)
super().mouseDoubleClickEvent(event)調(diào)用視圖的mapToScene()方法將視圖坐標(biāo)轉(zhuǎn)換為場(chǎng)景坐標(biāo),這樣圖元就可以找到,也就自然而然可以刪除掉了。
運(yùn)行截圖如下,橢圓被刪除了:

5.小結(jié)
1. 事件的傳遞順序?yàn)橐晥D->場(chǎng)景->圖元,如果是在圖元父子類之間傳遞的話,那傳遞順序是從子類到父類;
2. 碰撞檢測(cè)的范圍分為邊界和形狀兩種,需要明白兩者的不同;
3. 要給QGraphicsItem加上信號(hào)和槽機(jī)制以及動(dòng)畫(huà)的話,就自定義一個(gè)繼承于QGraphicsObject的類;
4. 往場(chǎng)景中添加QLabel, QLineEdit, QPushButton等控件,我們需要用到QGraphicsProxyWidget;
5. 視圖,場(chǎng)景和圖元都有自己的坐標(biāo)系,注意使用坐標(biāo)轉(zhuǎn)換函數(shù)進(jìn)行轉(zhuǎn)換;
6. 圖形視圖框架知識(shí)點(diǎn)太多,筆者寫(xiě)本章的目的只是盡量帶大家入門(mén),個(gè)別地方可能會(huì)沒(méi)有解釋詳細(xì),請(qǐng)各位諒解。關(guān)于更多細(xì)節(jié),大家可以在Qt Assistant中搜索“Graphics View Framework”來(lái)進(jìn)一步了解。

以上就是深入了解PyQt5中的圖形視圖框架的詳細(xì)內(nèi)容,更多關(guān)于PyQt5圖形視圖框架的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Python利用flask sqlalchemy實(shí)現(xiàn)分頁(yè)效果
這篇文章主要為大家詳細(xì)介紹了利用flask sqlalchemy實(shí)現(xiàn)分頁(yè)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07
Python環(huán)境下安裝使用異步任務(wù)隊(duì)列包Celery的基礎(chǔ)教程
這篇文章主要介紹了Python環(huán)境下安裝使用異步任務(wù)隊(duì)列包Celery的基礎(chǔ)教程,Celery的分布式任務(wù)管理適合用于服務(wù)器集群的管理和維護(hù),需要的朋友可以參考下2016-05-05
pyqt5 comboBox獲得下標(biāo)、文本和事件選中函數(shù)的方法
今天小編就為大家分享一篇pyqt5 comboBox獲得下標(biāo)、文本和事件選中函數(shù)的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-06-06
python opencv檢測(cè)目標(biāo)顏色的實(shí)例講解
下面小編就為大家分享一篇python opencv檢測(cè)目標(biāo)顏色的實(shí)例講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-04-04
CentOS下使用yum安裝python-pip失敗的完美解決方法
這篇文章主要介紹了CentOS下使用yum安裝python-pip失敗的完美解決方法,需要的朋友可以參考下2017-08-08
pandas讀取Excel批量轉(zhuǎn)換時(shí)間戳的實(shí)踐
本文主要介紹了pandas讀取Excel批量轉(zhuǎn)換時(shí)間戳的實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02
python實(shí)現(xiàn)從字典中刪除元素的方法
這篇文章主要介紹了python實(shí)現(xiàn)從字典中刪除元素的方法,涉及Python中del方法的使用技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-05-05

