0

I'm having a strange issue that I can't figure out.

When I click a button, I want it to update it's own text, but in some cases it's as if it's being blocked! Excuse my rubbish coding - my first time trying to make a GUI for something. Anyways, this is my buttons function

def ConvertToSTL(self):
    if DEBUG:
        print("You Clicked on ConvertToSTL")
        
    self.btnConvertToSTL.setText("Processing...")
    self.btnConvertToSTL.setEnabled(False)      
    
    if not (FILE_OUTLINE and FILE_OTHER):
        print("You MUST select BOTH files!")
        self.btnConvertToSTL.setText(CONVERT_BTN_TEXT)
        self.btnConvertToSTL.setEnabled(True)   
    else:
        if DEBUG:
            print("Outline: " + FILE_OUTLINE)
            if self.inputPCB.isChecked():
                print("Mask Top: " + FILE_OTHER)
            elif self.inputSolderStencil.isChecked():
                print("Paste Mask Top: " + FILE_OTHER)
                
        # Processing Files!
        outline_file = open(FILE_OUTLINE, "r")
        other_file = open(FILE_OTHER, "r")

        outline = gerber.loads(outline_file.read())
        other = gerber.loads(other_file.read())

        # outline=None
        
        if outline and other:
            output = process_gerber(
                outline,
                other,
                self.inputThickness.value(),
                self.inputIncludeLedge.isChecked,
                self.inputHeight.value(),
                self.inputGap.value(),
                self.inputIncreaseHoleSize.value(),
                self.inputReplaceRegions.isChecked(),
                self.inputFlip.isChecked(),
            )
            
            file_id = randint(1000000000, 9999999999)
            scad_filename = "./gerbertostl-{}.scad".format(file_id)
            stl_filename = "./gerbertostl-{}.stl".format(file_id)
            
            with open(scad_filename, "w") as scad_file:
                scad_file.write(output)
                
            p = subprocess.Popen(
                [
                    SCAD_BINARY,
                    "-o",
                    stl_filename,
                    scad_filename,
                ]
            )
            p.wait()

            if p.returncode:
                print("Failed to create an STL file from inputs")
            else:
                with open(stl_filename, "r") as stl_file:
                    stl_data = stl_file.read()
                os.remove(stl_filename)
                
            # Clean up temporary files
            os.remove(scad_filename)
            
            self.btnConvertToSTL.setText("Saving file...")

            saveFilename = QFileDialog.getSaveFileName(None, "Save STL", stl_filename, "STL File (*.stl)")[0]
            if DEBUG:
                print("File saved to: " + saveFilename)
            # needs a handler if user clicks cancel!
            saveFile = open(saveFilename,'w')
            saveFile.write(stl_data)
            saveFile.close()
            
            self.btnConvertToSTL.setEnabled(True)
            self.btnConvertToSTL.setText(CONVERT_BTN_TEXT)

Now, if outline or other is FALSE for any reason - to test I manually added set outline=None then my Button DOES correctly set it's text to read "Processing...". The problem however is that if outline and other are both TRUE so the functions progresses, the button text does NOT get set to "Processing".

Instead, the button text is not changed until it reaches self.btnConvertToSTL.setText("Saving file...") which as expected sets the text correctly. Then, once the file is saved, the button again correctly updates again to the variable CONVERT_BTN_TEXT

So my question is, why on earth does the initial "Processing..." text NOT get set correctly? I don't understand

Edit: minimal reproducible example. In this case, the button text never changes to "Processing..." for me. Obviously, Requires PYQT5

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'tmp.ui'
#
# Created by: PyQt5 UI code generator 5.15.7
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets
import time


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(800, 600)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setGeometry(QtCore.QRect(160, 160, 421, 191))
        self.pushButton.setObjectName("pushButton")
        self.pushButton.clicked.connect(self.PushButtonClicked)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 24))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(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"))
        
    def PushButtonClicked(self):
        print("Button Clicked")
        self.pushButton.setText("Processing")
        self.pushButton.setEnabled(False)
        
        if True and True:
            print("Sleep")
            time.sleep(5)
            print("Continue")
            self.pushButton.setText("DONE")
            self.pushButton.setEnabled(True)
            
            


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_())

The button will correctly say "DONE" after 5 seconds, but it never changes to "Processing"

