How to iterate over a QTreeView and color all matched cells?

ghz 7hours ago ⋅ 4 views

when pressing the search button, I would like to search in all items (aka cells) of a QTreeView and color all cells matching the searched text cells via a CSS style.

Is this possible?

Code currently (full working example):

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from PyQt5 import QtCore, QtGui, QtWidgets


class App(QtWidgets.QWidget):
    MAIL_RANGE = 4
    ID, FROM, SUBJECT, DATE = range(MAIL_RANGE)

    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.setGeometry(10, 10, 640, 240)

        self.dataGroupBox = QtWidgets.QGroupBox("Inbox")
        self.dataView = QtWidgets.QTreeView(
            rootIsDecorated=False,
            alternatingRowColors=True,
            selectionMode=QtWidgets.QAbstractItemView.ExtendedSelection,
            editTriggers=QtWidgets.QAbstractItemView.NoEditTriggers,
            selectionBehavior=QtWidgets.QAbstractItemView.SelectRows,
        )

        dataLayout = QtWidgets.QHBoxLayout()
        dataLayout.addWidget(self.dataView)
        self.dataGroupBox.setLayout(dataLayout)

        model = App.createMailModel(self)
        self.dataView.setModel(model)

        for i in range(0, 2):
            self.dataView.resizeColumnToContents(i)

        self.addMail(model, 1, 'service@github.com', 'Your Github Donation','03/25/2017 02:05 PM')
        self.addMail(model, 2, 'support@github.com', 'Github Projects','02/02/2017 03:05 PM')
        self.addMail(model, 3, 'service@phone.com', 'Your Phone Bill','01/01/2017 04:05 PM')
        self.addMail(model, 4, 'service@abc.com', 'aaaYour Github Donation','03/25/2017 02:05 PM')
        self.addMail(model, 5, 'support@def.com', 'bbbGithub Projects','02/02/2017 03:05 PM')
        self.addMail(model, 6, 'service@xyz.com', 'cccYour Phone Bill','01/01/2017 04:05 PM')

        self.dataView.setColumnHidden(0, True)

        self.leSearch = QtWidgets.QLineEdit()
        self.pbSearch = QtWidgets.QPushButton(
            "Search", clicked=self.on_pbSearch_clicked
        )

        hlay = QtWidgets.QHBoxLayout()
        hlay.addWidget(self.leSearch)
        hlay.addWidget(self.pbSearch)

        mainLayout = QtWidgets.QVBoxLayout(self)
        mainLayout.addLayout(hlay)
        mainLayout.addWidget(self.dataGroupBox)

    @staticmethod
    def createMailModel(parent):
        model = QtGui.QStandardItemModel(0, App.MAIL_RANGE, parent)
        for c, text in zip(
            (App.ID, App.FROM, App.SUBJECT, App.DATE),
            ("ID", "From", "Subject", "Date"),
        ):
            model.setHeaderData(c, QtCore.Qt.Horizontal, text)
        return model

    def addMail(self, model, mailID, mailFrom, subject, date):
        model.insertRow(0)
        for c, text in zip(
            (App.ID, App.FROM, App.SUBJECT, App.DATE),
            (mailID, mailFrom, subject, date),
        ):
            model.setData(model.index(0, c), text)

    @QtCore.pyqtSlot()
    def on_pbSearch_clicked(self):
        text = self.leSearch.text()
        self.leSearch.clear()
        if text:
            start = self.dataView.model().index(0, 2)
            # find index
            ixs = self.dataView.model().match(
                start,
                QtCore.Qt.DisplayRole,
                text,
                flags=QtCore.Qt.MatchStartsWith,
            )
            if ixs:
                ix = ixs[0]
                # scroll to index
                self.dataView.scrollTo(ix)
                ix_from = ix.sibling(ix.row(), 0)
                ix_to = ix.sibling(
                    ix.row(), self.dataView.model().columnCount() - 1
                )
                # select row
                self.dataView.selectionModel().select(
                    QtCore.QItemSelection(ix_from, ix_to),
                    QtCore.QItemSelectionModel.SelectCurrent,
                )
        else:
            self.dataView.clearSelection()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    ex = App()
    ex.show()
    sys.exit(app.exec_())

See method on_pbSearch_clicked -> here all the matched rows are flagged as selected. Instead I would like to color the cells via a CSS style.

Answer

Yes, it's possible to color the cells matching the search query using CSS styles in PyQt. To do this, you can use the QTreeView's setStyleSheet method in combination with a search function that will find all matching cells, then apply a CSS style to those matching cells.

The QTreeView uses the model-view architecture, so to apply the CSS styles to individual cells, you can modify the QStandardItem's setData method to include custom styles for the cells that match the search query.

Steps to achieve the desired behavior:

  1. Search all cells in the model:

    • Use the same method you are using (match) to identify cells that match the search text.
  2. Apply custom style to matching cells:

    • Once you have the matching cells, you can apply a CSS style (for example, a background color) to those cells.
  3. Use setData with QtCore.Qt.BackgroundRole to apply the CSS style.

Here's the updated code to highlight matching cells with a CSS style instead of selecting them:

Updated Code with CSS Styling for Search Results

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
from PyQt5 import QtCore, QtGui, QtWidgets


