Generic QML delegate, but non-generic View attached properties? - qt

A View attaches properties to a delegate. For a ListView, the delegate could access the ListView.view.width and ListView.isCurrentItem properties :
Rectangle {
width: ListView.view.width
height: 40
color: ListView.isCurrentItem ? "gray" : "lightGray"
Text {
anchors.centerIn: parent
text: index
}
}
By referring to the View by its type name, it seems like the Delegate loses its genericity.
What if I want to use the same delegate within a GridView ?

You should make a Component from your delegate and set property isCurrentItem during its instantiation. In other words, create new qml file and name it e.g. "Delegate.qml" and add property bool isCurrentItem:
import QtQuick 2.4
Rectangle {
property bool isCurrentItem: false
width: parent.width
height: 20
color: isCurrentItem ? "gray" : "lightGray"
Text {
anchors.centerIn: parent
text: index
}
}
than you can use it in ListView like:
ListView {
model: 10
width: 40
height: 200
delegate: Delegate {
isCurrentItem: ListView.isCurrentItem
}
}
and similarly in GridView:
GridView {
model: 10
width: 40
height: 200
delegate: Delegate {
isCurrentItem: ListView.isCurrentItem
}
}
You could do it same way to provide width of ListView/GridView to delegate, but in this case parent.width will also work the way you want.

Related

Looking for alternate function to QML ListView's itemAtIndex due to version conflict

I'm using Qt 5.12, so I can't access ListView's itemAtIndex which was introduced in Qt 5.13.
I can't upgrade Qt due to my project/platform related restrictions. Is there a way to find the item at a given index for ListView with the Qt versions prior to 5.13?
Otherwise, is there a way to get mouse positions of an item based on index?
I'm having a listview with adjacent items having different width(alternate items have same width). I'm trying to access listview's item which is of less width compared to the adjacent item. The space between two items in the above picture is also an item which is marked as dummy. I'm able to get the index of each item (both actual & dummy), but the x position I get seems to be incorrect as the rectangle cursor is not getting placed in the intended item's position.
Please suggest alternatives that gives the similar functionality as itemAtIndex. Thanks.
In the following example, I declare a MouseArea in each delegate. So, once the mouse hovers over that delegate, we trigger MouseArea.onEntered and can know which item, because that delegate will have the corresponding index value:
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Page {
background: Rectangle { color: "#848895" }
ColumnLayout {
anchors.fill: parent
ListView {
id: listView
Layout.fillWidth: true
Layout.preferredHeight: 150
model: 20
orientation: ListView.Horizontal
delegate: MyDelegate { }
ScrollBar.horizontal: ScrollBar {
height: 20
policy: ScrollBar.AlwaysOn
}
highlight: Item {
z: 2
Rectangle {
width: 10
height: parent.height
color: "lightsteelblue"
border.color: "black"
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Frame {
anchors.centerIn: parent
background: Rectangle { }
Text {
text: qsTr("ListView.currentIndex = %1").arg(listView.currentIndex)
}
}
}
}
}
// MyDelegate.qml
import QtQuick
import QtQuick.Controls
Rectangle {
property ListView listView: ListView.view
width: 120
height: listView.height - 20
implicitWidth: width
implicitHeight: height
color: "transparent"
Rectangle {
border.color: "grey"
color: "white"
y: 20
height: parent.height - y * 2
width: parent.width
Text {
anchors.centerIn: parent
text: qsTr("Item %1").arg(modelData + 1)
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: listView.currentIndex = index
}
}
You can Try it Online!

How to drag an item outside a ListView in QML

I am developing a QML application which basically contains two ListView. I would like to copy a QML item from one ListView to another. I tried to handle this by setting Drag property in the delegate but the item cannot go outside the view when I drag the item, I think the Flickable container handles mouse events.
So, I want to try the following:
create a mousearea which overlaps the to ListView
create a new object by calling **createComponent() / createObject()**
reparent this object to the mousearea
handle mouse events in the mousearea till drop
This solution seems to me a little complicated, so do you have a better way to achieve this ?
This was a bad idea and too much complicated. I think I got a way to achieve this:
each delegate of the ListView has a hidden Item which can be dragged,
as my ListView are in a reusable component, I use a property to pass a higher item (a Rectangle here and NOT a **MouseArea**) which can be used as parent for dragged items,
the higher item contains the two ListView (and maybe more in the future),
when the drag begins, the item is set to visible and reparented using a **State**
So, I missed the point that set the parent should solve my problem.
Next code is just an idea, but the key is to have a MouseArea inside a delegate for the first ListView so the user can drag the items and drop them into a DropArea which belongs to the second ListView.
In this example, model is very simple, just a number. And when the item is dropped, it is removed from the first ListView:
listView.model.remove(listView.dragItemIndex)
Just remove that line of code to copy the item instead of removing.
main.qml
import QtQuick 2.5
import QtQuick.Window 2.2
Window {
visible: true
width: 600
height: 600
Rectangle {
id: root
width: 400
height: 400
ListView {
id: listView
width: parent.width / 2
height: parent.height
property int dragItemIndex: -1
model: ListModel {
Component.onCompleted: {
for (var i = 0; i < 10; ++i) {
append({value: i});
}
}
}
delegate: Item {
id: delegateItem
width: listView.width
height: 50
Rectangle {
id: dragRect
width: listView.width
height: 50
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
color: "salmon"
border.color: Qt.darker(color)
Text {
anchors.centerIn: parent
text: modelData
}
MouseArea {
id: mouseArea
anchors.fill: parent
drag.target: dragRect
drag.onActiveChanged: {
if (mouseArea.drag.active) {
listView.dragItemIndex = index;
}
dragRect.Drag.drop();
}
}
states: [
State {
when: dragRect.Drag.active
ParentChange {
target: dragRect
parent: root
}
AnchorChanges {
target: dragRect
anchors.horizontalCenter: undefined
anchors.verticalCenter: undefined
}
}
]
Drag.active: mouseArea.drag.active
Drag.hotSpot.x: dragRect.width / 2
Drag.hotSpot.y: dragRect.height / 2
}
}
}
ListView {
id: listView2
width: parent.width / 2
height: parent.height
anchors.right: parent.right
property int dragItemIndex: -1
DropArea {
id: dropArea
anchors.fill: parent
onDropped: {
listView2.model.append(listView.model.get(listView.dragItemIndex))
listView.model.remove(listView.dragItemIndex)
listView.dragItemIndex = -1;
}
}
model: ListModel {
Component.onCompleted: {
for (var i = 0; i < 1; ++i) {
append({value: i});
}
}
}
delegate: Item {
id: delegateItem2
width: listView2.width
height: 50
Rectangle {
id: dragRect2
width: listView2.width
height: 50
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
color: "salmon"
border.color: Qt.darker(color)
Text {
anchors.centerIn: parent
text: modelData
}
}
}
}
}
}

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
}
}
}

