PyQt (PySide) 與 QML 互操作 - PyQt, QML ListView, model, QAbstractListModel

需求描述

我們需要通過 Python 操作 QML 對象, 使 QML 的 ListView 對象動態地加載元素.

實現

初始化

假設目錄結構爲:

demo
|- main.py
|- MyItem.qml  # 用於 view.qml 的列表元素. 注意文件名首字母必須大寫, 否則 QML 導入機制不能引用.
|- view.qml

最初的代碼長這樣:

// === view.qml ===
import QtQuick 2.14
import QtQuick.Window 2.12

Window {
    width: 600; height: 400
    visible: true

    ListView {
        objectName: "my_listview"
        anchors.fill: parent
        delegate: MyItem {
            my_name: name
            my_age: age
        }
    }
}

// === MyItem.qml ===
import QtQuick 2.14

Item {
    width: 100; height: 50

    property string my_name: ""
    property int my_age: 0
    
    Text {
        id: _name
        text: my_name
    }
    Text {
        id: _age
        anchors.left: _name.right  // 年齡位於姓名右側, 並相隔 10px 距離.
        anchors.leftMargin: 10
        text: my_age
    }
}

# === main.py ===
import sys

from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtWidgets import QApplication

if __name__ == '__main__':
    app = QApplication()
    engine = QQmlApplicationEngine('./view.qml')
    sys.exit(app.exec_())

啓動後如圖所示:

在這裏插入圖片描述

使用 QAbstractListModel

在 Qt 助手中, 我們查閱相關文檔知道, QML.ListView 通過 model 屬性綁定模型數據:

ListView {
    model: xxx
}

The model provides the set of data that is used to create the items in the view. Models can be created directly in QML using ListModel, XmlListModel or ObjectModel, or provided by C++ model classes. If a C++ model class is used, it must be a subclass of QAbstractItemModel or a simple list.

官方文檔說如果要通過 C++ 實現 (對於我們 PyQt 來說就是 Python 實現), 就需要使用 QAbstractItemModel 類 – PyQt.Core.QAbstractItemModel.

當查閱 QAbstractItemModel 的文檔時, 官方說 QAbstractItemModel 其實並不適合 QML.ListView, 更適合的是 QAbstractListModel 類 – PyQt.Core.QAbstractListModel – 所以這就是我們爲什麼要使用 QAbstractListModel 原因.

使用 QAbstractListModel 需創建一個子類繼承它, 並且 必須 覆寫父類的 rowCount(), data() 方法. 如下所示:

# === main.py ===
import sys

from PySide2.QtCore import *
from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtWidgets import QApplication


class MyListModel(QAbstractListModel):
    
    def __init__(self, model):
        """
        :param model: a list of persons. e.g. [{'name': 'Li Ming', 'age': 20},]
        """
        super().__init__()
        self.model = model

    # 必須覆寫 rowCount(), data() 方法.
    def rowCount(self, parent=None) -> int:
        return len(self.model)
    
    def data(self, index: QModelIndex, role: int = None):
        """
        QML.ListView 會從這個方法取數據, role 相當於 QML.ListView 請求的 "鍵", 
        我們需要根據 "鍵" 返回相對應的 "值".
         
        :param index: 特別注意, 這個是 QModelIndex 類型. 通過 QModelIndex.row()
            可以獲得 int 類型的位置. 這個位置是列表元素在列表中的序號, 從 0 開始
            數.
        :param role: 
        :return: 
        """
        index = index.row()
        row = self.model[index]  # type: dict
        # e.g. row = {'name': 'Li Ming', 'age': 20}
        
        # TODO: 根據 role 確定想要的 key, 並返回 row[key].
        #  (關於 role 的知識接下來要講.)


if __name__ == '__main__':
    app = QApplication()
    engine = QQmlApplicationEngine('./view.qml')
    sys.exit(app.exec_())

QAbstractListModel 的 roles

在 Python 模型中, 我們定義了 dict 類型的元素作爲列表元素的模型數據.

但是 Python 與 QML 的數據類型是不通用的, 也就是說 QML 無法直接識別 Python dict 類型的對象. 所以就有了 “Role”.

對於本示例的 MyItem 來說, Name 和 Age 就是兩個自定義的 Role. Role 可以用任意的整數來表示, 比如我們用:

class MyListModel(QAbstractListModel):
    NAME = 0
    AGE = 1
    
    ...
    

這樣來表示… 是不妥的. 因爲要避免和其他 Qt flag 常量重複, 所以推薦用 Qt.UserRole + int 來表示:

from PySide2.QtCore import Qt


class MyListModel(QAbstractListModel):
    NAME = Qt.UserRole + 0  # PS: Qt.UserRole 是一個常量, 值爲 0x100.
    AGE = Qt.UserRole + 1
    
    ...
    

定義好了 Role 以後, 在 data() 中就可以由 Role 來間接地表達 “鍵” 的功能了:

