Qt:如何等待多个信号?

4
我正在使用PySide和Qt开发一种GUI测试库。目前,当测试案例只需要等待一个条件(例如信号或超时)时,它的功能非常好。但是,我的问题是在进行数据验证之前必须等待多个条件发生。
为了不对主线程造成太大干扰,测试运行器在其自己的线程中运行。等待信号/超时是通过事件循环完成的,并且这部分工作非常出色(以下是一个简化示例)。
# Create a  simple event loop and fail timer (to prevent infinite waiting)
loop = QtCore.QEventLoop()
failtimer = QtCore.QTimer()
failtimer.setInterval(MAX_DELAY)
failtimer.setSingleShot(True)
failtimer.timeout.connect(loop.quit)

# Connect waitable signal to event loop
condition.connect(loop.quit) # condition is for example QLineEdit.textChanged() signal

# Perform test action
testwidget.doStuff.emit() # Function not called directly, but via signals

# Wait for condition, or fail timeout, to happen
loop.exec_()

# Verify data
assert expectedvalue == testwidget.readValue()

等待必须是同步的,因此事件循环是解决方法,但无法处理多个信号。当然可以等待任何多个条件,但无法等待多个条件/信号全部发生。那么对于如何继续进行,有什么建议吗?
我正在考虑一个辅助类,它计算接收到的信号数量,一旦达到所需计数就发出 ready()-信号。但这真的是最好的方法吗?该辅助程序还必须检查每个发送器,以便只计算特定信号的一个“实例”。

那么,对于如何继续进行,你有什么建议吗?我相信你想要模拟Qt库。这样会更容易,你会有更多的控制权。而且你目前的测试在某种程度上更多地测试了Qt库而不是你的代码。 - Bakuriu
请参阅unittest.mock作为示例。 - Bakuriu
@Bakuriu:我不确定模拟对象如何与多个信号问题相关。 - László Papp
@LaszloPapp 如果你嘲笑Qt库,那么你可以在超时后通过模拟对象检查哪些信号被调用了。 - Bakuriu
@Bakuriu:我一直在开发一个QtMock库,但是它没有进展,所以目前还没有“Qt友好”的模拟框架。更重要的是,我认为使用基于Qt事件循环的解决方案比使用极端超时更好。 - László Papp
@LaszloPapp 我同意你的看法,我更倾向于寻找一个干净的事件循环解决方案,而不是整个模拟库。 - Teemu Karimerto
3个回答

3
我最终实现了一个非常简单的辅助类。它有一个等待信号的集合和另一个接收信号的集合。每个等待信号都连接到单个槽。该槽将sender()添加到就绪集中,一旦集合大小匹配,就会发出ready信号。
如果有人感兴趣,这是我最终所做的:
from PySide.QtCore import QObject, Signal, Slot

class QMultiWait(QObject):
    ready = Signal()

    def __init__(self, parent=None):
        super(QMultiWait, self).__init__(parent)
        self._waitable = set()
        self._waitready = set()

    def addWaitableSignal(self, signal):
        if signal not in self._waitable:
            self._waitable.add(signal)
            signal.connect(self._checkSignal)

    @Slot()
    def _checkSignal(self):
        sender = self.sender()
        self._waitready.add(sender)
        if len(self._waitready) == len(self._waitable):
            self.ready.emit()

    def clear(self):
        for signal in self._waitable:
            signal.disconnect(self._checkSignal)
< p > clear 函数并不是必须的,但它允许重复使用类实例。


+1听起来像是等待多种情况时的一种通用方法。 - Trilarion

3
我个人会将所有必要的信号连接到它们对应的信号处理程序,即槽函数。它们都会标记它们的发射为“完成”,并且可以检查总体条件是否已经“完成”。每当一个信号处理程序设置了自己的“完成”后,就可以进行全局“完成”检查,如果符合要求,它们会发出一个“全局完成”信号。
然后你也可以最初连接到该“全局完成”信号,当相应的信号处理程序被触发时,你会知道它已经完成了,除非在此期间条件发生了变化。
理论设计后,你会得到类似以下的代码(伪代码):
connect_signal1_to_slot1();
connect_signal2_to_slot2();
...
connect_global_done_signal_to_global_done_slot();

