Buttons in QGraphicsView/ QGraphicsScene not painting correctly - python

When I put buttons inside a scene and inside a QGraphicsView, the region to the right of the button incorrectly turn gray.
I can reproduce this in Windows and Linux. Are there any tricks to get rid of this unwanted feature?
from PyQt5.QtWidgets import (QApplication, QGraphicsView, QGraphicsScene,
QPushButton, QLabel)
from PyQt5.QtCore import (Qt, QRectF)
from PyQt5 import QtCore
class MyView(QGraphicsView):
def __init__(self, parent = None):
super(MyView, self).__init__(parent)
self.button1 = QPushButton('Button1')
self.button1.setGeometry(-60, -60, 80, 40)
self.button2 = QPushButton('Button2')
self.button2.setGeometry(10, 10, 80, 40)
version = 'PYQT_VERSION_STR: ' + QtCore.PYQT_VERSION_STR + '\n'
version += 'QT_VERSION_STR: ' + QtCore.QT_VERSION_STR + '\n'
self.label = QLabel(version)
self.label.setGeometry(-100, 80, 160, 80)
self.setScene(QGraphicsScene())
self.scene().addWidget(self.button1)
self.scene().addWidget(self.button2)
self.scene().addWidget(self.label)
self.scene().setSceneRect(QRectF(-150, -150, 300, 300))
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
widget = MyView()
widget.show()
sys.exit(app.exec_())

