第15.22節 PyQt(Python+Qt)入門學習:Model/View架構詳解

一、簡介

在PyQt和Qt中,Model/View架構是圖形界面開發時用於管理數據和界面展現方式的關係。由該體系架構引入的功能分離使得開發人員能夠更靈活地定製展現數據項的呈現方式,並提供標準模型接口支持廣泛的數據源與預定義好的項視圖(item views)一起使用。

二、Model/View架構概述

2.1、引言

模型-視圖-控制器(Model-View-Controller,簡稱MVC)是一種源於Smalltalk在構建用戶界面時 廣泛使用的設計模式。在《Design Patterns》一書中,Gamma等人這樣描述到:“MVC由三種對象組成。模型Model是應用程序對象,視圖View是其屏幕表示,控制器Controller定義用戶界面對用戶輸入的反應方式。在MVC之前,用戶界面設計傾向於將這些對象組合在一起。MVC將它們解耦以增加靈活性和重用性。

如果將MVC架構中的視圖和控制器對象組合在一起,結果就是Model/View體系結構。這還是將數據的存儲方式與展現給用戶的方式分開,但是基於相同的原則提供了一個更簡單的框架。這種分離使得可以在幾個不同的視圖中顯示相同的數據,並實現新類型的視圖,而無需更改底層數據結構。

爲了允許靈活地處理用戶輸入,在PyQt和Qt中引入了代理Delegate的概念,代理允許自定義數據項的呈現和編輯方式。不過老猿不準備就代理的概念展開進行介紹。

2.2、架構模型

2.2.1 架構模型圖

完整的Model/View架構模型如下:
在這裏插入圖片描述

2.2.2、架構模型各組件功能

  • 模型Model與數據源通信,爲體系結構中的其他組件提供數據接口。與數據源通信的方式取決於數據源的類型(如文件、數據庫、消息等)以及模型的實現方式。
  • 視圖View從模型Model中根據一定條件(如行號、列號等)獲取模型索引,模型索引是一個指向數據項的引用。通過模型Model的模型索引,視圖View可以從數據源檢索數據項。
  • 在標準視圖中,代理Delegate展現數據項,編輯項時,代理Delegate直接使用模型索引與模型Model通信。

一般情況下Model/View可以分爲上述三組:模型Models、視圖Views和代理delegates。這些組件中的每一個都是由抽象類定義的,這些抽象類提供公共接口,在某些情況下還提供特性的默認實現。抽象類應該子類化,通過子類以便提供其他組件所需要的全部功能,同時特殊情況下這也可以針對特定場景編寫專門的組件。

2.2.3、架構模型各組件通信機制

模型、視圖和代理使用信號和插槽相互通信:

  • 來自模型Model的信號通知視圖View有關數據源所保存數據的更改
  • 來自視圖View的信號提供了有關用戶與所顯示項目的交互的信息
  • 來自代理delegate的信號在數據項編輯期間用於告訴模型Model和視圖View編輯器的狀態

三、模型Models

所有項模型(item models )類都是從基類QAbstractItemModel 類及其子類派生的,QAbstractItemModel 類定義了一個供視圖views 和代理delegates 訪問數據的接口。數據本身不必存儲在模型中,它可以保存在由單獨的類、文件、數據庫或其他應用程序組件提供的數據結構或存儲庫中。

QAbstractItemModel 提供了一個數據接口,該接口足夠靈活地處理以列表、表和樹的形式展現數據的視圖。下圖是三種模型對應數據層級示意:

在這裏插入圖片描述

然而,當爲列表和類似表的數據結構實現新模型時,QAbstractListModel和QAbstractTableModel類是更好的起點,因爲它們提供了公共函數的適當默認實現。這些類中的每一個都可以被子類化,以提供支持特殊類型列表或表的模型。

PyQt和Qt提供了一些現成的模型,可用於處理數據項:

  • QStringListModel用於存儲QString項的簡單列表。
  • QStandardItemModel管理更復雜的項樹結構,每個項都可以包含任意數據。
  • QFileSystemModel提供有關本地文件系統中的文件和目錄的信息。
  • QSqlQueryModel、QSqlTableModel和QSqlRelationalTableModel用於使用Model/View對應約定協議實現數據庫訪問。

