How to avoid over-packing non-srolling Qt layouts? - qt

A Qt packing layout, such as QVBoxLayout, can pack widgets inside it, such as buttons. In this case, they will be packed vertically as shown in image below:
When we pack too many widgets inside such a layout, and since scrolling is not added by default, the buttons will eventually get squeezed onto each other up to a point that they will overlap, as shown below:
My questions are:
How to tell Qt to not show/pack widgets beyond the available viewing space in the non-scrolling layout?
How to handle the case when the window is resized? I.e. Qt should add/remove widgets accordingly. E.g. if there is extra space available, then perhaps Qt should add some extra widgets that it couldn't add previously.
To be specific: "too many packed widgets" is when the widgets start invading spaces of other widgets, including their inter-widget spacings or margins.
Appendix
Images above are generated by this code below as run in a tile in i3, which is a modified version of this.
from PyQt5 import QtCore, QtWidgets
app = QtWidgets.QApplication([])
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(widget)
for i in range(40):
layout.addWidget(QtWidgets.QPushButton(str(i + 1)))
widget.show()
app.exec_()
When too many widgets are packed:
If the window is tiled, you see them overcrowded as in in the image.
If the window is floating, the window will keep growing until it is no longer fully visible in the monitor.
None of these outcomes are acceptable in my case. My goal is to have Qt only pack as much as will be visible, and add/remove/hide/show dynamically as the window gets resized.

Try this code. It does not rely on QVBoxLayout but it basically does the same as this layout. It hides the child widgets which are outside of the area. There are no partially visible widgets.
from PyQt5 import QtWidgets
class Container(QtWidgets.QWidget):
_spacing = 5
def __init__(self, parent=None):
super().__init__(parent)
y = self._spacing
for i in range(40):
button = QtWidgets.QPushButton("Button" + str(i + 1), self)
button.move(self._spacing, y)
y += button.sizeHint().height() + self._spacing
def resizeEvent(self, event):
super().resizeEvent(event)
for child in self.children():
if isinstance(child, QtWidgets.QWidget):
child.resize(self.width() - 2 * self._spacing, child.height())
child.setVisible(child.geometry().bottom() < self.height())
app = QtWidgets.QApplication([])
w = Container()
w.resize(500, 500)
w.show()
app.exec_()
Note that is in fact does not add nor remove widgets dynamically, this would be much more code and it would probably be very depending on your specific use case. Moreover it feels as a premature optimization. Unless you really need it, do not do it.
UPDATE:
I experimented with the code above and proposed some improvements. I especially wanted to make it responsive to changes in child widgets. The problem is that if the child widget changes it size, the parent container must be re-layouted. The code above does not react in any way. To make it responsive, we need to react to LayoutRequest event. Note that in the code below, I have created three types of buttons - one add a line to itself, other increases font size, and yet another decreases font size.
from PyQt5 import QtCore, QtWidgets
def changeFontSize(increment):
font = QtWidgets.QApplication.font()
font.setPointSize(font.pointSize() + increment)
QtWidgets.QApplication.setFont(font)
class Container(QtWidgets.QWidget):
_spacing = 5
_children = [] # maintains the order of creation unlike children()
def __init__(self, parent=None):
super().__init__(parent)
for i in range(100):
child = QtWidgets.QPushButton(self)
child.installEventFilter(self)
# these are just to test various changes in child widget itself to force relayout
r = i % 3
if r == 0:
text = "New line"
onClicked = lambda state, w=child: w.setText(w.text() + "\nclicked")
elif r == 1:
text = "Bigger font"
onClicked = lambda: changeFontSize(1)
elif r == 2:
text = "Smaller font"
onClicked = lambda: changeFontSize(-1)
child.setText(text)
child.clicked.connect(onClicked)
self._children.append(child)
def resizeEvent(self, event):
super().resizeEvent(event)
self._relayout()
def event(self, event):
if event.type() == QtCore.QEvent.LayoutRequest:
self._relayout()
return super().event(event)
def _relayout(self):
y = self._spacing
for child in self._children:
h = child.sizeHint().height()
child.move(self._spacing, y)
child.resize(self.width() - 2 * self._spacing, h)
y += h + self._spacing
child.setVisible(y < self.height())
app = QtWidgets.QApplication([])
w = Container()
w.resize(500, 500)
w.show()
app.exec_()
This code is satisfactory, however it is not perfect. I have observed that when the container is being re-layouted and some of the child widgets will change its visibility state, re-layouting is called again. This is not needed but I have not discovered how to prevent it.
Maybe there is some better way...

