Сбой в QQuickItem destructor/changeListeners при закрытии приложения (Qt 5.6)

У нас есть довольно большое приложение QtQuick с большим количеством модальных диалогов. Все эти модалы разделяют последовательный внешний вид и поведение, а также имеют кнопки leftButtons, rightButtons, содержимое и дополнительные предупреждения. Мы используем следующий базовый класс (PFDialog.qml):

Window {
    property alias content: contentLayout.children
    ColumnLayout {
        id: contentLayout
    }
}

и объявить диалоги следующим образом (main.qml):

Window {
    visible: true
    property var window: PFDialog {
        content: Text { text: "Foobar" }
    }
}

Проблема заключается в том, что при закрытии приложения в дескрипторе QQuickItem происходит segfault. Этот segfault трудно воспроизвести, но вот верный способ сделать это: с визуальной студией в режиме отладки освобожденная память заполняется 0xDDDDDDD с триггерами segfault каждый раз.

Полное приложение можно найти здесь: https://github.com/wesen/testWindowCrash

Сбой происходит в QQuickItem::~QQuickItem:

for (int ii = 0; ii < d->changeListeners.count(); ++ii) {
    QQuickAnchorsPrivate *anchor = d->changeListeners.at(ii).listener->anchorPrivate();
    if (anchor)
        anchor->clearItem(this);
}

Причиной этого является то, что содержимое нашего диалога (элемент Text в приведенном выше примере) является дочерним объектом QObject основного окна, но является визуальным дочерним элементом диалогового окна. При закрытии приложения диалоговое окно сначала уничтожается, и в то время, когда элемент Text удален, диалоговое окно (все еще зарегистрированное как changeListener) устарело.

Теперь мой вопрос:

  • Это ошибка QtQuick? Если диалог отменяет регистрацию как changeListener для своих детей, когда он уничтожается (я думаю, что он должен)
  • наш правильный шаблон property alias content: layout.children, или есть лучший способ сделать это? Это также происходит при объявлении псевдонима свойства по умолчанию.

Для полноты, вот как мы исправим его в нашем приложении. Когда содержимое изменяется, мы возвращаем все элементы в элемент макета. Элегантность, как вы все согласитесь.

function reparentTo(objects, newParent) {
    for (var i = 0; i < objects.length; i++) {
        qmlHelpers.qml_SetQObjectParent(objects[i], newParent)
    }
}
onContentChanged: reparentTo(content, contentLayout)

Ответ 1

У меня была эта проблема много раз, я не думаю, что это ошибка, больше похоже на ограничение дизайна. Чем больше неявного поведения вы получаете, тем меньше у вас контроля, что приводит к неуместным порядкам уничтожения объекта и доступа к оборванным ссылкам.

Существует множество ситуаций, когда это может произойти "по-своему", поскольку вы превышаете границы тривиального "в книге" qml-приложения, но в вашем случае это вы его делаете.

Если вы хотите получить правильное владение, не используйте это:

property var window: PFDialog {
    content: Text { text: "Foobar" }
}

Вместо этого используйте это:

property Window window: dlg // if you need to access it externally
PFDialog {
    id: dlg
    content: Text { text: "Foobar" }
}

Вот хорошая причина, по которой:

property var item : Item {
  Item {
    Component.onCompleted: console.log(parent) // qml: QQuickItem(0x4ed720) - OK
  }
}
// vs
property var item : Item {
  property var i: Item {
    Component.onCompleted: console.log(parent) // qml: null - BAD
  }
}

Ребенок не такой же, как свойство. Свойства все еще собираются, но они не являются родительскими.

Что касается достижения "динамического контента", у меня были хорошие результаты с ObjectModel:

Window { 
    property ObjectModel layout
    ListView {            
        width: contentItem.childrenRect.width // expand to content size
        height: contentItem.childrenRect.height
        model: layout
        interactive: false // don't flick
        orientation: ListView.Vertical
    }
}

Тогда:

PFDialog {
    layout: ObjectModel {
        Text { text: "Foobar" }
        // other stuff
    }
}

Наконец, ради выполнения явных очищений перед закрытием приложения в основном файле QML вы можете реализовать обработчик:

onClosing: {
    if (!canExit) doCleanup()
    close.accepted = true
}

Это гарантирует, что окно не будет уничтожено без предварительной очистки.

Наконец:

- это наш собственный псевдоним свойства: layout.children pattern correct, или есть лучший способ сделать это? Это также происходит при объявлении псевдоним свойства по умолчанию.

В прошлый раз я не заглянул в нее, но это было, по крайней мере, пару лет назад. Было бы неплохо иметь объекты, объявленные как дети, фактически становящиеся детьми какого-либо другого объекта, но в то время это было невозможно, и все еще может и не быть. Таким образом, необходимо немного более подробное решение, включающее объектную модель и представление списка. Если вы исследуете этот вопрос и находите что-то другое, оставьте комментарий, чтобы сообщить мне.

Ответ 2

Я считаю, что вы не можете объявить объект Window в var. В моих тестах SubWindow никогда не открывается и иногда разбивается при запуске.

Окно может быть объявлено внутри элемента или внутри другого окна; в этом случае внутреннее окно автоматически станет "переходным" для внешнего окна См.: http://doc.qt.io/qt-5/qml-qtquick-window-window.html

Измените код следующим образом:

Window {
    visible: true
    PFDialog {
        content: Text { text: "Foobar" }
    }
}