Resizing QSplitter doesn't respect QSizePolicy or setStretchFactor? - python

I set the size policy of items in a QSplitter but it doesn't seem to affect anything. When I resize the window in the example below I want my left widget to stop growing once it reaches its size hint (QSizePolicy.Preferred) and the right widget to expand as big as possible (QSizePolicy.Expanding).
It works if I put them in a layout:
from PyQt5 import QtWidgets, QtCore
app = QtWidgets.QApplication([])
class ColoredBox(QtWidgets.QFrame):
def __init__(self, color):
super().__init__()
self.setStyleSheet(f'background-color: {color}')
def sizeHint(self) -> QtCore.QSize:
return QtCore.QSize(200, 200)
box1 = ColoredBox('red')
box2 = ColoredBox('green')
splitter = QtWidgets.QHBoxLayout()
splitter.setContentsMargins(0, 0, 0, 0)
splitter.setSpacing(0)
splitter.addWidget(box1)
splitter.addWidget(box2)
box1.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
box2.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
container = QtWidgets.QWidget()
container.setLayout(splitter)
container.show()
app.exec_()
But it doesn't work if I put them in a QSplitter, the splitter just splits space between them evenly:
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtCore import Qt
app = QtWidgets.QApplication([])
class ColoredBox(QtWidgets.QFrame):
def __init__(self, color):
super().__init__()
self.setStyleSheet(f'background-color: {color}')
def sizeHint(self) -> QtCore.QSize:
return QtCore.QSize(200, 200)
box1 = ColoredBox('red')
box2 = ColoredBox('green')
splitter = QtWidgets.QSplitter(Qt.Horizontal)
splitter.setStretchFactor(0, 0)
splitter.setStretchFactor(1, 1)
splitter.addWidget(box1)
splitter.addWidget(box2)
box1.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
box2.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
container = QtWidgets.QMainWindow()
container.setCentralWidget(splitter)
container.show()
app.exec_()
Layout vs Splitter:

The problem comes from the fact that you're setting the stretch factor too early.
setStretchFactor() is ignored if done on a widget index that doesn't exist yet (see the source code for its implementation);
QSplitter resizes the widget proportions based on their size policy; the size policy also includes the horizontal and vertical stretch factors, and they're reset to the 0 default value whenever set again with the basic setSizePolicy(horizontalPolicy, verticalPolicy) (which means that you can specify the stretch in the QSizePolicy, which is the same as using setStretchFactor).
The solution is simple: move the setStretchFactor lines after adding the widgets and setting their policies.
Obviously, a possible alternative is to set the maximum dimension (based on the QSplitter orientation), but that is not exactly the same thing.
If all widgets in the splitter have a maximum size (including situations for which only one widget exists), the result is that the splitter will have a maximum size based on those widgets, depending on other widgets in (or "above") its layout, but if the splitter is the top level window then there will be some blank space remaining whenever it's resized to a size bigger than the maximum of its child[ren].

Related

Problem with GridLayout widget stretching/sizing