When a widget is added to a graphics scene, its proxy uses the minimumSizeHint() of the widget as the minimum size for its geometry, no matter if you resize the widget to a smaller size (I don't know if it's a bug or it's done by design).
This has the following result:
you cannot set a geometry for the proxy smaller than the minimum size [hint] of the source widget;
resizing the widget to a size smaller than the minimum [hint] will only resize the widget, but the proxy will still use that minimum size [hint];
For example, QPushButton has a minimum size hint that is about 80x30 (the actual values depend on the style and font in use), so even if you resize the button to a smaller size, its proxy will still be 80x30.
To avoid that, you can manually set the minimum size of the widget to reasonable values, or subclass the widget and override minimumSizeHint().

Related

`QPixmap` and `QLabel` size slightly increases when reloading

When I'm trying to make my app, I stumbled upon this unexpected behavior where when I re-display a new QPixmap in a QLabel. I tried to simplify the code and ended up with the code below. I also attached the video of the behavior.
I provided here a replicable example (It just needs some .jpg file in the same directory):
import sys
import os
import random
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QSizePolicy
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
class AppDemo(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(200, 200, 400, 400)
current_working_dir = os.path.abspath('')
dir_files = os.listdir(current_working_dir)
# Saving .jpg from the dir
self.picture = []
for file in dir_files:
if file.endswith(".jpg"):
self.picture.append(file)
self.label = QLabel()
self.label.setStyleSheet("border: 1px solid black;") # <- for the debugging
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding)
self.label.setPixmap(self.random_picture_selector())
button = QPushButton("Reload Picture")
button.clicked.connect(self.reload_picture)
layout = QVBoxLayout(self)
layout.addWidget(button)
layout.addWidget(self.label)
def reload_picture(self):
self.label.setPixmap(self.random_picture_selector())
def random_picture_selector(self):
rnd_picture = random.choice(self.picture)
pixmap = QPixmap(rnd_picture)
pixmap = pixmap.scaledToWidth(self.label.width(), Qt.SmoothTransformation)
# pixmap = pixmap.scaled(self.label.width(), self.label.height(), Qt.KeepAspectRatio) # <- even this is not working
return pixmap
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = AppDemo()
demo.show()
sys.exit(app.exec_())
Additional Infos:
When simplifying the code I realized that the problem disappears when I removed these following lines. (although I'm not very sure that these part of the code really causes the problem)
pixmap = pixmap.scaledToWidth(self.label.width(), Qt.SmoothTransformation)
# pixmap = pixmap.scaled(self.label.width(), self.label.height(), Qt.KeepAspectRatio) # <- even this is not working
I really have no idea what causes the problem even after looking for the Docs of QPixmap and QLabel.
The problem is caused by the stylesheet border. If you just print the pixmap and label size after setting the pixmap, you'll see that the label width is increased by 2 pixels, which is the sum of the left and right border.
You either remove the border, or you use the contentsRect():
width = self.label.contentsRect().width()
pixmap = pixmap.scaledToWidth(width, Qt.SmoothTransformation)
Read more about the Box Model in the Qt style sheet documentation.

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;

QPixmap / QPainter showing black window background

I am following an example from the PyQt5 book by Martin Fitzpatrick. When I run the following code, the background is black and the line is not drawn:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtCore import Qt
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.label = QtWidgets.QLabel()
canvas = QtGui.QPixmap(400, 300)
self.label.setPixmap(canvas)
self.setCentralWidget(self.label)
self.draw_something()
def draw_something(self):
painter = QtGui.QPainter(self.label.pixmap())
painter.drawLine(10, 10, 300, 200)
painter.end()
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
The expected result is on the left:
By default the memory that a QPixmap uses is not cleaned by efficiency so it has the bytes not altered as indicated by the docs:
QPixmap::QPixmap(int width, int height) Constructs a pixmap with the
given width and height. If either width or height is zero, a null
pixmap is constructed.
Warning: This will create a QPixmap with uninitialized data. Call
fill() to fill the pixmap with an appropriate color before drawing
onto it with QPainter.
(emphasis mine)
The solution is to use fill to set the background color:
canvas = QtGui.QPixmap(400, 300)
canvas.fill(QtGui.QColor("white"))

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_())

PyQt5: I can't understand QGraphicsScene's setSceneRect(x, y, w, h)

I see some people say if you want to put QGraphicsScene's origin of coordinates at the origin of QGraphicsView, i.e. top-left corner. You need to let both of them have the same size.
So here is what I do:
import sys
from PyQt5.QtWidgets import QApplication, QGraphicsLineItem,
QGraphicsScene, QGraphicsView
class Demo(QGraphicsView):
def __init__(self):
super(Demo, self).__init__()
self.resize(300, 300)
self.line = QGraphicsLineItem()
self.line.setLine(0, 0, 100, 100)
self.scene = QGraphicsScene()
self.scene.setSceneRect(0, 0, 300, 300)
self.scene.addItem(self.line)
self.setScene(self.scene)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
The View's size is 300x300 and I use setSceneRect() to make sure the Scene's size is 300x300.
In this case, the Scene's origin is at top-left corner.
However, when I use setSceneRect(0, 0, 150, 150), the origin is not there, but at (75, 75)!
Why? I thought the first two parameters of setSceneRect(x, y, w, h) set where the origin of coordinates should be. When the Scene is smaller than View, how can we make sure the Scene's origin is at the top-left corner?
Any help would be appreciated!
As the docs point out:
alignment : Qt::Alignment
This property holds the alignment of the
scene in the view when the whole scene is visible.
If the whole scene is visible in the view, (i.e., there are no visible
scroll bars,) the view's alignment will decide where the scene will be
rendered in the view. For example, if the alignment is
Qt::AlignCenter, which is default, the scene will be centered in the
view, and if the alignment is (Qt::AlignLeft | Qt::AlignTop), the
scene will be rendered in the top-left corner of the view.
So, by default, the scenerect is centered with the viewport of the QGraphicsView, and in the case of having the same size, the behavior you point out is observed, but in the second case the property of the centering is highlighted.
So the solution is to establish the alignment to:
import sys
from PyQt5.QtWidgets import QApplication, QGraphicsLineItem, QGraphicsScene, QGraphicsView
from PyQt5.QtCore import Qt
class Demo(QGraphicsView):
def __init__(self):
super(Demo, self).__init__()
self.resize(300, 300)
self.line = QGraphicsLineItem()
self.line.setLine(0, 0, 100, 100)
self.scene = QGraphicsScene()
self.scene.setSceneRect(0, 0, 150, 150)
self.scene.addItem(self.line)
self.setScene(self.scene)
self.setAlignment(Qt.AlignTop | Qt.AlignLeft)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
Explanation:
To understand what the scenerect is first, it must be understood that it is the QGraphicsView and the QGraphicsScene, these concepts are explained with an analogy to the recording of a movie, the QGraphicsView would be the camera, the QGraphicsScene represents what is recorded, ie the scene. The scene is delimited by the sceneRect, if the camera is very close to the scene, its limits will not be observed, but if the camera is far away, the scenerect projection in the camera will not occupy the whole screen, so it will have to be aligned in some position, in the case of QGraphicsView the alignment property is used.
why the scene is no longer centered in the view if I use setSceneRect(50, 50, 150, 150)?
To answer I use the following example where to make the scenerect visible I use a QGraphicsRectItem:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Demo(QtWidgets.QGraphicsView):
def __init__(self):
super(Demo, self).__init__()
self.resize(300, 300)
self.line = QtWidgets.QGraphicsLineItem()
self.line.setLine(0, 0, 100, 100)
self.scene = QtWidgets.QGraphicsScene()
self.scene.setSceneRect(50, 50, 150, 150)
self.scene.addItem(self.line)
rect_item = self.scene.addRect(QtCore.QRectF(50, 50, 150, 150))
rect_item.setPen(QtGui.QPen(QtGui.QColor("green")))
self.setScene(self.scene)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
As you can see the alignment is not about QRectF(0, 0, w, h) but the center of QRectF(x, y, w, h) which in this case is (100,100). So keep centered on sceneRect in the QGraphicsView.

Categories

Resources