使用 PySide2 将 python 信号连接到 QML ui 插槽

Posted

技术标签:

【中文标题】使用 PySide2 将 python 信号连接到 QML ui 插槽【英文标题】:Connect python signal to QML ui slot with PySide2 【发布时间】:2019-01-02 16:58:17 【问题描述】:

我刚刚开始为即将到来的项目使用 PySide2 和 QML,我立即偶然发现了一个问题:如何将 python 类(继承自 QObject)发出的信号连接到 .qml 文件中的插槽? 例如:我有一个 QThread(python 类),它每 50 毫秒生成几个 xy 坐标。我想将生成的对添加到 QML 文件中定义的 LineSeries 中,以生成类似示波器的图。

这个问题可能真的很简单而且很愚蠢,但我真的需要一些帮助。

最好的问候

兰多

编辑 4:

我找到了解决方案,但我不太喜欢它。你能建议我(如果存在的话)一种更优雅的方法吗?

Python 代码:

class Manager(QObject):

    dataReady = Signal(float,float)

    def __init__(self):
        QObject.__init__(self)
        self._currX = 0
        self._currY = 0
        self._delay = 0.5
        self._multiplier = 1.0
        self._power = 1.0
        self._xIncrement = 1.0
        self._starter = False
        self._threader = None

    @Property(bool)
    def starter(self):
        return self._starter

    @starter.setter
    def setStarter(self, val):
        print("New val: 0, oldVal: 1".format(val,self._starter))
        if self._starter == val:
            return

        self._starter = val
        if val:
            self.start()
        else:
            self.stop()

    @Property(float)
    def multiplier(self):
        return self._multiplier

    @multiplier.setter
    def setMultiplier(self, val):
        if self._multiplier == val:
            return
        print(val)
        self._multiplier = val

    @Property(int)
    def power(self):
        return self._power

    @power.setter
    def setPower(self, val):
        if self._power == val:
            return
        print(val)
        self._power = val

    @Property(float)
    def delay(self):
        return self._delay

    @delay.setter
    def setDelay(self, val):
        if self._delay == val:
            return
        print(val)
        self._delay = val

    @Property(float)
    def xIncrement(self):
        return self._xIncrement

    @xIncrement.setter
    def setXIncrement(self, val):
        if self._xIncrement == val:
            return
        print(val)
        self._xIncrement = val

    def generatePoint(self):
        self._currX += self._xIncrement
        self._currY = self._multiplier*(self._currX**self._power)

        return self._currX,self._currY

    def stop(self):
        self._goOn = False
        if self._threader is not None:
            while self._threader.isRunning():
                sleep(0.1)

    def start(self):
        self._goOn = True
        self._threader = Threader(core=self.core)
        self._threader.start()

    def core(self):
        while self._goOn:
            x,y = self.generatePoint()
            print([x,y])
            self.dataReady.emit(x,y)
            sleep(self._delay)

class Threader(QThread):

    def __init__(self,core,parent=None):
        QThread.__init__(self,parent)
        self._core = core
        self._goOn = False

    def run(self):
        self._core()

if __name__ == "__main__":
    os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
    app = QApplication(sys.argv)
    manager = Manager()
    engine = QQmlApplicationEngine()
    ctx = engine.rootContext()
    ctx.setContextProperty("Manager", manager)
    engine.load('main.qml')
    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())

QML 代码:

ApplicationWindow 
id: mainWindow
width:640
height: 480
title: qsTr("Simple ui")
visible: true
locale:locale

property var controlsColor: Material.DeepPurple
property var controlsAccent: Material.BlueGrey
property real x: 0.0
property int controlsElevation: 6
property int paneElevation: 4

function drawPoint(theX,theY)
    console.log(theX);
    mainLine.append(theX,theY)
    if (theX >= testXAxis.max)
        testXAxis.max = theX;
    
    if (theY >= testYAxis.max)
        testYAxis.max = theY;
    
    if (theY <= testYAxis.min)
        testYAxis.min = theY;
    


function clearLine()
    mainLine.clear();
    mainLine.append(0,0);