Why does anchors.fill does not work in a QML ListView's delegate's subviews, and what is a nice solution?

I encountered this:
ListView {
id: listView
model: ["Lorem","Ipsum"]
delegate: Item {
height: 20
Text {
z: 2
text: modelData
anchors.fill: parent
}
Rectangle {
z: 1
color: "red"
// this does not work:
anchors.fill: parent
// this works, but I have mixed feelings about it:
// height: 20; width: listView.width
}
}
}
So, apparently, anchors do not work in a delegate's subitem (in this case, Rectangle is not displayed at all). I would like to understand the mechanism behind this. Also, I'd like to ask what is the preferred way to deal with this situation?
Thank You!
Item has an implicitWidth and implicitHeight of zero, so making your Rectangle and Text fill it will result in them having no size as well.
There are two things wrong with your code:
The ListView has no width or height specified.
Your delegate has no width specified.
Here's one way of doing it correctly:
import QtQuick 2.0
import QtQuick.Window 2.0
Window {
width: 300
height: 300
visible: true
ListView {
id: listView
anchors.fill: parent
model: ["Lorem","Ipsum"]
delegate: Item {
width: listView.width
height: textItem.implicitHeight
Text {
id: textItem
z: 2
text: modelData
width: parent.width
}
Rectangle {
z: 1
color: "red"
anchors.fill: parent
}
}
}
}
The documentation of ListView has more information.

How to access the QML object inside the Loader's sourceComponent?

I may need to read or write to some of the properties of the Loader's sourceComponent from some outside function.
What is the way to access the property x of the object inside this Loader's sourceComponent?
import QtQuick 2.0
Item {
width: 200; height: 200
Loader {
anchors.fill: parent
sourceComponent: rect
}
Component {
id: rect
Rectangle
{
width: 50
height: 50
color: "red"
property int x
}
}
}
When you need to expose an inner object/property to the outside, you should create an alias to it.
import QtQuick 2.0
Item {
width: 200; height: 200
property alias loaderItem: loader.item
Loader {
id: loader
anchors.fill: parent
sourceComponent: rect
}
Component {
id: rect
Rectangle
{
width: 50
height: 50
color: "red"
property int x
}
}
}

Resources