Two-dimensional table with nested scrolling areas in QML - qt

I want to create, in QML, a TV-schedule where the vertical axis is a list of Channels and the horizontal axis is time-based. For example something like
(source: zappware.com)
Initially, I created
a vertical ListView with
model = the list of Channels
delegate = a horizontal ListView
every horizontal ListView has
model = the list of Events
delegate = an Item where the width is proportional to the duration of the Event
So far so good. Only drawback is that the horizontal ListViews scroll one by one while they should scroll together.
So somehow, the contentX property of every horizontal ListView should be bound to the contentX property of the moving/flicking horizontal ListView. Note that this binding is dynamic: when flicking in the first row, all other rows should bind to the contentX of the first row. But this should be changed when flicking in the second row.
Any advice on how this can be done?
I tried a somewhat different approach by
creating a Flickable Item on top of the vertical ListView (with contentWidth the complete time-window).
binding every horizontal ListView to the contentX of this Flickable (this is a static binding)
This resulted in nice synchronous scrolling but I still have some issues
I had to do some tricks to ensure that flicking is only horizontal or vertical but not both
I'm not able anymore to click on individual Events; I guess events are intercepted by the Flickable
I'm also not sure about the memory impact of such a Flickable with a huge contentWidth?
Feedback appreciated!

I'd say have only one vertical list view for the channels. But the channel names only, not the actual programs. Instead of a horizontal view for the programs, you can cram them all together in a single flickable, using the begin time and duration to layout the programs in the flickable by binding their x and width properties to the former.
Then you can bind the channel list view together with the vertical scrolling of the program items, so that you have the programs corresponding to their appropriate channels. This way you can scroll vertically from both, and only scroll horizontally the programs.
Here is a quick example:
ApplicationWindow {
id: main
width: 500
height: 100
visible: true
color: "white"
ListModel {
id: modC
ListElement { name: "Ch1" }
ListElement { name: "Ch2" }
ListElement { name: "Ch3" }
}
ListModel {
id: modP1
ListElement { name: "p1"; start: 0; duration: 6 }
ListElement { name: "p2"; start: 6; duration: 6 }
ListElement { name: "p3"; start: 12; duration: 6 }
ListElement { name: "p4"; start: 18; duration: 6 }
}
ListModel {
id: modP2
ListElement { name: "p1"; start: 0; duration: 12 }
ListElement { name: "p2"; start: 12; duration: 12 }
}
ListModel {
id: modP3
ListElement { name: "p1"; start: 0; duration: 8 }
ListElement { name: "p2"; start: 8; duration: 8 }
ListElement { name: "p3"; start: 16; duration: 8 }
}
property var subMod : [ modP1, modP2, modP3 ]
Component {
id: progDelegate
Rectangle {
property var source
x: source.start * 50
width: source.duration * 50
height: 50
color: "lightblue"
border.color: "black"
Text {
text: source.name
}
}
}
Row {
anchors.fill: parent
ListView {
id: list
height: parent.height
width: 100
model: modC
delegate: Item {
width: 100
height: 50
Rectangle {
anchors.fill: parent
color: "red"
border.color: "black"
Text {
anchors.centerIn: parent
text: name
}
}
Component.onCompleted: {
var mod = subMod[index]
for (var i = 0; i < mod.count; ++i) progDelegate.createObject(flick.contentItem, {"source": mod.get(i), "y": index * 50})
}
}
}
Flickable {
id: flick
height: parent.height
width: parent.width - list.width
contentWidth: 1200
contentHeight: contentItem.childrenRect.height
clip: true
flickableDirection: Flickable.HorizontalFlick
contentY: list.contentY
}
}
}

Related

Is there a way to make unselectable a "list item" in ListView?

