QML Repeater and QML Grid Layout with variable number of columns - qt

I have an model that consist of objects with some properties, e.g.:
ListModel {
id: fruitModel
ListElement {
name: "Apple"
color: "green"
cost: 2.45
}
ListElement {
name: "Orange"
color: "orange"
cost: 3.25
}
ListElement {
name: "Banana"
color: "yellow"
cost: 1.95
}
}
And now I want to display this model using GridLayout. For each property I want to use one element inside GridLayout, e.g:
GridLayout {
columns: 3
Text { text: "Apple" }
Rectangle { color: "green" }
SpinBox { value: 2.45 }
Text { text: "Orange" }
Rectangle { color: "orange" }
SpinBox { value: 3.25 }
Text { text: "Banana" }
Rectangle { color: "yellow" }
SpinBox { value: 1.95 }
}
The point is that I can easily change the columns property of GridLayout and make my layout narrower (e.g. to fit small screens). I can use Repeater to fill GridLayout. However such approach will fill my GridLayout in wrong order:
GridLayout {
columns: 3
Repeater {
model: fruitModel
Text { text: name }
}
Repeater {
model: fruitModel
Rectangle { color: color }
}
Repeater {
model: fruitModel
SpinBox { value: value }
}
}
And it is waste to use Layout.column and Layout.row attached properties since I want to easily change number of columns in GridLayout.
Is there any way to fill GridLayout with data from model row by row?
UPD1:
Behaviour that I want to get:
GridLayout {
columns: parent.width > 235 ? 3 : 1
Text { text: "Apple" }
Rectangle { color: "green"; width: 40; height: 40 }
SpinBox { value: 2 }
Text { text: "Orange" }
Rectangle { color: "orange"; width: 40; height: 40 }
SpinBox { value: 3 }
Text { text: "Banana" }
Rectangle { color: "yellow"; width: 40; height: 40 }
SpinBox { value: 1 }
}
UPD2:
Modified variant from #m7913d:
GridLayout {
id: layout
property int maxColumns: 3
columns: parent.width > 235 ? maxColumns : 1
Repeater {
model: fruitModel
Text {
text: name
Layout.row: layout.columns == maxColumns ? index : (maxColumns * index)
Layout.column: 0
}
}
Repeater {
model: fruitModel
Rectangle {
Layout.preferredWidth: 30
Layout.preferredHeight: 30
color: col
Layout.row: layout.columns == maxColumns ? index : (maxColumns * index + 1)
Layout.column: layout.columns == maxColumns ? 1 : 0
}
}
Repeater {
model: fruitModel
SpinBox {
value: val
Layout.row: layout.columns == maxColumns ? index : (maxColumns * index + 2)
Layout.column: layout.columns == maxColumns ? 2 : 0
}
}
}
It's working but not easy-to-modify solution plus sometimes during layout resize there is messages QGridLayoutEngine::addItem: Cell (1, 0) already taken.

I would use a Column filled by either Rows or Columns (or any other arrangement) as delegates.
Column {
id: rootCol
anchors.fill: parent
Repeater {
model: fruitModel
delegate: rootCol.width > 300 ? rowDel : colDel
}
Component {
id: rowDel
Row {
Text { width: 100; height: 50; text: model.name }
Rectangle { width: 50; height: 50; color: model.color }
SpinBox { width: 150; height: 50; value: model.cost }
}
}
Component {
id: colDel
Column {
Text { width: 100; height: 50; text: model.name }
Rectangle { width: 50; height: 50; color: model.color }
SpinBox { width: 150; height: 50; value: model.cost}
}
}
}
Or maybe:
Column {
id: rootCol
anchors.fill: parent
Repeater {
model: fruitModel
delegate: Flow {
anchors {
left: parent.left
right: parent.right
}
Text { width: 100; height: 50; text: model.name }
Rectangle { width: 50; height: 50; color: model.color }
SpinBox { width: 150; height: 50; value: model.value }
}
}
}

