Use QEnum like a Python Enum

Question:

Let’s say I have a python enum:

class UserState(Enum):
    OFFLINE = auto()
    ONLINE = auto()
    BUSY = auto()

I can access the different options with UserState.ONLINE, UserState.OFFLINE or UserState.BUSY.
If I wanted to make this a QEnum so I can use it in QML, I’d need to wrap it inside a QObject like this:

class UserState(QObject):
    @QEnum
    class Options(Enum):
        OFFLINE = auto()
        ONLINE = auto()
        BUSY = auto()

In QML I can access this enum now the same way I’d access a normal python enum in python. However if I wanted to access this enum from python, I’d have to write UserState.Options.ONLINE.

How can I create an enum that will work in python as well as QML using the same syntax?

I have found a solution for this which I will post in the answers section. However it involves nested metaclasses which just doesn’t look right. I think the optimal solution would be a class that derives from QObject as well as Enum to have all the functionality for every context.

If anyone can provide a version which works like that, I will make that the accepted answer.
Otherwise you can tell me, why my solution actually is a good one.

Asked By: Tim Woocker

||

Answers:

Here is a class I have written which adds the support I’m asking for in the question:

class CustomEnumMeta(type(Enum)):
    def __new__(cls, name, bases, attrs):
        # don't change type of base class
        if name == "CustomEnum":
            return super().__new__(cls, name, bases, attrs)

        original_attrs = attrs.copy()
        enum = super().__new__(cls, name, bases, attrs)

        class WrapperMeta(type(QObject)):
            def __new__(cls, wrapper_name, wrapper_bases, wrapper_attrs):
                return super().__new__(cls, wrapper_name, wrapper_bases, {**original_attrs, **wrapper_attrs})

        class Wrapper(QObject, metaclass=WrapperMeta):
            QEnum(enum)

        Wrapper.__name__ = name

        return Wrapper


class CustomEnum(Enum, metaclass=CustomEnumMeta):
    pass

The CustomEnum class can be inherited just like a normal python enum:

class UserState(CustomEnum):
    OFFLINE = auto()
    ONLINE = auto()
    BUSY = auto()

Now you can use UserState the same way in python as in QML. This is a valid statement for both python and QML: UserState.ONLINE

My implementation works by replacing the original class with a QObject, nesting the original class. And then copying all the attributes of the nested class to the outer class to make them accessible from python.

Answered By: Tim Woocker

You can just call QEnum inside the class and define the enum outside.

Example (PySide6):

file: main.py

import sys
from enum import Enum
from pathlib import Path

from PySide6.QtCore import Property, QEnum, QObject
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import (
    QmlElement,
    QQmlApplicationEngine,
    qmlRegisterSingletonInstance,
)

QML_IMPORT_NAME = "com.example.app"
QML_IMPORT_MAJOR_VERSION = 1


class Status(Enum):
    Connected, Disconnected, Stale = range(3)


@QmlElement
class Enums(QObject):
    QEnum(Status)


class App(QObject):
    def __init__(self):
        super().__init__(None)

    @Property(int, constant=True)
    def enumValue(self):
        return Status.Connected.value


if __name__ == "__main__":
    loop = QGuiApplication(sys.argv)
    engine = QQmlApplicationEngine()
    app = App()
    qmlRegisterSingletonInstance(QObject, "com.example.app", 1, 0, "App", app)
    qml_file = Path(__file__).parent / "main.qml"
    engine.load(str(qml_file))

    sys.exit(loop.exec())

file: main.qml

import QtQuick
import com.example.app 1.0

Window {
    visible: true
    width: 1000
    height: 700
    title: "POC"

    Rectangle {
        color: App.enumValue == Enums.Status.Connected ? "green" : "red"
        anchors.fill: parent
    }
}

Full code here

Answered By: ניר
Categories: questions Tags: , , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.