3

I have a subclassed QAbstractTableModel
to display the data in the Table View in full size without scrolling I have turned of the Scrollbars
to get ridd of the white space around the Table View I have set the vertical/horizontal table length to a specific value.

enter image description here
Problem is that I have added a add/deleate row Method to the Model so the Table View expands/shrinks now
to adjust the Table View behavior to display the data in full size and without white space I have set the horizontal Header to table_view.horizontalHeader().setStretchLastSection(True)
which cuts off the white space in horicontal direction correctly
the same operation for the vertical header cuts of the white space too but does over strech the last row

overstretching

I tryed to set each row to a default size with

table_view.verticalHeader().setSectionResizeMode(qtw.QHeaderView.Fixed)
        table_view.verticalHeader().setDefaultSectionSize(40)

but this turns the white space on again

In short form: Im looking for a way to display the model data in the Table View in Full Size without white Space while beeing able to deleate/insert a row


code example

#!/usr/bin/env python

"""

"""

import sys
import re


from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5.QtCore import Qt

from PyQt5 import QtGui as qtg


class ViewModel(qtc.QAbstractTableModel):

    def __init__(self, input_data=None):
        super().__init__()

        self.input_data = input_data or [["data","data","data","data"],["data","data","data","data"]]

    #

    def data(self, index, role):  # parameter index, role are needed !
        """

        """
        if role == qtc.Qt.DisplayRole:
            try:
                text = self.input_data[index.row()][index.column()]
            except IndexError:
                text = None

            return text

    def rowCount(self, index=qtc.QModelIndex()):
        return 0 if index.isValid() else len(self.input_data)


    def columnCount(self, index):
        return len(self.input_data[0])


    def insertRows(self, position, rows, parent=qtc.QModelIndex()):

        print(position) # -1
        position = (position + self.rowCount()) if position < 0 else position
        start = position

        end = position + rows - 1

        if end <= 8:
            self.beginInsertRows(parent, start, end)
            self.input_data.append([])
            self.endInsertRows()
            return True
        else:
            return False


    def removeRows(self, position, rows, parent=qtc.QModelIndex()):
        position = (position + self.rowCount()) if position < 0 else position

        start = position

        end = position + rows - 1

        if end >= 1:
            self.beginRemoveRows(parent, start, end)
            del self.input_data[start:end + 1]
            self.endRemoveRows()
            return True
        else:
            return False



    def headerData(self, section, orientation, role):

        if role == qtc.Qt.DisplayRole:

            if orientation == qtc.Qt.Horizontal:
                return "hight " + str(section+1) + " /mm"
            if orientation == qtc.Qt.Vertical:
                return "width " + str(section+1)


    def flags(self, index):
        return qtc.Qt.ItemIsEditable | qtc.Qt.ItemIsSelectable | qtc.Qt.ItemIsEnabled

    def setData(self, index, value, role=qtc.Qt.EditRole):
        if role == qtc.Qt.EditRole:
            try:
                row = index.row()
                column = index.column()

                pattern = '^[\d]+(?:,[\d]+)?$'


                if re.fullmatch(pattern, value, flags=0):
                    print("true")
                    self.input_data[row][column] = value  # float

                else:
                    print("nope")
                    pass

                return True

            except ValueError:
                print("not a number")
                return False


    def display_model_data(self):
        print(self.input_data)


class MainWindow(qtw.QWidget):
    def __init__(self):
        super().__init__()

        # geometry
        self.setGeometry(900, 360, 700, 800)


        # View
        table_view = qtw.QTableView()



        # done # turn scroll bars off
        table_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        table_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)





        self.model = ViewModel()
        table_view.setModel(self.model)




        table_view.horizontalHeader().setStretchLastSection(True)
        # table_view.verticalHeader().setStretchLastSection(True)

        table_view.verticalHeader().setSectionResizeMode(qtw.QHeaderView.Fixed)
        table_view.verticalHeader().setDefaultSectionSize(24)

        table_view.verticalHeader().setStretchLastSection(True)
        #     verticalHeader->setSectionResizeMode(QHeaderView::Fixed);
        # verticalHeader->setDefaultSectionSize(24);



        # widgets
        self.insert_row_button = qtw.QPushButton("insert row")
        self.deleate_row_button = qtw.QPushButton("deleate row")

        # layout
        layout = qtw.QVBoxLayout()
        layout.addWidget(table_view)
        layout.addWidget(self.insert_row_button)
        layout.addWidget(self.deleate_row_button)


        self.setLayout(layout)
        self.show()

        # function
        self.insert_row_button.clicked.connect(lambda: self.model.insertRows(-1, 1))
        self.deleate_row_button.clicked.connect(lambda: self.model.removeRows(-1, 1))