如果這些標準模型不滿足應用的需求,可以將QAbstractItemModel 、QAbstractListModel或QAbstractTableModel子類化,以創建自定義模型。

四、視圖Views

視圖負責數據的展現,PyQt和Qt爲不同類型的視圖提供了完整的實現類:

  • QListView展現一個列表,每個列表就是一個項
  • QTableView以表格形式展現表模型的數據
  • QTreeView以樹形方式展示分層列表模型(多層列表,每層與上層的某個節點有父子關係)中的數據

這些類中的每一個都基於QAbstractItemView抽象基類,儘管這些類已實現基類的所有 抽象方法可以直接使用,但它們也可以被子類化以提供自定義視圖使用。

五、代理Delegates

QAbstractItemDelegate 是Model/View架構中代理的抽象基類。代理爲視圖中的項提供展現和編輯功能。默認的代理實現由QStyledItemDelegate提供,PyQt和Qt中預定義好的標準視圖將其用作默認代理。QStyledItemDelegate使用當前樣式來繪製視圖中的項。Qt建議在實現自定義代理或使用Qt樣式表時使用QStyledItemDelegate作爲基類。

對於代理,老猿在PyQt相關內容中不進行展開介紹。

六、使用模型和視圖

6.1、兩個標準模型

PyQt和Qt提供的兩個標準模型是QStandardItemModel和QFileSystemModel。QStandardItemModel是一個多用途模型,可用於表示列表list、表table和樹tree類型視圖所需的各種不同數據結構,模型可以保存數據項。QFileSystemModel是一個維護文件目錄內容信息的模型,它本身不包含任何數據項,而只是表示本地文件系統上的文件和目錄。

QFileSystemModel 提供了一個隨時可用的模型來進行實驗,並且可以很容易地配置爲使用現有的數據。使用此模型,我們可以演示如何設置用於現成視圖的模型,並探索如何使用模型索引(model indexes)操作數據。 QListView 和 QTreeView這兩個視圖是最適合使用QFileSystemModel的視圖。

6.2、模型索引(Model indexes)

爲了確保數據的展現與訪問方式保持分離,引入了模型索引的概念。通過模型可以獲得的每一條數據都由模型索引表示。視圖和代理使用模型索引來請求要顯示的數據項。

因此,模型只需要知道如何獲取數據,而模型管理的數據類型可以相當普遍。模型索引包含指向創建它們的模型對象,這可以防止在使用多個模型時出現混淆。

Qt中Model/View中的Model Index是一個類,該類用於定位Model/View中數據模型中的數據。

Model Index對應類爲QModelIndex,用於在項視圖( item views)、代理(delegates)和選擇模型( selection models)使用來定位Model中的數據項。

模型索引引用模型中的數據項,包含一個指向創建模型索引的Model的指針,這樣可以避免使用多個Model時引起混淆,模型索引包含有定位數據項在模型中的位置所需的所有信息,包括索引位置給定的行和列位置,並且可能還有父索引,這些通過使用row()、column()和parent()來獲取。模型中的每個頂級項目都用一個沒有父索引的模型索引來表示——在這種情況下,parent() 將返回一個無效的模型索引,相當於一個用QModelIndex()無參數形式構造的索引。

爲了獲取相應數據項的模型索引,可以調用QAbstractItemModel.index() ,調用時必須指定Model的三個屬性:行數,列數,父項的模型索引。特殊情況下,引用模型中的頂級項時,使用QModelIndex()作爲父索引。

  • 下圖中的表模型中數據A、B、C的模型索引獲取方法代碼如下:

在這裏插入圖片描述

		indexA = self.model.index(0, 0, QtCore.QModelIndex())
        indexB = self.model.index(1, 1, QtCore.QModelIndex())
        indexC = self.model.index(2, 1, QtCore.QModelIndex())
  • 下圖中的樹模型中數據A、B、C的模型索引獲取方法代碼如下:
    在這裏插入圖片描述
		indexA = self.model.index(0, 0, QtCore.QModelIndex())
        indexC = self.model.index(2, 1, QtCore.QModelIndex())
        indexB = self.model.index(1, 0, indexA)

QModelIndex對象由模型使用QAbstractItemModel.createIndex() 函數創建。可以使用QModelIndex構造函數構造無效的模型索引。當引用模型中的頂級項時,無效索引通常用作父索引。

