I use as the central widget in a QMainWindow a QDialog with a bunch of QTableWidget in it.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
# the central widget
scrollArea = QScrollArea()
scrollArea.setWidgetResizable(True)
self.centerWidget = QDialog()
self.centerWidgetLayout = QVBoxLayout()
self.centerWidget.setLayout(self.centerWidgetLayout)
scrollArea.setWidget(self.centerWidget)
self.setCentralWidget(scrollArea)
self.populateLayout()
def defineTable(self, ncols):
tableWidget = QTableWidget()
tableWidget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
tableWidget.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
tableWidget.verticalHeader().hide()
tableWidget.horizontalHeader().hide()
tableWidget.setMinimumHeight(10)
tableWidget.horizontalHeader().setDefaultSectionSize(50)
tableWidget.horizontalHeader().setHighlightSections(False)
tableWidget.horizontalHeader().setSortIndicatorShown(False)
tableWidget.horizontalHeader().setStretchLastSection(False)
tableWidget.setColumnCount(ncols )
tableWidget.setRowCount(1)
for col in range(ncols):
tableWidget.setItem( 0, col, QTableWidgetItem("lalala"))
return tableWidget
def populateLayout(self):
for i in range(6):
self.centerWidgetLayout.addWidget( self.defineTable(i+1) )
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MainWindow()
ex.show()
sys.exit(app.exec_())
Curiously, the scroll bar is not effective.
Here is a picture illustrating the result when the size of the widget is large:
Everything is fine.
Now, I squeeze the widget and get:
This is not what I expect, and the widget is useless.
If I make it small enough, the scrollbar appears:
but it is still useless.
Any hint on how to solve this issue? Thanks.
Edit 1: As suggested by musicamante, the issue is triggered by
tableWidget.setMinimumHeight(10)
but if I remove this line, I get a blank space between the row and the scroll bar. Any idea how to remove this space?
Edit 2: The white space can be removed using
tableWidget.verticalHeader().setStretchLastSection(True)
However, the rows are now too large:
and my attempts to reduce the height of the rows were not successful. Any idea ?
If you want to limit the space of a view to its contents, you cannot use size restraints like setMinimum
or setMaximum
(with Size
, Height
or Width
), especially with arbitrary values.
When you call setMinimumHeight()
on those tables using a value bigger than 0, you tell the parent layout (the container widget of the scroll area) that they can also be shrunk down to that height.
Using a value of 10 for that height is clearly too small; you are telling that the whole view can be shrunk to 10 pixel, and that's a wrong value:
Removing setMinimumHeight()
is certainly necessary, but not enough.
Item views are scroll areas; by default all Qt scroll areas use expanding size policies, and have default size hints:
sizeHint()
usually defaults to 256x192
, and it's used when the parent layout has enough space to show it: if there is no constraint, that will be the size used to show the view, at least when initially shown;minimumSizeHint()
normally defaults to 75x75
, and it's used as the minimum possible size whenever the parent layout has to use the least space possible;For scroll areas, Qt will always try to use the minimumSizeHint
of its widgets if any of its dimensions are greater than 0, unless they have an explicit minimum size set (see the point above). This is what happens in your second attempt.
All the above means that you have to at least override the minimumSizeHint()
function and return a reasonable size, and the only proper way to do that is by using a subclass.
But, what size should be returned?
The minimum size of the contents of a QTableView is based on the header size hint of each section, but you also have to add the border (using the frameWidth()
property, since QAbstractScrollArea inherits from QFrame) and the relative scroll bar size, if visible.
Note that, for scroll areas that have the visibility of the scroll bars based on their necessity this can be tricky (the view may show scroll bars at the time of the sizeHint()
call, but the final hint might not require them); luckily that's not our case, since you've set the horizontal scroll bar policy to ScrollBarAlwaysOn
.
Considering all the above, this is a basic implementation:
class MinHeightTable(QTableWidget):
def minimumSizeHint(self):
height = (
self.frameWidth() * 2
+ self.verticalHeader().length()
+ self.horizontalScrollBar().sizeHint().height()
)
return QSize(super().minimumSizeHint().width(), height)
def sizeHint(self):
return QSize(
super().sizeHint().width(),
self.minimumSizeHint().height()
)
class MainWindow(QMainWindow):
# ...
def defineTable(self, ncols):
tableWidget = MinHeightTable()
# ...
That might not be sufficient though. For example, the view might might change its row count, making it impossible to properly see the other rows if not by using the mouse wheel or the keyboard navigation.
That could be easily achieved by setting the scroll area's sizeAdjustPolicy
property to AdjustToContents
, which will automatically tell the container that the size hint has changed.
Done? Not yet: the user might increase the size of the scroll area, which will still show the "gaps" near the right and bottom edges, in case no other widgets in the same layout claims the remaining available space (remember the "expanding" size policy).
Considering this specific case, we probably don't need to use size hints at all, since we want to always force a specific height. Then, we should just ensure that we always have a valid height using what learnt so far. Also, since we're already using a subclass, we could just move its configuration in its init.
Here is a possible implementation:
class MinHeightTable(QTableWidget):
def __init__(self, rows=1, cols=0):
super().__init__(rows, cols)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.verticalHeader().hide()
self.horizontalHeader().hide()
self.horizontalHeader().setDefaultSectionSize(
self.fontMetrics().horizontalAdvance('X' * 10))
# ensure that whenever the model changes in some way that could affect
# its "minimum" size, it notifies that its size hints have changed too
self.model().rowsInserted.connect(self.updateGeometry)
self.model().rowsRemoved.connect(self.updateGeometry)
# the dataChanged signal is included just as a precaution, in order to
# provide support in case the size hint of an item/index is explicitly
# changed; if both headers have resize modes set to ResizeToContent,
# the following may become redundant
self.model().dataChanged.connect(self.updateGeometry)
def baseHeight(self):
height = (
self.frameWidth() * 2
+ self.verticalHeader().length()
+ self.horizontalScrollBar().sizeHint().height()
)
if self.height() != height:
self.setFixedHeight(height)
return height
def minimumSizeHint(self):
return QSize(
super().minimumSizeHint().width(),
self.baseHeight()
)
def sizeHint(self):
return QSize(
super().sizeHint().width(),
self.baseHeight()
)
class MainWindow(QMainWindow):
# ...
def defineTable(self, ncols):
tableWidget = MinHeightTable(1, ncols)
for col in range(ncols):
tableWidget.setItem( 0, col, QTableWidgetItem("lalala"))
return tableWidget
Note: in the above code I did not consider transient scroll bars, which are only shown while hovering them or scrolling content, but do not change the content size if they are visible or not. You should probably check that by querying the current style hint with the QStyle.SH_ScrollBar_Transient
enum.
Finally, remember that QListView allows showing items in horizontal layouts. If you are going to always show only one "row" of items, QListView (or QListWidget) might be a better and actually simpler solution, because it also allows to lay out items horizontally; you would still need to override the size hint functions, but the final implementation might be much easier:
class SmallListWidget(QListWidget):
def __init__(self):
super().__init__()
self.setFlow(self.LeftToRight)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
def baseHeight(self):
opt = QStyleOptionViewItem()
opt.initFrom(self)
itemSize = self.style().sizeFromContents(
QStyle.CT_ItemViewItem, opt, QSize(), self)
height = (
itemSize.height()
+ self.frameWidth() * 2
+ self.horizontalScrollBar().sizeHint().height()
)
if self.height() != height:
self.setFixedHeight(height)
return height
def minimumSizeHint(self):
return QSize(
super().minimumSizeHint().width(),
self.baseHeight()
)
def sizeHint(self):
return QSize(
super().sizeHint().width(),
self.baseHeight()
)
class MainWindow(QMainWindow):
# ...
def defineList(self, ncols):
listWidget = SmallListWidget()
for col in range(ncols):
listWidget.addItem("lalala")
return listWidget
def populateLayout(self):
for i in range(6):
self.centerWidgetLayout.addWidget(self.defineList(i + 1))
The above approach has at least two major benefits: