1

I have a GridView in QML ApplicationWindow which should be filled with some Items.

I place my items with JS function "placeItems". But the problem is that when Component.onCreated signal of ApplicationWindow is called the GridView is not yet layouted. For example, the GridView has x coordinate equal to -425 in Component.onCreated of ApplicationWindow. If I call the same function a second later - everything is ok and GridView has correct coordinates (=75).

I've check the Qt reference back and forth and haven't found other signals (something like onLayouted or onLayoutComplete) that may be helpful. The question is when to call "placeItems" so the GridView in ApplicationWindow already has correct coordinates?

UPDATE1: To observe the bad behaviour just click File->Start after the application started. It will place the item in the correct place.

import QtQuick 2.2
import QtQuick.Window 2.1
import QtQuick.Controls 1.1

ApplicationWindow {
    id: mainWindow

    width:1000
    height: 900
    color : "white"
    visible: true
    flags: Qt.Window

    function max (a,b) { return a>b ? a : b; }
    function min (a,b) { return a<b ? a : b; }

    property int sizeMin: width < height ? width : height

    property int dimField: sizeMin - 50
    property int dimCellSpacing: 3
    property int dimCell: (dimField / 5 ) - 1 - dimCellSpacing

    GridView {
        id: field
        anchors.centerIn: parent
        model: 20

        width: dimField
        height: dimField

        cellWidth: dimCell
        cellHeight: dimCell

        delegate: cell

        property var items: []

        function centerCell(column,row) {
            return {x: field.x + (column + 0.5) * cellWidth,
                y: field.y + (row + 0.5) * cellHeight}
        }

        function placeItem(name, col, row) {
            var c = centerCell(col,row)
            items[name].centerX = c.x
            items[name].centerY = c.y
        }

        function placeItems() {
            placeItem ("a", 3, 3)
            //placeItem ("b", 4, 4)
        }

    }

    Component.onCompleted: field.placeItems()

    Component {
        id: cell

        Rectangle {
            id: rectCell

            width: dimCell
            height: dimCell
            color: "lightgray"

            border.width: 3
            border.color: "brown"
        }
    }

    Rectangle
    {
        id: rectItemA

        property int dimItem: 100
        property int centerX: 0
        property int centerY: 0
        property int margin: 5
        property var cell: field.items["a"] = this
        border.color: "black"
        border.width: 3

        width: dimItem
        height: dimItem

        x: centerX - width/2
        y: centerY - height/2

        color: "red"
        opacity: 0.5
    }

    menuBar: MenuBar {
        Menu {
            title: qsTr("File")
            MenuItem {
                text: qsTr("Start")
                onTriggered: field.placeItems();
            }
            MenuItem {
                text: qsTr("Exit")
                onTriggered: Qt.quit();
            }
        }
    }
}
outmind
  • 759
  • 1
  • 10
  • 30
  • Hi, welcome to StackOverflow. You're much more likely to get help if you can include the code that is causing the problem you're describing. – arco444 Aug 18 '14 at 13:56
  • If you include your code like @arco444 mentions, we may also be able to suggest alternative approaches that may eliminate the need to depend on the order of completion (which is undefined, like ddriver has already said). – Mitch Aug 18 '14 at 14:33
  • I'll try to reduce my code to a small enough example – outmind Aug 18 '14 at 19:46
  • The code is still pretty huge. – outmind Aug 18 '14 at 20:04
  • Is there any specific reason that prevents using parent/child relation to solve the placement problem (I mean the rectangle you're trying to place inside a cel being actually its child)? Anyhow, see my answer for a solution. – W.B. Aug 18 '14 at 20:41
  • Ahahaha guys! Just came up with the stupidest hack ever : ) As I want function to be called after x and y parameters are set up I've just add a new dummy property: `property int dummy: x + y + placeItems()` So now placeItems is called only after x and y are set. Its funny, but its still just a hack. – outmind Aug 18 '14 at 20:48
  • The worst part is that placeItems is called trice now. As I understand each time x/y are changed. – outmind Aug 18 '14 at 20:54
  • @LeonidDworzanski have you looked at my solution? Qt/Qml has mechanisms in place (signals/slots/property bindings) that don't require such silly workarounds (at least in this instance). Many Times that I thought Qml was lacking some trivial functionality, it turned out that it was me that was approaching the problem from the backside. – W.B. Aug 18 '14 at 20:55

