Overlapping widgets in QLayout - python

I want to create a window where is Qt3DWindow in the back and some QPushButtons above it. However only Qt3DWindow animation is shown and QPushButtons are not seen. I would also like to have Qt3DWindow functional and QPushButtons as well (So I can click on the buttons or 3D animation behind). Buttons are only seen when I set Qt3DWindow transparency to lower value. Of course in that case buttons are only seen but not functional.
class MainWindow(QMainWindow):
def __init__(self, *args):
QMainWindow.__init__(self, *args)
self.window = Window() # Qt3DExtras.Qt3DWindow
self.container = self.createWindowContainer(self.window)
self.buttons = Buttons()
self.layout().addWidget(self.buttons.view) # QtWidgets.QGraphicsView
self.layout().addWidget(self.container)

QWidget::createWindowContainer() will handle the geometry of the window however it does change the fact that the managed window still overlays the window that contains the widget. So any child of that widget will not be visible as it will be obscured by the Qt3DWindow.
Only viable alternative is move the widgets you want to overlay into their own window and handles it's geometry yourself.
Or use a Scene3D inside a QDeclarativeWidget but that will affect performance.

As per the comment, QMainWindow uses its own layout type which is responsible for a large part (most) of its functionality -- dock widgets, toolbars etc.
Rather than just add widgets to that layout you need to create your own widget hierarchy and pass that to QMainWindow::setCentralWidget.
If you want the Buttons to lie in front of the container you can probably use a QGridLayout.
So, you could try something like (untested)...
class MainWindow(QMainWindow):
def __init__(self, *args):
QMainWindow.__init__(self, *args)
self.window = Window() # Qt3DExtras.Qt3DWindow
self.container = self.createWindowContainer(self.window)
self.buttons = Buttons()
central_widget = QWidget()
central_widget_layout = QGridLayout()
central_widget_layout.addWidget(self.container, 0, 0, 2, 1)
central_widget_layout.addWidget(self.buttons.view, 0, 0, 1, 1)
central_widget.setLayout(central_widget_layout)
setCentralWidget(central_widget)

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 to align widgets in PyQt5

