Reputation: 1193
How is it possible to maintain widgets aspect ratio in Qt and what about centering the widget?
Upvotes: 41
Views: 49465
Reputation: 1
Here is an example. The AspectRatioLabel
widget needs to be inside a layout to best work.
from PyQt5.QtCore import QSize
from PyQt5 import QtWidgets
class AspectRatioLabel(QtWidgets.QLabel):
def __init__(self, aspect_ratio, parent=None):
super().__init__(parent)
self.aspect_ratio = aspect_ratio
def _adapt_size(self, size: QSize, prefer_increase=True):
width, height = size.width(), size.height()
min_width, min_height = self.minimumWidth(), self.minimumHeight()
if prefer_increase:
max_width, max_height = self.maximumWidth(), self.maximumHeight()
else:
# use initial size as maximum
max_width, max_height = size.width(), size.height()
if height < int(width / self.aspect_ratio):
# Increase height to match aspect ratio
height = int(width / self.aspect_ratio)
if height > max_height:
# Too tall, decrease width based on max_height
height = max_height
width = int(height * self.aspect_ratio)
if width < min_width:
# Too narrow, increase height based on min_width (but
# max_height will not be respected)
width = min_width
height = int(width / self.aspect_ratio)
elif width < int(height * self.aspect_ratio):
# Increase width to match aspect ratio
width = int(height * self.aspect_ratio)
if width > max_width:
# Too wide, decrease height based on max_width
width = max_width
height = int(width / self.aspect_ratio)
if height < min_height:
# Too short, increase width based on min_height (but
# max_width will not be respected)
height = min_height
width = int(height * self.aspect_ratio)
return QSize(width, height)
def minimumSizeHint(self):
# Provide a default minimum size with the correct aspect ratio
return self._adapt_size(super().minimumSizeHint())
def sizeHint(self):
# Provide a default size with the correct aspect ratio
return self._adapt_size(super().sizeHint())
def resizeEvent(self, event):
width, height = event.size().width(), event.size().height()
if (width == int(height * self.aspect_ratio)
or height == int(width / self.aspect_ratio)):
# Already matching aspect ratio
super().resizeEvent(event)
else:
# Decrease width or height to match aspect ratio
size = self._adapt_size(event.size(), prefer_increase=False)
self.resize(size) # no need to call super().resizeEvent(event)
# because a new resize event will be triggered
if __name__ == "__main__":
from GUI.GraphicApp import q_app
w = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(w)
label2 = AspectRatioLabel(2)
layout.addWidget(label2)
# styling of the label
control = QtWidgets.QTextEdit(
'background-color: yellow;\nmin-width: 100px;')
control.setFixedHeight(100)
layout.addWidget(control)
label2.setStyleSheet(control.toPlainText())
ok_button = QtWidgets.QPushButton('OK')
ok_button.clicked.connect(lambda: label2.setStyleSheet(
control.toPlainText()))
layout.addWidget(ok_button)
w.show()
q_app.exec()
Upvotes: 0
Reputation: 742
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:
heightForWidth
and hasHeightForWidth
as suggested by marc-mutz-mmutz simply didn't work for me.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.)
Upvotes: 8
Reputation: 790
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.
Upvotes: 4
Reputation: 25313
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.
Upvotes: 22
Reputation: 1193
The right answer is to create your custom layout manager. That is possible by subclassing QLayout.
Upvotes: 7
Reputation: 75815
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()
.
Upvotes: 7