Search code examples
c++qtqml

Crash when invoking QML function inside QMdiArea using QMetaObject::invokeMethod


Problem:

I have a QQuickWidget inside a QWidget, and I’m adding this widget to a QMdiArea. I am trying to call a QML function (plusBtn()) from C++ using QMetaObject::invokeMethod. However, my application crashes when I try to invoke the method. This crash happens only when the window is added to the QMdiArea. When I remove the widget from QMdiArea, everything works fine.

Here is my simplified code:

QML (Map.qml):

import QtQuick
import QtLocation
import QtPositioning

Rectangle {
    id: rectangle

    Plugin {
        id: mapPlugin
        name: "osm"
    }

    Map {
        id: mapView
        anchors.fill: parent
        plugin: mapPlugin
        zoomLevel: 15
        minimumZoomLevel: 2
        activeMapType: supportedMapTypes[0]
    }

    function plusBtn() {
        if (mapView.zoomLevel < mapView.maximumZoomLevel) {
            mapView.zoomLevel = Math.round(mapView.zoomLevel + 1);
        }
    }
}

C++ (MainWindow Implementation):

#include <QDebug>
#include <QMainWindow>
#include <QPushButton>
#include <QQmlEngine>
#include <QQuickItem>
#include <QQuickWidget>
#include <QVBoxLayout>
#include <QWidget>
#include <QApplication>
#include <QMDIArea>

class QmlWindow : public QWidget
{
public:
    QmlWindow(QWidget* parent = nullptr)
        : QWidget(parent)
    {
        QVBoxLayout* layout = new QVBoxLayout(this);

        // Create and initialize the QQuickWidget
        quickWidget = new QQuickWidget(this);
        quickWidget->setSource(QUrl(QStringLiteral("qrc:/Map.qml")));
        quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);

        auto rootObj = quickWidget->rootObject();

        // Create a button to invoke the QML method
        QPushButton* button = new QPushButton("Call QML Method");
        connect(button, &QPushButton::pressed, this, [=]() {
            if (!rootObj) {
                qWarning() << "Root object is null!";
                return;
            }
            QMetaObject::invokeMethod(rootObj, "plusBtn");
        });

        layout->addWidget(quickWidget);
        layout->addWidget(button);
        setLayout(layout);
    }

private:
    QQuickWidget* quickWidget;
};

class MainWindow1 : public QMainWindow
{
public:
    MainWindow1(QWidget* parent = nullptr)
        : QMainWindow(parent)
    {
        mdiArea = new QMdiArea(this);
        setCentralWidget(mdiArea);

        QPushButton* openWindowBtn = new QPushButton("Open QML Window", this);
        connect(openWindowBtn, &QPushButton::clicked, this, &MainWindow1::openQmlWindow);

        setMenuWidget(openWindowBtn);
    }

private:
    void openQmlWindow()
    {
        QmlWindow* window = new QmlWindow();
        QMdiSubWindow* subWindow = mdiArea->addSubWindow(window);
        window->show();
    }

    QMdiArea* mdiArea;
};

int main(int argc, char* argv[])
{
    QApplication app(argc, argv);
    MainWindow1 mainWindow;
    mainWindow.show();
    return app.exec();
}

The crash occurs when I try to invoke plusBtn() after the window is added to the QMdiArea. Sometimes the application crashes with Segmentation Fault, and other times I get the message: QMetaObject::invokeMethod: No such method QQuickPinchHandler::plusBtn()

What I tried: I checked that QQuickWidget is properly initialized, and setSource(QUrl("qrc:/Map.qml")) loads the QML file without issues. The QML file loads correctly, rootObject() is not nullptr, and the method plusBtn() exists in the QML object. The crash occurs only when the widget is added to QMdiArea—if I invoke plusBtn() before adding it, it works fine, but once the widget is inside QMdiArea, calling plusBtn() sometimes causes a segmentation fault.

Debugging Output Before the Crash:

auto rootObj = ui->quickWidget->rootObject();
if (!rootObj) {
    qWarning() << "Root object is null!";
    return;
}

qDebug() << "Root object type:" << rootObj->metaObject()->className();
for (int i = 0; i < rootObj->metaObject()->methodCount(); ++i) {
    qDebug() << "Method:" << rootObj->metaObject()->method(i).methodSignature();
}

Output:

Root object type: Map_QMLTYPE_0  
Method: "plusBtn()"  

I would appreciate any insights on why this might be happening and how I can resolve it.


Solution

  • When connecting your signal:

    auto rootObj = quickWidget->rootObject();
    // Create a button to invoke the QML method
    QPushButton* button = new QPushButton("Call QML Method");
    connect(button, &QPushButton::pressed, this, [=]() {
        if (!rootObj) {
            qWarning() << "Root object is null!";
            return;
        }
        QMetaObject::invokeMethod(rootObj, "plusBtn");
    });
    

    you are making a risky assumption that the pointer to root object obtained in your QmlWindow constructor will stay valid until the button is pressed and your lambda is invoked. Apparently that's not the case. I didn't find any line in documentation that mentions it explicitly, but rootObject call "Returns the view's root item." and I wouldn't rely on the "view" to never be altered.

    Instead obtain the rootObject ptr directly in your callback, especially considering that you keep QQuickWidget* as your class field anyway:

    QPushButton* button = new QPushButton("Call QML Method");
    connect(button, &QPushButton::pressed, this, [this]() {
        auto rootObj = quickWidget->rootObject();
        if (!rootObj) {
            qWarning() << "Root object is null!";
            return;
        }
        QMetaObject::invokeMethod(rootObj, "plusBtn");
    });