I thought I’ll try pyqt(5) for a change and wanted to create a simple dialog on startup of a script that let’s the user choose one of three options.
My goal is a popup like this (on startup of a script) that will close on pushing one of the buttons and returns the value of the button that was pressed:
And this is how I tried to implement it:
from PyQt5.QtWidgets import QApplication, QDialog, QPushButton, QDialogButtonBox input_db_connection="None" class MyDialog(QDialog): def __init__(self): super().__init__() self.setWindowTitle("Choose Database connection") self.buttonBox=QDialogButtonBox(self) for option in ["Production Read", "Production Read&Write", "Development"]: b = QPushButton(option) b.clicked.connect(lambda x: return_and_close(option)) self.buttonBox.addButton(b,0) del b def return_and_close(value): global input_db_connection print(value) input_db_connection=value self.close() app = QApplication([]) dg = MyDialog() dg.show() app.exec() print(input_db_connection)
I used a global variable, as I didn’t find a way to return anything from the QApplication.
No matter which button I press, the output is “Development”, the last option that was entered?
Advertisement
Answer
The main problem is in the lambda scope: what is inside lambda:
is evaluated only when it is executed.
When the button is clicked, the for
loop has already completed, and at that point option
will be the last value assigned in the loop.
The solution is to “force” keyword arguments so that there is a persistent reference in the inner scope. Note that the clicked
signal of Qt buttons has a checked
argument, and you must consider that in the lambda positional arguments.
Then, instead of creating a global variable (which is almost always a bad choice), you should properly use the functions of QDialog:
- use
exec()
, which blocks execution (but not the event loop) until the dialog returns; - use
done()
to set a valid result for the dialog and closes it by returning that result;
from PyQt5.QtWidgets import * class MyDialog(QDialog): def __init__(self): super().__init__() self.setWindowTitle("Choose Database connection") layout = QVBoxLayout(self) self.buttonBox = QDialogButtonBox(self) layout.addWidget(self.buttonBox) states = [ (0, "Production Read"), (1, "Production Read&Write"), (2, "Development"), ] for value, option in states: b = QPushButton(option) b.clicked.connect(lambda _, value=value: self.done(value)) self.buttonBox.addButton(b, 0) app = QApplication([]) dg = MyDialog() print(dg.exec())
Further notes:
- always use layout managers, creating child widgets is insufficient, and trying to set manual geometries is still not enough unless you have deep knowledge and profound experience about Qt events, size hints and policies;
- do not call
del
unnecessarily (in this case it’s also completely pointless, as it only deletes the python reference, and being it a local variable that would be overwritten at each for cycle and garbage collected when__init__
returns, it’s useless); - avoid local functions whenever possible, and always prefer instance methods instead;