Space is added to the layout even if nothing was added - python

I have been designing a PySide6 Application but the problem is that even if I have not added any space to the layout the layout still contains a lot of visible space.
Debug Screenshot with widget borders as red:
Is there anyway to remove the space? Here is the code:
from PySide6 import QtCore
from PySide6.QtWidgets import (
QMainWindow,
QVBoxLayout,
QFormLayout,
QWidget,
QApplication,
QLabel,
QCheckBox,
QHBoxLayout,
QLineEdit,
QFileDialog,
QPushButton
)
import os
import sys
class Styler_Py(QMainWindow):
def __init__(self, parent=None) -> None:
super().__init__(parent)
# Window Setup
self.setWindowTitle("PyStyler")
# Layout setup
self.container_widget = QWidget()
self.main_layout = QVBoxLayout()
self.container_widget.setLayout(self.main_layout)
self.setCentralWidget(self.container_widget)
# Irrelevant code here
# --- New Section ---
self.main_layout.addSpacing(10)
self.main_layout.addStretch()
# --- New Section ---
# Files
self.files_label = QLabel("<h3>Files</h3>") # Note for the future, the closure tag did not have forward slash and have been fixed using an answer below
self.main_layout.addWidget(self.files_label)
self.files_layout = QVBoxLayout()
self.main_layout.addLayout(self.files_layout)
# Files -> Input File
self.input_file = FileInput()
self.input_file_layout = QHBoxLayout()
self.files_layout.addLayout(self.input_file_layout)
self.input_file_label = QLabel("Input File")
self.input_file_layout.addWidget(self.input_file_label)
self.input_file_layout.addWidget(self.input_file)
class FileInput(QWidget):
def __init__(self, start_path=os.path.expanduser("~")) -> None:
super().__init__()
self._main_layout = QHBoxLayout()
self.setLayout(self._main_layout)
# Add Text edit for File Input
self._file_input = QLineEdit(start_path)
self._main_layout.addWidget(self._file_input)
self._file_input_browse = QPushButton("Browse")
self._file_input_browse.clicked.connect(self._browse_file_dialog)
self._main_layout.addWidget(self._file_input_browse)
self._start_path = start_path
def _browse_file_dialog(self):
if os.path.isfile(self._file_input.text()):
path = os.path.abspath(f"{self._file_input.text()}/..")
if os.path.isdir(self._file_input.text()):
path = os.path.abspath(self._file_input.text())
else:
path = self._start_path
file_name = QFileDialog.getOpenFileName(
self, "Open File", path
)
if len(file_name[0]) > 0:
self._file_input.setText(file_name[0])
def get_path(self):
return self._file_input.text()
def main():
app = QApplication(sys.argv)
app.setStyleSheet("*{ border: 1px solid red; }")
window = Styler_Py()
window.show()
app.exec()
main()
I think the problem is somewhere with my custom widget but I could not find it. I have tried adding and removing a lot of the widgets but nothing happens.
Edit
A Screenshot of window when resized to be a little bigger:

