Share equally the horizontal space in a QML Row - qt

I need to share equally the horizontal space between all "buttons" in my Row.
I use this code with a Repeater.
Component {
id: buttonComponent
Rectangle {
height: buttonRow.height
width: buttonRow.width / buttonsRepeater.count
color: "#FFDDDD"
Text {
anchors.centerIn: parent
text: model.text
}
}
}
Rectangle {
color: "#DDDDDD"
id: buttonBar
height: 30
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
}
Row {
id: buttonRow
anchors.fill: parent
Repeater {
id: buttonsRepeater
model: buttonsModel
delegate: buttonComponent
}
}
}
Now, I like to compute the ideal width of the Row such that all my button texts appear correctly.
How can I get this ideal width?

If you don't want to use QtQuick.Layouts as they are not really ready yet, you can use this :
Rectangle {
id: buttonBar;
color: "#DDDDDD";
height: 30;
width: (buttonColumn.width + 20 + buttonRow.spacing) * buttonsRepeater.count;
anchors {
bottom: parent.bottom;
left: parent.left;
}
Column {
id: buttonColumn;
visible: false;
Repeater {
model: buttonsModel;
delegate: Text {
text: model.text;
}
}
}
Row {
id: buttonRow;
anchors.fill: parent;
property real itemWidth : ((width + spacing) / buttonsRepeater.count) - spacing;
Repeater {
id: buttonsRepeater;
model: buttonsModel;
delegate: Component {
id: buttonDelegate;
Rectangle {
height: parent.height;
width: parent.itemWidth;
color: "#FFDDDD";
border.width: 1;
Text {
anchors.centerIn: parent;
text: model.text;
}
}
}
}
}
}
I just used a hidden Column to easily compute max width of Text elements, and added a little padding in the bar width to avoid unspaced text.

The minimum width of a button itself is the implicitWidth property of its Text element.
One solution to your problem might be to add code in the Component.onCompleted handler, i.e. code that is executed after the repeater has created its items, and then sum up these implicitWidth properties of each of the repeater's item (which you can get by using its itemAt(index) function).
These kinds of dynamic layout is a bit cumbersome in QML still, which will get much better in Qt 5.1 with the introduction of Qt Quick Layouts

Related

QML ListView: Binding loop detected for property "height"

