QML ListView anchor, margin = 0 goes outside parent fill? - qt

I was wondering why this draws the Icon components outside of the Item fill? Using Qt 5.5.1
import QtQuick 2.5
Item {
id: root
// width: 960 // Actually set in the higher level view
// height: 540 // Actually set in the higher level view
ListModel {
id: items
ListElement { icon: 'a.png', label: 'Example', x: 0, y: 0 }
ListElement { icon: 'b.png', label: 'Something', x: 100, y: 0 }
ListElememt { icon: 'c.png', label: 'Testing', x: 200, y: 50 }
}
ListView {
model: items
delegate: Icon {
text: model.label
iconSrc: model.icon
anchors {
top: parent.top
left: parent.left
leftMargin: model.x
topMargin: model.y
}
}
anchors {
top: parent.top
left: parent.left
}
}
}
The first two icons (y = 0) will appear about 15 pixels outside of the containing Item when it is inside a higher level view.
If I do it the below way (without ListView), they appear in the correct position (y = 0), ie. they will be inside of the containing Item when used in the higher level view.
import QtQuick 2.5
Item {
id: root
// width: 960 // Actually set in the higher level view
// height: 540 // Actually set in the higher level view
Icon {
text: 'Example'
iconSrc: 'a.png'
anchors {
top: parent.top
left: parent.left
leftMargin: 0
topMargin: 0
}
}
Icon {
text: 'Something'
iconSrc: 'b.png'
anchors {
top: parent.top
left: parent.left
leftMargin: 100
topMargin: 0
}
}
Icon {
text: 'Testing'
iconSrc: 'c.png'
anchors {
top: parent.top
left: parent.left
leftMargin: 200
topMargin: 50
}
}
}
Why do the Icon components render in a different position when generated inside a ListView compared to direct definitions, when using the same "X, Y" values for both?

The objective of the ListView is to show a set of items as a list so the anchors cannot be established since the geometry is handled by the ListView, instead Repeater must be used:
import QtQuick 2.5
Item {
id: root
// width: 960 // Actually set in the higher level view
// height: 540 // Actually set in the higher level view
ListModel{
id: items
ListElement { icon: 'a.png'; label: 'Example'; x: 0; y: 0 }
ListElement { icon: 'b.png'; label: 'Something'; x: 100; y: 0 }
ListElement { icon: 'c.png'; label: 'Testing'; x: 200; y: 50 }
}
Repeater {
model: items
delegate: Icon {
text: model.label
source: model.icon
anchors {
top: parent.top
left: parent.left
leftMargin: model.x
topMargin: model.y
}
}
}
}

Related

QML Dynamically Update ListModel

I'm confused at some point in QML as I'm new to it. The simplified QML codes define a window that includes one ListView which includes my delegate Items. One button loads and unloads this ListView Element. When I load and unload the ListView Element, all text that I wrote inside the TextInput is reset from listModel as I expected. So I need to dynamically update listModel thus I will not lose the text that I wrote in TextInput. I added a Keys.onPressed in TextInput to achieve that. But it works with some logical error. When I type, let's say "aaaa", then I unload the ListView and load again via Button, what I get is "aaa" (the last letter is not passed to listModel). This is understandable but how can I update the list model roles dynamically in this example?
main.qml
ApplicationWindow {
id: applicationWindow
width: 300
height: 200
visible: true
title: qsTr("01_Change_Model_Data")
ListModel {
id: listModel1
ListElement {labelText: "Text Field 1:"; textInput_text : "This is text 1"}
ListElement {labelText: "Text Field 2:"; textInput_text : "This is text 2"}
}
Button {
id: loadUnloadBtn
height: 24
width: 50
text: "Load"
anchors {
right: parent.right
rightMargin: 20
top: parent.top
topMargin: 10
}
onClicked: {
if(listAreaLoader.source == "") {
listAreaLoader.source = "ListArea.qml"
}else {
listAreaLoader.source = ""
}
}
}
Loader {
id: listAreaLoader
anchors {
top: parent.top
topMargin: 10
bottom: parent.bottom
bottomMargin: 10
left: parent.left
leftMargin: 10
right: parent.right
rightMargin: 80
}
source: ""
}
}
ListArea.qml:
Item {
id: listViewDelegate
ListView {
id: listView1
anchors.fill: parent
model: listModel1
delegate: listElementDelegate
spacing: 6
}
Component {
id: listElementDelegate
Rectangle {
color: "#00000000"
height: 24
width: 50
Label {
id: label1
text: labelText
}
Rectangle {
color: "grey"
radius: 4
width: 100
height: 20
anchors {
verticalCenter: label1.verticalCenter
left: label1.right
leftMargin: 10
}
TextInput {
id: textInput1
anchors.fill: parent
leftPadding: 5
rightPadding: 5
clip: true
verticalAlignment: Text.AlignVCenter
text: textInput_text
Keys.onPressed: {
listView1.currentIndex = index
listModel1.setProperty(index, "textInput_text", textInput1.text)
}
}
}
}
}
}

