调试Qt信号和槽的最佳实践是什么?

8

调试信号和槽可能很困难,因为当信号被发射时,调试器不会跳转到槽。以下是一些Qt信号和槽调试的最佳实践。

具体来说:

  1. 如何确保连接成功建立?
  2. 何时应该使用信号和槽,何时应该避免使用它们?
  3. 根据您的经验,哪些是最有效的调试技巧?
4个回答

9

有一篇博客文章叫做“20种调试Qt信号和槽的方法”,写得有一段时间了。
我认为它解决了你问题中的#1和#3。

对于#2,我认为没有必须使用或不使用信号/槽的硬性规定,因为它们是GUI框架的核心概念。信号是一种完美的方式来解耦一个组件与另一个组件的知识,允许您设计可重用的小部件,仅声明状态更改或通知。它还是一种非常好的方式来从非GUI线程循环通知GUI更改,通过发出主线程可以看到的信号。

有时候,你真正想要的不是信号/槽,而是使用事件,例如当父窗口应该成为多个子窗口的事件过滤器时。子项仍然不需要知道父项,而父项会获得更直接的事件,而不是信号连接。

在同一主题的事件上,有时候你真正想要的是从子项向父项->祖父项->等级传递事件。这里不应该使用信号,因为它们不是用于确定建议的事件是否应该导致操作(它们可以被这样使用)。事件允许您检查当前状态,决定此小部件是否应该执行任何操作,或者将其上升到链中供其他人检查。

有一个关于信号/槽和事件之间的区别非常好的答案。这里有一个好片段:

  • 你“处理”事件
  • 你“被通知”信号发射

我喜欢这个引用的原因是它描述了不同的需求情况。如果您需要在小部件中处理操作,则可能需要事件。如果您想被通知发生的事情,则可能需要信号。


5
你提供的链接在实际调试技术方面相当令人失望。要点基本上是:“避免写出错误,以下有20种可能出错的情况”。没有提到工具或技术来实际调试问题。当你得到一个崩溃转储时,会遇到一个异常深的调用堆栈、大量的“qt_static_metacall”调用,没有简单的方法来解密发出的信号,也无法知道任何特定信号-插槽连接建立的位置。 - IInspectable

2
除了已经提到的,这里还有一些额外的技巧。
如果您在使用QTest进行单元测试,则可以将 -vs 参数传递给可执行文件,所有信号都将显示在控制台中。
我查看了QTest的工作原理,并注册回调函数,当使用 QSignalDumper 类执行信号和插槽时会触发。但是,该API未被导出,可能随时中断。以下是我如何在Qt 5.10上使用GCC挂接所有信号和插槽的方法。
// QSignalSpyCallbackSet is defined in qt5/qtbase/src/corelib/kernel/qobject_p.h

struct QSignalSpyCallbackSet
{
    typedef void (*BeginCallback)(QObject *caller, int signal_or_method_index, void **argv);
    typedef void (*EndCallback)(QObject *caller, int signal_or_method_index);
    BeginCallback signal_begin_callback,
                    slot_begin_callback;
    EndCallback signal_end_callback,
                slot_end_callback;
};
typedef void (*register_spy_callbacks)(const QSignalSpyCallbackSet &callback_set);

static void showObject(QObject *caller, int signal_index, const QString &msg)
{
   const QMetaObject *metaObject = caller->metaObject();
   QMetaMethod member = metaObject->method(signal_index);
   qDebug() << msg << metaObject->className() << qPrintable(member.name());
}

static void onSignalBegin(QObject *caller, int signal_index, void **argv)
{
   showObject(caller, signal_index, "onSignalBegin");
}

static void onSlotBegin(QObject *caller, int signal_index, void **argv)
{
   showObject(caller, signal_index, "onSlotBegin");
}

int main(int argc, char *argv[])
{
   static QSignalSpyCallbackSet set = { onSignalBegin, onSlotBegin, 0, 0 };
   QLibrary qtcore("libQt5Core");
   register_spy_callbacks reg = (register_spy_callbacks)qtcore.resolve("_Z32qt_register_signal_spy_callbacksRK21QSignalSpyCallbackSet");

   if (reg) {
      reg(set);
   }
   ...
}

我认为Qt应该公开该API,因为我们可以将其用于除了调试之外的许多其他方面,例如监视槽中花费的时间、获取统计数据等。

0
关于#1,我想补充一些信息,我没有在上面或参考的博客文章中看到。
QObject :: connect()的文档中:
创建给定类型的连接,从发送器对象中的信号到接收器对象中的方法。如果连接成功,则返回true;否则返回false。
我更喜欢断言我的连接返回值,以确保连接成功,特别是因为并非所有Qt程序都会有控制台输出。这也导致更易于维护的代码,因为它将捕获稍后对信号或插槽所做的更改,并迫使进行更改的程序员也更新连接。

我知道这是一个古老的帖子,但以防最近被使用。文档实际上指出connect返回“handle”。我认为检查返回值的有效性 - 无论它是什么 - 更通用。 - user2839574
@JanHus 我引用的文档是Qt 4.8版本。在Qt5中,返回一个句柄,但更重要的是添加了一种基于函数对象的新连接方式,允许进行编译时检查,这更安全(请参见https://doc.qt.io/qt-5/signalsandslots-syntaxes.html)。 - user3288829
我一直在使用QtDesigner GUI(“编辑插槽/信号”),通过移动鼠标来使用。它有限且会生成一个奇怪的GUI字符串。由于没有代码可以检查,因此它可能会以旧的“连接”形式运行。这是一个不错的功能,但并不那么用户友好。 - user2839574

0
如何确保连接成功设置?
每次连接失败时,您的应用程序控制台输出中都会出现警告。
何时应该使用信号和槽,何时应该避免使用它们?
在我看来,任何时候都可以使用它们来维护类设计中的关注点分离。您的类可以发出一个信号,可能会被另一个完全不知道您的类的类(或类)回答。这可以降低耦合度。
从您的经验中,哪些是最有效的调试技术?
我无法添加比此博客文章更多的内容。20 ways to debug Qt signals and slots

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