The first problem comes from the wrong tag closure, as it should be like this:
self.files_label = QLabel("<h3>Files</h3>")
Then, that spacing is the result of two aspects: layout spacings/margins and rich text margins.
layout spacings are inherited by child layouts from their parent layout (so, the input_file_layout has a default spacing inherited by the files_layout, which in turns inherits that property from the main_layout;
when layout managers are installed on widgets, they use the widget's style layout margins and spacings (QStyle.PM_Layout*Margin and QStyle.PM_Layout*Spacing) unless overriden by layout.setContentsMargins() or layout.setSpacing(); this not only happens top level widgets, but for child widgets too: layouts set on children widgets do not inherit the parent layout margins or spacings, and they use the default system (or style) values; this can be overridden
rich text elements normally result in bigger space required by the label's sizeHint(); unfortunately there's no easy work around for that, as also explained in the Layout Issues documentation;
In order to better understand the boundaries of each widget, you could add a simple stylesheet for testing purposes:
app.setStyleSheet('*{ border: 1px solid black; }')
Please consider that the above will override any widget border (including buttons, for example), so use it only for debugging in order to understand how the layout works; do NOT use it for general purposes, especially if applied on the application.

Related

Qt Dialog in Main Window, make the background dimmed

I am trying to create a dynamic custom dialog class. That takes the central widget as a parameter and that centralwidget will be the main widget of this custom dialog. (for dynamic)
When It will show itself, it will make the background dark/dimmed.
Probably I should use exec_ function for not clickable out of dialog area.
The important thing is to add himself to the layout of the mainwindow, so it can automatically adjust itself when the mainwindow's size/position changes.
I can do this with hooking resizeEvent/moveEvent but I am looking better way to do this. If I add this custom dialog to mainwindow's layout, it is gonna be better.
Thanks.
Creating a child widget that is drawn over the current content of the window and resized in the resizeEvent() override is absolutely not a problem. In fact, it's practically what Qt does every time a widget using a layout manager is resized. The benefit of this approach is that you can completely "cover" all contents of the window, including the menu bar, the status bar and any dock/toolbar.
If you still want them to be usable and only want to cover the main widget, you can do the same by setting the "cover" as a child of the main widget itself, instead of using the window as a parent.
An alternative could be to use a QStackedWidget as a central widget, and set the layout (which is a QStackedLayout) to use the StackAll stackingMode, which will allow you to show all "pages" of the stacked layout superimposed.
Note that this approach has an important drawback: you must take care of the tab-focus. Since all widgets (including widgets that belong to another "page") are shown and enabled, changing focus through Tab will allow changing the focus to widgets that are not part of your "dialog".
I'll leave you with a basic example, the central widget is a QTableWidget, and it shows the "popup" whenever an item is doubleclicked.
Please carefully study it and try to understand what it does.
from PyQt5 import QtCore, QtWidgets
class Container(QtWidgets.QWidget):
def showEvent(self, event):
if not event.spontaneous():
self.setFocus()
# certain widgets might want to keep focus on tab
# so we delay the focusNextChild
QtCore.QTimer.singleShot(0, self.focusNextChild)
def focusNextPrevChild(self, isNext):
# keep tab focus on this widget
super().focusNextPrevChild(isNext)
return self.isAncestorOf(QtWidgets.QApplication.focusWidget())
def paintEvent(self, event):
# stylesheets set on QWidget subclasses need this
qp = QtWidgets.QStylePainter(self)
opt = QtWidgets.QStyleOption()
opt.initFrom(self)
qp.drawPrimitive(QtWidgets.QStyle.PE_Widget, opt)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.menuBar().addMenu('Test').addAction('Action')
self.stack = QtWidgets.QStackedWidget(self)
self.setCentralWidget(self.stack)
self.stack.layout().setStackingMode(QtWidgets.QStackedLayout.StackAll)
table = QtWidgets.QTableWidget(20, 30)
self.stack.addWidget(table)
table.cellDoubleClicked.connect(self.showDialog)
self.resize(QtWidgets.QApplication.primaryScreen().size() * 2 / 3)
def showDialog(self, row, column):
background = QtWidgets.QWidget(objectName='background')
background.setStyleSheet('''
#background {
background: rgba(64, 64, 64, 64);
}
Container {
background: palette(window);
border: 1px outset palette(window);
border-radius: 5px;
}
''')
backLayout = QtWidgets.QVBoxLayout(background)
container = Container()
backLayout.addWidget(container, alignment=QtCore.Qt.AlignCenter)
container.setAutoFillBackground(True)
layout = QtWidgets.QVBoxLayout(container)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(20)
font = self.font()
font.setPointSize(font.pointSize() * 3)
layout.addWidget(QtWidgets.QLabel(
'Hello!', font=font, alignment=QtCore.Qt.AlignCenter))
layout.addWidget(QtWidgets.QLabel(
'You doubleclicked cell {}, {}'.format(row + 1, column + 1)))
button = QtWidgets.QPushButton('Close')
layout.addWidget(button)
self.centralWidget().addWidget(background)
self.centralWidget().setCurrentWidget(background)
# Important! you must always delete the widget when you don't need it
# anymore. Alternatively, hide it if you want to reuse it again later
button.clicked.connect(background.deleteLater)
app = QtWidgets.QApplication([])
win = MainWindow()
win.show()
app.exec()

PYQT5 set all labels to black

I have several checkboxes which control various procedures, once the code has run all checkboxes which were successful are coloured green e.g. self.cbMarkerDetection.setStyleSheet("color: green;") as it goes to the next job I want to reset the colour back to black. Obviously I could do self.cbMarkerDetection.setStyleSheet("color: black;") for every checkbox... but I'm hoping there is some way to do something like:
for checkbox in allcheckboxes:
self.checkbox.setStyleSheet("color: black;")
A simple coded example, two check boxes, when selected it goes green in colour, when I click the other check box I want all to change back to black, imagine checkboxes a-g, when checkbox (or a button if you like) is selected/checked the selected goes green when checkbox z (or a button) is selected/clicked all the existing check boxes turn black. As stated above, I could list all the checkboxes and set them black, but I'm looking for a quicker way with less code:
import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QMainWindow, QLabel, QCheckBox, QWidget
from PyQt5.QtCore import QSize
class ExampleWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setMinimumSize(QSize(140, 40))
self.setWindowTitle("Checkbox")
self.a = QCheckBox("box1",self)
self.a.stateChanged.connect(self.clickBox)
self.a.move(20,20)
self.a.resize(320,40)
self.b = QCheckBox("box2",self)
self.b.stateChanged.connect(self.clickBox)
self.b.move(20,80)
self.b.resize(320,40)
def clickBox(self, state):
if state == QtCore.Qt.Checked:
print('Checked')
self.checkbox.setStyleSheet("color: green;")
else:
print('Unchecked')
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
mainWin = ExampleWindow()
mainWin.show()
sys.exit( app.exec_() )
The simplest solution is to use findChildren():
for checkbox in self.findChildren(QCheckBox, options=Qt.FindDirectChildrenOnly):
checkbox.setStyleSheet("color: black;")
If you have other checkboxes in your widget that should not included in the list, you can create an empty subclass and use that for the filter:
class MyCheckBox(QCheckBox): pass
# ...
self.a = MyCheckBox("box1", self)
# etc...
Then you use findChildren with the custom class name:
for checkbox in self.findChildren(MyCheckBox, options=Qt.FindDirectChildrenOnly):
checkbox.setStyleSheet("color: black;")
Note that using fixed geometries is discouraged, and you should prefer layout managers instead.

PyQt - How to use the "setStyleSheet" method properly?

if I use the setStyleSheet method in order to change the style for a specific widget, the other ones placed inside it, changes their style, but I don't want it! I can bring you two example:
when I change the border/background color for a frame (see the widgets placed inside it):
import PyQt5.QtGui as qtg
import PyQt5.QtCore as qtc
import PyQt5.QtWidgets as qtw
import sys
class MainWindow(qtw.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.resize(520,300)
self.setWindowTitle("Treeview Example")
self.layout = qtw.QVBoxLayout()
self.frame1=qtw.QFrame()
self.frame1layout=qtw.QVBoxLayout()
self.frame1layout.setContentsMargins(5, 5, 5, 5)
self.frame1.setLayout(self.frame1layout)
self.frame1.setStyleSheet("border: 1px solid; border-color:red; background-color:white") # I change the style for the main frame
self.layout.addWidget(self.frame1)
self.frame2=qtw.QFrame()
self.frame2layout=qtw.QVBoxLayout()
self.frame2layout.setContentsMargins(10, 10, 10, 10)
self.frame2.setLayout(self.frame2layout)
self.frame1layout.addWidget(self.frame2)
self.ProgressBar=qtw.QProgressBar()
self.frame2layout.addWidget(self.ProgressBar)
self.setLayout(self.layout)
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
or when I change the border color for a treeview widget (the scrolled treeview widget is became white):
import PyQt5.QtGui as qtg
import PyQt5.QtCore as qtc
import PyQt5.QtWidgets as qtw
import sys
class MainWindow(qtw.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.resize(520,300)
self.setWindowTitle("Treeview Example")
self.layout = qtw.QVBoxLayout()
self.treeview = qtw.QTreeView(self)
self.treeview.setStyleSheet("border: 1px solid; border-color:red") # it destroy the style of the objects inside the treeview widget!
model = qtg.QStandardItemModel()
rootNode = model.invisibleRootItem()
section1 = qtg.QStandardItem("A")
section1.appendRow([qtg.QStandardItem("A1")])
childnode = qtg.QStandardItem("A2")
section1.appendRow([childnode])
section2 = qtg.QStandardItem("B")
section2.appendRow([qtg.QStandardItem("B1")])
section2.appendRow([qtg.QStandardItem("B2")])
rootNode.appendRow([section1])
rootNode.appendRow([section2])
self.treeview.setHeaderHidden(True)
self.treeview.setModel(model)
self.layout.addWidget(self.treeview)
self.setLayout(self.layout)
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
my question is, how can I change the style of a specific widget without modifying the style of the other ones?
###########
UPDATE:
in my first example, if I don't use the instruction self.treeview.setStyleSheet("border: 1px solid; border-color:red") I realized that the progress bar widget doesn't expand as before. see this screenshot:
what is the issue?
I strongly suggest you to more carefully read the style sheet syntax and reference documentation, as everything is clearly specified and explained there:
Style sheets consist of a sequence of style rules. A style rule is made up of a selector and a declaration. The selector specifies which widgets are affected by the rule; the declaration specifies which properties should be set on the widget.
Stylesheets are, by definition, cascading.
The stylesheet set on a widget is propagated on its children, those children inherit the style of the parent.
If you set a generic property like this:
border: 1px solid black;
The result is that all its children will have that border: you only gave the declaration but without the selector, so a universal selector is used as implicit.
This is not just Qt, this is typical of CSS (from which QSS take their main concepts), and it works exactly as any other widget styling property: setting a font or a palette on a widget, propagates them to all its children.
The difference is that with stylesheets (exactly like standard CSS) you can use selectors.
If you want to style the tree widget only, then use that class selector:
self.treeview.setStyleSheet('''
QTreeView {
border: 1px solid;
border-color:red;
}
''')

Python PyQt5 how to show the full QMenuBar with a QWidget

I'm getting this weird result when using QMenuBar I've used this exact code before for the QMenuBar and it worked perfectly. But it doesn't show more than 1 QMenu
This is my code:
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
from functools import partial
class MainMenu(QWidget):
def __init__(self, parent = None):
super(MainMenu, self).__init__(parent)
# background = QWidget(self)
lay = QVBoxLayout(self)
lay.setContentsMargins(5, 35, 5, 5)
self.menu()
self.setWindowTitle('Control Panel')
self.setWindowIcon(self.style().standardIcon(getattr(QStyle, 'SP_DialogNoButton')))
self.grid = QGridLayout()
lay.addLayout(self.grid)
self.setLayout(lay)
self.setMinimumSize(400, 320)
def menu(self):
menubar = QMenuBar(self)
viewMenu = menubar.addMenu('View')
viewStatAct = QAction('Dark mode', self, checkable=True)
viewStatAct.setStatusTip('enable/disable Dark mode')
viewMenu.addAction(viewStatAct)
settingsMenu = menubar.addMenu('Configuration')
email = QAction('Set Email', self)
settingsMenu.addAction(email)
if __name__ == '__main__':
app = QApplication(sys.argv)
main = MainMenu()
main.show()
sys.exit(app.exec_())
Result:
I am aware that I am using QWidget when I should be using QMainWindow But is there a workaround???
(I apologize in advance for the terrible quality of the image, there is no good way to take a picture of a QMenuBar)
The problem is that with a QWidget you are not using the "private" layout that a QMainWindow has, which automatically resizes specific children widgets (including the menubar, the statusbar, the dock widgets, the toolbars and, obviously, the "centralWidget").
Remember that a QMainWindow has its own layout (which can't and shouldn't be changed), because it needs that specific custom layout to lay out the aforementioned widgets. If you want to set a layout for the main window, you'll need to apply it to its centralWidget.
Read carefully how the Main Window Framework behaves; as the documentation reports:
Note: Creating a main window without a central widget is not supported. You must have a central widget even if it is just a placeholder.
In order to work around that when using a basic QWidget, you'll have to manually resize the children widgets accordingly. In your case, you only need to resize the menubar, as long as you have a reference to it:
def menu(self):
self.menubar = QMenuBar(self)
# any other function has to be run against the *self.menubar* object
viewMenu = self.menubar.addMenu('View')
# etcetera...
def resizeEvent(self, event):
# calling the base class resizeEvent function is not usually
# required, but it is for certain widgets (especially item views
# or scroll areas), so just call it anyway, just to be sure, as
# it's a good habit to do that for most widget classes
super(MainMenu, self).resizeEvent(event)
# now that we have a direct reference to the menubar widget, we are
# also able to resize it, allowing all actions to be shown (as long
# as they are within the provided size
self.menubar.resize(self.width(), self.menubar.height())
Note: you can also "find" the menubar by means of self.findChild(QtWidgets.QMenuBar) or using the objectName, but using an instance attribute is usually an easier and better solution.
Set minimum width
self.setMinimumSize(320,240)

(Py)Qt: Spacing in QHBoxLayout shows centralwidget's background, not parent's

Consider the following example code:
from PyQt5.QtWidgets import (QApplication, QHBoxLayout, QLabel, QWidget,
QMainWindow, QVBoxLayout, QTextEdit)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
cwidget = QWidget(self)
cwidget.setStyleSheet("QWidget { background-color: red; }")
self.setCentralWidget(cwidget)
self.resize(100, 100)
vbox = QVBoxLayout(cwidget)
vbox.addWidget(QTextEdit(self))
vbox.addWidget(BlackBar(self))
class BlackBar(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setStyleSheet("* { background-color: black; color: white; }")
hbox = QHBoxLayout(self)
hbox.setSpacing(5)
hbox.addWidget(QLabel(text="eggs"))
hbox.addWidget(QLabel(text="bacon"))
if __name__ == '__main__':
app = QApplication([])
main = MainWindow()
main.show()
app.exec_()
It has:
A QMainWindow, QWidget as central widget (red), QVBoxLayout as a child of the cental widget. Inside there:
A QTextEdit (just as a filler)
A QWidget (black), which contains a QHBoxLayout. Inside that:
Two QLabels
This looks like this:
I'd expect the spaces between the labels to be black, because the QHBoxLayout is a child of BlackBar, but it seems BlackBar is just "invisible" in between and the central widget "shines through". Why is this?
The bugreport has now been answered with a solution that's easier than #ekhumoro's answer and works:
I don't think this is valid. The paint code your are looking for is not drawn in the paintEvent. Look for QWidgetPrivate::paintBackground instead. For performance reasons widgets will ignore style sheets by default, but you can set the WA_StyledBackground attribute on the widget and it should respect style sheet backgrounds.
And indeed, doing this before setting the stylesheet does the trick:
self.setAttribute(Qt.WA_StyledBackground)
Although the Style Sheet Syntax does not mention it, it seems that the QWidget class is treated differently when it comes to stylesheets.
Other widgets will work fine with your example code. For example, if QWidget is replaced everywhere with QFrame, then everything works as expected.
To get stylesheet support for QWidget subclasses, you need to reimplement the paintEvent and enable it explicitly:
class BlackBar(QWidget):
...
def paintEvent(self, event):
option = QStyleOption()
option.initFrom(self)
painter = QPainter(self)
self.style().drawPrimitive(
QStyle.PE_Widget, option, painter, self)

Categories

Resources