I have a QML ListView, and I'm trying to dynamically add elements to it. I want the background rectangle to also scale dynamically as elements are added/removed from the ListView. Right now I get a binding loop, and I understand what they are but I can't figure out where it's coming from. I played around changing the code a bit and I was able to get rid of the binding loop one time but then the ListView couldn't be scrolled. Anyone have any ideas?
import QtQuick 2.15
import QtQuick.Window 2.0
Window {
visible: true
width: 800
height: 800
Rectangle {
id: listContainer
height: childrenRect.height
width: parent.width
color: "transparent"
anchors {
top: parent.top
topMargin: 30
left: parent.left
leftMargin: 45
}
ListView {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
model: myModel
height: childrenRect.height
header:
Text {
z: 2
height: 50
text: "HEADER"
color: "black"
}
delegate: Component {
Item {
Text {
id: userName;
text: name;
color: "black";
font.pixelSize: 50
anchors {
left: parent.left
leftMargin: 20
}
}
Rectangle {
height: 1
color: 'black'
width: listContainer.width
anchors {
left: userName.left
top: userName.top
topMargin: - 12
leftMargin: -15
}
}
}
}
spacing: 80
}
}
ListModel {
id: myModel
}
/* Fill the model with default values on startup */
Component.onCompleted: {
for (var i = 0; i < 100; i++) {
myModel.append({
name: "Big Animal : " + i
})
}
}
}
EDIT: As suggested by #Aditya, the binding loop can be removed by having a static ListView height, but I don't want it to be that way. I'm using the rectangle as a background for the ListView and I want it to scale according to the ListView. For example, if I only add two elements, I want the rectangle to also scale for those two elements and not cover the entire screen. This causes a problem:
import QtQuick 2.15
import QtQuick.Window 2.0
Window {
visible: true
width: 800
height: 800
Rectangle {
id: listContainer
height: childrenRect.height
width: parent.width
color: "yellow"
anchors {
top: parent.top
topMargin: 30
left: parent.left
leftMargin: 45
}
ListView {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
model: myModel
height: 800//childrenRect.height
header:
Text {
z: 2
height: 50
text: "HEADER"
color: "black"
}
delegate: Component {
Item {
Text {
id: userName;
text: name;
color: "black";
font.pixelSize: 50
anchors {
left: parent.left
leftMargin: 20
}
}
Rectangle {
height: 1
color: 'black'
width: listContainer.width
anchors {
left: userName.left
top: userName.top
topMargin: - 12
leftMargin: -15
}
}
}
}
spacing: 80
}
}
ListModel {
id: myModel
}
/* Fill the model with default values on startup */
Component.onCompleted: {
for (var i = 0; i < 2; i++) {
myModel.append({
name: "Big Animal : " + i
})
}
}
}
I also tried separating the header from ListView into a different component and anchoring the listview below it and that worked. The only problem was it could not be scrolled with the listview. Worst case, I could make a scrolling animation for it but that seems like an inefficient solution and I'd like to know why this doesn't work.
You are probably also biting yourself with the Item as the top-level in the delegate, since that doesn't give any implicit size, which the ListView uses to calculate the scrolling needs. You can simply use Text directly as the delegate (you don't need the Component either) and put the line/rectangle inside. If doing so you can use the contentHeight property of ListView to size the background.
Furthermore, I would suggest to have the ListView as the top level and do any styling secondary, with which I mean, put the background Rectangle inside.
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
ListView {
id: listView
model: 3
anchors.fill: parent
Rectangle { //background
color: "yellow"
z: -1
width: listView.width
height: listView.contentHeight
}
delegate: Text {
text: "name" + index
color: "black";
font.pixelSize: 50
leftPadding: 20
Rectangle {
height: 1
color: 'black'
width: listView.width
y: - 12
x: -15
}
}
spacing: 80
}
}
Btw, if you are going to put the ListView in some RowLayout or something, you probably also want implicitHeight: contentHeight in the ListView.
The binding loop is originating from the ListView's height: childrenRect.height statement. It looks like the ListView needs to be a fixed height, or at least not dependent on childrenRect. It is most likely how the ListView element knows that the view should be scrollable to view elements below.
It really depends on what you're trying to achieve with setting the height to match childrenRect, but in my case, ListView height is changing based on the children (per your desire presumably). With a 100 items the height came out to be 7970. With 5 items in the model, the result was 350. You can check this by adding a debug or console.log() with onHeightChanged However, as a result of this scaling, the ListView is assumed to be big enough to view the entire data set regardless of the window parent container size.
You do not need to scale the ListView height to match the contents; that is what it is built for. It allows scrolling because the contents are too big to be shown within its limited height.
I was able to achieve get rid of the binding loop and be able to scroll by simply changing the statement to a static value, which is the parent height of 800 as an example:
Window {
visible: true
width: 800
height: 800
Rectangle {
id: listContainer
height: childrenRect.height
width: parent.width
color: "transparent"
anchors {
top: parent.top
topMargin: 30
left: parent.left
leftMargin: 45
}
ListView {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
model: myModel
height: 800//childrenRect.height
header:
Text {
z: 2
height: 50
text: "HEADER"
color: "black"
}
delegate: Component {
Item {
Text {
id: userName;
text: name;
color: "black";
font.pixelSize: 50
anchors {
left: parent.left
leftMargin: 20
}
}
Rectangle {
height: 1
color: 'black'
width: listContainer.width
anchors {
left: userName.left
top: userName.top
topMargin: - 12
leftMargin: -15
}
}
}
}
spacing: 80
}
}
ListModel {
id: myModel
}
/* Fill the model with default values on startup */
Component.onCompleted: {
for (var i = 0; i < 100; i++) {
myModel.append({
name: "Big Animal : " + i
})
}
}
}
Edit:
I feel like you're trying to just secure a background for a scalable ListView. Having a static background as a container works but not very well for modern unser interfaces - any bounce effects or such will not move the rectangle. You could achieve this by anchoring the rectangle to the ListView element but it is a very roundabout way. Instead, you could just set a rectangle to style each element of the ListView delegate instead.
delegate: Component {
Item {
Rectangle{
width: listContainer.width
height: userName.height+13
//add 13 to adjust for margin set below
anchors {
left: userName.left
top: userName.top
topMargin: - 12
leftMargin: -15
//just copying from the other rectangle below
}
gradient: Gradient {
//I am just using gradient here for a better understanding of spacing. You could use color.
GradientStop { position: 0.0; color: "aqua" }
GradientStop { position: 1.0; color: "green" }
}
}
Text {
id: userName;
text: name;
color: "black";
font.pixelSize: 50
anchors {
left: parent.left
leftMargin: 20
}
}
Rectangle {
height: 1
color: 'black'
width: listContainer.width
anchors {
left: userName.left
top: userName.top
topMargin: - 12
leftMargin: -15
}
}
}
}
This will make sure that the rectangle background behind the ListView will look like it is scrolling with the items. In reality we have broken one rectangle into multiple and just set each element with one. You can also use this type of styling to achieve alternate colors in your list for example.