if __name__ == '__main__':
    app = qtw.QApplication(sys.argv)
    w = MainWindow()
    sys.exit(app.exec_())

Sator
  • 636
  • 4
  • 13
  • 34
  • I don't understand. If you want the last row to stretch, it will obviously occupy all the remaining available vertical space. Can you clarify what you have in mind? – musicamante Jun 11 '20 at 15:17
  • @musicamante I added a picture for clarification – Sator Jun 11 '20 at 15:27
  • I still don't understand. Can't you just avoid using `setStretchLastSection` and `setDefaultSectionSize` with a fixed resize mode? – musicamante Jun 11 '20 at 15:39
  • @musicamante `table_view.verticalHeader().setSectionResizeMode(qtw.QHeaderView.Fixed)` with `table_view.verticalHeader().setDefaultSectionSize(x)` set the rows to the same hight but streches the vertical white space – Sator Jun 11 '20 at 15:52
  • If you use `setStretchLastSection` it will stretch the "vertical white space", that's **exactly** what setStretchLastSection does. I'm beginning to believe that you're asking for the wrong question. Maybe you want the *table* to "shrink" its height so that it only occupies as much vertical space as its rows need? – musicamante Jun 11 '20 at 16:09
  • @musicamante exactly, it should expand when a row is added and shrink when a row is been deleated without blank white space, "normaly" the scrollbars are managing this behavior but I turned them of so I dont know how to replicate this behavior without the scroll bars – Sator Jun 11 '20 at 16:29
  • It's still not clear (to me) how you want it to behave: for example, if there is a layout manager, should it take only the required space while leaving the remaining to other widgets if they require it? Or should that space be reserved anyway? And what about bigger data models, where the row count might require a height bigger than the available vertical screen space? – musicamante Jun 13 '20 at 22:46
  • @musicamante the space should be reserved for the expanding QTableView, about adding bigger data models: the easiest way would be to test with trial and error how much vertical space space is needed, but this could be a problem when running the window in Fullscreen – Sator Jun 14 '20 at 11:55

2 Answers2

3

space can't disappear magically. let's say the total table height is 600. if there are two rows in the table, the first row is 40. then, the second one is 600 - 40 = 560 if you don't wan't blank at the bottom of the table. if you set height of each row to 40, the height of the blank space would be 600 - 2 * 40 = 520. you can't require (a total height 600) + (two rows, 40 for each) + (no blank space at the bottom).

