Delegate

When it comes to using models and views in a custom user interface, the delegate plays a huge role in creating a look and behaviour. As each item in a model is visualized through a delegate, what is actually visible to the user are the delegates.

Each delegate gets access to a number of attached properties, some from the data model, others from the view. From the model, the properties convey the data for each item to the delegate. From the view, the properties convey state information related to the delegate within the view. Let’s dive into the properties from the view.

The most commonly used properties attached from the view are ListView.isCurrentItem and ListView.view. The first is a boolean indicating if the item is the current item, while the latter is a read-only reference to the actual view. Through access to the view, it is possible to create general, reusable delegates that adapt to the size and nature of the view in which they are contained. In the example below, the width of each delegate is bound to the width of the view, while the background color of each delegate depends on the attached ListView.isCurrentItem property.

  1. import QtQuick 6.2
  2. Rectangle {
  3. width: 120
  4. height: 300
  5. gradient: Gradient {
  6. GradientStop { position: 0.0; color: "#f6f6f6" }
  7. GradientStop { position: 1.0; color: "#d7d7d7" }
  8. }
  9. ListView {
  10. anchors.fill: parent
  11. anchors.margins: 20
  12. clip: true
  13. model: 100
  14. delegate: numberDelegate
  15. spacing: 5
  16. focus: true
  17. }
  18. Component {
  19. id: numberDelegate
  20. Rectangle {
  21. width: ListView.view.width
  22. height: 40
  23. color: ListView.isCurrentItem?"#157efb":"#53d769"
  24. border.color: Qt.lighter(color, 1.1)
  25. Text {
  26. anchors.centerIn: parent
  27. font.pixelSize: 10
  28. text: index
  29. }
  30. }
  31. }
  32. }

image

If each item in the model is associated with an action, for instance, clicking an item acts upon it, that functionality is a part of each delegate. This divides the event management between the view, which handles the navigation between items in the view, and the delegate which handles actions on a specific item.

The most basic way to do this is to create a MouseArea within each delegate and act on the onClicked signal. This is demonstrated in the example in the next section of this chapter.

Animating Added and Removed Items

In some cases, the contents shown in a view changes over time. Items are added and removed as the underlying data model is altered. In these cases, it is often a good idea to employ visual cues to give the user a sense of direction and to help the user understand what data is added or removed.

Conveniently enough, QML views attach two signals, onAdd and onRemove, to each item delegate. By triggering animations from these, it is easy to create the movement necessary to aid the user in identifying what is taking place.

The example below demonstrates this through the use of a dynamically populated ListModel. At the bottom of the screen, a button for adding new items is shown. When it is clicked, a new item is added to the model using the append method. This triggers the creation of a new delegate in the view, and the emission of the GridView.onAdd signal. The SequentialAnimation called addAnimation is started from the signal causes the item to zoom into view by animating the scale property of the delegate.