How to use toolseparator inside a repeater in qml?

Toolseparator is not working inside repeater. I need to show the image and text under that one line after each content, so to show the line i have used tool separator but it's not working, it just override all the content by showing only lines. Here is the example
Repeater {
model: 10
Row {
leftPadding: 10
spacing: 10
Rectangle {
height: 100
width: 200
}
Text {
text: "Username"
}
ToolSeparator {
height: 25
width: 335
orientation: Qt.Horizontal
}
}
}
You've done few mistakes, my friend.
If you are going to use Repeater -- typically you should use it inside of some of the positioning components (e.g. Column, Row). So in your case, Repeater should be inside Row. By the way, never neglect to review official Qt documentation: Repeater Detailed Description; Using QML Positioner and Repeater Items. In addition to this there is great learning source QmlBook, and for example its Quick Starter.
If you want to show your components one under another -- you should use Column component, and not Row.
If I understand you correctly you are going to get something like this:
Use this code as a reference:
Column {
spacing: 10
Repeater {
model: 4
Rectangle {
id: background
height: 50
width: 200
color: "bisque"
Text {
id: text
color: "chocolate"
text: "Username " + modelData
anchors.centerIn: parent
}
Rectangle {
id: separator
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
}
height: 5
color: "goldenrod"
}
}
}
}

Scroll two or more List views in QML

I need to scroll two or more list view at once using a single scrollBar. Initially, i used Column inside a Flickable but scroll was not happening as expected. Later, I used ListView and even that was not scrolling correctly.
So how to scroll a listview/layout content item with a scroll bar? Should I use ScrollView or Flickable or something else?
The stock scrollbar will only hook to a single scrollable item. However, it is trivial to make a custom scroller and hook multiple views to it:
Row {
Flickable {
width: 50
height: main.height
contentHeight: contentItem.childrenRect.height
interactive: false
contentY: (contentHeight - height) * scroller.position
Column {
spacing: 5
Repeater {
model: 20
delegate: Rectangle {
width: 50
height: 50
color: "red"
Text {
anchors.centerIn: parent
text: index
}
}
}
}
}
Flickable {
width: 50
height: main.height
contentHeight: contentItem.childrenRect.height
interactive: false
contentY: (contentHeight - height) * scroller.position
Column {
spacing: 5
Repeater {
model: 30
delegate: Rectangle {
width: 50
height: 50
color: "cyan"
Text {
anchors.centerIn: parent
text: index
}
}
}
}
}
Rectangle {
id: scroller
width: 50
height: 50
color: "grey"
property real position: y / (main.height - 50)
MouseArea {
anchors.fill: parent
drag.target: parent
drag.minimumY: 0
drag.maximumY: main.height - 50
drag.axis: Drag.YAxis
}
}
}
Note that it will work adequately even if the the views are of different content height, scrolling each view relative to the scroller position:
Realizing the question was not put that well, just in case someone wants to actually scroll multiple views at the same time comes around, I will nonetheless share another interesting approach similar to a jog wheel, something that can go indefinitely in every direction rather than having a limited range like a scrollbar. This solution will scroll the two views in sync until they hit the extent of their ranges. Unlike GrecKo's answer, this never leaves you with an "empty view" when the view size is different:
Row {
Flickable {
id: f1
width: 50
height: main.height
contentHeight: contentItem.childrenRect.height
interactive: false
Connections {
target: jogger
onScroll: f1.contentY = Math.max(0, Math.min(f1.contentHeight - f1.height, f1.contentY + p))
}
Column {
spacing: 5
Repeater {
model: 20
delegate: Rectangle {
width: 50
height: 50
color: "red"
Text {
anchors.centerIn: parent
text: index
}
}
}
}
}
Flickable {
id: f2
width: 50
height: main.height
contentHeight: contentItem.childrenRect.height
interactive: false
Connections {
target: jogger
onScroll: f2.contentY = Math.max(0, Math.min(f2.contentHeight - f2.height, f2.contentY + p))
}
Column {
spacing: 5
Repeater {
model: 30
delegate: Rectangle {
width: 50
height: 50
color: "cyan"
Text {
anchors.centerIn: parent
text: index
}
}
}
}
}
MouseArea {
id: jogger
width: 50
height: main.height
drag.target: knob
drag.minimumY: 0
drag.maximumY: main.height - 50
drag.axis: Drag.YAxis
signal scroll(real p)
property real dy: 0
onPressed: dy = mouseY
onPositionChanged: {
scroll(dy - mouseY)
dy = mouseY
}
onScroll: console.log(p)
Rectangle {
anchors.fill: parent
color: "lightgrey"
}
Rectangle {
id: knob
visible: parent.pressed
width: 50
height: 50
color: "grey"
y: Math.max(0, Math.min(parent.mouseY - 25, parent.height - height))
}
}
}
Another advantage the "jog" approach has it is it not relative but absolute. That means if your view is huge, if you use a scroller even a single pixel may result in a big shift in content, whereas the jog, working in absolute mode, will always scroll the same amount of pixels regardless the content size, which is handy where precision is required.
You could just use a Flickable with your Columns.
I don't know how your Columns are laid out horizontally but if they are inside a Row it's pretty straightforward:
import QtQuick 2.7
import QtQuick.Controls 2.0
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Multi Column")
Flickable {
anchors.fill: parent
contentWidth: row.implicitWidth
contentHeight: row.implicitHeight
Row {
id: row
Column {
spacing: 5
Repeater {
model: 20
delegate: Rectangle {
width: 50
height: 50
color: "red"
Text {
anchors.centerIn: parent
text: index
}
}
}
}
Column {
spacing: 5
Repeater {
model: 30
delegate: Rectangle {
width: 50
height: 50
color: "cyan"
Text {
anchors.centerIn: parent
text: index
}
}
}
}
}
ScrollBar.vertical: ScrollBar { }
}
}
Even if they are not in a Row you could do :
contentHeight: Math.max(column1.height, column2.height, ...)
Demonstration :