class App(QtWidgets.QWidget):
    MAIL_RANGE = 4
    ID, FROM, SUBJECT, DATE = range(MAIL_RANGE)

    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.setGeometry(10, 10, 640, 240)

        self.dataGroupBox = QtWidgets.QGroupBox("Inbox")
        self.dataView = QtWidgets.QTreeView(
            rootIsDecorated=False,
            alternatingRowColors=True,
            selectionMode=QtWidgets.QAbstractItemView.ExtendedSelection,
            editTriggers=QtWidgets.QAbstractItemView.NoEditTriggers,
            selectionBehavior=QtWidgets.QAbstractItemView.SelectRows,
        )

        dataLayout = QtWidgets.QHBoxLayout()
        dataLayout.addWidget(self.dataView)
        self.dataGroupBox.setLayout(dataLayout)

        model = App.createMailModel(self)
        self.dataView.setModel(model)

        for i in range(0, 2):
            self.dataView.resizeColumnToContents(i)

        self.addMail(model, 1, 'service@github.com', 'Your Github Donation','03/25/2017 02:05 PM')
        self.addMail(model, 2, 'support@github.com', 'Github Projects','02/02/2017 03:05 PM')
        self.addMail(model, 3, 'service@phone.com', 'Your Phone Bill','01/01/2017 04:05 PM')
        self.addMail(model, 4, 'service@abc.com', 'aaaYour Github Donation','03/25/2017 02:05 PM')
        self.addMail(model, 5, 'support@def.com', 'bbbGithub Projects','02/02/2017 03:05 PM')
        self.addMail(model, 6, 'service@xyz.com', 'cccYour Phone Bill','01/01/2017 04:05 PM')

        self.dataView.setColumnHidden(0, True)

        self.leSearch = QtWidgets.QLineEdit()
        self.pbSearch = QtWidgets.QPushButton(
            "Search", clicked=self.on_pbSearch_clicked
        )

        hlay = QtWidgets.QHBoxLayout()
        hlay.addWidget(self.leSearch)
        hlay.addWidget(self.pbSearch)

        mainLayout = QtWidgets.QVBoxLayout(self)
        mainLayout.addLayout(hlay)
        mainLayout.addWidget(self.dataGroupBox)

    @staticmethod
    def createMailModel(parent):
        model = QtGui.QStandardItemModel(0, App.MAIL_RANGE, parent)
        for c, text in zip(
            (App.ID, App.FROM, App.SUBJECT, App.DATE),
            ("ID", "From", "Subject", "Date"),
        ):
            model.setHeaderData(c, QtCore.Qt.Horizontal, text)
        return model

    def addMail(self, model, mailID, mailFrom, subject, date):
        model.insertRow(0)
        for c, text in zip(
            (App.ID, App.FROM, App.SUBJECT, App.DATE),
            (mailID, mailFrom, subject, date),
        ):
            model.setData(model.index(0, c), text)

    @QtCore.pyqtSlot()
    def on_pbSearch_clicked(self):
        text = self.leSearch.text()
        self.leSearch.clear()

        # Reset background color of all cells before searching
        model = self.dataView.model()
        for row in range(model.rowCount()):
            for col in range(model.columnCount()):
                item = model.item(row, col)
                item.setData(QtCore.Qt.GlobalColor.white, QtCore.Qt.BackgroundRole)

        if text:
            # Iterate over all rows and columns
            for row in range(model.rowCount()):
                for col in range(model.columnCount()):
                    index = model.index(row, col)
                    item = model.itemFromIndex(index)

                    # If the item contains the search text, highlight it
                    if text.lower() in str(item.text()).lower():
                        item.setData(QtGui.QColor(255, 255, 0), QtCore.Qt.BackgroundRole)  # Yellow background
        else:
            self.dataView.clearSelection()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    ex = App()
    ex.show()
    sys.exit(app.exec_())

Key Changes:

  1. Reset Background Color: Before searching, the background color of all cells is reset to white to ensure that any previously highlighted cells are cleared.

    # Reset background color of all cells before searching
    model = self.dataView.model()
    for row in range(model.rowCount()):
        for col in range(model.columnCount()):
            item = model.item(row, col)
            item.setData(QtCore.Qt.GlobalColor.white, QtCore.Qt.BackgroundRole)
    
  2. Search and Highlight Cells: For each cell in the model, the search text is checked. If it matches, the background color of that cell is set to yellow (QColor(255, 255, 0)).

    if text:
        # Iterate over all rows and columns
        for row in range(model.rowCount()):
            for col in range(model.columnCount()):
                index = model.index(row, col)
                item = model.itemFromIndex(index)
    
                # If the item contains the search text, highlight it
                if text.lower() in str(item.text()).lower():
                    item.setData(QtGui.QColor(255, 255, 0), QtCore.Qt.BackgroundRole)  # Yellow background
    
  3. CSS Styling for Highlighting: This is done using the setData method with QtCore.Qt.BackgroundRole, which allows you to apply custom background color to cells that match the search text.

Result:

  • All cells containing the search text will be highlighted with a yellow background.
  • You can easily change the background color to any other color by adjusting the QColor values.

This solution should give you the behavior you want: highlighting cells with a CSS-like style instead of selecting the rows.