How to bind to a property of a repeater-generated item outside of the repeater? - qt

I would like to be able to bind to a property of an item generated by Repeater to do something with it, e.g. to show its coordinates. For that purpose I am using itemAt() like this:
ListModel {
id: modelNodes
ListElement { name: "Banana"; x: 100; y: 200 }
ListElement { name: "Orange"; x: 150; y: 100 }
}
Repeater {
id: foo
model: modelNodes
Rectangle {
x: model.x; y: model.y
width: textBox.implicitWidth + 20
height: textBox.implicitHeight + 20
color: "red"
Drag.active: true
Text {
id: textBox
anchors.centerIn: parent
color: "white"
text: model.name + ": " + foo.itemAt(index).x
}
MouseArea {
anchors.fill: parent
drag.target: parent
}
}
}
Text {
id: moo
Binding {
target: moo
property: "text"
value: foo.itemAt(0).x + " -> " + foo.itemAt(1).x
}
}
Inside the delegate this works fine, but when I attempt to use it outside of the Repeater (i.e. to bind moo's text to it), I get the following error:
TypeError: Cannot read property 'x' of null
How to fix this?

The reason the Binding object doesn't work outside of the Repeater is because the Repeater has not constructed its items yet when the binding is being evaluated. To fix this, you can move the binding into the Component.onCompleted handler. Then just use the Qt.binding() function to do binding from javascript (docs).
Text {
Component.onCompleted: {
text = Qt.binding(function() { return foo.itemAt(0).x + ", " + foo.itemAt(1).x })
}
}

You don't.
(or more precisely, you shouldn't)
Delegates shouldn't store state or data, just display it or be able to interact with it.
In your case what you are after is the data stored in the model.
Your solution should be to modify your model in your delegates and get the data from your model if you want.
I've created a small example of what I mean:
import QtQuick 2.15
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
Window {
visible: true
width: 800
height: 640
ListModel {
id: modelNodes
ListElement { name: "Banana"; x: 50; y: 50 }
ListElement { name: "Orange"; x: 50; y: 100 }
}
Row {
anchors.centerIn: parent
spacing: 1
Repeater {
model: 2 // display 2 copy of the delegates for demonstration purposes
Rectangle {
color: "transparent"
width: 300
height: 300
border.width: 1
Repeater {
id: foo
model: modelNodes
Rectangle {
x: model.x; y: model.y
width: textBox.implicitWidth + 20
height: textBox.implicitHeight + 20
color: "red"
DragHandler {
dragThreshold: 0
}
onXChanged: model.x = x // modify model data when dragging
onYChanged: model.y = y
Text {
id: textBox
anchors.centerIn: parent
color: "white"
text: model.name + ": " + foo.itemAt(index).x
}
}
}
}
}
}
Instantiator {
model: modelNodes
delegate: Binding { // the hacky solution to the initial problem.
target: myText
property: model.name.toLowerCase() + "Point"
value: Qt.point(model.x, model.y)
}
}
Text {
id: myText
property point bananaPoint
property point orangePoint
anchors.right: parent.right
text: JSON.stringify(bananaPoint)
}
ListView {
anchors.fill: parent
model: modelNodes
delegate: Text {
text: `${model.name} - (${model.x} - ${model.y})`
}
}
}
I've used a hacky solution to your initial problem with an Instantiator of Bindings, I don't really understand the usecase so that might not be the ideal solution. Here it creates a binding for every element of your model but that's weird. If you only want data from your first row, you may want to do when: index === 0 in the Binding. I've created a third party library to get a cleaner code : https://github.com/okcerg/qmlmodelhelper
This will result in the following code for your outside Text (and allowing you to get rid of the weird Instantiator + Binding part):
Text {
readonly property var firstRowData: modelNodes.ModelHelper.map(0)
text: firstRowData.x + ", " + firstRowData.y
}
Note that my point about not storing data in delegates (or accessing them from outside) still stands for whatever solution you chose.

Related

QML: Show object over non siblings

I have a somehow very hard to solve problem in my QML code. I will summarize it the best I can since it is very long code..
I write a color picker qml file that is called when the user wants to pick a color. This is done in a big rectangle with little rectangles in it evenly distributed that have flat colors to choose from.
I have a parent rectangle, 1 outer repeater and nested in this repeater is another inner repeater that creates little rectangle in a row. The outer repeater places the inner repeaters under another so it fills the rectangle with little rectangles fully, preferably with different colors.
Every little rectangle also has a function that highlights itself with an animation. This animation is a circle that gets bigger than the rectangle itself. This is done so when the user clicks a color from e.g. a color history on the right, it should highlight the corresponding colors rectangle if is there.
Now, the problem:
No matter what z values I use, this animation won't show above the other rectangles. It will get blocked by neighboring rectangles. I have researched and it seems that z values don't account for non siblings, just for all items in a single parent.
Here's some code that leaves out all the unnecessary junk.
To note is that every rectangle has its own animation and mousearea.
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
visible: true
color: 'black'
width: 640
height: 480
title: qsTr("Hello World")
Rectangle {
id: parentRectangle
width: 400
height: 400
property int numberOfBoxesInARow: 5
property int numberOfBoxesInAColumn: 5
Repeater {
id: outerRepeater
model: parentRectangle.numberOfBoxesInARow
Repeater {
id: innerRepeater
model: parentRectangle.numberOfBoxesInAColumn
y: parentRectangle.height / parentRectangle.numberOfBoxesInAColumn * outerIndex
x: 0
height: parent.height / parentRectangle.numberOfBoxesInAColumn
width: parent.width
property int outerIndex: index
Rectangle {
id: individualRectangle
color: Qt.rgba(1 / (outerIndex + 1), 0, 1 / (index + 1), 1)
x: parentRectangle.width / parentRectangle.numberOfBoxesInARow * index
y: outerIndex * parentRectangle.height / parentRectangle.numberOfBoxesInAColumn
width: parentRectangle.width / parentRectangle.numberOfBoxesInARow
height: parent.height / parentRectangle.numberOfBoxesInAColumn
Component.onCompleted: {
console.log("Rectangle at " + outerIndex + "|" + index + " created, width / height: " + width.toFixed(2) + "| " + height.toFixed(2))
}
MouseArea {
anchors.fill: parent
onClicked: {
highlightAnimation.running = true
}
}
Rectangle {
id: highlightCircle
visible: highlightAnimation.running
color: 'white'
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
property real size: 0
width: size
height: size
radius: size/2
}
PropertyAnimation {
id: highlightAnimation
target: highlightCircle
property: 'size'
from: 200
to: 0
duration: 500
}
}
}
}
}
}
Ok, to paint an item over another one you have at least 2 ways:
z (for siblings items only)
creating order (the last created is the highest)
I guess that the second way is preferable for you. So you just need to create the circle item after all others. For example:
Repeater {
id: outerRepeater
Repeater {
id: innerRepeater
...
MouseArea {
anchors.fill: parent
onClicked: {
highlightCircle.item = individualRectangle;
highlightAnimation.running = true;
}
}
}
}
}
Rectangle {
id: highlightCircle
property var item
...
anchors.horizontalCenter: item ? item.horizontalCenter : undefined
anchors.verticalCenter: item ? item.verticalCenter : undefined
}
PropertyAnimation {
id: highlightAnimation
...
}