Here is yet another solution:
The Idea: add the children in the right order
For that we will need to make sure that all Items for a model entry.
GridLayout {
id: gl
columns: parent.width > 235 ? 3 : 1
}
Instantiator {
model: fruitModel
delegate: QtObject {
property Item text: Text { parent: gl; text: model.name }
property Item rect: Rectangle { parent: gl; color: model.color; width: 50; height: 50; }
property Item spin: SpinBox { parent: gl; value: model.cost; }
}
}
Note: This will fail when entries in the model will be inserted or reordered, as in this solution the entries are always appended.
Other than that, it will automatically support column changes

A possible approach is to extend this method with condition column/row indexes:
GridLayout {
id:gridLayout
columns: parent.width > 235 ? 3 : 1
Repeater {
model: fruitModel
implicitHeight: 50
Text {
text: name
Layout.row: gridLayout.columns == 3 ? index : 3 * index
Layout.column: 0
}
}
Repeater {
model: fruitModel
Rectangle {
color: model.color
implicitWidth: 50
implicitHeight: 50
Layout.row: gridLayout.columns == 3 ? index : 3 * index + 1
Layout.column: gridLayout.columns == 3 ? 1 : 0
}
}
Repeater {
model: fruitModel
SpinBox {
value: value
Layout.row: gridLayout.columns == 3 ? index : 3 * index + 2
Layout.column: gridLayout.columns == 3 ? 2 : 0
}
}
}

Related

QML - Possible to display 3 listViews in a single view

Is it possible to display multiple listViews in a single page?
I have 3 seperate list views and I'd like to display them on a single page and I'm struggling to lay them out. They all overlap each other.
Simple example of the layout:
ListView {
id: listOne
property string headertitle: "list one header"
spacing: 5
header: headerComponent
model: ListOneModel
}
ListView {
id: listTwo
property string headertitle: "list two header"
spacing: 5
header: headerComponent
model: ListTwoModel
}
ListView {
id: listThree
property string headertitle: "list three header"
spacing: 5
header: headerComponent
model: ListThreeModel
}
Note that a Column with 3 ListViews in it will give you a rather odd scrolling experience, where you wouldn't be scrolling it all as if it is a single view, but scroll individual list views.
As long as you don't go crazy with the model sizes (and I mean like thousands), you can simply use a Flickable with a Column with 3 repeaters in it. This will give you a single continuous column you could scroll through.
The reason this solution will not be efficient for thousands of model items is that under normal circumstances, list view creates and destroys delegates as needed, and keeps created only what is visible on screen plus some optional caching. Whereas this solution will create all items, in order to get one uniform column to scroll through.
Here is a quick example demonstrating the 2 distinct behaviors:
Row {
anchors.fill: parent
Flickable {
width: parent.width * .5
height: parent.height
flickableDirection: Flickable.VerticalFlick
contentHeight: contentItem.childrenRect.height
Column {
spacing: 2
Repeater {
model: 5
delegate: Rectangle { width: 100; height: 100; color: "red" }
}
Repeater {
model: 5
delegate: Rectangle { width: 100; height: 100; color: "green" }
}
Repeater {
model: 5
delegate: Rectangle { width: 100; height: 100; color: "blue" }
}
}
}
Column {
width: parent.width * .5
height: parent.height
spacing: 2
ListView {
spacing: 2
width: parent.width
height: parent.height / 3
model: 5
clip: true
delegate: Rectangle { width: 100; height: 100; color: "red" }
}
ListView {
spacing: 2
width: parent.width
height: parent.height / 3
model: 5
clip: true
delegate: Rectangle { width: 100; height: 100; color: "green" }
}
ListView {
spacing: 2
width: parent.width
height: parent.height / 3
model: 5
clip: true
delegate: Rectangle { width: 100; height: 100; color: "blue" }
}
}
}
You can use Column:
import QtQuick 2.0
Rectangle {
width: 180; height: 200
id: root
Column {
ListView {
width: root.width; height: root.height/3
model: myModel
delegate: Text {
text: name + ": " + number
}
}
ListView {
width: root.width; height: root.height/3
model: myModel
delegate: Text {
text: name + ": " + number
}
}
ListView {
width: root.width; height: root.height/3
model: myModel
delegate: Text {
text: name + ": " + number
}
}
}
ListModel {
id: myModel
ListElement {
name: "Bill Smith"
number: "555 3264"
}
ListElement {
name: "John Brown"
number: "555 8426"
}
ListElement {
name: "Sam Wise"
number: "555 0473"
}
}
}

