Search code examples
pythonqt-designerpyside2qtreeviewqprogressbar

Add a Widget (QProgressbar) as a childItem within a QTreeView


I made a model that adds data to a QTreeView. I want to add a widget (eg: QProgressbar) as a child element to each row within that QTreeView. So that every time if we click on each row we can see the progressbar as a childItem for that row

main.py

import sys
from PySide2.QtWidgets import QApplication, QMainWindow, QHeaderView, QPushButton
from PySide2 import QtCore
from TreeViewUI import Ui_MainWindow
from custom_models.table_model import CustomTableModel


class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, data):
        QMainWindow.__init__(self)
        Ui_MainWindow.__init__(self)
        self.setupUi(self)
        self.table_model = CustomTableModel(data)
        self.treeView.setIconSize(QtCore.QSize(360 / 4, 640 / 4))
        self.treeView.header().setSectionResizeMode(QHeaderView.ResizeToContents)
        self.treeView.setModel(self.table_model)

def get_data():
    content_list = [
        {
            "user_id": 101,
            "user_name": "User_1",
            "user_image": "images/demo.jpg",
            "user_details": "details_1",
            "user_progress": 45,

        },
        {
            "user_id": 102,
            "user_name": "User_2",
            "user_image": "images/demo.jpg",
            "user_details": "details_2",
            "user_progress": 33,

        },
        {
            "user_id": 103,
            "user_name": "User_3",
            "user_image": "images/demo.jpg",
            "user_details": "details_3",
            "user_progress": 80,

        },



    ]
    return content_list


if __name__ == '__main__':
    app = QApplication([])
    data = get_data()
    main_window = MainWindow(data)
    main_window.showMaximized()
    sys.exit(app.exec_())

table_model.py

from PySide2.QtCore import Qt, QAbstractTableModel, QModelIndex
from PySide2.QtGui import QColor, QImage, QIcon, QPixmap

class CustomTableModel(QAbstractTableModel):
    def __init__(self, data):
        QAbstractTableModel.__init__(self)
        self.content_id = []
        self.content_names = []
        self.content_url = []
        self.content_variant = []
        self.content_progress = []
        for content_data in data:
            self.content_id.append(content_data["user_id"])
            self.content_names.append(content_data["user_name"])
            self.content_url.append(content_data["user_image"])
            self.content_variant.append(content_data["user_details"])
            self.content_progress.append(content_data["user_progress"])

        self.row_count = len(self.content_id)
        self.column_count = 4

    def rowCount(self, parent=QModelIndex()):
        return self.row_count

    def columnCount(self, parent=QModelIndex()):
        return self.column_count


    def headerData(self, section, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return ("ID", "NAME", "IMAGE PREVIEW", "DETAILS")[section]


    def data(self, index, role=Qt.DisplayRole):
        column = index.column()
        row = index.row()
        if role == Qt.DisplayRole:
            if column == 0:
                content_id = self.content_id[row]
                return content_id
            elif column == 1:
                return self.content_names[row]
            elif column == 3:
                return self.content_variant[row]




        elif role == Qt.DecorationRole:
            if column == 2:
                image_path = self.content_url[row]
                image = QImage()
                image.load(image_path)
                icon = QIcon()
                icon.addPixmap(QPixmap.fromImage(image))
                return icon


        elif role == Qt.TextAlignmentRole:
            return Qt.AlignLeft

TreeViewUI.py

from PySide2.QtCore import (QCoreApplication, QMetaObject, QObject, QPoint,
    QRect, QSize, QUrl, Qt)
from PySide2.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont,
    QFontDatabase, QIcon, QLinearGradient, QPalette, QPainter, QPixmap,
    QRadialGradient)
from PySide2.QtWidgets import *


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        if MainWindow.objectName():
            MainWindow.setObjectName(u"MainWindow")
        MainWindow.resize(817, 600)
        self.centralwidget = QWidget(MainWindow)
        self.centralwidget.setObjectName(u"centralwidget")
        self.gridLayout = QGridLayout(self.centralwidget)
        self.gridLayout.setObjectName(u"gridLayout")
        self.verticalLayout = QVBoxLayout()
        self.verticalLayout.setObjectName(u"verticalLayout")
        self.label = QLabel(self.centralwidget)
        self.label.setObjectName(u"label")

        self.verticalLayout.addWidget(self.label)

        self.treeView = QTreeView(self.centralwidget)
        self.treeView.setObjectName(u"treeView")

        self.verticalLayout.addWidget(self.treeView)


        self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1)

        MainWindow.setCentralWidget(self.centralwidget)

        self.retranslateUi(MainWindow)

        QMetaObject.connectSlotsByName(MainWindow)
    # setupUi

    def retranslateUi(self, MainWindow):
        MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None))
        self.label.setText(QCoreApplication.translate("MainWindow", u"TreeView", None))
    # retranslateUi

These are the files I created to test for a sample implementation for QTreeView. The basic structure of the Treeview would be as follows:

User_ID  User_Name  User_Image  User_Details
101       ABC       abc.jpg      abc
 QProgressBar widget for the above user(101)# QProgressbar as the childItem for each row.
102       DEF       def.jpg      def
 QProgressBar widget for the above user(102)
103       GHI       ghi.png      ghi
 QProgressBar widget for the above user(103)

Solution

  • If the structure is of tree type then the model must have that structure so that model based on QAbstractItemModel could be used but to make it simpler I have implemented it using the QStandardItemModel class where I have established the values in the appropriate roles, and so that the progressbar It is displayed in a new file so you must be a child of the item. To show the progressbar I have not set a widget but it has been painted using the QStyle through a delegate:

    from PySide2.QtCore import Qt
    from PySide2.QtGui import QIcon, QStandardItem, QStandardItemModel
    
    
    class UserModel(QStandardItemModel):
        def __init__(self, parent=None):
            super(UserModel, self).__init__(parent)
            self.setColumnCount(4)
    
        def appendUser(self, id_, name, image, details, progress):
            items = []
            row = self.rowCount()
            for text, column in zip((id_, name, details), (0, 1, 3)):
                it = QStandardItem()
                it.setData(text, Qt.DisplayRole)
                self.setItem(row, column, it)
                items.append(it)
            image_item = QStandardItem()
            image_item.setData(QIcon(image), Qt.DecorationRole)
            self.setItem(row, 2, image_item)
            progress_item = QStandardItem()
            progress_item.setData(progress, Qt.UserRole)
            items[0].appendRow(progress_item)
    
    import os
    import sys
    
    from PySide2.QtCore import QSize, Qt
    from PySide2.QtWidgets import (
        QApplication,
        QMainWindow,
        QHeaderView,
        QStyledItemDelegate,
        QStyleOptionProgressBar,
        QStyle,
    )
    
    from TreeViewUI import Ui_MainWindow
    from custom_models.table_model import UserModel
    
    CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
    
    
    class ProgressDelegate(QStyledItemDelegate):
        def paint(self, painter, option, index):
            if index.parent().isValid():
                r = option.rect
                r.setWidth(200)
                progress = index.data(Qt.UserRole)
                progress_option = QStyleOptionProgressBar()
                progress_option.rect = r
                progress_option.minimum = 0
                progress_option.maximum = 100
                progress_option.progress = progress
                progress_option.text = "{}%".format(progress)
                progress_option.textVisible = True
                QApplication.style().drawControl(
                    QStyle.CE_ProgressBar, progress_option, painter
                )
                return
            super(ProgressDelegate, self).paint(painter, option, index)
    
    
    class MainWindow(QMainWindow, Ui_MainWindow):
        def __init__(self, data):
            super(MainWindow, self).__init__()
            self.setupUi(self)
            self.model = UserModel(self)
            for content_data in data:
                self.model.appendUser(
                    content_data["user_id"],
                    content_data["user_name"],
                    os.path.join(CURRENT_DIR, content_data["user_image"]),
                    content_data["user_details"],
                    content_data["user_progress"],
                )
            self.treeView.setIconSize(QSize(360 / 4, 640 / 4))
            self.treeView.header().setSectionResizeMode(QHeaderView.ResizeToContents)
            self.treeView.setModel(self.model)
            delegate = ProgressDelegate(self.treeView)
            self.treeView.setItemDelegate(delegate)
    
    
    def get_data():
        content_list = [
            {
                "user_id": 101,
                "user_name": "User_1",
                "user_image": "images/demo.jpg",
                "user_details": "details_1",
                "user_progress": 45,
            },
            {
                "user_id": 102,
                "user_name": "User_2",
                "user_image": "images/demo.jpg",
                "user_details": "details_2",
                "user_progress": 33,
            },
            {
                "user_id": 103,
                "user_name": "User_3",
                "user_image": "images/demo.jpg",
                "user_details": "details_3",
                "user_progress": 80,
            },
        ]
        return content_list
    
    
    if __name__ == "__main__":
        app = QApplication([])
        data = get_data()
        main_window = MainWindow(data)
        main_window.showMaximized()
        sys.exit(app.exec_())
    

    enter image description here

    Another possible solution is to create a persistent editor through the delegate:

    class ProgressDelegate(QStyledItemDelegate):
        def paint(self, painter, option, index):
            if index.parent().isValid():
                view = option.widget
                if isinstance(view, QTreeView) and index.model() is view.model():
                    view.openPersistentEditor(index)
                return
            super(ProgressDelegate, self).paint(painter, option, index)
    
        def createEditor(self, parent, option, index):
            if index.parent().isValid():
                editor = QProgressBar(parent)
                editor.setFixedWidth(200)
                editor.setContentsMargins(0, 0, 0, 0)
                editor.setValue(index.data(Qt.UserRole))
                return editor
            super(ProgressDelegate, self).createEditor(parent, option, index)
    

    Nota: Painting a QProgressBar as I did in the first delegate is a trivial task but if you want to place more complex widgets (widgets that have more states like QPushButton) then it is better to use the second option.