Binding text from a TextField that belongs to a Repeater

My main.qml:
Window
{
visible: true
width: 640
height: 480
color: "grey"
GridLayout
{
anchors.fill: parent
columns : 2
rows : 2
Repeater
{
id: rectRepeater
model: 3
TextField
{
text: "hi"
}
}
}
Rectangle
{
id: r1
width: 100
height: 100
x: 200
y: 200
border.color: "red"
Text
{
id: t1
}
}
Component.onCompleted:
{
t1.text= rectRepeater.itemAt(0).text
}
}
The Text in the rectangle r1 displays the text at the start, but if I enter new text to the TextField, the rectangle will not be updated. How can I solve this?
A more elegant and maintainable solution is to implement a model that reflects the changes, and then make a binding of the first element with the text that shows Text:
Window{
visible: true
width: 640
height: 480
color: "grey"
ListModel{
id: mymodel
}
Component.onCompleted: {
for(var i=0; i<3; i++){
mymodel.append({"text" : "hi"})
}
}
GridLayout{
anchors.fill: parent
columns : 2
rows : 2
Repeater{
model: mymodel
TextField{
id: tf
onTextChanged: model.text = tf.text
Component.onCompleted: tf.text= model.text
}
}
}
Rectangle{
id: r1
width: 100
height: 100
x: 200
y: 200
border.color: "red"
Text {
id: t1
text: mymodel.count > 1 ? mymodel.get(0).text : ""
}
}
}
What you want, is to create a binding between the two.
Component.onCompleted:
{
t1.text = Qt.binding(function() { return rectRepeater.itemAt(0).text })
}
That being said, we would need to know exactly what you are trying to do, because creating bindings manually is an anti-pattern when not required. It is much better to bind directly, or to use signals.
Do you need the first elements, and the repeater, or is this just an test for you? What is your UI and what are you trying to achieve? This is some context worth giving for a proper answer.
One possible simpler solution
Repeater
{
id: rectRepeater
model: 3
TextField
{
text: "hi"
// See also `onEditingFinished` and `onValidated`
onTextChanged: {
if (index == 0)
t1.text = text
}
}
}
For more details about the property thing, look at my answers from your other question: Qml Repeater with ids

