PyQt5: Why does the default style of a QGroupBox disappear when painting? - python

My application uses a variety of QGroupBoxes arranged in a QGridLayout to display information. I would like one of them to display custom shapes that I draw. Since I want the shapes to be it's GroupBox and it's difficult and not good to get the widget's position relative to the window, I decided to subclass the GroupBox and add the paint event to the subclass. This works beautifully, however it completely eliminates the default style of the GroupBox, including the title.
The following code creates a simple window with two GroupBoxes, one using the standard class and one with the paint event in a sub class. You should see that the one on the right only has the painted rectangle and none of the GroupBox style.
If you comment out the paint event, then the GroupBox displays as usual. Why is this happening and what should I do to keep the GroupBox style? Is there another way to use the painter within a GroupBox that doesn't use a paint event in a subclass?
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class MainWindow(QWidget):
def __init__(self, *args, **kwargs):
## Set up Window layout
super(MainWindow, self).__init__(*args, **kwargs)
self.layout = QGridLayout()
self.setLayout(self.layout)
self.setGeometry(300, 50, 1000, 700)
leftGroup = QGroupBox('Left Group')
self.layout.addWidget(leftGroup, 0, 0)
rightGroup = RightGroup()
self.layout.addWidget(rightGroup, 0, 1)
class RightGroup(QGroupBox):
def __init__(self):
super(RightGroup, self).__init__('Right Group')
def paintEvent(self, event):
painter = QPainter(self)
# Paint Style
painter.setPen(QPen(Qt.black, .5, Qt.SolidLine))
painter.drawRect(QRectF(0, 0, self.width(), self.height()))
if __name__ == '__main__':
## Creates a QT Application
app = QApplication([])
## Creates a window
window = MainWindow()
## Shows the window
window.show()
app.exec_()
Window with paint event enabled
Window with paint event commented out