QML ListView: Binding loop detected for property "height"

I have a QML ListView, and I'm trying to dynamically add elements to it. I want the background rectangle to also scale dynamically as elements are added/removed from the ListView. Right now I get a binding loop, and I understand what they are but I can't figure out where it's coming from. I played around changing the code a bit and I was able to get rid of the binding loop one time but then the ListView couldn't be scrolled. Anyone have any ideas?
import QtQuick 2.15
import QtQuick.Window 2.0
Window {
visible: true
width: 800
height: 800
Rectangle {
id: listContainer
height: childrenRect.height
width: parent.width
color: "transparent"
anchors {
top: parent.top
topMargin: 30
left: parent.left
leftMargin: 45
}
ListView {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
model: myModel
height: childrenRect.height
header:
Text {
z: 2
height: 50
text: "HEADER"
color: "black"
}
delegate: Component {
Item {
Text {
id: userName;
text: name;
color: "black";
font.pixelSize: 50
anchors {
left: parent.left
leftMargin: 20
}
}
Rectangle {
height: 1
color: 'black'
width: listContainer.width
anchors {
left: userName.left
top: userName.top
topMargin: - 12
leftMargin: -15
}
}
}
}
spacing: 80
}
}
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
})
}
}
}
EDIT: As suggested by #Aditya, the binding loop can be removed by having a static ListView height, but I don't want it to be that way. I'm using the rectangle as a background for the ListView and I want it to scale according to the ListView. For example, if I only add two elements, I want the rectangle to also scale for those two elements and not cover the entire screen. This causes a problem:
import QtQuick 2.15
import QtQuick.Window 2.0
Window {
visible: true
width: 800
height: 800
Rectangle {
id: listContainer
height: childrenRect.height
width: parent.width
color: "yellow"
anchors {
top: parent.top
topMargin: 30
left: parent.left
leftMargin: 45
}
ListView {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
model: myModel
height: 800//childrenRect.height
header:
Text {
z: 2
height: 50
text: "HEADER"
color: "black"
}
delegate: Component {
Item {
Text {
id: userName;
text: name;
color: "black";
font.pixelSize: 50
anchors {
left: parent.left
leftMargin: 20
}
}
Rectangle {
height: 1
color: 'black'
width: listContainer.width
anchors {
left: userName.left
top: userName.top
topMargin: - 12
leftMargin: -15
}
}
}
}
spacing: 80
}
}
ListModel {
id: myModel
}
/* Fill the model with default values on startup */
Component.onCompleted: {
for (var i = 0; i < 2; i++) {
myModel.append({
name: "Big Animal : " + i
})
}
}
}
I also tried separating the header from ListView into a different component and anchoring the listview below it and that worked. The only problem was it could not be scrolled with the listview. Worst case, I could make a scrolling animation for it but that seems like an inefficient solution and I'd like to know why this doesn't work.
You are probably also biting yourself with the Item as the top-level in the delegate, since that doesn't give any implicit size, which the ListView uses to calculate the scrolling needs. You can simply use Text directly as the delegate (you don't need the Component either) and put the line/rectangle inside. If doing so you can use the contentHeight property of ListView to size the background.
Furthermore, I would suggest to have the ListView as the top level and do any styling secondary, with which I mean, put the background Rectangle inside.
import QtQuick 2.12
import QtQuick.Window 2.12
Window {
width: 640
height: 480
visible: true
title: qsTr("Hello World")
ListView {
id: listView
model: 3
anchors.fill: parent
Rectangle { //background
color: "yellow"
z: -1
width: listView.width
height: listView.contentHeight
}
delegate: Text {
text: "name" + index
color: "black";
font.pixelSize: 50
leftPadding: 20
Rectangle {
height: 1
color: 'black'
width: listView.width
y: - 12
x: -15
}
}
spacing: 80
}
}
Btw, if you are going to put the ListView in some RowLayout or something, you probably also want implicitHeight: contentHeight in the ListView.
The binding loop is originating from the ListView's height: childrenRect.height statement. It looks like the ListView needs to be a fixed height, or at least not dependent on childrenRect. It is most likely how the ListView element knows that the view should be scrollable to view elements below.
It really depends on what you're trying to achieve with setting the height to match childrenRect, but in my case, ListView height is changing based on the children (per your desire presumably). With a 100 items the height came out to be 7970. With 5 items in the model, the result was 350. You can check this by adding a debug or console.log() with onHeightChanged However, as a result of this scaling, the ListView is assumed to be big enough to view the entire data set regardless of the window parent container size.
You do not need to scale the ListView height to match the contents; that is what it is built for. It allows scrolling because the contents are too big to be shown within its limited height.
I was able to achieve get rid of the binding loop and be able to scroll by simply changing the statement to a static value, which is the parent height of 800 as an example:
Window {
visible: true
width: 800
height: 800
Rectangle {
id: listContainer
height: childrenRect.height
width: parent.width
color: "transparent"
anchors {
top: parent.top
topMargin: 30
left: parent.left
leftMargin: 45
}
ListView {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
model: myModel
height: 800//childrenRect.height
header:
Text {
z: 2
height: 50
text: "HEADER"
color: "black"
}
delegate: Component {
Item {
Text {
id: userName;
text: name;
color: "black";
font.pixelSize: 50
anchors {
left: parent.left
leftMargin: 20
}
}
Rectangle {
height: 1
color: 'black'
width: listContainer.width
anchors {
left: userName.left
top: userName.top
topMargin: - 12
leftMargin: -15
}
}
}
}
spacing: 80
}
}
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
})
}
}
}
Edit:
I feel like you're trying to just secure a background for a scalable ListView. Having a static background as a container works but not very well for modern unser interfaces - any bounce effects or such will not move the rectangle. You could achieve this by anchoring the rectangle to the ListView element but it is a very roundabout way. Instead, you could just set a rectangle to style each element of the ListView delegate instead.
delegate: Component {
Item {
Rectangle{
width: listContainer.width
height: userName.height+13
//add 13 to adjust for margin set below
anchors {
left: userName.left
top: userName.top
topMargin: - 12
leftMargin: -15
//just copying from the other rectangle below
}
gradient: Gradient {
//I am just using gradient here for a better understanding of spacing. You could use color.
GradientStop { position: 0.0; color: "aqua" }
GradientStop { position: 1.0; color: "green" }
}
}
Text {
id: userName;
text: name;
color: "black";
font.pixelSize: 50
anchors {
left: parent.left
leftMargin: 20
}
}
Rectangle {
height: 1
color: 'black'
width: listContainer.width
anchors {
left: userName.left
top: userName.top
topMargin: - 12
leftMargin: -15
}
}
}
}
This will make sure that the rectangle background behind the ListView will look like it is scrolling with the items. In reality we have broken one rectangle into multiple and just set each element with one. You can also use this type of styling to achieve alternate colors in your list for example.