Adaptable UI in QML

I am developing a QML application where I have three main views. My code looks like below.
SplitView{
ListView{
id: firstView
}
ListView{
id: secondView
}
WebView{
id: thirdView
}
}
Now this is working fine, but I would like to do this: when my main window is resized below a certain width (500) I would like to show only one view, so that clicking on the delegate will show the next view (with the possibility of going back to the previous view). So for example clicking on the first view will show the second view and clicking on the second view will show the third view. The approach I want is very similar to the Mail application in Windows 10.
Does anyone know how can this be achieved in QML?
OK, I've cooked up a crude example, I didn't modularize it deliberately, so you can see how it works in a single source. Also, I don't know how the windows 10 mail application does it, since I don't have it, but it still close enough to your description.
You begin with 3 list views in a row, which are sized to fill the entire UI, but if you reduce the UI size to the minimum of 500, the views will increase in size to fill almost the entire ui, and when you click on a view item, it will move you to the next view, and if you click the showing previous view, you will be returned back to it.
ApplicationWindow {
title: qsTr("Hello World")
width: 800
height: 300
minimumWidth: 500
visible: true
Item {
id: adapt
width: parent.width
height: parent.height
property int sizeUnit: width > 500 ? width / 5 : 400
property bool isPaged: width == 500 ? true : false
onIsPagedChanged: { if (!isPaged) { x = 0; page = 0; } }
property int page: 0
Behavior on x { NumberAnimation { duration: 250; easing.type: Easing.OutBack } }
ListModel {
id: mod
ListElement { name: "one" }
ListElement { name: "two" }
ListElement { name: "three" }
ListElement { name: "four" }
ListElement { name: "five" }
}
ListView {
id: v1
width: adapt.sizeUnit
height: parent.height
model: mod
delegate: Rectangle {
height: 70
width: v1.width
color: "red"
border.color: "black"
Text { anchors.centerIn: parent; text: name }
MouseArea {
anchors.fill: parent
onClicked: {
if (adapt.isPaged) {
if (adapt.page == 0) {
adapt.x = -(v2.x - 100)
adapt.page = 1
} else {
adapt.x = 0
adapt.page = 0
}
}
}
}
}
}
ListView {
id: v2
width: adapt.sizeUnit
height: parent.height
x: adapt.sizeUnit + 10
model: mod
delegate: Rectangle {
height: 70
width: v2.width
color: "cyan"
border.color: "black"
Text { anchors.centerIn: parent; text: name }
MouseArea {
anchors.fill: parent
onClicked: {
if (adapt.isPaged) {
if (adapt.page == 1) {
adapt.x = -(v3.x - 100)
adapt.page = 2
} else {
adapt.x = -(v2.x - 100)
adapt.page = 1
}
}
}
}
}
}
ListView {
id: v3
width: adapt.isPaged ? adapt.sizeUnit : 3 * adapt.sizeUnit - 20
height: parent.height
x: v2.x + v2.width + 10
model: mod
delegate: Rectangle {
height: 70
width: v3.width
color: "yellow"
border.color: "black"
Text { anchors.centerIn: parent; text: name }
}
}
}
}
This should be enough to get you going. Obviously, for production you can go for more elegant layouting and nagivation, such as use a function for the actual "page sliding" and anchors, the above is just for the purpose of example.
Here is my idea of how to solve the problem.
// When width of MainWindow is smaller than 500 stop using SplitView
property bool useSplitView: (width < 500 ? false : true)
onUseSplitViewChanged: {
if (useSplitView) {
firstView.anchors.fill = undefined
secondView.anchors.fill = undefined
thirdView.anchors.fill = undefined
firstView.parent = splitView // splitView is id of SplitView
secondView.parent = splitView
thirdView.parent = splitView
}
else {
firstView.parent = mainWindow.contentItem // mainWindow is id of MainWindow
secondView.parent = mainWindow.contentItem
thirdView.parent = mainWindow.contentItem
secondView.visible = false
thirdView.visible = false
firstView.anchors.fill = firstView.parent
secondView.anchors.fill = secondView.parent
thirdView.anchors.fill = thirdView.parent
}
}
When useSplitView flag changes you need to enable some kind of touch area on views and switch between them on click.

