Home > front end >  PyQt5 very simple button input dialog
PyQt5 very simple button input dialog

Time:03-01

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:

window on startup

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)
  1. I used a global variable, as I didn't find a way to return anything from the QApplication.

  2. No matter which button I press, the output is "Development", the last option that was entered?

CodePudding user response:

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:

  1. use exec(), which blocks execution (but not the event loop) until the dialog returns;
  2. 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;
  • Related