QML ListView, How to make list elements to not overlap the header, when scrolling

I have ListView with a header delegate enabled. I have a header positioning property set to "OverlayHeader". The header will stay in place when scrolling through the elements. However, the elements will overlap the header. Is there a way to prevent this.
Example of list elements overlapping the header.
import QtQuick 2.5
Rectangle {
width: 360; height: 600
ListView {
width: 350; height: 200
anchors.centerIn: parent
id: myList
model: myModel
highlight: highlightBar
clip: true
snapMode: ListView.SnapToItem
headerPositioning: ListView.OverlayHeader
header: Rectangle {
id: headerItem
width: myList.width
height:50
color: "blue"
Text {
text: "HEADER"
color: "red"
}
}
delegate: Item {
id: delegateItem
width: 400; height: 20
clip: true
Text { text: name
}
MouseArea {
id: mArea
anchors.fill: parent
onClicked: { myList.currentIndex = index; }
}
}
}
Component {
id: highlightBar
Rectangle {
width: parent.width; height: 20
color: "#FFFF88"
}
}
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});
}
}
}
The header's default z value is 1, so you can set it to a higher value to ensure that it's over the delegates:
import QtQuick 2.5
Rectangle {
width: 360
height: 600
ListView {
width: 350
height: 200
anchors.centerIn: parent
id: myList
model: myModel
highlight: highlightBar
clip: true
snapMode: ListView.SnapToItem
headerPositioning: ListView.OverlayHeader
header: Rectangle {
id: headerItem
width: myList.width
height: 50
z: 2
color: "blue"
Text {
text: "HEADER"
color: "red"
}
}
delegate: Item {
id: delegateItem
width: 400
height: 20
Text {
text: name
}
MouseArea {
id: mArea
anchors.fill: parent
onClicked: {
myList.currentIndex = index
}
}
}
}
Component {
id: highlightBar
Rectangle {
width: parent.width
height: 20
color: "#FFFF88"
}
}
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
})
}
}
}
Note that clipping view delegates is bad for performance.

How to access the property of a GridView - or ListView - element

Here is the code, I create 4 buttons. When one is clicked I wanna that its color changes to red and the color of all the others change to black.
But looks like I could not access the color property.
Rectangle {
id: root
width: 200; height: 100
DelegateModel {
id: visualModel
model: ListModel {
ListElement { my_color: "red" }
ListElement { my_color: "black" }
ListElement { my_color: "black" }
ListElement { my_color: "black" }
}
groups: [
DelegateModelGroup { name: "selected" }
]
delegate: Rectangle {
id: item
height: 25
width: 200
color:my_color
MouseArea {
anchors.fill: parent
onClicked: {
console.log(visualModel.items.get(index).color)
for (var i = 0; i < root.count; i++){
if(index == i)
visualModel.items.get(i).color = "red";
else
visualModel.items.get(i).color = "black";
}
}
}
}
}
ListView {
anchors.fill: parent
model: visualModel
}
}
I advice you to use ExclusiveGroup from QML controls. Usually it is used for Action but it's possible to use it for any other Item. From the Qt docs:
It is possible to add support for ExclusiveGroup for an object or
control. It should have a checked property, and either a
checkedChanged, toggled(), or toggled(bool) signal.
So all we need is to add suitable property. Small example:
import QtQuick 2.5
import QtQuick.Window 2.0
import QtQuick.Controls 1.4
Window {
width: 200
height: 400
ExclusiveGroup { id: exclusiveGroup }
ListView {
anchors.fill: parent
anchors.margins: 5
spacing: 2
model: 10
delegate: Rectangle {
id: myItem
property bool checked: false // <-- this is necessary
height: 30
width: parent.width
color: myItem.checked ? "lightblue" : "#DEDEDE"
border { width: 1; color: "#999" }
radius: 5
Text { text: "item" + (index + 1); anchors.centerIn: parent}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: myItem.checked = !myItem.checked;
}
Component.onCompleted: {
exclusiveGroup.bindCheckable(myItem);
}
}
}
}