I am new to Qt. Wondering if there a possibility to make an item "unselectable" in ListView.
I see there are a lot of other things, e.g: collapsing , expanding, etc.
**
I have not find any simple example for this problem. **
Can you provide some minimalistic examples to make a specific item in the list unselectable?
I have the following minimalistic example. How can I set list item index 2 to be unselectable?
Window {
id: mainWindow
width: 130
height: 240
visible: truetitle: qsTr("Hello")
Rectangle {
id: bg
color: "#ffffff"
anchors.fill: parent
ListModel {
id: nameModel
ListElement { name: "Alice" }
ListElement { name: "Bob" }
ListElement { name: "Jay" }
ListElement { name: "Kate" }
}
Component {
id: nameDelegate
Text {
text: model.name
font.pixelSize: 24
}
}
ListView {
anchors.fill: parent
model: nameModel
delegate: nameDelegate
clip: true
highlight: Rectangle {
anchors { left: parent.left; right: parent.right }
//height: parent.height
color: "lightgrey"
}
}
}
}
I found numerous issues with your code snippet, so I attempted to address them all:
I made use of Page and set Page.background instead of declaring an outer Rectangle. This removes a level of indentation
I refactored NameComponent.qml to reduce the complexity of your main program
I change the delegate from Text to ItemDelegate so that it is clickable, and, it being clickable, I can (1) make the ListView have active focus so that it can receive keyboard events, (2) change the current item in the ListView => I think this achieves your criteria of being able to select a different item
I removed unnecessary anchoring from your highlight - your highlight will default anchor to your selected item
I set the width of your delegate to listView.width - I also made use of the ListView.view attached property so that your delegate and access properties from the ListView
Finally, I added a 20 pixel width vertical ScrollBar
import QtQuick
import QtQuick.Controls
Page {
background: Rectangle { color: "#ffffff" }
ListModel {
id: nameModel
ListElement { name: "Alice" }
ListElement { name: "Bob" }
ListElement { name: "Jay" }
ListElement { name: "Kate" }
}
ListView {
anchors.fill: parent
model: nameModel
delegate: NameDelegate { }
clip: true
highlight: Rectangle {
color: "lightgrey"
}
ScrollBar.vertical: ScrollBar {
width: 20
policy: ScrollBar.AlwaysOn
}
}
}
// NameDelegate.qml
import QtQuick
import QtQuick.Controls
ItemDelegate {
property ListView listView: ListView.view
width: listView.width - 20
text: model.name
font.pixelSize: 24
onClicked: {
listView.forceActiveFocus();
if (listView.currentIndex === index) {
listView.currentIndex = -1;
} else {
listView.currentIndex = index;
}
}
}
You can Try it Online!

QML: How to set coordinates inside Repeater

I have an issue with setting coordinates inside Repeater:
import QtQuick
Window {
id: mainWindow
property int wi: 640
property int he: 500
width: wi
height: he
visible: true
title: qsTr("Game")
Rectangle {
id: gameWindow
width: wi/1.6
height: he
anchors.right: parent.right
visible: true
color: "black"
clip: true
Grid {
id: gameGrid
columns: 25
spacing: 0
rows: 32
anchors.fill: parent
Repeater {
model: 600
Rectangle {
width: wi/40
height: 20
border.width: 2
color: "grey"
}
}
}
Grid {
id: sGrid
columns: gameGrid.columns
spacing: gameGrid.spacing
rows: gameGrid.rows
anchors.fill: gameGrid
Repeater {
model: 5
Rectangle {
// anchors.horizontalCenter: sGrid.horizontalCenter
// anchors.verticalCenter: sGrid.verticalCenter
// x: (wi/2) + (index * (wi/40) )
// y: he/2
width: wi/40
height: 20
border.width: 1
color: "red"
}
}
}
}
}
Whole code above, but my question is about the second Repeater with 5 Rectangles.
I have tried to solve that with many ways. Most obvious seemed to me placing coordinates inside Repeater, but now I know it does not work like this - I have to place coordinates somehow inside Rectangle. I have commented code, where are the ways I have tried to solve this.
Anchors work very well - it places the first element exactly where I am expecting.
Problem appears with the next elements. They are placing inside the same element of Grid. I do not understand why the coordinates does not working. Documentation shows I could use "index", don't know, maybe the point is that's "read only" property? I have tried to set Rectangle with prefix "delegate:" with the same result as well.
In your question, you mention you have Grid + Repeater + Rectangle. I am not sure what you want to achieve, but, it feels like you may have better luck by going GridView + Rectangle because GridView supports a model.
Since you want coordinate control of your Rectangles, it is possible to do this alone with Repeater + Rectangle. No need for Grid since the Grid will impact the coordinate system of your Rectangle.
Below illustrates how you can use a simple ListModel to control the placement of your Rectangles:
import QtQuick
import QtQuick.Controls
Page {
Repeater {
model: ListModel {
ListElement { p:100; q:100; w:50; h:50; c:"red"; o:0 }
ListElement { p:200; q:100; w:50; h:50; c:"green"; o:15 }
ListElement { p:300; q:200; w:50; h:50; c:"blue"; o:30 }
ListElement { p:300; q:300; w:50; h:50; c:"orange"; o:45 }
ListElement { p:200; q:400; w:50; h:50; c:"purple"; o:60 }
}
delegate: Rectangle {
x: p
y: q
width: w
height: h
color: c
rotation: o
}
}
}
You can Try it Online!
[EDIT]
With the following, it shows how you could use index in 10 delegates to place your rectangles using a formula:
import QtQuick
import QtQuick.Controls
Page {
Repeater {
model: 10
delegate: Rectangle {
x: 100 + (index % 4) * 100
y: 100 + Math.floor(index / 4) * 100
width: 50
height: 50
color: ["green","red","orange","blue","purple"][index%5]
rotation: index * 10
Text {
anchors.centerIn: parent
text: index
color: "white"
}
}
}
}
You can Try it Online!