When a delegate in the view is clicked, the item is removed from the model through a call to the remove method. This causes the GridView.onRemove signal to be emitted, starting the removeAnimation SequentialAnimation. This time, however, the destruction of the delegate must be delayed until the animation has completed. To do this, PropertyAction element is used to set the GridView.delayRemove property to true before the animation, and false after. This ensures that the animation is allowed to complete before the delegate item is removed.

  1. import QtQuick 6.2
  2. Rectangle {
  3. width: 480
  4. height: 300
  5. gradient: Gradient {
  6. GradientStop { position: 0.0; color: "#dbddde" }
  7. GradientStop { position: 1.0; color: "#5fc9f8" }
  8. }
  9. ListModel {
  10. id: theModel
  11. ListElement { number: 0 }
  12. ListElement { number: 1 }
  13. ListElement { number: 2 }
  14. ListElement { number: 3 }
  15. ListElement { number: 4 }
  16. ListElement { number: 5 }
  17. ListElement { number: 6 }
  18. ListElement { number: 7 }
  19. ListElement { number: 8 }
  20. ListElement { number: 9 }
  21. }
  22. Rectangle {
  23. anchors.left: parent.left
  24. anchors.right: parent.right
  25. anchors.bottom: parent.bottom
  26. anchors.margins: 20
  27. height: 40
  28. color: "#53d769"
  29. border.color: Qt.lighter(color, 1.1)
  30. Text {
  31. anchors.centerIn: parent
  32. text: "Add item!"
  33. }
  34. MouseArea {
  35. anchors.fill: parent
  36. onClicked: {
  37. theModel.append({"number": ++parent.count});
  38. }
  39. }
  40. property int count: 9
  41. }
  42. GridView {
  43. anchors.fill: parent
  44. anchors.margins: 20
  45. anchors.bottomMargin: 80
  46. clip: true
  47. model: theModel
  48. cellWidth: 45
  49. cellHeight: 45
  50. delegate: numberDelegate
  51. }
  52. Component {
  53. id: numberDelegate
  54. Rectangle {
  55. id: wrapper
  56. width: 40
  57. height: 40
  58. gradient: Gradient {
  59. GradientStop { position: 0.0; color: "#f8306a" }
  60. GradientStop { position: 1.0; color: "#fb5b40" }
  61. }
  62. Text {
  63. anchors.centerIn: parent
  64. font.pixelSize: 10
  65. text: number
  66. }
  67. MouseArea {
  68. anchors.fill: parent
  69. onClicked: {
  70. theModel.remove(index);
  71. }
  72. }
  73. GridView.onRemove: removeAnimation.start();
  74. SequentialAnimation {
  75. id: removeAnimation
  76. PropertyAction { target: wrapper; property: "GridView.delayRemove"; value: true }
  77. NumberAnimation { target: wrapper; property: "scale"; to: 0; duration: 250; easing.type: Easing.InOutQuad }
  78. PropertyAction { target: wrapper; property: "GridView.delayRemove"; value: false }
  79. }
  80. GridView.onAdd: addAnimation.start();
  81. SequentialAnimation {
  82. id: addAnimation
  83. NumberAnimation { target: wrapper; property: "scale"; from: 0; to: 1; duration: 250; easing.type: Easing.InOutQuad }
  84. }
  85. }
  86. }
  87. }

Shape-Shifting Delegates

A commonly used mechanism in lists is that the current item is expanded when activated. This can be used to dynamically let the item expand to fill the screen to enter a new part of the user interface, or it can be used to provide slightly more information for the current item in a given list.

In the example below, each item is expanded to the full extent of the ListView containing it when clicked. The extra space is then used to add more information. The mechanism used to control this is a state expanded that each item delegate can enter, where the item is expanded. In that state, a number of properties are altered.

First of all, the height of the wrapper is set to the height of the ListView. The thumbnail image is then enlarged and moved down to make it move from its small position into its larger position. In addition to this, the two hidden items, the factsView and closeButton are shown by altering the opacity of the elements. Finally, the ListView is setup.

Setting up the ListView involves setting the contentsY, that is the top of the visible part of the view, to the y value of the delegate. The other change is to set interactive of the view to false. This prevents the view from moving. The user can no longer scroll through the list or change the current item.