I'm trying to make a simple image editor GUI using the GridLayout. However, I am coming across a problem where the ratios between the image and the side panels are not what I want them to be. Currently, my code is:
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class Window(QMainWindow):
def __init__(self, parent = None):
super().__init__(parent)
self.setWindowTitle("PyEditor")
self.setGeometry(100, 100, 500, 300)
self.centralWidget = QLabel()
self.setCentralWidget(self.centralWidget)
self.gridLayout = QGridLayout(self.centralWidget)
self.createImagePanel()
self.createDrawPanel()
self.createLayerPanel()
def createImagePanel(self):
imageLabel = QLabel(self)
pixmap = QPixmap('amongus.png')
imageLabel.setPixmap(pixmap)
self.gridLayout.addWidget(imageLabel, 0, 0, 3, 4)
def createDrawPanel(self):
drawPanel = QLabel(self)
drawLayout = QVBoxLayout()
drawPanel.setLayout(drawLayout)
tabs = QTabWidget()
filterTab = QWidget()
drawTab = QWidget()
tabs.addTab(filterTab, "Filter")
tabs.addTab(drawTab, "Draw")
drawLayout.addWidget(tabs)
self.gridLayout.addWidget(drawPanel, 0, 4, 1, 1)
def createLayerPanel(self):
layerPanel = QLabel(self)
layerLayout = QVBoxLayout()
layerPanel.setLayout(layerLayout)
tab = QTabWidget()
layerTab = QWidget()
tab.addTab(layerTab, "Layers")
layerLayout.addWidget(tab)
self.gridLayout.addWidget(layerPanel, 1, 4, 1, 1)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
This gives me the following window:
When I resize the window, only the filter/draw and layer panels are stretching, and not the image panel. I want to image panel to stretch as well and take up the majority of the window instead.
While theoretically every Qt widget could be used as a container, some widgets should not be used for such a purpose, as their size hints, size policies and resizing have different and specific behavior depending on their nature.
QLabel is intended as a display widget, not as a container. Everything related to its size is based on the content (text, image or animation), so the possible layout set for it will have no result in size related matters and will also create some inconsistencies in displaying the widgets added to that layout.
If a basic container is required, then basic QWidget is the most logical choice.
Then, if stretching is also a requirement, that should be applied using the widget or layout stretch factors. For QGridLayout, this is achieved by using setColumnStretch() or setRowStretch().
Trying to use the row or column span is not correct for this purpose, as the spanning only indicates how many grid "cells" a certain layout item will use, which only makes sense whenever there are widgets that should occupy more than one "cell", exactly like the spanning of a table.
So, the following changes are required to achieve the wanted behavior:
change all QLabel to QWidget (except for the label that shows the image, obviously);
use the proper row/column spans; the imageLabel should be added with only one column span (unless otherwise required):
self.gridLayout.addWidget(imageLabel, 0, 0, 3, 1, alignment=Qt.AlignCenter)
set a column stretch of (at least) 1 for the first column:
self.gridLayout.setColumnStretch(0, 1)
if you want the image to be center aligned in the available space, set the alignment on the widget (not when adding it to the layout):
imageLabel = QLabel(self, alignment=Qt.AlignCenter)
Note that all the above will not scale the image whenever the available size is greater than that of the image. While you can set the scaledContents to True, the result will be that the image will be stretched to fill the whole available space, and unfortunately QLabel doesn't provide the ability to keep the aspect ratio. If you need that, then it's usually easier to subclass QWidget and provide proper implementation for size hint and paint event.
class ImageViewer(QWidget):
_pixmap = None
def __init__(self, pixmap=None, parent=None):
super().__init__(parent)
self.setPixmap(pixmap)
def setPixmap(self, pixmap):
if self._pixmap != pixmap:
self._pixmap = pixmap
self.updateGeometry()
def sizeHint(self):
if self._pixmap and not self._pixmap.isNull():
return self._pixmap.size()
return super().sizeHint()
def paintEvent(self, event):
if self._pixmap and not self._pixmap.isNull():
qp = QPainter(self)
scaled = self._pixmap.scaled(self.width(), self.height(),
Qt.KeepAspectRatio, Qt.SmoothTransformation)
rect = scaled.rect()
rect.moveCenter(self.rect().center())
qp.drawPixmap(rect, scaled)

Using QScrollArea collapses children widgets

