QML StackView: Item Destroyed on pop - qml

Consider the following example:
import QtQuick 2.4
import QtQuick.Controls 1.3
import QtQuick.Window 2.2
Window {
visible: true
width: 320
height: 320
StackView {
id: stackView
anchors.fill: parent
Component.onCompleted: {
stackView.push( { item: comp1, destroyOnPop:false } )
}
}
Component {
id: comp1
Rectangle {
color: "lightgray"
Text {
id: mtext
anchors.centerIn: parent
text: "First Page"
}
}
}
Component {
id: comp2
Rectangle {
color: "lightgreen"
Text {
id: mtext
anchors.centerIn: parent
text: "Second Page"
}
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked: mtext.text += " Clicked"
}
}
}
Row {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
Button {
id: next
text: "Next"
onClicked: {
stackView.push( { item: comp2, destroyOnPop:false } )
enabled = false
prev.enabled = true
}
}
Button {
id: prev
text: "Prev"
enabled: false
onClicked: {
stackView.pop()
enabled = false
next.enabled = true
}
}
}
}
Here I'm pushing 2 Components into the StackView on the each button clicks, i.e. clicking the "Next" button loads the second page. When I click on it, it displays an updated text("Second Page Clicked"). Then clicking "Prev" loads the first page as expected.
Now if I click "Next" again, it should load the second page with that updated text ("Second Page Clicked"), but it doesn't. It shows the initial text ("Second Page").
So the question is whether the Item is destroyed on pop? If not then shouldn't the second page display the updated text ("Second Page Clicked")?
I have even set the flag destroyOnPop to false. Have I misunderstood the concept of StackView? Any possible way to solve this?
I want to load any page from any point in StackView and that the state of Item be as it is where I left it. Just like QStackWidget, where we use setCurrentIndex().

Documentation is referring to Item object whereas you are using Components. The documentation is pretty clear about the fact that a component is not an Item since it is not derived from Item. Indeed a Component act like a Class which is instanced to a specific object.
Each time you call
stackView.push( { item: comp2, destroyOnPop:false } )
a new "instance" of the component comp2 is generated and such new instance is used instead of the previous one. I strongly suggest to read this article to better understand the correlation between Component objects and generated instances.
Instances can be generated via createObject function. Hence, you can create an array of Items and use such Items instead of the Components. Final code looks like this:
import QtQuick 2.4
import QtQuick.Controls 1.3
import QtQuick.Window 2.2
Window {
visible: true
width: 320
height: 320
StackView {
id: stackView
anchors.fill: parent
Component.onCompleted: {
push( { item: items[0], destroyOnPop:false })
}
property variant items: [comp1.createObject(), comp2.createObject()] // objects from the components
}
Component {
id: comp1
Rectangle {
color: "lightgray"
Text {
id: mtext
anchors.centerIn: parent
text: "First Page"
}
}
}
Component {
id: comp2
Rectangle {
color: "lightgreen"
Text {
id: mtext
anchors.centerIn: parent
text: "Second Page"
}
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked: mtext.text += " Clicked"
}
}
}
Row {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
Button {
id: next
text: "Next"
onClicked: {
stackView.push({ item: stackView.items[1], destroyOnPop:false })
enabled = false
prev.enabled = true
}
}
Button {
id: prev
text: "Prev"
enabled: false
onClicked: {
stackView.pop()
enabled = false
next.enabled = true
}
}
}
}
The same correct result could be achieved by defining directly the Rectagle objects in the source. In this case we have two Items which are reparented to the Window thanks the destroyOnPop set to false.
Rectangle {
id: comp1
color: "lightgray"
visible: false
Text {
id: mtext
anchors.centerIn: parent
text: "First Page"
}
}
Rectangle {
id: comp2
color: "lightgreen"
visible: false
Text {
id: mtext2
anchors.centerIn: parent
text: "Second Page"
}
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked: mtext2.text += " Clicked"
}
}

Related

How can I make a custom delegate for my ListView?

