PyQT5 - mix VBox and HBox Layout? - python

I created the following windows which generally works fine:
from PyQt5.QtWidgets import (QApplication, QComboBox, QDialog,
QDialogButtonBox, QFormLayout, QGridLayout, QGroupBox, QHBoxLayout,
QLabel, QLineEdit, QMenu, QMenuBar, QPushButton, QSpinBox, QTextEdit,
QVBoxLayout)
import sys
class Dialog(QDialog):
NumGridRows = 3
NumButtons = 4
def __init__(self):
super(Dialog, self).__init__()
self.createFormGroupBox()
self.Button1 = QPushButton(self)
self.Button1.setText("Calc")
self.Button2 = QPushButton(self)
self.Button2.setText("Reset")
# buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
# buttonBox.accepted.connect(self.accept)
# buttonBox.rejected.connect(self.reject)
mainLayout = QVBoxLayout()
mainLayout.addWidget(self.formGroupBox)
# mainLayout.addWidget(buttonBox)
mainLayout.addWidget(self.Button1)
mainLayout.addWidget(self.Button2)
self.setLayout(mainLayout)
self.setWindowTitle("Form Layout")
def createFormGroupBox(self):
self.formGroupBox = QGroupBox("Form layout")
layout = QFormLayout()
layout.addRow(QLabel("Name:"), QLineEdit())
layout.addRow(QLabel("Country:"), QComboBox())
layout.addRow(QLabel("Age:"), QSpinBox())
self.formGroupBox.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
dialog = Dialog()
sys.exit(dialog.exec_())
The only thing I would like to change is that the buttons are horizontally arranged under the form layout and not vertically.
How can I achieve that to mix a vertical box with a horizontally box inside?