I am trying to create a dynamic GUI with multiple Groupbox objects in a QVBoxLayout. As there are a lot of them, I will be needing a scroll area to make them available to the end user.
So I tried to change to top widget of this tab from a QWidget to a QScrollArea.
Before the change:
This is the kind of result I want but with a scroll bar because the window is too high.
After the change to QScrollArea:
My GroupBoxs are now "collapsed" and there is not scrollbar. I tried setting their size but it is not adequate because they are not fixed. I searched the documentation and tried to use WidgetResizable or I tried to set a fixed height or the sizehint but nothing worked as I wanted.
After creating the the Groupbox, the sizeHint for my QScrollArea is already very low (around 150px of height) so I think I'm missing a parameter.
It would be complicated to provide code as it is intricate. If necessary I could recreate the problem in a simpler way.
How to reproduce:
from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtWidgets import *
import sys
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
v_layout = QVBoxLayout()
scroll_area = QScrollArea()
self.layout().addWidget(scroll_area)
scroll_area.setLayout(v_layout)
# v_layout.setSizeConstraint(QLayout.SetMinimumSize)
for i in range(50):
box = QGroupBox()
grid = QGridLayout()
box.setLayout(grid)
grid.addWidget(QLabel("totototo"), 0, 0)
grid.addWidget(QLineEdit(), 1, 0)
grid.addWidget(QPushButton(), 2, 0)
v_layout.addWidget(box)
self.show()
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
Uncommenting # v_layout.setSizeConstraint(QLayout.SetMinimumSize) allows the content of the group boxes to deploy and fixes the first part of the issue. But there is still not scroll bar.
You have 2 errors:
A widget should not be added to the layout of a QMainWindow, but the setCentralWidget method should be used.
You should not add the layout to the QScrollArea but use a widget as a container for the other widgets, also if you use layouts then you have to activate the widgetResizable property.
Considering the above, the solution is:
def initUI(self):
scroll_area = QScrollArea(widgetResizable=True)
self.setCentralWidget(scroll_area)
container = QWidget()
scroll_area.setWidget(container)
v_layout = QVBoxLayout(container)
for i in range(50):
box = QGroupBox()
grid = QGridLayout()
box.setLayout(grid)
grid.addWidget(QLabel("totototo"), 0, 0)
grid.addWidget(QLineEdit(), 1, 0)
grid.addWidget(QPushButton(), 2, 0)
v_layout.addWidget(box)
self.show()

How does stretch factor work in Qt?

I'm working on a GUI application with pyqt5. At a certain dialog I need many components, being one of those a QWebEngineView as a canvas for plotting data, which should take most of the space available even if the chart is not ready when creating the dialog.
I expect it to look something like this:
I investigated and found about the stretch factor. I saw that QSizePolicy is directly applicable only to widgets and not to layouts, as shown in this SO answer. But then I saw that the methods addWidget and addLayout allow me to set the stretch factor in the direction of the QBoxLayout, and that seemed ideal for my intentions.
So I tried with:
from PyQt5.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout
from strategy_table import StrategyTable
layout = QVBoxLayout()
layout.addWidget(QLabel("Strategy components"))
# Upper layout for a table. Using a 30% of vertical space
upper_layout = QHBoxLayout() # I'm using a HBox because I also want a small button at the right side of the table
self.table = StrategyTable(self) # Own class derived from QTableWidget
upper_layout.addWidget(self.table)
add_button = QPushButton('Add')
add_button.clicked.connect(self._show_add_dialog)
upper_layout.addWidget(add_button)
layout.addLayout(upper_layout, 3) # Setting 20% of size with the stretch factor
# Then the plot area, using 60% of vertical space
layout.addWidget(QLabel("Plot area"))
canvas = QWebEngineView()
layout.addWidget(self.canvas, 6)
# Finally, a small are of 10% of vertical size to show numerical results
layout.addWidget(QLabel("Results"))
params_layout = self._create_results_labels() # A column with several QLabel-QTextArea pairs, returned as a QHBoxLayout
layout.addLayout(params_layout, 1)
self.setLayout(layout)
But it looked exactly the same as before:
It looked quite ok before adding the results Layout at the bottom, I guess because the upper table is empty at the beginning, and therefore took very little space and left the rest to the canvas.
Anyway, it seems that the stretch factor is being ignored, so I don't know if I am missing something here, or that I didn't fully understand the stretch factor.
BTW, I know I would use the QtEditor for designing the GUI, but I kind of prefer doing these things manually.
The problem is simple, the layouts handle the position and size of the widgets, but it has limits, among them the minimum size of the widgets, and in your case the last element has a height higher than 10%, so physically it is impossible. We can see that by removing the content or using a QScrollArea:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets
class StrategyTable(QtWidgets.QTableWidget):
pass
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
lay = QtWidgets.QVBoxLayout(self)
table = StrategyTable()
button = QtWidgets.QPushButton("Add")
hlay = QtWidgets.QHBoxLayout()
hlay.addWidget(table)
hlay.addWidget(button)
canvas = QtWebEngineWidgets.QWebEngineView()
canvas.setUrl(QtCore.QUrl("http://www.google.com/"))
scroll = QtWidgets.QScrollArea()
content_widget = QtWidgets.QWidget()
scroll.setWidgetResizable(True)
scroll.setWidget(content_widget)
vlay = QtWidgets.QVBoxLayout()
vlay.addWidget(QtWidgets.QLabel("Results:"))
params_layout = self._create_results_labels()
content_widget.setLayout(params_layout)
vlay.addWidget(scroll)
lay.addLayout(hlay, 3)
lay.addWidget(canvas, 6)
lay.addLayout(vlay, 1)
def _create_results_labels(self):
flay = QtWidgets.QFormLayout()
for text in ("Delta", "Gamma", "Rho", "Theta", "Vega"):
flay.addRow(text, QtWidgets.QTextEdit())
return flay
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
Hope this will be useful to understand Layouts along with stretch factor
Sample Layout based on percentages (CPP)
QVBoxLayout *topMostVerticalLayout = new QVBoxLayout(this);
QHBoxLayout *upperHorznLayout = new QHBoxLayout();
QHBoxLayout *bottomHorznLayout = new QHBoxLayout();
QVBoxLayout *InnerVerticalLayout1 = new QVBoxLayout(this);
QVBoxLayout *InnerVerticalLayout2 = new QVBoxLayout(this);
QVBoxLayout *InnerVerticalLayout3 = new QVBoxLayout(this);
QVBoxLayout *InnerVerticalLayout4 = new QVBoxLayout(this);
QVBoxLayout *InnerVerticalLayout5 = new QVBoxLayout(this);
bottomHorznLayout->addLayout(InnerVerticalLayout1,15); //(15% stretch)
bottomHorznLayout->addLayout(InnerVerticalLayout2,15); //(15% stretch)
bottomHorznLayout->addLayout(InnerVerticalLayout3,15); //(15% stretch)
bottomHorznLayout->addLayout(InnerVerticalLayout4,15); //(15% stretch)
bottomHorznLayout->addLayout(InnerVerticalLayout5,40); //(40% stretch)
topMostVerticalLayout->addLayout(upperHorznLayout,3); //(30% stretch)
topMostVerticalLayout->addLayout(bottomHorznLayout,7); //(70% stretch)
this->setLayout(topMostVerticalLayout);

