The GUI runs in the main thread; that's where app.exec() is called. QThread instances you create represent "secondary" threads. Never call a method of a QObject (like QLabel.setText) from a non-main thread. Instead, define custom signals on the class that you move to the secondary thread, and connect them to slots of your GUI objects. Then PyQt takes care of calling the slot in the main thread, even if the signal is emitted from a secondary thread. Naively implemented, this would look something like this:
class UpdateTime(QtCore.QObject):
sig_minute = pyqtSignal(str)
sig_hour = pyqtSignal(str)
def __init__(self):
QtCore.QObject.__init__(self)
def UpdateHour(self):
hour = 0
while True:
hour += 1
print("update hour")
self.sig_hour.emit(str(hour))
time.sleep(1)
def UpdateMinute(self):
minute = 0
while True:
minute += 2
print("update minute")
self.sig_minute.emit(str(minute))
time.sleep(1)
You could establish the connections in main:
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
infoscreen = Infoscreen()
thread = QtCore.QThread()
obj = UpdateTime()
obj.moveToThread(thread)
obj.sig_hour.connect(infoscreen.hour.setText)
obj.sig_minute.connect(infoscreen.minute.setText)
thread.finished.connect(app.exit)
thread.start()
infoscreen.show()
sys.exit(app.exec_())
Note how this has decoupled InfosSreen from UpdateTime. One could also establish the connections in InfoScreen.__init__ instead: better encapsulation since the hour and minute labels are members of InfoScreen. Or one could establish the connections in UpdateTime.__init__, but in Qt it seems the philosophy is for the visuals to know about the workers (logic, controllers, etc) rather than the other way around.
In any case, it is important for the connections to be established after the moveToThread: although the default connection type is AUTO, this just means that at the time the connection is made, Qt will determine which type of connection to establish (blocking or queued). So if you connect InfoScreen to UpdateTime before moveToThread, then Qt thinks they "live" in the same thread and makes the connection "blocking". Even after moveToThread is called, the connection remains blocking, so a signal emitted by a function executing in secondary thread causes the connected slot from InfosScreen to be called in the same thread rather than in the main thread. If on the other hand the UpdateTime is first moved to thread, then connected, Qt knows it is in a non-main thread, so makes the connection queued. In that case, emitting a signal in secondary thread will cause the connected slot to be called in the main thread.
However the above won't do anything because moving an object to a thread does not "run" the object. What runs is the thread's event loop, once QThread.start() is called. So to have the object do some work, you have to call a slot on your object, such that the slot executes in the secondary thread. One way is via the start signal of the thread, connected after moveToThread:
thread.started.connect(obj.run)
Then eventhough the QThread instance lives in the main thread, its started signal will be handled in the secondary thread, asynchronously. The obj.run method can call the UpdateHour and UpdateMinute as appropriate. However, your UpdateHour and UpdateMinute cannot both be looping indefinitely (using while True) in the same thread. You would either have to create two separate objects living in two separate threads, one for hour and one for minute, or make the two updates cooperate by running for only small chunk of time. The details depend on what functionality you are porting from TK, but for example of latter approach, it could look like this:
class UpdateTime(QtCore.QObject):
sig_minute = pyqtSignal(str)
sig_hour = pyqtSignal(str)
def __init__(self):
super().__init__(self)
self.hour = 0
self.minute = 0
self.seconds = 0
def run(self):
while True:
time.sleep(1)
self.seconds += 1
self.UpdateMinute()
self.UpdateHour()
def UpdateHour(self):
if self.minute == 60:
self.hour += 1
print("update hour")
self.sig_minute.emit(str(self.hour))
self.minute = 0
def UpdateMinute(self):
if self.seconds == 60:
self.minute += 1
print("update minute")
self.sig_minute.emit(str(self.minute))
self.seconds = 0
Instead of connecting the started signal to call the obj.run, you could connect a QTimer's timeout signal instead, in single shot mode, so that obj.run gets called once, at some later time. Or connect a button's clicked signal, to start it only when a button clicked. In your case the UpdateTime could also be implemented by calling QObject.startTimer and overriding eventTimer to increase minute by 1. See Qt timers for more info on timers.
Not tested the above for bugs, but you get the idea.