Qt/QML: How to bidirectional sync ScrollView in QML? - qt

I want to sync contentY of two scrollable listviews as shown in this simplified code
Item {
SplitView {
orientation: Qt.Horizontal
Component1 {
id: left
contentY: right.contentY
}
Component1 {
id: right
contentY: left.contentY
}
}
}
//Component1.qml
Item {
property alias contentY: component2.contentY
Component2 {
id: component2
}
}
//Component2.qml
Item {
property alias contentY: list.contentY
ScrollView {
ListView {
id: list
}
}
}
It's working when I start or reload the QML scene and keep scrolling in only one splitview. However, as soon as I start scrolling in the other listview, the bidirectional binding is broken and contentY isn't in sync anymore. I can only scroll the listviews separately from each other. How can I avoid this? Is there a better way to sync contentY?

I found a solution which seems to work:
Item {
SplitView {
orientation: Qt.Horizontal
Component1 {
id: left
Binding {
target: right
property: "contentY"
value: left.contentY
}
}
Component1 {
id: right
Binding {
target: left
property: "contentY"
value: right.contentY
}
}
}
}
//Component1.qml
Item {
property alias contentY: component2.contentY
Component2 {
id: component2
}
}
//Component2.qml
Item {
property alias contentY: list.contentY
ScrollView {
ListView {
id:list
}
}
}
However, if there is a better solution I would be thankful for any hint :)

Related

How to dynamically append elements to ListModel from exterior scope

Suppose I had the Component
Component {
id: myComp1
Item {
id: item
ListView {
id: listView
model : ListModel { id: listModel }
delegate : RowLayout { /* display model data*/ }
Component.onCompleted {
// get data from server ...
model.append(dataFromServer)
}
}
}
}
Then I have a second Component, which is another page in the stack, and I want to use this component to update mycomp1, i.e:
Component {
id: myComp2
Button {
onClicked: {
myComp1.item.listView.listModel.append(someNewData) // want to be able to do this
}
}
}
And these components are tied together in a StackView
Now, this doesnt seem to work since myComp2 cant seem to access the necessary scope to update the model of myComp1. Is there any way around this?
Thanks for the help.
The problem is that a Component is like a type declaration. It does not define an instance of an object, so you cannot access its members.
You could pull the ListModel outside of that Component so that both Components can access it.
ListModel {
id: listModel
}
Component {
id: comp1
ListView { model: listModel }
}
Component {
id: comp2
Button {
onClicked: { listModel.append(someNewData) }
}
}

Adding dynamically to qml tabbar and stacklayout

I'm trying to create a tabbed pages in qml. I used TabBar associated with StackLayout:
TabBar {
id: bar
width: parent.width
TabButton {
text: qsTr("Home")
}
TabButton {
text: qsTr("Discover")
}
TabButton {
text: qsTr("Activity")
}
}
StackLayout {
width: parent.width
currentIndex: bar.currentIndex
Item {
id: homeTab
}
Item {
id: discoverTab
}
Item {
id: activityTab
}
}
A new tabButton can be easily added by this code dynamically:
var tab = tabButton.createObject(TTabButton, {text: tabName});
bar.addItem(tab);
which TTabButton is a separate file consisting TabButton item. But I can't find any ways to add a new page to StackLayout. It seems it is supposed to be static. So my question is how to have dynamic tab-paged in qml?
You can add to the children of StackLayout:
var item = stackItem.createObject(null, {id: "tabName"})
layout.children.push(item)
Where stackItem is the Component of those items you add to your StackLayout layout.
You can manipulate the children (or data) property of the StackLayout directly as suggested, but potentially a better idea is to use a Repeater in conjunction with an ObjectModel:
StackLayout
{
id: stackLayout
currentIndex: bar.currentIndex
Repeater
{
model: ObjectModel
{
id: container
}
}
}
This way you get the much richer and easier API of ObjectModel:
let tabPage = tabPageComponent.createObject(stackLayout);
container.append(tabPage);
let tabPage2 = tabPageComponent.createObject(stackLayout);
container.insert(0, tabPage2);
container.move(1, 0);
// etc.
One important detail here is that when removing a tab, it's necessary to manually destroy() it, since the Repeater/ObjectModel only manages the hierarchy, not lifetime.
let tabPage = container.get(0);
container.remove(0);
tabPage.destroy();

Change property of delegate on signal