Set widget width based on its height

I am trying to find a way to have one of my widgets maintain a width based on its height. I have other widgets working fine reimplementing the heightForWidth method. That was easy because that method is standard. I know there is no included widthForHeight method so I have tried many options the internet has suggested but have not gotten anything to work all the way. What I currently have almost gets me there.
I first reimplement my widget's sizeHint to get its width to be a ratio of its parent's (QHBoxLayout) height.
The sizeHint makes MyCustomLabel show with the right size at first show but did not update during times the user resized the window. I don't know if this the best way but to fix that I am reimplementing resizeEvent and calling adjustSize to force the sizeHint recalculation.
With those two reimplemented methods MyCustomLabel shows with the right size. I placed this custom widget in a QHBoxLayout with a few other standard widgets. The problem is the other widgets in the layout don't respect the new size of my MyCustomLabel when the user resizes the window. What I end up with is the other widgets in the layout either overlapping or being placed too far from MyCustomLabel. I kind of get it, I am brute forcing my widget to a size and not letting the layout do the work. However I thought updating the sizeHint would inform the layout of MyCustomLabel's new size and adjust everything accordingly. How do I fix this layout problem or am I going about this widthForHeight problem all the wrong way?
Edit:
I tried #AlexanderVX suggestion of setting the SizePolicy to Minimum and while it does prevent the other widgets from overlapping it also locked MyCustomLabel to a fixed size. I need the widget to expand and shrink with the layout. I also tried Preferred, Expanding, MinimumExpanding policies just to see if they would do anything but with no luck.
from __future__ import division
from PySide import QtCore
from PySide import QtGui
import sys
class MyCustomLabel(QtGui.QLabel):
clicked = QtCore.Signal(int)
dblClicked = QtCore.Signal(int)
def __init__(self, leadSide='height', parent=None):
super(MyCustomLabel, self).__init__()
self.leadSide = leadSide
# sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred,
# QtGui.QSizePolicy.Preferred)
# sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,
# QtGui.QSizePolicy.Expanding)
# sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding,
# QtGui.QSizePolicy.MinimumExpanding)
# sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum,
# QtGui.QSizePolicy.Minimum)
# self.setSizePolicy(sizePolicy)
def resizeEvent(self, event):
super(MyCustomLabel, self).resizeEvent(event)
self.adjustSize()
def sizeHint(self):
super(MyCustomLabel, self).sizeHint()
parentSize = self.parent().size().toTuple()
if self.leadSide.lower() == 'height':
new_size = QtCore.QSize(parentSize[1] * (16 / 9), parentSize[1]) * .9
if self.leadSide.lower() == 'width':
new_size = QtCore.QSize(parentSize[0], parentSize[0] / (16 / 9)) * .9
return new_size
class __Test__(QtGui.QWidget):
def __init__(self):
super(__Test__, self).__init__()
self.initUI()
def initUI(self):
customLabel = MyCustomLabel(leadSide='height')
# customLabel.setScaledContents(True)
customLabel.setStyleSheet('background-color: blue;'
'border:2px solid red;')
btn01 = QtGui.QPushButton('button')
btn01.setFixedHeight(80)
textEdit = QtGui.QTextEdit()
textEdit.setFixedSize(150, 150)
layout01 = QtGui.QHBoxLayout()
layout01.setContentsMargins(0,0,0,0)
layout01.setSpacing(0)
layout01.addWidget(customLabel)
layout01.addWidget(btn01)
layout01.addWidget(textEdit)
self.setLayout(layout01)
self.setGeometry(300, 300, 600, 300)
self.setWindowTitle('Testing')
self.show()
def main():
app = QtGui.QApplication(sys.argv)
ex = __Test__()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