So, let me guess, you want (a. no blank space at the bottom) + (b, space is evenly split into row, so that the last row won't look weird.). If that if the case, I've edited your code to below which explains everything:

"""

"""

import sys
import re


from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5.QtCore import Qt

from PyQt5 import QtGui as qtg


class ViewModel(qtc.QAbstractTableModel):

    def __init__(self, input_data=None):
        super().__init__()

        self.input_data = input_data or [["data","data","data","data"],["data","data","data","data"]]

    #

    def data(self, index, role):  # parameter index, role are needed !
        """

        """
        if role == qtc.Qt.DisplayRole:
            try:
                text = self.input_data[index.row()][index.column()]
            except IndexError:
                text = None

            return text

    def rowCount(self, index=qtc.QModelIndex()):
        return 0 if index.isValid() else len(self.input_data)


    def columnCount(self, index):
        return len(self.input_data[0])


    def insertRows(self, position, rows, parent=qtc.QModelIndex()):

        print(position) # -1
        position = (position + self.rowCount()) if position < 0 else position
        start = position

        end = position + rows - 1

        if end <= 8:
            self.beginInsertRows(parent, start, end)
            self.input_data.append([])
            self.endInsertRows()
            return True
        else:
            return False


    def removeRows(self, position, rows, parent=qtc.QModelIndex()):
        position = (position + self.rowCount()) if position < 0 else position

        start = position

        end = position + rows - 1

        if end >= 1:
            self.beginRemoveRows(parent, start, end)
            del self.input_data[start:end + 1]
            self.endRemoveRows()
            return True
        else:
            return False



    def headerData(self, section, orientation, role):

        if role == qtc.Qt.DisplayRole:

            if orientation == qtc.Qt.Horizontal:
                return "hight " + str(section+1) + " /mm"
            if orientation == qtc.Qt.Vertical:
                return "width " + str(section+1)


    def flags(self, index):
        return qtc.Qt.ItemIsEditable | qtc.Qt.ItemIsSelectable | qtc.Qt.ItemIsEnabled

    def setData(self, index, value, role=qtc.Qt.EditRole):
        if role == qtc.Qt.EditRole:
            try:
                row = index.row()
                column = index.column()

                pattern = '^[\d]+(?:,[\d]+)?$'


                if re.fullmatch(pattern, value, flags=0):
                    print("true")
                    self.input_data[row][column] = value  # float

                else:
                    print("nope")
                    pass

                return True

            except ValueError:
                print("not a number")
                return False


    def display_model_data(self):
        print(self.input_data)


class NoBlankSpaceAtBottomEnvenlySplitTableView(qtw.QTableView):
    def sizeHintForRow(self, row):
        row_count = self.model().rowCount()
        height = self.viewport().height()
        row_height = int(height/row_count)
        if row < row_count - 1:
            return row_height
        else:
            return super().sizeHintForRow(row)


class MainWindow(qtw.QWidget):
    def __init__(self):
        super().__init__()

        # geometry
        self.setGeometry(900, 360, 700, 800)


        # View
        # table_view = qtw.QTableView()
        table_view = NoBlankSpaceAtBottomEnvenlySplitTableView()



        # done # turn scroll bars off
        table_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        table_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)





        self.model = ViewModel()
        table_view.setModel(self.model)




        table_view.horizontalHeader().setStretchLastSection(True)
        table_view.verticalHeader().setStretchLastSection(True)

        # table_view.verticalHeader().setSectionResizeMode(qtw.QHeaderView.Fixed)
        #table_view.verticalHeader().setDefaultSectionSize(24)
        table_view.verticalHeader().setSectionResizeMode(
            qtw.QHeaderView.ResizeToContents)  # Add this line

        table_view.verticalHeader().setStretchLastSection(True)
        #     verticalHeader->setSectionResizeMode(QHeaderView::Fixed);
        # verticalHeader->setDefaultSectionSize(24);



        # widgets
        self.insert_row_button = qtw.QPushButton("insert row")
        self.deleate_row_button = qtw.QPushButton("deleate row")

        # layout
        layout = qtw.QVBoxLayout()
        layout.addWidget(table_view)
        layout.addWidget(self.insert_row_button)
        layout.addWidget(self.deleate_row_button)


        self.setLayout(layout)
        self.show()

        # function
        self.insert_row_button.clicked.connect(lambda: self.model.insertRows(-1, 1))
        self.deleate_row_button.clicked.connect(lambda: self.model.removeRows(-1, 1))


if __name__ == '__main__':
    app = qtw.QApplication(sys.argv)
    w = MainWindow()
    sys.exit(app.exec_())

Edit: Table auto adjusts its height according to rows

import sys
import re


from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import QSizePolicy

from PyQt5 import QtGui as qtg