model()函數返回索引引用的Model(類型爲QAbstractItemModel),child()函數用於訪問給定行和列對應索引下保存的子項。sibling()函數用於在模型中遍歷與索引相同級別的數據項。

注意:模型索引爲數據項提供了臨時參照,通過它可以用來提取或修改Model中的數據。模型索引在獲得後應該立即使用,由於Model經常會重新組織內部的結構,使得模型索引失效,因此不應保存模型索引。如果需要一個對數據項的長期參照,必須創建一個永久的模型索引。這樣會爲不斷更新的Model信息提供一個參照。臨時模型索引由QModelIndex類提供,而永久模型索引則由QPersistentModelIndex類提供。

6.3、項角色(Item Roles)

在PyQt中,模型可以針對不同的組件(或者組件的不同部分,比如存儲數據、界面展示數據、按鈕的提示等)提供不同的數據。例如,Qt.DisplayRole用於視圖的文本顯示。通常來說,模型中的數據項包含一系列不同的數據角色,數據角色定義在 Qt.ItemDataRole 枚舉中,包括下列枚舉值:

Qt.DisplayRole:文本表格中要渲染顯示的數據,當存儲的內部字典值要顯示爲可理解的文字含義數據時對應數據與實際存儲數據會不一致
Qt.EditRole:編輯器中正在編輯的數據,老猿認爲這也應該是實際存儲的數據
Qt.ToolTipRole:數據項的工具提示的顯示數據
Qt.WhatsThisRole:項爲"What’s This?"模式顯示的數據
Qt.DecorationRole:數據被渲染爲圖標等裝飾(數據爲QColor/ QIcon/ QPixmap類型)
Qt.StatusTipRole:數據顯示在狀態欄中(數據爲QString類型)
Qt.SizeHintRole:數據項的大小提示,將會應用到視圖(數據爲QString類型)
Qt.CheckStateRole:數據項前面的checkbox選擇狀態,當數據項構建時使用了setCheckable(True)時會發生作用
Qt.TextAlignmentRole:數據項對齊方式,當設置了數據項的對齊格式時有效

幾個常量的值:
Qt.DisplayRole=0
Qt.DecorationRole=1
Qt.EditRole=2
Qt.ToolTipRole=3
Qt.StatusTipRole=4
Qt.WhatsThisRole=5
Qt.TextAlignmentRole=7
Qt.CheckStateRole=10
Qt.SizeHintRole=13

下圖是幾種常用數據角色的示意圖
在這裏插入圖片描述
通過爲每一個角色提供恰當的數據,模型可以告訴視圖和委託如何向用戶顯示內容。不同類型的視圖可以選擇忽略自己不需要的數據,也可以添加所需要的額外數據。

七、關於排序

在Model/View體系架構中,有兩種方法可以進行排序;選擇哪種方法取決於底層模型。

  • 如果模型是可排序的,即模型類實現了QAbstractItemModel.sort()函數,如QTableView和QTreeView都提供一個API,允許以編程方式對模型數據進行排序。此外,還可以通過將QHeaderView.sortIndicatorChanged()信號連接到QTableView .sortByColumn()槽函數或QTreeView.sortByColumn()槽函數來啓用交互式排序(即允許用戶通過單擊視圖的標題對數據進行排序)。
  • 另一種方法是,如果模型沒有所需的接口,或者想使用列表視圖(list View)來顯示數據,則在視圖中顯示數據之前,使用代理模型來轉換模型的結構。

八、代理模型Proxy Models

8.1、概述

在Model/View框架中,單個模型提供的數據項可以由任意數量的視圖共享,並且每個視圖可能以完全不同的方式表示相同的信息。自定義視圖和代理是爲同一數據提供完全不同展示結果的有效方法。但應用程序通常需要爲相同數據的已處理版本提供常規視圖,例如爲列表數據提供不同排序的展現視圖。

儘管將排序和篩選操作作爲視圖的內部方法來執行看起來可行,但是排序和篩選操作代價高,如果存在多個視圖展示相同的數據時,每個視圖數據排序按不同方式排序,如果每個視圖實現類似的方法,這種操作代價高昂。

另一種方法就是在模型本身對數據進行排序,這導致每個視圖都必須顯示根據最近的排序或刷選操作處理後的數據項,同樣代價高。