How to remove spacing inside a gridLayout (QT)?

I want to create a child container layout which will contains 2 widgets. Those 2 widgets should be placed right next to each other but my current setup still has some spacing in between.
I have already set the spacing to 0 setSpacing(0). And setContentsMargins(0,0,0,0) doesn't helped.
I am using PyQt5 but it shouldn't be a problem converting c++ code.
As you can see in the picture there is still a small gap:
(Left: LineEdit - Right: PushButton)
import PyQt5.QtCore as qc
import PyQt5.QtGui as qg
import PyQt5.QtWidgets as qw
import sys
class Window(qw.QWidget):
def __init__(self):
qw.QWidget.__init__(self)
self.initUI()
def initUI(self):
gridLayout = qw.QGridLayout()
height = 20
self.label1 = qw.QLabel("Input:")
self.label1.setFixedHeight(height)
gridLayout.addWidget(self.label1, 0, 0)
# Child Container
childGridLayout = qw.QGridLayout()
childGridLayout.setContentsMargins(0,0,0,0)
childGridLayout.setHorizontalSpacing(0)
self.lineEdit1 = qw.QLineEdit()
self.lineEdit1.setFixedSize(25, height)
childGridLayout.addWidget(self.lineEdit1, 0, 0)
self.pushButton1 = qw.QPushButton("T")
self.pushButton1.setFixedSize(20, height)
childGridLayout.addWidget(self.pushButton1, 0, 1)
# -----------------
gridLayout.addItem(childGridLayout, 0,1)
self.setLayout(gridLayout)
if __name__ == '__main__':
app = qw.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
The QT documentation says:
By default, QLayout uses the values provided by the style. On most platforms, the margin is 11 pixels in all directions.
Ref:http://doc.qt.io/qt-4.8/qlayout.html#setContentsMargins
So you may need to use "setHorizontalSpacing(int spacing)" for horizontal space and "setVerticalSpacing(int spacing)" for vertical.
Based on the documentation, this may delete space in your case.
Ref:http://doc.qt.io/qt-4.8/qgridlayout.html#horizontalSpacing-prop
If not resolved, there is an option to override style settings for space (from where the layout gets).... I think this is tedious
If you want to provide custom layout spacings in a QStyle subclass, implement a slot called layoutSpacingImplementation() in your subclass.
More detials:
http://doc.qt.io/qt-4.8/qstyle.html#layoutSpacingImplementation

Categories