Qt Quick - What is the correct way to create a GridView with variable row height?

I'm Developing an application with Qt 5.13.0 on a Windows 10 professional computer. I need to create a Whatsapp like message view, and to achieve that, I used a GridView component as a base. However, in order to draw the messages correctly, I need to create each GridView row with a different height, depending on the message text.
But to my surprise, I could not find any solution on the internet to achieve that, although I thought it was a simple formality. I tried a solution by myself, which I attached below, however it doesn't work. The issue is that all the rows take the height of the last resized one.
I have no doubt that Qt can do this, unfortunately, I have been looking for days now, and I can not find a solution to this issue. I simply have no idea about how to achieve that. So someone can explain to me how to create a GridView with variable height rows, or if the GridWiew isn't the appropriate component to do that, whcih I should use instead?
Here is my qss file:
import QtQuick 2.12
import QtQuick.Controls 2.5
import QtQuick.Window 2.12
import QtQuick.Layouts 1.11
Window
{
visible: true
width: 640
height: 480
title: qsTr("Grid view")
color: "#ffffff"
ColumnLayout
{
transformOrigin: Item.Center
spacing: 0
x: 0
y: 0
width: parent.width
height: parent.height
/**
* Grid view item
*/
Component
{
id: itGridItem
Item
{
Column
{
Rectangle
{
property int messageWidth: (gvMessageGrid.cellWidth / 2) - 50
id: itemRect
x: senderIsMyself ? 25 : gvMessageGrid.cellWidth - (25 + messageWidth)
y: 5
width: messageWidth
height: itemTextID.height + 20
color: senderIsMyself ? "#d5d5d5" : "#800b940e"
radius: 5
clip: true
Text
{
id: itemTextID
width: parent.width - 20
text: itemText
renderType: Text.NativeRendering
textFormat: TextEdit.RichText
wrapMode: Text.WordWrap
font.family: "Segoe UI Emoji"
font.pixelSize: 18
anchors.margins: 10
anchors.left: parent.left
anchors.top: parent.top
color: "#101010"
}
onHeightChanged: gvMessageGrid.cellHeight = height + 10
}
}
}
}
/**
* Messages grid view
*/
GridView
{
id: gvMessageGrid
y: 0
Layout.fillHeight: true
flickableDirection: Flickable.VerticalFlick
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
clip: true
contentWidth: 700
contentHeight: 300
cellWidth: contentWidth
cellHeight: 150
model: lmGridModel
delegate: itGridItem
ScrollBar.vertical: ScrollBar
{
visible: true
}
onWidthChanged: cellWidth = width
}
}
}
--- Edited on October 18, 2019
Following the eyllanesc suggestion below, here are 2 screenshots of what I want and what I get:
what I want
what I get
NOTE I'm using several languages, for test purposes, because my application must have international support. However the issue I face has nothing to do with that, i.e it's not an error with text calculation, because 1. The green rect sizes around the text are always correct, and 2. I face the exact same issue with pure English text.
I suggest you to use ListView instead of GridView. You can easily view anything inside full-width list item. Let me share with you my ListDelegate class from an old project. Pay attention to usage of isIncoming property in anchor binding, for example:
anchors {
left: isIncoming? undefined : parent.left
right: isIncoming? parent.right : undefined
}
Full listing:
import QtQuick 2.5
import "units.js" as Units
Rectangle {
id: chatMsgDelegRoot
property bool isIncoming: !model.out
property bool isSelected: model.isSelected
signal clicked(int index)
width: parent.width
height: dlgColumn.height + Units.gu(2.5)
color: "#edf1f5"
Column {
id: dlgColumn
spacing: Units.gu(4)
width: parent.width
anchors.verticalCenter: parent.verticalCenter
BorderImage {
id: borderImage
source: isIncoming?
(isSelected ? "/images/img/MsgOut_Selected_2.png" : "/images/img/MsgOut_2.png") :
(isSelected ? "/images/img/MsgIn_Selected_2.png" : "/images/img/MsgIn_2.png")
// Texture-dependent.
border {
left: isIncoming? 20 : 30
top: 20
right: isIncoming? 30 : 20
bottom: 35
}
anchors {
left: isIncoming? undefined : parent.left
right: isIncoming? parent.right : undefined
}
width: Math.max(content.width + Units.gu(15), Units.gu(21))
height: content.height + Units.gu(9)
MouseArea {
id: msgDelegateMa
anchors.fill: parent
onClicked: chatMsgDelegRoot.clicked(model.index)
}
Loader {
id: content
sourceComponent: model.type === "Text" ? textComponent : controlComponent
anchors {
left: isIncoming? undefined : parent.left
right: isIncoming? parent.right : undefined
leftMargin: Units.gu(10)
rightMargin: Units.gu(10)
top: parent.top
topMargin: Units.gu(4)
}
}
Text {
text: model.date.toTimeString()
font.pointSize: 8
font.italic: true;
color: "lightgrey"
anchors {
left: isIncoming? undefined : parent.left
right: isIncoming? parent.right : undefined
rightMargin: Units.gu(7.5)
leftMargin: Units.gu(7.5)
bottom: parent.bottom
bottomMargin: Units.gu(1)
}
}
} // BorderImage
} // Column
// TODO To separate files.
Component {
id: textComponent
Rectangle {
id: textComponentRoot
color: "#00000000"
width: msgText.paintedWidth
height: msgText.height
Text {
id: msgText
font.pointSize: 10
textFormat: Text.RichText
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
width: chatMsgDelegRoot.width * 0.7
text: model.body
color: isSelected? "white" : "black"
}
}
} // Component
Component {
id: controlComponent
Rectangle {
id: textComponentRoot
color: "#00000000"
width: innerColumn.width
height: innerColumn.height
Column {
id: innerColumn
spacing: Units.gu(1)
Text {
id: fileNameText
font.pointSize: 10
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
width: chatMsgDelegRoot.width * 0.7
elide: Text.ElideRight
text: "File transfer: " + model.body
color: isSelected? "white" : "black"
}
Row {
id: innerRow
anchors.right: parent.right
spacing: Units.gu(1)
SimpleButton {
id: allowBtn
width: Units.gu(15)
height: Units.gu(8)
text: "Allow"
}
SimpleButton {
id: denyBtn
width: Units.gu(15)
height: Units.gu(8)
text: "Deny"
}
}
} // Column
}
} // Component
}

