How to fix hover effect for multi QFrame in PyQt5? - python

My intention is to make a Hover effect for multiple frames at a time. For Example, Below is my code, I have three frames. Two inner frames and one outer frame. If the mouse enters where ever, that is, either in an outer frame or in the inner frames, I need a hover effect for three frames, at a time.
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
class Dynamic_Widgets(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Dynamic Widget")
self.lbl = QLabel("this is text")
self.btn = QPushButton("Button")
self.lbl_1 = QLabel("-----------")
self.lbl_r = QLabel("My text ")
self.btn_r = QPushButton("Button888888")
self.lbl_1r = QLabel(('\u2261' * 60))
my_font_1 = QFont("Arial", 10, QFont.Bold)
my_font_1.setLetterSpacing(QFont.AbsoluteSpacing, -6)
self.lbl_1r.setFont(my_font_1)
self.vbox = QHBoxLayout()
self.frame = QFrame()
self.frame_right = QFrame()
self.frame_all = QFrame()
self.frame.setObjectName("ob_frame")
self.frame_right.setObjectName("ob_frame_right")
self.frame_all.setObjectName("ob_frame_all")
self.frame.setProperty("type", "2")
self.frame_right.setProperty("type", "2")
self.frame_all.setProperty("type", "2")
self.framebox = QVBoxLayout(self.frame)
self.framebox_right = QVBoxLayout(self.frame_right)
self.framebox_all = QHBoxLayout(self.frame_all)
self.framebox_all.setContentsMargins(0, 0, 0, 0)
self.framebox_all.setSpacing(0)
self.framebox_all.addWidget(self.frame)
self.framebox_all.addWidget(self.frame_right)
self.vbox.addWidget(self.frame_all)
self.setLayout(self.vbox)
self.frame.setFixedSize(100, 100)
self.frame_right.setFixedSize(100, 100)
self.frame_all.setFixedSize(220, 120)
self.frame.setStyleSheet(f"QFrame#ob_frame{{background-color: lightgreen;}}")
self.frame_right.setStyleSheet(f"QFrame#ob_frame_right{{background-color: lightgreen;}}")
self.frame_all.setStyleSheet(f"QFrame#ob_frame_all{{background-color: green;}}")
#self.frame_all.setStyleSheet('QFrame:hover { background: yellow; }')
self.framebox.addWidget(self.lbl)
self.framebox.addWidget(self.btn)
self.framebox.addWidget(self.lbl_1)
self.framebox_right.addWidget(self.lbl_r)
self.framebox_right.addWidget(self.btn_r)
self.framebox_right.addWidget(self.lbl_1r)
def main():
app = QApplication(sys.argv)
ex = Dynamic_Widgets()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

Setting individual stylesheets can be useful for very specific and relatively static styling, but, in general, it's usually better to have a centralized QSS or, at least, different QSS set for common parents.
In normal situations, a single Class:hover selector would suffice, but if the children have properties that need to be overridden by a change in the parent, it is necessary to install an event filter on the parent.
Considering that the event filter will match Enter and Leave events, the hover selector doesn't make a lot of sense anymore: we can create a "default" style sheet stored as instance attribute and set/override it depending on those events:
class Dynamic_Widgets(QWidget):
def __init__(self):
# ...
self.frame_all_qss = '''
QFrame#ob_frame_all {
background-color: green;
}
QFrame#ob_frame {
background-color: lightgreen;
}
QFrame#ob_frame_right {
background-color: lightgreen;
}
'''
self.frame_all.setStyleSheet(self.frame_all_qss)
self.frame_all.installEventFilter(self)
def eventFilter(self, obj, event):
if obj == self.frame_all:
if event.type() == event.Enter:
res = obj.event(event)
self.frame_all.setStyleSheet(
'QFrame#ob_frame_all { background: yellow; }')
return res
elif event.type() == event.Leave:
res = obj.event(event)
self.frame_all.setStyleSheet(self.frame_all_qss)
return res
return super().eventFilter(obj, event)

Related

update QStyle in EnterEvent

i wanna update the QStyle of the LineEdit while the mouse is in\out the widget (enterEvent\leaveEvent ) i tried to add a bool variable to the drawPrimitive function but i get this error
TypeError: drawPrimitive(self, QStyle.PrimitiveElement, QStyleOption, QPainter, widget: QWidget = None): 'a' is not a valid keyword argument
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PushButton_2 import Push_Button_
import sys
class LineEditStyle(QProxyStyle):
def drawPrimitive(self, element, option, painter, widget,a=None):
if a :
self.pen = QPen(QColor('green'))
else :
self.pen = QPen(QColor('red'))
self.pen.setWidth(4)
if element == QStyle.PE_FrameLineEdit:
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(self.pen)
painter.drawRoundedRect(QRect(0,0,400,40), 10, 10)
else:
super().drawPrimitive(element, option, painter, widget)
class LineEdit(QLineEdit):
def __init__(self,*args,**kwargs):
QLineEdit.__init__(self,*args,**kwargs)
self.a = 0
def enterEvent(self, a0):
self.a = 1
def leaveEvent(self, a0):
self.a = 0
def paintEvent(self,event):
option = QStyleOption()
option.initFrom(self)
self.style().drawPrimitive(QStyle.PE_FrameLineEdit,option,a=self.a)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = QMainWindow()
window.setGeometry(500,500,400,400)
window.setStyleSheet('background-color:#373737')
line = LineEdit(parent=window)
line.setGeometry(20,200,400,40)
style = LineEditStyle()
line.setStyle(style)
window.show()
sys.exit(app.exec())
You mustn't use the QStyleSheet with the QStyle because it makes a confusion and you have to set the default parameter Widget as None
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PushButton_2 import Push_Button_
import sys
class LineEditStyle(QProxyStyle):
def drawPrimitive(self, element, option, painter, widget=None,a=None):
if a :
self.pen = QPen(QColor('green'))
else :
self.pen = QPen(QColor('red'))
self.pen.setWidth(4)
if element == QStyle.PE_FrameLineEdit:
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(self.pen)
painter.drawRoundedRect(QRect(0,0,400,40), 10, 10)
else:
super().drawPrimitive(element, option, painter, widget)
def subElementRect(self, element, option, widget):
if element == QStyle.SE_LineEditContents :
return QRect(0,0,50,30)
else :
return super().subElementRect(element, option, widget)
def drawItemText(self, painter, rect, flags, pal, enabled, text, textRole):
rect_ = QRect(20,20,50,50)
text = text.upper()
painter.drawText(text,rect_,Qt.AlignCenter)
class LineEdit(QLineEdit):
def __init__(self,*args,**kwargs):
QLineEdit.__init__(self,*args,**kwargs)
self.a = 0
def enterEvent(self, a0):
self.a = 1
def leaveEvent(self, a0):
self.a = 0
def paintEvent(self,event):
option = QStyleOption()
option.initFrom(self)
painter = QPainter(self)
self.style().drawPrimitive(QStyle.PE_FrameLineEdit,option,painter,a=self.a)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = QMainWindow()
window.setGeometry(500,500,400,400)
#window.setStyleSheet('background-color:#373737')
line = LineEdit(parent=window)
line.setGeometry(20,200,400,40)
style = LineEditStyle()
line.setStyle(style)
window.show()
sys.exit(app.exec())
You might find QStyleSheets useful for something like this.
I mocked this up in Qt Designer by entering the following in styleSheet property (in code you would do mywidget.setStyleSheet('<parameters>') ):
QLineEdit {
border: 3px solid red;
}
QLineEdit:focus {
border: 3px solid green;
}
Edit: styleSheet string above works on focus, but the original question was about enterEvent/leaveEvent, which trigger on mouse hover. As #musicamente rightfully points out, to work on mouse hover the :hover pseudo state can be used instead of :focus:
QLineEdit {
border: 3px solid red;
}
QLineEdit:hover {
border: 3px solid green;
}
I made a QMainWindow with 2 QLineEdits: 1 has styleSheet set and the other is default. When the focus moves to the regular QLineEdit, the modified QLineEdit turns red.
You can make ALL of the QLineEdits behave the same by editing the styleSheet of their parent. In your case, you could use window.setStyleSheet('..... Here's another mockup in Qt Designer:

How to have two widgets in one Main window

I am trying the whole morning already to fix that.
So I have a PyQt Main Window where I want to display two widgets.
In the first widget there are articles listed (which works so far).
When I click on them until now a QMessageBox is opening, but I want that
a second widget is opening where I can read the RSS Feed.
But this is not working. See Code below:
class ArticleWidgets(QWidget):
def __init__(self, *args):
super().__init__(*args)
self.setGeometry(610, 610, 600, 600)
self.initUi()
def initUi(self):
self.box = QHBoxLayout(self)
def show(self, feed=None):
self.title = QLabel()
self.summary = QLabel()
self.link = QLabel()
if feed:
self.title.setText(feed[0])
self.summary.setText(feed[1])
self.link.setText(feed[2])
self.box.addWidget(self.title)
self.box.addWidget(self.summary)
self.box.addWidget(self.link)
self.setLayout(self.box)
class TitleWidgets(QWidget):
def __init__(self, *args):
super().__init__(*args)
self.setGeometry(10, 10, 600, 600)
self.initUi()
def initUi(self):
vbox = QHBoxLayout(self)
self.titleList = QListWidget()
self.titleList.itemDoubleClicked.connect(self.onClicked)
self.titleList.setGeometry(0, 0, 400, 400)
self.news = ANFFeed()
for item in self.news.all_feeds:
self.titleList.addItem(item[0])
vbox.addWidget(self.titleList)
def onClicked(self, item):
feeds = self.news.all_feeds
id = 0
for elem in range(len(feeds)):
if feeds[elem][0] == item.text():
id = elem
summary = feeds[id][1] + '\n\n'
link = feeds[id][2]
if feeds and id:
#ANFApp(self).show_articles(feeds[id])
show = ANFApp()
show.show_articles(feed=feeds[id])
QMessageBox.information(self, 'Details', summary + link)
class ANFApp(QMainWindow):
def __init__(self, *args):
super().__init__(*args)
self.setWindowState(Qt.WindowMaximized)
self.setWindowIcon(QIcon('anf.png'))
self.setAutoFillBackground(True)
self.anfInit()
self.show()
def anfInit(self):
self.setWindowTitle('ANF RSS Reader')
TitleWidgets(self)
#article_box = ArticleWidgets(self)
exitBtn = QPushButton(self)
exitBtn.setGeometry(600, 600, 100, 50)
exitBtn.setText('Exit')
exitBtn.setStyleSheet("background-color: red")
exitBtn.clicked.connect(self.exit)
def show_articles(self, feed=None):
present = ArticleWidgets()
present.show(feed)
def exit(self):
QCoreApplication.instance().quit()
Solution using Pyqtgraph's Docks and QTextBrowser
Here is a code trying to reproduce your sketch. I used the Pyqtgraph module (Documentation here: Pyqtgraph's Documentation and Pyqtgraph's Web Page) because its Dock widget is easier to use and implement from my perspective.
You must install the pyqtgraph module before trying this code:
import sys
from PyQt5 import QtGui, QtCore
from pyqtgraph.dockarea import *
class DockArea(DockArea):
## This is to prevent the Dock from being resized to te point of disappear
def makeContainer(self, typ):
new = super(DockArea, self).makeContainer(typ)
new.setChildrenCollapsible(False)
return new
class MyApp(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
central_widget = QtGui.QWidget()
layout = QtGui.QVBoxLayout()
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
label = QtGui.QLabel('This is a label, The widgets will be below')
label.setMaximumHeight(15)
## The DockArea as its name says, is the are where we place the Docks
dock_area = DockArea(self)
## Create the Docks and change some esthetic of them
self.dock1 = Dock('Widget 1', size=(300, 500))
self.dock2 = Dock('Widget 2', size=(400, 500))
self.dock1.hideTitleBar()
self.dock2.hideTitleBar()
self.dock1.nStyle = """
Dock > QWidget {
border: 0px solid #000;
border-radius: 0px;
}"""
self.dock2.nStyle = """
Dock > QWidget {
border: 0px solid #000;
border-radius: 0px;
}"""
self.button = QtGui.QPushButton('Exit')
self.widget_one = WidgetOne()
self.widget_two = WidgetTwo()
## Place the Docks inside the DockArea
dock_area.addDock(self.dock1)
dock_area.addDock(self.dock2, 'right', self.dock1)
## The statment above means that dock2 will be placed at the right of dock 1
layout.addWidget(label)
layout.addWidget(dock_area)
layout.addWidget(self.button)
## Add the Widgets inside each dock
self.dock1.addWidget(self.widget_one)
self.dock2.addWidget(self.widget_two)
## This is for set the initial size and posotion of the main window
self.setGeometry(100, 100, 600, 400)
## Connect the actions to functions, there is a default function called close()
self.widget_one.TitleClicked.connect(self.dob_click)
self.button.clicked.connect(self.close)
def dob_click(self, feed):
self.widget_two.text_box.clear()
## May look messy but wat i am doing is somethin like this:
## 'Title : ' + feed[0] + '\n\n' + 'Summary : ' + feed[1]
self.widget_two.text_box.setText(
'Title : ' + feed[0]\
+ '\n\n' +\
'Summary : ' + feed[1]
)
class WidgetOne(QtGui.QWidget):
## This signal is created to pass a "list" when it (the signal) is emited
TitleClicked = QtCore.pyqtSignal([list])
def __init__(self):
QtGui.QWidget.__init__(self)
self.layout = QtGui.QVBoxLayout()
self.setLayout(self.layout)
self.titleList = QtGui.QListWidget()
self.label = QtGui.QLabel('Here is my list:')
self.layout.addWidget(self.label)
self.layout.addWidget(self.titleList)
self.titleList.addItem(QtGui.QListWidgetItem('Title 1'))
self.titleList.addItem(QtGui.QListWidgetItem('Title 2'))
self.titleList.itemDoubleClicked.connect(self.onClicked)
def onClicked(self, item):
## Just test values
title = item.text()
summary = "Here you will put the summary of {}. ".format(title)*50
## Pass the values as a list in the signal. You can pass as much values
## as you want, remember that all of them have to be inside one list
self.TitleClicked.emit([title, summary])
class WidgetTwo(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.layout = QtGui.QVBoxLayout()
self.setLayout(self.layout)
self.label2 = QtGui.QLabel('Here we show results?:')
self.text_box = QtGui.QTextBrowser()
self.layout.addWidget(self.label2)
self.layout.addWidget(self.text_box)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = MyApp()
window.show()
sys.exit(app.exec_())
Again, there are comments inside the code to help you understand what I did.
Here is how it looks:
If you pass the mouse between the two widgets you will see the mouse icon will change, with that you can readjust on the run the size of both widgets.
Final Words
This is another approach, more "interactive" and more esthetic than my previous answer. As you said, using a QSplitter works too.
Problems
The way you are building your GUI is, in my opinion, messy and it may lead to errors. I suggest the use of Layouts for a more organized GUI.
The other problem is that each widget is an independent class so if you want to connect an action in one widget to do something in the other widget through the Main Window, you must use Signals.
Edit : Another suggestion, use other name for the close function instead of exit and try using self.close() instead of QCoreApplication.instance().quit()
Solution
Trying to emulate what you want to do I made this GUI:
import sys
from PyQt5 import QtGui, QtCore
class MyWindow(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
## Generate the structure parts of the MainWindow
self.central_widget = QtGui.QWidget() # A QWidget to work as Central Widget
self.layout1 = QtGui.QVBoxLayout() # Vertical Layout
self.layout2 = QtGui.QHBoxLayout() # Horizontal Layout
self.widget_one = WidgetOne()
self.widget_two = WidgetTwo()
self.exitBtn = QtGui.QPushButton('Exit')
## Build the structure
# Insert a QWidget as a central widget for the MainWindow
self.setCentralWidget(self.central_widget)
# Add a principal layout for the widgets/layouts you want to add
self.central_widget.setLayout(self.layout1)
# Add widgets/layuts, as many as you want, remember they are in a Vertical
# layout: they will be added one below of the other
self.layout1.addLayout(self.layout2)
self.layout1.addWidget(self.exitBtn)
# Here we add the widgets to the horizontal layout: one next to the other
self.layout2.addWidget(self.widget_one)
self.layout2.addWidget(self.widget_two)
## Connect the signal
self.widget_one.TitleClicked.connect(self.dob_click)
def dob_click(self, feed):
## Change the properties of the elements in the second widget
self.widget_two.title.setText('Title : '+feed[0])
self.widget_two.summary.setText('Summary : '+feed[1])
## Build your widgets same as the Main Window, with the excepton that here you don't
## need a central widget, because it is already a widget.
class WidgetOne(QtGui.QWidget):
TitleClicked = QtCore.pyqtSignal([list]) # Signal Created
def __init__(self):
QtGui.QWidget.__init__(self)
##
self.layout = QtGui.QVBoxLayout() # Vertical Layout
self.setLayout(self.layout)
self.titleList = QtGui.QListWidget()
self.label = QtGui.QLabel('Here is my list:')
self.layout.addWidget(self.label)
self.layout.addWidget(self.titleList)
self.titleList.addItem(QtGui.QListWidgetItem('Title 1'))
self.titleList.addItem(QtGui.QListWidgetItem('Title 2'))
self.titleList.itemDoubleClicked.connect(self.onClicked)
def onClicked(self, item):
## Just test parameters and signal emited
self.TitleClicked.emit([item.text(), item.text()+item.text()])
class WidgetTwo(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.layout = QtGui.QVBoxLayout()
self.setLayout(self.layout)
self.title = QtGui.QLabel('Title : ---')
self.summary = QtGui.QLabel('Summary : ---')
self.link = QtGui.QLabel('Link : ---')
self.layout.addWidget(self.title)
self.layout.addWidget(self.summary)
self.layout.addWidget(self.link)
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec_())
Inside the code, there are comments to help you understand why I did to build an organized GUI. There is also an example of a Signal being used to connect the action of itemDoubleClicked from the first widget to the second one. Here is how the MainWindow looks:
It is not very clear how the layouts work just from seeing the result, so I did a little paint over to a better understanding:
The blue box is the vertical layout (QVBoxLayout) and the red one is the horizontal layout (QHBoxLayout). Inside the blue layout, are located the red layout (above) and the exit button (below); and inside the red layout, are located the widget_1 (left) and the widget_2 (right).
Other Solution
An "easier" solution will be building the widgets inside the MainWindow instead of creating separate classes. With this you will avoid the use of signals, but the code will become a little more confusing because all the code will be cramped in one class.

PyQT 5 Add widgets dynamically to a spoiler

I am writing a small program and ideally I want to have a fixed size of groups which can unfold in which I have further spoilers which represent items which i can open and close in order to add some entities to my system.
I have been looking for similar questions here and got to the following to work semiproperly:
I have added the -Buttons to remove those childs from the groups and a + to add childs to a group.
This seems to work fine as long as I am not removing or adding widgets.
My code looks like this:
spoilers.py
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class Spoiler(QWidget):
def __init__(self, parent=None, title='', animationDuration=300, addRemoveOption='None'):
super(Spoiler, self).__init__(parent=parent)
self.animationDuration = animationDuration
self.toggleAnimation = QParallelAnimationGroup()
self.contentArea = QScrollArea()
self.headerLine = QFrame()
self.toggleButton = QToolButton()
self.mainLayout = QGridLayout()
self.childWidgets = []
self.toggleButton.setStyleSheet("QToolButton { border: none; }")
self.toggleButton.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.toggleButton.setArrowType(Qt.RightArrow)
self.toggleButton.setText(str(title))
self.toggleButton.setCheckable(True)
self.toggleButton.setChecked(False)
self.addRemoveOperation = addRemoveOption
if addRemoveOption is not 'None':
if addRemoveOption is 'Add':
self.addRemoveButton = QPushButton('+')
else:
self.addRemoveButton = QPushButton('-')
self.addRemoveButton.clicked.connect(self.onAddRemoveButton)
self.contentArea.setStyleSheet("QScrollArea { background-color: white; border: none; }")
self.contentArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
# start out collapsed
self.contentArea.setMaximumHeight(0)
self.contentArea.setMinimumHeight(0)
# let the entire widget grow and shrink with its content
self.toggleAnimation.addAnimation(QPropertyAnimation(self, b"minimumHeight"))
self.toggleAnimation.addAnimation(QPropertyAnimation(self, b"maximumHeight"))
self.toggleAnimation.addAnimation(QPropertyAnimation(self.contentArea, b"maximumHeight"))
# don't waste space
self.mainLayout.setVerticalSpacing(0)
self.mainLayout.setContentsMargins(0, 0, 0, 0)
self.mainLayout.addWidget(self.toggleButton, 0, 0, 1, 1, Qt.AlignLeft)
if addRemoveOption is not 'None':
self.mainLayout.addWidget(self.addRemoveButton, 0, 2, 1, 1, Qt.AlignRight)
self.mainLayout.addWidget(self.contentArea, 1, 0, 1, 3)
self.setLayout(self.mainLayout)
def start_animation(checked):
arrow_type = Qt.DownArrow if checked else Qt.RightArrow
direction = QAbstractAnimation.Forward if checked else QAbstractAnimation.Backward
self.toggleButton.setArrowType(arrow_type)
self.toggleAnimation.setDirection(direction)
self.toggleAnimation.start()
self.toggleButton.clicked.connect(start_animation)
self.contentLayout = QVBoxLayout()
self.setContentLayout(self.contentLayout)
def setContentLayout(self, contentLayout):
self.contentArea.destroy()
self.contentArea.setLayout(contentLayout)
collapsedHeight = self.sizeHint().height() - self.contentArea.maximumHeight()
contentHeight = contentLayout.sizeHint().height()
for i in range(self.toggleAnimation.animationCount()-1):
spoilerAnimation = self.toggleAnimation.animationAt(i)
spoilerAnimation.setDuration(self.animationDuration)
spoilerAnimation.setStartValue(collapsedHeight)
spoilerAnimation.setEndValue(collapsedHeight + contentHeight)
contentAnimation = self.toggleAnimation.animationAt(self.toggleAnimation.animationCount() - 1)
contentAnimation.setDuration(self.animationDuration)
contentAnimation.setStartValue(0)
contentAnimation.setEndValue(contentHeight)
def addChild(self, child):
self.childWidgets += [child]
self.contentLayout.addWidget(child)
self.setContentLayout(self.contentLayout)
def removeChild(self, child):
self.childWidgets.remove(child)
#self.contentLayout.removeWidget(child)
child.destroy()
#self.setContentLayout(self.contentLayout)
def onAddRemoveButton(self):
self.addChild(LeafSpoiler(root=self))
class LeafSpoiler(Spoiler):
def __init__(self, parent=None, root=None, title=''):
if(root == None):
addRemoveOption = 'None'
else:
addRemoveOption = 'Sub'
super(LeafSpoiler, self).__init__(parent=parent, title=title, addRemoveOption=addRemoveOption)
self.root = root
def onAddRemoveButton(self):
self.root.removeChild(self)
gui.py
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from spoilers import *
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
self.setGeometry(300, 300, 300, 220)
# self.setWindowIcon(QIcon('web.png'))
self.centralWidget = QFrame()
self.centralLayout = QVBoxLayout()
self.centralWidget.setLayout(self.centralLayout)
self.spoiler1 = Spoiler(addRemoveOption='Add', title='Group 1')
self.spoiler2 = Spoiler(addRemoveOption='Add', title='Group 2')
for i in range(3):
leaf = LeafSpoiler(root=self.spoiler1)
self.spoiler1.addChild(leaf)
leaf = LeafSpoiler(root=self.spoiler2)
self.spoiler2.addChild(leaf)
self.centralLayout.addWidget(self.spoiler1)
self.centralLayout.addWidget(self.spoiler2)
self.setCentralWidget(self.centralWidget)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
I am not quiet sure why this doesnt work. I assume that Spoiler.setContentLayout() is not supposed to be called more than once.
I would be very happy if someone could help me out on this one!
Greetings,
Finn
I am not too sure whether I understood your question correctly. I assume you are talking about pyqt crashing when trying to remove a Spoilerleaf? At least this is what's happening on my machine.
Your "removeChild" method seems to be a culprit here. Without knowing too much about the source of the crash, replacing this with a call to deleteLater() enables child deletion on my machine:
class LeafSpoiler(Spoiler):
# [...] same init as your's
def onAddRemoveButton(self):
self.deleteLater()

PyQt5 Laying Properly

I've already looked heavily to see how to properly layer my ui and haven't found out how to layer my windows so it comes off looking somewhat like this:
I want to have my background layer which I have set as a label with an image and then have a qt widget with login centered in the middle of it almost popping out at the user however when I do this it comes out with the widget behind my main window, it doesn't align properly and it also doesnt "follow" the window when I move it around image provided:
import PyQt5.QtWidgets
import sys
class LoginPanel(PyQt5.QtWidgets.QWidget):
def __init__(self):
PyQt5.QtWidgets.QWidget.__init__(self)
self.setFixedSize(600,400)
self.setWindowFlags(PyQt5.QtCore.Qt.FramelessWindowHint | PyQt5.QtCore.Qt.WindowStaysOnTopHint)
self.setStyleSheet("""
QWidget {
background-color: #CBCAB7;
border-radius: 50px;
}
""")
self.show()
class Auth(PyQt5.QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Login")
self.setFixedSize(1200,800)
self.setWindowFlags(PyQt5.QtCore.Qt.WindowCloseButtonHint | PyQt5.QtCore.Qt.WindowMinimizeButtonHint)
self.setWindowIcon(PyQt5.QtGui.QIcon("assets\\login.ico"))
self.background = PyQt5.QtWidgets.QLabel("", self)
self.layout = PyQt5.QtWidgets.QGridLayout()
self.layout.addWidget(LoginPanel(), 0, 1)
self.set_background()
self.show()
#self.layout.setAlignment(PyQt5.QtCore.Qt.AlignCenter)
def set_background(self):
img = PyQt5.QtGui.QPixmap("assets\\background.png")
pixmap = img.scaled(self.width(), self.height())
self.background.setPixmap(img)
self.background.resize(self.width(), self.height())
if __name__ == "__main__":
app = PyQt5.QtWidgets.QApplication(sys.argv)
a = Auth()
sys.exit(app.exec())
Here is my current code, I just wanted some help sense while looking online I was unable to find any great examples or references.
My recommendation is not to create a new window but to set it as a child of the window, and to raise it above any other child, you must use raise_() method, also add a QGraphicsDropShadowEffect to establish the floating window effect:
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class LoginPanel(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(600, 400)
self.container = QtWidgets.QWidget(self)
self.container.setStyleSheet(
"""
background-color: #CBCAB7;
border-radius: 50px;
"""
)
offset = 30
self.container.setGeometry(
self.rect().adjusted(offset, offset, -offset, -offset)
)
effect = QtWidgets.QGraphicsDropShadowEffect(
blurRadius=50, offset=QtCore.QPointF(0, 0)
)
self.container.setGraphicsEffect(effect)
lay = QtWidgets.QFormLayout(self)
lay.setContentsMargins(2 * offset, 2 * offset, 2 * offset, 2 * offset)
lay.addRow("Username:", QtWidgets.QLineEdit())
lay.addRow("Email:", QtWidgets.QLineEdit())
class Auth(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Login")
self.setFixedSize(1200, 800)
self.setWindowFlags(
QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowMinimizeButtonHint
)
self.setWindowIcon(QtGui.QIcon("assets\\login.ico"))
self.background = QtWidgets.QLabel(self)
self.set_background()
self.panel = LoginPanel(self)
self.center_panel()
def set_background(self):
img = QtGui.QPixmap("assets\\background.png")
pixmap = img.scaled(self.size())
self.background.setPixmap(pixmap)
self.background.resize(self.size())
def resizeEvent(self, event):
super().resizeEvent(event)
self.center_panel()
def center_panel(self):
g = self.panel.geometry()
g.moveCenter(self.rect().center())
self.panel.setGeometry(g)
self.panel.raise_()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
a = Auth()
a.show()
sys.exit(app.exec())

Custom QPushButton inside QStyledItemDelegate

Problem:
I'm able to add a QPushButton to a QStyledItemDelegate just fine. I'm faking the button press inside the delegate's editorEvent method so when you press it an action happens. I'm having trouble getting my QPushButton's style sheet working though- It only reads the first background parameter which is "red" and doesn't change on mouse hover or press.
It's unclear how I should go about setting up button click and hover detection to make the button act like a real button on the delegate. Do I need to set up an eventFilter? Should I do this at the view level? Do I do this inside the delegate's paint method? A combination of everything?
Goals:
Mouse hover over the list time will show the button button's icon.
Mouse hover over the button will change its background color.
Mouse clicks on the button will darken the background color to show a a click happened.
I'd like to set these parameters in a style sheet if possible, but I also don't mind doing it all within a paint function. Whatever works!
Current implementation
The button widget is red with a folder icon. The items correctly change color on select and hover (I want to keep that), but the item's buttons don't change at all.
Thanks!
Here's what I've put together so far:
import sys
from PySide2 import QtCore
from PySide2 import QtGui
from PySide2 import QtWidgets
class DelegateButton(QtWidgets.QPushButton):
def __init__(self, parent=None):
super(DelegateButton, self).__init__(parent)
# self.setLayout(QHBoxLayout())
size = 50
self.setFixedSize(size, size)
self.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DialogOpenButton))
self.setStyleSheet("""
QPushButton{
background:red;
height: 30px;
font: 12px "Roboto Thin";
border-radius: 25
}
QPushButton:hover{
background: green;
}
QPushButton:hover:pressed{
background: blue;
}
QPushButton:pressed{
background: yellow;
}
""")
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.resize(300, 300)
# Model/View
entries = ['one', 'two', 'three']
model = QtGui.QStandardItemModel()
delegate = ListItemDelegate()
self.listView = QtWidgets.QListView(self)
self.listView.setModel(model)
self.listView.setItemDelegate(delegate)
for i in entries:
item = QtGui.QStandardItem(i)
model.appendRow(item)
# Layout
main_layout = QtWidgets.QVBoxLayout()
main_layout.addWidget(self.listView)
self.setLayout(main_layout)
# Connections
delegate.delegateButtonPressed.connect(self.on_delegate_button_pressed)
def on_delegate_button_pressed(self, index):
print('"{}" delegate button pressed'.format(index.data(QtCore.Qt.DisplayRole)))
class ListItemDelegate(QtWidgets.QStyledItemDelegate):
delegateButtonPressed = QtCore.Signal(QtCore.QModelIndex)
def __init__(self):
super(ListItemDelegate, self).__init__()
self.button = DelegateButton()
def sizeHint(self, option, index):
size = super(ListItemDelegate, self).sizeHint(option, index)
size.setHeight(50)
return size
def editorEvent(self, event, model, option, index):
# Launch app when launch button clicked
if event.type() == QtCore.QEvent.MouseButtonRelease:
click_pos = event.pos()
rect_button = self.rect_button
if rect_button.contains(click_pos):
self.delegateButtonPressed.emit(index)
return True
else:
return False
else:
return False
def paint(self, painter, option, index):
spacing = 10
icon_size = 40
# Item BG #########################################
painter.save()
if option.state & QtWidgets.QStyle.State_Selected:
painter.setBrush(QtGui.QColor('orange'))
elif option.state & QtWidgets.QStyle.State_MouseOver:
painter.setBrush(QtGui.QColor('black'))
else:
painter.setBrush(QtGui.QColor('purple'))
painter.drawRect(option.rect)
painter.restore()
# Item Text ########################################
rect_text = option.rect
QtWidgets.QApplication.style().drawItemText(painter, rect_text, QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft, QtWidgets.QApplication.palette(), True, index.data(QtCore.Qt.DisplayRole))
# Custom Button ######################################
self.rect_button = QtCore.QRect(
option.rect.right() - icon_size - spacing,
option.rect.bottom() - int(option.rect.height() / 2) - int(icon_size / 2),
icon_size,
icon_size
)
option = QtWidgets.QStyleOptionButton()
option.initFrom(self.button)
option.rect = self.rect_button
# Button interactive logic
if self.button.isDown():
option.state = QtWidgets.QStyle.State_Sunken
else:
pass
if self.button.isDefault():
option.features = option.features or QtWidgets.QStyleOptionButton.DefaultButton
option.icon = self.button.icon()
option.iconSize = QtCore.QSize(30, 30)
painter.save()
self.button.style().drawControl(QtWidgets.QStyle.CE_PushButton, option, painter, self.button)
painter.restore()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Try:
https://doc.qt.io/qt-4.8/qstyle.html#StateFlag-enum
import sys
import PySide.QtCore as core
import PySide.QtGui as gui
QPushButton#pushButton {
background-color: yellow;
}
QPushButton#pushButton:hover {
background-color: rgb(224, 255, 0);
}
QPushButton#pushButton:pressed {
background-color: rgb(224, 0, 0);
}
Your custom QStyledItemDelegate catches the mouse event, so that it is not passed to the QListView. So in the QStyledItemDelegate.editor(Event) one simply needs to add.
if event.type() == core.QEvent.MouseButtonPress:
return False
Now the selection is recognizable in the paint()-method using option.state & gui.QStyle.State_Selected.
if __name__ == '__main__':
app = gui.QApplication(sys.argv)
app.setStyleSheet('QListView::item:hover {background: none;}')
mw = gui.QMainWindow()
model = MyListModel()
view = gui.QListView()
view.setItemDelegate(MyListDelegate(parent=view))
view.setSpacing(5)
view.setModel(model)
mw.setCentralWidget(view)
mw.show()
sys.exit(app.exec_())
class MyListDelegate(gui.QStyledItemDelegate):
w = 300
imSize = 90
pad = 5
h = imSize + 2*pad
sepX = 10
def __init__(self, parent=None):
super(MyListDelegate, self).__init__(parent)
def paint(self, painter, option, index):
mouseOver = option.state in [73985, 73729]
if option.state & QStyle.State_MouseOver::
painter.fillRect(option.rect, painter.brush())
pen = painter.pen()
painter.save()
x,y = (option.rect.x(), option.rect.y())
dataRef = index.data()
pixmap = dataRef.pixmap()
upperLabel = dataRef.upperLabel()
lowerLabel = dataRef.lowerLabel()
if mouseOver:
newPen = gui.QPen(core.Qt.green, 1, core.Qt.SolidLine)
painter.setPen(newPen)
else:
painter.setPen(pen)
painter.drawRect(x, y, self.w, self.h)
painter.setPen(pen)
x += self.pad
y += self.pad
painter.drawPixmap(x, y, pixmap)
font = painter.font()
textHeight = gui.QFontMetrics(font).height()
sX = self.imSize + self.sepX
sY = textHeight/2
font.setBold(True)
painter.setFont(font)
painter.drawText(x+sX, y-sY,
self.w-self.imSize-self.sepX, self.imSize,
core.Qt.AlignVCenter,
upperLabel)
font.setBold(False)
font.setItalic(True)
painter.setFont(font)
painter.drawText(x+sX, y+sY,
self.w-self.imSize-self.sepX, self.imSize,
core.Qt.AlignVCenter,
lowerLabel)
painter.restore()
def sizeHint(self, option, index):
return core.QSize(self.w, self.imSize+2*self.pad)
def editorEvent(self, event, model, option, index):
if event.type() == core.QEvent.MouseButtonRelease:
print 'Clicked on Item', index.row()
if event.type() == core.QEvent.MouseButtonDblClick:
print 'Double-Clicked on Item', index.row()
return True
window.button->setAutoFillBackground(false);
window.button->setAutoFillBackground(true);
window.button->setPalette(*palette_red);
Another Solution To Set CSS:
import sys
from PyQt5 import Qt as qt
class TopLabelNewProject(qt.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
layout = qt.QHBoxLayout(self)
layout.setContentsMargins(40, 0, 32, 0)
self.setLayout(layout)
self.setFixedHeight(80)
self.label = qt.QLabel("Dashboard")
layout.addWidget(self.label, alignment=qt.Qt.AlignLeft)
# self.newProjectButton = Buttons.DefaultButton("New project", self)
self.newProjectButton = qt.QPushButton("New project", self)
layout.addWidget(self.newProjectButton, alignment=qt.Qt.AlignRight)
style = '''
QWidget {
background-color: white;
}
QLabel {
font: medium Ubuntu;
font-size: 20px;
color: #006325;
}
QPushButton {
background-color: #006325;
color: white;
min-width: 70px;
max-width: 70px;
min-height: 70px;
max-height: 70px;
border-radius: 35px;
border-width: 1px;
border-color: #ae32a0;
border-style: solid;
}
QPushButton:hover {
background-color: #328930;
}
QPushButton:pressed {
background-color: #80c342;
}
'''
if __name__ == '__main__':
app = qt.QApplication(sys.argv)
app.setStyleSheet(style)
ex = TopLabelNewProject()
ex.show()
sys.exit(app.exec_())

Categories