Python Forum
[PyQt] How to generically set the Header Item Data
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[PyQt] How to generically set the Header Item Data
#1
I am basically a newbie to PyQt5 but a long time experienced software engineer I have in the last couple of weeks been able to figure out quite a few things about PyQt5 but this one troubles me since I will eventually have numerous columns and I am having to set this information individually for each column using a separate "container" which seems counter-intuitive. This is what I have thus far -- what I am trying to figure out is how to have a single HdrItem and apply it to each of the HeaderLabels -- this gets added into a CenterPane = QSplitter(Qt.Horizontal, self) once its created and that works fine but having to do this for each and every column header seems rather cumbersome which is why it seems counter-intuitive to how PyQt5 works. Note if I do not do it this way it complains about adding duplicates and only the first column header gets the formatting

class ItemDsplyr(QTreeView):
    def __init__(self, parent):
        QTreeView.__init__(self, parent)

        HdrItem0 = QStandardItem()
        HdrItem1 = QStandardItem()
        HdrItem2 = QStandardItem()

        font = QFont()
        font.setBold(True)
        font.setPointSize(10)
        HdrItem0.setFont(font)
        HdrItem1.setFont(font)
        HdrItem2.setFont(font)

        brush = QBrush()
        brush.setColor(Qt.blue)
        brush.setStyle(Qt.SolidPattern)
        HdrItem0.setBackground(brush)

        HdrItem0.setForeground(brush)
        HdrItem1.setForeground(brush)
        HdrItem2.setForeground(brush)

        self.model = QStandardItemModel(0, 3)
        self.model.setHorizontalHeaderItem(0, HdrItem0)
        self.model.setHorizontalHeaderItem(1, HdrItem1)
        self.model.setHorizontalHeaderItem(2, HdrItem2)
        self.model.setHorizontalHeaderLabels(['Column1', 'Column2', 'Column3'])

        self.setModel(self.model)
        self.clicked.connect(self.itemSingleClicked)
Reply
#2
We could better help, if you create a small program
with your problem. Then we can run it on our own PCs
for better understanding.
Reply
#3
Quick Container Program for the above Class that runs using Python3.7 and PyQt5

import sys
from PyQt5.QtCore import *
from PyQt5.QtGui  import *
from PyQt5.QtWidgets import *

class ItemDsplyr(QTreeView):
    def __init__(self):
        QTreeView.__init__(self)
 
        HdrItem0 = QStandardItem()
        HdrItem1 = QStandardItem()
        HdrItem2 = QStandardItem()
 
        font = QFont()
        font.setBold(True)
        font.setPointSize(10)
        HdrItem0.setFont(font)
        HdrItem1.setFont(font)
        HdrItem2.setFont(font)
 
        brush = QBrush()
        brush.setColor(Qt.blue)
        brush.setStyle(Qt.SolidPattern)
        HdrItem0.setBackground(brush)
 
        HdrItem0.setForeground(brush)
        HdrItem1.setForeground(brush)
        HdrItem2.setForeground(brush)
 
        self.model = QStandardItemModel(0, 3)
        self.model.setHorizontalHeaderItem(0, HdrItem0)
        self.model.setHorizontalHeaderItem(1, HdrItem1)
        self.model.setHorizontalHeaderItem(2, HdrItem2)
        self.model.setHorizontalHeaderLabels(['Column1', 'Column2', 'Column3'])
 
        self.setModel(self.model)
        self.clicked.connect(self.itemSingleClicked)

    def itemSingleClicked(self, index):
        Item = self.selectedIndexes()[0]
        ItemVal = Item.model().itemFromIndex(index).text()
        print("Item Clicked:",ItemVal)

    def SetContent(self):
        self.model.setRowCount(0)

        ItmRecSet = [
            {'CatgryName':'Cat-1', 'GroupName':'Run-Grp-1', 'ItemName':'Run-Itm-1'},
            {'CatgryName':'Cat-1', 'GroupName':'Run-Grp-1', 'ItemName':'Run-Itm-2'},
            {'CatgryName':'Cat-1', 'GroupName':'Run-Grp-1', 'ItemName':'Run-Itm-3'}
            ]

        for Item in ItmRecSet:
            ItmNam = QStandardItem(Item['ItemName'])
            CatNam = QStandardItem(Item['CatgryName'])
            GrpNam = QStandardItem(Item['GroupName'])

            self.model.appendRow([CatNam, GrpNam, ItmNam])

