PyQt:连接信号到槽以启动后台操作

18

我有以下代码,它在更新UI上的进度条(progress)的同时执行后台操作(scan_value)。 scan_value 迭代obj中的一些值,每当该值更改时发出一个信号(value_changed)。由于某些原因,在另一个线程中必须将此封装在对象(Scanner)中。当单击按钮scan时,会调用Scanner。这里是我的问题...以下代码可以正常工作(即进度条准确更新)。

# I am copying only the relevant code here.

def update_progress_bar(new, old):
    fraction = (new - start) / (stop - start)
    progress.setValue(fraction * 100)

obj.value_changed.connect(update_progress_bar)

class Scanner(QObject):

    def scan(self):
        scan_value(start, stop, step)
        progress.setValue(100)

thread = QThread()
scanner = Scanner()
scanner.moveToThread(thread)
thread.start()

scan.clicked.connect(scanner.scan)

但是如果我把最后一部分改成这样:

thread = QThread()
scanner = Scanner()
scan.clicked.connect(scanner.scan) # This was at the end!
scanner.moveToThread(thread)
thread.start()

进度条只有在结束时才会更新(我猜测一切都在同一个线程上运行)。如果我在将接收对象移动到线程之前或之后连接信号到槽中,这是否无关紧要。


3
看起来ekhumoro是正确的(pyqt/qt似乎不能自动正确检测连接类型,除非您明确地使用@pyqtSlot()修饰您的插槽)。然而,我想指出的是,行progress.setValue(100)是线程不安全的,因为您正在从主线程以外的线程访问Qt GUI对象。您发布的代码的其余部分在Qt GUI操作方面是线程安全的。 - three_pineapples
1
@three_pineapples。有趣的是,我们不知道这里是否存在PyQt bug,或者这只是PyQt连接到Python可调用对象的特殊性质。我知道当没有使用@pyqtSlot时会创建某种代理对象,但对于排队连接来说,它会产生什么后果,我并不清楚。 - ekhumoro
1
@ekhumoro 我认为这可能是PyQt4的一个bug,或者至少是应该纠正的不足之处。在PySide中,它肯定不会显示相同的行为(无论信号连接到哪里或如何装饰插槽,PySide始终在QThread中运行“scan”函数)。我在这里制作了一个最小化的示例http://pastebin.com/SqP3WM1z,可以打印出事物正在运行的线程。 - three_pineapples
2
@three_pineapples,感谢你提供的测试用例。我认为我已经找到了问题出现的原因(请查看我的更新答案)。考虑到PyQt当前的工作方式,我现在认为这是一种不足而不是错误。不确定是否有可能进行修正。 - ekhumoro
3个回答

23

无论将worker对象移动到其他线程之前还是之后建立连接都没有关系。引用自Qt文档:

Qt::AutoConnection - 如果信号是从不同的线程中发射出来的,那么信号会被排队,表现为Qt::QueuedConnection。否则,插槽将直接被调用,表现为Qt::DirectConnection当信号被发射时,连接类型被确定。[强调添加]

因此,只要connecttype参数设置为QtCore.Qt.AutoConnection(默认值),Qt就应该确保信号以适当的方式被发射。

示例代码中的问题更可能与slot有关而不是signal。连接信号的Python方法可能需要使用pyqtSlot装饰器标记为Qt slot:

from QtCore import pyqtSlot

class Scanner(QObject):
    
    @pyqtSlot()
    def scan(self):
        scan_value(start, stop, step)
        progress.setValue(100)

编辑:

需要澄清的是,只有在较新版本的Qt中,在信号发出时才确定连接的类型。这种行为是在版本4.4中引入的(与Qt的多线程支持的其他更改一起)。

此外,进一步扩展PyQt特定问题可能会很有价值。在PyQt中,一个信号可以连接到一个Qt槽,另一个信号或任何Python可调用对象(包括lambda函数)。对于后一种情况,将在内部创建一个代理对象,该对象包装Python可调用对象并提供Qt信号/槽机制所需的槽。

正是这个代理对象导致了问题。一旦代理被创建,PyQt就会简单地执行以下操作:

    if (rx_qobj)
        proxy->moveToThread(rx_qobj->thread());

如果连接是在接收对象(即rx_qobj)被移动到其线程之后建立的,则没有问题;但是如果在此之前建立连接,则代理将留在主线程中。

使用@pyqtSlot装饰器完全避免了这个问题,因为它直接创建一个Qt槽,根本不使用代理对象。

最后需要注意的是,这个问题目前不影响PySide。


0

我的问题通过将连接移动到工作线程初始化的位置得以解决,在我的情况下,因为我正在访问一个仅在另一个线程中存在的 Worker Object 类实例化后才存在的对象。

只需在 self.createWorkerThread() 之后连接信号即可。

敬礼


-1

这与Qt的连接类型有关。

http://pyqt.sourceforge.net/Docs/PyQt5/signals_slots.html#connect

http://qt-project.org/doc/qt-4.8/qt.html#ConnectionType-enum

如果两个对象在同一个线程中,将建立标准连接类型,这将导致普通函数调用。在这种情况下,耗时操作发生在GUI线程中,界面会被阻塞。

如果连接类型是消息传递风格的连接,则使用消息发出信号,该消息在另一个线程中处理。GUI线程现在可以自由地更新用户界面。

当您在connect函数中不指定连接类型时,类型会自动检测。


这似乎并没有解决手头的问题,不是吗?即使您自己注意到默认设置为自动(应该可以工作)。请参阅其他答案以获取详细信息。 - László Papp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接