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 这篇文章的帮助很大, 直接解决了这篇文章的全部问题.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章