QTableView header word wrap
Asked Answered
C

5

6

I'm trying to set the horizontal and vertical headers in QTableView to word wrap but without any success.

I want to set all columns to be the same width (including the vertical header), and those columns that have multiline text to word wrap. If word is wider than the column it should elide right. I've managed to set the elide using QTableView -> horizontalHeader() -> setTextElideMode(Qt::ElideRight), but I can't do the same for word wrap since QHeaderView doesn't have setWordWrap method. So event if text is multiline it will just elide. Setting the word wrap on the table view doesn't do anything. The table cells contain only small numbers so the issue is only with the headers, and I want to avoid using '/n' since the headers are set dynamically. Is there maybe some other setting I've changed that's not allowing word wrap to function?

Cymoid answered 13/7, 2017 at 15:4 Comment(0)
M
3

I've managed to find the solution using subclassing of QHeaderView and reimplementing sectionSizeFromContents and paintSection methods in it. Here's the demo in PyQt5 (tested with Python 3.5.2 and Qt 5.6):

import sys
import string
import random
from PyQt5 import QtCore, QtWidgets, QtGui

class HeaderViewWithWordWrap(QtWidgets.QHeaderView):
    def __init__(self):
        QtWidgets.QHeaderView.__init__(self, QtCore.Qt.Horizontal)

    def sectionSizeFromContents(self, logicalIndex):
        if self.model():
            headerText = self.model().headerData(logicalIndex,
                                                 self.orientation(),
                                                 QtCore.Qt.DisplayRole)
            options = self.viewOptions()
            metrics = QtGui.QFontMetrics(options.font)
            maxWidth = self.sectionSize(logicalIndex)
            rect = metrics.boundingRect(QtCore.QRect(0, 0, maxWidth, 5000),
                                        self.defaultAlignment() |
                                        QtCore.Qt.TextWordWrap |
                                        QtCore.Qt.TextExpandTabs,
                                        headerText, 4)
            return rect.size()
        else:
            return QtWidgets.QHeaderView.sectionSizeFromContents(self, logicalIndex)

    def paintSection(self, painter, rect, logicalIndex):
        if self.model():
            painter.save()
            self.model().hideHeaders()
            QtWidgets.QHeaderView.paintSection(self, painter, rect, logicalIndex)
            self.model().unhideHeaders()
            painter.restore()
            headerText = self.model().headerData(logicalIndex,
                                                 self.orientation(),
                                                 QtCore.Qt.DisplayRole)
            painter.drawText(QtCore.QRectF(rect), QtCore.Qt.TextWordWrap, headerText)
        else:
            QtWidgets.QHeaderView.paintSection(self, painter, rect, logicalIndex)

class Model(QtCore.QAbstractTableModel):
    def __init__(self):
        QtCore.QAbstractTableModel.__init__(self)
        self.model_cols_names = [ "Very-very long name of my first column",
                                  "Very-very long name of my second column",
                                  "Very-very long name of my third column",
                                  "Very-very long name of my fourth column" ]
        self.hide_headers_mode = False
        self.data = []
        for i in range(0, 10):
            row_data = []
            for j in range(0, len(self.model_cols_names)):
                row_data.append(''.join(random.choice(string.ascii_uppercase +
                                        string.digits) for _ in range(6)))
            self.data.append(row_data)

    def hideHeaders(self):
        self.hide_headers_mode = True

    def unhideHeaders(self):
        self.hide_headers_mode = False

    def rowCount(self, parent):
        if parent.isValid():
            return 0
        else:
            return len(self.data)

    def columnCount(self, parent):
        return len(self.model_cols_names)

    def data(self, index, role):
        if not index.isValid():
            return None
        if role != QtCore.Qt.DisplayRole:
            return None

        row = index.row()
        if row < 0 or row >= len(self.data):
            return None

        column = index.column()
        if column < 0 or column >= len(self.model_cols_names):
            return None

        return self.data[row][column]

    def headerData(self, section, orientation, role):
        if role != QtCore.Qt.DisplayRole:
            return None
        if orientation != QtCore.Qt.Horizontal:
            return None
        if section < 0 or section >= len(self.model_cols_names):
            return None
        if self.hide_headers_mode == True:
            return None
        else:
            return self.model_cols_names[section]

class MainForm(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        QtWidgets.QMainWindow.__init__(self, parent)
        self.model = Model()
        self.view = QtWidgets.QTableView()
        self.view.setModel(self.model)
        self.view.setHorizontalHeader(HeaderViewWithWordWrap())
        self.setCentralWidget(self.view)

def main():
    app = QtWidgets.QApplication(sys.argv)
    form = MainForm()
    form.show()
    app.exec_()

if __name__ == '__main__':
    main()
Mccourt answered 14/7, 2017 at 10:10 Comment(2)
I was able to migrate your code to c++ and got the header to wrap text. But when reimplementing the paintSection method we are just setting the plain text, all the other stuff in the header like background, borders, etc. is gone and it looks ugly. Is there a way to just set the text without changing the rest of the style?Cymoid
There is no good generic way but I found one slightly hackish approach, updated the answer now. Basically, you can implement paintSection as follows: 1. tell the model hide the header data i.e. to return empty data; 2. call parent class' paintSection method so that it paints everythiing but the text (which is empty); 3. tell the model to stop hiding the header data; 4. retrieve the header data and paint its textMccourt
F
6

I was able to consolidate the two approaches above (c++, Qt 5.12) with a pretty nice result. (no hideheaders on the model)

  1. Override QHeaderView::sectionSizeFromContents() such that size accounts for text wrapping
QSize MyHeaderView::sectionSizeFromContents(int logicalIndex) const 
{
    const QString text = this->model()->headerData(logicalIndex, this->orientation(), Qt::DisplayRole).toString();
    const int maxWidth = this->sectionSize(logicalIndex);
    const int maxHeight = 5000; // arbitrarily large
    const auto alignment = defaultAlignment();
    const QFontMetrics metrics(this->fontMetrics());
    const QRect rect = metrics.boundingRect(QRect(0, 0, maxWidth, maxHeight), alignment, text);

    const QSize textMarginBuffer(2, 2); // buffer space around text preventing clipping
    return rect.size() + textMarginBuffer;
}
  1. Set the default alignment to have word wrap (optionally, center)
 tableview->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter | (Qt::Alignment)Qt::TextWordWrap);
Frosting answered 12/12, 2019 at 14:59 Comment(0)
M
3

I've managed to find the solution using subclassing of QHeaderView and reimplementing sectionSizeFromContents and paintSection methods in it. Here's the demo in PyQt5 (tested with Python 3.5.2 and Qt 5.6):

import sys
import string
import random
from PyQt5 import QtCore, QtWidgets, QtGui

class HeaderViewWithWordWrap(QtWidgets.QHeaderView):
    def __init__(self):
        QtWidgets.QHeaderView.__init__(self, QtCore.Qt.Horizontal)

    def sectionSizeFromContents(self, logicalIndex):
        if self.model():
            headerText = self.model().headerData(logicalIndex,
                                                 self.orientation(),
                                                 QtCore.Qt.DisplayRole)
            options = self.viewOptions()
            metrics = QtGui.QFontMetrics(options.font)
            maxWidth = self.sectionSize(logicalIndex)
            rect = metrics.boundingRect(QtCore.QRect(0, 0, maxWidth, 5000),
                                        self.defaultAlignment() |
                                        QtCore.Qt.TextWordWrap |
                                        QtCore.Qt.TextExpandTabs,
                                        headerText, 4)
            return rect.size()
        else:
            return QtWidgets.QHeaderView.sectionSizeFromContents(self, logicalIndex)

    def paintSection(self, painter, rect, logicalIndex):
        if self.model():
            painter.save()
            self.model().hideHeaders()
            QtWidgets.QHeaderView.paintSection(self, painter, rect, logicalIndex)
            self.model().unhideHeaders()
            painter.restore()
            headerText = self.model().headerData(logicalIndex,
                                                 self.orientation(),
                                                 QtCore.Qt.DisplayRole)
            painter.drawText(QtCore.QRectF(rect), QtCore.Qt.TextWordWrap, headerText)
        else:
            QtWidgets.QHeaderView.paintSection(self, painter, rect, logicalIndex)