I have a TreeView, and on the signal onSortIndicatorChanged I would like to set the checked property of a CheckBox inside the delegate of a TableViewColumn in my TreeView. However, I don't know how to do this.
Component {
id: mycomp
Item {
id: myitm
CheckBox {
id: mycbx
checked: true
}
}
}
TreeView {
TableViewColumn {
delegate: myDelegate
}
onSortIndicatorChanged{
// set the checked property of the delegates to true
}
}
You could try to add a property checked to your tree view and bind the check boxes to this property instead. Then, in the even handler, just set the value of the TreeView's property:
Component {
id: mycomp
Item {
id: myitm
CheckBox {
id: mycbx
checked: view.checked
}
}
}
TreeView {
id: view
property bool checked: false
TableViewColumn {
delegate: mycomp
}
onSortIndicatorChanged {
view.checked = true
}
}
Update:
In case you want to set a per-item checked state, you can use a similar approach where you store a function as a property of the view. In the delegate, you could call this stored function to get an item specific property value:
Component {
id: mycomp
Item {
id: myitm
CheckBox {
id: mycbx
checked: view.checkFunction ? view.checkFunction(modelData) : true
}
}
}
TreeView {
id: view
property var checkFunction: null
TableViewColumn {
delegate: mycomp
}
onSortIndicatorChanged {
view.checkFunction = function(modelData) {
// Calculate the checked state based on the modelData
return modelData.foo == "bar";
}
}
}

QML/QtQuick Binding delegate's property with ListView's currentIndex

Inside the delegate, I bind Image's source property to ListView's currentIndex which determines which image to load. This works great:
ListView {
id: answerListView
model: 5
currentIndex: -1
delegate: answerDelegate
}
Component {
id: answerDelegate
Item {
width: 100
height: 100
Image {
source: answerListView.currentIndex === index
? "selected.png" : "not_selected.png"
}
MouseArea {
anchors.fill: parent
onClicked: {
answerListView.currentIndex = index
}
}
Component.onCompleted: {
answerListView.currentIndex = 1; // doesn't work!!
}
}
}
Since currentIndex: -1, it will always show not_selected.png. To show selected.png, I change currentIndex in Component.onLoaded inside delegate.
I was expecting image to load selected.png since currentIndex was updated.
What is the correct way and what am I misunderstanding here?
Ok, new guess:
You want to have the posibility to select multiple Items. As currentIndex only stores one value, which is the value you assigned it last, you can use it to mark only one Item.
Therefore you need to find another way to store your selection. You might for example have a property in the delegate: property bool selected: false which you set to true upon selection.
The problem with this solution is, that it only works if all Items are instantiated at all times. As soon as one Item will be destroyed again, the information will be lost, and uppon the next creation, the selection/unselection is undone.
The better way would be to introduce a role in your model, that stores the selection outside of the non-persistant delegates for you:
ListView {
id: answerListView
model: lm
delegate: answerDelegate
width: 100
height: 220
}
ListModel {
id: lm
ListElement { selected: false }
ListElement { selected: false }
ListElement { selected: false }
ListElement { selected: false }
ListElement { selected: false }
}
Component {
id: answerDelegate
Item {
width: 100
height: 100
Image {
anchors.fill: parent
source: model.selected ? "selected.png" : "notselected.png"
}
Text {
text: (model.selected ? 'selected ' : 'notselected ')
}
Component.onCompleted: {
model.selected = true // doesn't work!!
}
MouseArea {
anchors.fill: parent
onClicked: {
model.selected = !model.selected
}
}
}
}
Another option would probably be a ItemSelectionModel, but I don't know atm, how it works.
Otherwise your example works as expected:
The Item with index 1 is shown, and displays the Image selected.png. All other Items are not shown (for the ListView is to small) but if the would be shown, they would show notselected.png for the answerListView.currentIndex is not equal to their index.

Qt/QML: How to refer to a component/object from a GridView's model's ListElement

I have a GridView with a delegate that is supposed to use a Loader to load and display components which are defined in the same QML file.
Let's say I have a GridView like this:
GridView {
delegate: Rectangle {
Loader { sourceComponent: model.pageContents }
}
model: ListModel {
ListElement { /* how do I reference any of the components defined below from here, so the Loader can actually load and display it... ? */ }
}
}
Component {
id: page_01
Rectangle {
color: "red"
// Page contents for page 1 go here.
}
}
Component {
id: page_02
Rectangle {
color: "red"
// Page contents for page 2 go here.
}
}
I know I can create QML objects and components from Strings, external files and URLs. But I'd like to ideally do something like this:
ListModel {
ListElement { pageContents: page_01 }
ListElement { pageContents: page_02 }
}
I'd prefer to keep everything in a single QML file, so I can easily transfer and store it on the device without having to worry about resolving external dependencies, etc.
How do I refer to components in the same QML file from within ListElements?
Due to ListElement values limitation you cannot just put item id here. But you easily can use some external storage, for example property to store pointers to your pages. In example below I use array with pages ids and an index to wanted page as ListElement data:
import QtQuick 2.3
import QtQuick.Window 2.2
Window {
visible: true
width: 600
height: 600
GridView {
id: grid
anchors.fill: parent
property var pages: [page_01, page_02]
model: ListModel {
ListElement { pageIndex: 0 }
ListElement { pageIndex: 1 }
}
delegate: Loader { sourceComponent: grid.pages[pageIndex] }
}
Component {
id: page_01
Rectangle {
color: "red"
width: 100
height: 100
Component.onCompleted: console.log("page_01 was created")
}
}
Component {
id: page_02
Rectangle {
color: "blue"
width: 100
height: 100
Component.onCompleted: console.log("page_02 was created")
}
}
}

Resources