QML Dialog is broken?

4.3k views Asked by At

I have this code:

import QtQuick 2.3
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.1
import QtQuick.Controls 1.2

Dialog {
    standardButtons: StandardButton.Ok | StandardButton.Cancel

    width: layout.implicitWidth
    height: layout.implicitHeight

    RowLayout {
        id: layout
        anchors.fill: parent

        Item {
            width: 10
            height: 1
        }

        GridLayout {
            columns: 2
            rowSpacing: 10

            Layout.fillHeight: true
            Layout.fillWidth: true

            Text {
                text: "Hello world? "
            }
            Text {
                text: "Hello world!"
            }

            Text {
                text: "Goodbye world? "
            }
            Text {
                text: "Goodbye world!"
            }

        }

        Item {
            width: 10
            height: 1
        }
    }
}

When you run it it looks like this, and the dialog can be resized to any size. Also the RowLayout actually doesn't fill its parent as you can see.

damn pls

How can I make it so that the dialog can't be resized below the minimum size of the layout, and so that the layout fills the dialog?

2

There are 2 answers

1
Timmmm On

Unfortunately this is a bug in Qt. Currently the documentation is misleading and Dialog does not size itself correctly to the contents. Consider this working example, which I based on the DefaultFontDialog:

AbstractDialog {
    title: "Hello"
    id: root
//  standardButtons: StandardButton.Ok | StandardButton.Cancel
    modality: Qt.NonModal
    Rectangle {
        id: content
        implicitWidth: mainLayout.implicitWidth + outerSpacing * 2
        implicitHeight: mainLayout.implicitHeight + outerSpacing * 2

        property real spacing: 6
        property real outerSpacing: 12

        color: "white"
        GridLayout {
            id: mainLayout
            anchors { fill: parent; margins: content.outerSpacing }
            rowSpacing: content.spacing
            columnSpacing: content.spacing
            columns: 5

            Text { text: "Hello" } Text { text: "Hello" } Text { text: "Hello"  } Text { text: "Hello" } Text { text: "Hello" }
            Text { text: "Hello" } Text { text: "Hello" } Text { text: "Hello"  } Text { text: "Hello" } Text { text: "Hello" }
            Text { text: "Hello" } Text { text: "Hello" } Text { text: "Hello"  } Text { text: "Hello" } Text { text: "Hello" }
            Text { text: "Hello" } Text { text: "Hello" } Text { text: "Hello"  } Text { text: "Hello" } Text { text: "Hello" }
        }
    }
}

This works exactly as expected, though of course you don't get the buttons.

If you just change it to a Dialog and uncomment the standardButtons, then it stops working - you can resize the dialog to clip its contents (width-wise at least), and the contents do not expand to the dialog size.

The reason for the minimum width not working becomes clear when we look at the source code for Dialog (in qtquickcontrols/src/dialogs/DefaultDialogWrapper.qml):

