I am trying to create a desktop app using Python and Qt for the first time. Since this is my first time creating a gui based application, I hope someone experienced can guide me in the right direction on some of the questions I have.
Background: I am trying to create a desktop app using Python and Qt. The Python logic is used to interface with hardware (sensors and motors) to control a process. The front end gui should allow user to monitor the process as well as change process parameters as it is occurring. Previously I have used LabVIEW to achieve my goals but now I would like to migrate to Python. However, I found gui development in Python is not as easy as LabVIEW. After some research I have found QML and Qt Quick based gui would be easiest as opposed to the typical widget based gui using Qt (pyqt/pyside) or wxPython.
Problem: I am having difficulty trying to understand how I can get two binding between my process variable and the frontend gui. These process variables can be input/output for various hardware in the process such as sensors and motors. I read this tutorial which uses properties based approach to get two-way binding. Another approach for data binding is also explained here. However, it seems one would have to write a lot of code (signal, slot, setter, getter) for each variable in the process. When the backend logic can have tens or hundereds of process parameters that need to be displayed and modified in the front end gui, the code can get very complex.
I wrote a example program which allows user to calculate area from a given input length and width. All these input/ouput params are updated on the gui. Although this example may not capture the true complexities of the backend logic in controlling some hardware and parallel processes, I hope it can help answer some of the questions I have.
Questions:
- I created a class called twoWayBindedParam which would minimize the code I would have write for signal, slot, setter, getter for each type of process parameter. Is this the best approach to achieve two way binding between backend logic parameters and the front end gui? Is the there something can be improved?
- In the twoWayBindedParam class, I defined properties
qml_prop_*
, which would be supplied to the front end gui. However, the problem I am having is that I must specify the type of signal/slot it is emitting (ex: int, float, str, etc...). I can NOT just simply specify it as 'object' type. In addition I can not refer to theself.typeOfParam
when creating this property. The documentation on the QtCore Property module is limited. How can I create a property can be interfaced with the front end QML code where I don't have to specify exactly what type of value the signal slot is being created (ex: int, float, str, etc...). - In the main() function, I have to inject the QQmlApplicationEngine object to my myBackendLogic object instance. Without this I can't do rootContext().setContextProperty(). Is there a way I can get the current running instance of the QQmlApplicationEngine() in the myBackendLogic class WITHOUT it having to be supplied as instantiating parameter when creating the myBackendLogic object?
- How can I get the QML front end class a function that is NOT part of a class? For example, in the example code below I want run the function doSomething() when Calculate button is pressed. However, I am unsure how to call this doSomething() function because rootContext().setContextProperty() requires the name of a class object.
- Is there a suggested or simple approach to the backend logic where I can load tens/hundreds of per-configured parameters stored in some file (ex: json, xml, csv) when the gui app is launched.
- My current gui works as intended but when I drag the gui window to a second monitor, all the controls become frozen. I need to draw the window back to main monitor when it becomes unfrozen.
I hope those who answer my questions are mindful to use Python instead of C++ for example codes.
main.py
import sys
import os
import random
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
from PySide2.QtCore import QObject, Signal, Slot, Property
class twoWayBindedParam(QObject):
# A class to represent a variable that is two-binded in the Python logic and in QML gui
def __init__(self, value):
QObject.__init__(self)
self.value = value
self.typeOfParam = type(self.value) # Determine if its a str, int, float, etc...
@Signal
def valueChanged(self):
pass
@Slot(int)
def set(self, value):
self.value = value
self.valueChanged.emit()
def get(self):
return self.value
# Problem: I must create different properties for each type
# In the QML gui, I must use the correct type of property
# The problem is when creating the Property() object,
# I can NOT refer to the self.typeOfParam
# Chagning the type to 'object' doesn't work: https://stackoverflow.com/a/5186587/4988010
qml_prop_int = Property(int, get, set, notify=valueChanged)
qml_prop_float = Property(float, get, set, notify=valueChanged)
qml_prop_bool = Property(bool, get, set, notify=valueChanged)
class myBackendLogic(QObject):
def __init__(self, app_engine):
QObject.__init__(self)
# The app_engine object is needed to use the function rootContext().setContextProperty()
# Is there a way to get the current instance of the app_engine that is created in the main
# without it having to be passed as a paramter to the myBackendLogic() object?
self.eng = app_engine
self.init_default_params()
def init_default_params(self):
random.seed(23)
length = random.randint(0,100)
width = random.randint(0,100)
area = self.whatIsArea(length,width)
self.length_param = twoWayBindedParam(length)
self.eng.rootContext().setContextProperty("length_param", self.length_param)
self.width_param = twoWayBindedParam(width)
self.eng.rootContext().setContextProperty("width_param", self.width_param)
self.area_param = twoWayBindedParam(area)
self.eng.rootContext().setContextProperty("area_param", self.area_param)
self.continuous_calc_param = twoWayBindedParam(False)
self.eng.rootContext().setContextProperty("continuous_calc_param", self.continuous_calc_param)
def whatIsArea(self, l,w):
result = float(l*w) + random.random() # Add some noise
return result
@Slot()
def calculate_area_param(self):
area = self.whatIsArea(self.length_param.get(),self.width_param.get())
self.area_param.set(area)
def doSomething():
print('Do something')
def main():
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
mylogic = myBackendLogic(engine)
engine.rootContext().setContextProperty("mylogic", mylogic)
engine.load(os.path.join(os.path.dirname(__file__), "main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())
if __name__ == "__main__":
main()
main.qml
import QtQuick 2.13
import QtQuick.Window 2.13
import QtQuick.Controls 2.2
Window {
title: qsTr("Hello World")
width: 640
height: 480
visible: true
Column {
id: column
x: 131
y: 63
width: 72
height: 263
TextInput {
id: textInput_length
width: 80
height: 20
text: length_param.qml_prop_int
font.pixelSize: 12
}
Slider {
id: slider_length
to: 100
orientation: Qt.Vertical
value: length_param.qml_prop_int
onValueChanged: {
length_param.set(value)
if (continuous_switch.checked) { mylogic.calculate_area_param() }
}
}
}
Column {
id: column1
x: 249
y: 63
width: 72
height: 263
TextInput {
id: textInput_width
width: 80
height: 20
text: width_param.qml_prop_int
font.pixelSize: 12
}
Slider {
id: slider_width
to: 100
value: width_param.qml_prop_int
orientation: Qt.Vertical
onValueChanged: {
width_param.set(value)
if (continuous_switch.checked) { mylogic.calculate_area_param() }
}
}
}
Row {
id: row
x: 110
y: 332
width: 274
height: 53
Slider {
id: slider_area
to: 10000
value: area_param.qml_prop_float
}
Label {
id: label_area
text: area_param.qml_prop_float
}
}
Switch {
id: continuous_switch
x: 343
y: 149
text: qsTr("Continuous calculate")
checked: continuous_calc_param.qml_prop_bool
}
Button {
id: button
x: 383
y: 205
text: qsTr("Calculate")
onClicked: {
mylogic.calculate_area_param()
}
}
Label {
id: label
x: 131
y: 23
text: qsTr("Length")
font.pointSize: 12
}
Label {
id: label1
x: 249
y: 23
text: qsTr("Width")
font.pointSize: 12
}
Label {
id: label3
x: 196
y: 377
text: qsTr("Area")
font.pointSize: 12
}
}