How can I expand a list in a Toolbar in QML when the user tap the title of the it?

I want to perform the expansion action in the ToolBar when the user taps on the title of it, as in the pictures that I attach.
When the user touches the title of the toolbar then you should see a list of the filters that you can apply.
You have any ideas on how to implement this action in QML?
Pretty easy. Copy the following in the awesome QML web editor:
import QtQuick 2.0
Column {
width: 500
Rectangle {
id: toolbar
width: parent.width
height: 50
Text {
text: "Elenco"
anchors.centerIn: parent
font.pointSize: 24; font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: listBox.visible = !listBox.visible
}
}
Rectangle {
id: listBox
color: "gray"
width: parent.width
visible: false
height: 200
Column {
width: parent.width
Repeater {
model: 4
delegate:
Rectangle {
width: parent.width
color: index % 2 ? "#C9D6DE" : "#E7F6FF"
height: 50
Text { anchors.centerIn: parent; text: "Persona " + (index + 1) }
}
}
}
}
}

ListView dynamic anchoring

Let us suppose I have a card made using Rectangle and I want to show buttons on top of it when clicked. I'm calling showMenu() function to do that and for buttons I'm using an ListView with dynamic ListModel. The problem with such is that the button gets added bellow the Rectangle instead of the top of it. The anchor is not updating after appending an item to the model. Here is my code
Item {
width: 120
height: 120
Rectangle {
id: card
width: 50
height: 100
color: "pink"
anchors.bottom: parent.bottom
Item {
id: rec
width: 50
anchors.bottom: parent.top // This anchor is not updating after appending an item to the list.
ListModel {
id: menuListModel
}
Component {
id: delegate
Rectangle {
width: 120
height: 20
color: "blue"
Text {
anchors.fill: parent
anchors.centerIn: parent
text: commandText
}
}
}
ListView {
anchors.fill: parent
model:menuListModel
delegate: delegate
interactive: false
}
}
MouseArea {
anchors.fill: parent
onClicked: menuListModel.append({"commandText" : "Normal Summon"});
}
}
}
This is more or less a duplicate of this question. The Item needs a height. As mentioned in the answer to that question, you can add debug statements to the code when things like this happen. In this situation, you can also add a Rectangle as a child of the Item and make sure that it's visible:
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: "darkorange"
}
If it's not visible, you know that the problem lies with that (parent) item.

Resources