How to maintain widgets aspect ratio in Qt?
Asked Answered
R

5

40

How is it possible to maintain widgets aspect ratio in Qt and what about centering the widget?

Racon answered 16/1, 2009 at 22:50 Comment(0)
C
22

You don't have to implement your own layout manager. You can do with inheriting QWidget and reimplementing

int QWidget::heightForWidth( int w ) { return w; }

to stay square. However, heightForWidth() doesn't work on toplevel windows on X11, since apparently the X11 protocol doesn't support that. As for centering, you can pass Qt::AlignCenter as the third parameter of QBoxLayout::addWidget() or the fifth parameter of QGridLayout::addWidget().

Note: In newer versions of Qt at least, QWidget does not have the heightForWidth or widthForHeight anymore (so they cannot be overriden), and therefore setWidthForHeight(true) or setHeightForWidth(true) only have an effect for descendants of QGraphicsLayout.

Coon answered 21/7, 2009 at 17:8 Comment(6)
Yes. You need to set a sizePolicy that hasHeightForWidth() == true, and the layout will often give the widget more width or height that it asks for, but that's the layout trying to satisfy the constraints. You will get the same effect with the QLayoutItem solution, as they're equivalent (cf. QWidgetLayoutItem).Coon
@MarcMutz-mmutz: Of course X11 does support this: Look at XSizeHints tronche.com/gui/x/xlib/ICC/client-to-window-manager/… which allow to tell a window's aspect ratio to the window manager.Grimona
What if I want to set a widthForHeight instead? QWidget doesn't have a widthForHeight method.Rabid
So how to do this in new Qt versions?Roley
Mmm... as for Qt 5.7 doc.qt.io/qt-5/qwidget.html#hasHeightForWidthSecondly
As of Qt 6.7, heightForWidth works for any widget. However, widthForHeight is only for subclasses of QGraphicLayout(doc.qt.io/qt-6/qsizepolicy.html#setHeightForWidth).Deoxidize
R
8

The right answer is to create your custom layout manager. That is possible by subclassing QLayout.

Methods to implement when subclassing QLayout

void addItem(QLayoutItem* item);
Adds item to layout.
int count() const;
Returns the item count.
QLayoutItem* itemAt(int index) const;
Returns item reference at index or 0 if there's none.
QLayoutItem* takeAt(int index);
Takes and returns item from the layout from index or returns 0 if there is none.
Qt::Orientations expandingDirections() const;
Returns the layouts expanding directions.
bool hasHeightForWidth() const;
Tells if the layout handles height for width calculation.
QSize minimumSize() const;
Returns the layouts minimum size.
void setGeometry(const QRect& rect);
Sets the geometry of the layout and the items inside it. Here you have to maintain the aspect ratio and do the centering.
QSize sizeHint() const;
Returns the preferred size for the layout.

Further reading

Racon answered 18/2, 2009 at 19:29 Comment(2)
You are talking about subclassing QLayout, but the functions you quote are from QLayoutItem, which just calls the corresponding functions from QWidget. So what can inheriting QLayout give you that QWidget itself can't?Coon
also, all links are dead.Roley
T
7

Calling resize() from within resizeEvent() has never worked well for me -- at best it will cause flickering as the window is resized twice (as you have), at worst an infinite loop.

I think the "correct" way to maintain a fixed aspect ratio is to create a custom layout. You'll have to override just two methods, QLayoutItem::hasHeightForWidth() and QLayoutItem::heightForWidth().

Trevor answered 17/1, 2009 at 0:13 Comment(2)
This seems like the right answer but I have to look into it when I'm at work and maybe give a more detailed answer after I've done that.Racon
I agree with the fact that it can cause an infinite loop. Because of calling setMinimumHeight inside the resizeEvent function, my call stack is huge when my program crashes sometimes when I resize it. Sadly, I am looking into creating my own layout.Virelay
P
6

I too was trying to achieve the requested effect: a widget that keeps a fixed aspect ratio while staying centred in its allocated space. At first I tried other answers from this question:

  • implementing heightForWidth and hasHeightForWidth as suggested by marc-mutz-mmutz simply didn't work for me.
  • I briefly looked at implementing a custom layout manager, but all Bleadof's links were dead, and when I found the documentation and read through it, it looked way too complicated for what I was trying to achieve.

I ended up creating a custom widget that responds to resizeEvent and uses setContentsMargin to set margins such that the remaining content area keeps the desired ratio.

I found I also had to set the widget's size policy to QSizePolicy::Ignored in both directions to avoid odd resizing issues resulting from the size requests of child widgets—the end result is that my widget accepts whatever size its parent allocates to it (and then sets its margins as described above to keep the desired aspect ratio in its content area).

My code looks like this:

from PySide2.QtWidgets import QWidget, QSizePolicy


class AspectWidget(QWidget):
    '''
    A widget that maintains its aspect ratio.
    '''
    def __init__(self, *args, ratio=4/3, **kwargs):
        super().__init__(*args, **kwargs)
        self.ratio = ratio
        self.adjusted_to_size = (-1, -1)
        self.setSizePolicy(QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored))

    def resizeEvent(self, event):
        size = event.size()
        if size == self.adjusted_to_size:
            # Avoid infinite recursion. I suspect Qt does this for you,
            # but it's best to be safe.
            return
        self.adjusted_to_size = size

        full_width = size.width()
        full_height = size.height()
        width = min(full_width, full_height * self.ratio)
        height = min(full_height, full_width / self.ratio)

        h_margin = round((full_width - width) / 2)
        v_margin = round((full_height - height) / 2)

        self.setContentsMargins(h_margin, v_margin, h_margin, v_margin)