class CenterPane(QWidget):
    def __init__(self, parent):
        QWidget.__init__(self)

        self.MainWin = parent

        self.ItemDsply = ItemDsplyr()
        self.ItemDsply.SetContent()

        CntrPane = QSplitter(Qt.Horizontal, self)
        CntrPane.addWidget(QTextEdit())
        CntrPane.addWidget(self.ItemDsply)
        CntrPane.setSizes([75,200])

        hbox = QHBoxLayout(self)
        hbox.addWidget(CntrPane)

        self.setLayout(hbox)

    @property
    def MainWin(self):
        return self.__parent

    @MainWin.setter
    def MainWin(self, value):
        self.__parent = value

class Window(QMainWindow):
    def __init__(self, parent=None):
        super(Window, self).__init__(parent)
        self.setCentralWidget(CenterPane(self))

if __name__ == "__main__":
    qApp = QApplication([])
    GUI = Window()
    GUI.show()
    sys.exit(qApp.exec_())
Reply
#4
You could use a loop:

        
        self.model = QStandardItemModel(0, 3)
        for i in range(3):
            HdrItem = QStandardItem()

            font = QFont()
            font.setBold(True)
            font.setPointSize(10)
            HdrItem.setFont(font)

            brush = QBrush()
            brush.setColor(Qt.blue)
            brush.setStyle(Qt.SolidPattern)
            HdrItem.setBackground(brush)
            HdrItem.setForeground(brush)
            self.model.setHorizontalHeaderItem(i, HdrItem)

        self.model.setHorizontalHeaderLabels(['Column1', 'Column2', 'Column3'])
        self.setModel(self.model)
Reply
#5
Okay but (assuming your suggestion works) that still means I am creating new object for each column which seems a bit excessive so far I have had someone suggest overriding the QStandardItemModel class which would probably be cleaner but it too seems a bit excessive to manipulate that specific bit of information in a cleaner manner. I feel that some where in that vast amount of documentation there is a better way but it like the proverbial needle in a haystack. If for some reason that aspect of the object is truly not accessible like it should be then perhaps the best solution is to (for me) do the override but then submit a solution to the source code as well. Manipulating the formatting of Headers should not be this complicated.
Reply
#6
Hey Denni, I think overridding your model is right way to go here: what you're looking to do is change the presentation of your data, which is exactly what the model in model-view is for. This avoids creating needless objects, and allows you to easily update the styles on the fly.

In the example below the column styles are stored in two dictionaries, but you can of course change them out for anything you like.

import sys
from PyQt5.QtCore import *
from PyQt5.QtGui  import *
from PyQt5.QtWidgets import *


brush = QBrush()
brush.setColor(Qt.blue)
brush.setStyle(Qt.SolidPattern)

COLUMN_FOREGROUND_STYLES = {
    1: brush,
    2: brush,
    3: brush,
}

COLUMN_BACKGROUND_STYLES = {
    0: brush,
}


class CustomItemModel(QStandardItemModel):

    def headerData(self, section, orientation, role):
        if role == Qt.ForegroundRole:
            return COLUMN_FOREGROUND_STYLES.get(section)
            
        elif role == Qt.BackgroundRole:
            return COLUMN_BACKGROUND_STYLES.get(section)
        
        elif role == Qt.FontRole:
            font = QFont()
            font.setBold(True)
            font.setPointSize(10)
            return font
            
        return super().headerData(section, orientation, role)

 