class ViewModel(qtc.QAbstractTableModel):

    def __init__(self, input_data=None):
        super().__init__()

        self.input_data = input_data or [["data","data","data","data"],["data","data","data","data"]]

    #

    def data(self, index, role):  # parameter index, role are needed !
        """

        """
        if role == qtc.Qt.DisplayRole:
            try:
                text = self.input_data[index.row()][index.column()]
            except IndexError:
                text = None

            return text

    def rowCount(self, index=qtc.QModelIndex()):
        return 0 if index.isValid() else len(self.input_data)


    def columnCount(self, index):
        return len(self.input_data[0])


    def insertRows(self, position, rows, parent=qtc.QModelIndex()):

        print(position) # -1
        position = (position + self.rowCount()) if position < 0 else position
        start = position

        end = position + rows - 1

        if end <= 8:
            self.beginInsertRows(parent, start, end)
            self.input_data.append([])
            self.endInsertRows()
            return True
        else:
            return False


    def removeRows(self, position, rows, parent=qtc.QModelIndex()):
        position = (position + self.rowCount()) if position < 0 else position

        start = position

        end = position + rows - 1

        if end >= 1:
            self.beginRemoveRows(parent, start, end)
            del self.input_data[start:end + 1]
            self.endRemoveRows()
            return True
        else:
            return False



    def headerData(self, section, orientation, role):

        if role == qtc.Qt.DisplayRole:

            if orientation == qtc.Qt.Horizontal:
                return "hight " + str(section+1) + " /mm"
            if orientation == qtc.Qt.Vertical:
                return "width " + str(section+1)


    def flags(self, index):
        return qtc.Qt.ItemIsEditable | qtc.Qt.ItemIsSelectable | qtc.Qt.ItemIsEnabled

    def setData(self, index, value, role=qtc.Qt.EditRole):
        if role == qtc.Qt.EditRole:
            try:
                row = index.row()
                column = index.column()

                pattern = '^[\d]+(?:,[\d]+)?$'


                if re.fullmatch(pattern, value, flags=0):
                    print("true")
                    self.input_data[row][column] = value  # float

                else:
                    print("nope")
                    pass

                return True

            except ValueError:
                print("not a number")
                return False


    def display_model_data(self):
        print(self.input_data)


class AutoExpandingTableView(qtw.QTableView):
    # def sizeHintForRow(self, row):
    #     row_count = self.model().rowCount()
    #     height = self.viewport().height()
    #     row_height = int(height/row_count)
    #     if row < row_count - 1:
    #         return row_height
    #     else:
    #         return super().sizeHintForRow(row)

    def sizeHint(self):
        viewport_size_hint = self.viewportSizeHint()
        return QSize(
            self.width(),
            viewport_size_hint.height()
        )


class MainWindow(qtw.QWidget):
    def __init__(self):
        super().__init__()

        # geometry
        self.setGeometry(900, 360, 700, 800)


        # View
        # table_view = qtw.QTableView()
        table_view = AutoExpandingTableView()
        table_view.setSizePolicy(
            QSizePolicy.Expanding,
            QSizePolicy.Preferred
        )

        # done # turn scroll bars off
        table_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        table_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        self.model = ViewModel()
        table_view.setModel(self.model)
        table_view.model().rowsInserted.connect(table_view.adjustSize)
        table_view.model().rowsRemoved.connect(table_view.adjustSize)

        table_view.horizontalHeader().setStretchLastSection(True)
        # table_view.verticalHeader().setStretchLastSection(True)

        # table_view.verticalHeader().setSectionResizeMode(qtw.QHeaderView.Fixed)
        #table_view.verticalHeader().setDefaultSectionSize(24)
        table_view.verticalHeader().setSectionResizeMode(
            qtw.QHeaderView.ResizeToContents)  # Add this line

        # widgets
        self.insert_row_button = qtw.QPushButton("insert row")
        self.deleate_row_button = qtw.QPushButton("deleate row")

        # layout
        layout = qtw.QVBoxLayout()
        layout.addWidget(table_view)
        layout.addStretch()
        layout.addWidget(self.insert_row_button)
        layout.addWidget(self.deleate_row_button)


        self.setLayout(layout)
        self.show()

        # function
        self.insert_row_button.clicked.connect(lambda: self.model.insertRows(-1, 1))
        self.deleate_row_button.clicked.connect(lambda: self.model.removeRows(-1, 1))


if __name__ == '__main__':
    app = qtw.QApplication(sys.argv)
    w = MainWindow()
    sys.exit(app.exec_())