Pane
    id: mainPanel
    anchors.fill: parent
    //Material.theme: Material.Dark

    RowLayout
        id: mainRowLO
        anchors.fill: parent
        spacing: 15

        //Chart pane
        Pane
            id: chartPane
            Material.elevation: paneElevation
            //Material.background: Material.Grey
            Layout.fillHeight: true
            Layout.fillWidth: true
            Layout.minimumHeight: 200
            Layout.minimumWidth: 400

            ChartView 
                id: testChart
                title: "Line"
                anchors.fill: parent
                antialiasing: true
                LineSeries 
                    id: mainLine
                    name: "LineSeries"
                    axisX: ValueAxis
                        id: testXAxis
                        min: 0.0
                        max: 2.0
                    
                    axisY: ValueAxis
                        id: testYAxis
                        min: 0.0
                        max: 2.0
                    
                    XYPoint  x: 0; y: 0 
                
            
        

        Pane
            id: controlsPane
            Material.elevation: paneElevation
            //Material.background: Material.Grey
            Layout.fillHeight: true
            Layout.fillWidth: true
            Layout.minimumHeight: 200
            Layout.minimumWidth: 200
            Layout.maximumWidth: 200

            ColumnLayout
                id: controlsColumnLO
                anchors.fill: parent
                spacing: 40

                Label
                    id: powerLabel
                    text: "Exponent"
                    Layout.topMargin: 40
                    Layout.leftMargin: 10
                    Layout.rightMargin: 10
                

                SpinBox
                    id: powerNum
                    from: 0
                    value: 1
                    to: 5
                    stepSize: 1
                    width: 80
                    validator: DoubleValidator 
                        bottom: Math.min(powerNum.from, powerNum.to)
                        top:  Math.max(powerNum.from, powerNum.to)
                    
                    Material.foreground: controlsColor
                    Material.accent: controlsAccent
                    Material.elevation: controlsElevation
                    Layout.fillWidth: true
                    Layout.leftMargin: 10
                    Layout.rightMargin: 10
                    editable: true
                    onValueChanged: function()
                        Manager.power = value;
                    
                

                Label
                    id: multiplierLabel
                    text: "Multiplier"
                    Layout.fillWidth: true
                    Layout.leftMargin: 10
                    Layout.rightMargin: 10
                

                Slider
                    id: multiplierSlider
                    from: -50
                    value: 1
                    to: 50
                    Material.foreground: controlsColor
                    Material.accent: controlsAccent
                    Material.elevation: controlsElevation
                    Layout.leftMargin: 10
                    Layout.rightMargin: 10
                    Layout.fillWidth: true
                    onValueChanged: function()
                        Manager.multiplier = value;
                    
                

                Label
                    id: multValueLabel
                    text: String(multiplierSlider.value)
                    Layout.leftMargin: 10
                    Layout.rightMargin: 10
                

                Label
                    id: delayLable
                    text: "Delay[s]"
                    Layout.fillWidth: true
                    Layout.leftMargin: 10
                    Layout.rightMargin: 10
                

                Slider
                    id: delaySlider
                    from: 0.05
                    value: 0.1
                    to: 1
                    stepSize: 0.01
                    Material.foreground: controlsColor
                    Material.accent: controlsAccent
                    Material.elevation: controlsElevation
                    Layout.leftMargin: 10
                    Layout.rightMargin: 10
                    Layout.fillWidth: true
                    onValueChanged: function()
                        Manager.delay = value;
                    
                

                Label
                    id: incrementLable
                    text: "Increment"
                    Layout.fillWidth: true
                    Layout.leftMargin: 10
                    Layout.rightMargin: 10
                

                Slider
                    id: incrementSlider
                    from: 1.0
                    value: 1.0
                    to: 5.0
                    stepSize: 0.01
                    Material.foreground: controlsColor
                    Material.accent: controlsAccent
                    Material.elevation: controlsElevation
                    Layout.leftMargin: 10
                    Layout.rightMargin: 10
                    Layout.fillWidth: true
                    onValueChanged: function()
                        Manager.xIncrement = value;
                    
                

                Item 
                    // spacer item
                    id: controlsSpacer
                    Layout.fillWidth: true
                    Layout.fillHeight: true
                    Pane  anchors.fill: parent //; Material.background: Material.Light; Material.elevation: 4  // to visualize the spacer
                

                Button
                    id: startPointBtn
                    text: "START"
                    Material.foreground: controlsColor
                    Material.accent: controlsAccent
                    Material.elevation: controlsElevation
                    Layout.fillWidth: true
                    Layout.leftMargin: 10
                    Layout.rightMargin: 10
                    onClicked: function()
                        console.log(text);
                        console.log(text=="START")
                        if(text=="START")
                            Manager.starter = true;
                            Manager.dataReady.connect(drawPoint);
                            clearLine();
                            text = "STOP";
                        
                        else
                            Manager.starter = false;
                            text = "START";
                            Manager.dataReady.disconnect(drawPoint);
                        
                    
                
            
        
    


