QML TreeView with custom delegate
-
Hi!
I'm trying to work with QML TreeView in Qt 6.8, but I'm having issues and can't make progress.
I want to create a hierarchy like the one shown in the screenshot, which includes:- an expand/collapse indicator
- an icon
- text
- the number of items (for root nodes) or the size (for leaf nodes)
To start, I'm trying to solve a simple problem: aligning the size value to the right edge. However, any attempts to use anchors result in the entire list shifting somewhere, and I can't fix it.
The second problem is: how can I add an image?
Does anyone have examples of using TreeView with custom delegates? I can customize them perfectly for ListView, but I can't figure out how they work with TreeView.
Thanks for any advice!
Right now my code looks like this:
import QtQuick import QtQuick.Controls import Qt.labs.qmlmodels Item { id: root width: 260 height: 700 TreeView { id: treeView anchors.fill: parent model: assetModel selectionModel: ItemSelectionModel { model: treeView.model } delegate: DelegateChooser { id: viewDelegate DelegateChoice { id: nameDelegateChoice column: 0 delegate: TreeViewDelegate { id: nameDelegate implicitWidth: 150 background: Rectangle { id: backgroundRectangle color: "transparent" } contentItem: Row { id: row Image { source: "../../images/svg-folder.svg" width: 20 height: 20 } Label { text: model.display color: "white" font.pixelSize: 14 font.family: "Roboto Medium" } } } } DelegateChoice { id: sizeDelegateChoice column: 1 delegate: TreeViewDelegate { id: treeViewDelegate background: Rectangle { id: backgroundRectangle2 color: "transparent" } contentItem: Row { id: row2 Label { text: model.display color: "white" font.pixelSize: 14 font.family: "Roboto Medium" } } } } } } }
Model:
#include "AssetModel.hpp" #include <QVariantList> #include "AssetItem.hpp" AssetModel::AssetModel(QObject *parent) : QAbstractItemModel{parent} , m_rootItem(std::make_unique<AssetItem>("RootDir", AssetItem::Type::Directory)) { setupModelData(); } AssetModel::~AssetModel() = default; QVariant AssetModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) { return QVariant(); } AssetItem *item = static_cast<AssetItem *>(index.internalPointer()); if (role == Qt::DisplayRole) { return item->data(index.column()); } return QVariant(); } Qt::ItemFlags AssetModel::flags(const QModelIndex &index) const { if (!index.isValid()) { return Qt::NoItemFlags; } return Qt::ItemIsSelectable | Qt::ItemIsEnabled; } QVariant AssetModel::headerData(int section, Qt::Orientation orientation, int role) const { return orientation == Qt::Horizontal && role == Qt::DisplayRole ? m_rootItem->data(section) : QVariant{}; } QModelIndex AssetModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) { return QModelIndex(); } AssetItem *parentItem = parent.isValid() ? static_cast<AssetItem *>(parent.internalPointer()) : m_rootItem.get(); AssetItem *childItem = parentItem->child(row); return childItem ? createIndex(row, column, childItem) : QModelIndex(); } QModelIndex AssetModel::parent(const QModelIndex &index) const { if (!index.isValid()) { return QModelIndex(); } AssetItem *childItem = static_cast<AssetItem *>(index.internalPointer()); AssetItem *parentItem = childItem->parentItem(); if (parentItem == m_rootItem.get()) { return QModelIndex(); } return createIndex(parentItem->row(), 0, parentItem); } int AssetModel::rowCount(const QModelIndex &parent) const { AssetItem *parentItem = parent.isValid() ? static_cast<AssetItem *>(parent.internalPointer()) : m_rootItem.get(); return parentItem->childCount(); } int AssetModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) return 2; // Columns: name and size } // void AssetModel::setupModelData(const QList<QStringView> &lines, AssetItem *parent) void AssetModel::setupModelData() { // Example auto dir1 = std::make_unique<AssetItem>("Uploads", AssetItem::Type::Directory, m_rootItem.get()); dir1->appendChild( std::make_unique<AssetItem>("logo.png", AssetItem::Type::File, dir1.get(), 1024)); dir1->appendChild( std::make_unique<AssetItem>("Arial.ttf", AssetItem::Type::File, dir1.get(), 4332523)); dir1->appendChild( std::make_unique<AssetItem>("Hello.zip", AssetItem::Type::File, dir1.get(), 3245452)); dir1->appendChild( std::make_unique<AssetItem>("Intro.mp3", AssetItem::Type::File, dir1.get(), 53245132)); dir1->appendChild( std::make_unique<AssetItem>("mem.gif", AssetItem::Type::File, dir1.get(), 35353)); dir1->appendChild( std::make_unique<AssetItem>("File.xml", AssetItem::Type::File, dir1.get(), 77332)); auto dir2 = std::make_unique<AssetItem>("Assets", AssetItem::Type::Directory, m_rootItem.get()); dir2->appendChild( std::make_unique<AssetItem>("avatar_1.png", AssetItem::Type::File, dir2.get(), 43245)); dir2->appendChild( std::make_unique<AssetItem>("avatar_2.png", AssetItem::Type::File, dir2.get(), 73412)); m_rootItem->appendChild(std::move(dir1)); m_rootItem->appendChild(std::move(dir2)); }
And TreeView looks like this:
-
Hi!
Thank you for your responses!
Unfortunately, the documentation for TreeView is not very detailed, and examples are quite scarce. I followed your advice and dug deeper into how the delegate works for TreeView. As a result, I managed to achieve exactly what I needed, as shown in the original screenshot.
To help others understand this topic more easily, I’ll share my code below.
Key ideas:
- The entire view layout can be customized using the delegate. There’s no need to come up with complex solutions like DelegateChooser, which I had been trying earlier.
- You can use the column and row variables to determine which row or column is being processed.
- As mentioned earlier, to align text, you should use horizontalAlignment.
- You need to align the row yourself (make indents depending on the nesting).
import QtQuick import QtQuick.Controls import Qt5Compat.GraphicalEffects Item { id: root width: 260 height: 700 TreeView { id: treeView anchors.fill: parent anchors.margins: 10 clip: true model: assetModel selectionModel: ItemSelectionModel { model: treeView.model } delegate: Item { implicitWidth: column === 0 ? root.width * 2/3 - padding * 2 : root.width * 1/3 - padding * 2 implicitHeight: label.implicitHeight * 1.5 readonly property real indentation: 20 readonly property real padding: 5 // Assigned to by TreeView: required property TreeView treeView required property bool isTreeNode required property bool expanded required property int hasChildren required property int depth required property int row required property int column required property bool current property Animation indicatorAnimation: NumberAnimation { target: indicator property: "rotation" from: expanded ? 0 : 90 to: expanded ? 90 : 0 duration: 100 easing.type: Easing.OutQuart } TableView.onPooled: indicatorAnimation.complete() TableView.onReused: if (current) indicatorAnimation.start() onExpandedChanged: indicator.rotation = expanded ? 90 : 0 Rectangle { id: background anchors.fill: parent color: row === treeView.currentRow ? "#0085F8" : "transparent" } Label { id: indicator x: padding + (depth * indentation) + 5 anchors.verticalCenter: parent.verticalCenter visible: isTreeNode && hasChildren text: "▶" color: "white" TapHandler { onSingleTapped: { let index = treeView.index(row, column) treeView.selectionModel.setCurrentIndex(index, ItemSelectionModel.NoUpdate) treeView.toggleExpanded(row) } } } Image { id: image x: (isTreeNode ? (depth + 1) * indentation : 0) anchors.verticalCenter: parent.verticalCenter width: 13 height: 13 source: hasChildren ? "../../images/svg-folder.svg" : "../../images/svg-file.svg" visible: column === 0 ? true : false ColorOverlay { source: image anchors.fill: image color: "white" } } Label { id: label x: image.x + image.width + 5 anchors.verticalCenter: parent.verticalCenter width: parent.width - padding - x clip: true text: model.display color: column === 0 ? "white" : "#ABABAB" font.pixelSize: column === 0 ? 14 : 10 horizontalAlignment: isTreeNode ? Text.AlignLeft : Text.AlignRight rightPadding: 10 font.family: "Roboto Medium" } } MouseArea { id: menuMouseArea anchors.fill: parent acceptedButtons: Qt.RightButton Connections { target: menuMouseArea onClicked: { menu.open() menu.x = menuMouseArea.mouseX menu.y = menuMouseArea.mouseY } } } } Menu { id: menu width: 150 leftPadding: 5 font.family: "Roboto Medium" palette.window: "#1f1f1f" palette.text: "white" palette.windowText: "white" palette.light: "#898989" MenuItem { text: "Rename" icon.source: "../../images/svg-edit.svg" icon.width: 14 icon.height: 14 } MenuItem { text: "Delete" icon.source: "../../images/svg-trash.svg" icon.width: 14 icon.height: 14 } } }
Now it looks like this:
Thank you all!
-
@oYASo said in QML TreeView with custom delegate:
To start, I'm trying to solve a simple problem: aligning the size value to the right edge. However, any attempts to use anchors result in the entire list shifting somewhere, and I can't fix it.
I found it a bit tricky to unpick what it is you are asking. It looks like the issue mentioned above is not illustrated in the code you provided - correct?
So the main issue you discuss is the image one. I am not saying that what you have done is incorrect but in my projects I have always included images in my resources rather than trying to find them as relative paths. My
source:
settings then look something like this:Image { source: "qrc:///icons/arrow-up.svg" }
This corresponds to a line like this in my
qml.qrc
:<file>icons/arrow-up.svg</file>
The actual image file in my project is at the path specified above, relative to the location of the
qml.qrc
.Something looks odd to me in your
DelegateChooser
code, though it might be because I don't have much experience with using this component. I thought that theDelegateChooser
was supposed to define a role name for the role that will be used to select the choice, and that theDelegateChoices
would each specify aroleValue
to determine which value of the role they should correspond to. I'm also not entirely sure that you are using columns correctly in your model and view but again it could just be my ignorance.To make progress, I would simplify this as much as possible - perhaps forget about the delegate choice in the first instance, and focus on getting the image displaying.
-
@Bob64 Thank you for your response, and sorry for the unclear explanation of my issue.
The first problem I’m facing is how to align content to the left or right in a TreeView column. The first screenshot in my post shows how I’d like it to look, while the second screenshot shows the current state. On the second screenshot, you can see that the file size is aligned to the left, but I need it to be aligned to the right.
I tried solving this issue by using alignment in the model, specifically with the TextAlignmentRole, but it seems like this doesn’t work.
I also attempted to achieve this by aligning the contentItem in the delegate, but this didn’t work either.
So, my initial question is: how can I align the second column in a TreeView to the right?
-
OK, thanks for the clarification.
It seems like this should not be too difficult. If I understand correctly now, the tree has two columns and second delegate choice is for the second column.
I would probably start by removing the
Row
from the content item of the second column as it doesn't seem to be serving much purpose and will get in the way of using other layout mechanisms like anchors. I would replace it with anItem
that fills the available width. You should then be able to anchor the text to the right of the containing item. -
TreeView and TreeViewDelegate ignores the TextAlignmentRole.
Getting rid of the Row and using the Label with horizontalAlignment should do the trick:
delegate: TreeViewDelegate { background: null // there's no point in using a Rectangle with a transparent background contentItem: Label { text: model.display horizontalAlignment: Text.AlignRight color: "white" font.pixelSize: 14 font.family: "Roboto Medium" } }
-
Hi!
Thank you for your responses!
Unfortunately, the documentation for TreeView is not very detailed, and examples are quite scarce. I followed your advice and dug deeper into how the delegate works for TreeView. As a result, I managed to achieve exactly what I needed, as shown in the original screenshot.
To help others understand this topic more easily, I’ll share my code below.
Key ideas:
- The entire view layout can be customized using the delegate. There’s no need to come up with complex solutions like DelegateChooser, which I had been trying earlier.
- You can use the column and row variables to determine which row or column is being processed.
- As mentioned earlier, to align text, you should use horizontalAlignment.
- You need to align the row yourself (make indents depending on the nesting).
import QtQuick import QtQuick.Controls import Qt5Compat.GraphicalEffects Item { id: root width: 260 height: 700 TreeView { id: treeView anchors.fill: parent anchors.margins: 10 clip: true model: assetModel selectionModel: ItemSelectionModel { model: treeView.model } delegate: Item { implicitWidth: column === 0 ? root.width * 2/3 - padding * 2 : root.width * 1/3 - padding * 2 implicitHeight: label.implicitHeight * 1.5 readonly property real indentation: 20 readonly property real padding: 5 // Assigned to by TreeView: required property TreeView treeView required property bool isTreeNode required property bool expanded required property int hasChildren required property int depth required property int row required property int column required property bool current property Animation indicatorAnimation: NumberAnimation { target: indicator property: "rotation" from: expanded ? 0 : 90 to: expanded ? 90 : 0 duration: 100 easing.type: Easing.OutQuart } TableView.onPooled: indicatorAnimation.complete() TableView.onReused: if (current) indicatorAnimation.start() onExpandedChanged: indicator.rotation = expanded ? 90 : 0 Rectangle { id: background anchors.fill: parent color: row === treeView.currentRow ? "#0085F8" : "transparent" } Label { id: indicator x: padding + (depth * indentation) + 5 anchors.verticalCenter: parent.verticalCenter visible: isTreeNode && hasChildren text: "▶" color: "white" TapHandler { onSingleTapped: { let index = treeView.index(row, column) treeView.selectionModel.setCurrentIndex(index, ItemSelectionModel.NoUpdate) treeView.toggleExpanded(row) } } } Image { id: image x: (isTreeNode ? (depth + 1) * indentation : 0) anchors.verticalCenter: parent.verticalCenter width: 13 height: 13 source: hasChildren ? "../../images/svg-folder.svg" : "../../images/svg-file.svg" visible: column === 0 ? true : false ColorOverlay { source: image anchors.fill: image color: "white" } } Label { id: label x: image.x + image.width + 5 anchors.verticalCenter: parent.verticalCenter width: parent.width - padding - x clip: true text: model.display color: column === 0 ? "white" : "#ABABAB" font.pixelSize: column === 0 ? 14 : 10 horizontalAlignment: isTreeNode ? Text.AlignLeft : Text.AlignRight rightPadding: 10 font.family: "Roboto Medium" } } MouseArea { id: menuMouseArea anchors.fill: parent acceptedButtons: Qt.RightButton Connections { target: menuMouseArea onClicked: { menu.open() menu.x = menuMouseArea.mouseX menu.y = menuMouseArea.mouseY } } } } Menu { id: menu width: 150 leftPadding: 5 font.family: "Roboto Medium" palette.window: "#1f1f1f" palette.text: "white" palette.windowText: "white" palette.light: "#898989" MenuItem { text: "Rename" icon.source: "../../images/svg-edit.svg" icon.width: 14 icon.height: 14 } MenuItem { text: "Delete" icon.source: "../../images/svg-trash.svg" icon.width: 14 icon.height: 14 } } }
Now it looks like this:
Thank you all!
-