ToSimplicity
  • 293
  • 1
  • 8
  • your solution gives me two oversized rows at the beginning, not quit what Im looking for, is there a posibillity to add spacers to the layout and decease/increase there size if a row is beeing added/deleated so the rows dont overstretch? – Sator Jun 14 '20 at 12:01
  • let me understand what you want better: if your window height is 800, area height for displaying content is 600(excluding title bar, menu bar, etc), and there are three rows in the table, what is the height of the 1st row you want? what is the height of the 2nd row you want? what is the height of the 3rd row you want?what's the height of table you want? – ToSimplicity Jun 14 '20 at 12:15
  • the height of each row should be equall, like (20,20,20) thats the problem with `table_view.verticalHeader().setStretchLastSection(True)` its stretches the last row to fill out the left space – Sator Jun 14 '20 at 12:30
  • then, if the height of table is 600, blank area has height of 600 - 20 * 3 = 540. So, do you want to keep the table height at 600 and has blank area of height 540 inside? or change the table height to 60 and no blank area inside table, but there is a blank area between the bottom of table and the bottom of your window? – ToSimplicity Jun 14 '20 at 12:44
  • theese are the options Im stuck with.if you look at my posted code example and described problem ///// eather has the tableview white space on the buttom, or a a overstretched table, what Im trying to achive is to increase/decrease the hight of the table_view without blank space and while all rows are keeping there same height – Sator Jun 14 '20 at 13:25
  • nice work ! can you pls explain what exactly`def sizeHint(self):` does? – Sator Jun 14 '20 at 17:35
  • telling QT the baseline of the size user wants. QSizePolicy tells QT how user understands "baseline" – ToSimplicity Jun 14 '20 at 17:39
  • @ToSimplicity calling `adjustSize` is not a very good choice, and (while technically not *that* harmful) I don't think it's generally a good idea to use `self.width()` (or `self.height()`) inside the `sizeHint()` override. – musicamante Jun 15 '20 at 18:48
  • @musicamante, why? I am considering self.width() as "whatever it is now, it's my favorite". I agree that ajustSize is not ideal here. if there is other requirement in the future, this will likely need to be changed. – ToSimplicity Jun 15 '20 at 19:27
  • @ToSimplicity as I said, using the "current" size is not *that* harmful, but you've to consider that the width of a widget should be computed *after* sizeHint returns (in complex layout systems, that call happens *a lot*), and it might cause unexpected behavior when a window is mapped the first time; this also could cause some problems in case `adjustSize` is called. If you're not sure about the size to return, you can just stick with the value(s) returned from the base sizeHint implementation of the widget for the specified size, or, eventually, -1. – musicamante Jun 15 '20 at 21:52
2

The most important aspect to consider is the sizeHint(), that is the recommended size a widget suggests to the layout that contains it.

Item views are tricky, though. They might have headers, their content could change many times during the lifespan of the program, and each item might have different sizes (which the user could interactively modify).

To achieve what you want, you have to use updateGeometry():

Notifies the layout system that this widget has changed and may need to change geometry.

Call this function if the sizeHint() or sizePolicy() have changed.

Note that calling adjustSize() is not suggested for this.

The size hint of an item view then must take into account the (visible) headers and the frame width, since all QAbstractItemView descendants inherit from QFrame.

Finally, to ensure that the size hint is dynamically adjusted and the layout system is notified about it, you should also connect all the correct signals that the model AND the header might send.
Note that, while you can connect all those signals externally, it is usually better to let the class itself take care of it internally.

class ExpandingTableView(qtw.QTableView):
    shown = False
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        self.verticalHeader().sectionResized.connect(self.updateGeometry)
        self.verticalHeader().sectionCountChanged.connect(self.updateGeometry)

    def setVerticalHeader(self, header):
        self.verticalHeader().sectionResized.disconnect(self.updateGeometry)
        self.verticalHeader().sectionCountChanged.disconnect(self.updateGeometry)
        super().setVerticalHeader(header)
        header.sectionResized.connect(self.updateGeometry)
        header.sectionCountChanged.connect(self.updateGeometry)

    def setModel(self, model):
        if self.model():
            self.model().rowsInserted.disconnect(self.updateGeometry)
            self.model().rowsRemoved.disconnect(self.updateGeometry)
        super().setModel(model)
        if model:
            model.rowsInserted.connect(self.updateGeometry)
            model.rowsRemoved.connect(self.updateGeometry)
        self.updateGeometry()

    # optional, if you want to ensure that a minimum height is always respected
    def updateGeometry(self):
        self.setMinimumHeight(min(self.sizeHint().height(), 
            self.verticalHeader().defaultSectionSize() * 8))
        super().updateGeometry()

    def sizeHint(self):
        height = 0
        if self.horizontalHeader().isVisible():
            height += self.horizontalHeader().height()
        height += self.verticalHeader().length() + self.frameWidth() * 2
        return QSize(super().sizeHint().width(), height)

    def showEvent(self, event):
        super().showEvent(event)
        # when the view is shown the first time it might not have computed the
        # correct size hint, let's ensure that we notify the underlying
        # layout manager(s)
        if not self.shown:
            self.shown = True
            self.updateGeometry()
musicamante
  • 41,230
  • 6
  • 33
  • 58