Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to expand /decrease QTable View without visible blank white space

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_())

like image 363
Sator Avatar asked Feb 07 '26 10:02

Sator


2 Answers

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_())
like image 102
ToSimplicity Avatar answered Feb 08 '26 22:02

ToSimplicity


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()
like image 32
musicamante Avatar answered Feb 08 '26 23:02

musicamante



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!