I am trying to override the paintEvent() of QMenu to make it have rounded corners.
The context menu should look something like this.
Here is the code I have tried But nothing appears:
from PyQt5 import QtWidgets, QtGui, QtCore
import sys
class Example(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Context menu')
self.show()
def contextMenuEvent(self, event):
cmenu = AddContextMenu(self)
newAct = cmenu.addAction("New")
openAct = cmenu.addAction("Open")
quitAct = cmenu.addAction("Quit")
action = cmenu.exec_(self.mapToGlobal(event.pos()))
class AddContextMenu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super(AddContextMenu, self).__init__()
self.painter = QtGui.QPainter(self)
self.setMinimumSize(150, 200)
self.pen = QtGui.QPen(QtCore.Qt.red)
#self.setStyleSheet('color:white; background:gray; border-radius:4px; border:2px solid white;')
def paintEvent(self, event) -> None:
self.pen.setWidth(2)
self.painter.setPen(self.pen)
self.painter.setBrush(QtGui.QBrush(QtCore.Qt.blue))
self.painter.drawRoundedRect(10, 10, 100, 100, 4.0, 4.0)
self.update()
#self.repaint()
#super(AddContextMenu, self).paintEvent(event)
def main():
app = QtWidgets.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Note: setting a style sheet doesn't work for me:
this is what I get when using the style sheet It isn't completely rounded.
This is the paintEvent after #musicamante suggestion(This is just for him/her to check)
def paintEvent(self, event) -> None:
painter = QtGui.QPainter(self)
#self.pen.setColor(QtCore.Qt.white)
#painter.setFont(QtGui.QFont("times", 22))
#painter.setPen(self.pen)
#painter.drawText(QtCore.QPointF(0, 0), 'Hello')
self.pen.setColor(QtCore.Qt.red)
painter.setPen(self.pen)
painter.setBrush(QtCore.Qt.gray)
painter.drawRoundedRect(self.rect(), 20.0, 20.0)
and in the init()
self.pen = QtGui.QPen(QtCore.Qt.red)
self.pen.setWidth(2)
I cannot comment on the paintEvent functionality, but it is possible to implement rounded corners using style-sheets. Some qmenu attributes have to be modified in order to disable the default rectangle in the background, which gave you the unwanted result.
Here is a modified version of your Example using style-sheets + custom flags (no frame + transparent background):
from PyQt5 import QtWidgets, QtCore
import sys
class Example(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Context menu')
self.show()
def contextMenuEvent(self, event):
cmenu = QtWidgets.QMenu()
# disable default frame and background
cmenu.setWindowFlags(QtCore.Qt.FramelessWindowHint)
cmenu.setAttribute(QtCore.Qt.WA_TranslucentBackground)
# set stylesheet, add some padding to avoid overlap of selection with rounded corner
cmenu.setStyleSheet("""
QMenu{
background-color: rgb(255, 255, 255);
border-radius: 20px;
}
QMenu::item {
background-color: transparent;
padding:3px 20px;
margin:5px 10px;
}
QMenu::item:selected { background-color: gray; }
""")
newAct = cmenu.addAction("New")
openAct = cmenu.addAction("Open")
quitAct = cmenu.addAction("Quit")
action = cmenu.exec_(self.mapToGlobal(event.pos()))
def main():
app = QtWidgets.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Setting the border radius in the stylesheet for a top level widget (a widget that has its own "window") is not enough.
While the solution proposed by Christian Karcher is fine, two important considerations are required:
The system must support compositing; while this is true for most modern OSes, at least on Linux there is the possibility that even an up-to-date system does not support it by choice (I disabled on my computer); if that's the case, setting the WA_TranslucentBackground attribute will not work.
The FramelessWindowHint should not be set on Linux, as it may lead to problems with the window manager, so it should be set only after ensuring that the OS requires it (Windows).
In light of that, using setMask() is the correct fix whenever compositing is not supported, and this has to happen within the resizeEvent(). Do note that masking is bitmap based, and antialiasing is not supported, so rounded borders are sometimes a bit ugly depending on the border radius.
Also, since you want custom colors, using stylesheets is mandatory, as custom painting of a QMenu is really hard to achieve.
class AddContextMenu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super(AddContextMenu, self).__init__()
self.setMinimumSize(150, 200)
self.radius = 4
self.setStyleSheet('''
QMenu {{
background: blue;
border: 2px solid red;
border-radius: {radius}px;
}}
QMenu::item {{
color: white;
}}
QMenu::item:selected {{
color: red;
}}
'''.format(radius=self.radius))
def resizeEvent(self, event):
path = QtGui.QPainterPath()
# the rectangle must be translated and adjusted by 1 pixel in order to
# correctly map the rounded shape
rect = QtCore.QRectF(self.rect()).adjusted(.5, .5, -1.5, -1.5)
path.addRoundedRect(rect, self.radius, self.radius)
# QRegion is bitmap based, so the returned QPolygonF (which uses float
# values must be transformed to an integer based QPolygon
region = QtGui.QRegion(path.toFillPolygon(QtGui.QTransform()).toPolygon())
self.setMask(region)
Some side notes about your paintEvent implementation, not necessary in this specific case for the above reason, but still important (some points are related to portions of code that have been commented, but the fact that you tried them makes worth mentioning those aspects):
The QPainter used for a widget must never be instanciated outside a paintEvent(): creating the instance in the __init__ as you did is a serious error and might even lead to crash. The painter can only be created when the paintEvent is received, and shall never be reused. This clearly makes useless to set it as an instance attribute (self.painter), since there's no actual reason to access it after the paint event.
If the pen width is always the same, then just set it in the constructor (self.pen = QtGui.QPen(QtCore.Qt.red, 2)), continuously setting it in the paintEvent is useless.
QPen and QBrush can directly accept Qt global colors, so there's no need to create a QBrush instance as the painter will automatically (internally and fastly) set it: self.painter.setBrush(QtCore.Qt.blue).
self.update() should never be called within a paintEvent (and not even self.repaint() should). The result in undefined and possibly dangerous.
If you do some manual painting with a QPainter and then call the super paintEvent, the result is most likely that everything painted before will be hidden; as a general rule, the base implementation should be called first, then any other custom painting should happen after (in this case it obviously won't work, as you'll be painting a filled rounded rect, making the menu items invisible).
I have implemented round corners menu using QListWidget and QWidget. You can download the code in https://github.com/zhiyiYo/PyQt-Fluent-Widgets/blob/master/examples/menu/demo.py.
Related
I am trying to override the paintEvent() of QMenu to make it have rounded corners.
The context menu should look something like this.
Here is the code I have tried But nothing appears:
from PyQt5 import QtWidgets, QtGui, QtCore
import sys
class Example(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Context menu')
self.show()
def contextMenuEvent(self, event):
cmenu = AddContextMenu(self)
newAct = cmenu.addAction("New")
openAct = cmenu.addAction("Open")
quitAct = cmenu.addAction("Quit")
action = cmenu.exec_(self.mapToGlobal(event.pos()))
class AddContextMenu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super(AddContextMenu, self).__init__()
self.painter = QtGui.QPainter(self)
self.setMinimumSize(150, 200)
self.pen = QtGui.QPen(QtCore.Qt.red)
#self.setStyleSheet('color:white; background:gray; border-radius:4px; border:2px solid white;')
def paintEvent(self, event) -> None:
self.pen.setWidth(2)
self.painter.setPen(self.pen)
self.painter.setBrush(QtGui.QBrush(QtCore.Qt.blue))
self.painter.drawRoundedRect(10, 10, 100, 100, 4.0, 4.0)
self.update()
#self.repaint()
#super(AddContextMenu, self).paintEvent(event)
def main():
app = QtWidgets.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Note: setting a style sheet doesn't work for me:
this is what I get when using the style sheet It isn't completely rounded.
This is the paintEvent after #musicamante suggestion(This is just for him/her to check)
def paintEvent(self, event) -> None:
painter = QtGui.QPainter(self)
#self.pen.setColor(QtCore.Qt.white)
#painter.setFont(QtGui.QFont("times", 22))
#painter.setPen(self.pen)
#painter.drawText(QtCore.QPointF(0, 0), 'Hello')
self.pen.setColor(QtCore.Qt.red)
painter.setPen(self.pen)
painter.setBrush(QtCore.Qt.gray)
painter.drawRoundedRect(self.rect(), 20.0, 20.0)
and in the init()
self.pen = QtGui.QPen(QtCore.Qt.red)
self.pen.setWidth(2)
I cannot comment on the paintEvent functionality, but it is possible to implement rounded corners using style-sheets. Some qmenu attributes have to be modified in order to disable the default rectangle in the background, which gave you the unwanted result.
Here is a modified version of your Example using style-sheets + custom flags (no frame + transparent background):
from PyQt5 import QtWidgets, QtCore
import sys
class Example(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Context menu')
self.show()
def contextMenuEvent(self, event):
cmenu = QtWidgets.QMenu()
# disable default frame and background
cmenu.setWindowFlags(QtCore.Qt.FramelessWindowHint)
cmenu.setAttribute(QtCore.Qt.WA_TranslucentBackground)
# set stylesheet, add some padding to avoid overlap of selection with rounded corner
cmenu.setStyleSheet("""
QMenu{
background-color: rgb(255, 255, 255);
border-radius: 20px;
}
QMenu::item {
background-color: transparent;
padding:3px 20px;
margin:5px 10px;
}
QMenu::item:selected { background-color: gray; }
""")
newAct = cmenu.addAction("New")
openAct = cmenu.addAction("Open")
quitAct = cmenu.addAction("Quit")
action = cmenu.exec_(self.mapToGlobal(event.pos()))
def main():
app = QtWidgets.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Setting the border radius in the stylesheet for a top level widget (a widget that has its own "window") is not enough.
While the solution proposed by Christian Karcher is fine, two important considerations are required:
The system must support compositing; while this is true for most modern OSes, at least on Linux there is the possibility that even an up-to-date system does not support it by choice (I disabled on my computer); if that's the case, setting the WA_TranslucentBackground attribute will not work.
The FramelessWindowHint should not be set on Linux, as it may lead to problems with the window manager, so it should be set only after ensuring that the OS requires it (Windows).
In light of that, using setMask() is the correct fix whenever compositing is not supported, and this has to happen within the resizeEvent(). Do note that masking is bitmap based, and antialiasing is not supported, so rounded borders are sometimes a bit ugly depending on the border radius.
Also, since you want custom colors, using stylesheets is mandatory, as custom painting of a QMenu is really hard to achieve.
class AddContextMenu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super(AddContextMenu, self).__init__()
self.setMinimumSize(150, 200)
self.radius = 4
self.setStyleSheet('''
QMenu {{
background: blue;
border: 2px solid red;
border-radius: {radius}px;
}}
QMenu::item {{
color: white;
}}
QMenu::item:selected {{
color: red;
}}
'''.format(radius=self.radius))
def resizeEvent(self, event):
path = QtGui.QPainterPath()
# the rectangle must be translated and adjusted by 1 pixel in order to
# correctly map the rounded shape
rect = QtCore.QRectF(self.rect()).adjusted(.5, .5, -1.5, -1.5)
path.addRoundedRect(rect, self.radius, self.radius)
# QRegion is bitmap based, so the returned QPolygonF (which uses float
# values must be transformed to an integer based QPolygon
region = QtGui.QRegion(path.toFillPolygon(QtGui.QTransform()).toPolygon())
self.setMask(region)
Some side notes about your paintEvent implementation, not necessary in this specific case for the above reason, but still important (some points are related to portions of code that have been commented, but the fact that you tried them makes worth mentioning those aspects):
The QPainter used for a widget must never be instanciated outside a paintEvent(): creating the instance in the __init__ as you did is a serious error and might even lead to crash. The painter can only be created when the paintEvent is received, and shall never be reused. This clearly makes useless to set it as an instance attribute (self.painter), since there's no actual reason to access it after the paint event.
If the pen width is always the same, then just set it in the constructor (self.pen = QtGui.QPen(QtCore.Qt.red, 2)), continuously setting it in the paintEvent is useless.
QPen and QBrush can directly accept Qt global colors, so there's no need to create a QBrush instance as the painter will automatically (internally and fastly) set it: self.painter.setBrush(QtCore.Qt.blue).
self.update() should never be called within a paintEvent (and not even self.repaint() should). The result in undefined and possibly dangerous.
If you do some manual painting with a QPainter and then call the super paintEvent, the result is most likely that everything painted before will be hidden; as a general rule, the base implementation should be called first, then any other custom painting should happen after (in this case it obviously won't work, as you'll be painting a filled rounded rect, making the menu items invisible).
I have implemented round corners menu using QListWidget and QWidget. You can download the code in https://github.com/zhiyiYo/PyQt-Fluent-Widgets/blob/master/examples/menu/demo.py.
I've made a custom StyleItemDelegate and for some reason when it paints the checkbox indicator, it doesn't match what is defined in my Stylesheet. How can I fix this? You can see on the right side of the application that the StyleSheet properly affects how the checkbox is displayed in the default listview paint event.
Update #1
I've made a custom style item delegate to support rich text html rendering which all works great. I need to re-implement the checkbox since i've overwritten the paint event and ensure the checkbox is still available. However my text is overlapping the checkbox making it un useable. As a result when trying to paint the checkbox indicator, the highlighting of the ListItem is broken and only shows a slim blue strip on the left side.
Screenshot
Code
################################################################################
# imports
################################################################################
import os
import sys
from PySide2 import QtGui, QtWidgets, QtCore
################################################################################
# QStyledItemDelegate
################################################################################
class MyDelegate(QtWidgets.QStyledItemDelegate):
MARGINS = 10
def __init__(self, parent=None, *args):
QtWidgets.QStyledItemDelegate.__init__(self, parent, *args)
# overrides
def sizeHint(self, option, index):
'''
Description:
Since labels are stacked we will take whichever is the widest
'''
options = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(options, index)
# draw rich text
doc = QtGui.QTextDocument()
doc.setHtml(index.data(QtCore.Qt.DisplayRole))
doc.setDocumentMargin(self.MARGINS)
doc.setDefaultFont(options.font)
doc.setTextWidth(option.rect.width())
return QtCore.QSize(doc.idealWidth(), doc.size().height())
# methods
def paint(self, painter, option, index):
painter.save()
painter.setClipping(True)
painter.setClipRect(option.rect)
opts = QtWidgets.QStyleOptionViewItem(option)
self.initStyleOption(opts, index)
style = QtGui.QApplication.style() if opts.widget is None else opts.widget.style()
# Draw background
if option.state & QtWidgets.QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight().color())
else:
painter.fillRect(option.rect, QtGui.QBrush(QtCore.Qt.NoBrush))
# Draw checkbox
if (index.flags() & QtCore.Qt.ItemIsUserCheckable):
cbStyleOption = QtWidgets.QStyleOptionButton()
if index.data(QtCore.Qt.CheckStateRole):
cbStyleOption.state |= QtWidgets.QStyle.State_On
else:
cbStyleOption.state |= QtWidgets.QStyle.State_Off
cbStyleOption.state |= QtWidgets.QStyle.State_Enabled
cbStyleOption.rect = option.rect.translated(self.MARGINS, 0)
style.drawControl(QtWidgets.QStyle.CE_CheckBox, cbStyleOption, painter, option.widget)
# Draw Title
doc = QtGui.QTextDocument()
doc.setHtml(index.data(QtCore.Qt.DisplayRole))
doc.setTextWidth(option.rect.width())
doc.setDocumentMargin(self.MARGINS)
doc.setDefaultFont(opts.font)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
# highlight text
if option.state & QtWidgets.QStyle.State_Selected:
ctx.palette.setColor(option.palette.Text, option.palette.color(option.palette.Active, option.palette.HighlightedText))
else:
ctx.palette.setColor(option.palette.Text, option.palette.color(option.palette.Active, option.palette.Text))
textRect = style.subElementRect(QtWidgets.QStyle.SE_ItemViewItemText, option)
painter.translate(textRect.topLeft())
painter.setClipRect(textRect.translated(-textRect.topLeft()))
doc.documentLayout().draw(painter, ctx)
# end
painter.restore()
################################################################################
# Widgets
################################################################################
class ListViewExample(QtWidgets.QWidget):
'''
Description:
Extension of listview which supports searching
'''
def __init__(self, parent=None):
super(ListViewExample, self).__init__(parent)
self.setStyleSheet('''
QListView {
color: rgb(255,255,255);
background-color: rgb(60,60,60);
}
QCheckBox, QCheckBox:disabled {
background: transparent;
}
QWidget::indicator {
width: 12px;
height: 12px;
border: 2px solid rgb(90,90,90);
border-radius: 3px;
background: rgb(30,30,30);
}
QWidget::indicator:checked {
border: 2px solid rgb(76,175,80);
background: rgb(0,255,40);
}
''')
self.itemModel = QtGui.QStandardItemModel()
self.checkbox = QtWidgets.QCheckBox('Sample')
self.listView = QtWidgets.QListView()
self.listView.setIconSize(QtCore.QSize(128,128))
self.listView.setModel(self.itemModel)
self.listView.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.checkboxA = QtWidgets.QCheckBox('Sample')
self.listViewA = QtWidgets.QListView()
self.listViewA.setIconSize(QtCore.QSize(128,128))
self.listViewA.setModel(self.itemModel)
self.listViewA.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
# layout
self.mainLayout = QtWidgets.QGridLayout()
self.mainLayout.addWidget(self.checkbox,0,0)
self.mainLayout.addWidget(self.listView,1,0)
self.mainLayout.addWidget(self.checkboxA,0,1)
self.mainLayout.addWidget(self.listViewA,1,1)
self.setLayout(self.mainLayout)
################################################################################
# Widgets
################################################################################
def main():
app = QtWidgets.QApplication(sys.argv)
window = ListViewExample()
window.resize(600,400)
window.listView.setItemDelegate(MyDelegate())
window.itemModel.clear()
for i in range(10):
html = '''
<span style="font-size:12px;">
<b> Player <span>•</span> #{}</b>
</span>
<br>
<span style="font-size:11px;">
<b>Status:</b> <span style='color:rgb(255,0,0);'>⬤</span> Active
<br>
<b>Position:</b> WR
<br>
<b>Team:</b> <span style='color:rgb(0,128,255);'>█</span> Wings
</span>
'''.format(i)
item = QtGui.QStandardItem()
item.setData(html, QtCore.Qt.DisplayRole)
item.setCheckable(True)
window.itemModel.appendRow(item)
window.show()
app.exec_()
if __name__ == '__main__':
pass
main()
QStyle painting functions assume that you're using the default behavior of that style for the control/primitive. This means that, in most cases, the last widget argument can be ignored, since no overriding is being considered.
When using stylesheets, instead, things change. If the widget or any of its parents (including the QApplication) has a style sheet, the widget will use an internal QStyleSheetStyle, inheriting the behavior of the QApplication style, and overridden by any style sheet set for the parents.
In this case, that argument becomes mandatory, as the underlying QStyleSheetStyle will need to check if the widget has (or inherits) a stylesheet and eventually "climb" the widget tree in order to know if and how any custom styling has been set for it.
You just need to add that to the arguments of the function:
style.drawControl(QtWidgets.QStyle.CE_CheckBox, cbStyleOption, painter,
option.widget)
^^^^^^^^^^^^^
The above solves the issue of the styling, but not of the drawing.
While the appearance of a check box in an item view is usually the same of a QCheckBox (and the ::indicator pseudo selector can be used for both), they are actually painted with different functions by the style.
The problem of your implementation is that you're drawing with drawControl and CE_CheckBox, but for item views you must use drawPrimitive and PE_IndicatorItemViewItemCheck. Also, since you're just translating the original option rect, the result is that drawControl will paint over a rectangle that is big as the whole item rectangle (thus painting over the selection).
The proper solution is to create a new QStyleOptionViewItem based on the existing one, and use the rectangle returned by subElementRect with SE_ItemViewItemCheckIndicator.
def paint(self, painter, option, index):
if (index.flags() & QtCore.Qt.ItemIsUserCheckable):
cbStyleOption = QtWidgets.QStyleOptionViewItem(opts)
if index.data(QtCore.Qt.CheckStateRole):
cbStyleOption.state |= QtWidgets.QStyle.State_On
else:
cbStyleOption.state |= QtWidgets.QStyle.State_Off
cbStyleOption.state |= QtWidgets.QStyle.State_Enabled
cbStyleOption.rect = style.subElementRect(
style.SE_ItemViewItemCheckIndicator, opts, opts.widget)
style.drawPrimitive(style.PE_IndicatorItemViewItemCheck,
cbStyleOption, painter, opts.widget)
Note that you should not use translation in the painting, as that would make it inconsistent with the mouse interaction.
To translate the element, use the top and left properties in the stylesheet instead:
QWidget::indicator {
left: 10px;
...
}
Also note that you're getting the text rectangle using the original option, which is not initialized, so that could return an invalid rectangle; you should then use the actually initialized option and also use the widget argument as explained above:
textRect = style.subElementRect(QtWidgets.QStyle.SE_ItemViewItemText,
opts, opts.widget)
I want to change the color of selected item in QListItem, and I find qss may be a solution. And the code is:
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import sys
class Window(QWidget):
def __init__(self):
super().__init__()
with open('mainStyle.qss', 'r', encoding='utf-8') as file:
self.setStyleSheet(file.read())
# self.setStyleSheet('*{font-size: 15px;background-color: rgb(150, 150, 150);}'
# 'QListWidget::item:selected{background: rgb(128,128,255);}')
self.setStyleSheet('QListWidget::item:selected{background: rgb(128,128,255);}')
layout = QVBoxLayout()
self.setLayout(layout)
listWidget = QListWidget()
layout.addWidget(listWidget)
w1 = QWidget()
w1Item = QListWidgetItem()
w1Item.setSizeHint(QSize(150, 150))
listWidget.insertItem(0, w1Item)
listWidget.setItemWidget(w1Item, w1)
w2 = QWidget()
w2Item = QListWidgetItem()
w2Item.setSizeHint(QSize(150, 150))
listWidget.insertItem(1, w2Item)
listWidget.setItemWidget(w2Item, w2)
if __name__ == '__main__':
app = QApplication(sys.argv)
win = Window()
win.show()
app.exec_()
We can see that the color is changed to be blue when the item is selected.
However, I need to provide a general background color for other widgets. So I change the style from
self.setStyleSheet('QListWidget::item:selected{background: rgb(0,0,255);}')
to
self.setStyleSheet('*{font-size: 15px;background-color: rgb(150, 150, 150);}'
'QListWidget::item:selected{background: rgb(0,0,0);}')
Then, I find QListWidget::item:selected do not work. The color do not change when I select a item.
What's the wrong with my code?
The problem is that you're setting a QWidget for the item, and since you're using a universal selector (with the wildcard), the result is that all QWidget will have that background color, including those added as item widgets for the list view.
The color you used for the :selected pseudo is only valid for the item painted by the view, since the item widget has its own background set from the universal selector, that background won't be visible.
The solution is to use a proper selector combination ensuring that the rule only matches children of the list view that have a usable selector, and set a transparent color for those widgets.
A possibility is to set a custom property for the widgets that must be set before adding the widgets to the list (otherwise, you need to set the stylesheet after adding them, or request a style.unpolish()).
self.setStyleSheet('''
QWidget {
font-size: 15px;
background: rgb(150, 150, 150);
}
QListWidget::item:selected {
background: rgb(128,128,255);
}
QListWidget QWidget[widgetItem=true] {
background: transparent;
}
''')
# ...
w1 = QWidget()
w1.setProperty('widgetItem', True)
# ...
w2 = QWidget()
w2.setProperty('widgetItem', True)
# ...
Another way to do so is to use an "empty" subclass for widgets that are going to be added to the item view:
class CustomItemWidget(QWidget): pass
class Window(QWidget):
def __init__(self):
super().__init__()
self.setStyleSheet('''
QWidget {
font-size: 15px;
background: rgb(150, 150, 150);
}
QListWidget::item:selected {
background: rgb(128,128,255);
}
CustomItemWidget {
background: transparent;
}
''')
# ...
w1 = CustomItemWidget()
# ...
w2 = CustomItemWidget()
# ...
Consider that using universal selectors for colors is usually not a good idea, as it can result in inconsistent behavior, especially for complex widgets: for instance, the scroll bars of scroll areas (like QListWidget) might not be correctly styled and can even become unusable.
If you plan to have a common background color for all widgets, it's better to set the Window role of the application palette:
app = QApplication(sys.argv)
palette = app.palette()
palette.setColor(palette.Window, QColor(150, 150, 150))
app.setPalette(palette)
# ...
In this way the current style will know exactly when to use that color as background, or as a component for other UI elements.
I want to up my game in UI design using PyQt5. I feel like the resources for UI customization in PyQt5 are not easy to find. It is possible to try and make personalized widget, but the overall method seems non-standardized.
I need to build an arrow widget that is hoverable, overlappable with other widgets and highly customized. As I read in this tutorial and some other posts, it possible to do exactly what you need using paintEvent. Thus that is what I tried, but overall, I feel like the method is quite messy, and I'd like some guidelines on building complex Customized, general widget. Here's what I have:
Customized Shape: I built my code based on this
Hoverable property: I read everywhere that modifying the projects styleSheet is usually the way to go, especially if you want to make your Widget general and adapt to colors, the problem is that I wasn't able to find how to use properly self.palette to fetch the current colors of the QApplication styleSheet. I feel like i's have to maybe use enterEvent and leaveEvent, but I tried to redraw the whole widget with a painter in those functions and it said
QPainter::begin: Painter already active
QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::setRenderHint: Painter must be active to set rendering hints
Overlappable Property: I found a previous post which seemed to have found a solution: create a second widget that is children of the main widget, in order to be able to move the children around. I tried that but it seems that it doesn't want to move, no matter the position I give the widget.
Here is my code:
import sys
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QGraphicsDropShadowEffect, QApplication, QFrame, QPushButton
from PyQt5.QtCore import Qt, QPoint, QLine
from PyQt5.QtGui import QPainter, QPen, QColor, QPalette
class MainWidget(QWidget):
def __init__(self):
super(MainWidget, self).__init__()
self.resize(500, 500)
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.myPush = QPushButton()
self.layout.addWidget(self.myPush)
self.arrow = ArrowWidget(self)
position = QPoint(-40, 0)
self.layout.addWidget(self.arrow)
self.arrow.move(position)
class ArrowWidget(QWidget):
def __init__(self, parent=None):
super(ArrowWidget, self).__init__(parent)
self.setWindowFlag(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.w = 200
self.h = 200
self.blurRadius = 20
self.xO = 0
self.yO = 20
self.resize(self.w, self.h)
self.layout = QHBoxLayout()
# myFrame = QFrame()
# self.layout.addWidget(myFrame)
self.setLayout(self.layout)
self.setStyleSheet("QWidget:hover{border-color: rgb(255,0,0);background-color: rgb(255,50,0);}")
shadow = QGraphicsDropShadowEffect(blurRadius=self.blurRadius, xOffset=self.xO, yOffset=self.yO)
self.setGraphicsEffect(shadow)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.begin(self)
# painter.setBrush(self.palette().window())
# painter.setPen(QPen(QPalette, 5))
ok = self.frameGeometry().width()/2-self.blurRadius/2-self.xO/2
oky = self.frameGeometry().height()/2-self.blurRadius/2-self.yO/2
painter.drawEllipse(QPoint(self.frameGeometry().width()/2-self.blurRadius/2-self.xO/2, self.frameGeometry().height()/2-self.blurRadius/2-self.yO/2), self.w/2-self.blurRadius/2-self.yO/2-self.xO/2, self.h/2-self.blurRadius/2-self.yO/2-self.xO/2)
painter.drawLines(QLine(ok-25, oky-50, ok+25, oky), QLine(ok+25, oky, ok-25, oky+50))
painter.end()
if __name__ == '__main__':
app = QApplication(sys.argv)
testWidget = MainWidget()
testWidget.show()
sys.exit(app.exec_())
If someone could help me make this work and explain along the way to help us better understand the structure of customized widgets and explain a better method that isn't messy like this one, I believe it would be a plus to the beginners like me using PyQt5 as a main Framework for UI making.
There is no "standard" method for custom widgets, but usually paintEvent overriding is required.
There are different issues in your example, I'll try and address to them.
Overlapping
If you want a widget to be "overlappable", it must not be added to a layout. Adding a widget to a layout will mean that it will have its "slot" within the layout, which in turn will try to compute its sizes (based on the widgets it contains); also, normally a layout has only one widget per "layout slot", making it almost impossible to make widget overlap; the QGridLayout is a special case which allows (by code only, not using Designer) to add more widget to the same slot(s), or make some overlap others. Finally, once a widget is part of a layout, it cannot be freely moved nor resized (unless you set a fixedSize).
The only real solution to this is to create the widget with a parent. This will make it possible to use move() and resize(), but only within the boundaries of the parent.
Hovering
While it's true that most widgets can use the :hover selector in the stylesheet, it only works for standard widgets, which do most of their painting by themself (through QStyle functions). About this, while it's possible to do some custom painting with stylesheets, it's generally used for very specific cases, and even in this case there is no easy way to access to the stylesheet properties.
In your case, there's no need to use stylesheets, but just override enterEvent and leaveEvent, set there any color you need for painting and then call self.update() at the end.
Painting
The reason you're getting those warnings is because you are calling begin after declaring the QPainter with the paint device as an argument: once it's created it automatically calls begin with the device argument. Also, it usually is not required to call end(), as it is automatically called when the QPainter is destroyed, which happens when the paintEvent returns since it's a local variable.
Example
I created a small example based on your question. It creates a window with a button and a label within a QGridLayout, and also uses a QFrame set under them (since it's been added first), showing the "overlapping" layout I wrote about before. Then there's your arrow widget, created with the main window as parent, and that can be moved around by clicking on it and dragging it.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class ArrowWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
# since the widget will not be added to a layout, ensure
# that it has a fixed size (otherwise it'll use QWidget default size)
self.setFixedSize(200, 200)
self.blurRadius = 20
self.xO = 0
self.yO = 20
shadow = QtWidgets.QGraphicsDropShadowEffect(blurRadius=self.blurRadius, xOffset=self.xO, yOffset=self.yO)
self.setGraphicsEffect(shadow)
# create pen and brush colors for painting
self.currentPen = self.normalPen = QtGui.QPen(QtCore.Qt.black)
self.hoverPen = QtGui.QPen(QtCore.Qt.darkGray)
self.currentBrush = self.normalBrush = QtGui.QColor(QtCore.Qt.transparent)
self.hoverBrush = QtGui.QColor(128, 192, 192, 128)
def mousePressEvent(self, event):
if event.buttons() == QtCore.Qt.LeftButton:
self.mousePos = event.pos()
def mouseMoveEvent(self, event):
# move the widget based on its position and "delta" of the coordinates
# where it was clicked. Be careful to use button*s* and not button
# within mouseMoveEvent
if event.buttons() == QtCore.Qt.LeftButton:
self.move(self.pos() + event.pos() - self.mousePos)
def enterEvent(self, event):
self.currentPen = self.hoverPen
self.currentBrush = self.hoverBrush
self.update()
def leaveEvent(self, event):
self.currentPen = self.normalPen
self.currentBrush = self.normalBrush
self.update()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setRenderHints(qp.Antialiasing)
# painting is not based on "pixels", to get accurate results
# translation of .5 is required, expecially when using 1 pixel lines
qp.translate(.5, .5)
# painting rectangle is always 1px smaller than the actual size
rect = self.rect().adjusted(0, 0, -1, -1)
qp.setPen(self.currentPen)
qp.setBrush(self.currentBrush)
# draw an ellipse smaller than the widget
qp.drawEllipse(rect.adjusted(25, 25, -25, -25))
# draw arrow lines based on the center; since a QRect center is a QPoint
# we can add or subtract another QPoint to get the new positions for
# top-left, right and bottom left corners
qp.drawLine(rect.center() + QtCore.QPoint(-25, -50), rect.center() + QtCore.QPoint(25, 0))
qp.drawLine(rect.center() + QtCore.QPoint(25, 0), rect.center() + QtCore.QPoint(-25, 50))
class MainWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
self.button = QtWidgets.QPushButton('button')
layout.addWidget(self.button, 0, 0)
self.label = QtWidgets.QLabel('label')
self.label.setAlignment(QtCore.Qt.AlignCenter)
layout.addWidget(self.label, 0, 1)
# create a frame that uses as much space as possible
self.frame = QtWidgets.QFrame()
self.frame.setFrameShape(self.frame.StyledPanel|self.frame.Raised)
self.frame.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
# add it to the layout, ensuring it spans all rows and columns
layout.addWidget(self.frame, 0, 0, layout.rowCount(), layout.columnCount())
# "lower" the frame to the bottom of the widget's stack, otherwise
# it will be "over" the other widgets, preventing them to receive
# mouse events
self.frame.lower()
self.resize(640, 480)
# finally, create your widget with a parent, *without* adding to a layout
self.arrowWidget = ArrowWidget(self)
# now you can place it wherever you want
self.arrowWidget.move(220, 140)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
testWidget = MainWidget()
testWidget.show()
sys.exit(app.exec_())
given that, I'm starting with the PyQt5 module, I'm still slowly understanding the logic behind it. That said, I'm having a problem that I can not find an answer to and I hope you can help me.
I have this script:
import sys, socket, time
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from io import BytesIO as by
class loadGame(QWidget):
wLoadDisplay = 768
hLoadDisplay = 576
wLoadBar = 650
hLoadBar = 40
pbarCSS = """
QProgressBar
{
font-size: 20px;
font-weight: bold;
background-color: #FFF;
border: 4px solid #000;
text-align: center;
}
QProgressBar::chunk
{
background-color: #FF0000;
width: 1px;
}
"""
labelCSS = """
QLabel
{
font-size: 20px;
font-weight: bold;
background-color: #FFF;
border: 4px solid #000;
}
"""
fileResource = []
imgResource = []
vidResource = []
audResource = []
diaResource = []
txtResource = []
internetConnection = False
def __init__(self, *args, **kwargs):
QWidget.__init__(self, *args, **kwargs)
self.outputFile = by()
self.pbar = QProgressBar(self)
self.pbar.setGeometry((self.wLoadDisplay / 2 - self.wLoadBar / 2), (self.hLoadDisplay / 2 - self.hLoadBar * 2),
self.wLoadBar, self.hLoadBar)
self.pbar.setFormat("%v/%m")
self.pbar.setValue(0)
self.pbar.setStyleSheet(self.pbarCSS)
self.label = QLabel(self)
self.label.setGeometry((self.wLoadDisplay / 2 - self.wLoadBar / 2), (self.hLoadDisplay / 2),
self.wLoadBar, self.hLoadBar)
self.label.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
self.label.setStyleSheet(self.labelCSS)
self.setGeometry(0, 0, self.wLoadDisplay, self.hLoadDisplay)
oImage = QImage("bgloading.png")
sImage = oImage.scaled(QSize(self.wLoadDisplay, self.hLoadDisplay))
palette = QPalette()
palette.setBrush(10, QBrush(sImage))
self.setPalette(palette)
qtRectangle = self.frameGeometry()
centerPoint = QDesktopWidget().availableGeometry().center()
qtRectangle.moveCenter(centerPoint)
self.move(qtRectangle.topLeft())
self.run()
def run(self):
self.checkConnection()
if self.internetConnection:
self.checkUpdate()
else:
pass
def checkConnection(self):
self.objectChange("Check Internet Connection", 1)
try:
host = socket.gethostbyname("www.google.it")
s = socket.create_connection((host, 80), 2)
self.internetConnection = True
except:
pass
self.count()
self.reset()
def checkUpdate(self):
pass
def objectChange(self, object, n):
self.label.setText(object)
self.pbar.setMaximum(n)
def count(self):
self.pbar.setValue(self.pbar.value() + 1)
def reset(self):
time.sleep(2)
self.pbar.setMaximum(0)
self.pbar.setValue(0)
self.label.setText("...")
if __name__ == '__main__':
loadDisplay = QApplication(sys.argv)
load = loadGame()
load.show()
sys.exit(loadDisplay.exec_())
Searching on the web, I discovered that the problem is related to "time.sleep (2)", that is, the instruction blocks the window that does not appear until two seconds have passed.
The fact is that I would like to spend one or two seconds, showing the completion of the bar, before resetting and move on to the next statement contained in "def run (self)".
So, is there a way to make that pause, without using the Time module? I do not know, maybe with QTimer? I repeat, I do not know much about PyQt5 yet, so I'm not aware if QTimer can do the same thing.
If QTimer can not do it, is it possible in any other way? I would like to avoid the "threads" of PyQt5, because I read that it would be possible to do it, but I would like to avoid using it only for the Timer module.
I only add one more question, to avoid opening another one and publishing the same script.
In the script, the window background is done via "oImage = QImage (" bgloading.png ")" etc.
I noticed, however, that if the file name were found to be wrong, or the file itself is missing, the background is colored black. So, if there are any errors (wrong name or missing file) it's possible set a background, for example, white?
Because when the window is loaded with "this error", no exception is raised and the script continues.
Edit: I've edited the published script so that it contains only the PyQt5 part, so you can try it out. Obviously only the image is missing, which can be replaced with any image.
Unfortunately I had forgotten to write that the part where "self.set_display ()" was reported was to show that once the work performed by PyQt5 was terminated, it would be closed (which was still missing, because using Pycharm I would close the execution of the script from the program). The script would continue by calling the "self.set_display ()" function.
Edit2: I tried, as suggested, to replace "time.sleep (2)", but I get the same result. The problem is that if I do not put the pause, the window appears normally, but the reset happens too quickly and the user does not see the filling of the progress bar. If instead I put "time.sleep (2)" or, the suggested solution, the window appears only after the pause, that is when the reset has already occurred.
An expression equivalent to time.sleep(2) that is friendly to PyQt is as follows:
loop = QEventLoop()
QTimer.singleShot(2000, loop.quit)
loop.exec_()
The problem is caused because you are showing the widget after the pause, you must do it before calling run(), Also another error is to use QImage, for questions of widgets you must use QPixmap.
If the file does not exist then QPixmap will be null and to know if it is you should use the isNull() method:
[...]
self.setGeometry(0, 0, self.wLoadDisplay, self.hLoadDisplay)
palette = self.palette()
pixmap = QPixmap("bgloading.png")
if not pixmap.isNull():
pixmap = pixmap.scaled(QSize(self.wLoadDisplay, self.hLoadDisplay))
palette.setBrush(QPalette.Window, QBrush(pixmap))
else:
palette.setBrush(QPalette.Window, QBrush(Qt.white))
self.setPalette(palette)
qtRectangle = self.frameGeometry()
centerPoint = QDesktopWidget().availableGeometry().center()
qtRectangle.moveCenter(centerPoint)
self.move(qtRectangle.topLeft())
self.show()
self.run()
[...]
def reset(self):
loop = QEventLoop()
QTimer.singleShot(2000, loop.quit)
loop.exec_()
self.pbar.setMaximum(0)
self.pbar.setValue(0)
self.label.setText("...")