Reputation: 3665
4-5 years I needed a widget with the following properties
This subwidget should be used in a layout to provide some detail on how the other GUI elements in the layout work but only consume a minimum space to display its content.
I thought this was an easy one - but each time I return to the challenge I always end by giving up.
The main problem is that the layout breaks down when heightForWidth() is implemented and a QSizePolicy with setHeightForWidth(True) is used. It can shrink to infinitely small. Apparently this is Qt bug.
Another approach is to call updateGeometry() when a resizeEvent() occurs and call setFixedHeight(h) using a width dependent height. But this also gives rise to some weird layout behavior.
If anybody has any good suggestions on how to approach this, please let me know.
Below I include a snippet that reproduces the layout resizing behavior.
Best regards,
Mads
import sys
from PyQt4 import QtCore, QtGui
class Square(QtGui.QLabel):
def __init__(self, parent=None):
QtGui.QLabel.__init__(self, parent)
self.setAutoFillBackground(True)
palette = QtGui.QPalette()
palette.setColor(QtGui.QPalette.Window, QtGui.QColor('red'))
self.setPalette(palette)
policy = self.sizePolicy()
policy.setHeightForWidth(True)
self.setSizePolicy(policy)
def sizeHint(self):
return QtCore.QSize(128, 128)
def heightForWidth(self, width):
return width
class Widget(QtGui.QWidget):
def __init__(self, parent=None):
# Call base class constructor
QtGui.QWidget.__init__(self, parent)
# Add a layout
layout = QtGui.QVBoxLayout()
self.setLayout(layout)
# Add Square
label = Square()
layout.addWidget(label)
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
layout.addItem(spacerItem)
# Some dummy button
self._push_button = QtGui.QPushButton('Press me')
layout.addWidget(self._push_button)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
widget = Widget()
widget.show()
sys.exit(app.exec_())
Upvotes: 7
Views: 2595
Reputation: 788
I found this extremely problematic. I think the core of the problem is as follows:
parentwidget->setLayout(layout)
).QLayoutItem
-derived class (e.g. QWidgetItem
).hasHeightForWidth() == true
and providing int heightForWidth(int width)
-- have limited ability to describe their constraints to layouts, in that they can offer a minimumSizeHint()
, a sizeHint()
, and a heightForWidth(width)
. They can, if necessary, also call functions like setMinimumSize()
and setMaximumSize()
. But most Qt layouts, like QVBoxLayout
, QHBoxLayout
, and QGridLayout
, don't pay particular attention to the heightForWidth(width)
results when offering their own min/preferred/max size to their parent, because they do so via QLayout::minimumSize()
, QLayout::sizeHint()
, and QLayout::maximumSize()
-- none of which are called with information about the layout's target size (in a Catch-22-like situation), so they can't easily provide the width
value to their children.setGeometry(const QRect& layout_rect)
. Now the layout knows how big it is. It assigns space to its children with child->setGeometry()
.So you see two categories of solution, as you've outlined above, where size needs to be "properly" constrained to that required.
The first:
QLabel
-derived classes using word wrap, or images wanting to fix their aspect ratio, can provide sensible values for their minimum and maximum size, and a sensible sizeHint()
(being the size they'd like to be).QWidget::resizeEvent(QResizeEvent* event)
to find out their new width (e.g. from event->size()
); (2) calculate their preferred height via their own heightForWidth()
function; and (3) force their height via, for example, setFixedHeight(height)
followed by updateGeometry()
.resizeEvent
, and if the parent widget has a layout with hasHeightForWidth() == true
, doing something like setFixedHeight(layout->heightForWidth(width())); updateGeometry();
.The second:
parent->setFixedHeight()
.hasHeightForWidth()
and heightForWidth()
to have the new layouts (and their parent widgets, and any ancestor layouts using this mechanism) adjust their height.I've put C++ code at http://egret.psychol.cam.ac.uk/code/2017_01_16_qt_height_for_width/ for the following layouts:
BoxLayoutHfw
, VBoxLayoutHfw
, HBoxLayoutHfw
-- replacements for QBoxLayout
etc.GridLayoutHfw
-- replacements for QGridLayout
.FlowLayoutHfw
-- replacement for Qt's FlowLayout
(http://doc.qt.io/qt-5/qtwidgets-layouts-flowlayout-example.html).and the following widgets:
AspectRatioPixmapLabel
-- image maintaining its aspect ratio;LabelWordWrapWide
-- word-wrapping label that tries to use as much horizontal space before it word-wraps.VerticalScrollArea
-- as its name suggests, a vertical scroll area, but one that supports height-for-width cleanly.... plus some infrastructure code (#define
s etc.) that should make the layouts revert to their Qt equivalent's behaviour, and some support files (including gui_defines.h
and layouts.h
that make the choice of layout and base widget conditional on your preference in this regard).
One residual problem that I've not successfully addressed is that I think QLabel
's heightForWidth()
seems to return slightly wrong values (slightly overestimating its space requirements) with stylesheets under some circumstances. I suspect the problem is in QLabelPrivate::sizeForWidth
but I've just worked around it imperfectly by calculating some stylesheet borders; it's still not quite right, but overestimating (leading to whitespace) is better than underestimating (leading to clipping).
Upvotes: 8
Reputation: 704
Size hint will not dictate child widget's size except Fixed policy. If autoresize-like policies will surplus by parent.
Upvotes: 0