Currently writing a GUI application that launches from and interfaces with The Foundry's Nuke, a visual effects compositing software (although this is not the issue AFAIK, since it's repeatable inside an IDE). I am attempting to figure out why running one native Python thread in my Pyside2 GUI is crashing when calling a function that results in updating a QLabel, while another similar function that is calling the same function to update a different QLabel seems to work every time without issue. Both of these are threaded as they're background processes involving data I/O and API requests that can take some time. This is in Pyside2/Qt 5.12, and python 3.7, neither of which can be updated due to software limitations.
I've already read here that using QThread is usually "better" than the regular threading module for interacting with Qt, and here that "PyQt GUI calls like setText() on a QLineEdit are not allowed from a thread." I will likely switch to QThread if that is the solution, but there's a lot more data passing that I'd need to do after creating a separate Worker class, as is suggested by many tutorials I've seen, as well as this SO answer, so I'd like to keep it using the native threading module if at all possible because that seems less complicated.
However, it seemed a good idea to ask, as it seems like one of the buttons connected to a thread has the ability to update a QLabel just fine, and the other one consistently crashes, which doesn't make sense. Below is some stripped down code replicating the problem. The connectAPI function is for connecting to an external API that does some logic and processing for the current shot, simulated with a time.sleep(). (Also I am aware that my functions and variables are not PEP8 compliant, camelCase is the convention in this codebase)
# PySide2 / QT 5.12
# Python 3.7
import PySide2.QtCore as QtCore
import PySide2.QtGui as QtGui
import PySide2.QtWidgets as QtWidgets
import time
import sys
import threading
class MyMainWindow(QtWidgets.QWidget):
def __init__(self, pathToFile):
super(MyMainWindow, self).__init__()
self.resize(250, 200)
self.connectionEstablished = False
self.inputFile = pathToFile
self.createWidgets()
self.layoutWidgets()
def startConnectionThread(self):
self.connectionValid = False
# This never shows in the UI, but does print from the updateStatus(), so presumably it's just immediately getting changed by the
# first updateStatus() call inside of connectAPI
self.updateStatus(self.connectionLabel, 'inprogress', 'Attempting Connection')
connectionThread = threading.Thread(target=self.connectAPI)
connectionThread.start()
def connectAPI(self):
print("connectAPI function")
# This works fine, even though it "shouldn't" because it's updating a QLabel from inside a thread
self.updateStatus(self.connectionLabel, 'inprogress', "Searching...")
# Simulated external API connection and processing
self.connectionObject = True
for x in range(0,11):
print(x)
time.sleep(0.2)
if self.connectionObject == False:
self.updateStatus(self.connectionLabel, 'failure', "Object Not Found!")
return False
else:
self.objectFound = True
# This works fine, even though it "shouldn't" because it's updating a QLabel from inside a thread
self.updateStatus(self.connectionLabel, 'success', "Search Complete")
def updateStatus(self, label, color, text):
print(f"update status: {text}")
if color == 'success':
color = 'ForestGreen'
if color == 'failure':
color = 'OrangeRed'
elif color == 'inprogress':
color = 'Tan'
label.setText('<FONT COLOR="{color}"><b>{text}<b></FONT>'.format(color=color, text=text))
def submitButtonThread(self):
print("starting submit thread")
submit_thread = threading.Thread(target=self.testFunction)
submit_thread.start()
def testFunction(self):
print("test function")
for x in range(0, 11):
print(x)
time.sleep(0.2)
# Uncommenting the following line will crash the GUI, but only in this function
self.updateStatus(self.submitLabel, 'inprogress', 'test function end...')
def createWidgets(self):
# Widget - Verify Connection Button
self.connectionGet = QtWidgets.QPushButton('Verify Name')
self.connectionGet.clicked.connect(self.startConnectionThread)
# Widget - Connection Success Label
self.connectionLabel = QtWidgets.QLabel('')
self.connectionLabel.sizeHint()
self.connectionLabel.setAlignment(QtCore.Qt.AlignCenter)
# Widget - Creation Button
self.createSubmitButton = QtWidgets.QPushButton('Create Submission')
self.createSubmitButton.clicked.connect(self.submitButtonThread)
# Widget - Submit Success Label
self.submitLabel = QtWidgets.QLabel('')
self.submitLabel.sizeHint()
self.submitLabel.setAlignment(QtCore.Qt.AlignCenter)
self.submitLabel.setMaximumHeight(30)
def layoutWidgets(self):
self.mainLayout = QtWidgets.QVBoxLayout(self)
self.connectionGroup = QtWidgets.QGroupBox('API Connection')
connectionLayout = QtWidgets.QGridLayout()
connectionLayout.addWidget(self.connectionGet, 0, 2, 1, 2)
connectionLayout.addWidget(self.connectionLabel, 1, 0, 1, 4)
self.connectionGroup.setLayout(connectionLayout)
self.creationSection = QtWidgets.QGroupBox('Creation Section')
creationLayout = QtWidgets.QGridLayout()
creationLayout.addWidget(self.createSubmitButton, 0, 2, 1, 2)
creationLayout.addWidget(self.submitLabel, 1, 0, 1, 4)
self.creationSection.setLayout(creationLayout)
# Add them all to main
self.mainLayout.addWidget(self.connectionGroup)
self.mainLayout.addSpacing(10)
self.mainLayout.addWidget(self.creationSection)
self.setLayout(self.mainLayout)
# This function is how it's called from inside Nuke, included for completeness
def launchGUI(pathToFile):
global window
window = MyMainWindow(pathToFile)
window.show()
# This is used inside the IDE for testing
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
global window
window = MyMainWindow("//Server/path/to/file.txt")
window.show()
sys.exit(app.exec_())
Output when clicking connectionGet button:
update status: Attempting Connection
connectAPI function
update status: Searching...
0
1
2
3
4
5
6
7
8
9
10
update status: Search Complete
Output when clicking createSubmitButton button:
starting submit thread
test function
0
1
2
3
4
5
6
7
8
9
10
update status: test function end...
Process finished with exit code -1073741819 (0xC0000005)
(And it crashes with that exit code)
I tried swapping the threads targeted by the two functions to see if the results would be the same, thinking maybe only one set of threads/updates can happen, and got even more confusing results:
connectionThread = threading.Thread(target=self.testFunction)
Results in:
update status: Attempting Connection
test function
0
1
2
3
4
5
6
7
8
9
10
update status: test function end...
Process finished with exit code -1073741819 (0xC0000005)
(Crash)
submit_thread = threading.Thread(target=self.connectAPI)
Results in:
starting submit thread
connectAPI function
update status: Searching...
0
1
Process finished with exit code -1073741819 (0xC0000005)
(Crash)