I am trying to use PyQt5 for one of my GUI application. I could be able to add widgets as I want, but couldn't align them properly. I want to align my widgets as below:
But, My code is working something like this,
Following is my code, can anyone help me please?
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import QRect
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QDesktopWidget, QLabel
class GroupBox(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.setGeometry(QtCore.QRect(20, 20, 900, 700))
self.setWindowTitle("InvoiceMee - Split Documents")
layout = QtWidgets.QGridLayout(self)
groupbox = QtWidgets.QGroupBox("Files to Convert", checkable=False)
layout.addWidget(groupbox)
hbox = QtWidgets.QHBoxLayout()
groupbox.setLayout(hbox)
label = QLabel()
pixmap = QPixmap('images.jpg')
label.setPixmap(pixmap)
label.resize(pixmap.width(), pixmap.height())
pathBox = QtWidgets.QLineEdit(self)
pathBox.setPlaceholderText("Enter the Path Here")
pathBox.setGeometry(QRect(160, 150, 201, 20))
selectFileBtn = QtWidgets.QPushButton("Select")
convertButton = QtWidgets.QPushButton("Convert")
good_radiobutton = QtWidgets.QRadioButton("Invoices")
naive_radiobutton = QtWidgets.QRadioButton("Credit Notes")
hbox.addWidget(pathBox, alignment=QtCore.Qt.AlignCenter)
hbox.addWidget(selectFileBtn, alignment=QtCore.Qt.AlignCenter)
hbox.addWidget(convertButton, alignment=QtCore.Qt.AlignCenter)
hbox.addWidget(good_radiobutton, alignment=QtCore.Qt.AlignCenter)
hbox.addWidget(naive_radiobutton, alignment=QtCore.Qt.AlignCenter)
hbox.addWidget(label,alignment=QtCore.Qt.AlignCenter)
hbox.addStretch()
self.center()
def center(self):
# geometry of the main window
qr = self.frameGeometry()
# center point of screen
cp = QDesktopWidget().availableGeometry().center()
# move rectangle's center point to screen's center point
qr.moveCenter(cp)
# top left of rectangle becomes top left of window centering it
self.move(qr.topLeft())
Use QGridLayout instead of QHBoxLayout. Grid Layout gives you the option to layout your widgets in a grid like struture. Here's the official documentation for QGridLayout.
You can change your layout like this:
grid = QtWidgets.QGridLayout()
groupbox.setLayout(grid)
grid.addWidget(label,0,0,1,0,QtCore.Qt.AlignCenter)
grid.addWidget(pathBox,1,0,QtCore.Qt.AlignRight)
grid.addWidget(selectFileBtn,1,1,QtCore.Qt.AlignLeft)
grid.addWidget(good_radiobutton,2,0,QtCore.Qt.AlignRight)
grid.addWidget(naive_radiobutton,2,1,QtCore.Qt.AlignLeft)
grid.addWidget(convertButton,3,0,1,0,QtCore.Qt.AlignCenter)
Add a vertical spacer item if you want to remove space between your widgets:
verticalSpacer = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
grid.addItem(verticalSpacer, 6, 0, QtCore.Qt.AlignTop)
You are using a QHBoxLayout (which stands for horizontal box layout). This means that all widgets that you added will be always displayed side by side, horizontally, according to the order of insertion.
You should use a layout that allows vertical orientation, instead.
You're using more than a widget per row, so you could use a QGridLayout, but, since some of those widgets have different horizontal sizes, the result might not be what you showed us.
The solution is to use nested layouts, with a main grid layout with stretch sets for first/third row and column and a "central" layout added to the second row/column of the grid. Then, whenever you need more than one widget in a row, add a nested QHBoxLayout.
class GroupBox(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.setGeometry(QtCore.QRect(20, 20, 900, 700))
self.setWindowTitle("InvoiceMee - Split Documents")
layout = QtWidgets.QGridLayout(self)
groupbox = QtWidgets.QGroupBox("Files to Convert", checkable=False)
layout.addWidget(groupbox)
# the "main" layout, used to ensure that the actual layout containing
# all widgets stays in the center
groupLayout = QtWidgets.QGridLayout()
groupbox.setLayout(groupLayout)
groupLayout.setColumnStretch(0, 1)
groupLayout.setColumnStretch(2, 1)
groupLayout.setRowStretch(0, 1)
groupLayout.setRowStretch(2, 1)
# this is the actual layout used to add widgets
centerLayout = QtWidgets.QVBoxLayout()
groupLayout.addLayout(centerLayout, 1, 1)
label = QLabel()
pixmap = QPixmap('images.jpg')
label.setPixmap(pixmap)
# this won't work
# label.resize(pixmap.width(), pixmap.height())
pathBox = QtWidgets.QLineEdit(self)
pathBox.setPlaceholderText("Enter the Path Here")
# this won't work either, the layout will try to move and resize it anyway
# pathBox.setGeometry(QRect(160, 150, 201, 20))
# use minimum width instead
pathBox.setMinimumWidth(200)
selectFileBtn = QtWidgets.QPushButton("Select")
convertButton = QtWidgets.QPushButton("Convert")
good_radiobutton = QtWidgets.QRadioButton("Invoices")
naive_radiobutton = QtWidgets.QRadioButton("Credit Notes")
centerLayout.addWidget(label, alignment=QtCore.Qt.AlignCenter)
# the second row has more than one widget, use a nested horizontal layout
inputLayout = QtWidgets.QHBoxLayout()
centerLayout.addLayout(inputLayout)
inputLayout.addWidget(pathBox)
inputLayout.addWidget(selectFileBtn)
# the same for the radio buttons
radioLayout = QtWidgets.QHBoxLayout()
centerLayout.addLayout(radioLayout)
# use horizontal alignment to keep buttons closer, otherwise the layout
# will try to expand them as much as possible (depending on the other
# widgets in the centerLayout)
radioLayout.addWidget(good_radiobutton, alignment=QtCore.Qt.AlignRight)
radioLayout.addWidget(naive_radiobutton, alignment=QtCore.Qt.AlignLeft)
# use center alignment so that the button doesn't expand
centerLayout.addWidget(convertButton, alignment=QtCore.Qt.AlignCenter)
I'd suggest you to carefully study how layout work and behave, make some experiments and also use Qt Designer to easily see how nested layout can work.
Also, consider that in some cases it might be necessary to set a specific size policy to avoid widgets expanding too much, and use a QWidget "container" can make things easier.
For example, instead of using the horizontal alignment when you add the radio buttons, you can use a QWidget container:
# ...
radioContainer = QtWidgets.QWidget()
centerLayout.addWidget(radioContainer, alignment=QtCore.Qt.AlignCenter)
radioContainer.setSizePolicy(QtWidgets.QSizePolicy.Maximum,
QtWidgets.QSizePolicy.Preferred)
radioLayout = QtWidgets.QHBoxLayout(radioContainer)
radioLayout.addWidget(good_radiobutton)
radioLayout.addWidget(naive_radiobutton)
# ...

Do I ever need to manually destroy objects (like pixmaps)?

I am writing a PySide2-based applicatioon, which includes a QScrollArea that holds a lot of QPixmap images (or better: a list of QLabel's that in turn contain the pixmaps). That list of images can grow quite large over time, so when a certain number is reached I periodically remove some of these images from the scroll area - which works fine.
I do have the impression, however, that even after removing some of the images the memory consumption of my application is still the same. So removing the label widgets might not be sufficient. From the PySide2 docs on QLayout.removeWidget():
Removes the widget widget from the layout. After this call, it is the caller’s responsibility to give the widget a reasonable geometry or to put the widget back into a layout or to explicitly hide it if necessary.
In order to remove the widget I do the following:
while self.images_scroll_layout.count() > MAX_IMAGES:
to_remove = self.images_scroll_layout.itemAt(self.images_scroll_layout.count() - 1)
self.images_scroll_layout.removeItem(to_remove)
to_remove.widget().deleteLater()
So my question is: Do I need to manually destroy the labels/pixmaps I removed from the layout, or should they be garbage-collected automatically?
To understand the operation you have to have the following clear concepts:
A QObject will not be removed by the GC if it has a parent.
When a widget is added to a layout, then the widget is set as a child of the widget where the layout was established.
When using removeWidget() then only the widget is removed from the widget list that handles the layout, so the parent of the widget is still the widget that handles the layout.
To verify you can use the following code where the destroyed signal that indicates when a QObject is deleted will not be emitted.
from PySide2 import QtWidgets
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.add_button = QtWidgets.QPushButton(self.tr("Add"), clicked=self.add_widget)
self.remove_button = QtWidgets.QPushButton(
self.tr("Remove"), clicked=self.remove_widget
)
scrollarea = QtWidgets.QScrollArea(widgetResizable=True)
widget = QtWidgets.QWidget()
scrollarea.setWidget(widget)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.add_button)
lay.addWidget(self.remove_button)
lay.addWidget(scrollarea)
self.resize(640, 480)
self.label_layouts = QtWidgets.QVBoxLayout(widget)
self.counter = 0
def add_widget(self):
label = QtWidgets.QLabel(f"label {self.counter}")
self.label_layouts.addWidget(label)
self.counter += 1
def remove_widget(self):
item = self.label_layouts.itemAt(0)
if item is None:
return
widget = item.widget()
if widget is None:
return
widget.destroyed.connect(print)
print(f"widget: {widget} Parent: {widget.parentWidget()}")
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
In conclusion: removeWidget() is not used to remove the widget from memory but only makes the layout not handle that widget, if you want to remove a widget you must use deleteLater().
def remove_widget(self):
item = self.label_layouts.itemAt(0)
if item is None:
return
widget = item.widget()
if widget is None:
return
widget.destroyed.connect(print)
widget.deleteLater()

PyQt4 - Ambiguity in QLayout Spacing

I have a QHBoxLayout which I add widgets to, one by one.
I alternate by adding a Custom Widget, than a QLabel (repeating).
The QHBoxLayout is owned by a QGroupBox.
However, I notice that the spacing between the Custom Widget and the QLabel is 'irregular' when there are few widgets added to the Layout.
I intend for the QLabels to be centered in the gap between the custom widgets, but the QLabels only approach the centre as more widgets are added.
These screenshots show the two cases.
The desired case (only occuring with lots of widgets in the layout),
and the undesired case (occuring when little widgets are in the layout).
As you can see in the bottom case, the QLabels are not centered between the Custom Widgets. Instead, they are very far to the right!
What is causing this behaviour??
I believe the QGroupBox has a centered (horizontal) alignment and the QLabels have a fixed width of 10 (or 20 for the '->' QLabel) pixels (to avoid ugly overlap).
Any help at all would be greatly appreciated!
Thanks!
Specs:
python 2.7.1
PyQt4
Windows 7
The instantiation of the QHBoxLayout is absolutely normal and similiar to all examples.
Here is the code for the filling of the layout.
for i in range (0,len(Reactants)):
self.WidgetHouse.Reaction_Element_Layout.addWidget(eval('self.OverallContainer_Reactants.Reactant_'+str(i)))
# self.WidgetHouse.Reaction_Element_Layout is the QHBoxLayout
# self.OverallContainer_Reactants.Reactant_'+str(i) is a Custom Widget
if i != (len(Reactants)-1):
tmp = QtGui.QLabel('+')
tmp.setFixedWidth(10)
tmp.setAlignment(QtCore.Qt.AlignCenter)
self.WidgetHouse.Reaction_Element_Layout.addWidget(tmp)
else:
tmp = QtGui.QLabel('->')
tmp.setFixedWidth(20)
tmp.setAlignment(QtCore.Qt.AlignCenter)
self.WidgetHouse.Reaction_Element_Layout.addWidget(tmp)
EDIT:
Setting a fixed width of the QLabels (tmp.setFixedWidth(10)) is the source of the 'right anchoring'.
However, not setting a fixed width results in the same space being dedicated to QLabels and Custom Widgets in the layout, leading to an overlap of QLabels and Custom Widgets.
Identical to the code above, discluding 'tmp.setFixedWidt(10)'
What can I do to prevent this that's not completely horrible?
Can I move the Labels 'back' from the front?
(Calling .raise_() on all the custom widgets after everything was added to the layout did not work)
(Or will I have to do something like manually calculate the appropriate width of the labels based off the amount of widgets in the layout? Yuck!)
ANOTHER EDIT:
Progress:
I do not change the maximum/minimum (or fixed) width of the QLabels, but I do set their alignment to center.
Instead, I set a minimum width of the custom widget.
This fixes the apparent 'overlapping' (which wasn't really the case) and makes the Labels appear 'more centered'.
However, as you can see, the QLabels still aren't perfectly centered - too far right.
If I don't set a center alignment on the QLabels they are too far left.
What could be the problem now??
(I do not set a maximum width on the labels)
Thanks for all the help so far guys!
Here is a simple example script which is a reasonable approximation of the UI in the question, but without any of the layout issues:
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.groupBox = QtGui.QGroupBox(self)
hbox = QtGui.QHBoxLayout(self.groupBox)
length = 3
for index in range(length):
hbox.addWidget(Widget(u'H\u2082O', self))
if index < length - 1:
hbox.addWidget(Label(u'+', self))
else:
hbox.addWidget(Label(u'\u2192', self))
hbox.addWidget(Widget(u'4 H\u2082O', self))
hbox.addWidget(Label(u'+', self))
hbox.addWidget(Widget(u'H\u2084O\u2082', self))
vbox = QtGui.QVBoxLayout(self)
vbox.addWidget(self.groupBox)
vbox.addStretch()
class Label(QtGui.QLabel):
def __init__(self, label, parent=None):
QtGui.QLabel.__init__(self, label, parent)
self.setAlignment(QtCore.Qt.AlignCenter)
class Widget(QtGui.QWidget):
def __init__(self, label, parent=None):
QtGui.QWidget.__init__(self, parent)
self.setMaximumWidth(100)
layout = QtGui.QGridLayout(self)
self.label = QtGui.QLabel(label, self)
self.label.setAlignment(QtCore.Qt.AlignCenter)
layout.addWidget(self.label, 0, 0, 1, 2)
self.lineEdit = QtGui.QLineEdit(self)
layout.addWidget(self.lineEdit, 1, 0, 1, 2)
self.toolButton = QtGui.QToolButton(self)
layout.addWidget(self.toolButton, 2, 0, 1, 1)
self.comboBox = QtGui.QComboBox(self)
layout.addWidget(self.comboBox, 2, 1, 1, 1)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
Solution to all problems:
Give the custom widgets a fixed width.
Results in perfectly centered, not 'overlapping' widgets.
:)
Thanks for all the help guys!

Categories