Is it possible to make a cycle swipe items in qml?

I use swipeview in qml. I can swipe items from first to final and back. Is it possible to swipe from final to first immediatly ? I didn't find any information in docs.
You can do that with PathView. Qt Quick Controls 2's Tumbler can also be made to wrap because it uses PathView internally.
You can use PathView. Some codes like this:
import QtQuick 2.0
Rectangle {
width: 200
height: 200
ListModel {
id: model
ListElement {
color: "red"
}
ListElement {
color: "green"
}
ListElement {
color: "blue"
}
}
Component {
id: delegate
Rectangle {
id: wrapper
width: view.width
height: view.height
color: model.color
Text {
anchors.centerIn: parent
font.pointSize: 26
font.bold: true
color: "white"
text: index
}
}
}
PathView {
id: view
anchors.fill: parent
snapMode: PathView.SnapOneItem
highlightRangeMode: PathView.StrictlyEnforceRange
currentIndex: -1
model: model
delegate: delegate
path: Path {
startX: -view.width / 2 // let the first item in left
startY: view.height / 2 // item's vertical center is the same as line's
PathLine {
relativeX: view.width * view.model.count // all items in lines
relativeY: 0
}
}
}
}

Layered animations create gaps between items

I am using a Row to layout a modifiable list of items whose size can change.
I want to animate insertions/removals smoothly so I added the move transition.
However, I also want to animate size changes of the items themselves.
This creates two animations: the size change of the resized item and the position change of items to their right.
Together, this results in a gap between the shrinking item and the one to its right.
You can see this effect with the following example code (left click on an item to change its size):
import QtQuick 2.7
import QtQuick.Window 2.2
Window {
visible: true
width: row.width
height: row.height
Row {
id: row
move: Transition {
NumberAnimation {
property: "x"
easing.type: Easing.InOutQuad
duration: 100
}
}
Repeater {
model: ListModel {
id: listModel
ListElement {
itemColor: "red"
}
ListElement {
itemColor: "green"
}
ListElement {
itemColor: "blue"
}
ListElement {
itemColor: "yellow"
}
ListElement {
itemColor: "orange"
}
}
delegate: Rectangle {
id: rectangle
width: 100
height: 100
color: modelData
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: rectangle.width = 400 - rectangle.width
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: listModel.remove(index)
}
Behavior on width {
PropertyAnimation {
easing.type: Easing.InOutQuad
duration: 100
}
}
}
}
}
}
So the question is how to avoid the gap.
Ideally, I'd like to separate the animation for insertion/removal from the animation for size changes. However, the documentation for move says that it is used for
Child items that move when they are displaced due to the addition, removal or rearrangement of other items in the positioner
Child items that are repositioned due to the resizing of other items in the positioner
So how can I avoid that gap while still animating the deletion and size change?
Please note
that the example above is only a minimal way to show the problem as I cannot post the real code here where the deletion is triggered by the QAbstractItemModel derivate's row deletion signal.
The gap is created by your move property which also interferes with your PropertyAnimation. So you should remove that property.
I wrapped a GridView and used in-built properties to handle animation. For example remove to define what definition and displaced to animate the movement when items get displaced.
import QtQuick 2.7
import QtQuick.Window 2.2
Window {
visible: true
width: 100*listModel.count
height: 100
ListModel {
id: listModel
ListElement {
itemColor: "red"
}
ListElement {
itemColor: "green"
}
ListElement {
itemColor: "blue"
}
ListElement {
itemColor: "yellow"
}
ListElement {
itemColor: "orange"
}
}
GridView {
model: listModel
anchors.fill: parent
remove: Transition {
PropertyAnimation {
easing.type: Easing.InOutQuad
duration: 100
property: "width"
to: 0
}
}
displaced: Transition {
NumberAnimation {
properties: "x,y"
easing.type: Easing.InOutQuad
duration: 100 }
}
delegate:
Rectangle {
id: rectangle
width: 100
height: 100
color: modelData
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: rectangle.width = 400 - rectangle.width
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: listModel.remove(index)
}
Behavior on width {
PropertyAnimation {
easing.type: Easing.InOutQuad
duration: 100
}
}
}
}
}
For example look at the documentation: http://doc.qt.io/qt-5/qml-qtquick-gridview.html#remove-prop
In the documentation of ViewTransition you can find more properties in GridView and ListView like populate, add.

