pythonpyqt6

GUI closing unexpectedly


I'm using PyQT6, PyCharm and Python 3.11.

Very briefly, I'm trying to convert (mentally) from VBA to Python.

I made a GUI in pyQT6, let's call it test.ui. It has a number of buttons and a table. I convert this to a Python file using the pyuic6 utility. I now have a Test.py file in the same folder. All good so far.

I add

self.mybutton.clicked.connect("buttonpushed")  

to the Test.py script and tag on a

def buttonpushed(self):
  print("Button Pushed") 

This works great and as expected. My intention is to have a form with many buttons, so rather than including the def in the Test.py file, which will change every time I run pyuic6, I move it across to another file called ExtraBits.py and include this in to Test.py with the line

import ExtraBits

It still works as expected and print the words correctly.

Now my problem: I want this button to hide a table on my GUI. So I added

self.mytbl.hide() 

to the buttonpushed procedure in Test.py and it works. If I add this to the procedure when it's in the ExtraBits.py, I still get the printed words, but the table doesn't hide and the GUI closes. I've convinced myself that it is due to me wrongly referencing the mytable object in the imported ExtraBits.py file but not sure how to fix it.

I've tried every combination I can think of including the name of the main window object etc. I don't get any errors, just the hide() doesn't hide and the GUI bombs out, but only if in the ExtraBits.py file.

Re made the file in to minimal.py and ExtraBits.py to try and highlight my issue

from PyQt6 import QtCore, QtGui, QtWidgets
import ExtraBits

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(800, 600)
        self.centralwidget = QtWidgets.QWidget(parent=MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.tableWidget = QtWidgets.QTableWidget(parent=self.centralwidget)
        self.tableWidget.setGeometry(QtCore.QRect(30, 20, 256, 192))
        self.tableWidget.setObjectName("tableWidget")
        self.tableWidget.setColumnCount(0)
        self.tableWidget.setRowCount(0)
        self.pushButton = QtWidgets.QPushButton(parent=self.centralwidget)
        self.pushButton.setGeometry(QtCore.QRect(40, 260, 75, 23))
        self.pushButton.setObjectName("pushButton")
        self.pushButton.clicked.connect(ExtraBits.PushButton)
        MainWindow.setCentralWidget(self.centralwidget)
        self.statusbar = QtWidgets.QStatusBar(parent=MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
    
        self.retranslateUi(MainWindow)
            QtCore.QMetaObject.connectSlotsByName(MainWindow)
    
        def retranslateUi(self, MainWindow):
            _translate = QtCore.QCoreApplication.translate
            MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
            self.pushButton.setText(_translate("MainWindow", "PushButton"))
    
    
    if __name__ == "__main__":
        import sys
        app = QtWidgets.QApplication(sys.argv)
        MainWindow = QtWidgets.QMainWindow()
        ui = Ui_MainWindow()
        ui.setupUi(MainWindow)
        MainWindow.show()
        sys.exit(app.exec())

and ExtraBits.py is

def PushButton(self):
    print("Button pressed")
    self.tableWidget.hide()

Solution

  • While this is a typical case that would make it a duplicate of QtDesigner changes will be lost after redesign User Interface, it may deserve a related answer.

    The problem comes from the fact that whatever is done in ExtraBits has absolutely no reference of the context from which it is called.

    Specifically, your attempt to use self is misguided:

    def PushButton(self):
        print("Button pressed")
        self.tableWidget.hide()
    

    This is for two reasons:

    The above happens because PushButton() is a standard function, not an instance method, so there is no predefined self argument for it indicating the instance: it's a function, there's no instance.

    Consider this:

    def someFunction(self, *args):
        print('hello', self)
    
    class Test(object):
        def someFunction(self, *args):
            print('hello', self)
    
    someFunction('world')
    Test().someFunction('world')
    

    While the functions are called with the same argument, the standard function will actually print hello world, while the second, which is an instance method, will print hello followed by the memory address representation of the Test() instance (something like <__main__.Test object at 0xdeadbeef>).

    When using signals with arguments, then, any positional argument that the function accepts is set with the signal argument values.
    Change your function to the following:

    def PushButton(self):
        print(self)
    

    And it will print False. This is the final problem with your code: since self is a boolean, it obviously has no tableWidget attribute. This raises an AttributeError exception that is not managed, therefore causing the program to crash.

    This is also one of the main reasons for which programs should always be tested outside of the IDE (as they are sometimes incapable of providing the full traceback) and in a context that shows debugging output: if you had run your original code in a terminal or prompt, you would have seen that exception immediately.

    So, how to solve that?

    One option could be to use a lambda and "send" the instance as the function argument:

    self.pushButton.clicked.connect(lambda: ExtraBits.PushButton(self))
    

    While technically valid, though, this approach presents important issues; among them:

    It may be acceptable for a single case, but as soon as the program increases in complexity, all this would make code unnecessarily complex and difficult to read, also causing more problems while debugging.

    In reality, all this can be easily avoided as soon as the proper approach is used, which is to never edit pyuic files, but instead follow the official guidelines about using Designer: a real, separated main script should be used instead, with the pyuic classes eventually imported into it.

    Use the following code as the actual main script for your program, and rebuild the UI file with pyuic (in this case, I named it TestUi.py, which is what is used for the second import statement):

    from PyQt6.QtWidgets import *
    from TestUi import Ui_MainWindow
    
    class MainWindow(QMainWindow, Ui_MainWindow):
        def __init__(self):
            super().__init__()
            self.setupUi(self)
    
            self.pushButton.clicked.connect(self.buttonClicked)
    
        def buttonClicked(self):
            self.tableWidget.hide()
    
    
    if __name__ == '__main__':
        import sys
        app = QApplication(sys.argv)
    
        window = MainWindow()
        window.show()
    
        sys.exit(app.exec())
    

    I strongly suggest you to take your time to do some research about fundamental OOP aspects, including classes, instances and how their members and methods work.

    Some reliable PyQt tutorials can be found in the official Python wiki, and valid Python resource websites like realpython.com and pythonguis.com.