A Qt layout manager is a "container" of QLayoutItems, which are abstract items that represent visual objects that can be layed out.
Those items usually contain a Qt widget, but they can also be other objects, like spacers (QSpacerItem) or even other layouts (note that QLayout actually inherits from QLayoutItem).
You can actually do nested layouts.
The solution to your problem is actually pretty easy:
create a horizontal layout;
add the buttons to that layout;
add the layout to the main layout;
class Dialog(QDialog):
NumGridRows = 3
NumButtons = 4
def __init__(self):
super(Dialog, self).__init__()
self.setWindowTitle("Form Layout")
self.createFormGroupBox()
# QLayout(widget) automatically sets the layout on "widget" so you
# don't need to call widget.setLayout()
mainLayout = QVBoxLayout(self)
mainLayout.addWidget(self.formGroupBox)
self.button1 = QPushButton("Calc")
self.button2 = QPushButton("Reset")
buttonLayout = QHBoxLayout()
buttonLayout.addWidget(self.button1)
buttonLayout.addWidget(self.button2)
mainLayout.addLayout(buttonLayout)
Notes:
when widgets are going to be added to a layout, there's no point in providing the widget argument (self, in this case), as adding them to a layout that is set on a widget automatically reparents them;
only classes and constants should have capitalized names, not variables, functions or instance attributes (that's why I changed button1 and button2); remember that readability is very important, see the official Style Guide for Python Code to know more;
QDialogButtonBox is a QWidget on its own, with its internal layout; you can still use it for custom buttons, though; for instance, you could change the text of the Ok button doing buttonBox.button(buttonBox.Ok).setText("Calc"); or you could add a custom button by doing okButton = QPushButton("Calc") and buttonBox.addButton(okButton, buttonBox.AcceptRole); please carefully study the documentation to better understand the behavior of that class;

Related

PyQt5 - Buttons and Label shall have the same height

it's a simple application. Just one label and three buttons. But the layout is making me trouble.
I want to have the childlayout for the label and the childlayout for the buttons to have the same height. But I can't get it to work. Neither with BoxLayouts nor with GridLayouts. I tried to addStretch, so both layouts have the same stretchfactor, and various different stuff. I'm sure the answer is easy but I can't figure it out.
I also tried it in QtDesigner, so I could inspect and compare the codeparts, but:
I designed this
and got this when running the code, again the buttons stick to the bottom and dont have the same height as the label
However, here's my code:
(I know that the Grid Layout is not necessary here, it was just some sort of trying to solve the problem. Anyways I don't think that the Grid Layout is the problem, as I had the same problems with BoxLayouts or just the label without a layout.)
import sys
from PyQt5 import QtCore
from PyQt5.QtWidgets import QWidget, QApplication, QHBoxLayout, QLabel, QVBoxLayout, QPushButton, QGroupBox, \
QGridLayout, QMainWindow, QSizePolicy
class Window(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(100, 50, 400, 300)
self.setWindowTitle("Fretboard Note Quiz")
self.layout()
self.show()
def layout(self):
glay = QGridLayout()
self.setLayout(glay)
hbox1 = QHBoxLayout() #upper layout for label
hbox2 = QHBoxLayout() #bottom layout for buttons
btn1 = QPushButton("1")
btn2 = QPushButton("2")
btn3 = QPushButton("3")
#adding the buttons to the bottom layout
hbox2.addWidget(btn1)
hbox2.addWidget(btn2)
hbox2.addWidget(btn3)
label = QLabel("Text")
hbox1.addWidget(label)
glay.addLayout(hbox1, 0, 1)
glay.addLayout(hbox2, 1, 1)
glay.setRowStretch(0, 1)
glay.setRowStretch(1, 1)
if __name__ == "__main__":
app = QApplication(sys.argv)
win = Window()
sys.exit(app.exec_())
In this case, the simplest thing is to use a QWidget as a container since by default they are stretched in the same way as the QLabel, so they will try to occupy the same space.
The problem with your initial code is that the stretch factors do not apply to layouts but to widgets.
Finally layout() is a method so do not hide it using it for other things, the names of the functions should describe the action of the method.
import sys
from PyQt5.QtWidgets import (
QApplication,
QHBoxLayout,
QLabel,
QPushButton,
QVBoxLayout,
QWidget,
)
class Window(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(100, 50, 400, 300)
self.setWindowTitle("Fretboard Note Quiz")
self.build_layout()
def build_layout(self):
btn1 = QPushButton("1")
btn2 = QPushButton("2")
btn3 = QPushButton("3")
label = QLabel("Text")
button_container = QWidget()
hlay = QHBoxLayout(button_container)
hlay.addWidget(btn1)
hlay.addWidget(btn2)
hlay.addWidget(btn3)
vlay = QVBoxLayout(self)
vlay.addWidget(label)
vlay.addWidget(button_container)
if __name__ == "__main__":
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
Maybe you could set the minimunHeight of your buttons like:
ui.pushButton.setMinimumHeight(100)

QDockWidget with widgets in titlebar can't be collapsed

I have a QDockWidget and I have put some widgets on it's titlebar like in this MRE:
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (QWidget, QCheckBox, QMainWindow, QLabel, QDockWidget, QApplication, QHBoxLayout,
QSizePolicy)
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
central_widget = QLabel("Window stuff!")
self.setCentralWidget(central_widget)
self.dock = QDockWidget()
self.docker_layout = QHBoxLayout()
self.docker_layout.setContentsMargins(0, 0, 0, 0)
self.docker_layout.setAlignment(Qt.AlignLeft)
container = QWidget()
container.setLayout(self.docker_layout)
label = QLabel("Docker name")
label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
self.dock.setTitleBarWidget(container)
# Widgets creation:
self.docker_layout.addWidget(label)
self.check_1 = QCheckBox("Check 1")
self.docker_layout.addWidget(self.check_1)
self.check_2 = QCheckBox("Check 2", checked=True)
self.docker_layout.addWidget(self.check_2)
self.check_3 = QCheckBox("Check 3", checked=True)
self.docker_layout.addWidget(self.check_3)
self.check_4 = QCheckBox("You cant hide me haha!", checked=True)
self.docker_layout.addWidget(self.check_4)
self.dock.setTitleBarWidget(container)
self.addDockWidget(Qt.RightDockWidgetArea, self.dock)
self.dock_content = QLabel("Dock stuff!")
self.dock.setWidget(self.dock_content)
self.show()
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
app.exec()
But as you can see in the following giph, the dock is not collapsable due to the widgets in the sidebar. How can I allow the widgets to get out of screen and resize the dock freely?
That depends by the fact that the dock widget sets the minimum width based on that of the title bar widget: if no explicit minimum width is set, then the width returned by minimumSizeHint() is used.
A possible solution is to subclass the layout and reimplement both minimumWidth() and setGeometry().
The first ensures that the title bar can be resized to a smaller width, while the second lays outs the items by ignoring the given geometry at all.
The result is, obviously, that part of the title bar will be hidden.
class IgnoreWidthLayout(QHBoxLayout):
def minimumSize(self):
return QSize(80, super().minimumSize().height())
def setGeometry(self, geometry):
geometry.setWidth(super().minimumSize().width())
super().setGeometry(geometry)
class MainWindow(QMainWindow):
def __init__(self):
# ...
self.docker_layout = IgnoreWidthLayout()
Note that:
setting the Minimum size policy on the label is not required in this situation (and you probably should need Maximum instead, consider that QSizePolicy flags have names that are unintuitive at first);
you're calling setTitleBarWidget twice;
setting the alignment on the layout has usually little use (and is almost useless in this specific case): it doesn't tell the alignment of the child items, it specifies the alignment that layout manager will have once it's set on a parent widget or added to another layout;

Qt - Don't stretch widgets in QVBoxLayout

Issue
When I drag to resize window in Qt, it is stretching the widgets inside my QVBoxLayout. I would like to avoid this.
Here are a few pictures of the demo app (created for this stackoverflow post):
Screenshot: More Widgets than Space (works as expected -> scoll down to view content)
Screenshots: Less Widgets than Space (problem - Qt is stretching my AccountWidget items)
How can I use Qt Layouts, or other Qt Widget classes to solve this problem? Here is the code for my Demo app (about 50 lines):
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, \
QGroupBox, QGridLayout, QLabel, QScrollArea
from typing import List
class AccountWidget(QWidget):
def __init__(self, data: dict, parent=None):
super().__init__(parent)
self.group_box_layout = QGridLayout()
for i, (key, value) in enumerate(data.items()):
self.group_box_layout.addWidget(QLabel(key), i+1, 1)
self.group_box_layout.addWidget(QLabel(value), i+1, 2)
self.group_box = QGroupBox()
self.group_box.setLayout(self.group_box_layout)
self.main_layout = QVBoxLayout()
self.main_layout.addWidget(self.group_box)
self.setLayout(self.main_layout)
class AccountListWidget(QWidget):
def __init__(self, data: List[dict], parent=None):
super().__init__(parent)
self.main_layout = QVBoxLayout()
for account_data in data:
self.main_layout.addWidget(AccountWidget(account_data))
self.setLayout(self.main_layout)
class MainWidget(QWidget):
def __init__(self, data: List[dict], parent=None):
super().__init__(parent)
self.account_list_widget = AccountListWidget(data)
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setWidget(self.account_list_widget)
self.main_layout = QVBoxLayout()
self.main_layout.addWidget(self.scroll_area)
self.setLayout(self.main_layout)
if __name__ == "__main__":
app = QApplication(sys.argv)
data = [{"created":"2019","balance":"100","deposits":"50","type":"Chq"},
{"created":"2020","balance":"80","deposits":"45","type":"Chq"},
{"created":"2020","balance":"70","deposits":"55","type":"Sav"}]
mainWidget = MainWidget(data)
mainWindow = QMainWindow()
mainWindow.setCentralWidget(mainWidget)
mainWindow.setWindowTitle("Demo App")
mainWindow.resize(300, 300)
mainWindow.show()
app.exec()
Just add a stretch to the bottom of the layout (this is only possible for boxed layout):
class AccountListWidget(QWidget):
def __init__(self, data: List[dict], parent=None):
# ...
self.main_layout.addStretch()
If you need to do something like this on a grid layout, you can add a QSpacerItem or a single empty QWidget with its size policy set to expanded:
spacer = QtWidgets.QWidget()
spacer.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanded)
gridLayout.addWidget(spacer, gridLayout.rowCount(), 0)
Another solution is to set the size policy to the container (instead of adding the stretch):
class AccountListWidget(QWidget):
def __init__(self, data: List[dict], parent=None):
# ...
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
A couple of suggestions:
avoid using unnecessary container widgets: Your AccountWidget only contains a QGroupBox, so you should probably just subclass from QGroupBox instead;
grid layouts, as most things, use 0-based indexes, so your widgets should be added starting from row and column 0 (not 1); if you're doing that to add margins, use layout.setContentsMargins;

Why my QToolButton which I try to make collapsible doesn't collapse properly?

guys.
Asking for your help to troubleshoot my test script.
I am practicing to make collapsible button with widgets inside.
Script was mainly taken from another question in stackoverflow about collapsible buttons.
So I am trying to put under QTabWidget my class CollpsibleBox(QWidget). Problem is that my CollapsibleBox is acting very weird - buttons are jumping , sometimes it doesn't open/close properly.
I was wondering if it's some mistake in placing correctly my widget under QTabWidget or is there some problem with animation?
import random
from PySide2.QtGui import QPixmap, QBrush, QColor, QIcon, QPainterPath, QPolygonF, QPen, QTransform
from PySide2.QtCore import QSize, Qt, Signal, QPointF, QRect, QPoint, QParallelAnimationGroup, QPropertyAnimation, QAbstractAnimation
from PySide2.QtWidgets import QMainWindow, QDialog, QVBoxLayout, QHBoxLayout, QGraphicsView, QGraphicsScene, QFrame, \
QSizePolicy, QGraphicsPixmapItem, QApplication, QRubberBand, QMenu, QMenuBar, QTabWidget, QWidget, QPushButton, \
QSlider, QGraphicsPolygonItem, QToolButton, QScrollArea, QLabel
extraDict = {'buttonSetA': ['test'], 'buttonSetB': ['test']}
tabList = ['Main', 'Extra']
_ui = dict()
class MainWindow(QDialog):
def __init__(self, parent=None):
QDialog.__init__(self, parent=parent)
self.create()
def create(self, **kwargs):
_ui['mainLayout'] = QVBoxLayout()
_ui['tabWidget'] = QTabWidget()
_ui['mainLayout'].addWidget(_ui['tabWidget'])
for tab in tabList:
_ui['tab' + tab] = QWidget()
_ui['tabWidget'].addTab(_ui['tab' + tab], tab)
_ui['tabExtra'].layout = QVBoxLayout()
_ui['tabExtra'].setLayout(_ui['tabExtra'].layout)
_ui['content'] = QWidget()
_ui['tabExtra'].layout.addWidget(_ui['content'])
vlay = QVBoxLayout(_ui['content'])
for name in extraDict.keys():
box = CollapsibleBox(name)
vlay.addWidget(box)
lay = QVBoxLayout()
for j in range(8):
label = QLabel("{}".format(j))
color = QColor(*[random.randint(0, 255) for _ in range(3)])
label.setStyleSheet(
"background-color: {}; color : white;".format(color.name())
)
label.setAlignment(Qt.AlignCenter)
lay.addWidget(label)
box.setContentLayout(lay)
self.setLayout(_ui['mainLayout'])
class CollapsibleBox(QWidget):
def __init__(self, name):
super(CollapsibleBox, self).__init__()
self.toggle_button = QToolButton(text=name, checkable=True, checked=False)
self.toggle_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.toggle_button.setArrowType(Qt.RightArrow)
self.toggle_button.pressed.connect(self.on_pressed)
self.toggle_animation = QParallelAnimationGroup(self)
self.content_area = QScrollArea(maximumHeight=0, minimumHeight=0)
self.content_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.content_area.setFrameShape(QFrame.NoFrame)
lay = QVBoxLayout(self)
lay.setSpacing(0)
lay.setContentsMargins(0, 0, 0, 0)
lay.addWidget(self.toggle_button)
lay.addWidget(self.content_area)
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"minimumHeight"))
self.toggle_animation.addAnimation(QPropertyAnimation(self, b"maximumHeight"))
self.toggle_animation.addAnimation(QPropertyAnimation(self.content_area, b"maximumHeight"))
def on_pressed(self):
checked = self.toggle_button.isChecked()
self.toggle_button.setArrowType(Qt.DownArrow if not checked else Qt.RightArrow)
self.toggle_animation.setDirection(QAbstractAnimation.Forward
if not checked
else QAbstractAnimation.Backward
)
self.toggle_animation.start()
def setContentLayout(self, layout):
lay = self.content_area.layout()
del lay
self.content_area.setLayout(layout)
collapsed_height = (self.sizeHint().height() - self.content_area.maximumHeight())
content_height = layout.sizeHint().height()
for i in range(self.toggle_animation.animationCount()):
animation = self.toggle_animation.animationAt(i)
animation.setDuration(500)
animation.setStartValue(collapsed_height)
animation.setEndValue(collapsed_height + content_height)
content_animation = self.toggle_animation.animationAt(self.toggle_animation.animationCount() - 1)
content_animation.setDuration(500)
content_animation.setStartValue(0)
content_animation.setEndValue(content_height)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.setGeometry(500, 100, 500, 500)
window.show()
sys.exit(app.exec_())
The problem is that you are only adding two widgets to the full layout, and the layout will try to place them as better as possible (tipically at the center of the area that is available for each widget, based on its size hints).
You could either set the alignment of the widget for the layout (placing the buttons on top of their available space):
vlay = QVBoxLayout(_ui['content'])
for name in extraDict.keys():
box = CollapsibleBox(name)
vlay.addWidget(box, alignment=Qt.AlignTop)
Or add a stretch to the bottom of the layout:
vlay = QVBoxLayout(_ui['content'])
for name in extraDict.keys():
# ...
vlay.addStretch(1)
which will position all buttons on top of the layout.
As a side note, I'd suggest you to avoid the dictionary logic for the ui, as it might become very confusing and prone to errors. If you really need to do that for some (I hope, very good) reason that's ok, but please avoid it when asking questions: it makes really hard to read your code, and people might end up just ignoring your question at all.

How to control dimensions of layouts PyQt5

I'm trying to setup a layout similar to this question. Create Qt layout with fixed height. I'm trying to use the solution posted there but I cant seem to recreate it in python. How do I create a widget with a horizontal box layout and set the dimensions of the new widget? When I tried recreating the code in python my widget ended up as a layout instead of a widget. Below is the code I'm trying to rewrite in python, thanks in advance:
// first create the four widgets at the top left,
// and use QWidget::setFixedWidth() on each of them.
// then set up the top widget (composed of the four smaller widgets):
QWidget *topWidget = new QWidget;
QHBoxLayout *topWidgetLayout = new QHBoxLayout(topWidget);
topWidgetLayout->addWidget(widget1);
topWidgetLayout->addWidget(widget2);
topWidgetLayout->addWidget(widget3);
topWidgetLayout->addWidget(widget4);
topWidgetLayout->addStretch(1); // add the stretch
topWidget->setFixedHeight(50);
// now put the bottom (centered) widget into its own QHBoxLayout
QHBoxLayout *hLayout = new QHBoxLayout;
hLayout->addStretch(1);
hLayout->addWidget(bottomWidget);
hLayout->addStretch(1);
bottomWidget->setFixedSize(QSize(50, 50));
// now use a QVBoxLayout to lay everything out
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(topWidget);
mainLayout->addStretch(1);
mainLayout->addLayout(hLayout);
mainLayout->addStretch(1);
The translation of C++ to Python does not have so much science since you only have to understand the logic:
from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QApplication, QComboBox, QHBoxLayout, QLineEdit, QPushButton, QSpinBox, QToolButton, QVBoxLayout, QWidget
class Widget(QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
widget1 = QPushButton("widget1")
widget2 = QSpinBox()
widget3 = QComboBox()
widget4 = QLineEdit()
bottomWidget = QToolButton(text="botton")
# first create the four widgets at the top left,
# and use QWidget::setFixedWidth() on each of them.
# then set up the top widget (composed of the four smaller widgets):
topWidget = QWidget()
topWidgetLayout = QHBoxLayout(topWidget)
topWidgetLayout.addWidget(widget1)
topWidgetLayout.addWidget(widget2)
topWidgetLayout.addWidget(widget3)
topWidgetLayout.addWidget(widget4)
topWidgetLayout.addStretch(1)
topWidget.setFixedHeight(50)
# now put the bottom (centered) widget into its own QHBoxLayout
hLayout = QHBoxLayout()
hLayout.addStretch(1)
hLayout.addWidget(bottomWidget)
hLayout.addStretch(1)
bottomWidget.setFixedSize(QSize(50, 50))
# now use a QVBoxLayout to lay everything out
mainLayout = QVBoxLayout()
mainLayout.addWidget(topWidget)
mainLayout.addStretch(1)
mainLayout.addLayout(hLayout)
mainLayout.addStretch(1)
self.setLayout(mainLayout)
self.resize(640, 480)
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())

Categories

Resources