QWidget becomes invisible on layout assignment - python

I'm making a custom window using QGridLayout to place window elements: like title bar, size grip, etc.
When I set any layout to my widget it doesn't show up on a start.
If I set .setVisible(True) it works nice.
So the question is: why does it happen, why widget becomes invisible on layout assignment?
Is that some kind of a bug or so it is conceived?
Widget file is:
from PySide2 import QtWidgets, QtGui, QtCore
class QCustomWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.background_color = QtGui.QColor(23, 23, 34)
self._initUI()
def _initUI(self):
self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
print(self.isVisible())
#self.setVisible(True)
# Is visible without layout
self.setLayout(QtWidgets.QGridLayout())
self.layout().setMargin(0)
self.layout().setSpacing(0)
# ---------------------------------------
def showEvent(self, event: QtGui.QShowEvent):
self.centerOnScreen()
def paintEvent(self, event: QtGui.QPaintEvent):
painter = QtGui.QPainter(self)
painter.setBrush(self.background_color)
painter.setPen(QtCore.Qt.NoPen)
painter.drawRect(0, 0, self.width(), self.height())
def centerOnScreen(self):
screen = QtWidgets.QDesktopWidget()
screen_geometry = screen.screenGeometry(self)
screen_center_x = screen_geometry.center().x()
screen_center_y = screen_geometry.center().y()
self.move(screen_center_x - self.width() // 2,
screen_center_y - self.height() // 2)
App file is:
from PySide2 import QtWidgets
from QCustomWindow import QCustomWindow
import sys
app = QtWidgets.QApplication(sys.argv)
window = QCustomWindow()
window.show()
sys.exit(app.exec_())

Qt does not update the geometry unless necessary or force him to do it, and it does this for efficiency reasons. So if the setVisible(True) or equivalent method is not called then the geometry will not change.
By not calling setVisible(True) then the size is recalculated when the parent widget is visible, and at that moment the widget analyzes the information of the QSizePolicy, QLayouts, etc. In your particular case the one that has the preference has the layout, and the layout calculates the size based on the widgets that are added to it but in your case it has no added widgets so the geometry calculated by the layout is 0x0 making it not visible for our sight. So if you add widgets to the QGridLayout you won't see that problem.
But if you call setVisible(True) the geometry is calculated at that moment, since there is no layout at that moment, the sizeHint is used, which by default is 640x480 and is therefore visible. And when the layout is established the container size is equal to the maximum of the previous size and the size provided by the layout.
Summary:
why widget becomes invisible on layout assignment?
If there is a layout set in a widget, then the size of the container widget will be the size determined by the widgets assigned to the layout, which in your case, since there are none, the size is 0x0.
Is that some kind of a bug or so it is conceived?
No, it is not a bug but an expected behavior. Add widgets or set a size to show the window.

Related

Pyqt5 window full screen does not show border

I create a pyqt window without any title bar and transparent. Also added blue border for my window but when on running the app I can't see any blue border for the window.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import Qt
import sys
class Window(QMainWindow):
def __init__(self):
super().__init__()
# this will hide the title bar
self.setWindowFlag(Qt.FramelessWindowHint)
self.setStyleSheet("border : 3px solid blue;")
self.setWindowOpacity(0.01)
# setting the geometry of window
self.setGeometry(100, 100, 400, 300)
self.showFullScreen()
# create pyqt5 app
App = QApplication(sys.argv)
# create the instance of our Window
window = Window()
window.show()
# start the app
sys.exit(App.exec())
How can I show the border for my window?
You can use the TranslucentBackground attribute and paint the border/background in paintEvent.
class Window(QMainWindow):
def __init__(self):
super().__init__()
# this will hide the title bar
self.setWindowFlag(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
# setting the geometry of window
self.setGeometry(100, 100, 400, 300)
self.showFullScreen()
def paintEvent(self, event):
qp = QPainter(self)
qp.setPen(QPen(Qt.blue, 6))
qp.drawRect(self.rect())
qp.setOpacity(0.01)
qp.setPen(Qt.NoPen)
qp.setBrush(self.palette().window())
qp.drawRect(self.rect())
The main problem with the original code is that the whole window gets almost transparent (having an alpha channel value of 0.01); this makes any content of the window as much as transparent, including the border, which becomes practically invisible unless the desktop background (or underlying windows) have enough contrast with the color set.
While the proposed solution works as expected and properly answer the OP question, there's another possibility that doesn't directly involve overriding paintEvent().
The issue of setting the border in the stylesheet and setting the WA_TranslucentBackground attribute is that only explicit opaque parts of the window are visible (child widgets, and any other painting implemented in paintEvent()): the background is automatically ignored, and, normally, the border along with it[1].
A possibility is to add a child widget to the main one and set the border for that widget only using a proper style sheet selector:
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlag(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
borderWidget = QWidget(objectName='borderWidget')
self.setCentralWidget(borderWidget)
bgd = self.palette().color(QPalette.Window)
bgd.setAlphaF(.01)
self.setStyleSheet('''
#borderWidget {{
border: 3px solid blue;
background: {bgd};
}}
'''.format(bgd=bgd.name(bgd.HexArgb)))
self.showFullScreen()
Note: in the code above (and that below) the background is still visible, but with a very low alpha channel value. If you don't need it, the background value could be ignored, but, for consistency it's better to explicitly declare it: background: transparent;.
That said, consider that QMainWindow has its own private layout, which could add unwanted margins on some systems (and trying to override them might not work at all).
Unless you really need any of the QMainWindow features (menu bar, status bar, tool bars and dock widgets), you should use a basic QWidget instead, which will give you direct control over the layout:
class Window(QWidget):
def __init__(self):
super().__init__()
self.setWindowFlag(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
borderWidget = QWidget(objectName='borderWidget')
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(borderWidget)
bgd = self.palette().color(QPalette.Window)
bgd.setAlphaF(.01)
self.setStyleSheet('''
#borderWidget {{
border: 3px solid blue;
background: {bgd};
}}
'''.format(bgd=bgd.name(bgd.HexArgb)))
self.showFullScreen()
Remember that setting the basic background/border properties of a QWidget stylesheet only works for actual QWidget instances: Qt subclasses implement it on their own way, and if you are going to create the child widget as a custom QWidget subclass the above will NOT work (see this question and this note in the documentation).
[1] This depends on the implementation of the platform and how Qt deals with it through its QPlatformPlugin. For instance, using older versions of xcompmgr I can get the border just by unsetting the WA_NoSystemBackground attribute (which, as the documentation reports, is automatically set when WA_TranslucentBackground is). While properly implementing specific-platform issues would be better, it's almost impossible as their behavior is often inconsistent across different versions, and the combinations between the window and composition managers are almost infinite. The proposed solution should work in all the situations, and with a minimal, possible, overhead.

Size of QWidget.rect doesn't change even when I resize the main window

I have a Main window, and I want to apply some QEvent in that window mouse events like move press... etc., My problem is after I created the app and main window I resized it, but when I call the rect() function which is from QWidget it actually gives me the default (0, 0, 100, 30), I want it to give me the size of the window.
class TestTools(unittest.TestCase):
app = QApplication([])
main_window = QMainWindow()
main_window.resize(958, 584)
scene = QGraphicsScene()
canvas = Canvas(scene, main_window)
print(canvas.rect()) # It Print (0, 0, 100, 30) Whereas it should be (0, 0, 958, 584)
def test():
pass # There is many functions am using but I don't think it is important to share it
And This is Canvas Class
class Canvas(QGraphicsView):
def __init__(self, scene, centralwidget):
super().__init__(scene, centralwidget)
self.scene = scene
background_color = Qt.white
self.pixmap_item: QGraphicsItem = self.scene.addPixmap(QPixmap(780, 580))
self.pixmap_item.setTransformationMode(Qt.FastTransformation)
So Where is the problem.
You're making an assumption based on wrong premises.
First of all, you're just creating a widget (the QGraphicsView) with a parent. Doing this will only make the widget a child of that parent, but without any size constraints: the widget is just "floating" inside its parent, so it will have a default size that usually is 100x30 for new child widgets (and 640x480 for widgets without parent set in the constructor)).
If you want to adjust the child to the parent it must be added to a layout. QMainWindow has its own (private) layout manager that uses a central widget to show the "main" content of the window, so if that QGraphicsView is going to be the only widget, you can just set that using main_window.setCentralWidget(canvas).
Then, a widget doesn't usually receive a resizeEvent until it's mapped the first time, unless it's manually resized. When it's shown the first time, any non previously layed out item receives the resize event. Since you're not showing the main window, no resize event is emitted, but even in that case, you resized the window before adding the child, so you would still see the same default size.
In order to ensure that the layout is properly computed even without showing the widget in which it is set, activate() must be explicitly called.
So, summing it up:
widgets must be added to a layout in order to adapt their size to their parent;
whenever a QMainWindow is used, setCentralWidget() must be used on the "main widget";
manual resizing should be done after adding children to the layout in order to ensure that their geometries adapt to the parent (if you do the opposite, some size policies could result in the parent resizing itself based on the children);
if the widget is not going to be shown, the layout must be manually activated;
class TestTools(unittest.TestCase):
app = QApplication([])
main_window = QMainWindow()
scene = QGraphicsScene()
canvas = Canvas(scene, main_window)
main_window.setCentralWidget(canvas)
main_window.resize(958, 584)
main_window.layout().activate()
print(canvas.rect())

How to prevent QComboBox from displaying unnecessary scrollbar

The code below, which is based on an example from zetcode.com, creates a single combo box. There are several issues with the resulting dialog, but the following are especially annoying:
PyQt displays a vertical scrollbar for the combo box, although there is plenty of space to display the entire list of options without a scrollbar.
I've tried to move the combo box to a position near the upper-left corner of the window, but this isn't working.
#!/usr/bin/python
import sys
from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QComboBox, QApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.setFixedWidth(400)
self.setFixedHeight(500)
self.initUI()
def initUI(self):
hbox = QHBoxLayout()
self.lbl = QLabel('Animals', self)
self.lbl.setStyleSheet('font-size:11pt')
combo = QComboBox(self)
combo.addItem('bear')
combo.addItem('cat')
combo.addItem('dog')
combo.addItem('dolphin')
combo.addItem('elephant')
combo.addItem('fish')
combo.addItem('frog')
combo.addItem('horse')
combo.addItem('rabbit')
combo.addItem('rat')
combo.addItem('shark')
combo.addItem('snake')
combo.addItem('tiger')
combo.addItem('whale')
combo.activated[str].connect(self.onActivated)
hbox.addWidget(combo)
hbox.setSpacing(20)
hbox.addWidget(self.lbl)
self.setContentsMargins(20, 20, 20, 20)
self.setLayout(hbox)
combo.move(20, 60)
self.setWindowTitle('QComboBox')
self.show()
def onActivated(self, text):
self.lbl.setText(text)
self.lbl.adjustSize()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
There are two wrong assumptions in the question.
the list of a QComboBox is a popup widget, it doesn't care (nor it should) about the available space the combobox might have: consider it as much as a context menu, which just pops out over the window, possibly going outside its boundaries if it requires more space (and that's just because those boundaries are meaningless to the menu);
the combo has been added to a layout manager, which takes care about resizing and positioning its (managed) child widgets, and that's why you cannot manually "move" them: the layout already sets the geometries automatically on its own everytime the widget is resized (which also happen when it's shown the first time), so any attempt to use move(), resize() or setGeometry() is completely useless;
When adding a widget to a layout, the default behavior is to make it occupy as much space as possible; since a QComboBox is one of those widgets that have a fixed size, the result is that it's actually centered (vertically and horizontally) in the space the layout is "assigning" to it, and this is clearly visible in your case because you set a fixed size for the container widget that is much bigger than what its contents would need.
There are two ways to align those widgets on top:
add the alignment arguments to addWidget:
hbox.addWidget(combo, alignment=QtCore.Qt.AlignTop)
hbox.addWidget(self.lbl, alignment=QtCore.Qt.AlignTop)
note that this won't give you good results in your case, because the label and the combo box have different heights, so the label might look "higher" than the combo;
use a QVBoxLayout layout as main layout for the widget, add the horizontal layout to it and then add a stretch after that (a stretch on a box layout is a "spacer" that tries to occupy as much space as possible)
# ...
mainLayout = QVBoxLayout()
mainLayout.addLayout(hbox)
mainLayout.addStretch()
self.setLayout(mainLayout)
PS: if you need to add lots of (string only) elements to a QComboBox, use addItems() instead of individually adding each of them.

How to set QStackedWidget size to child widgets minimum size?

Unable to get the QLabel in this example to be of the minimum size to contain its text. I need the layout and stacked widget to then size themselves to the minimum required to fit the label.
I have used code from https://www.tutorialspoint.com/pyqt/pyqt_qstackedwidget.htm to demonstrate my issue.
Setting the size policies seems to work when the application starts, but increasing the application width eventually causes the label to expand after the list reaches a certain width.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class stackedExample(QWidget):
def __init__(self):
super(stackedExample, self).__init__()
self.rightlist = QListWidget()
self.rightlist.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding)
self.rightlist.insertItem(0, 'Contact')
self.stack1 = QWidget()
self.stack1.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Minimum)
self.stack1UI()
self.Stack = QStackedWidget(self)
self.Stack.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Minimum)
self.Stack.addWidget(self.stack1)
hbox = QHBoxLayout(self)
hbox.addWidget(self.Stack)
hbox.addWidget(self.rightlist)
self.setLayout(hbox)
self.rightlist.currentRowChanged.connect(self.display)
self.setGeometry(300, 50, 10, 10)
self.setWindowTitle('StackedWidget demo')
self.show()
def stack1UI(self):
layout = QVBoxLayout()
label = QLabel("Hello World")
label.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Minimum)
label.setStyleSheet("QLabel { background-color : red; color : blue; }")
layout.addWidget(label)
self.stack1.setLayout(layout)
def display(self, i):
self.Stack.setCurrentIndex(i)
def main():
app = QApplication(sys.argv)
ex = stackedExample()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
tl;dr
Remove the size policy settings for everything but the QStackedWidget only (for which you'll have to set the horizontal policy to Maximum), and everything should be fine.
Explanation
I have to admit: I always felt that QSizePolicy enum names are confusing (I know that I'm not the only one), so I sympathize with your doubts.
Setting the stretch resolves the issue only partially, because sometime you might need or want to manually set the stretches, and that will possibly mess around with some size policies.
The problem is that you're setting the size policy to "Minimum", which, as the QSizePolicy documentation explains, says that:
The sizeHint() is minimal, and sufficient. The widget can be expanded [...]
And that's because Minimum uses the GrowFlag.
This means that, if the layout "thinks" that there's some available space for a widget, it will let it expand: Minimum does not mean that the widget will use it's minimal size (or, better, its minimumSizeHint()), but that it will use the sizeHint() as a minimum size for the layout, while it keeping its capacity to expand; if there's available space, it will use it.
What you actually need is to set the horizontal policy to Maximum instead, and, specifically, to the Stack object only (the QStackWidget, not the QWidget container, nor the QLabel).
That's because Maximum actually uses the ShrinkFlag (again, from the QSizePolicy docs):
The sizeHint() is a maximum. The widget can be shrunk any amount
without detriment if other widgets need the space (e.g. a separator
line). It cannot be larger than the size provided by sizeHint().
That said, be aware that there are known issues with QLabels in certain cases, specifically if the label has word wrapping.
Not sure if it is the correct approach, but adding in a stretch factor seems to have achieved what I was looking for.
hbox.addWidget(self.rightlist, stretch=1)

PyQt5 add custom QWidget to QLayout

Currently I am trying to add a custom QWidget class to a QVBoxLayout. The problem I'm getting is that the widget doesn't appear at all in the layout. I even tried setting the minimum size of the QWidget because I thought that the widget wasn't showing because it's default size was set to zero.
This is a simplification of what the class looks like:
class myWidget(QWidget):
def __init__(self):
super().__init__()
# Slider
self.mySlider = QSlider(Qt.Horizontal)
self.mySlider.setRange(-360, 360)
# LCD Screen
self.lcd = QLCDNumber()
self.lcd.setMinimumHeight(45)
self.lcd.setMaximumHeight(75)
# set Size
self.setMinimumSize(QSize(400,300))
I removed the signals and slots made between the slider and the LCD screen because I am not worried here about the functionality. Only the fact that I get a gray area of QSize(400,300) directly between the two buttons in the following code:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
#Create Widgets to be Added to Central Widget
self.w1 = QPushButton("First")
self.w2 = myWidget()
self.w3 = QPushButton("Third")
#Set Central Widget and VBox
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.layout = QVBoxLayout()
self.central_widget.setLayout(self.layout)
#Add widgets
self.layout.addWidget(self.w1)
self.layout.addWidget(self.w2)
self.layout.addWidget(self.w3)
So what I'm simply doing is creating the 3 widgets, and placing them into the QVBoxLayout within the central widget. The 2 button widgets w1 and w3 appear but my custom widget doesn't appear and increasing the size of the widget via setMinimumSize only adds grey spacing between w1 and w3.
So the widget is there it just isn't visible for some reason. I am pretty new to PyQt so please explain why this has happened.
QWidgets are just containers for other widgets. A QWidget without any layout and subwidgets will just look like empty space unless you're doing some custom painting or styling.
In your example, you're not actually adding any sub-widgets to your custom widget. In order to add a sub-widget to another widget, you need to either set the parent of the subwidget, or add the subwidget to the layout of the parent widget (which automatically re-parents the subwidget)
class myWidget(QWidget):
def __init__(self):
super().__init__()
# Slider
self.mySlider = QSlider(Qt.Horizontal)
Here, you are creating a QSlider, but it's not actually owned by MyWidget, it's going to end up being owned by Qt, and I would expect it to be drawn in the upper left hand corner of your main window.
In order to make this a subwidget of MyWidget you need to set the parent and add it to a layout.
class myWidget(QWidget):
def __init__(self):
super().__init__()
self.myLay = QVBoxLayout()
self.setLayout(self.myLay)
# Notice self is being passed in to set the parent
self.mySlider = QSlider(Qt.Horizontal, self)
# You need to add widgets to a layout to position them properly
self.myLay.addWidget(self.mySlider)

Categories

Resources