Is there easy way to duplicate a RowLayout in QtQuick?

In my ui.qml I have:
RowLayout {
id: rowLayout2
x: 0
y: 397
width: 640
height: 50
clip: false
Text {
id: text4
width: 105
height: 32
text: qsTr("房间类型")
font.pixelSize: 17
wrapMode: Text.WordWrap
}
ComboBox {
id: comboBox1
activeFocusOnPress: false
model: ListModel {
id: cbItems
ListElement { text: "标准间"; color: "Yellow" }
ListElement { text: "三人间"; color: "Green" }
ListElement { text: "大床房"; color: "Brown" }
ListElement { text: "豪华套房"; color: "Blue" }
}
}
}
And I want to make a button that when clicked on duplicates this RowLayout below the original one, how do I do that?
Put it inside a Repeater and increment the model count when the button is clicked.
Button {
onClicked: {
repeater.model += 1;
}
}
...
Column {
Repeater {
model: 1
// Your rowLayout2 code
}
}

QML: How to move items within a grid

I have a 4x4 grid and I want to associate arrow key presses with the movement of items within the grid. How does one do that?
Here is a sample QML:
import QtQuick 1.1
Rectangle {
id: main;
width: 500; height: 500;
color: "darkgreen";
property int emptyBlock: 16;
Grid {
id: grid16;
x: 5; y: 5;
width: 490; height: 490;
rows: 4; columns: 4; spacing: 5;
Repeater {
model: 1;
Rectangle {
width: 118; height: 118; color: "darkblue";
}
}
}
Keys.onRightPressed: pressRight();
function pressRight() {
console.log("Left key pressed");
}
focus: true;
}
Update 1: Thanks to sebasgo and alexisdm for the answers. If moving within a grid is not that easy why we have the move transition property [http://qt-project.org/doc/qt-4.8/qml-grid.html#move-prop]
You'd better use a GridView Item instead of your Grid approach.
This way you can use it's currentIndex property to choose which item to move like this:
import QtQuick 1.1
Rectangle {
id: main;
width: 500; height: 500;
color: "darkgreen";
property int emptyBlock: 16;
GridView {
id: grid16;
x: 5; y: 5;
width: 490; height: 490;
model: gridModel
delegate: Component{
Rectangle {
width: 118; height: 118; color: "darkblue";
Text {
anchors.centerIn: parent
font.pixelSize: 20
text: value
}
}
}
}
ListModel {
id: gridModel
ListElement {value: 1}
ListElement {value: 2}
ListElement {value: 3}
ListElement {value: 4}
}
Keys.onRightPressed: {
gridModel.move(grid16.currentIndex, grid16.currentIndex+1, 1)
}
Keys.onLeftPressed: {
gridModel.move(grid16.currentIndex, grid16.currentIndex-1, 1)
}
focus: true;
}
Grids give you no way to manipulate the position of the contained items directly. Instead their position is directly derived from the physically order of the child items of the grid. There is no easy way to to manipulate child items in QML dynamically, so I think you should abandon the Grid item and specify the position of the child items explicitly with the x and y properties. Applied to your code this could look like:
Rectangle {
id: main;
width: 500; height: 500;
color: "darkgreen";
Item {
x: 5; y: 5;
width: 490; height: 490;
Repeater {
id: pieces
model: 1;
Rectangle {
property int column: 0
property int row: 0
x: column * 123
y: row * 123
width: 118; height: 118; color: "darkblue";
}
}
}
Keys.onRightPressed: pressRight();
function pressRight() {
console.log("Left key pressed");
pieces.itemAt(0).column++
}
focus: true;
}
Update 1:
Grids (in combination with a Repeater) can be used to visualize models, e.g., a XmlListModel item or an QAbstractItemModel descendent.
With move property it's easy to react to layout changes in the model (if an entry is removed/added) in an animated way. Still, the items in the Grid are laid out strictly in the order of the entries of the model.
So if you want have manual control over the position of your items, even in cellular layout, use of a Grid is not advisable.
You can change the number of items before the item you want to move to change its position:
import QtQuick 1.1
Rectangle {
id: main;
width: 500; height: 500;
color: "darkgreen";
property int emptyBlock: 16;
property int posX: 0;
property int posY: 0;
Grid {
id: grid;
x: 5; y: 5;
width: 490; height: 490;
rows: 4; columns: 4; spacing: 5;
Repeater {
id: beforeTheItem
model: main.posX + parent.columns * main.posY
Rectangle {
width: 118; height: 118; color: "transparent";
}
}
Rectangle {
id:theItem
width: 118; height: 118; color: "darkblue";
}
}
Keys.onPressed: {
// To avoid flickering, the item is hidden before the change
// and made visible again after
theItem.visible = false;
switch(event.key) {
case Qt.Key_Left: if(posX > 0) posX--;
break;
case Qt.Key_Right: if(posX < grid.columns - 1) posX++;
break;
case Qt.Key_Up: if(posY > 0) posY--;
break;
case Qt.Key_Down: if(posY < grid.rows - 1) posY++;
break;
}
theItem.visible = true;
}
focus: true;
}
Now, by using Qt 5.1 or above and GridLayout you can move your items without hassle:
import QtQuick 2.4
import QtQuick.Window 2.2
import QtQuick.Layouts 1.1
Window
{
visible: true
MainForm
{
GridLayout {
id: gridLayout
columns: 3
property int oneRow: 0
property int oneCol: 0
Text { id: one; Layout.row :grid.oneRow; Layout.column: grid.oneCol; text: "My"; font.bold: true; }
Text { text: "name"; color: "red" }
Text { text: "is"; font.underline: true }
Text { text: "not"; font.pixelSize: 20 }
Text { text: "Ravan"; font.strikeout: true }
}
Component.onCompleted:
{
gridLayout.oneRow = 1
gridLayout.oneCol = 2
}
}
}
The GridView is a very confusing monster. It just populates one row from a given model, which leads to confusion since it is called GRID. But it can still be used as a fixed size grid, as I show in the example below. A single square can be moved with arrow keys on a 4x4 sized grid.
GridView {
id: grid16;
anchors.fill: parent
cellWidth: parent.width / 2
cellHeight: parent.height / 2
model: gridModel
delegate:
Rectangle {
Component.onCompleted: if( index >= 1 ) visible = false
width: grid16.cellWidth ; height: grid16.cellHeight ; color: "yellow";
Text {
anchors.centerIn: parent
font.pixelSize: 20
text: value
}
}
move: Transition {
NumberAnimation { properties: "x,y"; duration: 1000 }
}
}
ListModel {
id: gridModel
ListElement {value: 1}
//Necessary, otherwise the grid will have the dimension 1x1
ListElement {value: 2}
ListElement {value: 3}
ListElement {value: 4}
}
Keys.onRightPressed: { gridModel.move(grid16.currentIndex, grid16.currentIndex+1, 1) }
Keys.onLeftPressed: { gridModel.move(grid16.currentIndex, grid16.currentIndex-1, 1) }
Keys.onUpPressed: { gridModel.move(grid16.currentIndex, grid16.currentIndex-2, 1) }
Keys.onDownPressed: { gridModel.move(grid16.currentIndex, grid16.currentIndex+2, 1) }
focus: true;
}

Resources