【问题讨论】:

不知道怎么写代码。这就是我问的原因。我没有找到关于这个主题的任何信息,所以我不知道如何连接来自 python 类的信号和 qml 文件上的插槽之间的连接 【参考方案1】:

最简单的解决方案是使用 Connections,但在 PySide/PySide2 的情况下,您无法获取参数,因此我将使用它们在 this answer 中指向的技巧。

如果您要发送一个点,请使用 QPoint,因为它直接转换为 QML 中的点类型。您还必须计算最大值和最小值以更新轴。

综合考虑,解决办法如下:

*.py

import os
import sys
import time
from PySide2 import QtCore, QtWidgets, QtQml

class Manager(QtCore.QObject):
    dataReady = QtCore.Signal(QtCore.QPointF, name='dataReady')

    def __init__(self, parent=None):
        super(Manager, self).__init__(parent)
        self._currX = 0
        self._currY = 0
        self._delay = 0.5
        self._multiplier = 1.0
        self._power = 1.0
        self._xIncrement = 1.0
        self._starter = False
        self._goOn = False
        self._threader = None

    @QtCore.Property(bool)
    def starter(self):
        return self._starter

    @starter.setter
    def starter(self, val):
        if self._multiplier == val:
            return
        print(val)
        if val:
            self.start()
        else:
            self.stop()
        self._starter = val

    @QtCore.Property(float)
    def multiplier(self):
        return self._multiplier

    @multiplier.setter
    def multiplier(self, val):
        if self._multiplier == val:
            return
        print(val)
        self._multiplier = val

    @QtCore.Property(int)
    def power(self):
        return self._power

    @power.setter
    def power(self, val):
        if self._power == val:
            return
        print(val)
        self._power = val

    @QtCore.Property(float)
    def delay(self):
        return self._delay

    @delay.setter
    def delay(self, val):
        if self._delay == val:
            return
        print(val)
        self._delay = val

    @QtCore.Property(float)
    def xIncrement(self):
        return self._xIncrement

    @xIncrement.setter
    def xIncrement(self, val):
        if self._xIncrement == val:
            return
        print(val)
        self._xIncrement = val

    def generatePoint(self):
        self._currX += self._xIncrement
        self._currY = self._multiplier*(self._currX**self._power)

        return self._currX,self._currY

    def stop(self):
        self._goOn = False
        if self._threader is not None:
            while self._threader.isRunning():
                time.sleep(0.1)

    def start(self):
        self._goOn = True
        self._threader = Threader(self.core, self)
        self._threader.start()

    def core(self):
        while self._goOn:
            p = QtCore.QPointF(*self.generatePoint())
            self.dataReady.emit(p)
            time.sleep(self._delay)

# -------------------------------------------------

class Threader(QtCore.QThread):
    def __init__(self,core,parent=None):
        super(Threader, self).__init__(parent)
        self._core = core

    def run(self):
        self._core()