3 Answers3

1
    function placeItem(name, col, row) {
        items[name].anchors.horizontalCenter = field.left;
        items[name].anchors.verticalCenter = field.top;
        items[name].anchors.horizontalCenterOffset = (col + 0.5) * cellWidth;
        items[name].anchors.verticalCenterOffset = (row + 0.5) * cellHeight;
    }

The key is to anchor the element in the grid view and then move it according to your calculations.

BTW, you know that QML has built in functions Math.min/Math.max?

EDIT

Or better yet, why not define the bindings in rectItemA directly?

W.B.
  • 5,445
  • 19
  • 29
  • Ha! Nice hint! You mean to get rid of absolute coordinate in favor of dependencies... Thank you! I think that it is the answer. It is really different way, but even more elegant! I'm still wondering what is the event model of Components in QML. Do you know are there any events other than onCompleted? I've checked the doc myself, but no luck. – outmind Aug 18 '14 at 20:59
  • @LeonidDworzanski every property has an `onChanged` signal. So whenever a property changes, the signal gets emitted. Mind the capitalisation of `PropertyName` - if you have a property named `someProp`, the signal would be `onSomePropChanged`. – W.B. Aug 18 '14 at 21:01
  • Thank you, W.B.! Are there something like "onPropagated/onLayoutComplete" events? – outmind Aug 18 '14 at 21:04
  • Never heard of it. I don't think the symptom you're seeing is related to the items not being laid out when your function is being called. It is most likely related to your app window animations performed by your OS. So the items are laid out fine, but at that point the window is most likely sized 0px/0px. It's therefore much better to 'hook' into on screen position/dimensions of the item. After all that's what you were doing in `CenterCell` function, but instead of binding to those properties, you were copying their values. – W.B. Aug 18 '14 at 21:08
  • I've added trace points with console.log. The x coordinate of field in onCompleted = -425. The sizes and x of mainWindows are ok (mainWindow.x==527 and after I moved it a bit mw.x==238 so these coordinates are real). When I click on File->Start field.x == 75. – outmind Aug 18 '14 at 21:22
  • @LeonidDworzanski I still don't get it why you're clinging so tightly to that idea of knowing when items are laid out. That's not what QML is about. It's about signals, slots and property bindings. However, if you really must know, then why don't you assign a function to field.onXChanged signal and test, whether it is in the middle of its parent. – W.B. Aug 18 '14 at 21:43
  • @LeonidDworzanski - using absolute coordinates is almost always a bad idea. It is just not how modern UI works. – dtech Aug 18 '14 at 22:00
  • @W.B. Just tried to obtain complete understanding of this system. – outmind Aug 19 '14 at 01:01
  • @LeonidDworzanski that's fine by me, didn't mean anything by it. – W.B. Aug 19 '14 at 10:57
  • @W.B. I constantly receive "Cannot anchor to an item that isn't a parent or sibling." with this code... And if I reparent this objects then when I drag item they are overlapped by cells created later. So I can move figures only up and left. – outmind Aug 19 '14 at 22:49
  • @LeonidDworzanski you must have changed the code. It was working, I tested it. Let's move it to a chat - show me your code there. – W.B. Aug 20 '14 at 07:19
0

Why don't you just delay the placeItems function so it runs with a tiny delay so that when it runs the "static" components are all completed.

Timer {
        interval: 250 // might want to tune that
        repeat: false
        onTriggered: placeItems()
    }

In a perfect world, Component.onCompleted would nest perfectly and the root item would be the last one to be emitted. But unfortunately Qt does not guarantee the order, and indeed as I did a quick test, the parent item emits (or at least responds to) onCompleted BEFORE the child items.

And if you don't want to pollute your QML file with the timer, you can actually instantiate a "helper object" from a JS function dynamically, set it to do its work and then delete itself when done. Similar to the "reparent helper" I outlined in this answer: Better way to reparent visual items in QML but rather delete itself on the timer timeout rather than in the JS function which would not give it the time to trigger.