You are overriding the whole painting, and "overriding" means that you ignore the default behavior.
The default behavior of paintEvent() of a standard Qt widget results in painting that widget (whether it's a button, a label, a groupbox, etc.). If you just override that, that widget will never be painted, unless you call the default implementation (i.e.: super().paintEvent(event)).
For instance, consider the following:
class RightGroup(QGroupBox):
pass
which will properly show a normal group box.
Now, this:
class RightGroup(QGroupBox):
def paintEvent(self, event):
pass
will show nothing, as you're explicitly ignoring the default painting behavior. To "revert" this and also add custom drawing:
class RightGroup(QGroupBox):
def paintEvent(self, event):
super().paintEvent(event)
qp = QPainter(self)
qp.drawRect(self.rect().adjusted(0, 0, -1, -1)
Note: unless you use antialiasing and the pen width is smaller or equal than 0.5, you should always restrict the right/bottom edges of the object rectangle to an integer smaller or equal to half of the pen width. That's the reason of the adjusted(0, 0, -1, -1) above. For your above code, it would have been QRectF(0, 0, self.width() - 1, self.height() - 1)
That said, you probably want to draw inside that groupbox, so, a better solution would be to create a subclass for the contents of that groupbox, add that to the groupbox and override the paint event of that subclass instead.
class MainWindow(QWidget):
def __init__(self, *args, **kwargs):
# ...
rightGroup = QGroupBox('Right Group')
self.layout.addWidget(rightGroup, 0, 1)
rightLayout = QVBoxLayout(rightGroup)
rightLayout.addWidget(Canvas())
class Canvas(QWidget):
def __init__(self):
super().__init__()
self.path = QPainterPath()
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.path.moveTo(event.pos())
def mouseMoveEvent(self, event):
# note: here we use "buttons", not "button".
if event.buttons() == Qt.LeftButton:
self.path.lineTo(event.pos())
self.update()
def mouseReleaseEvent(self, event):
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHints(painter.Antialiasing)
painter.setPen(QPen(Qt.black, .5, Qt.SolidLine))
painter.drawRect(self.rect())
painter.drawPath(self.path)

Related

Scale only one layer of image with QGraphicsView [duplicate]

I've been looking at other StackOverflow questions regarding this error (and elsewhere on the web) but I don't understanding how the answers relate to my code. So, I'm hoping for either a fixed example that makes sense to me, or a better explanation of how and when events occur.
The code below was intended to figure out the dimensions of the screen it's running on, resize to that and draw a circle in the center that occupies most of the available screen real estate. It tried to do a lot more, but I've stripped it down -- enough, I hope. Now it just tries to draw a circle.
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class Viewport(QGraphicsView):
def __init__(self, parent=None):
super(Viewport, self).__init__(parent)
self.scene = QGraphicsScene(self)
self.setScene(self.scene)
def paintEvent(self, event):
super(Viewport, self).paintEvent(event)
qp = QPainter()
qp.begin(self)
square = QRect(10, 10, 30, 30)
qp.drawEllipse(square)
qp.end()
class UI(QDialog):
def __init__(self, parent=None):
super(UI, self).__init__(parent)
self.view = Viewport(self)
gridLayout = QGridLayout()
gridLayout.addWidget(self.view, 0, 0, 1, 1)
self.setLayout(gridLayout)
def resizeEvent(self, event):
super(UI, self).resizeEvent(event)
self.view.setFrameShape(QFrame.NoFrame)
self.view.setSceneRect(0, 0, 400, 400)
self.view.setFixedSize(400, 400)
app = QApplication(sys.argv)
ui = UI()
ui.show()
sys.exit(app.exec_())
The above was stripped out of broken code that had a moving SVG item and the circle originally had a gradient fill. The SVG item was displaying and moving okay but the circle never showed up.
The gradient-filled circle worked fine in another program when it was in a drawn by a paintEvent for a QGroupBox, but I cannot grok how QGraphicsScene and QGraphicsView work.
UPDATED
The error message, exactly as I see it (sadly w/o line numbers):
$ ./StackOverflow.py
QPainter::begin: Widget painting can only begin as a result of a paintEvent
QPainter::end: Painter not active, aborted
You need to paint on the viewport():
def paintEvent(self, event):
super(Viewport, self).paintEvent(event)
qp = QPainter()
qp.begin(self.viewport())
square = QRect(10, 10, 30, 30)
qp.drawEllipse(square)
qp.end()

QPaintEvent event rect for scrollable widget

I have a QPaintEvent override for a custom widget that has a fixed size set. This fixed size can change per instance but in this simple example, ive set it. however the PaintEvent doesn't take it into account so when the users scrolls to the right the rectangle shouldn't paint rounded corners since the widget extends past the visible viewport. How do i fix this?
Full widget painted correctly...
When i resize dialog and scroll right, you'll see rounded corners appear on the left side... when it should NOT.
They should look like this...
Code
import os
import sys
from PySide2 import QtGui, QtWidgets, QtCore, QtSvg
class Card(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Card, self).__init__(parent=parent)
self.label = QtWidgets.QLabel('Help This Paint Event Is Broken')
self.label.setFixedHeight(40)
self.label.setFixedWidth(300)
self.mainLayout = QtWidgets.QVBoxLayout(self)
self.mainLayout.addWidget(self.label)
# overrides
def paintEvent(self, event):
painter = QtGui.QPainter()
painter.begin(self)
painter.setOpacity(1.0)
painter.setRenderHints(QtGui.QPainter.Antialiasing)
painter.setPen(QtGui.QColor(0, 0, 0, 128))
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(QtGui.QColor('#F44336'))
painter.drawRoundedRect(event.rect(), 12, 12)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.end()
class ListViewExample(QtWidgets.QWidget):
def __init__(self, parent=None):
super(ListViewExample, self).__init__(parent)
self.resize(200,200)
self.listView = QtWidgets.QListWidget()
self.listView.setSpacing(10)
self.listView.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.listView.verticalScrollBar().setSingleStep(10)
# layout
self.mainLayout = QtWidgets.QVBoxLayout()
self.mainLayout.setContentsMargins(0,0,0,0)
self.mainLayout.addWidget(self.listView)
self.setLayout(self.mainLayout)
for x in range(50):
wgt = Card()
self.appendItem(wgt)
def appendItem(self, widget):
lwi = QtWidgets.QListWidgetItem()
lwi.setSizeHint(widget.sizeHint())
self.listView.addItem(lwi)
self.listView.setItemWidget(lwi, widget)
################################################################################
# Widgets
################################################################################
def unitTest_CardDelegate():
app = QtWidgets.QApplication(sys.argv)
window = ListViewExample()
window.show()
app.exec_()
if __name__ == '__main__':
pass
unitTest_CardDelegate()
QPaintEvent::rect() returns the visible rectangle, not the rectangle of the widget itself, so you observe this behavior. The solution is:
painter.drawRoundedRect(self.rect(), 12, 12)

With PyQt5, how to add drop shadow effect on the rectangle area of a layout

I'm trying to implement a grid of images with titles under them, with a drop shadow on hover. What I've done so far is to add a drop shadow on the two widgets (the label with the image, and the label with the title), but I would like to have a drop shadow on the rectangular area that contains them. It tried to put them on another widget and apply the effect on this widget, but it still applies to both labels. Code below.
import sys, os
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class App(QMainWindow):
def __init__(self):
super().__init__()
self.title = 'PyQt5 layout - pythonspot.com'
self.left = 100
self.top = 100
self.width = 800
self.height = 600
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
content_widget = QWidget()
self.setCentralWidget(content_widget)
self._lay = QGridLayout(content_widget)
self.shadow = QGraphicsDropShadowEffect(self)
self.shadow.setBlurRadius(5)
nb = 6
i = 0
for i in range(0, 12):
panel=QWidget()
vbox = QVBoxLayout()
pixmap = QPixmap(str(i+1)+"jpg")
pixmap = pixmap.scaled(100, 150, transformMode=Qt.SmoothTransformation)
img_label = QLabel(pixmap=pixmap)
vbox.addWidget(img_label)
txt_label = QLabel(str(i+1))
vbox.addWidget(txt_label)
vbox.addStretch(1)
panel.setLayout(vbox)
self._lay.addWidget(panel , int(i/nb), i%nb)
panel.installEventFilter(self)
i = i+1
self.show()
def eventFilter(self, object, event):
if event.type() == QEvent.Enter:
object.setGraphicsEffect(self.shadow)
self.shadow.setEnabled(True)
elif event.type() == QEvent.Leave:
print("Mouse is not over the label")
self.shadow.setEnabled(False)
return False
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
The problem comes from the fact that a plain QWidget usually doesn't paint anything on its own: it's just a transparent widget. If you apply a graphics effect, it will be the result of what's inside that widget.
The solution is to ensure that the widget is opaque, by calling setAutoFillBackground(True).
Unfortunately, especially in your case, the result won't be very good, because you've lots of other widgets and a certain amount of spacing between them. You'll end up having the shadow behind everything:
The solution would be to call raise_() whenever the graphics effect is set, in order to ensure that the widget is actually above anything else (among the siblings and subchildren of its parent, obviously).
Unfortunately - again - this has a small but important issue, related to your implementation: the first time the effect is removed from a widget because it's set on another, the surrounding widgets don't get updated correctly.
This is mostly due to the optimizations of the paint engine and the implementation of the graphics effect.
To avoid this issue, there are two possibilities:
set an unique graphics effect for each widget, disabled upon creation, and then enable it only on the enterEvent:
class App(QMainWindow):
def __init__(self):
# ...
for i in range(0, 12):
panel = QWidget()
effect = QGraphicsDropShadowEffect(panel, enabled=False, blurRadius=5)
panel.setGraphicsEffect(panel)
# ...
def eventFilter(self, obj, event):
if event.type() == QEvent.Enter and obj.graphicsEffect():
obj.graphicsEffect().setEnabled(True)
elif event.type() == QEvent.Leave and obj.graphicsEffect():
obj.graphicsEffect().setEnabled(False)
return super().eventFilter(obj, event)
alternatively, get the bounding rect of the graphics effect, translate it to the coordinates of the widget, check if any other "sibling" geometry intersects the bounding rect and eventually call update() on those widgets:
class App(QMainWindow):
def __init__(self):
# ...
self.panels = []
for i in range(0, 12):
panel = QWidget()
self.panels.append(panel)
# ...
def eventFilter(self, obj, event):
if event.type() == QEvent.Enter:
obj.setGraphicsEffect(self.shadow)
self.shadow.setEnabled(True)
obj.raise_()
elif event.type() == QEvent.Leave:
obj.graphicsEffect().setEnabled(False)
rect = self.shadow.boundingRect().toRect()
rect.translate(obj.geometry().topLeft())
for other in self.panels:
if other != obj and other.geometry().intersects(rect):
other.update()
return super().eventFilter(obj, event)
PS: object is a built-in type of Python, you should not use it as a variable.

Qt.ScrollBarAsNeeded not showing scrollbar when it's actually needed

I'm implementing a python application using PyQt5 and I encountered some problems when making use of a QScrollArea. This is the layout of my application:
It's composed of 2 QScrollArea (left and right pane) and a QMdiArea (center widget) arranged into a QHBoxLayout. When I expand the widgets on the left pane by clicking on the controls, and the height of the QWidget of the QScrollArea is bigger than then height of the QScrollArea itself, the scrollbar appears (as expected), but it's overlapping the content of the QScrollArea. To fix this problem I reimplemented the resizeEvent adding the necessary space for the scrollbar (till this point everything works.
Now, when I manually resize the Main Window, the left Pane gets more space and the scrollbar should disappear (but it doesn't) and it overlaps the widgets of the pane:
I also tried to manually toggle the visibility of the scrollbar (when the resizeEvent is received): when I do this, I can successfully hide the scrollbar but then I can't show it again (not matter if I call setVisible(True) on the scrollbar). This results in the space for the scrollbar being added, but the scrollbar is missing and the content of the pane is not scrollable:
Here is the implementation of the pane widget:
class Pane(QScrollArea):
MinWidth = 186
def __init__(self, alignment=0, parent=None):
super().__init__(parent)
self.mainWidget = QWidget(self)
self.mainLayout = QVBoxLayout(self.mainWidget)
self.mainLayout.setAlignment(alignment)
self.mainLayout.setContentsMargins(0, 0, 0, 0)
self.mainLayout.setSpacing(0)
self.setContentsMargins(0, 0, 0, 0)
self.setFrameStyle(QFrame.NoFrame)
self.setFixedWidth(Pane.MinWidth)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Ignored)
self.setWidgetResizable(True)
self.setWidget(self.mainWidget)
def resizeEvent(self, resizeEvent):
if self.viewport().height() < self.widget().height():
self.setFixedWidth(Pane.MinWidth + 18)
# THIS DOESN'T WORK
#self.verticalScrollBar().show()
else:
self.setFixedWidth(Pane.MinWidth)
#self.verticalScrollBar().hide()
def addWidget(self, widget):
self.mainLayout.addWidget(widget)
def removeWidget(self, widget):
self.mainLayout.removeWidget(widget)
def update(self, *__args):
for item in itemsInLayout(self.mainLayout):
item.widget().update()
super().update(*__args)
What I want to achieve is pretty simple (but practically it seems not as simple): I would like to dynamically show the vertical scrollbar on my left/right pane widgets only when it's needed, and add the necessary space for the scrollbar so it doesn't overlap the widgets in the QScrollArea.
Before someone asks, I already tried to do something like this:
def resizeEvent(self, resizeEvent):
if self.viewport().height() < self.widget().height():
self.setFixedWidth(Pane.MinWidth + 18)
scrollbar = self.verticalScrollbar()
scrollbar.setVisible(True)
self.setVerticalScrollBar(scrollbar) ## APP CRASH
else:
self.setFixedWidth(Pane.MinWidth)
#self.verticalScrollBar().hide()
which results in my application to crash.
I hope that someone already faced this issue and is able to help me.
EDIT: I'm using PyQt5.5 compiled against Qt5.5 under OSX Yosemite 10.10.4 using clang.
Everything seems to work as expected for me without any need for workarounds. However, I strongly suspect there are additional constraints in your real code that you have not revealed in your question.
UPDATE
Below is a simple example that resizes the scrollareas when the scrollbars are shown/hidden:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QMainWindow):
def __init__(self):
super(Window, self).__init__()
widget = QtWidgets.QWidget(self)
layout = QtWidgets.QHBoxLayout(widget)
self.mdi = QtWidgets.QMdiArea(self)
self.leftScroll = Pane(
QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft, self)
self.rightScroll = Pane(
QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft, self)
layout.addWidget(self.leftScroll)
layout.addWidget(self.mdi)
layout.addWidget(self.rightScroll)
self.setCentralWidget(widget)
for scroll in self.leftScroll, self.rightScroll:
for index in range(4):
widget = QtWidgets.QTextEdit()
widget.setText('one two three four five')
scroll.addWidget(widget)
class Pane(QtWidgets.QScrollArea):
MinWidth = 186
def __init__(self, alignment=0, parent=None):
super().__init__(parent)
self.mainWidget = QtWidgets.QWidget(self)
self.mainLayout = QtWidgets.QVBoxLayout(self.mainWidget)
self.mainLayout.setAlignment(alignment)
self.mainLayout.setContentsMargins(0, 0, 0, 0)
self.mainLayout.setSpacing(0)
self.setContentsMargins(0, 0, 0, 0)
self.setFrameStyle(QtWidgets.QFrame.NoFrame)
self.setFixedWidth(Pane.MinWidth)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
self.setSizePolicy(QtWidgets.QSizePolicy.Maximum,
QtWidgets.QSizePolicy.Ignored)
self.setWidgetResizable(True)
self.setWidget(self.mainWidget)
self.verticalScrollBar().installEventFilter(self)
def addWidget(self, widget):
self.mainLayout.addWidget(widget)
def removeWidget(self, widget):
self.mainLayout.removeWidget(widget)
def eventFilter(self, source, event):
if isinstance(source, QtWidgets.QScrollBar):
if event.type() == QtCore.QEvent.Show:
self.setFixedWidth(Pane.MinWidth + source.width())
elif event.type() == QtCore.QEvent.Hide:
self.setFixedWidth(Pane.MinWidth)
return super(Pane, self).eventFilter(source, event)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 800, 300)
window.show()
sys.exit(app.exec_())

How to add a second pixmap to an abstract button when pushed in PySide/PyQt

I'm very new to PySide/PyQt environment. I'm trying to make a menu of buttons on top and assign a task to each so that when they are clicked a function draws a painting on the central window. But I also want to make the button change when they are clicked.
I think this might be an straighforward problem to solve if I use QPushButton, but my buttons are images and I'm using the method suggested HERE and use QAbstractButton to create them.
It is mentioned there that
You can add second pixmap and draw it only when the mouse pointer is
hover over button.
And I'm trying to do exactly that. My question is this:
what are possible ways to achieve this? Are the same methods in QPushButtons applicable here? If so, are there any examples of it somewhere?
Here is a snippet of my code:
import sys
from PySide import QtGui, QtCore
BACKGROUND_COLOR = '#808080'
ICON_PATH_ACTIVE = 'icons/activ'
ICON_PATH_PASSIVE = 'icons/pasiv'
class MainWindow(QtGui.QMainWindow):
def __init__(self, app=None):
super(MainWindow, self).__init__()
self.initUI()
def initUI(self):
dockwidget = QtGui.QWidget()
self.setGeometry(200, 200, 400, 300)
hbox = QtGui.QHBoxLayout()
1_button = PicButton(QtGui.QPixmap("icons/pasiv/1.png"))
2_button = PicButton(QtGui.QPixmap("icons/pasiv/2.png"))
3_button = PicButton(QtGui.QPixmap("icons/pasiv/3.png"))
hbox.addWidget(1_button)
hbox.addWidget(2_button)
hbox.addWidget(3_button)
vbox = QtGui.QVBoxLayout()
vbox.addLayout(hbox)
vbox.setAlignment(hbox, QtCore.Qt.AlignTop)
dockwidget.setLayout(vbox)
self.setCentralWidget(dockwidget)
class PicButton(QtGui.QAbstractButton):
def __init__(self, pixmap, parent=None):
super(PicButton, self).__init__(parent)
self.pixmap = pixmap
self.setFixedSize(100, 100)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.drawPixmap(event.rect(), self.pixmap)
def main():
app = QtGui.QApplication(sys.argv)
central = MainWindow()
central.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Thank you.
Use a regular QPushButton with an icon.
iplay = QtGui.QIcon("path/play_icon.png")
ipause = QtGui.QIcon("path/pause_icon.png")
btn = QtGui.QPushButton(ipause, "", None)
def toggle_play():
if btn.icon() == ipause:
btn.setIcon(iplay)
# Do Pause Action
else:
btn.setIcon(ipause)
# Do Play Action
btn.clicked.connect(toggle_play)
btn.show()
If you want hover functionality then you will have to subclass the QPushButton
class MyButton(QtGui.QPushButton):
custom_click_signal = QtCore.Signal()
def enterEvent(self, event):
super().enterEvent(event)
# Change icon hove image here
def leaveEvent(self, event):
super().leaveEvent(event)
# Change icon back to original image here.
def mousePressEvent(self, event):
super().mousePressEvent(event)
self.custom_click_signal.emit()
# connect to signal btn.custom_click_signal.connect(method)
Icons are probably the easiest way instead of manually managing the paint event. There are also mousePressEvent and mouseReleaseEvents if you want the icon to change for someone holding the button down.

Categories

Resources