if __name__ == "__main__":
    os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
    app = QtWidgets.QApplication(sys.argv)
    manager = Manager()
    app.aboutToQuit.connect(manager.stop)
    manager.start()
    engine = QtQml.QQmlApplicationEngine()
    ctx = engine.rootContext()
    ctx.setContextProperty("Manager", manager)
    engine.load('main.qml')
    if not engine.rootObjects():
        sys.exit(-1)

    sys.exit(app.exec_())

*.qml

import QtQuick 2.9
import QtCharts 2.2
import QtQuick.Controls 1.4
import QtQuick.Layouts 1.12
import QtQuick.Controls 2.5
import QtQuick.Controls.Material 2.12

ApplicationWindow 
    id: mainWindow
    width:640
    height: 480
    title: qsTr("Simple ui")
    visible: true
    locale:locale

    property int controlsColor: Material.DeepPurple
    property int controlsAccent: Material.BlueGrey
    property real x: 0.0
    property int controlsElevation: 6
    property int paneElevation: 4

    signal reemitted(point p)
    Component.onCompleted: Manager.dataReady.connect(mainWindow.reemitted)
    onReemitted: 
        testXAxis.max = Math.max(testXAxis.max, p.x)
        testXAxis.min = Math.min(testXAxis.min, p.x)
        testYAxis.max = Math.max(testYAxis.max, p.y)
        testYAxis.min = Math.min(testYAxis.min, p.y)
        mainLine.append(p.x, p.y)
    

    function drawPoint(xy)
        mainLine.append(xy[0],xy[1])
        if (mainWindow.x >= testXAxis.max)
            testXAxis.max = mainWindow.x;
        
        if (py >= testYAxis.max)
            testYAxis.max = py;
        
        if (py <= testYAxis.min)
            testYAxis.min = py;
        
    

    function clearLine()
        mainLine.clear();
        mainLine.append(0,0);
    

    Pane
        id: mainPanel
        anchors.fill: parent
        //Material.theme: Material.Dark

        RowLayout
            id: mainRowLO
            anchors.fill: parent
            spacing: 15

            //Chart pane
            Pane
                id: chartPane
                Material.elevation: paneElevation
                //Material.background: Material.Grey
                Layout.fillHeight: true
                Layout.fillWidth: true
                Layout.minimumHeight: 200
                Layout.minimumWidth: 400

                ChartView 
                    id: testChart
                    title: "Line"
                    anchors.fill: parent
                    antialiasing: true
                    LineSeries 
                        id: mainLine
                        name: "LineSeries"
                        axisX: ValueAxis
                            id: testXAxis
                            min: 0.0
                            max: 2.0
                        
                        axisY: ValueAxis
                            id: testYAxis
                            min: 0.0
                            max: 2.0
                        
                        XYPoint  x: 0; y: 0 
                    
                
            

            Pane
                id: controlsPane
                Material.elevation: paneElevation
                //Material.background: Material.Grey
                Layout.fillHeight: true
                Layout.fillWidth: true
                Layout.minimumHeight: 200
                Layout.minimumWidth: 200
                Layout.maximumWidth: 200

                ColumnLayout
                    id: controlsColumnLO
                    anchors.fill: parent
                    spacing: 40

                    Label
                        id: powerLabel
                        text: "Exponent"
                        Layout.topMargin: 40
                        Layout.leftMargin: 10
                        Layout.rightMargin: 10
                    

                    SpinBox
                        id: powerNum
                        from: 0
                        value: 1
                        to: 5
                        stepSize: 1
                        width: 80
                        validator: DoubleValidator 
                            bottom: Math.min(powerNum.from, powerNum.to)
                            top:  Math.max(powerNum.from, powerNum.to)
                        
                        Material.foreground: controlsColor
                        Material.accent: controlsAccent
                        Material.elevation: controlsElevation
                        Layout.fillWidth: true
                        Layout.leftMargin: 10
                        Layout.rightMargin: 10
                        editable: true
                        onValueChanged: function()
                            Manager.power = value;
                        
                    

                    Label
                        id: multiplierLabel
                        text: "Multiplier"
                        Layout.fillWidth: true
                        Layout.leftMargin: 10
                        Layout.rightMargin: 10
                    

                    Slider
                        id: multiplierSlider
                        from: -50
                        value: 1
                        to: 50
                        Material.foreground: controlsColor
                        Material.accent: controlsAccent
                        Material.elevation: controlsElevation
                        Layout.leftMargin: 10
                        Layout.rightMargin: 10
                        Layout.fillWidth: true
                        onValueChanged: function()
                            Manager.multiplier = value;
                        
                    

                    Label
                        id: multValueLabel
                        text: String(multiplierSlider.value)
                        Layout.leftMargin: 10
                        Layout.rightMargin: 10
                    

                    Label
                        id: delayLable
                        text: "Delay[s]"
                        Layout.fillWidth: true
                        Layout.leftMargin: 10
                        Layout.rightMargin: 10
                    

                    Slider
                        id: delaySlider
                        from: 0.05
                        value: 0.1
                        to: 1
                        stepSize: 0.01
                        Material.foreground: controlsColor
                        Material.accent: controlsAccent
                        Material.elevation: controlsElevation
                        Layout.leftMargin: 10
                        Layout.rightMargin: 10
                        Layout.fillWidth: true
                        onValueChanged: function()
                            Manager.delay = value;
                        
                    

                    Label
                        id: incrementLable
                        text: "Increment"
                        Layout.fillWidth: true
                        Layout.leftMargin: 10
                        Layout.rightMargin: 10
                    

                    Slider
                        id: incrementSlider
                        from: 1.0
                        value: 1.0
                        to: 5.0
                        stepSize: 0.01
                        Material.foreground: controlsColor
                        Material.accent: controlsAccent
                        Material.elevation: controlsElevation
                        Layout.leftMargin: 10
                        Layout.rightMargin: 10
                        Layout.fillWidth: true
                        onValueChanged: function()
                            Manager.xIncrement = value;
                        
                    

                    Item 
                        // spacer item
                        id: controlsSpacer
                        Layout.fillWidth: true
                        Layout.fillHeight: true
                        Pane  anchors.fill: parent //; Material.background: Material.Light; Material.elevation: 4  // to visualize the spacer
                    

                    Button
                        id: startPointBtn
                        text: "START"
                        Material.foreground: controlsColor
                        Material.accent: controlsAccent
                        Material.elevation: controlsElevation
                        Layout.fillWidth: true
                        Layout.leftMargin: 10
                        Layout.rightMargin: 10
                        onClicked: function()
                            console.log(text);
                            if(text=="START")
                                clearLine();
                                Manager.starter = true;
                                text = "STOP";
                            
                            else
                                Manager.starter = false;
                                text = "START";
                            
                        
                    
                
            
        
    

