Qt小部件析构函数通过连接信号间接调用小部件方法,并崩溃

Posted

技术标签:

【中文标题】Qt小部件析构函数通过连接信号间接调用小部件方法,并崩溃【英文标题】:Qt widget destructor indirectly calls widget method via connected signal, and crashes 【发布时间】:2018-05-13 16:45:57 【问题描述】:

我基于QGraphicsView 制作了一个小部件(QDataflowCanvas),我将信号QGraphicsScene::selectionChanged() 连接到我的主窗口的插槽MainWindow::onSelectionChanged

void MainWindow::onSelectionChanged()

    // canvas is ptr to QDataflowCanvas, subclass of QGraphicsView
    auto selNodes = canvas->selectedNodes();
    auto selConns = canvas->selectedConnections();
    ...

当我关闭我的MainWindow 并且在QGraphicsView 中选择了某些项目时,就会出现问题。

我认为我不需要提供完整的代码(虽然可以找到 here),因为我已经隔离了崩溃的原因。

这就是将要发生的事情(按因果顺序):

MainWindow 的析构函数被调用 QDataflowCanvas 的析构函数被调用 QGraphicsView 的析构函数被调用 QGraphicsScene 的析构函数被调用,触发删除所有项目(clear()) QGraphicsItem 的析构函数被调用 将触发 selectionChange 事件 调用 MainWindow::onSelectionChanged 槽 方法 QDataflowCanvas::selectedNodes() 被调用,但对象被销毁 崩溃!

可以从崩溃的堆栈跟踪中看到更详细的信息:

我找到了这个解决方法:如果我断开MainWindow::~MainWindow中的信号,它当然不会崩溃:

MainWindow::~MainWindow()

    QObject::disconnect(canvas->scene(), &QGraphicsScene::selectionChanged, this, &MainWindow::onSelectionChanged);

但这似乎是一种相当不典型的做法:我从来没有发现自己必须手动严重的信号槽连接,否则程序会崩溃。

必须有更合适的解决方案。

【问题讨论】:

【参考方案1】:

首先,您的项目名称是错误的。采用Q-prefixed 命名空间。在任何使用 Qt 的项目中,您不应该有任何 Q-prefixed 类。例如,您应该将项目重命名为 DataflowCanvas

有三种解决方案:

    按值保留所有子项,根据子项的依赖关系对子项进行排序。从QDataFlowCanvas 调用的QWidgetPrivate::deleteChildren 将是无操作的,或者至少它不会触及您关心的对象。

    连接到MainWindow::onSelectionChanged 插槽时使用旧的connect 语法。请注意,当您的插槽被调用时,主窗口对象是 QWidget 动态类型,而不是 MainWindow 类型。使用旧的连接语法建立的连接尊重对象的动态类型,与给定类的槽建立的连接将保证对象是该类的动态 ,即在运行时。

    清除析构函数中的选择 - 然后将不再处理选择更改。

第一个解决方案使所有内容都变得明确,并且是我会使用的解决方案:

class DataFlowCanvas : public QGraphicsView 
  ...
private:
    QDataflowModel *model_;
    QDataflowTextCompletion *completion_;
    QSet<QDataflowNode*> ownedNodes_;
    QSet<QDataflowConnection*> ownedConnections_;
    QMap<QDataflowModelNode*, QDataflowNode*> nodes_;
    QMap<QDataflowModelConnection*, QDataflowConnection*> connections_;
    bool showIOletsTooltips_;
    bool showObjectHoverFeedback_;
    bool showConnectionHoverFeedback_;
    qreal gridSize_;
    bool drawGrid_;
    QGraphicsSecene scene_;
;

场景在任何其他字段之前被破坏。问题解决了。你也应该按价值持有其他一切。例如。 completion_ 等。指针间接没有用。

第二个解决方案突出了一个不幸的 Qt 错误。也就是说——在下面的代码中,旧的连接语法永远不会调用Derived2::aSlot2,因为在调用槽时,对象不再是Derived2 类型:

#include <QtCore>

int ctr1, ctr2;

struct Derived1 : QObject 
  Q_SLOT void aSlot1()  ctr1++; qDebug() << __FUNCTION__; 
  Q_SIGNAL void aSignal();
  ~Derived1()  Q_EMIT aSignal(); 
  Q_OBJECT
;

struct Derived2 : Derived1 
  Q_SLOT void aSlot2()  ctr2++; qDebug() << __FUNCTION__ << qobject_cast<Derived2*>(this); 
  Q_OBJECT
;

int main() 
  
    Derived2 d;
    QObject::connect(&d, &Derived2::aSignal, &d, &Derived2::aSlot2);
    QObject::connect(&d, SIGNAL(aSignal()), &d, SLOT(aSlot2()));
    QObject::connect(&d, SIGNAL(aSignal()), &d, SLOT(aSlot1()));
  
  Q_ASSERT(ctr1 == 1);
  Q_ASSERT(ctr2 == 1);

#include "main.moc"

输出清楚地说明了问题:

aSlot2 QObject(0x0)   <-- aSlot2 called but `this` is of `Derived1*` type!
aSlot1

【讨论】:

感谢您的详细回答,非常有见地。我尝试在我的小部件类中按值保留QGraphicsSecene,但我遇到了另一个相同类型的类似崩溃(基本上相同的模式:场景的析构函数删除所有对象,它发出一个由客户端处理的 selectionChange 信号,它尝试使用被销毁的小部件的方法)。似乎唯一的方法是清除小部件析构函数开头的选择。 (我还没有尝试解决方案#2)【参考方案2】:

我想得太简单了 :) 只检查canvas 指针怎么样:

void MainWindow::onSelectionChanged()

    if (!qobject_cast<QGraphicsScene*>(canvas))
        return;

    auto selNodes = canvas->selectedNodes();
    auto selConns = canvas->selectedConnections();
    ...

我使用qobject_cast 来检查指针canvas 是否仍然存在。您可以以其他(更好的)方式进行检查。代码有效。

【讨论】:

以上是关于Qt小部件析构函数通过连接信号间接调用小部件方法,并崩溃的主要内容,如果未能解决你的问题,请参考以下文章

Qt:动态小部件信号和槽连接

从 C++ 中的多个线程调用 Qt 中小部件类的信号函数是不是安全?

如何在 Qt 中将消息从子窗口小部件发送到父窗口?

如何避免鼠标单击一个小部件触发 Qt 中其他小部件的信号?

QT Creator 中的 Slot 信号,connect() 函数在哪里?

如何在颤动中间接调用另一个小部件中的 ontap 函数?