How to keep last items in view when adding them to listview?

I have a ListView where I dynamically add ListElements. The ListView has a maximum of 10 items that can be in view, so I also have a ScrollBar. When I add the 11th+ item, I always want it to scroll into view.
ListView {
id: logListView
delegate: logListViewDelegate
model: logListModel
anchors.fill: parent
ScrollBar.vertical: ScrollBar {}
}
ListModel {
id: logListModel
}
Component {
id: logListViewDelegate
Item {
height: 44
width: logListView.width
Text {
id: countText
width: 18
font {
pixelSize: 16
family: variables.globalFont
}
color: colors.foregroundColor3
text: index+1
anchors {
left: parent.left
leftMargin: 7
verticalCenter: parent.verticalCenter
}
}
Text {
id: timeText
width: 96
horizontalAlignment: Text.AlignRight
font {
pixelSize: 24
family: variables.globalFont
}
color: colors.foregroundColor1
text: time
anchors {
left: countText.right
verticalCenter: parent.verticalCenter
}
}
Text {
id: unitText
width: 18
font {
pixelSize: 16
family: variables.globalFont
}
color: colors.foregroundColor3
text: unit
anchors {
left: timeText.right
leftMargin: 6
bottom: timeText.bottom
bottomMargin: 2
}
}
}
}
I have a button outside of the listview that when clicked just does:
logListModel.append({
time: myTime, unit: myUnit
})
The new item just gets added to the bottom of the list and is hidden when there are more than 10. When adding an item , I would like the list to automatically scroll to it.
In your ListView, scroll to the bottom by changing the currentIndex when onCountChanged is called (when your model has changed):
ListView {
id: logListView
delegate: logListViewDelegate
model: logListModel
anchors.fill: parent
ScrollBar.vertical: ScrollBar {}
onCountChanged: {
var newIndex = count - 1 // last index
positionViewAtEnd()
currentIndex = newIndex
}
}