slotX: mark_conditionX_done(); if global_done: emit global_done_signal();
global_done_slot: do_foo();

您可能还可以简化,只有两个信号和槽,即:一个用于本地完成操作,基于传递的参数“标记”本地信号完成,然后就会有“全局完成”信号和槽。

区别在于语义,是使用一个信号和槽带参数还是没有参数的多个信号和槽,但原则上是相同的理论。


这听起来是一个可行的方法,但是这种方法的问题在于我不一定事先知道我可能需要多少个特定的插槽。测试用例在单独的 .xml 文件中,并且不是由我编写的,因此可能会有一个或半打。 - Teemu Karimerto
@TeemuKarimerto:我不明白有什么问题?你担心什么?当然,你也可以通过参数将所有内容放入一个信号槽关系中,然后在一个槽中完成设置和全局检查。 - László Papp
现在我想起来,多个不同的插槽方法确实可以工作。我只需要跟踪已经分配的插槽,并在添加新条件时选择下一个插槽即可。幸运的是,这在Python中相对简单 :) 我另外考虑的一种方法是创建一个接收信号的字典,并检查字典的大小是否等于可等待条件计数,然后发出“全局”ready()信号。但是这可能会有意想不到的问题? - Teemu Karimerto
@TeemuKarimerto:我不认为这是一种不同的方式。在我看来,这是相同的概念。您添加一个本地完成标志,并根据全局完成标志进行检查,无论它是计数(int)、布尔值还是其他什么,这只是细节而已。 - László Papp
@KubaOber:问题是关于Pyside,而不是C++。此外,Teemu的答案中的解决方案(使用“sender”)在C++中也可以正常工作。 - László Papp
显示剩余4条评论

1
在C++中,实现这个非常简单的方法是:
  1. 建立一个期望被信号激发的(对象,信号索引)对的集合。

  2. 在启动等待之前复制该集合。

  3. 在槽函数中,从复制的列表中删除(sender(),senderSignalIndex())元素。如果列表为空,则说明完成了。

此解决方案的优点是可移植性: 该方法适用于PySide和C++。

在C++中,通常使用SIGNAL或SLOT宏来包装方法参数调用connect()。这些宏会在方法代码之前添加一段代码:'0'、'1'或'2',以指示它是可调用方法、信号还是槽函数。调用registerSignal时会跳过这种方法代码,因为该函数需要原始方法名称。

由于在registerSignal中调用indexOfMethod需要标准化的签名,所以connect方法会将其标准化。

class SignalMerge : public QObject {
    Q_OBJECT
#if QT_VERSION>=QT_VERSION_CHECK(5,0,0)
    typedef QMetaObject::Connection Connection;
#else
    typedef bool Connection;
#endif
    typedef QPair<QObject*, int> ObjectMethod;
    QSet<ObjectMethod> m_signals, m_pendingSignals;

    void registerSignal(QObject * obj, const char * method) {
        int index = obj->metaObject()->indexOfMethod(method);
        if (index < 0) return;
        m_signals.insert(ObjectMethod(obj, index));
    }
    Q_SLOT void merge() {
        if (m_pendingSignals.isEmpty()) m_pendingSignals = m_signals;
        m_pendingSignals.remove(ObjectMethod(sender(), senderSignalIndex()));
        if (m_pendingSignals.isEmpty()) emit merged();
    }
public:

    void clear() {
        foreach (ObjectMethod om, m_signals) {
            QMetaObject::disconnect(om.first, om.second, this, staticMetaObject.indexOfSlot("merge()"));
        }
        m_signals.clear();
        m_pendingSignals.clear();
    }
    Q_SIGNAL void merged();
    Connection connect(QObject *sender, const char *signal, Qt::ConnectionType type = Qt::AutoConnection) {
        Connection conn = QObject::connect(sender, signal, this, SLOT(merge()), type);
        if (conn) registerSignal(sender, QMetaObject::normalizedSignature(signal+1));
        return conn;
    }
};

这也是一种非常有趣的方法,尽管与我已经实现的方法类似。然而,对于将来的参考,特别是对于C++来说,这确实是好东西。我之前不知道senderSignalIndex(),你能解释一下为什么要使用(sender, signal+1)调用registerSignal吗?也就是为什么要加上+1跳过一个字符? - Teemu Karimerto

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