AbstractDialog {
    id: root
    default property alias data: defaultContentItem.data
    onVisibilityChanged: if (visible && contentItem) contentItem.forceActiveFocus()

    Rectangle {
        id: content
        property real spacing: 6
        property real outerSpacing: 12
        property real buttonsRowImplicitWidth: minimumWidth
        property bool buttonsInSingleRow: defaultContentItem.width >= buttonsRowImplicitWidth
        property real minimumHeight: implicitHeight
        property real minimumWidth: Screen.pixelDensity * 50
        implicitHeight: defaultContentItem.implicitHeight + spacing + outerSpacing * 2 + buttonsRight.implicitHeight
        implicitWidth: Math.min(root.__maximumDimension, Math.max(
            defaultContentItem.implicitWidth, buttonsRowImplicitWidth, Screen.pixelDensity * 50) + outerSpacing * 2);

minimumWidth is hardcoded to Screen.pixelDensity * 50!! There was never any hope that it would match the dialog contents. minimumHeight does work better (though not perfect, I believe because the spacing isn't considered).

I'm not sure why the defaultContentItem does not expand correctly, but anyway. It looks like the only solution at the moment is to use AbstractDialog and implement the buttons and accepted()/rejected()/etc. signals yourself. Bit of a pain.

Edit / Solution

I did some further investigation.

  1. The reason the defaultContentItem doesn't expand is because it's bottom anchor isn't tied to the top of the button row:

    Item {
        id: defaultContentItem
        anchors {
            left: parent.left
            right: parent.right
            top: parent.top
            margins: content.outerSpacing
        }
        implicitHeight: childrenRect.height
    }
    
  2. Minimum sizes just don't work that well with anchor-based layouts. They do with GridLayout-based layouts.

  3. Unfortunately childrenRect has no implicitWidth/Height so we have to actually have the child items go into a ColumnLayout rather than be the ColumnLayout.

...

import QtQuick 2.3
import QtQuick.Controls 1.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.1
import QtQuick.Window 2.2

// A Dialog that resizes properly. The defualt dialog doesn't work very well for this purpose.
AbstractDialog {
    id: root
    default property alias data: defaultContentItem.data
    onVisibilityChanged: if (visible && contentItem) contentItem.forceActiveFocus()

    Rectangle {
        id: content
        property real spacing: 6
        property real outerSpacing: 12
        property real buttonsRowImplicitWidth: minimumWidth
        property bool buttonsInSingleRow: defaultContentItem.width >= buttonsRowImplicitWidth
        property real minimumHeight: implicitHeight
        property real minimumWidth: implicitWidth // Don't hard-code this.
        implicitWidth: Math.min(root.__maximumDimension, Math.max(Screen.pixelDensity * 10, mainLayout.implicitWidth + outerSpacing * 2))
        implicitHeight: Math.min(root.__maximumDimension, Math.max(Screen.pixelDensity * 10, mainLayout.implicitHeight + outerSpacing * 2))
        color: palette.window
        Keys.onPressed: {
            event.accepted = true
            switch (event.key) {
                case Qt.Key_Escape:
                case Qt.Key_Back:
                    reject()
                    break
                case Qt.Key_Enter:
                case Qt.Key_Return:
                    accept()
                    break
                default:
                    event.accepted = false
            }
        }

        SystemPalette { id: palette }

        // We use layouts rather than anchors because there are no minimum widths/heights
        // with the anchor system.
        ColumnLayout {
            id: mainLayout
            anchors { fill: parent; margins: content.outerSpacing }
            spacing: content.spacing

            // We have to embed another item so that children don't go after the buttons.
            ColumnLayout {
                id: defaultContentItem
                Layout.fillWidth: true
                Layout.fillHeight: true
            }

            Flow {
                Layout.fillWidth: true

                id: buttonsLeft
                spacing: content.spacing

                Repeater {
                    id: buttonsLeftRepeater
                    Button {
                        text: (buttonsLeftRepeater.model && buttonsLeftRepeater.model[index] ? buttonsLeftRepeater.model[index].text : index)
                        onClicked: root.click(buttonsLeftRepeater.model[index].standardButton)
                    }
                }

                Button {
                    id: moreButton
                    text: qsTr("Show Details...")
                    visible: false
                }
            }

            Flow {
                Layout.fillWidth: true

                id: buttonsRight
                spacing: content.spacing
                layoutDirection: Qt.RightToLeft

                Repeater {
                    id: buttonsRightRepeater
                    // TODO maybe: insert gaps if the button requires it (destructive buttons only)
                    Button {
                        text: (buttonsRightRepeater.model && buttonsRightRepeater.model[index] ? buttonsRightRepeater.model[index].text : index)
                        onClicked: root.click(buttonsRightRepeater.model[index].standardButton)
                    }
                }
            }
        }
    }
    function setupButtons() {
        buttonsLeftRepeater.model = root.__standardButtonsLeftModel()
        buttonsRightRepeater.model = root.__standardButtonsRightModel()
        if (!buttonsRightRepeater.model || buttonsRightRepeater.model.length < 2)
            return;
        var calcWidth = 0;

        function calculateForButton(i, b) {
            var buttonWidth = b.implicitWidth;
            if (buttonWidth > 0) {
                if (i > 0)
                    buttonWidth += content.spacing
                calcWidth += buttonWidth
            }
        }

        for (var i = 0; i < buttonsRight.visibleChildren.length; ++i)
            calculateForButton(i, buttonsRight.visibleChildren[i])
        content.minimumWidth = calcWidth + content.outerSpacing * 2
        for (i = 0; i < buttonsLeft.visibleChildren.length; ++i)
            calculateForButton(i, buttonsLeft.visibleChildren[i])
        content.buttonsRowImplicitWidth = calcWidth + content.spacing
    }
    onStandardButtonsChanged: setupButtons()
    Component.onCompleted: setupButtons()
}

You have to use it a bit differently to a normal Dialog. Just imagine it is a ColumnLayout (this is a slightly different example to the original question):

ColumnLayoutDialog {
    id: dialog1
    standardButtons: StandardButton.Ok | StandardButton.Cancel

    Text {
        text: "Hello world? "
    }
    Text {
        text: "Hello world!"
    }

    // Spacer.
    Item {
        Layout.fillHeight: true;
    }

    Text {
        text: "Goodbye world? "
    }
    Text {
        text: "Goodbye world!"
    }
}

By the way you could change the ColumnLayout to a GridLayout and expose the columns property if you want. That might make more sense.

A small issue

It turns out a QWindow's minimum width and height only ensure that the dialog isn't actively resized to be less than its content. It doesn't ensure that the dialog is never smaller than its content, because the content can grow after the dialog is created (e.g. extra items added). To workaround this I added this function to my ColumnLayoutDialog:

// The minimumWidth/Height values of content are accessed by the C++ class, but they
// only ensure that the window isn't resized to be smaller than its content. They
// don't ensure that if the content grows the window grows with it.
function ensureMinimumSize()
{
    if (root.width < content.minimumWidth)
        root.width = content.minimumWidth;
    if (root.height < content.minimumHeight)
        root.height = content.minimumHeight;
}

It has to be called manually when you change the dialog contents. Or to do it automatically you can add this to the content rectangle:

    onMinimumHeightChanged: {
        if (root.height < content.minimumHeight)
            root.height = content.minimumHeight;
    }
    onMinimumWidthChanged: {
        if (root.width < content.minimumWidth)
            root.width = content.minimumWidth;
    }
0
pepan On

This is a bug in QT up to version 5.6.0. Most likely the bug number 49058. The code from the question works as expected in QT 5.6.1 and 5.7.0.

A partial workaround for the old versions is to remove the lines

width: layout.implicitWidth
height: layout.implicitHeight

and replace

anchors.fill: parent

with

anchors.right: parent.right
anchors.left: parent.left

The dialog then respects the minimum height and the contents expand horizontally.

Here is also a complete workaround, but it relies on undocumented implementation details of Dialog, so it should be used with caution. It works in 5.5.1, 5.6.0, 5.6.1 and 5.7.0. Note also that the second Item is changed to a red Rectangle to make the behavior more apparent.

import QtQuick 2.3
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.1
import QtQuick.Controls 1.2

Dialog {
    visible: true
    standardButtons: StandardButton.Ok | StandardButton.Cancel

    RowLayout {
        id: layout

        // In the horizontal direction, expansion and shrinking can be achieved with anchors.
        anchors.left: parent.left
        anchors.right: parent.right

        // Used only for guessing the height of the Dialog's standard buttons.
        Button {
            id: hiddenButton
            visible: false
        }

        // Repeats until the relevant parts of the dialog (parent of the parent of the RowLayout)
        // are complete, then overwrites the minimum width and implicit height and stops repeating.
        Timer {
            id: timer
            interval: 50; running: true; repeat: true;
            onTriggered: {
                if(layout.parent.parent) {
                    var lp = layout.parent
                    var lpp = layout.parent.parent
                    lpp.minimumWidth = layout.implicitWidth + 2 * lpp.outerSpacing
                    layout.buttonHeight = 2 * lpp.outerSpacing + hiddenButton.implicitHeight + lpp.spacing
                    lp.implicitHeight = layout.implicitHeight + 2 * lpp.outerSpacing
                    running = false
                }
            }
        }

        // The guessed space needed for the Dialog's buttons.
        property int buttonHeight: 80

        // Expand and shrink vertically when the dialog is resized.
        height: parent.parent ? Math.max(parent.parent.height-buttonHeight, implicitHeight) : implicitHeight

        Item {
            width: 10
            height: 1
        }

        GridLayout {
            columns: 2
            rowSpacing: 10

            Layout.fillHeight: true
            Layout.fillWidth: true

            Text {
                text: "Hello world? "
            }
            Text {
                text: "Hello world!"
            }

            Text {
                text: "Goodbye world? "
            }
            Text {
                text: "Goodbye world!"
            }

        }

        Rectangle {
            Layout.fillHeight: true
            color: 'red'
            width: 10
        }
    }
}