3
votes

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?

4

4 Answers

3
votes

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()
2
votes

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);
0
votes

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.

0
votes

In python:

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