您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關PyQt5中圖形視圖框架的示例分析,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。
圖元可以是文本、圖片,規則幾何圖形或者任意自定義圖形。該類已經提供了一些標準的圖元,比如:
直線圖元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) brush2 = QBrush(Qt.SolidPattern) brush2.setColor(Qt.blue) brush3 = QBrush(Qt.SolidPattern) brush3.setColor(Qt.red) self.rect1.setPen(pen1) self.rect1.setBrush(brush2) self.rect2.setPen(pen2) self.rect2.setBrush(brush3) self.ellipse1.setPen(pen1) self.ellipse1.setBrush(brush2) self.ellipse2.setPen(pen2) self.ellipse2.setBrush(brush3) # 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秒,動畫無限循環。
運行截圖如下,矩形圖元從上而下緩緩移動:
控制臺打印內容:
在之前的小節中,我們要往場景中添加圖元的話都是先把圖元實例化好,再調用場景的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中嵌入的控件自然也就在場景上了,最后將場景顯示在視圖中就可以了。
視圖其實是一個滾動區域,如果視圖小于場景大小的話,那窗口就會顯示滾動條好讓用戶可以觀察到全部場景(在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()來獲取圖元,也可以設置視圖背景、視圖圖緩存模式和鼠標拖曳模式等等。大家可按需查閱(這里講多了怕混亂(ー`´ー))。
(更新) 圖形視圖基于笛卡爾坐標系,視圖,場景和圖元坐標系都一樣——左上角為原點,向右為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()方法將視圖坐標轉換為場景坐標,這樣圖元就可以找到,也就自然而然可以刪除掉了。
運行截圖如下,橢圓被刪除了:
1. 事件的傳遞順序為視圖->場景->圖元,如果是在圖元父子類之間傳遞的話,那傳遞順序是從子類到父類;
2. 碰撞檢測的范圍分為邊界和形狀兩種,需要明白兩者的不同;
3. 要給QGraphicsItem加上信號和槽機制以及動畫的話,就自定義一個繼承于QGraphicsObject的類;
4. 往場景中添加QLabel, QLineEdit, QPushButton等控件,我們需要用到QGraphicsProxyWidget;
5. 視圖,場景和圖元都有自己的坐標系,注意使用坐標轉換函數進行轉換;
6. 圖形視圖框架知識點太多,筆者寫本章的目的只是盡量帶大家入門,個別地方可能會沒有解釋詳細,請各位諒解。關于更多細節,大家可以在Qt Assistant中搜索“Graphics View Framework”來進一步了解。
關于“PyQt5中圖形視圖框架的示例分析”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,使各位可以學到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。