使用 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 插槽的主要内容,如果未能解决你的问题,请参考以下文章