Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

heightForWidth label

Tags:

qt

pyqt

4-5 years I needed a widget with the following properties

  • Display text incl. HTML
  • Text should be wrapped on several lines
  • When the widget is put into a layout, the height of the widget should be adjusted in such a way that the text exactly fits the widget geometry

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_())
like image 876
repoman Avatar asked Jan 09 '13 15:01

repoman


People also ask

How to set the height and width of the label widget?

To set the height and width of the label widget, we should declare the Label widget with a variable. Instantiating the label widget with a variable allows the users to add/edit the properties of the Label widget.

How to control the width of the <label> tag in HTML?

In this tutorial, we’ll demonstrate some examples of controlling the width of the <label> tag by using the display property set to “block” in combination with the width property. Usa a <form> element. Place the <label> tag with the for attribute and the <input> tag with the id, name, and type attributes inside the <form> element.

Is the width of a label the same as the CSS code?

It's the same width whether the css code exists or not. The label is an inline element, meaning it is only as big as it needs to be. Set the display property to either inline-block or block in order for the width property to take effect.

What is the use of the height() function in Qt?

This function was introduced in Qt 5.0. Returns the preferred height for this widget, given the width w. If this widget has a layout, the default implementation returns the layout's preferred height. if there is no layout, the default implementation returns -1 indicating that the preferred height does not depend on the width.


1 Answers

I found this extremely problematic. I think the core of the problem is as follows:

  • Qt layouts are typically assigned to a widget that we can call the parent widget (with parentwidget->setLayout(layout)).
  • Layouts ask their child widgets (or child layouts) for their size preferences (minimum size, size hint [preferred size], and maximum size). This is done in a slightly complex way via a QLayoutItem-derived class (e.g. QWidgetItem).
  • Height-for-width (HFW) widgets -- those having 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.
  • So, the layout asks its children how big they want to be without much HFW thought, and thus sets its own minimum/preferred/maximum size (determining, potentially in conjunction with other constraints, the size of the parent widget).
  • After the layout has told its parent (and its parent, etc.) how much space it needs, Qt works out how big it thinks everything should be. The layout is called via its setGeometry(const QRect& layout_rect). Now the layout knows how big it is. It assigns space to its children with child->setGeometry().
  • But only at this point has the layout discovered its final width. So up to this point, it cannot offer a final width to its children, and thus HFW widgets can't know their final width until they're being laid out finally. By this time, the layout and its parent may already have been set to the wrong height (can be too big; can be too small).
  • An excellent description of the widget/layout interaction is at http://kdemonkey.blogspot.co.uk/2013/11/understanding-qwidget-layout-flow.html; beyond this, you're best off looking at the Qt source code itself.

So you see two categories of solution, as you've outlined above, where size needs to be "properly" constrained to that required.

The first:

  • HFW widgets, like 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).
  • Then, when they're laid out, they (1) intercept 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().
  • This tends to work reasonably, except that any parent widget that wants to match its size to such an HFW widget has to do the same thing, e.g. intercepting resizeEvent, and if the parent widget has a layout with hasHeightForWidth() == true, doing something like setFixedHeight(layout->heightForWidth(width())); updateGeometry();.
  • That leads to faff as you have to modify potentially arbitrary widgets in a long line of parenthood.
  • It can also lead to quite a lot of redrawing, which can be slow and cause visual flickering.

The second:

  • Rewrite the layouts.
  • The best approach I've found is to have layouts calculate their children's geometry assuming some sort of standard rectangle (Qt itself often starts with a 640x480 one when drafting a layout); offering height information based on this draft; then, when setGeometry() is called, if the resulting height (based on a potentially new width) doesn't match what the layout previously advertised, re-constraining by calling parent->setFixedHeight().
  • This allows you to use arbitrary widgets, and HFW widgets only need to support hasHeightForWidth() and heightForWidth() to have the new layouts (and their parent widgets, and any ancestor layouts using this mechanism) adjust their height.
  • It can lead to some redrawing, but often not too much, as it happens on a per-layout not per-widget basis.

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 (#defines 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).

like image 98
Rudolf Cardinal Avatar answered Sep 30 '22 17:09

Rudolf Cardinal