As the item first is clicked, it enters the expanded state, causing the item delegate to fill the ListView and the contents to rearrange. When the close button is clicked, the state is cleared, causing the delegate to return to its previous state and re-enabling the ListView.

  1. import QtQuick 6.2
  2. Item {
  3. width: 300
  4. height: 480
  5. Rectangle {
  6. anchors.fill: parent
  7. gradient: Gradient {
  8. GradientStop { position: 0.0; color: "#4a4a4a" }
  9. GradientStop { position: 1.0; color: "#2b2b2b" }
  10. }
  11. }
  12. ListView {
  13. id: listView
  14. anchors.fill: parent
  15. delegate: detailsDelegate
  16. model: planets
  17. }
  18. ListModel {
  19. id: planets
  20. ListElement { name: "Mercury"; imageSource: "images/mercury.jpeg"; facts: "Mercury is the smallest planet in the Solar System. It is the closest planet to the sun. It makes one trip around the Sun once every 87.969 days." }
  21. ListElement { name: "Venus"; imageSource: "images/venus.jpeg"; facts: "Venus is the second planet from the Sun. It is a terrestrial planet because it has a solid, rocky surface. The other terrestrial planets are Mercury, Earth and Mars. Astronomers have known Venus for thousands of years." }
  22. ListElement { name: "Earth"; imageSource: "images/earth.jpeg"; facts: "The Earth is the third planet from the Sun. It is one of the four terrestrial planets in our Solar System. This means most of its mass is solid. The other three are Mercury, Venus and Mars. The Earth is also called the Blue Planet, 'Planet Earth', and 'Terra'." }
  23. ListElement { name: "Mars"; imageSource: "images/mars.jpeg"; facts: "Mars is the fourth planet from the Sun in the Solar System. Mars is dry, rocky and cold. It is home to the largest volcano in the Solar System. Mars is named after the mythological Roman god of war because it is a red planet, which signifies the colour of blood." }
  24. }
  25. Component {
  26. id: detailsDelegate
  27. Item {
  28. id: wrapper
  29. width: listView.width
  30. height: 30
  31. Rectangle {
  32. anchors.left: parent.left
  33. anchors.right: parent.right
  34. anchors.top: parent.top
  35. height: 30
  36. color: "#333"
  37. border.color: Qt.lighter(color, 1.2)
  38. Text {
  39. anchors.left: parent.left
  40. anchors.verticalCenter: parent.verticalCenter
  41. anchors.leftMargin: 4
  42. font.pixelSize: parent.height-4
  43. color: '#fff'
  44. text: name
  45. }
  46. }
  47. Rectangle {
  48. id: image
  49. width: 26
  50. height: 26
  51. anchors.right: parent.right
  52. anchors.top: parent.top
  53. anchors.rightMargin: 2
  54. anchors.topMargin: 2
  55. color: "black"
  56. Image {
  57. anchors.fill: parent
  58. fillMode: Image.PreserveAspectFit
  59. source: imageSource
  60. }
  61. }
  62. MouseArea {
  63. anchors.fill: parent
  64. onClicked: parent.state = "expanded"
  65. }
  66. Item {
  67. id: factsView
  68. anchors.top: image.bottom
  69. anchors.left: parent.left
  70. anchors.right: parent.right
  71. anchors.bottom: parent.bottom
  72. opacity: 0
  73. Rectangle {
  74. anchors.fill: parent
  75. gradient: Gradient {
  76. GradientStop { position: 0.0; color: "#fed958" }
  77. GradientStop { position: 1.0; color: "#fecc2f" }
  78. }
  79. border.color: '#000000'
  80. border.width: 2
  81. Text {
  82. anchors.fill: parent
  83. anchors.margins: 5
  84. clip: true
  85. wrapMode: Text.WordWrap
  86. color: '#1f1f21'
  87. font.pixelSize: 12
  88. text: facts
  89. }
  90. }
  91. }
  92. Rectangle {
  93. id: closeButton
  94. anchors.right: parent.right
  95. anchors.top: parent.top
  96. anchors.rightMargin: 2
  97. anchors.topMargin: 2
  98. width: 26
  99. height: 26
  100. color: "#157efb"
  101. border.color: Qt.lighter(color, 1.1)
  102. opacity: 0
  103. MouseArea {
  104. anchors.fill: parent
  105. onClicked: wrapper.state = ""
  106. }
  107. }
  108. states: [
  109. State {
  110. name: "expanded"
  111. PropertyChanges { target: wrapper; height: listView.height }
  112. PropertyChanges { target: image; width: listView.width; height: listView.width; anchors.rightMargin: 0; anchors.topMargin: 30 }
  113. PropertyChanges { target: factsView; opacity: 1 }
  114. PropertyChanges { target: closeButton; opacity: 1 }
  115. PropertyChanges { target: wrapper.ListView.view; contentY: wrapper.y; interactive: false }
  116. }
  117. ]
  118. transitions: [
  119. Transition {
  120. NumberAnimation {
  121. duration: 200;
  122. properties: "height,width,anchors.rightMargin,anchors.topMargin,opacity,contentY"
  123. }
  124. }
  125. ]
  126. }
  127. }
  128. }

image

image

The techniques demonstrated here to expand the delegate to fill the entire view can be employed to make an item delegate shift shape in a much smaller way. For instance, when browsing through a list of songs, the current item could be made slightly larger, accommodating more information about that particular item.