class ItemDsplyr(QTreeView):
    def __init__(self):
        QTreeView.__init__(self)
  
        self.model = CustomItemModel(0, 3)
        self.model.setHorizontalHeaderLabels(['Column1', 'Column2', 'Column3'])
  
        self.setModel(self.model)
        self.clicked.connect(self.itemSingleClicked)
 
    def itemSingleClicked(self, index):
        Item = self.selectedIndexes()[0]
        ItemVal = Item.model().itemFromIndex(index).text()
        print("Item Clicked:",ItemVal)
 
    def SetContent(self):
        self.model.setRowCount(0)
 
        ItmRecSet = [
            {'CatgryName':'Cat-1', 'GroupName':'Run-Grp-1', 'ItemName':'Run-Itm-1'},
            {'CatgryName':'Cat-1', 'GroupName':'Run-Grp-1', 'ItemName':'Run-Itm-2'},
            {'CatgryName':'Cat-1', 'GroupName':'Run-Grp-1', 'ItemName':'Run-Itm-3'}
            ]
 
        for Item in ItmRecSet:
            ItmNam = QStandardItem(Item['ItemName'])
            CatNam = QStandardItem(Item['CatgryName'])
            GrpNam = QStandardItem(Item['GroupName'])
 
            self.model.appendRow([CatNam, GrpNam, ItmNam])
 
class CenterPane(QWidget):
    def __init__(self, parent):
        QWidget.__init__(self)
 
        self.MainWin = parent
 
        self.ItemDsply = ItemDsplyr()
        self.ItemDsply.SetContent()
 
        CntrPane = QSplitter(Qt.Horizontal, self)
        CntrPane.addWidget(QTextEdit())
        CntrPane.addWidget(self.ItemDsply)
        CntrPane.setSizes([75,200])
 
        hbox = QHBoxLayout(self)
        hbox.addWidget(CntrPane)
 
        self.setLayout(hbox)
 
    @property
    def MainWin(self):
        return self.__parent
 
    @MainWin.setter
    def MainWin(self, value):
        self.__parent = value
 
class Window(QMainWindow):
    def __init__(self, parent=None):
        super(Window, self).__init__(parent)
        self.setCentralWidget(CenterPane(self))
 
if __name__ == "__main__":
    qApp = QApplication([])
    GUI = Window()
    GUI.show()
    sys.exit(qApp.exec_())
Let me know if this is way off the mark.
Reply
#7
Thanks @mfitzp I think you are right on changing the QStandardItemModel -- seems (at least to me) like modifying that information ought to have been a feature of the standard model but it is looking like it is not. However you outline did give me an idea for how to perhaps streamline that a bit so I changed your CustomItemModel class to look as follows:
class CustomItemModel(QStandardItemModel):
    def headerData(self, section, orientation, role):
        if role == Qt.ForegroundRole:
            brush = QBrush()
            brush.setColor(Qt.blue)
            brush.setStyle(Qt.SolidPattern)
            return brush
             
        elif role == Qt.BackgroundRole:
            brush = QBrush()
            brush.setColor(Qt.yellow)
            brush.setStyle(Qt.SolidPattern)
            return brush
         
        elif role == Qt.FontRole:
            font = QFont()
            font.setBold(True)
            font.setPointSize(10)
            return font
             
        return super().headerData(section, orientation, role)
This gets rid of the multiple instances -- however -- it does not seem to get the Background to work any more than my version did not work -- aka I cannot get the header row to have a different background color even with your version of this. Do you have any idea how one adjusts the background color since this is not working?
Reply
#8
Here's a screenshot of your app running on my computer (MacOS)

[Image: gKUCb9D.png]

...and of mine...

[Image: TN66ncz.png]

Googling a bit I read that on Windows this may not be possible as the header background is overridden by the window style. You can set the application style to Fusion (a cross-platform, Qt-specific style, where everything should work) to at least see if this is the problem.

if __name__ == "__main__":
    qApp = QApplication([])
    qApp.setStyle("fusion")
    GUI = Window()
    GUI.show()
    sys.exit(qApp.exec_())
If that works then I guess we at least have our answer!
Reply
#9
Awesome sauce thanks @mfitzp that was the issue with the background looks good now.
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020