class Model(QtCore.QAbstractTableModel):
    def __init__(self):
        QtCore.QAbstractTableModel.__init__(self)
        self.model_cols_names = [ "Very-very long name of my first column",
                                  "Very-very long name of my second column",
                                  "Very-very long name of my third column",
                                  "Very-very long name of my fourth column" ]
        self.hide_headers_mode = False
        self.data = []
        for i in range(0, 10):
            row_data = []
            for j in range(0, len(self.model_cols_names)):
                row_data.append(''.join(random.choice(string.ascii_uppercase +
                                        string.digits) for _ in range(6)))
            self.data.append(row_data)

    def hideHeaders(self):
        self.hide_headers_mode = True

    def unhideHeaders(self):
        self.hide_headers_mode = False

    def rowCount(self, parent):
        if parent.isValid():
            return 0
        else:
            return len(self.data)

    def columnCount(self, parent):
        return len(self.model_cols_names)

    def data(self, index, role):
        if not index.isValid():
            return None
        if role != QtCore.Qt.DisplayRole:
            return None

        row = index.row()
        if row < 0 or row >= len(self.data):
            return None

        column = index.column()
        if column < 0 or column >= len(self.model_cols_names):
            return None

        return self.data[row][column]

    def headerData(self, section, orientation, role):
        if role != QtCore.Qt.DisplayRole:
            return None
        if orientation != QtCore.Qt.Horizontal:
            return None
        if section < 0 or section >= len(self.model_cols_names):
            return None
        if self.hide_headers_mode == True:
            return None
        else:
            return self.model_cols_names[section]

class MainForm(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        QtWidgets.QMainWindow.__init__(self, parent)
        self.model = Model()
        self.view = QtWidgets.QTableView()
        self.view.setModel(self.model)
        self.view.setHorizontalHeader(HeaderViewWithWordWrap())
        self.setCentralWidget(self.view)

def main():
    app = QtWidgets.QApplication(sys.argv)
    form = MainForm()
    form.show()
    app.exec_()

if __name__ == '__main__':
    main()
Mccourt answered 14/7, 2017 at 10:10 Comment(2)
I was able to migrate your code to c++ and got the header to wrap text. But when reimplementing the paintSection method we are just setting the plain text, all the other stuff in the header like background, borders, etc. is gone and it looks ugly. Is there a way to just set the text without changing the rest of the style?Cymoid
There is no good generic way but I found one slightly hackish approach, updated the answer now. Basically, you can implement paintSection as follows: 1. tell the model hide the header data i.e. to return empty data; 2. call parent class' paintSection method so that it paints everythiing but the text (which is empty); 3. tell the model to stop hiding the header data; 4. retrieve the header data and paint its textMccourt
S
3

In python:

myTableView.horizontalHeader().setDefaultAlignment(Qt.AlignCenter | Qt.Alignment(Qt.TextWordWrap))

Subvention answered 16/3, 2020 at 15:3 Comment(4)
Which import you used for this. Is it from PyQt5 import QtWidgets, QtCore; from PyQt5.QtCore import Qt; from PyQt5.QtWidgets import *Clapper
@SAndrew from PyQt5.QtCore import QtSubvention
This is the most sttraight forward solution and should get more votes!Unheardof
How to port this to Qt6? Qt.Alignment does not exist anymore in PySide6Bufford
H
2

An open Qt issue from 2010 on this suggests that this may not be easily possible. However, according to the only comment from 2015, there is a simple workaround for this very issue which goes like this:

   myTable->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter | (Qt::Alignment)Qt::TextWordWrap);

I just tested with Qt 5.12 and fortunately found it still working.

Hosier answered 6/11, 2019 at 9:50 Comment(0)
B
1

Jamie Gordon answer can be ported to Qt6 (PySide6) as:

horizontalHeader().setDefaultAlignment(Qt.AlignmentFlag.AlignCenter | Qt.TextFlag.TextWordWrap)
Bufford answered 30/8 at 9:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.