Move items between ListModels

Assume a QML application with two list models, each holding, say 3, items. Graphically the two lists are displayed in two containers. I'd like to implement a drag-and-drop functionality to reorder the items inside their list and to swap two items between different lists.
Reordering in one list poses no problem due to the move method of the ListModel. For swapping items between the two list models, however, I think I have to use the remove and insert or set operations of ListModel. First experiments using set didn't work.
What is the canonical way to achieve this that also works with transitions? (E.g. just swapping the items' roles is no solution, I will have to swap the items themselves.)
You can just use ListModel.get() to fetch an element from one view and ListModel.append() or ListModel.insert() to put it in another one (not exactly with same type)
The simple example:
import QtQuick 2.4
import QtQuick.Window 2.2
import QtQuick.Layouts 1.1
import QtQuick.Controls 1.4
Window {
width: 400
height: 400
visible: true
Component.onCompleted: {
fillModel(model1);
fillModel(model2);
}
Component {
id: delegate
Rectangle {
width: parent.width
property int itemIndex: index
property var view: ListView.view
color: (itemIndex === view.currentIndex) ? "orange" : "white"
height: 20
Text { text: name; color: moved ? "red" : "black"; anchors.centerIn: parent }
MouseArea {
anchors.fill: parent
onClicked: {
view.currentIndex = itemIndex;
}
}
}
}
RowLayout {
anchors.fill: parent
ListView {
id: list1
Layout.fillHeight: true
Layout.fillWidth: true
model: ListModel { id: model1 }
delegate: delegate
}
ColumnLayout {
Layout.alignment: Qt.AlignCenter
width: 50
Button { text: ">>>"; onClicked: moveItem(list1,list2) }
Button { text: "<<<"; onClicked: moveItem(list2,list1) }
}
ListView {
id: list2
Layout.fillHeight: true
Layout.fillWidth: true
model: ListModel { id: model2 }
delegate: delegate
add: Transition {
id: list2Transition
enabled: false
property int fromX
property int fromY
ParallelAnimation {
NumberAnimation { properties: "x"; from: list2Transition.fromX; duration: 300; }
NumberAnimation { properties: "y"; from: list2Transition.fromY; duration: 300;easing.type: Easing.OutCirc; }
PropertyAnimation {property: "color"; from: "red"; to: "white"; duration: 500 }
}
}
}
}
function fillModel(model) {
for(var i = 0;i < 15;i ++)
model.append({name: "item" + i, moved: false});
}
function moveItem(listfrom,listto) {
var item = listfrom.model.get(listfrom.currentIndex);
var newPos1 = listfrom.parent.mapFromItem(listfrom,0,listfrom.currentIndex * 20);
var newPos2 = listto.parent.mapFromItem(listto,0,listto.currentIndex * 20);
list2Transition.fromX = newPos1.x - newPos2.x;
list2Transition.fromY = newPos1.y;
list2Transition.enabled = true;
item.moved = true;
listto.model.insert(listto.currentIndex, item);
listfrom.model.remove(listfrom.currentIndex, 1);
}
}
You can also do the same action by using drag & drop functionality.

How to limit the size of drop-down of a ComboBox in QML

I am using a ComboBox in QML and when populated with a lot of data it exceeds my main windows bottom boarder. From googling I have learned that the drop-down list of a ComboBox is put on top of the current application window and therefore it does not respect its boundaries.
Ideally I would want the ComboBox to never exceed the main applications boundary, but I can not find any property in the documentation.
A different approach would be to limit the number of visible items of the drop-down list so that it do not exceed the window limits for a given window geometry. I was not able to find this in the documentation either and I have run out of ideas.
Take a look to the ComboBox source code, the popup is of a Menu type and it doesn't have any property to limit its size. Moreover, the z property of the Menu is infinite, i.e. it's always on top.
If you Find no way but to use the ComboBox of Qt you can create two models one for visual purpose, I will call it visual model, you will show it in your ComboBox and the complete one , it will be the reference model. Items count in your VisualModel wil be equal to some int property maximumComboBoxItemsCount that you declare . you'll need o find a way that onHovered find the index under the mouse in the visualmodel if it's === to maximumComboBoxIemsCount you do visualModel.remove(0) et visualModel.add(referenceModel.get(maximum.. + 1) and you'll need another property minimumComboBoxIemsCount, same logic but for Scroll Up , I dont know if it will work. but it's an idea
I think there is no solution using the built-in component and you should create your own comboBox. You can start from the following code.
ComboBox.qml
import QtQuick 2.0
Item {
id: comboBox
property string initialText
property int maxHeight
property int selectedItem:0
property variant listModel
signal expanded
signal closed
// signal sgnSelectedChoice(var choice)
width: 100
height: 40
ComboBoxButton {
id: comboBoxButton
width: comboBox.width
height: 40
borderColor: "#fff"
radius: 10
margin: 5
borderWidth: 2
text: initialText
textSize: 12
onClicked: {
if (listView.height == 0)
{
listView.height = Math.min(maxHeight, listModel.count*comboBoxButton.height)
comboBox.expanded()
source = "qrc:/Images/iconUp.png"
}
else
{
listView.height = 0
comboBox.closed()
source = "qrc:/Images/iconDown.png"
}
}
}
Component {
id: comboBoxDelegate
Rectangle {
id: delegateRectangle
width: comboBoxButton.width
height: comboBoxButton.height
color: "#00000000"
radius: comboBoxButton.radius
border.width: comboBoxButton.borderWidth
border.color: comboBoxButton.borderColor
Text {
color: index == listView.currentIndex ? "#ffff00" : "#ffffff"
anchors.centerIn: parent
anchors.margins: 3
font.pixelSize: 12
text: value
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: {
listView.height = 0
listView.currentIndex = index
comboBox.selectedItem = index
tools.writePersistence(index,5)
comboBoxButton.text = value
comboBox.closed()
}
}
}
}
ListView {
id: listView
anchors.top: comboBoxButton.bottom
anchors.left: comboBoxButton.left
width: parent.width
height: 0
clip: true
model: listModel
delegate: comboBoxDelegate
currentIndex: selectedItem
}
onClosed: comboBoxButton.source = "qrc:/Images/iconDown.png"
Component.onCompleted: {
var cacheChoice = tools.getPersistence(5);
listView.currentIndex = tools.toInt(cacheChoice)
selectedItem = listView.currentIndex
comboBoxButton.text = cacheModel.get(selectedItem).value
}
}
ComboBoxButton.qml
import QtQuick 2.0
Item {
id: container
signal clicked
property string text
property alias source : iconDownUp.source
property string color: "#ffffff"
property int textSize: 12
property string borderColor: "#00000000"
property int borderWidth: 0
property int radius: 0
property int margin: 0
Rectangle {
id: buttonRectangle
anchors.fill: parent
color: "#00000000"
radius: container.radius
border.width: container.borderWidth
border.color: container.borderColor
Image {
id: image
anchors.fill: parent
source: "qrc:/Images/buttonBackground.png"
Image {
id: iconDownUp
source: "qrc:/Images/iconDown.png"
sourceSize.height:20
sourceSize.width: 20
anchors.verticalCenter: parent.verticalCenter
}
}
Text {
id:label
color: container.color
anchors.centerIn: parent
font.pixelSize: 10
text: container.text
font.bold: true
}
MouseArea {
id: mouseArea;
anchors.fill: parent
onClicked: {
container.clicked()
buttonRectangle.state = "pressed"
startTimer.start()
}
}
Timer{
id:startTimer
interval: 200
running: false;
repeat: false
onTriggered: buttonRectangle.state = ""
}
states: State {
name: "pressed"
when: mouseArea.pressed
PropertyChanges { target: image; scale: 0.7 }
PropertyChanges { target: label; scale: 0.7 }
}
transitions: Transition {
NumberAnimation { properties: "scale"; duration: 200; easing.type: Easing.InOutQuad }
}
}
}
I've used it in some software of mine, hence it is possible that It could not work "out of the box". I use it like this:
ComboBox{
id:cacheChoice
initialText: "None"
anchors.top: baseContainer.top
anchors.topMargin: 2
anchors.right: baseContainer.right
maxHeight: 500
listModel: cacheModel
onExpanded: {
cacheChoice.height = 500
}
onClosed: {
cacheChoice.height = 20
}
}
In case you are working with ComboBox from Qt Quick Controls 2, here's the source code for it:
https://github.com/qt/qtquickcontrols2/blob/5.12/src/imports/controls/ComboBox.qml
Based on that, this override of the behavior works to limit the height to something reasonable:
myComboBox.popup.contentItem.implicitHeight = Qt.binding(function () {
return Math.min(250, myComboBox.popup.contentItem.contentHeight);
});
It is possible to access the hidden MenuStyle within the ComboBoxStyle component. There you can use all the things and hidden things you have within a MenuStyle, including its maximum height.
The thing looks roughly like this.
Not pretty but it works well enough.
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.3
import QtQuick.Window 2.2
ComboBox {
id: comboBox
style: ComboBoxStyle {
// drop-down customization here
property Component __dropDownStyle: MenuStyle {
__maxPopupHeight: 400
__menuItemType: "comboboxitem" //not 100% sure if this is needed
}
}
As it came up resonantly in our team, here is a updated version of the idea shown above. The new version restricts the size automatically to the size of your application.
ComboBox {
id: root
style: ComboBoxStyle {
id: comboBoxStyle
// drop-down customization here
property Component __dropDownStyle: MenuStyle {
__maxPopupHeight: Math.max(55, //min value to keep it to a functional size even if it would not look nice
Math.min(400,
//limit the max size so the menu is inside the application bounds
comboBoxStyle.control.Window.height
- mapFromItem(comboBoxStyle.control, 0,0).y
- comboBoxStyle.control.height))
__menuItemType: "comboboxitem" //not 100% sure if this is needed
} //Component __dropDownStyle: MenuStyle
} //style: ComboBoxStyle
} //ComboBox

Resources