Related

QT - QPixmap not rescaling with current parent size

I'm trying to build a topBar to put in other widgets layout but I don't know why the `QPixmap is not rescaling as we change the application window size. Here's the code:
QPixmap is within QLabel within a QHBoxLayout of a QWidget that is the centralWidget of a QMainWindow
QT 5.8 - Python 3.6
I've updated this code and deleted the previous version on March 24, 2017.
0 - Dependencies
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys
1 - Main Window
class MainWindow(QMainWindow):
def __init__(self):
print("I've been in main window")
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle("I'm here, the main window")
2 - Top Bar
class topBar(QWidget):
def __init__(self):
print("I've been in topBar")
super().__init__()
self.initUI()
def initUI(self):
self.setObjectName("topBar")
self.setStyleSheet("""
QWidget {background-color: pink;}
QLabel {background-color: green; }""")
def resizeEvent(self,event):
resizeHandler(self,event) # You'll see this little dude right next
3 - The resizeEvent Handler, that's were I believe the issue is
def resizeHandler(self,event):
print(" I've been in resizeHandler")
if self.objectName() == "topBar":
print("I've been resizing the topBar")
logo = QPixmap('some_Image')
# Debug
# You'll be able to see that it does re-scale, but it's not updating the Pixmap.
logo.scaled(event.size(),Qt.KeepAspectRatio).save("pixmap.png")
# ...
logoContainer = QLabel()
logoContainer.setPixmap(logo.scaled(event.size(),Qt.KeepAspectRatio,Qt.FastTransformation))
logoContainer.setMaximumSize(logo.width(),logo.height())
containerLayout = QHBoxLayout()
containerLayout.addWidget(logoContainer,0)
container = QWidget()
container.setLayout(containerLayout)
# Layout
layout = QHBoxLayout()
layout.addWidget(container,0)
self.setLayout(layout)
main.setCentralWidget(self)
4 - Testing
if __name__ == '__main__':
print("I've been in __main__")
app = 0
app = QApplication(sys.argv)
app.aboutToQuit.connect(app.deleteLater)
app.setWindowIcon(QIcon('someIcon'))
main = MainWindow()
main.layout().setSizeConstraint(QLayout.SetNoConstraint)
bar = topBar()
main.setCentralWidget(bar)
main.show()
app.exec_()
If it's possible I'd also like to limit topBar itself to not exceed 20% of the current screen size vertically (setMaximumHeight? But based on what?) but I'm not sure how.
Thanks!
To get a widget to fill out the container, you would want to set the vertical and horizontal size policy to either minimum, expanding, minimum expanding, or ignored. http://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum
As far as the second question, it's not a built-in feature of Qt widgets. You might get better luck with QML or Web Engine. You could create a sub-class of QWidget that uses setGeometry() and some window calculation to constrain its size.
I think you're looking at the wrong thing here when trying to diagnose the problem.
Your last comment states...
the real problem is that QPixmap is not updating its size
That's because the QLabel that's displaying it isn't being resized. Going back to your original code I think all you need to do is insert a layout between container and the QLabel logo...
class topBar(QWidget):
def __init__(self,parent):
super().__init__()
container = QWidget()
container.setMaximumSize(587,208)
container.setMinimumSize(0,0)
## Logo
logo = QLabel(container)
#logo = QLabel(container_layout)
logo.setPixmap(QPixmap('.some_image_in_the_current_working_dir.png'))
logo.setScaledContents(1)
# G.M.
container_layout = QHBoxLayout(container)
container_layout.addWidget(logo)
# Layout
## to center content horizontally in wrapper w/o preventing rescale
layout = QHBoxLayout(self)
layout.addWidget(container)
self.setStyleSheet("""
QWidget {background-color: red;}
QLabel {background-color: green; Qt::KeepAspectRatio;}""")
if __name__ == '__main__':
app = 0
app = QApplication(sys.argv)
app.aboutToQuit.connect(app.deleteLater)
test = topBar(None)
test.show()
app.exec_()
(Look for the G.M. comment)
The code above simply creates a layout container_layout for container and makes logo a child of it. That appears to solve the problem I think you're describing.
After a lot of debugging and reading here and there I came up with the following solution (using numpy to help with the rescaling):
def resizeHandler(self,event):
if self.objectName() == "topBar":
# Wiping the old layout
temp = QWidget()
temp.setLayout(self.layout())
# Pixmap
logoPixmap = QPixmap('./img/exampleImage.png')
# Label
logoLabel = QLabel()
logoLabel.setPixmap(logoPixmap)
logoLabel.setScaledContents(True)
## Label Container Layout
containerLayout = QHBoxLayout()
containerLayout.addWidget(logoLabel,0)
# Label Container
logoContainer = QWidget()
logoContainer.setLayout(containerLayout)
# Finding the width and height of the scaled box
# Image unit vectors
imageSize = np.array((logoPixmap.width(),logoPixmap.height()))
screenSize = np.array((event.size().width(),event.size().height()))
# Proportion of each dimension in relation to the smallest side
# Note that one will always be the unit vector and the other greater than a unit
screenUVec = screenSize / screenSize.min()
imageUVec = imageSize / imageSize.min()
# minBorder 11 is the distance between the Application vertical border and the central widget
# 22 is the minimum height where the CentralWidget begins to appear
# Which should vary according to the height of menubar and statsbar of the QMainWindow
minBorder = np.array([11,22]) *2
screenSize -= minBorder
for axis,size in enumerate(screenSize):
if size < 0:
screenSize[axis] = 0
maxSize = np.zeros(2)
# Ideal ratio based on the maxSide
ratio = int(round(screenSize[imageUVec.argmax()] / imageUVec.max() - 0.49999999))
if ratio >=1 and 0 not in screenSize: # Image is scalable on the current screen
maxSize[imageUVec.argmin()] = min(screenSize[imageUVec.argmin()],ratio) # We should add imageSize[imageUVec.argmin()] if we want to limit the maxSize to the maximum size of the image
maxSize[imageUVec.argmax()] = maxSize[imageUVec.argmin()] * imageUVec.max()
sizeUVec = maxSize / maxSize.min()
# Layout
layout = QHBoxLayout()
layout.addWidget(logoContainer,0)
logoLabel.setMaximumSize(QSize(maxSize[0],maxSize[1]))
self.setLayout(layout)
A special thanks to #alexisdm, he showed me HERE that we should first wipe the old layout. When I started watching the globals I saw that several layouts were stacked.
As for the rescaling part, I still went through an unconventional path, but it's behaving the way I want.

Autodesk Maya model panel resize event

I'm writing a simple tool menu for Maya, and I'd like to stick it to the border of model panel (perspective).
from PySide import QtCore, QtGui
from maya import OpenMayaUI as omui
from shiboken import wrapInstance
class TestWidget(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent = self.getMayaWindow())
self.setWindowFlags(QtCore.Qt.Tool | QtCore.Qt.FramelessWindowHint)
self.setFixedSize(100, 100)
panelPtr = omui.MQtUtil.findControl('modelPanel4')
panel = wrapInstance(long(panelPtr), QtGui.QWidget)
position = panel.mapToGlobal(panel.pos())
self.move(position.x(), position.y() + panel.geometry().height() / 2 - self.geometry().height() / 2)
mainLayout = QtGui.QVBoxLayout(self)
button = QtGui.QPushButton('CLOSE')
button.setFixedSize(80, 80)
button.clicked.connect(self.deleteLater)
mainLayout.addWidget(button)
def getMayaWindow(self):
omui.MQtUtil.mainWindow()
ptr = omui.MQtUtil.mainWindow()
return wrapInstance(long(ptr), QtGui.QWidget)
w = TestWidget()
w.show()
The main widget is positioned exactly where I want when it is created (horizontally on the left side of model panel, vertically - in the middle of model panel).
I need to reposition it accordingly when the model panel is resized, but model panel does not emit resized() signal. I'd appreciate any advise.
I've been trying many things to get this working yesterday. I did some additionnal researches today and came to this topic: cgsociety: Creating a floating button inside the viewport
In case of broken link, this is one of the answer:
You can use geometry but there are some issues with triggering
commands based on selection and the undo queue. If you want to go that
route, I would suggest looking into zooHud and zooTriggers (Part of
the zooToolbox)
If you are wanting actual GUI control parented to the viewport, mel
only offers hudslider, hudbutton, and headsUpMessage.
You can also use PyQt and parent in your own custom widgets/layouts or
whatever you want using something like this:
import maya.OpenMayaUI as apiUI import sip
from PyQt4 import QtGui
view = apiUI.M3dView()
apiUI.M3dView.getM3dViewFromModelPanel('modelPanel4', view)
viewWidget = sip.wrapinstance(long(view.widget()), QtCore.QObject)
global myBtn
myBtn = QtGui.QPushButton(viewWidget)
myBtn.setText('testing!')
myBtn.move(100, 100) #Relative to top-left corner of viewport myBtn.show()
You can do anything a full qt widget can do with that, so it's
extremely flexible. but it would require having PyQt installed, which
can be a barrier depending on your tools distribution.
I did a mix of this answer and your code:
from PySide import QtCore, QtGui
from maya import OpenMayaUI as omui
from shiboken import wrapInstance
class CustomQWidget(QtGui.QWidget):
def __init__(self, *args, **kwargs):
QtGui.QWidget.__init__(self, *args, **kwargs)
mainLayout = QtGui.QVBoxLayout(self)
closeButton = QtGui.QPushButton('CLOSE')
closeButton.setFixedSize(80, 40)
closeButton.clicked.connect(self.deleteLater)
helloButton = QtGui.QPushButton('HELLO')
helloButton.setFixedSize(80, 40)
helloButton.clicked.connect(self.printHello)
#Trying to fix glitchy background / Doesn't work, why?
#Is it because layouts don't have background?
p = self.palette()
p.setColor(self.backgroundRole(), QtCore.Qt.red)
self.setPalette(p)
self.setAttribute(QtCore.Qt.WA_StyledBackground, True)
##############################
mainLayout.addWidget(closeButton)
mainLayout.addWidget(helloButton)
def printHello(self):
print "Hello"
view = omui.M3dView()
omui.M3dView.getM3dViewFromModelPanel('modelPanel4', view) #Given the name of a model panel,
#get the M3dView used by that panel. If this fails, then a panel with the given name could not be located.
viewWidget = wrapInstance(long(view.widget()), QtGui.QWidget)
position = viewWidget.mapToGlobal(viewWidget.pos())
w = CustomQWidget(viewWidget)
w.move(0, viewWidget.geometry().height() / 2 - 100 / 2) #Relative to middle-left corner of viewport
w.show()
One of the issue I have it that the background of the widget is glitched:
If anyone knows why and how to fix it, I'll edit my answer with pleasure.
Else, when running this script from Maya's script editor, the widget follows the panel when it is resized.
I did fix such a problem, but not using Python/PyQt.
The problem itself is, that your Qt Widget is there. I have not found a way to make it not paint its background.
My solution was different: I derived from a Qt Layout, pushed all my widgets into that layout and used MQtUtil to get the QWidget of that modelPanel's modelEditor to attach the "real Qt layout" to it.
Heavy caveat that may make Python not suited: Maya doesn't expect "non-Maya" Layouts to be bound to "real-Maya" Widgets like modelEditors. So you need to listen to QEvents and find out when to destroy your layout, so Maya doesn't crash trying.
set autofillbackground True to fix your background painting issue

QLabel doesn't redraw correctly when in a QHBoxLayout with a stretch

I'm having an odd problem with a label not being redrawn correctly when the text is changed, when it's inside a QHBoxLayout with an added stretch.
Consider the following (PyQt) example code:
from PyQt5.QtWidgets import QApplication, QHBoxLayout, QWidget, QLabel
from PyQt5.QtCore import QTimer
def clearlabel():
print("clearing")
lbl.setText("")
lbl2.setText("")
app = QApplication([])
# Widget 1: with stretch
w = QWidget()
w.move(0, 0)
w.resize(100, 20)
w.show()
lbl = QLabel()
lbl.setText("foo")
h = QHBoxLayout(w)
h.addStretch()
h.addWidget(lbl)
# Widget 2: without stretch
w2 = QWidget()
w2.move(0, 40)
w2.resize(100, 20)
w2.show()
lbl2 = QLabel()
lbl2.setText("foo")
h2 = QHBoxLayout(w2)
h2.addWidget(lbl2)
QTimer.singleShot(1000, clearlabel)
app.exec_()
Two widgets are shown, one with a QHBoxLayout with a stretch added, one without:
After 2 seconds, a timer sets both label texts from "foo" to an empty string. In the widget without stretch it works like expected - with the one with, however, the label text doesn't get redrawn:
What's going on there? Is this a Qt bug? Am I missing something?
What I found out so far:
This only seems to happen when setting an empty string, setting a shorter string works fine.
However in my real application, it happens without a stretch added as well.
I have now submitted this as QTBUG-36945.
Thanks to the helpful people in the #qt IRC channel (on Freenode), I figured out doing a repaint after setting the text works around the issue:
class Label(QLabel):
...
def setText(self, text):
super().setText(text)
if not text:
self.repaint()
Still I'd be glad to know if I'm just wrong somewhere, or if I should submit a Qt bug.

Centralize QTableView in window with vertical spacers

I am trying to create a main window (fixed size) that contains a QTableView, with QSpacerItems above and below, in order to centralise the table (vertically).
(Sorry, can't post an image, apparently).
I have a QVBoxLayout, into which I have a vertical spacer, the QTableView, and another vertical spacer. I've played with all combinations of QSizePolicy for all three widgets, but I cannot get the table to be displayed without scrollbars. (I cannot use Qt.ScrollBarAlwaysOff because they will be needed if the number of items exceeds the main window's size). So the vertical scrollbars on the QTableView are displayed, even though the vertical spacers are absorbing plenty of space between the view and the main window.
I want the vertical spacers to take up the minimum space required above and below the table widget in order to centralise the rows, and the table widget to display as many rows as possible, without scrollbars.
You can subclass QTableView, use QSizePolicy::Fixed in the vertical direction and override sizeHint() to return your preferred vertical height.
Here's a working example (You didn't specify language, so I am going to assume it is Python :-) :
import sys
from PySide import QtCore, QtGui
class MyTableView(QtGui.QTableView):
def __init__(self, parent=None):
super().__init__(parent)
#assume expanding in horizontal direction and fixed in vertica direction
self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed)
def sizeHint(self):
return QtCore.QSize(400, 500) #I allow you to edit that!
class MyApplication(QtGui.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
layout = QtGui.QVBoxLayout()
table_view = MyTableView()
layout.addWidget(table_view)
self.model = QtGui.QStringListModel() #use a string list model for simplicity
table_view.setModel(self.model)
self.strings = ['1', '2', '3']
self.model.setStringList(self.strings) #initialize the model
self.counter = 4
button = QtGui.QPushButton('Add Cell') #this button updates the model and adds cells
button.clicked.connect(self.addCell)
layout.addWidget(button)
self.setLayout(layout)
def addCell(self):
self.strings.append(str(self.counter))
self.counter += 1
self.model.setStringList(self.strings)
app = QtGui.QApplication(sys.argv)
main = MyApplication()
main.show()
sys.exit(app.exec_())

QVBoxLayout: How to vertically align widgets to the top instead of the center

In Qt, When I add widgets to my layout, they are vertically centered by default. Is there a way to "List" the widgets from top to bottom instead of centering them vertically?
If you have a QVBoxLayout and want your fixed size widgets to be stacked at the top, you can simply append a vertical stretch at the end:
layout.addStretch()
If you have multiple stretchers or other stretch items, you can specify an integer stretch factor argument that defines their size ratio.
See also addStretch and addSpacerItem.
Add two layout.addStretch() before and after adding the widgets to center them vertically:
layout.addStretch()
layout.addWidget(self.message)
layout.addWidget(self.userid_field)
layout.addWidget(self.password_field)
layout.addWidget(self.loginButton)
layout.addStretch()
Not sure whether this answers your original question, but it is the answer to the one that I had when googling and being led to this page - so it might be useful for others too.
use void QLayout::setAlignment ( Qt::Alignment alignment ) method to set alignment according to your choice.
I find this a little more complicated than just using layout.setAlignment(). It kept not working for me until just now, when I figured out that if you have expanding widgets that you set a maximum height for, then that widget will not be aligned the way you want.
Here is example code that does not top align the QTextBrowser() widget even though I call layout.setAlignment(Qt.AlignTop). Sorry that it is in Python, but it is pretty easy to translate to C++ (I have gone the other way many times).
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class MyWidget(QWidget):
"""
Create a widget that aligns its contents to the top.
"""
def __init__(self, parent=None):
QWidget.__init__(self, parent)
layout = QVBoxLayout()
label = QLabel('label:')
layout.addWidget(label)
info = QTextBrowser(self)
info.setMinimumHeight(100)
info.setMaximumHeight(200)
layout.addWidget(info)
# Uncomment the next line to get this to align top.
# layout.setAlignment(info, Qt.AlignTop)
# Create a progress bar layout.
button = QPushButton('Button 1')
layout.addWidget(button)
# This will align all the widgets to the top except
# for the QTextBrowser() since it has a maximum size set.
layout.setAlignment(Qt.AlignTop)
self.setLayout(layout)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
widget = MyWidget()
widget.show()
widget.resize(QSize(900, 400))
app.exec_()
The following explicitly calls layout.setAlignment(info, Qt.AlignTop) to get the expanding text widget to work.
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class MyWidget(QWidget):
"""
Create a widget that aligns its contents to the top.
"""
def __init__(self, parent=None):
QWidget.__init__(self, parent)
layout = QVBoxLayout()
label = QLabel('label:')
layout.addWidget(label)
info = QTextBrowser(self)
info.setMinimumHeight(100)
info.setMaximumHeight(200)
layout.addWidget(info)
# Uncomment the next line to get this to align top.
layout.setAlignment(info, Qt.AlignTop)
# Create a progress bar layout.
button = QPushButton('Button 1')
layout.addWidget(button)
# This will align all the widgets to the top except
# for the QTextBrowser() since it has a maximum size set.
layout.setAlignment(Qt.AlignTop)
self.setLayout(layout)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
widget = MyWidget()
widget.show()
widget.resize(QSize(900, 400))
app.exec_()
After comparison between the two solutions, it seems that :
myLayout.setAlignment(Qt::AlignTop)
works for several widget alignement but :
myLayout.setAlignment(myWidget, Qt::AlignTop)
works only for the first widget you add to the layout.
After all, the solution depends also to the QSizePolicy of yours widgets.
If you are using QT creator, you just add a "Vertical Spacers" at the bottom of your widget.
In pyQt (and PySide) we have Qt.AlignCenter (align on main direction), Qt.AlignHCenter (align on horizontal direction), Qt.AlignVCenter (align on vertical direction), use one of it when you need it.

Resources