【讨论】:

经过一些更新(Qt 或 Fedora 打包,不确定),每当我尝试从 QML 设置属性时,我只会得到main.qml:216: TypeError: 'NoneType' object is not callable。有谁知道为什么会这样,该怎么办? @dreua 你是如何安装 Qt 和 PySide2 的?这两个库的版本是什么? sudo dnf install qt-creator python3-pyside2 (刚刚在新的 Fedora 33 VM 中尝试过,不幸的是同样的问题。但是,我确信这在以前有效 - 也许是 F32)两个版本 5.15.2,Python 3.9 .0. @dreua 可能是 bug,尝试创建一个 virtualenv 并使用 pip 安装 PySide2 并验证它不起作用。 @dreua 我刚刚测试了最新版本的 PySide2 并重现了该问题。也许我改变了 PySide2 内部的一些东西,或者它可能是一个错误。当我有时间时,我会分析它并告诉你我的结论

以上是关于使用 PySide2 将 python 信号连接到 QML ui 插槽的主要内容,如果未能解决你的问题,请参考以下文章

Pyside2 信号槽 2d 数组签名定义,等效于列表列表

如何将信号连接到不同线程中的插槽

尝试使用信号将 QTcpServer 连接到 GUI

PyQt 5.6:连接到 DBus 信号挂起

QT:使用移动语义将信号连接到插槽

将 qml 信号连接到 Qt