Issue
I've created a very simple table widget that has 1 selectable and 2 non-selectable rows. I want to then restrict the table so that it doesn't stretch to fill all available space.
The table should stop after the "Other" entry rather than continuing to fill the space.
Solution
A QTableWidget is not a good choice for such an interface: if you use a table with the left item for the text, then you're forced to always compute the proper spanning of that cell, which might not work well if the text requires more vertical space than the contents on the right.
Consider your case: what would have happened to the text if you only had one item on the right?
Also, what if you have much more elements? When the user scrolls the view to see the other options, then the text would scroll along with it.
A proper choice would be, instead, using a standard text widget (possibly, a QLabel, but be aware about known layout issues) and QListWidget for the contents.
In order to properly achieve the result, we have to consider some important aspects.
First, all Qt scroll areas are expanding by default (which is the main point of your problem), and also implement various aspects of widgets in order to ensure that they always have a proper size by showing as much content as possible, and provide a common, expected behavior.
The default behavior of a layout when adding a widget is to let it occupy the whole space of its layout cell: if a widget has a size hint of 50x50
, the layout allows for 100x100
, and the widget size policy allows it to grow (which is not the same as expand), then the widget will occupy the whole 100x100
size.
To prevent this, you can specify the alignment
flag to addWidget()
, and in this way the widget will only use its minimum size hint of the alignment direction; considering the case above:
- if you use
alignment=Qt.AlignTop
, the widget will be aligned on top, and will have a size of100x50
; - for a
Qt.AlignLeft
alignment, the widget will be on the left, and sized50x100
;
Ok, but what now?
A common solution is to override the sizeHint()
and return a proper height based on the contents. For instance:
class MyList(QListWidget):
def sizeHint(self):
hint = super().sizeHint()
if self.count():
height = sum(self.sizeHintForRow(r) for r in range(self.count()))
hint.setHeight(height + self.frameWidth() * 2)
return hint
Note: the self.frameWidth() * 2
considers the border margins around the scroll area; see the related note below.
This "works", but has some issues. For instance, if items are inserted/removed after the widget is added to an already active layout, the hint is not considered. So we also need to notify the layout whenever the widget size hint changes, by calling updateGeometry()
:
class MyList(QListWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.model().rowsInserted.connect(self.updateGeometry)
self.model().rowsRemoved.connect(self.updateGeometry)
Unfortunately, still not enough: we still can get a view that is bigger than the contents, because there are not enough items: Qt scroll areas also return a valid minimum size hint (normally, 75x75
), meaning that if you have contents that are smaller than that size, the view will still be bigger than you would hope.
Now, Qt item views actually implement a sizeAdjustPolicy
property, which results in returning a different size from sizeHint()
based on the view's contents and eventually adjust the whole layout.
Even this partially works, though, because of the minimumSizeHint
property explained above. To avoid this, then we can just revert to the default behavior of QWidget, which is to return an invalid QSize from minimumSizeHint()
.
We're almost there.
Qt scroll areas also have a viewportSizeHint()
function, that returns a basic size hint of the viewport, which is the hint of the visible area in which the contents are visible and scroll may happen. This is what the sizeAdjustPolicy
property above becomes important: the scroll area uses the viewportSizeHint
to compute the overall size hint, including contents, margins and scroll bars (if always/already shown).
Finally, there's a peculiar case: when an item is wide enough to require the horizontal scroll bar. While this is not really a problem when many items are shown, it can be annoying when they are few, and especially if there's only one. On systems that use scroll bars that have heights similar to the font size, the result is that the item becomes almost (if not completely) invisible.
While we could just ignore all the problems above just for this situation, and just implement sizeHint()
, this is not a very good solution if we don't have absolute knowledge of the possible contents of the view. Since we cannot use any of the hint-related functions to know if the scroll bar is needed (we'd risk recursion), a possibility is to implement the heightForWidth()
function (which is normally called after size hint calls) and return a proper height considering the possible presence of the scroll bar. Note that this must also consider the frameWidth
property (the "border") as did above; this is normally not an issue, unless you specify different border size properties in a Qt Style Sheet (e.g. a left border that is different from the top one).
Ok, we're probably done.
Here is a basic code that shows the proper implementation of all the above:
class AdjustList(QListWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setSizeAdjustPolicy(self.AdjustToContents)
def hasHeightForWidth(self):
return True
def heightForWidth(self, width):
height = self.sizeHint().height()
if (self.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded
and width < self.sizeHintForColumn(0) + self.frameWidth() * 2
):
height += self.horizontalScrollBar().sizeHint().height()
return height
def minimumSizeHint(self):
return QSize()
def viewportSizeHint(self):
hint = super().viewportSizeHint()
if self.count():
height = sum(self.sizeHintForRow(r) for r in range(self.count()))
hint.setHeight(height)
return hint
And here is a possible result:
The code for the image above, just for test and explanation purposes:
from random import choice, randrange
from string import ascii_lowercase as letters
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class AdjustList(QListWidget):
# as above
class WizardPage(QWidget):
def __init__(self, text, choices):
super().__init__()
self.resize(420, 300)
self.descriptionLabel = QLabel(text,
wordWrap=True, alignment=Qt.AlignTop)
self.choiceList = AdjustList()
self.choiceList.addItems(choices)
layout = QVBoxLayout(self)
hLayout = QHBoxLayout()
layout.addLayout(hLayout)
hLayout.addWidget(self.descriptionLabel, stretch=3)
hLayout.addWidget(self.choiceList, stretch=4, alignment=Qt.AlignTop)
layout.addStretch(1)
# just for explanation purposes
for i in range(1, 11):
QTimer.singleShot(i * 1000, lambda:
self.choiceList.addItem(
''.join(choice(letters) for l in range(randrange(1, 80))
).capitalize()
))
app = QApplication([])
words = [''.join(choice(letters) for l in range(randrange(10))) for w in range(25)]
text = 'Some random text ' + ' '.join(words)
items = ['Google', 'iCloud', 'Other']
wiz = WizardPage(text, items)
wiz.show()
app.exec()
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.