Quantcast
Channel: Active questions tagged qtquick2 - Stack Overflow
Viewing all articles
Browse latest Browse all 107

Building a simple QML `TreeView` based off a Python dictionary

$
0
0

Introduction

I am using Qt for Python, specific Qt Quick with QML and PySide6, to build a GUI application. The application is a log viewer, and I want a TreeView QML type to display the hierarchy of logger names. The logger names, which are . separated to build a hierarchy, get parsed from a log file to a list:

[    ["root", "child"],    ["root"],    ["asyncio", "clients", "camera"],    ["root", "controller"],    ["root", "controller", "telemetry"],]

which is then converted to a Python dictionary:

{"root": {"child": {},"controller": {"telemetry": {}},    },"asyncio": {"clients": {"camera": {}}},}

GUI prototype

Now, I want this dictionary to be presented as a tree view with checkboxes in the GUI such that it will toggle from which logger names/paths the log messages will display from. The tree view should look something like this:

enter image description here

The tree view is on the left.

QML and backend Python

To try and implement this in QML and Python, I have something that looks like this:

// Main.qmlimport QtQuickimport QtQuick.Windowimport QtQuick.Layoutsimport QtQuick.Controlsimport com.viewerWindow {    width: 740    height: 540    visible: true    title: "Python log viewer"    RowLayout {        TreeView {            id: names            Layout.preferredWidth: 100            Layout.fillWidth: true            Layout.fillHeight: true            model: LogViewer.log_names            selectionModel: ItemSelectionModel {}            delegate: Item {                // Assigned to by TreeView:                required property TreeView treeView                required property bool isTreeNode                required property bool expanded                required property int hasChildren                required property int depth                required property int row                required property int column                required property bool current                Text {                    text: treeView.index(row, column)                }            }        }        ScrollView {            id: scrollView            Layout.fillWidth: true            TextArea {                id: completeLog                text: LogViewer.complete_log            }        }    }}

Here is the backing Python code that implements a Qt singleton and the various models:

# main.py# These variables *must* be placed prior to the imports due to some particulars of how PySide6 works.# The import name is how QML code can import the singleton object defined in this file.# The import can be done like this: `import com.als.gui`QML_IMPORT_NAME = "com.viewer"QML_IMPORT_MAJOR_VERSION = 1# Core dependenciesfrom datetime import datetimefrom pathlib import Pathimport reimport sysfrom typing import NamedTuple, Any# Package dependenciesfrom PySide6.QtCore import QObject, Signal, Slot, Property, QTimer, Qt, QAbstractItemModel, QModelIndex  # type: ignore[import-not-found]from PySide6.QtGui import QGuiApplication  # type: ignore[import-not-found]from PySide6.QtQml import QQmlApplicationEngine, QmlElement, QmlSingleton  # type: ignore[import-not-found]# How the logger name paths, which are normally dot-separated like "root.child",# are parsed into lists.logger_name_hierarchy = [    ["root", "child"],    ["root"],    ["asyncio", "clients", "camera"],    ["root", "controller"],    ["root", "controller", "telemetry"],]# Just an example of the tree as a dictionary that the above lists are rearranged intologger_name_dictionary = {"root": {"child": {},"controller": {"telemetry": {}},    },"asyncio": {"clients": {"camera": {}}},}class LogEntry(NamedTuple):    Timestamp: datetime    Name: list[str]    Level: str    Message: str    File: str    Line: int    ThreadID: int    ThreadName: str | None    AsyncioTaskName: str | Nonelog_pattern = re.compile(    r"(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) :: "    r"(?P<name>[^:].+) :: "    r"(?P<level>[^:].+) :: "    r"(?P<message>[^:].+) :: "    r"(?P<file>[^:].+) :: "    r"(?P<line>\d+) :: "    r"(?P<thread_id>\d+) :: "    r"(?P<thread_name>[^:].+) :: "    r"(?P<asyncio_task_name>.+)")def parse_log_line(line: str) -> LogEntry:    match = log_pattern.match(line)    print(match)    if match:        return LogEntry(            Timestamp=datetime.strptime(                match.group("timestamp"), "%Y-%m-%d %H:%M:%S.%f"            ),            Name=match.group("name").split("."),            Level=match.group("level"),            Message=match.group("message"),            File=match.group("file"),            Line=int(match.group("line")),            ThreadID=int(match.group("thread_id")),            ThreadName=(                match.group("thread_name")                if match.group("thread_name") != "None"                else None            ),            AsyncioTaskName=(                match.group("asyncio_task_name")                if match.group("asyncio_task_name") != "None"                else None            ),        )    else:        raise ValueError("Line does not match log pattern")def parse_log_file(log_file: str) -> list[LogEntry]:    with open(log_file, "r") as file:        return [            parse_log_line(line.strip())            for line in file.readlines()            if line.strip() != ""        ]def insert_into_tree(tree, path):    current = tree    for node in path:        if node not in current:            current[node] = {}        current = current[node]def build_tree(paths):    tree = {}    for path in paths:        insert_into_tree(tree, path)    return tree# Ignore MyPy error: Class cannot subclass "QObject" (has type "Any")@QmlElement@QmlSingletonclass LogViewer(QObject):  # type: ignore[misc]    log_changed = Signal()    status_changed = Signal()    complete_log_changed = Signal()    log_names_changed = Signal()    def __init__(self) -> None:        super().__init__()        self.__log = self.complete_log        self.complete_log_changed.emit()        self.__log_names = self.log_names        self.log_names_changed.emit()    @Property(str, notify=complete_log_changed)    def complete_log(self) -> str:        lines = [str(line) for line in parse_log_file(r"example.log")]        print(lines)        return ("\n").join(lines)    @Property(QAbstractItemModel, notify=log_names_changed)    def log_names(self) -> QAbstractItemModel:        tree = build_tree([line.Name for line in parse_log_file(r"example.log")])        # This needs to be a QAbstractItemModel that can be displayed in the QML        # It is this where I am struggling what to do and how to properly subclass        # and implement a QAbstractItemModel.        return treeif __name__ == "__main__":    application = QGuiApplication(sys.argv)    engine = QQmlApplicationEngine()    qml_file = Path(__file__).resolve().parent / "qml" / "Main.qml"    engine.load(qml_file)    if not engine.rootObjects():        sys.exit(-1)    als_model: LogViewer = engine.singletonInstance("com.viewer", "LogViewer")    print(als_model)    sys.exit(application.exec())

Here is an example log file:

2024-07-01 19:16:03.326 :: root.child :: DEBUG :: This is a debug message :: __init__.py :: 22 :: 26480 :: MainThread :: None2024-07-01 19:16:03.326 :: root.child :: INFO :: This is an info message :: __init__.py :: 23 :: 26480 :: MainThread :: None2024-07-01 19:16:03.326 :: root.child :: WARNING :: This is a warning message :: __init__.py :: 24 :: 26480 :: MainThread :: None2024-07-01 19:16:03.326 :: root.child :: ERROR :: This is an error message :: __init__.py :: 25 :: 26480 :: MainThread :: None2024-07-01 19:16:03.326 :: root.child :: CRITICAL :: This is a critical message :: __init__.py :: 26 :: 26480 :: MainThread :: None2024-07-01 19:16:03.326 :: asyncio :: DEBUG :: This is a debug message :: __init__.py :: 28 :: 26480 :: MainThread :: None2024-07-01 19:16:03.326 :: asyncio :: INFO :: This is an info message :: __init__.py :: 29 :: 26480 :: MainThread :: None2024-07-01 19:16:03.326 :: asyncio :: WARNING :: This is a warning message :: __init__.py :: 30 :: 26480 :: MainThread :: None2024-07-01 19:16:03.326 :: asyncio :: ERROR :: This is an error message :: __init__.py :: 31 :: 26480 :: MainThread :: None2024-07-01 19:16:03.326 :: asyncio :: CRITICAL :: This is a critical message :: __init__.py :: 32 :: 26480 :: MainThread :: None2024-07-01 19:16:03.326 :: asyncio.clients :: DEBUG :: This is a debug message :: __init__.py :: 34 :: 26480 :: MainThread :: None2024-07-01 19:16:03.326 :: asyncio.clients :: INFO :: This is an info message :: __init__.py :: 35 :: 26480 :: MainThread :: None2024-07-01 19:16:03.326 :: asyncio.clients :: WARNING :: This is a warning message :: __init__.py :: 36 :: 26480 :: MainThread :: None2024-07-01 19:16:03.326 :: asyncio.clients :: ERROR :: This is an error message :: __init__.py :: 37 :: 26480 :: MainThread :: None2024-07-01 19:16:03.326 :: asyncio.clients :: CRITICAL :: This is a critical message :: __init__.py :: 38 :: 26480 :: MainThread :: None

Final questions

  1. How do I properly implement a QAbstractItemModel that "wraps" or converts the dictionary:
{"root": {"child": {},"controller": {"telemetry": {}},    },"asyncio": {"clients": {"camera": {}}},}

into something that TreeView can display? It seems that this should be straightforward, but the Qt documentation is too terse on this for me to figure out, and I have been unable to find any examples. I was thinking that a dictionary could automatically be treated by Qt, as lists are for QAbstractListModel, but that doesn't seem to be the case.

  1. Does my QML need adjusting once this is working, in particular for the selection model and delegate?

Thank you!


Viewing all articles
Browse latest Browse all 107

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>