我正在尝试将我在Tkinter
中编写的应用程序移植到PyQt
。该应用程序有几个线程,用于查询外部信息(通过API)并使用它来更新应用程序中的文本标签。没有用户互动。
在Tkinter
我正在使用after()
方法。在PyQT
中,据我了解,我应该使用QtCore.QThread()
。
以下示例程序应该更新两个标签(hour
和minute
)。 UI是从Qt Designer生成的,基本上是上面的两个文本标签。我在应用程序窗口中看到小时数已更新(它们在GUI中更改,并且"update hour"
打印在控制台上)。会议纪要不是。我(失败)的理解是started.connect()
用指示的方法启动一个线程。看起来它只能工作一次。
我应该如何更改代码,以便为UpdateTime
中的每个方法启动一个线程?
import sys
import time
from PyQt4 import QtGui, QtCore
from infoscren_ui import Ui_MainWindow
class Infoscreen(QtGui.QMainWindow, Ui_MainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.setupUi(self)
class UpdateTime(QtCore.QObject):
def __init__(self, root):
QtCore.QObject.__init__(self)
self.root = root
def UpdateHour(self):
hour = 0
while True:
hour += 1
print("update hour")
self.root.hour.setText(str(hour))
time.sleep(1)
def UpdateMinute(self):
minute = 0
while True:
minute += 2
print("update minute")
self.root.minute.setText(str(minute))
time.sleep(1)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
infoscreen = Infoscreen()
thread = QtCore.QThread()
obj = UpdateTime(infoscreen)
obj.moveToThread(thread)
thread.started.connect(obj.UpdateHour)
thread.started.connect(obj.UpdateMinute)
thread.finished.connect(app.exit)
thread.start()
infoscreen.show()
sys.exit(app.exec_())
答案 0 :(得分:2)
您不能在同一个线程中同时运行UpdateHour
和UpdateMinute
,因为第一个要启动的只会阻止另一个。如果你想运行两个这样的阻塞循环,每个阻塞循环都需要自己的线程。
此外,从主GUI线程外部(即应用程序首次启动的线程)直接调用GUI方法不是线程安全的。相反,您应该定义要从工作线程发出的自定义信号:
class UpdateHours(QtCore.QObject):
hourChanged = QtCore.pyqtSignal(object)
@QtCore.pyqtSlot()
def updateHour(self):
...
self.hourChanged.emit(hour)
主线程中的对象可以连接到这些信号并从那里更新GUI:
self.hourObject.hourChanged.connect(self.handleHourChanged)
...
def handleHourChanged(self, hour):
# do something with hour value...
默认情况下,当发送方和接收方处于不同的线程时,Qt将自动确保信号被异步处理(它们被添加到应用程序的事件队列中),这足以保证线程安全。
答案 1 :(得分:1)
GUI在主线程中运行;那个叫app.exec()的地方。您创建的QThread实例代表" secondary"线程。永远不要从非主线程调用QObject的方法(如QLabel.setText
)。而是在您移动到辅助线程的类上定义自定义信号,并将它们连接到GUI对象的插槽。然后PyQt负责调用主线程中的槽,即使信号是从辅助线程发出的。天真实施,这看起来像这样:
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)
您可以在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_())
请注意这是如何将InfosSreen
与UpdateTime
分离的。也可以在InfoScreen.__init__
中建立连接:更好的封装,因为hour
和minute
标签是InfoScreen
的成员。或者可以在UpdateTime.__init__
中建立连接,但在Qt中,似乎理念是让视觉知道工人(逻辑,控制器等)而不是相反。
在任何情况下,在moveToThread
之后建立连接很重要:尽管默认连接类型是AUTO,这只是意味着在建立连接时,Qt将确定哪种类型的连接连接建立(阻塞或排队)。因此,如果您在InfoScreen
之前将UpdateTime
与moveToThread
联系起来,那么Qt认为他们已经#34;生活"在同一个线程中并进行连接"阻止"。即使在调用moveToThread
之后,连接仍然是阻塞的,因此在辅助线程中执行的函数发出的信号会导致来自InfosScreen
的连接槽在同一线程中而不是在主线程中被调用。另一方面,如果UpdateTime
首先移动到线程,然后连接,Qt知道它在非主线程中,因此使连接排队。在这种情况下,在辅助线程中发出信号将导致在主线程中调用连接的槽。
然而,上面不会做任何事情,因为将一个对象移动到一个线程不会运行"物体。一旦调用QThread.start()
,运行的是线程的事件循环。因此,要使对象执行某些操作,您必须在对象上调用一个插槽,以便插槽在辅助线程中执行。一种方法是通过线程的start
信号,在moveToThread
之后连接:
thread.started.connect(obj.run)
然后尽管QThread
实例存在于主线程中,但它的started
信号将在辅助线程中异步处理。 obj.run
方法可以根据需要调用UpdateHour
和UpdateMinute
。但是,您的UpdateHour
和UpdateMinute
不能在同一个帖子中无限期地循环(使用while True
)。您可能需要创建两个独立的对象,这些对象位于两个单独的线程中,一个用于小时,一个用于分钟,或者通过仅运行一小段时间使两个更新协作。细节取决于您从TK移植的功能,但是例如后一种方法,它可能看起来像这样:
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
不是连接started
信号来呼叫obj.run
,而是可以在单拍模式下连接QTimer
timeout
信号, obj.run
稍后会被调用一次。或者连接按钮的clicked
信号,仅在点击按钮时启动它。在您的情况下,UpdateTime
也可以通过调用QObject.startTimer
并覆盖eventTimer
以将分钟增加1来实现。有关计时器的更多信息,请参阅Qt timers。
没有测试上面的错误,但你明白了。