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.