(Obviously, this code is in Python, but it should be straightforward to express in C++ or your language of choice.)

Psalmody answered 4/5, 2020 at 10:19 Comment(2)
This is one of the few options that really works. I tend to find the window stuttering when resizing, is it something related to my application in particular or is it something common for this solution?Eustoliaeutectic
@LorenzoCastellino I can't see any stutter on window resize in my application.Psalmody
S
4

In my case overriding heightForWidth() doesn't work. And, for someone, it could be helpful to get working example of using resize event.

At first subclass qObject to create filter. More about event filters.

class FilterObject:public QObject{
public:
    QWidget *target = nullptr;//it holds a pointer to target object
    int goalHeight=0;
    FilterObject(QObject *parent=nullptr):QObject(parent){}//uses QObject constructor
    bool eventFilter(QObject *watched, QEvent *event) override;//and overrides eventFilter function
};

Then eventFilter function. It's code should be defined outside of FilterObject definition to prevent warning. Thanks to this answer.

bool FilterObject::eventFilter(QObject *watched, QEvent *event) {
    if(watched!=target){//checks for correct target object.
        return false;
    }
    if(event->type()!=QEvent::Resize){//and correct event
        return false;
    }

    QResizeEvent *resEvent = static_cast<QResizeEvent*>(event);//then sets correct event type

    goalHeight = 7*resEvent->size().width()/16;//calculates height, 7/16 of width in my case
    if(target->height()!=goalHeight){
        target->setFixedHeight(goalHeight);
    }

    return true;
};

And then in main code create FilterObject and set it as EventFilter listener to target object. Thanks to this answer.

FilterObject *filter = new FilterObject();
QWidget *targetWidget = new QWidget();//let it be target object
filter->target=targetWidget;
targetWidget->installEventFilter(filter);

Now filter will receive all targetWidget's events and set correct height at resize event.

Struggle answered 29/9, 2018 at 15:58 Comment(1)
To avoid flickering - as mentioned in an answer above, do not add the widget to a layout, simply make it a child of the parent and then set size manually.Breechcloth

© 2022 - 2024 — McMap. All rights reserved.