Skip to content
Advertisement

How to word wrap the header contents of QTableWidget in PyQt5 Python

I am working on PyQt5 where I have a QTableWidget. It has a header column which I want to word wrap. Below is how the table looks like:

enter image description here

As we can see that the header label like Maximum Variation Coefficient has 3 words, thus its taking too much column width. How can wrap the words in the header.

Below is the code:

import sys

from PyQt5 import QtWidgets
from PyQt5.QtWidgets import *


# Main Window
class App(QWidget):
    def __init__(self):
        super().__init__()
        self.title = 'PyQt5 - QTableWidget'
        self.left = 0
        self.top = 0
        self.width = 300
        self.height = 200

        self.setWindowTitle(self.title)
        self.setGeometry(self.left, self.top, self.width, self.height)

        self.createTable()

        self.layout = QVBoxLayout()
        self.layout.addWidget(self.tableWidget)
        self.setLayout(self.layout)

        # Show window
        self.show()

    # Create table
    def createTable(self):
        self.tableWidget = QTableWidget()

        # Row count
        self.tableWidget.setRowCount(3)

        # Column count
        self.tableWidget.setColumnCount(2)

        self.tableWidget.setHorizontalHeaderLabels(["Maximum Variation Coefficient", "Maximum Variation Coefficient"])
        self.tableWidget.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
        self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)

        self.tableWidget.setItem(0, 0, QTableWidgetItem("3.44"))
        self.tableWidget.setItem(0, 1, QTableWidgetItem("5.3"))
        self.tableWidget.setItem(1, 0, QTableWidgetItem("4.6"))
        self.tableWidget.setItem(1, 1, QTableWidgetItem("1.2"))
        self.tableWidget.setItem(2, 0, QTableWidgetItem("2.2"))
        self.tableWidget.setItem(2, 1, QTableWidgetItem("4.4"))

        # Table will fit the screen horizontally
        self.tableWidget.horizontalHeader().setStretchLastSection(True)
        self.tableWidget.horizontalHeader().setSectionResizeMode(
            QHeaderView.Stretch)


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

I tried adding this self.tableWidget.setWordWrap(True) but this doesnt make any change. Can anyone give some good solution. Please help. Thanks

EDIT:

Also tried this :

self.tableWidget.horizontalHeader().setDefaultAlignment(QtCore.Qt.AlignHCenter | Qt.Alignment(QtCore.Qt.TextWordWrap))

But it didnt worked

Advertisement

Answer

In order to achieve what you need, you must set your own header and proceed with the following two assumptions:

  • the header must provide the correct size hint height according to the section contents in case the width of the column is not sufficient;
  • the text alignment must include the QtCore.Qt.TextWordWrap flag, so that the painter knows that it can wrap text;

Do note that, while the second aspect might be enough in some situations (as headers are normally tall enough to fit at least two lines), the first point is mandatory as the text might require more vertical space, otherwise some text would be cut out.


The first point requires to subclass QHeaderView and reimplement sectionSizeFromContents():

class WrapHeader(QtWidgets.QHeaderView):
    def sectionSizeFromContents(self, logicalIndex):
        # get the size returned by the default implementation
        size = super().sectionSizeFromContents(logicalIndex)
        if self.model():
            if size.width() > self.sectionSize(logicalIndex):
                text = self.model().headerData(logicalIndex, 
                    self.orientation(), QtCore.Qt.DisplayRole)
                if not text:
                    return size
                # in case the display role is numeric (for example, when header 
                # labels are not defined yet), convert it to a string; 
                text = str(text)

                option = QtWidgets.QStyleOptionHeader()
                self.initStyleOption(option)
                alignment = self.model().headerData(logicalIndex, 
                    self.orientation(), QtCore.Qt.TextAlignmentRole)
                if alignment is None:
                    alignment = option.textAlignment

                # get the default style margin for header text and create a 
                # possible rectangle using the current section size, then use
                # QFontMetrics to get the required rectangle for the wrapped text
                margin = self.style().pixelMetric(
                    QtWidgets.QStyle.PM_HeaderMargin, option, self)
                maxWidth = self.sectionSize(logicalIndex) - margin * 2
                rect = option.fontMetrics.boundingRect(
                    QtCore.QRect(0, 0, maxWidth, 10000), 
                    alignment | QtCore.Qt.TextWordWrap, 
                    text)

                # add vertical margins to the resulting height
                height = rect.height() + margin * 2
                if height >= size.height():
                    # if the height is bigger than the one provided by the base
                    # implementation, return a new size based on the text rect
                    return QtCore.QSize(rect.width(), height)
        return size


class App(QWidget):
    # ...
    def createTable(self):
        self.tableWidget = QTableWidget()
        self.tableWidget.setHorizontalHeader(
            WrapHeader(QtCore.Qt.Horizontal, self.tableWidget))
        # ...

Then, to set the word wrap flag, there are two options:

  1. set the alignment flag on the underlying model with setHeaderData() for each existing column:
        # ...
        model = self.tableWidget.model()
        default = self.tableWidget.horizontalHeader().defaultAlignment()
        default |= QtCore.Qt.TextWordWrap
        for col in range(self.tableWidget.columnCount()):
            alignment = model.headerData(
                col, QtCore.Qt.Horizontal, QtCore.Qt.TextAlignmentRole)
            if alignment:
                alignment |= QtCore.Qt.TextWordWrap
            else:
                alignment = default
            model.setHeaderData(
                col, QtCore.Qt.Horizontal, alignment, QtCore.Qt.TextAlignmentRole)
  1. Use a QProxyStyle to override the painting of the header, by applying the flag on the option:
# ...

class ProxyStyle(QtWidgets.QProxyStyle):
    def drawControl(self, control, option, painter, widget=None):
        if control in (self.CE_Header, self.CE_HeaderLabel):
            option.textAlignment |= QtCore.Qt.TextWordWrap
        super().drawControl(control, option, painter, widget)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    app.setStyle(ProxyStyle())
    ex = App()
    sys.exit(app.exec_())

Finally, consider that:

  • using setSectionResizeMode with ResizeToContents or Stretch, along with setStretchLastSection, will always cause the table trying to use as much space as required by the headers upon showing the first time;

  • by default, QHeaderView sections are not clickable (which is a mandatory requirement for sorting) and the highlightSections property is also False; both QTableView and QTableWidget create their headers with those values as True, so when a new header is set you must explicitly change those aspects if sorting and highlighting are required:

      self.tableWidget.setHorizontalHeader(
          WrapHeader(QtCore.Qt.Horizontal, self.tableWidget))
      self.tableWidget.horizontalHeader().setSectionsClickable(True)
      self.tableWidget.horizontalHeader().setHighlightSections(True)
    
  • both sorting and section highlighting can create some issues, as the sort indicator requires further horizontal space and highlighted sections are normally shown with a bold font (but are shown normally while the mouse is pressed); all this might create some flickering and odd behavior; unfortunately, there’s no obvious solution for these problems, but when using the QProxyStyle it’s possible to avoid some flickering by overriding the font style:

      def drawControl(self, control, option, painter, widget=None):
          if control in (self.CE_Header, self.CE_HeaderLabel):
              option.textAlignment |= QtCore.Qt.TextWordWrap
              if option.state & self.State_Sunken:
                  option.state |= self.State_On
          super().drawControl(control, option, painter, widget)
    
User contributions licensed under: CC BY-SA
3 People found this is helpful
Advertisement