Search code examples
pythonqmlpyside2scxml

weird behaviour of QScxmlStateMachine.connectToEvent in PySide2


I am trying to write a desktop application using Python on backend, QML on frontend and scxml for creating state machine. I am using PySide2. My intention is to describe logic of the application by state machine. Based on the state, UI in QML should react accordingly. Also, backend should execute methods based on entering states, etc.

My project consist of 3 files: main.py with backend logic, ui.qml with UI, stateMachine.scxml with State machine.

content of main.py:

from PySide2.QtScxml import QScxmlStateMachine
from PySide2.QtWidgets import QApplication
from PySide2.QtCore import QUrl, QObject, Slot
from PySide2.QtQml import QQmlApplicationEngine


class BackEnd(QObject):
    def __init__(self):
        super().__init__()

    @Slot()
    def ev_slot(self):
        '''method called on event t1'''
        print('event t1')

    @Slot(bool)
    def st_slot(self, active):
        '''method called on entering and exiting state s2'''
        if active:
            print('s2 entered')
        else:
            print('s2 exited')


app = QApplication([])
qml_url = QUrl("ui.qml")

engine = QQmlApplicationEngine()

# loading state machine
my_state_machine = QScxmlStateMachine.fromFile('stateMachine.scxml')

backend = BackEnd()

# registering state machine in QML context
engine.rootContext().setContextProperty("stateMachine", my_state_machine)
# connecting event of state machine to method of backend
conn1 = my_state_machine.connectToEvent("t1", backend, "aev_slot()")
# connecting state of state machine to method of backend
conn2 = my_state_machine.connectToState("s2", backend, "ast_slot(bool)")

my_state_machine.start()

engine.load(qml_url)

app.exec_()

UI is very simple: it consists only of 3 buttons which are submitting events to the state machine (ui.qml file):

import QtQuick 2.0
import QtQuick.Controls 2.2


ApplicationWindow {
    width: column.width
    height: column.height
    visible: true

    Column {
        id: column
        spacing: 10
        Button {
            id: button0
            text: qsTr("t1")
            onClicked: stateMachine.submitEvent('t1')
        }

        Button {
            id: button1
            text: qsTr("t2")
            onClicked: stateMachine.submitEvent('t2')
        }

        Button {
            id: button2
            text: qsTr("t3")
            onClicked: stateMachine.submitEvent('t3')
        }
    }

}

State machine stateMachine.scxml consists of 3 states: s1, s2, s3 and transitions t1, t2, t3:

<?xml version="1.0" encoding="UTF-8"?>
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" binding="early" xmlns:qt="http://www.qt.io/2015/02/scxml-ext" name="stateMachine" initial="s1">
    <state id="s1">
        <transition type="external" event="t1" target="s2"/>
        <onentry>
            <log label="entered" expr="s1"/>
        </onentry>
    </state>
    <state id="s2">
        <transition type="external" event="t2" target="s3"/>
        <onentry>
            <log label="entered" expr="s2"/>
        </onentry>
    </state>
    <state id="s3">
        <transition type="external" event="t3" target="s1">
        </transition>
        <onentry>
            <log label="entered" expr="s3"/>
        </onentry>
    </state>
</scxml>

The problem here is that everything works. Line my_state_machine.connectToEvent("t1", backend, "aev_slot()") has a mistake: method name is ev_slot(), not aev_slot(). But if I change it to a proper name, I am getting the following error:

QObject::connect: No such slot BackEnd::v_slot()

Somehow, the first letter in method's name is ignored. Am I doing something wrong? I am very new to Qt and PySide2. Is it overall a good approach? I am using PySide2 5.11.1a1.dev1530708810518


Solution

  • You have to use SLOT() to pass the method as a string (keep in mind that SLOT is different from the Slot decorator).

    from PySide2 import QtCore, QtGui, QtQml, QtScxml
    
    
    class BackEnd(QtCore.QObject):
        @QtCore.Slot()
        def ev_slot(self):
            '''method called on event t1'''
            print('event t1')
    
        @QtCore.Slot(bool)
        def st_slot(self, active):
            '''method called on entering and exiting state s2'''
            if active:
                print('s2 entered')
            else:
                print('s2 exited')
    
    
    if __name__ == '__main__':
        import sys
        app = QtGui.QGuiApplication(sys.argv)
        qml_url = QtCore.QUrl.fromLocalFile("ui.qml")
    
        # loading state machine
        my_state_machine = QtScxml.QScxmlStateMachine.fromFile('stateMachine.scxml')
    
        backend = BackEnd()
        conn1 = my_state_machine.connectToEvent("t1", backend, QtCore.SLOT("ev_slot()"))
        conn2 = my_state_machine.connectToState("s2", backend, QtCore.SLOT("st_slot(bool)"))
        my_state_machine.start()
    
        engine = QtQml.QQmlApplicationEngine()
        engine.rootContext().setContextProperty("stateMachine", my_state_machine)
        engine.load(qml_url)
    
        if not engine.rootObjects():
            sys.exit(-1)
    
        sys.exit(app.exec_())
    

    SLOT only prefixes 1 to the name of the slot, and SIGNAL does the same with 2, so if you do not want to use it just prefix 1: (..., backend, "1ev_slot()"), (I do not recommend doing it since it makes the code less readable)