Community
  • 1
  • 1
dtech
  • 47,916
  • 17
  • 112
  • 190
  • Thank you, ddriver! But this solution is really a hack. I'll use it of course, if there are no options. But still has this irritating feeling that this can be solved in "a correct way". I'll vote up your solution when I'll get my reputation. – outmind Aug 18 '14 at 20:15
  • You are new to Qt, it is a very good and vast framework, which most likely put you in the same trap I once fell, believing it would provide me with everything I needed to do. Unfortunately, there are many aspects in which you don't have control over Qt, internal private implementations which simply don't work the way you need them to. You are then faced with 2 options - either reinvent the wheel or use a few hacks to overcome the design limitations of Qt, and in your case the lack of order in nested component `onCompleted` is just that - a design limitation. – dtech Aug 18 '14 at 20:22
  • BTW I've already used this hack several times, you may find it useful when you have a lot of interconnected C++/QML functionality that requires "initialization" of some objects, used to bridge C++ and QML, the QML factory needs to be given time to instantiate everything before it can be accessed from C++ sort of a "chicken and egg" initialization paradox. In other words, you may find other potential uses for delayed invocation as you run into other problems down the Qt line. – dtech Aug 18 '14 at 22:18
0

Another, less hackish way to have the right behavior (don't play with Timer with layout, really, it's a bad idea):

You are defining your Rectangle as an item centered in a instance of a item belonging to your GridView. So, I use a little of your way (getting an item at the r row and the c column in the gridview), and then I reparent the Rectangle to this item. To make it centered, it is only needed to anchor it to the center of its newly bound parent.

import QtQuick 2.2
import QtQuick.Window 2.1
import QtQuick.Controls 1.1

ApplicationWindow {
    id: mainWindow

    width:1000
    height: 900
    color : "white"
    visible: true
    flags: Qt.Window

    property int sizeMin: Math.min(width, height)

    property int dimField: sizeMin - 50
    property int dimCellSpacing: 3
    property int dimCell: (dimField / 5 ) - 1 - dimCellSpacing

    GridView {
        id: field
        anchors.centerIn: parent
        model: 20

        width: dimField
        height: dimField

        cellWidth: dimCell
        cellHeight: dimCell

        delegate: cell

        function cellAt(row, col) {
            return itemAt(row * (dimCell + dimCellSpacing), col * (dimCell + dimCellSpacing));
        }
    }

    Component {
        id: cell

        Rectangle {
            id: rectCell

            width: dimCell
            height: dimCell
            color: "lightgray"

            border.width: 3
            border.color: "brown"
        }
    }

    Rectangle
    {
        id: rectItemA

        property int dimItem: 100
        property int margin: 5
        border.color: "black"
        border.width: 3

        width: dimItem
        height: dimItem

        anchors.centerIn: parent

        color: "red"
        opacity: 0.5
    }

    Component.onCompleted: {
        rectItemA.parent = field.cellAt(3, 3);
    }

    menuBar: MenuBar {
        Menu {
            title: qsTr("File")
            MenuItem {
                text: qsTr("Exit")
                onTriggered: Qt.quit();
            }
        }
    }
}
epsilon
  • 2,849
  • 16
  • 23
  • I don't know why, but in your solution red rectangle just disappeared. – outmind Aug 18 '14 at 21:54
  • @LeonidDworzanski: well spotted. I post the wrong code. Should be fixed. By the way, to be able to call `GridView::itemAt`, the component should be completed. It makes sense as all its children need to be sized before being able to localize them on their coordinate – epsilon Aug 18 '14 at 21:58
  • I've fixed it with `GridView {... property var items : []}` but it looks like the W.B. answer, doesn't it? He just used other anchors? – outmind Aug 18 '14 at 22:06
  • Just another way. I just use a binding on parent for the centering, so you don't have to manage code to make the calculation for that part. You don't need to maintain an array of items, associating string key to (row, column) pair. Yet this is the same idea. I just feel my way is more declarative. – epsilon Aug 18 '14 at 22:14