QML: scroll multiple ListViews synchronously

I want to have a single (vertical) ListView with (horizontal) ListView delegates.
The horizontal delegates should scroll synchronously. To do so, I put a Flickable on top of the ListViews and bind the contentX of the horizontal ListView to the contentX of the Flickable (and the same for the contentY of the vertical ListView) (Note: Here a different approach was described for the synchronous ListView scrolling but this seems to have performance issues on mobile devices)
The code below kind of works but still has the following issues
I don't get the onClicked in the Rectangle (I do get it when I remove the top Flickable)
I want either horizontal flicking or vertical flicking but not both at the same time. I can restrict the flicking of the top Flickable by setting flickableDirection: Flickable.HorizontalFlick but then I can't flick vertically anymore (I was hoping that the Flickable would pass on unused mouse events to the vertical ListView but this doesn't seem to happen)
Suggestions on how to fix these issues?
Any help appreciated!
import QtQuick 2.0
Item {
id: main
visible: true
width: 600
height: 600
ListView {
id: verticalList
width: parent.width;
height: parent.height;
contentY : flickable.contentY
anchors.fill: parent
spacing: 10
orientation: ListView.Vertical
model: 100
delegate:
ListView {
id: horizontalList
width: parent.width;
height: 100;
contentX : flickable.contentX
spacing: 10
orientation: ListView.Horizontal
model: 20
property var verticalIndex : index
delegate:
Rectangle
{
property var colors : ['red', 'green', 'blue']
property var widths : [100, 200, 300]
height: 100
width: widths[(verticalIndex + model.index) % widths.length]
color: colors[model.index % colors.length]
MouseArea {
anchors.fill: parent
onClicked: {
console.log("Rectangle.onClicked")
}
}
}
}
}
//on top a Flickable
Flickable {
id: flickable
height: parent.height
width: parent.width
contentHeight: 100*100 //nrOfRows * rowHeight
contentWidth: 20*300 //nrOfEvent * max/averageEventWidth
}
}
I'm not giving you a perfect solution, but it's working. When you are using Flickable on the top of the ListView, you are not able to interact with it. So, I've used Flickable bellow the ListView and bounded the contentX of Flickable and ListView, but this is causing a loop and I'm getting the following output, but we're getting the desired behavior.
QML Binding: Binding loop detected for property "value"
EDIT
So, instead of using ListView for vertical list, I just used a Repeater and Column and used property binding. It's working well now.
Following is the updated version.
import QtQuick 2.0
Item {
id: main
visible: true
width: 600
height: 600
property bool virticalFlick: false //To get either vertical or horizontal flicking
Flickable {
anchors.fill: parent
contentWidth: contentItem.childrenRect.width
contentHeight: contentItem.childrenRect.height
flickableDirection: Flickable.VerticalFlick
interactive: (virticalFlick === true)?true:false
Column {
id: column
spacing: 10
Repeater {
id: repeater
model: 20
ListView {
id: horizontalList
width: 600;
height: 100;
clip: true
interactive: (virticalFlick === true)?false:true
spacing: 10
orientation: ListView.Horizontal
model: 20
property var verticalIndex : index
onMovingChanged: {
if(moving == true) {
for(var i=0; i<repeater.count ; i++) {
/* If the property is later assigned a static value from a JavaScript statement,
this will remove the binding.
However if the intention is to create a new binding then the property
must be assigned a Qt.binding() value instead. This is done by passing a function to
Qt.binding() that returns the desired result */
if (i !== index)
repeater.itemAt(i).contentX = Qt.binding(function() { return contentX });
}
}
else {
for(var i=0; i<repeater.count ; i++) {
if (i !== index)
repeater.itemAt(i).contentX = contentX; // This will remove binding
}
}
}
delegate: Rectangle {
property var colors : ['red', 'green', 'blue']
property var widths : [100, 200, 300]
height: 100
width: widths[(ListView.view.verticalIndex + model.index) % widths.length]
color: colors[model.index % colors.length]
MouseArea {
anchors.fill: parent
onClicked: {
console.log("Rectangle.onClicked")
}
}
}
}
}
}
}
}
The following does work, however the initial attempt seemed more elegant.
I still need to compare the performance (fps) when flicking, especially on a mobile device. I also get "Binding loop" warnings but I think they are false positives.
import QtQuick 2.0
Item {
id: main
visible: true
width: 600
height: 600
ListView {
id: verticalList
width: parent.width;
height: parent.height;
anchors.fill: parent
spacing: 10
cacheBuffer: 500 // in pixels
orientation: ListView.Vertical
model: 100
property var activeIndex : 0
property var lastContentX : 0
delegate:
ListView {
id: horizontalList
width: parent.width;
height: 100;
spacing: 10
cacheBuffer: 500 // in pixels
orientation: ListView.Horizontal
model: 20
property var verticalIndex : index
delegate:
Rectangle
{
property var colors : ['red', 'green', 'blue']
color: colors[model.index % colors.length]
height: 100
property var widths : [100, 200, 300]
width: widths[(verticalIndex + model.index ) % widths.length]
MouseArea {
z:10
anchors.fill: parent
onClicked: {
console.log("Rectangle.onClicked")
}
onPressed: {
console.log("Rectangle.onPressed")
}
onReleased: {
console.log("Rectangle.onReleased")
}
}
}
onContentXChanged: {
if(model.index === verticalList.activeIndex)
{
verticalList.lastContentX = contentX
}
}
onMovementStarted: {
verticalList.activeIndex = model.index
unbind();
}
onMovementEnded: {
bind();
}
Component.onCompleted: {
bind();
}
function bind()
{
contentX = Qt.binding(function() { return verticalList.lastContentX })
}
function unbind()
{
contentX = contentX ;
}
}
}
}
The following modifications were needed to my initial attempt
limit the Flickable to flickableDirection : Flickable.HorizontalFlick and remove the contentY : flickable.contentY on the verticalList
by doing so, there is no vertical scrolling anymore. This can be fixed by moving the Flickable inside the ListView
onClicked events are received by adding the following MouseArea to the Flickable
eg.
MouseArea {
anchors.fill: parent
//see http://stackoverflow.com/questions/29236762/mousearea-inside-flickable-is-preventing-it-from-flicking
onReleased: {
if (!propagateComposedEvents) {
propagateComposedEvents = true
}
}
}

Resources