IAmOrion
  • 37
  • 1
  • 10
  • @Alexander - I just happened to stumble across https://stackoverflow.com/questions/29551438/why-is-if-not-a-and-b-faster-than-if-not-a-or-not-b whilst I was double checking how to use != in python so went with the 'faster' ```if not (a and b)```. That part functions as expected. Really it was purely because when I read the code back my brain processes that easier but I believe both have the same result – IAmOrion Aug 12 '22 at 00:39
  • please create an [example] – Alexander Aug 12 '22 at 00:45
  • @Alexander - I technically already test that - the problem Is when I minimalize it, it functions as expected and it's not until it's basically what you see that it causes a problem! I will try build a minimal function again line by line so to speak until I can re-produce the problem – IAmOrion Aug 12 '22 at 00:48
  • @Alexander - Minimal reproducible example added (Assuming I've not done something wrong in that code) – IAmOrion Aug 12 '22 at 00:56
  • 1
    A [example] is a minimal amount of code that produces the same issue you are currently experiencing, that other people can simply copy and paste and run it themselves to try to determine the cause of the issue. – Alexander Aug 12 '22 at 01:04
  • @Alexander I know, I've added minimal code that doesn't change the button text for me – IAmOrion Aug 12 '22 at 01:05
  • But nobody other than you would be able to copy and paste that code block to be able to "reproduce" the same issue. Anybody else trying to run that would raise Exceptions immediately – Alexander Aug 12 '22 at 01:08
  • @Alexander updated Q with full min example – IAmOrion Aug 12 '22 at 01:19
  • Now I see the problem. – Alexander Aug 12 '22 at 01:31

1 Answers1

1

The issue is because the code you are invoking once the button is pressed is locking the GUI, so when it is finally released back to the main event loop it processes all of the changes at once which is why you only see the final message displayed in the button description. That is also why you can't move around the window or interact with it in any other way while the method is processing.

The solution to this is to simply not write code that freezes the GUI. This means either breaking up the process or running it in a separate thread, and using Qt's signals and slots API.

For example:

from PyQt5 import QtCore, QtWidgets
import time

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setObjectName("MainWindow")
        self.resize(800, 600)
        self.centralwidget = QtWidgets.QWidget(self)
        self.centralwidget.setObjectName("centralwidget")
        self.pushButton = QtWidgets.QPushButton("Button", self.centralwidget)
        self.pushButton.setGeometry(QtCore.QRect(160, 160, 421, 191))
        self.pushButton.setObjectName("pushButton")
        self.pushButton.clicked.connect(self.PushButtonClicked)
        self.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(self)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 24))
        self.menubar.setObjectName("menubar")
        self.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(self)
        self.statusbar.setObjectName("statusbar")
        self.setStatusBar(self.statusbar)

    def PushButtonClicked(self):
        self.thread = Thread(self)
        self.thread.started.connect(self.show_processing)
        self.thread.finished.connect(self.show_done)
        self.thread.start()

    def show_done(self):
        self.pushButton.setText("DONE")
        self.pushButton.setEnabled(True)

    def show_processing(self):
        self.pushButton.setText("Processing")
        self.pushButton.setEnabled(False)

class Thread(QtCore.QThread):

    def run(self):
        print("Button Clicked")
        if True and True:
            print("Sleep")
            time.sleep(5)
            print("Continue")

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

So for your specific use case, it might look something like this:

...
...
   ...
   ...
   ...

   def convert_to_STL(self):
        if DEBUG:
            print("You Clicked on ConvertToSTL")
        self.btnConvertToSTL.setText("Processing...")
        self.btnConvertToSTL.setEnabled(False)      
        if not (FILE_OUTLINE and FILE_OTHER):
            print("You MUST select BOTH files!")
            self.btnConvertToSTL.setText(CONVERT_BTN_TEXT)
            self.btnConvertToSTL.setEnabled(True)   
        else:
            self.thread = Thread(self)
            self.thread.finished.connect(self.show_done)
            self.thread.start()

    def show_done(self):
        self.btnConvertToSTL.setEnabled(True)
        self.btnConvertToSTL.setText(CONVERT_BTN_TEXT)

class Thread(QtCore.QThread):
    def __init__(self, parent):
        self.window = parent

    def run(self):
        if DEBUG:
            print("Outline: " + FILE_OUTLINE)
            if self.window.inputPCB.isChecked():
                print("Mask Top: " + FILE_OTHER)
            elif self.window.inputSolderStencil.isChecked():
                print("Paste Mask Top: " + FILE_OTHER)
                
        # Processing Files!
        outline_file = open(FILE_OUTLINE, "r")
        other_file = open(FILE_OTHER, "r")

        outline = gerber.loads(outline_file.read())
        other = gerber.loads(other_file.read())

        # outline=None
        
        if outline and other:
            output = process_gerber(
                outline,
                other,
                self.window.inputThickness.value(),
                self.window.inputIncludeLedge.isChecked,
                self.window.inputHeight.value(),
                self.window.inputGap.value(),
                self.window.inputIncreaseHoleSize.value(),
                self.window.inputReplaceRegions.isChecked(),
                self.window.inputFlip.isChecked(),
            )
            
            file_id = randint(1000000000, 9999999999)
            scad_filename = "./gerbertostl-{}.scad".format(file_id)
            stl_filename = "./gerbertostl-{}.stl".format(file_id)
            
            with open(scad_filename, "w") as scad_file:
                scad_file.write(output)
                
            p = subprocess.Popen(
                [
                    SCAD_BINARY,
                    "-o",
                    stl_filename,
                    scad_filename,
                ]
            )
            p.wait()

            if p.returncode:
                print("Failed to create an STL file from inputs")
            else:
                with open(stl_filename, "r") as stl_file:
                    stl_data = stl_file.read()
                os.remove(stl_filename)
                
            # Clean up temporary files
            os.remove(scad_filename)
            saveFilename = QFileDialog.getSaveFileName(None, "Save STL", stl_filename, "STL File (*.stl)")[0]
            if DEBUG:
                print("File saved to: " + saveFilename)
            # needs a handler if user clicks cancel!
            saveFile = open(saveFilename,'w')
            saveFile.write(stl_data)
            saveFile.close()


P.S. You shouldn't edit UIC files.

Alexander
  • 16,091
  • 5
  • 13
  • 29
  • 1
    I see. thanks a lot for the info, I’ll take a look over the weekend and try it out then report back but looks like a solid answer just reading it. FWIW I don’t edit UIC files, I only did it in my example because it was quickest way to give a complete example that included pyqt5 generated gui . :) – IAmOrion Aug 12 '22 at 02:30