I'm trying to make a custom ListView (AppDataListView) that can be used for data manipulation (essentially having a bunch of inputs). The problem I'm having is finding a way to tell AppDataListView what input types to use in the delegate of the ListView.
I currently try and do this by creating a loader in the ListView delegate, setting its source to a property of type Component, and then when I create an instance of AppDataListView, I specify the Component... However, I don't have any access to the model data, so it's kind of pointless.
Does anyone know how I can accomplish this?
Main.qml
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick .Controls 2.12
import QtQuick.Layouts 1.12
Window {
width: 640
height: 480
visible: true
ListModel {
id: sampleData
ListElement {
itemId: 1
name: "Name1"
}
ListElement {
itemId: 2
name: "Name2"
}
ListElement {
itemId: 3
name: "Name3"
}
ListElement {
itemId: 4
name: "Name4"
}
}
AppDataListView {
anchors.fill: parent
headers: ["ID", "Name"]
model: sampleData
delegate: Component {
RowLayout {
anchors.fill: parent
TextArea {
// I can't access itemId from here even though this is loaded into the delegate.
text: itemId
}
TextArea {
// I can't access name from here even though this is loaded into the delegate.
text: name
}
}
}
}
}
AppDataListView.qml
import QtQuick 2.0
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
Rectangle {
property var headers: []
property alias model: listView.model
property alias listView: listView
required property Component delegate;
id: root
color: "#bdbdbd"
ColumnLayout {
anchors.fill: parent
anchors.margins: 1
spacing:0
RowLayout {
spacing: 1
Layout.fillWidth: true
Layout.preferredHeight: childrenRect.height
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
Item {
Layout.minimumWidth: 30
Layout.maximumWidth: 30
}
Repeater {
id: headerRepeater
model: headers
delegate: Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: childrenRect.height
gradient: Gradient {
GradientStop { position: 0.0; color: "#FFFFFF" }
GradientStop { position: 0.5; color: "#F1F1F1" }
GradientStop { position: 1.0; color: "#FFFFFF" }
}
Label {
text: modelData
padding: 5
anchors.centerIn: parent
}
}
}
}
ListView {
id: listView
Layout.fillWidth: true
Layout.fillHeight: true
interactive: true
clip: true
boundsBehavior: Flickable.StopAtBounds
spacing: 1
ScrollBar.vertical: ScrollBar {
active: true
policy: ScrollBar.AlwaysOn
}
delegate: RowLayout {
width: parent.width
spacing: 1
Button {
id: rowBtn
Layout.minimumWidth: 30
Layout.maximumWidth: 30
background: Rectangle {
gradient: Gradient {
GradientStop { position: 0.0; color: (rowBtn.down ? "#56aff5" : rowBtn.hovered ? "#d9ebf9" : "#FFFFFF") }
GradientStop { position: 0.5; color: (rowBtn.down ? "#1b93f1" : rowBtn.hovered ? "#a4b2bd" : "#F1F1F1") }
GradientStop { position: 1.0; color: (rowBtn.down ? "#56aff5" : rowBtn.hovered ? "#d9ebf9" : "#FFFFFF") }
}
}
}
Loader {
sourceComponent: root.delegate
}
}
}
}
}
Capturing #Amfasis excellent comments to the question and adding some more detail....
Change your Loader reference to this:
Loader {
sourceComponent: root.delegate
property int itemId: model.itemId
property string name: model.name
}
fixes it. Here's why....
A Component declaration is put into the QML namespace hierarchy where it is declared not where it is instantiated at runtime. In other words, by declaring the delegate within main.qml, it can only see that namespace regardless of where it is instantiated at (in this case in AppDataListView).
More info here:
https://doc.qt.io/qt-5/qml-qtqml-component.html#creation-context
As #Amfasis pointed out, the workaround in this case is to declare properties on the Loader to pass in the model references you need. In this case the Loader acts as a bridge of sorts from the ListView namespace over to the delegate Component's namespace in main.qml.
More info on that here:
https://doc.qt.io/qt-5/qml-qtquick-loader.html#using-a-loader-within-a-view-delegate

Changing model doesn't affect ComboBox

Suppose, we have the following code:
import QtQuick 2.4
import QtQuick.Window 2.0
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1
Window {
id: win
width: 800
height: 600
ListModel {
id: listModel
ListElement { name: "element1" }
ListElement { name: "element2" }
ListElement { name: "element3" }
}
ColumnLayout {
anchors.centerIn: parent
width: 200
height: 200
ComboBox {
model: listModel
currentIndex: 1
Layout.fillWidth: true
}
ListView {
model: listModel
delegate: Text {
text: name
}
Layout.fillHeight: true
Layout.fillWidth: true
}
Button {
text: "Change model"
onClicked: {
listModel.get(1).name = "changed text";
//listModel.setProperty(1,"name","changed text"); this line not works too
}
}
}
}
So clicking the button have to change model's element with index 1. But changing the model affects only ListView. The ComboBox remains unchanged.
Why that happens? Is it bug or feature? Is there a way to update ComboBox after changing its model?
I had a similar problem, I used a workaround. In onClicked function of button, create copy of model, change it as you want and then assign it again to ListViews model:
ListView {
id: listView
...
}
Button {
onClicked: {
var copy = listView.model;
copy.get(1).name = "changed text";
listView.model = copy; }
}
}