Change Text Color of the selected item in a ListView

Let me start by saying that I am pretty new to QML.
I have a ListView (with model and delegate), it works fine in my model but I would like to change the color (currently color: skin.gray) of the selected item to something else when the item is the currentIndex-item.
ListView {
id: menuBody_listview
width: parent.width
height: parent.height
currentIndex: 0
clip: true
highlight: highlighter
highlightFollowsCurrentItem: true
Behavior on opacity {
NumberAnimation { property: "opacity"; duration: 300; easing.type: Easing.InOutQuad }
}
anchors {
top: menuHeader_listview.bottom
bottom: parent.bottom
}
model: ListModel {
ListElement {
itemIconLeft: 'images/icons/menu/pause.png'
itemText: "Cancel"
itemIconRight: 'images/icons/menu/take-me-home.png'
}
ListElement {
itemIconLeft: 'images/icons/menu/pause.png'
itemText: "Mute"
itemIconRight: 'images/nill.png'
}
ListElement {
itemIconLeft: 'images/icons/menu/repeat.png'
itemText: "Repeate"
itemIconRight: 'images/nill.png'
}
}
delegate: MenuBodyItem {
width: menuBody_listview.width
anchors.horizontalCenter: parent.horizontalCenter
iconLeft: itemIconLeft
message: itemText
iconRight: itemIconRight
}
}
Following is the code for the item which is being populated, ManuBodyItem.qml.
Item {
width: 100
height: 50
property alias iconLeft: menuitem_icon_start.source
property alias message: menuitem_text.text
property alias iconRight: menuitem_icon_end.source
RowLayout {
spacing: 20
anchors.fill: parent
Image {
id: menuitem_icon_start
fillMode: Image.PreserveAspectCrop
anchors {
left: parent.left
verticalCenter: parent.verticalCenter
}
}
Text {
id: menuitem_text
anchors {
left: menuitem_icon_start.right
verticalCenter: parent.verticalCenter
verticalCenterOffset: -2
leftMargin: 20
}
color: skin.gray
font {
family: "TBD"
}
}
Image {
id: menuitem_icon_end
fillMode: Image.PreserveAspectCrop
source: iconRight
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
}
}
}
}
Use ListView's isCurrentItem attached property:
Text {
id: menuitem_text
anchors {
left: menuitem_icon_start.right
verticalCenter: parent.verticalCenter
verticalCenterOffset: -2
leftMargin: 20
}
color: itemDelegate.ListView.isCurrentItem ? "red" : skin.gray
font {
family: "TBD"
}
}
Note that you have to give your root delegate item an ID in order to qualify the expression above:
Item {
id: itemDelegate
RowLayout {
// ...
}
// ...
}
You can see the same approach used in the example I linked to.
From your example:
color: skin.gray is used for the Text element which will change the color of the text and not it's background viz. i understood you want.
You can use a Rectangle element here which can act as a background component to set the background color.
So instead of Item root element in the delegate you can use Rectangle. So MenuBodyItem.qml will look as
Rectangle {
width: 100
height: 50
...
}
Now to set background color to the Rectangle if it is current one you can use ListView.isCurrentItem to check.
So,
Rectangle {
color: ListView.isCurrentItem ? "cyan" : "lightblue"
width: 100
height: 50
}
and now finally you will have to set the clicked item as the current one which can be done in the MouseArea of the Delegate Item
delegate: MenuBodyItem {
width: menuBody_listview.width
anchors.horizontalCenter: parent.horizontalCenter
iconLeft: itemIconLeft
message: itemText
iconRight: itemIconRight
MouseArea {
anchors.fill: parent
onClicked: menuBody_listview.currentIndex = index
}
}

Resources