3

I'm new to QML, QtQuick and Python. I would like to display a list of files (full path) using QML. It seems like I should use a ListView and ListElements. The examples and tutorials I have found all use list data that is hard-coded and very simple. I do not understand how to go from those examples to something more realistic.

How do I use a Python string array from my backend to populate a list displayed by the QML UI?

The length of the string array is arbitrary. I want the list items to be clickable (like a QML url type, possibly). They will open the operating system's default application for that file/url type.

My backend code is similar to this:

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_())

Right now I am just printing the files_list string array from the backend to the console, but the goal is to use that string array to populate the QML list in the UI.

An example of the contents of files_list is:

['/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', '']

(I will need to figure out how to deal with the null string at the end of that array.)

A rough outline of my QML (to the best of my current ability) is like this:

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)
    }
}

The most relevant questions I have found are these, but they did not resolve my issue:

qt - Dynamically create QML ListElement and content - Stack Overflow Dynamically create QML ListElement and content

qt - QML ListElement pass list of strings - Stack Overflow QML ListElement pass list of strings

5
  • Look into QStringlistModel, ListModel isn't suited here Commented Jan 11, 2020 at 21:19
  • Anything like this for Python? bogotobogo.com/Qt/… Commented Jan 11, 2020 at 21:30
  • @FrankOsterfeld QStringlistModel appears to be a C++ class. I'm using Python. Commented Jan 11, 2020 at 22:16
  • @MountainX-for-Monica PyQt5 and PySide2 are a binding of Qt5 so most C++ classes exist in python. Commented Jan 12, 2020 at 2:47
  • See e.g. doc.qt.io/qtforpython/PySide2/QtCore/QStringListModel.html Commented Jan 12, 2020 at 7:27

1 Answer 1

5

You don't need to use a ListModel to populate a ListView since as the docs points out a model can be a list:

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.

(emphasis mine)

I also recommend Data Models.

In this case, a list can be displayed through a pyqtProperty. On the other hand do not use subprocess.Popen() as it is blocking causing the GUI to freeze, instead use 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")
}

You can also use 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")
}
Sign up to request clarification or add additional context in comments.

3 Comments

Your answers are amazing. Thank you.
I am using the QStringListModel. Thanks for including that. It helped me learn. How do make the ListView refresh? I have a button that will delete a selected file in the list, but I don't know how to then cause the ListView to refresh with the remaining files.
@MountainX-for-Monica Only implement on the python side: @pyqtSlot() def clear(self): self._model.setStringList([]) and the QML side you use: backend.clear()

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.