How to highlight the clicked (by mouse) element of a delegate w.r.t FolderListModel?

import QtQuick 2.0
import Qt.labs.folderlistmodel 2.0
Item
{
Component {
id: highlight
Rectangle {
id: rooot
width: 180; height: 20
color: ListView.isCurrentItem ? "black" : "red"; radius: 5
y: list.currentItem.y
Behavior on y {
SpringAnimation {
spring: 3
damping: 0.2
}
}
}
}
ListView {
id: list
width: 480; height: 400
model: folderModel
delegate: Text { id: h; text: fileName }
highlight: highlight
highlightFollowsCurrentItem: false
focus: true
}
FolderListModel
{
id: folderModel
folder: "/home/anisha/"
nameFilters: ["*"]
}
}
This works only when I use keyboard. How to make it work on mouse clicks?
To react on mouse events you need to place MouseArea item.
In the sample below (being an expanded version of the code you provided) I have added a MouseArea to the delegate item that upon being clicked sets the ListView's currentIndex to the delegate's index (a special property visible in the ListView's delegate).
import QtQuick 2.0
import Qt.labs.folderlistmodel 2.0
Item
{
Component {
id: highlight
Rectangle {
id: rooot
width: 180; height: 20
color: ListView.isCurrentItem ? "black" : "red"; radius: 5
y: list.currentItem.y
Behavior on y {
SpringAnimation {
spring: 3
damping: 0.2
}
}
}
}
ListView {
id: list
width: 480; height: 400
model: folderModel
delegate:
Text {
id: h;
text: fileName
MouseArea {
anchors.fill: parent
onClicked: list.currentIndex = index
}
}
highlight: highlight
highlightFollowsCurrentItem: false
focus: true
}
FolderListModel
{
id: folderModel
folder: "/home/anisha/"
nameFilters: ["*"]
}
}
As an alternative approach you might try placing a single MouseArea filling the whole ListView and use ListView's indexAt(int x, int y) method to check which delegate was clicked. However, you would need to care about more edge-conditions in such case.

QML Window is not visible

I have got QML application and there should be a lot of dialogs. When user press ToolButton appropriate dialog should be visible so that user can modify contols of that dialog. Here is minimum code for that:
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Window 2.0
ApplicationWindow {
visible: true
property variant dialog: Loader{sourceComponent: wind}
toolBar: ToolBar {
Row {
anchors.fill: parent
ToolButton {
iconSource: "1.png"
checkable: true
checked: false
onClicked: dialog.visible=checked
}
}
}
Component {
id: wind
Window{
visible: false
flags: Qt.Dialog
Button{
text: "hello"
}
}
}
}
However when I press ToolButton dialog is not visible. What is wrong?
property variant dialog: Loader{sourceComponent: wind} - that is wrong, don't expect the element to show when declared as a property, it has to be a child of its parent component.
onClicked: dialog.visible=checked - this is wrong, you need to use the item property of the dialog to refer to the object the loader instantiates
Code that works:
ApplicationWindow {
visible: true
toolBar: ToolBar {
Row {
anchors.fill: parent
ToolButton {
checkable: true
checked: false
onClicked: dialog.item.visible = checked
}
}
}
Loader {
id: dialog
sourceComponent: wind
}
Component {
id: wind
Window {
width: 100
height: 100
Button {
text: "hello"
}
}
}
}

How to fill a QTableview using QML Tool Button

I need to press my create button under red circle and by pressing this button i am trying to fill first row of my table view. I am a new user in QT Quick please help me. I have wasted lot of time but have no way to do it.
I am providing UI and code as well please have a look.
Thank you !
import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.0
import QtQuick.Dialogs 1.0
import "content"
import "images"
ApplicationWindow {
visible: true
title: "Mask Editor: Subsystem"
width: 720
height: 420
minimumHeight: 400
minimumWidth: 720
statusBar: StatusBar {
id: minstatusbar
RowLayout {
id:row1
spacing: 2
width: parent.width
Button {
text:"Unmask"
}
Item { Layout.fillWidth: true }
Button {
text:"OK"
}
Button {
text:"Cancel"
}
Button {
text:"Help"
}
Button {
text:"Apply"
}
}
}
/* ListModel {
id: largeModel
Component.onCompleted: {
for (var i=0 ; i< 10 ; ++i)
largeModel.append({"index":i , "prompt": "pmpt", "variable":i+10, "type" : "Female","tabname":"something"})
}
}*/
Action {
id: openAction
text: "&Open"
shortcut: "Ctrl+O"
iconSource: "images/document-open.png"
onTriggered: fileDialog.open()
tooltip: "Open an Image"
}
TabView {
id:frame
enabled: enabledCheck.checked
tabPosition: controlPage.item ? controlPage.item.tabPosition : Qt.TopEdge
anchors.fill: parent
anchors.margins: Qt.platform.os === "osx" ? 12 : 2
Tab {
id: controlPage
title: "Parameters"
Item {
id:root
anchors.fill: parent
anchors.margins: 8
ColumnLayout {
id: mainLayout
anchors.fill: parent
spacing: 4
GroupBox {
id: gridBox
title: "Grid layout"
Layout.fillWidth: true
ListModel {
id: nestedModel
ListElement{index:"1";prompt:"pmt";variable:10; type: "to be defined";tabname:"something"}
}
GridLayout {
id: gridLayout
anchors.fill: parent
rows: 3
flow: GridLayout.TopToBottom
ToolButton { iconSource: "images/window-new.png"
action:filldata
Accessible.name: "New window"
onClicked: {
nestedmodel.append({"index":i , "prompt": "pmpt", "variable":i+10, "type" : "Female","tabname":"something"})
}
tooltip: "Toggle visibility of the second window" }
ToolButton { iconSource: "images/view-refresh.png"
onClicked: window1.visible = !window1.visible
Accessible.name: "New window"
tooltip: "Toggle visibility of the second window" }
ToolButton { Accessible.name: "Save as"
iconSource: "images/document-save-as#2x.png"
tooltip: "(Pretend to) Save as..." }
TableView{
model: modelcontl
Layout.rowSpan: 3
Layout.fillHeight: true
Layout.fillWidth: true
//anchors.fill: parent
TableViewColumn {
role: "index"
title: "#"
width: 36
resizable: false
movable: false
}
TableViewColumn {
role: "prompt"
title: "Prompt"
width: 120
}
TableViewColumn {
role: "variable"
title: "Variable"
width: 120
}
TableViewColumn {
role: "type"
title: "Type"
width: 200
visible: true
}
TableViewColumn {
role: "tabname"
title: "Tab Name"
Layout.fillWidth: true
visible: true
}
}
}
}
Rectangle{
Layout.fillHeight: true
Layout.fillWidth: true
GridLayout {
//id: gridLayout
rows: 1
flow: GridLayout.TopToBottom
anchors.fill: parent
GridLayout{
id: grid1
columns: 2
anchors.fill: parent
GroupBox {
//id: gridBox
title: "Grid layout"
Layout.fillHeight: true
Layout.fillWidth: true
}
GroupBox {
//id: gridBox
title: "Grid layout"
Layout.fillHeight: true
Layout.fillWidth: true
}
}
}
}
}
}
}
Tab {
title: "Documentation"
Controls { }
}
Tab {
title: "dialog"
MessageDialogs { }
}
}
}
If you want to add a QML control into a cell you just have to define custom delegate. Since you need different delegates for different columns it can be done in a bit "tricky" way with Loader. See my example below:
Component {
id: comboDelegate
ComboBox {
model: ListModel {
ListElement { text: "Banana" }
ListElement { text: "Apple" }
ListElement { text: "Coconut" }
}
}
}
Component {
id: textDelegate
Text {
text: itemText
}
}
TableView {
Layout.fillWidth: true
Layout.fillHeight: true
TableViewColumn{ role: "col1" ; title: "Column1" }
TableViewColumn{ role: "col2" ; title: "Column2" }
TableViewColumn{ role: "col3" ; title: "Column3" }
model: ListModel {
id: myModel
}
itemDelegate: Item {
height: 25
Loader {
property string itemText: styleData.value
sourceComponent: styleData.column === 0 ? comboDelegate : textDelegate
}
}
}
Try this:
TableView {
TableViewColumn{ role: "col1" ; title: "Column1" }
TableViewColumn{ role: "col2" ; title: "Column2" }
TableViewColumn{ role: "col3" ; title: "Column3" }
model: ListModel {
id: myModel
}
}
Button {
text: "Add row"
onClicked: {
myModel.append({col1: "some value",
col2: "Another value",
col3 : "One more value" });
}
}