Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

PySide2: Best practice for using modal dialog loaded from a .ui file



  • Hi All,

    I was having a difficult time getting a simple modal dialog to work in PySide2 when loading the dialog from a .ui file and so I am writing this post for some advice/feedback on my solution.

    The context is that I have a main window, which is itself a single dialog widget and I wanted to open the modal dialog after the user clicks a button in the main dialog. Simple, right? I originally put the code for launching the sub dialog in the slot connected to the pressed signal of the button in the main dialog, but lots of things went wrong, like multiple copies of the sub dialog appearing on screen -- it seemed like the slot was getting called twice somehow, but if I commented out just the code to open the subdialog, it was not getting called twice. Weird.

    I eventually figured out that one has to instantiate the sub dialog object OUTSIDE the slot that tries to use it. Do not create the dialog object from the .ui file inside a slot and then try to use it immediately in that slot. I don't recall if this is an issue with Qt in C++. Perhaps it is and I forgot about this "rule."

    So now I am creating the instance of the sub dialog class as a member variable in the init of of the main dialog class, then using the member variable in the slot called then the user presses a button in the main dialog and things are working OK now. If this is what one needs to do, then I think It would be very good to note this clearly in the online docs about QDialog and QUiLoader.

    As for best practices, I find it awkward (see code below) to have to manage the object tree that comes out of QUiLoader as a separate member variable (self.window or self.ui) inside an object that is already a QDialog. Is there anyway to get QUiLoader to load the UI directly into an object which is a subclass of QDialog, or to create a QDialog from a .ui file?

    Here is a snippet of my code for creating the sub dialog class:

    class USlideIDsListDialog(QDialog):
        def __init__(self, parent=None):
            super(USlideIDsListDialog, self).__init__(parent)
            loader = QUiLoader()
            uifile = QFile(os.path.abspath("slide_list_dialog.ui"))
            if uifile.open(QFile.ReadOnly):
                self.window = loader.load(uifile, parent)
                uifile.close()
                self.window.doneButton.pressed.connect(self.do_done)
                self.window.cancelButton.pressed.connect(self.do_cancel)
    # more window initialization here...
    
        def show(self):
            self.window.show()
    
        def exec_(self):
            self.window.exec_()
    
        def do_cancel(self):
          # do nothing, no changes to settings
            self.window.close()
            self.reject()
    
        def do_done(self):
            # code to save some stuff to the settings goes here...
            self.window.close()
            self.accept()
    

    Then, in the script for the main dialog window I have this slot which is called when the user presses a button in the main dialog (to open the sub dialog window):

        @Slot()
        def do_slide_list_dialog(self):
            self.slide_list_dialog.show()
            self.slide_list_dialog.exec_()
            print(self.slide_list_dialog.result())
    

    Note: the .ui file has a QDialog object at its root and the QDialog is set in creator as Application Modal.

    Is there a better way to do this in PySide2?

    Thank you for your feedback,
    Doug


  • Banned

    Well depending on who you speak to "best" practice can have extremely different meanings. However quality best practices state that you should try to keep your code as simple and straight forward as possible without using any "black-box untouchable code" as that only eventually causes issues. That being said one ought not use UI files UNLESS you need that XML code it produces (aka do not add extra complexity if its not necessary) which most of the time is not the case. Further if you do not use a UI (aka black-box-untouchable-code) then you can do all sorts of Python-Qt magic -- here is a Dialog box thing like you demonstrated in your code snippet but render in layers to demonstrate what one could possibly do and this is only scratching the surface. I have helped others create stackable center panels that both were sequential in nature as well as dynamic. Further I have helped others create modular windows that you could dynamically pick from based on user selected criterion -- and again this is just a few examples of what you can easily do and easily interact with if you use straight Python-Qt and leave those annoying headache riddled UIs behind and embrace Python-Qt fully. Note making a Python-Qt GUI is as simple (or simpler) and as fast (or faster) as creating one of those UI files but you can do so much more with it when you are done and its extremely much easier to work with than a UI

    try:
        from PyQt5.QtCore    import pyqtSlot as Slot
        from PyQt5.QtWidgets import QApplication, QMainWindow, QHBoxLayout, QVBoxLayout
        from PyQt5.QtWidgets import QStyleFactory, QDialog, QWidget, QPushButton
    
    except ImportError:
        from PySide2.QtCore    import Slot
        from PySide2.QtWidgets import QApplication, QMainWindow, QHBoxLayout, QVBoxLayout
        from PySide2.QtWidgets import QStyleFactory, QDialog, QWidget, QPushButton
    
    
    class DiagDsply(QWidget):
        def __init__(self, parent):
            QWidget.__init__(self)
            self.Parent = parent
            
            self.btnDone = QPushButton('Done')
            self.btnDone.clicked.connect(self.DoDone)
    
            self.btnCancel = QPushButton('Cancel')
            self.btnCancel.clicked.connect(self.DoCancel)
            
            HBox = QHBoxLayout()
            HBox.addWidget(self.btnDone)
            HBox.addWidget(self.btnCancel)
            HBox.addStretch(1)
    
            self.setLayout(HBox)
    
        @Slot()
        def DoDone(self):
            self.Parent.DoDone()
        
        @Slot()
        def DoCancel(self):
            self.Parent.DoCancel()
    
    class USlideIDsListDialog(QDialog):
      # If your not going to use parent do not include but if you include then use it
        def __init__(self, parent):
          # One should not use super( ) in Python as it introduces 4 known issues that
          # must be handled properly. Further there were still actual bugs within the
          # usage of super( ) when used in Python. Yes while super( ) works fine  within
          # C++ it does not work as seamlessly within Python due to the major 
          # differences between these 2 languages. Next the reason it was created was 
          # to handle a rather rare issue and unless you are doing some complicated 
          # inheritance you will most likely never run into this extremely rare issue
          # However the 4 major issues that get included by using super( ) you are much 
          # more likely to occur than that rare issue its meant to solve. Of course using
          # the basic explicit method, as follows, does not cause these issues and is as
          # just as simple as using `super( )` further you do not actually gain anything
          # useful by using `super( )` in Python that could not be done in a much safer
          # manner.
            QDialog.__init__(self)
    
            self.Parent = parent
            Left=700; Topp=300; Width=300; Hight=100
            self.setGeometry(Left, Topp, Width, Hight)
            self.setWindowTitle('Dialog')
    
            self.DsplydDiag = DiagDsply(self)
    
            HBox = QHBoxLayout()
            HBox.addWidget(self.DsplydDiag)
            
            self.setLayout(HBox)
    
        def DoDone(self):
          # Code to save some stuff to the settings goes here...
            self.Parent.Results('Accept')
            self.close()
    
        def DoCancel(self):
          # Do nothing, no changes to settings
            self.Parent.Results('Reject')
            self.close()
    
    
    class CenterPanel(QWidget):
        def __init__(self, parent):
            QWidget.__init__(self)
            self.Parent = parent
    
            self.btnDsplyr = QPushButton('Display It')
            self.btnDsplyr.clicked.connect(self.DoSlideListDialog)
     
            HBox = QHBoxLayout()
            HBox.addStretch(1)
            HBox.addWidget(self.btnDsplyr)
            HBox.addStretch(1)
    
            VBox = QVBoxLayout()
            VBox.addStretch(1)
            VBox.addLayout(HBox)
            VBox.addStretch(1)
    
            self.setLayout(VBox)
    
            self.SlideListDialog = USlideIDsListDialog(self)
            
        @Slot()
        def DoSlideListDialog(self):
            TxtMsg = 'Get Results'
            self.Parent.SetStatus(TxtMsg)
    
            self.SlideListDialog.show()
            self.SlideListDialog.exec_()
    
        def Results(self, Reslt):
            TxtMsg = 'Dialog Results :' + str(Reslt)
            self.Parent.SetStatus(TxtMsg)
    
    
    class MainDisply(QMainWindow):
        def __init__(self):
            QMainWindow.__init__(self)
            Top=300; Left=700; Width=300; Hight=100
            self.setGeometry(Left, Top, Width, Hight)
            self.setWindowTitle('Main Display Area')
            self.setStyle(QStyleFactory.create('Cleanlooks'))
    
            self.CenterPane = CenterPanel(self)
            self.setCentralWidget(self.CenterPane)
    
            self.StatBar = self.statusBar()
            self.SetStatus('Ready')
    
        def SetStatus(self, StatusMsg):
            self.StatBar.showMessage(StatusMsg)
    
    
    if __name__ == "__main__":
        MainEventHandler = QApplication([])
    
        MainApplication = MainDisply()
        MainApplication.show()
        
        MainEventHandler.exec()
    


  • @Doug-Wood
    I'm struggling to understand the situation you are in.

    You are not supposed to have create a "holding" QDialog which then has a self.window member which is the actual dialog, if that is what you are saying.

    but lots of things went wrong, like multiple copies of the sub dialog appearing on screen -- it seemed like the slot was getting called twice somehow, but if I commented out just the code to open the subdialog, it was not getting called twice. Weird.

    I eventually figured out that one has to instantiate the sub dialog object OUTSIDE the slot that tries to use it. Do not create the dialog object from the .ui file inside a slot and then try to use it immediately in that slot. I don't recall if this is an issue with Qt in C++. Perhaps it is and I forgot about this "rule."

    Not sure, you would have to explain. But in any case I'm not sure I'd want to be creating a dialog out of QUiLoader.load(ui_file) each time I'm about to show it, I'd probably create it once and then re-use it.

    I don't do things via QUiLoader, I prefer having pyside2-uic generate a Python class. I don't know if you have looked at that, or if you wish to use QUiLoader.


Log in to reply