【问题标题】:Qt: How to wait for multiple signals?Qt:如何等待多个信号?
【发布时间】:2014-01-14 07:54:59
【问题描述】:

我正在使用 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() 信号。但这真的是最好的方法吗?助手还必须检查每个发送者,以便只考虑特定信号的一个“实例”。

【问题讨论】:

  • "那么您有什么建议可以如何进行吗?"我相信你想mock Qt 库。更容易,你有更多的控制权。此外,您当前的测试以某种方式测试 Qt 库,而不是您的代码。
  • @LaszloPapp 例如见unittest.mock
  • @Bakuriu:我不确定 mock 与多信号问题有什么关系。
  • @LaszloPapp 如果您模拟 Qt 库,那么您可以简单地检查超时后通过模拟对象调用了哪些信号。
  • @Bakuriu:我一直在研究 QtMock 库,但还没有进展,所以还没有“Qt 友好”的模拟框架。更重要的是,我认为拥有一个基于 qt 事件循环的解决方案而不是一个剧烈的超时会更好。

标签: python qt pyside qt-signals qeventloop


【解决方案1】:

我个人会将所有必要的信号连接到它们相应的信号处理程序,也就是。插槽。

他们都会将他们的发射标记为“完成”,并且可以检查整体条件是否“完成”,并且在每个信号处理程序设置自己的“完成”之后,可能会有一个全局“完成”检查,如果足够了,它们会发出“全局完成”信号。

然后你也可以最初连接到那个“全局完成”信号,当相应的信号处理程序被触发时,你会知道它已经完成,除非同时条件发生变化。

经过理论设计,你会有这样的东西(伪代码)

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文件中,不是我写的,所以可能有一个或六个。
  • @TeemuKarimerto:我看不出有什么问题?你有什么顾虑?当然你也可以通过参数把所有东西都放到一个信号槽关系中,然后在一个槽中完成设置和全局检查。
  • 现在我想起来了,多个不同的插槽方法确实可以工作。我只需要跟踪我已经分配了哪些插槽,然后在添加新条件时选择下一个。幸运的是,这对于 Python 来说都是相对简单的 :) 我想的另一种方法是制作接收信号的字典并检查字典的大小是否等于等待条件计数,然后发出“全局”就绪()信号。然而,这可能有无法预料的问题?
  • @TeemuKarimerto:我不认为这是一种不同的方式。在我看来,同样的概念。您添加一个本地完成,并检查全局完成,无论它是计数(int)、布尔还是其他东西,这只是 IMO 的详细信息。
  • 是的,你是对的。我将继续执行此操作。谢谢你的建议。
【解决方案2】:

我最终实现了一个相当简单的帮助类。它有一组用于等待信号,另一组用于接收信号。每个可等待信号都连接到一个插槽。插槽将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)

clear 函数几乎不是必需的,但允许重用类实例。

【讨论】:

  • +1 听起来像是等待多种情况时的通用方法。
【解决方案3】:

在 C++ 中,一个非常简单的方法是:

  1. 拥有一组您希望收到信号的(对象、信号索引)对。

  2. 在等待开始之前复制集合。

  3. 在槽中,从复制的列表中删除 (sender(), senderSignalIndex()) 元素。如果列表是空的,你就知道你已经完成了。

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

在 C++ 中,connect() 通常使用封装在 SIGNALSLOT 宏中的方法参数来调用。这些宏在方法代码前面加上“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跳过一个字符一样?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-03
  • 1970-01-01
  • 2011-12-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多