class MyListModel(QAbstractListModel):
    NAME = Qt.UserRole + 0
    AGE = Qt.UserRole + 1
    
    def data(self, index: QModelIndex, role: int = None):
        """
        QML.ListView 會從這個方法取數據, role 相當於 QML.ListView 請求的 "鍵", 
        我們需要根據 "鍵" 返回相對應的 "值".
         
        :param index: 特別注意, 這個是 QModelIndex 類型. 通過 QModelIndex.row()
            可以獲得 int 類型的位置. 這個位置是列表元素在列表中的序號, 從 0 開始
            數.
        :param role: 
        :return: 
        """
        index = index.row()
        row = self.model[index]  # type: dict
        # e.g. row = {'name': 'Li Ming', 'age': 20}
        
        # 根據 role 確定想要的 key, 並返回 row[key].
        if role == self.NAME:
            return row['name']
        elif role == self.AGE:
            return row['age']

    ...
    

這裏還有一個問題我們沒有解決: QML.ListView 如何知道這兩個角色跟自己有關呢?

// === view.qml ===
import QtQuick 2.14
import QtQuick.Window 2.12

Window {
    width: 600; height: 400
    visible: true

    ListView {
        objectName: "my_listview"
        anchors.fill: parent
        delegate: MyItem {
            my_name: name  // 我如何知道 name 是 model.NAME 呢? 爲什麼不可以是 
            // model.Name, model.MyName, model.MY_NAME, model.Person... 呢?
            my_age: age
        }
    }
}

所以這裏還缺了一個方法, MyListModel 需要覆寫父類的 roleNames() 方法完成二者的 “關聯”:

class MyListModel(QAbstractListModel):
    NAME = Qt.UserRole + 0
    AGE = Qt.UserRole + 1
    
    def roleNames(self):
        return {
            self.NAME: b'name',  # 此處的 b'name' 是指 QML 中的 name. 特別注意的
            # 是必須是 bytes 類型, 而不是 str 類型.
            self.AGE: b'age',
        }
    
    ...
    

這樣 MyListModel 纔算完整了.

運行

完整的 main.py 如下所示, 注意我加了 load_model() 方法用於實例化 MyListModel 並與 QML 產生操作.

# === main.py ===
import sys

from PySide2.QtCore import *
from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtWidgets import QApplication


def load_model(root):
    listview = root.findChild(QObject, 'my_listview')  # type: QObject
    
    # 這裏第一個參數 'model' 會被 Pycharm 顯示黃色警示, setProperty() 似乎要求的
    # 是 bytes 類型; 但改成 b'model' 程序就報錯了. 所以我忽略了黃色警示.
    # noinspection PyTypeChecker
    listview.setProperty('model', MyListModel(
        [
            {'name': 'Li Ming', 'age': 20},
            {'name': 'Zhang Lin', 'age': 23},
        ]
    ))


class MyListModel(QAbstractListModel):
    NAME = Qt.UserRole + 1
    AGE = Qt.UserRole + 2
    
    def __init__(self, model):
        """
        :param model: a list of persons. e.g. [{'name': 'Li Ming', 'age': 20},]
        """
        super().__init__()
        self.model = model

    # 必須覆寫 rowCount(), data() 方法.
    def rowCount(self, parent=None) -> int:
        return len(self.model)
    
    def data(self, index: QModelIndex, role: int = None):
        """
        QML.ListView 會從這個方法取數據, role 相當於 QML.ListView 請求的 "鍵",
        我們需要根據 "鍵" 返回相對應的 "值".
        
        :param index: 特別注意, 這個是 QModelIndex 類型. 通過 QModelIndex.row()
            可以獲得 int 類型的位置. 這個位置是列表元素在列表中的序號, 從 0 開始
            數.
        :param role:
        :return:
        """
        index = index.row()
        row = self.model[index]  # type: dict
        # e.g. row = {'name': 'Li Ming', 'age': 20}
        
        # 根據 role 確定想要的 key, 並返回 row[key].
        if role == self.NAME:
            return row['name']
        elif role == self.AGE:
            return row['age']

    def roleNames(self):
        return {
            self.NAME: b'name',  # 此處的 b'name' 是指 QML 中的 name. 特別注意的
            # 是必須是 bytes 類型, 而不是 str 類型.
            self.AGE: b'age',
        }


if __name__ == '__main__':
    app = QApplication()
    engine = QQmlApplicationEngine('./view.qml')
    load_model(engine.rootObjects()[0])  # engine.rootObjects()[0] 獲得的是 QML 
    # 的根對象.
    sys.exit(app.exec_())

運行截圖如下:

在這裏插入圖片描述

參考

  • https://www.xdbcb8.com/archives/701.html 一個模擬 QQ 聯繫人分組的演示, 把視圖/模型的概念講得很清楚.
  • http://cn.voidcc.com/question/p-zilozvfe-ug.html 這篇文章的幫助很大, 直接解決了這篇文章的全部問題.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章