如何从 Python3 任意长度的字符串数组构建 QML ListElements

How to build QML ListElements from Python3 array of strings of arbitrary length

我是 QML、QtQuick 和 Python 的新手。我想使用 QML 显示文件列表(完整路径)。看来我应该使用 ListView 和 ListElements。我发现的示例和教程都使用硬编码且非常简单的列表数据。我不明白如何从这些示例转到更现实的东西。

如何使用后端的 Python 字符串数组来填充 QML UI 显示的列表?

字符串数组的长度是任意的。我希望列表项是可点击的(可能像 QML url 类型)。他们将打开该 file/url 类型的操作系统默认应用程序。

我的后台代码是这样的:

import sys
from subprocess import Popen, PIPE
import getpass
from PyQt5.QtWidgets import QApplication, QMessageBox
from PyQt5.QtCore import Qt, QCoreApplication, QObject, pyqtSlot
from PyQt5.QtQml import QQmlApplicationEngine

class Backend(QObject):

basepath = '/path/to/files'
list_files_cmd = "find " + basepath + " -type f -readable"

myfiles = Popen(list_files_cmd, shell=True, stdout=PIPE, stderr=PIPE)
output, err = myfiles.communicate()
# the output is a Byte literal like this: b'/path/to/file1.txt\n/path/to/file2.txt\n'. Transform into a regular string:
newstr = output.decode(encoding='UTF-8')
files_list = newstr.split('\n')
for file in files_list:
    print(file)

if __name__ == '__main__':

    backend = Backend()

    QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
    QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)
    app = QApplication(sys.argv)
    engine = QQmlApplicationEngine('view.qml')
    engine.rootContext().setContextProperty("backend", backend)
    sys.exit(app.exec_())

现在我只是将 files_list 字符串数组从后端打印到控制台,但目标是使用该字符串数组填充 UI 中的 QML 列表。

files_list内容的一个例子是:

['/path/to/files/xdgr/todo.txt', '/path/to/files/xdgr/a2hosting.txt', '/path/to/files/xdgr/paypal.txt', '/path/to/files/xdgr/toggle.txt', '/path/to/files/xdgr/from_kty.txt', '/path/to/files/xdgr/feed59.txt', '/path/to/files/one/sharing.txt', '/path/to/files/two/data.dbx', '']

(我需要弄清楚如何处理该数组末尾的空字符串。)

我的 QML 的大致轮廓(尽我目前的能力)是这样的:

import QtQml.Models 2.2
import QtQuick.Window 2.2
import QtQuick 2.2
import QtQuick.Controls 1.3
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3

ApplicationWindow {
    visible: true
    TabView {
        anchors.fill: parent
        Tab {
            title: "Files"
            anchors.fill: parent
            ListView {
                id: mListViewId
                anchors.fill: parent
                model: mListModelId
                delegate : delegateId
            }
            ListModel {
                id: mListModelId
                // I would like backend.files_list to provide the model data
            }
        }
    } 
    Component.onCompleted: {
        mListModelId.append(backend.files_list)
    }
}

我发现的最相关的问题是这些,但它们没有解决我的问题:

qt - 动态创建 QML ListElement 和内容 - 堆栈

qt - QML ListElement 传递字符串列表 - 堆栈

您不需要使用 ListModel 来填充 ListView,因为 the docs 指出模型可以是列表:

model : model

This property holds the model providing data for the list.

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.

(强调我的)

我也推荐Data Models

在这种情况下,可以通过pyqtProperty显示一个列表。另一方面,不要使用 subprocess.Popen() 因为它阻塞导致 GUI 冻结,而是使用 QProcess.

import os
import sys

from PyQt5.QtCore import (
    pyqtProperty,
    pyqtSignal,
    pyqtSlot,
    QCoreApplication,
    QObject,
    QProcess,
    Qt,
    QUrl,
)
from PyQt5.QtWidgets import QApplication
from PyQt5.QtQml import QQmlApplicationEngine


class Backend(QObject):
    filesChanged = pyqtSignal()

    def __init__(self, parent=None):
        super().__init__(parent)

        self._files = []

        self._process = QProcess(self)
        self._process.readyReadStandardOutput.connect(self._on_readyReadStandardOutput)
        self._process.setProgram("find")

    @pyqtProperty(list, notify=filesChanged)
    def files(self):
        return self._files

    @pyqtSlot(str)
    def findFiles(self, basepath):
        self._files = []
        self.filesChanged.emit()
        self._process.setArguments([basepath, "-type", "f", "-readable"])
        self._process.start()

    def _on_readyReadStandardOutput(self):
        new_files = self._process.readAllStandardOutput().data().decode().splitlines()
        self._files.extend(new_files)
        self.filesChanged.emit()


if __name__ == "__main__":

    QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
    QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)
    app = QApplication(sys.argv)

    backend = Backend()

    engine = QQmlApplicationEngine()
    engine.rootContext().setContextProperty("backend", backend)

    current_dir = os.path.dirname(os.path.realpath(__file__))
    filename = os.path.join(current_dir, "view.qml")
    engine.load(QUrl.fromLocalFile(filename))

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())

view.qml

import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Controls 1.4

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    TabView {
        anchors.fill: parent
        Tab {
            title: "Files"
            ListView {
                id: mListViewId
                clip: true
                anchors.fill: parent
                model: backend.files
                delegate: Text{
                    text: model.modelData
                }
                ScrollBar.vertical: ScrollBar {}
            }
        }
    }
    Component.onCompleted: backend.findFiles("/path/to/files")
}

您也可以使用 QStringListModel。

import os
import sys

from PyQt5.QtCore import (
    pyqtProperty,
    pyqtSignal,
    pyqtSlot,
    QCoreApplication,
    QObject,
    QProcess,
    QStringListModel,
    Qt,
    QUrl,
)
from PyQt5.QtWidgets import QApplication
from PyQt5.QtQml import QQmlApplicationEngine


class Backend(QObject):
    def __init__(self, parent=None):
        super().__init__(parent)

        self._model = QStringListModel()

        self._process = QProcess(self)
        self._process.readyReadStandardOutput.connect(self._on_readyReadStandardOutput)
        self._process.setProgram("find")

    @pyqtProperty(QObject, constant=True)
    def model(self):
        return self._model

    @pyqtSlot(str)
    def findFiles(self, basepath):
        self._model.setStringList([])
        self._process.setArguments([basepath, "-type", "f", "-readable"])
        self._process.start()

    def _on_readyReadStandardOutput(self):
        new_files = self._process.readAllStandardOutput().data().decode().splitlines()
        self._model.setStringList(self._model.stringList() + new_files)


if __name__ == "__main__":

    QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
    QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)
    app = QApplication(sys.argv)

    backend = Backend()

    engine = QQmlApplicationEngine()
    engine.rootContext().setContextProperty("backend", backend)

    current_dir = os.path.dirname(os.path.realpath(__file__))
    filename = os.path.join(current_dir, "view.qml")
    engine.load(QUrl.fromLocalFile(filename))

    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())

view.qml

import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Controls 1.4

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    TabView {
        anchors.fill: parent
        Tab {
            title: "Files"
            ListView {
                id: mListViewId
                clip: true
                anchors.fill: parent
                model: backend.model
                delegate: Text{
                    text: model.display
                }
                ScrollBar.vertical: ScrollBar {}
            }
        }
    }
    Component.onCompleted: backend.findFiles("/path/to/files")
}