爲了解決這個問題,Model/View框架使用代理模型來管理在各個模型和視圖之間交互的信息。代理模型是一些組件,從視圖的角度來看,它們的行爲類似於普通Model,並代表該視圖訪問源模型中的數據。Model/View框架使用的信號和槽機制確保無論在其自身和源模型之間放置了多少代理模型,每個視圖都會得到適當的更新。

老猿理解代理模型就是提供在其他的model和view之間排序和過濾數據的支持功能使用的的,在代理模型中可以對項進行排序和篩選,這種方法允許一個model採用和其視圖功能匹配的要求重新組織,但不需要在數據和源模型上做任何處理,也不需要複製內存中的數據,可以有效提高效率。

8.2、使用代理模型

代理模型可以插入到現有模型和任意數量的視圖之間。PyQt和Qt提供了一個標準的代理模型QSortFilterProxyModel,它通常是直接實例化和使用的,但也可以從其派生子類來提供自定義的篩選和排序行爲。

QSortFilterProxyModel類可以按以下方式使用:

1. 定義代理模型對象

  語法:proxyModel = QSortFilterProxyModel((QObject parent)

2. 設置代理模型的數據源模型

語法:代理模型.setSourceModel(數據源模型)

其中代理模型就是第一步定義的模型,數據源模型即前面第三部分介紹的Model,爲真正訪問數據的模型。

3. 設置視圖對應模型爲代理模型

語法:視圖.setModel(proxyModel )

8.3、代理模型小結

從以上語法看到,代理模型本身對外是個Model,但自身的數據源也是個Model。
由於代理模型繼承自QAbstractItemModel,因此它們可以連接到任何類型的視圖,並且可以在視圖之間共享。它們還可用於從其他代理模型獲得信息,類似代理模型到數據Model之間象管道一樣排列使用。

QSortFilterProxyModel類被設計爲實例化並直接在應用程序中使用,也可以通過特殊派生的子類實現所需的比較操作,從而創建更專門的代理模型。

QSortFilterProxyModel的具體過濾和刷選的方法請參考類相關的方法介紹,在此不進行展開說明。

九、Model/View便利類

爲了使依賴於PyQt或Qt基於項的視圖和表類的應用程序受益,從標準視圖類派生了許多便利類,這些便利類不建議使用於派生子類。

這些類包括QListWidget、QTreeWidget和QTableWidget。這些類比視圖類更不靈活,不能與任意模型一起使用。PyQt或Qt建議使用Model/View方法處理項視圖(item views)中的數據,除非強烈需要一組基於項的類(item-based)。

如果希望在仍使用基於項的接口類時利用Model/View方法提供的功能,請考慮使用視圖類,例如使用QStandardItemModel作爲模型Model的QListView、QTableView和QTreeView視圖類。

20200123日補充:

本節部分還缺一個與Model/View架構相關的類的總體介紹,在此請大家參考《PyQt學習隨筆:Qt中Model/View相關的主要類及繼承關係》。

十、後記

本節主要介紹了:

  • Model/View架構中模型、視圖、模型索引、代理等相關的概念

  • 模型索引(Model indexes )以獨立於任何底層數據結構的方式提供有關模型中數據項的位置的信息給代理和視圖使用,模型索引是在其他組件(如視圖和代理)的請求下由模型構造的

  • 項的構成要素包括行和列編號以及其父項的模型索引

  • 角色用於區分與項關聯的代表同一數據中的要素但展現不同用途的數據

    本文主要介紹Model/View開發架構的相關概念,相關內容主要從Qt官網文章翻譯而成,結合老猿的理解做了一些補充。沒有全部寫新的內容是因爲概念引用原文比較成體系。

本節應該是Model/View相關章節的首節來介紹,但老猿也是邊學習邊摸索,可不能簡單將Qt官網文檔簡單翻譯,因此纔在此部分進行介紹。

另外老猿關於PyQt的付費專欄《使用PyQt開發圖形界面Python應用》只需要9.9元,該部分與第十五章的內容基本對應,但同樣內容在付費專欄上總體來說更詳細、案例更多。本節內容在付費專欄的《第十四章、Model/View開發:Model/View架構程序設計模式》。如果有興趣也願意支持老猿的讀者,歡迎購買付費專欄。

老猿Python,跟老猿學Python!

發佈了623 篇原創文章 · 獲贊 3419 · 訪問量 34萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章