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_())
Related
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)
I'm making a GUI using PyQt5 and I made the background of my widgets change color when you hover your mouse over them. However, when I hover over a widget, the widget beneath it overlaps and the background is cut off at the bottom.
Its because of the way I made the background, I had to space the widgets very close together and increase the border size in order to achieve that background effect, but it messes with the hover like I said before. Is there any way to ignore this overlapping, or perhaps a different method I could use to draw the background without having to overlap the widgets?
Here is a screenshot of what I want in case my explanation was bad (The bottom widget works fine because there is nothing beneath it to overlap)
I will also attach my code here so you can see what I did
from PyQt5 import QtGui
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.layout = QVBoxLayout()
self.layout.addWidget(MyBar(self))
self.setLayout(self.layout)
self.layout.setContentsMargins(0,0,0,0)
self.setFixedSize(450,550)
self.setWindowFlags(Qt.FramelessWindowHint)
self.setStyleSheet('background-color: #121A2B;')
self.checkbox_style='''
QCheckBox
{
background-color : rgb(25,34,52);
border-radius : 5px;
spacing : 10px;
padding : 15px;
min-height : 15px;
}
QCheckBox::unchecked
{
color : rgb(159,172,168);
}
QCheckBox::checked
{
color : rgb(217,223,227);
}
QCheckBox::indicator
{
border : 2px solid rgb(105, 139, 194);
width : 12px;
height : 12px;
border-radius : 8px;
}
QCheckBox::indicator:checked
{
image : url(red.png);
border : 2px solid rgb(221, 54, 77);
}
QCheckBox::hover
{
background-color: #263450
}
'''
font = QtGui.QFont()
font.setFamily("Arial")
font.setPointSize(11)
font.setBold(True)
self.checkbox_layout = QVBoxLayout()
self.checkbox1 = QCheckBox('checkbox1')
self.checkbox_layout.addWidget(self.checkbox1)
self.checkbox1.setFont(font)
self.checkbox1.setStyleSheet(self.checkbox_style)
self.checkbox2 = QCheckBox('checkbox2')
self.checkbox_layout.addWidget(self.checkbox2)
self.checkbox2.setFont(font)
self.checkbox2.setStyleSheet(self.checkbox_style)
self.checkbox3 = QCheckBox('checkbox3')
self.checkbox_layout.addWidget(self.checkbox3)
self.checkbox3.setFont(font)
self.checkbox3.setStyleSheet(self.checkbox_style)
self.checkbox3 = QCheckBox('checkbox4')
self.checkbox_layout.addWidget(self.checkbox3)
self.checkbox3.setFont(font)
self.checkbox3.setStyleSheet(self.checkbox_style)
self.layout.addLayout(self.checkbox_layout)
self.checkbox_layout.setContentsMargins(15,7,290,350)
self.setLayout(self.layout)
self.layout.setAlignment(Qt.AlignTop)
class MyBar(QWidget):
def __init__(self, parent):
super(MyBar, self).__init__()
self.parent = parent
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.title = QLabel("")
font = QtGui.QFont()
font.setFamily("Bebas Neue")
font.setPointSize(25)
font.setBold(False)
self.title.setFont(font)
self.btn_close = QPushButton()
self.btn_close.clicked.connect(self.btn_close_clicked)
self.btn_close.setFixedSize(40, 35)
self.btn_close.setStyleSheet("QPushButton::hover"
"{"
"background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,stop: 0 #fc0703, stop: 1 #a10303);"
"border : none;"
"}"
"QPushButton"
"{"
"background-color : rgb(25, 34, 52)"
"}")
self.btn_close.setFlat(True)
self.btn_close.setIcon(QIcon("C:/Users/User/Documents/GitHub/guirebuild/close.png"))
self.btn_close.setIconSize(QSize(15, 15))
self.title.setFixedHeight(53)
self.title.setAlignment(Qt.AlignTop)
self.layout.addWidget(self.title)
self.layout.addWidget(self.btn_close,alignment=Qt.AlignTop)
self.title.setStyleSheet("""
background-color: rgb(25, 34, 52);
color: white;
""")
self.setLayout(self.layout)
self.start = QPoint(0, 0)
self.pressing = False
def resizeEvent(self, QResizeEvent):
super(MyBar, self).resizeEvent(QResizeEvent)
self.title.setFixedWidth(self.parent.width())
def mousePressEvent(self, event):
self.start = self.mapToGlobal(event.pos())
self.pressing = True
def mouseMoveEvent(self, event):
if self.pressing:
self.end = self.mapToGlobal(event.pos())
self.movement = self.end-self.start
self.parent.setGeometry(self.mapToGlobal(self.movement).x(),
self.mapToGlobal(self.movement).y(),
self.parent.width(),
self.parent.height())
self.start = self.end
def mouseReleaseEvent(self, QMouseEvent):
self.pressing = False
def btn_close_clicked(self):
self.parent.close()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
The problem is due to the stacking of new widgets, and the simplest solution would be to set a transparent background for those widgets if you're sure that they won't have a different background when not hovered:
self.checkbox_style='''
QCheckBox
{
background-color : rgb(0, 0, 0, 0);
...
Unfortunately, this will not really solve your problem, as there are serious logical issues in your implementation.
First of all, setting hardcoded content margins of a layout based on the overall layout is a serious problem for many reasons:
it forces you to change those hardcoded margins based on the menu contents;
it is based on your assumption about the current available fonts, which could potentially make the UI elements partially (or completely) hidden;
it doesn't allow you to properly add more objects to the layout;
After seriously considering the above matters (which are very important and should never be underestimated), the problem is that, while Qt allows to lay out items "outside" their possible position through QSS paddings, you need to consider the widget z-level, which by default stacks a new widget above any other previously created sibling widget (widgets that have a common ancestor), and that's why you see partially hidden check boxes.
The base solution would be to install an event filter, check if it's a HoverMove event type and then call raise_() to ensure that it's put above any other sibling:
class MainWindow(QWidget):
def __init__(self):
# ...
for check in self.findChildren(QCheckBox):
check.installEventFilter(self)
def eventFilter(self, obj, event):
if event.type() == event.HoverMove:
obj.raise_()
return super().eventFilter(obj, event)
Again, this will only partially solve your problem, as you'll soon find out that your hardcoded sizes and positions will not work well (remember, what you see on your screen is never what others see on theirs).
A possible solution would be to create a separate QWidget subclass for the menu, add checkboxes in there, and always compute the maximum height considering the padding. Then, to ensure that the widget box is properly put where it should you should add a addStretch() or at least a "main widget" (which is what is normally used for a "main window").
class Menu(QWidget):
padding = 15
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
self.setMaximumHeight(0)
layout.setContentsMargins(0, 15, 0, 15)
font = QtGui.QFont()
font.setFamily("Arial")
font.setPointSize(11)
font.setBold(True)
self.setFont(font)
self.setStyleSheet('''
QCheckBox
{
background-color : rgb(25,34,52);
border-radius : 5px;
spacing : 10px;
padding : 15px;
min-height : 15px;
}
QCheckBox::unchecked
{
color : rgb(159,172,168);
}
QCheckBox::checked
{
color : rgb(217,223,227);
}
QCheckBox::indicator
{
border : 2px solid rgb(105, 139, 194);
width : 12px;
height : 12px;
border-radius : 8px;
}
QCheckBox::indicator:checked
{
image : url(red.png);
border : 2px solid rgb(221, 54, 77);
}
QCheckBox::hover
{
background-color: #263450
}
''')
self.checks = []
def addOption(self, option):
checkBox = QCheckBox(option)
self.checks.append(checkBox)
checkBox.installEventFilter(self)
self.layout().addWidget(checkBox)
count = len(self.checks)
self.setMaximumHeight(count * checkBox.sizeHint().height() - 15)
def eventFilter(self, obj, event):
if event.type() == event.HoverMove:
obj.raise_()
return super().eventFilter(obj, event)
class MainWindow(QWidget):
def __init__(self):
# ...
self.menu = Menu()
self.layout.addWidget(self.menu, alignment=Qt.AlignTop)
for i in range(4):
self.menu.addOption('checkbox{}'.format(i + 1))
self.layout.addStretch()
Note that in the code above I still used an event filter while still using explicit opaque background colors, if you are fine with the parent background, you can avoid installing and using the event filter and only set the default background to the transparent rgb(0, 0, 0, 0).
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:
I'm currently creating a to-do application. I have a side menu (which is just QPushButtons in a vbox) and have a main window widget to show content. However, I need a way to show different content in the main widget based on what side menu button is pressed. I have tried to use QStackedLayout, but I don't like the way it closes the main window and switches to a new one. I've also tried to use QTabWidget, but the tabs are at the top. Is there a way to sub-class QTabWidget and create a custom QTabWidget with the tab buttons on the side? If not, is there a way to do this? The image above is what I have so far.
This is all my code:
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import *
from datetime import date
import sys
months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November",
"December"]
stylesheet = """
QWidget{
background-color: white;
}
QWidget#sideMenuBackground{
background-color: #f7f7f7;
}
QVBoxLayout#sideMenuLayout{
background-color: grey;
}
QPushButton#sideMenuButton{
text-align: left;
border: none;
background-color: #f7f7f7;
max-width: 10em;
font: 16px;
padding: 6px;
}
QPushButton#sideMenuButton:hover{
font: 18px;
}
QLabel#today_label{
font: 25px;
max-width: 70px;
}
QLabel#todays_date_label{
font: 11px;
color: grey;
}
QPushButton#addTodoEventButton{
border: none;
max-width: 130px;
}
"""
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("To-Do Application")
self.setGeometry(200, 200, 800, 500)
self.initUI()
def initUI(self):
self.nextWeekPage = QtWidgets.QLabel()
backgroundWidget = QtWidgets.QWidget()
backgroundWidget.setObjectName("sideMenuBackground")
backgroundWidget.setFixedWidth(150)
layout = QtWidgets.QHBoxLayout()
layout.addWidget(backgroundWidget)
sideMenuLayout = QtWidgets.QVBoxLayout()
sideMenuLayout.setObjectName("sideMenuLayout")
taskLayout = QtWidgets.QVBoxLayout()
backgroundWidget.setLayout(sideMenuLayout)
layout.addLayout(taskLayout)
self.setSideMenu(sideMenuLayout)
sideMenuLayout.addStretch(0)
self.setMainLayout(taskLayout)
taskLayout.addStretch(0)
mainWidget = QtWidgets.QWidget()
mainWidget.setLayout(layout)
self.setCentralWidget(mainWidget)
def setSideMenu(self, layout):
self.todayButton = QtWidgets.QPushButton(" Today")
self.nextWeekButton = QtWidgets.QPushButton("Next 7 Days")
self.calendarButton = QtWidgets.QPushButton("Calendar")
sideMenuButtons = [self.todayButton, self.nextWeekButton, self.calendarButton]
for button in sideMenuButtons:
button.setObjectName("sideMenuButton")
layout.addWidget(button)
sideMenuButtons[0].setIcon(QtGui.QIcon("today icon.png"))
sideMenuButtons[1].setIcon(QtGui.QIcon("week icon.png"))
sideMenuButtons[2].setIcon(QtGui.QIcon("calendar icon.png"))
sideMenuButtons[0].pressed.connect(self.todayButtonPress)
sideMenuButtons[1].pressed.connect(self.nextWeekButtonPress)
sideMenuButtons[2].pressed.connect(self.calendarButtonPress)
def setMainLayout(self, layout):
today_label_widget = QtWidgets.QWidget()
today_label_layout = QtWidgets.QHBoxLayout()
layout.addWidget(today_label_widget)
today_label_widget.setLayout(today_label_layout)
month = date.today().month
day = date.today().day
today = f"{months[month - 1]}{day}"
self.todays_date = QtWidgets.QLabel(today)
self.todays_date.setObjectName("todays_date_label")
self.today_label = QtWidgets.QLabel("Today")
self.today_label.setObjectName("today_label")
self.addTodoEventButton = QtWidgets.QPushButton()
self.addTodoEventButton.setObjectName("addTodoEventButton")
self.addTodoEventButton.setIcon(QtGui.QIcon("add event button.png"))
self.addTodoEventButton.setToolTip("Add To Do Event")
today_label_layout.addWidget(self.today_label)
today_label_layout.addWidget(self.todays_date)
today_label_layout.addWidget(self.addTodoEventButton)
self.labels = ["button1", "button2", "button3", "button4", "Button5"]
for today_events in self.labels:
label = QtWidgets.QLabel(today_events)
layout.addWidget(label)
def addTodoEvent(self):
pass
def todayButtonPress(self):
print("today button pressed")
def nextWeekButtonPress(self):
print("Next week button pressed")
def calendarButtonPress(self):
print("calendar button pressed")
def main():
app = QtWidgets.QApplication(sys.argv)
app.setStyleSheet(stylesheet)
window = MainWindow()
window.show()
app.exec_()
if __name__ == "__main__":
main()
Using a stacked layout shouldn't open a new window when used correctly. The snippet below outlines how the original code could be adapted to use a stacked layout to open different pages in the same window.
class MainWindow(QtWidgets.QMainWindow):
def initUI(self):
# same as before
self.taskLayout = QtWidgets.QStackedLayout()
self.setMainLayout(self.taskLayout)
# same as before
def setMainLayout(self, layout)
today = self.todayWidget()
next_week = self.nextWeekWidget()
calendar_widget = self.calendarWidget()
layout.addWidget(today)
layout.addWidget(next_week)
layout.addWidget(calendar_widget)
def todayWidget(self)
widget = QtWidgets.QWidget(self)
layout = QVBoxLayout(widget)
# setup layout for today's widget
return widget
def nextWeekWidget(self)
widget = QtWidgets.QWidget(self)
layout = QVBoxLayout(widget)
# setup layout for next week's widget
return widget
def calendarWidget(self)
widget = QtWidgets.QWidget(self)
layout = QVBoxLayout(widget)
# setup layout for calendar widget
return widget
def todayButtonPress(self):
self.taskLayout.setCurrentIndex(0)
def nextWeekButtonPress(self):
self.taskLayout.setCurrentIndex(1)
def calendarButtonPress(self):
self.taskLayout.setCurrentIndex(2)
Here is a solution using a custom QListWidget combined with a QStackedWidget.
QListWidget on the left and QStackedWidget on the right, then add a QWidget to it in turn.
When adding a QWidget on the right, there are two variants:
The list on the left is indexed according to the serial number. When adding a widget, the variable name with the serial number is indicated on the right, for example, widget_0, widget_1, widget_2, etc., so that it can be directly associated with the serial number QListWidget.
When an item is added to the list on the left side, the value of the corresponding widget variable is indicated on the right.
from random import randint
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (QWidget, QListWidget, QStackedWidget,
QHBoxLayout, QListWidgetItem, QLabel)
class LeftTabWidget(QWidget):
def __init__(self, *args, **kwargs):
super(LeftTabWidget, self).__init__(*args, **kwargs)
self.resize(800, 600)
# Left and right layout (one QListWidget on the left + QStackedWidget on the right)
layout = QHBoxLayout(self, spacing=0)
layout.setContentsMargins(0, 0, 0, 0)
# List on the left
self.listWidget = QListWidget(self)
layout.addWidget(self.listWidget)
# Cascading window on the right
self.stackedWidget = QStackedWidget(self)
layout.addWidget(self.stackedWidget)
self.initUi()
def initUi(self):
# Initialization interface
# Switch the sequence number in QStackedWidget by the current item change of QListWidget
self.listWidget.currentRowChanged.connect(
self.stackedWidget.setCurrentIndex)
# Remove the border
self.listWidget.setFrameShape(QListWidget.NoFrame)
# Hide scroll bar
self.listWidget.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.listWidget.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# Here we use the general text with the icon mode (you can also use Icon mode, setViewMode directly)
for i in range(5):
item = QListWidgetItem(
QIcon('Ok.png'), str('Option %s' % i), self.listWidget)
# Set the default width and height of the item (only height is useful here)
item.setSizeHint(QSize(16777215, 60))
# Text centered
item.setTextAlignment(Qt.AlignCenter)
# Simulate 5 right-side pages (it won't loop with the top)
for i in range(5):
label = QLabel('This is the page %d' % i, self)
label.setAlignment(Qt.AlignCenter)
# Set the background color of the label (randomly here)
# Added a margin margin here (to easily distinguish between QStackedWidget and QLabel colors)
label.setStyleSheet('background: rgb(%d, %d, %d); margin: 50px;' % (
randint(0, 255), randint(0, 255), randint(0, 255)))
self.stackedWidget.addWidget(label)
# style sheet
Stylesheet = """
QListWidget, QListView, QTreeWidget, QTreeView {
outline: 0px;
}
QListWidget {
min-width: 120px;
max-width: 120px;
color: white;
background: black;
}
QListWidget::item:selected {
background: rgb(52, 52, 52);
border-left: 2px solid rgb(9, 187, 7);
}
HistoryPanel::item:hover {background: rgb(52, 52, 52);}
QStackedWidget {background: rgb(30, 30, 30);}
QLabel {color: white;}
"""
if __name__ == '__main__':
import sys
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
app.setStyleSheet(Stylesheet)
w = LeftTabWidget()
w.show()
sys.exit(app.exec_())
I'd like to set the menu and title in one bar, but have no idea that how to layout the menu bar and title bar (or my own title bar).
# -*- coding:utf-8 -*-
from PyQt5 import QtWidgets,QtGui,QtCore
import sys
qss = ""
class UI(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setui()
def setui(self):
#----------main-window----------------------
self.setGeometry(0,0,1366,768) #x,y,w,h
self.setWindowTitle('hello world')
self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
#----------menu-bar---------------------
#--------file-menu-----
self.menu_file=self.menuBar().addMenu('file')
self.menu_file_open=self.menu_file.addAction('open')
self.menu_file_save=self.menu_file.addAction('save')
self.menu_file_saveas=self.menu_file.addAction('save as...')
self.menu_file_quit=self.menu_file.addAction('exit')
#-----------experient-menu----------
self.menu_work=self.menuBar().addMenu('work')
#-------------analysis-menu---------
self.menu_analysis=self.menuBar().addMenu('analysis')
#------------edit-menu--------------
self.menu_edit=self.menuBar().addMenu('edit')
#------------window-menu--------------
self.menu_window=self.menuBar().addMenu('window')
#------------help---menu--------------
self.menu_help=self.menuBar().addMenu('help')
#-------------set---qss----------------------
self.setStyleSheet(qss)
#-------functions--connect-------------------
self.menu_file_quit.triggered.connect(QtWidgets.qApp.quit)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
ex = UI()
sys.exit(app.exec_())
I expect a bar include icon, menus, title and the three buttons, just like the menu bar of vscode.
While the answer provided by #SNick is fine, I'd like to add my own proposal.
It is a bit more complex, but has some features his solution is missing and that are quite important in my opinion.
it actually uses a QMainWindow;
it can be used with ui files created in Designer. I only tried using uic.loadUi, but I think it could work with files created with pyuic too;
since it can use ui files, you can create the menu directly in designer;
being a QMainWindow, the statusBar is an actual QStatusBar;
supports the current system theme style and colors;
under Windows, it supports the window system menu;
has no size contraints, as the menu and title resize themselves according to the available space
That said, it's not perfect. So far, the only drawback I found is that it doesn't support toolbars and dockwidgets in the top area; I think it could be done, but it would be a bit more complicated.
The trick is to create a QMenuBar (or use the one set in Designer) that is not a native one, which is very important for MacOS, and add a top margin to the central widget, then use an internal QStyleOptionTitleBar to compute its size and draw its contents.
Be aware that whenever you want to add menubar items/menu, set other properties, or manually set a central widget, you must ensure that you do that after the *** END OF SETUP *** comment.
import sys
if sys.platform == 'win32':
import win32gui
import win32con
from PyQt5 import QtCore, QtGui, QtWidgets
# a "fake" button class that we need for hover and click events
class HiddenButton(QtWidgets.QPushButton):
hover = QtCore.pyqtSignal()
def __init__(self, parent):
super(HiddenButton, self).__init__(parent)
# prevent any painting to keep this button "invisible" while
# still reacting to its events
self.setUpdatesEnabled(False)
self.setFocusPolicy(QtCore.Qt.NoFocus)
def enterEvent(self, event):
self.hover.emit()
def leaveEvent(self, event):
self.hover.emit()
class SpecialTitleWindow(QtWidgets.QMainWindow):
__watchedActions = (
QtCore.QEvent.ActionAdded,
QtCore.QEvent.ActionChanged,
QtCore.QEvent.ActionRemoved
)
titleOpt = None
__menuBar = None
__titleBarMousePos = None
__sysMenuLock = False
__topMargin = 0
def __init__(self):
super(SpecialTitleWindow, self).__init__()
self.widgetHelpers = []
# uic.loadUi('titlebar.ui', self)
# enable the system menu
self.setWindowFlags(
QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowSystemMenuHint
)
# set the WindowActive to ensure that the title bar is painted as active
self.setWindowState(self.windowState() | QtCore.Qt.WindowActive)
# create a StyleOption that we need for painting and computing sizes
self.titleOpt = QtWidgets.QStyleOptionTitleBar()
self.titleOpt.initFrom(self)
self.titleOpt.titleBarFlags = (
QtCore.Qt.Window | QtCore.Qt.MSWindowsOwnDC |
QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMinMaxButtonsHint |
QtCore.Qt.WindowCloseButtonHint
)
self.titleOpt.state |= (QtWidgets.QStyle.State_Active |
QtWidgets.QStyle.State_HasFocus)
self.titleOpt.titleBarState = (int(self.windowState()) |
int(QtWidgets.QStyle.State_Active))
# create "fake" buttons
self.systemButton = HiddenButton(self)
self.systemButton.pressed.connect(self.showSystemMenu)
self.minimizeButton = HiddenButton(self)
self.minimizeButton.hover.connect(self.checkHoverStates)
self.minimizeButton.clicked.connect(self.minimize)
self.maximizeButton = HiddenButton(self)
self.maximizeButton.hover.connect(self.checkHoverStates)
self.maximizeButton.clicked.connect(self.maximize)
self.closeButton = HiddenButton(self)
self.closeButton.hover.connect(self.checkHoverStates)
self.closeButton.clicked.connect(self.close)
self.ctrlButtons = {
QtWidgets.QStyle.SC_TitleBarMinButton: self.minimizeButton,
QtWidgets.QStyle.SC_TitleBarMaxButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarNormalButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarCloseButton: self.closeButton,
}
self.widgetHelpers.extend([self.minimizeButton, self.maximizeButton, self.closeButton])
self.resetTitleHeight()
# *** END OF SETUP ***
fileMenu = self.menuBar().addMenu('File')
fileMenu.addAction('Open')
fileMenu.addAction('Save')
workMenu = self.menuBar().addMenu('Work')
workMenu.addAction('Work something')
analysisMenu = self.menuBar().addMenu('Analysis')
analysisMenu.addAction('Analize action')
# just call the statusBar to create one, we use it for resizing purposes
self.statusBar()
def resetTitleHeight(self):
# minimum height for the menu can change everytime an action is added,
# removed or modified; let's update it accordingly
if not self.titleOpt:
return
# set the minimum height of the titlebar
self.titleHeight = max(
self.style().pixelMetric(
QtWidgets.QStyle.PM_TitleBarHeight, self.titleOpt, self),
self.menuBar().sizeHint().height()
)
self.titleOpt.rect.setHeight(self.titleHeight)
self.menuBar().setMaximumHeight(self.titleHeight)
if self.minimumHeight() < self.titleHeight:
self.setMinimumHeight(self.titleHeight)
def checkHoverStates(self):
if not self.titleOpt:
return
# update the window buttons when hovering
pos = self.mapFromGlobal(QtGui.QCursor.pos())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, ctrl, self)
# since the maximize button can become a "restore", ensure that it
# actually exists according to the current state, if the rect
# has an actual size
if rect and pos in rect:
self.titleOpt.activeSubControls = ctrl
self.titleOpt.state |= QtWidgets.QStyle.State_MouseOver
break
else:
# no hover
self.titleOpt.state &= ~QtWidgets.QStyle.State_MouseOver
self.titleOpt.activeSubControls = QtWidgets.QStyle.SC_None
self.titleOpt.state |= QtWidgets.QStyle.State_Active
self.update()
def showSystemMenu(self, pos=None):
# show the system menu on windows
if sys.platform != 'win32':
return
if self.__sysMenuLock:
self.__sysMenuLock = False
return
winId = int(self.effectiveWinId())
sysMenu = win32gui.GetSystemMenu(winId, False)
if pos is None:
pos = self.systemButton.mapToGlobal(self.systemButton.rect().bottomLeft())
self.__sysMenuLock = True
cmd = win32gui.TrackPopupMenu(sysMenu,
win32gui.TPM_LEFTALIGN | win32gui.TPM_TOPALIGN | win32gui.TPM_RETURNCMD,
pos.x(), pos.y(), 0, winId, None)
win32gui.PostMessage(winId, win32con.WM_SYSCOMMAND, cmd, 0)
# restore the menu lock to hide it when clicking the system menu icon
QtCore.QTimer.singleShot(0, lambda: setattr(self, '__sysMenuLock', False))
def actualWindowTitle(self):
# window title can show "*" for modified windows
title = self.windowTitle()
if title:
title = title.replace('[*]', '*' if self.isWindowModified() else '')
return title
def updateTitleBar(self):
# compute again sizes when resizing or changing window title
menuWidth = self.menuBar().sizeHint().width()
availableRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
left = availableRect.left()
if self.menuBar().sizeHint().height() < self.titleHeight:
top = (self.titleHeight - self.menuBar().sizeHint().height()) // 2
height = self.menuBar().sizeHint().height()
else:
top = 0
height = self.titleHeight
title = self.actualWindowTitle()
titleWidth = self.fontMetrics().width(title)
if not title and menuWidth > availableRect.width():
# resize the menubar to its maximum, but without hiding the buttons
width = availableRect.width()
elif menuWidth + titleWidth > availableRect.width():
# if the menubar and title require more than the available space,
# divide it equally, giving precedence to the window title space,
# since it is also necessary for window movement
width = availableRect.width() // 2
if menuWidth > titleWidth:
width = max(left, min(availableRect.width() - titleWidth, width))
# keep a minimum size for the menu arrow
if availableRect.width() - width < left:
width = left
extButton = self.menuBar().findChild(QtWidgets.QToolButton, 'qt_menubar_ext_button')
if self.isVisible() and extButton:
# if the "extButton" is visible (meaning that some item
# is hidden due to the menubar cannot be completely shown)
# resize to the last visible item + extButton, so that
# there's as much space available for the title
minWidth = extButton.width()
menuBar = self.menuBar()
spacing = self.style().pixelMetric(QtWidgets.QStyle.PM_MenuBarItemSpacing)
for i, action in enumerate(menuBar.actions()):
actionWidth = menuBar.actionGeometry(action).width()
if minWidth + actionWidth > width:
width = minWidth
break
minWidth += actionWidth + spacing
else:
width = menuWidth
self.menuBar().setGeometry(left, top, width, height)
# ensure that our internal widget are always on top
for w in self.widgetHelpers:
w.raise_()
self.update()
# helper function to avoid "ugly" colors on menubar items
def __setMenuBar(self, menuBar):
if self.__menuBar:
if self.__menuBar in self.widgetHelpers:
self.widgetHelpers.remove(self.__menuBar)
self.__menuBar.removeEventFilter(self)
self.__menuBar = menuBar
self.widgetHelpers.append(menuBar)
self.__menuBar.installEventFilter(self)
self.__menuBar.setNativeMenuBar(False)
self.__menuBar.setStyleSheet('''
QMenuBar {
background-color: transparent;
}
QMenuBar::item {
background-color: transparent;
}
QMenuBar::item:selected {
background-color: palette(button);
}
''')
def setMenuBar(self, menuBar):
self.__setMenuBar(menuBar)
def menuBar(self):
# QMainWindow.menuBar() returns a new blank menu bar if none exists
if not self.__menuBar:
self.__setMenuBar(QtWidgets.QMenuBar(self))
return self.__menuBar
def setCentralWidget(self, widget):
if self.centralWidget():
self.centralWidget().removeEventFilter(self)
# store the top content margin, we need it later
l, self.__topMargin, r, b = widget.getContentsMargins()
super(SpecialTitleWindow, self).setCentralWidget(widget)
# since the central widget always uses all the available space and can
# capture mouse events, install an event filter to catch them and
# allow us to grab them
widget.installEventFilter(self)
def eventFilter(self, source, event):
if source == self.centralWidget():
# do not propagate mouse press events to the centralWidget!
if (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and
event.y() <= self.titleHeight):
self.__titleBarMousePos = event.pos()
event.accept()
return True
elif source == self.__menuBar and event.type() in self.__watchedActions:
self.resetTitleHeight()
return super(SpecialTitleWindow, self).eventFilter(source, event)
def minimize(self):
self.setWindowState(QtCore.Qt.WindowMinimized)
def maximize(self):
if self.windowState() & QtCore.Qt.WindowMaximized:
self.setWindowState(
self.windowState() & (~QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive))
else:
self.setWindowState(
self.windowState() | QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive)
# whenever a window is resized, its button states have to be checked again
self.checkHoverStates()
def contextMenuEvent(self, event):
if event.pos() not in self.menuBar().geometry():
self.showSystemMenu(event.globalPos())
def mousePressEvent(self, event):
if not self.centralWidget() and (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and event.y() <= self.titleHeight):
self.__titleBarMousePos = event.pos()
def mouseMoveEvent(self, event):
super(SpecialTitleWindow, self).mouseMoveEvent(event)
if event.buttons() == QtCore.Qt.LeftButton and self.__titleBarMousePos:
# move the window
self.move(self.pos() + event.pos() - self.__titleBarMousePos)
def mouseDoubleClickEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.maximize()
def mouseReleaseEvent(self, event):
super(SpecialTitleWindow, self).mouseReleaseEvent(event)
self.__titleBarMousePos = None
def changeEvent(self, event):
# change the appearance of the titlebar according to the window state
if event.type() == QtCore.QEvent.ActivationChange:
if self.isActiveWindow():
self.titleOpt.titleBarState = (
int(self.windowState()) | int(QtWidgets.QStyle.State_Active))
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Active)
else:
self.titleOpt.titleBarState = 0
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Inactive)
self.update()
elif event.type() == QtCore.QEvent.WindowStateChange:
self.checkHoverStates()
elif event.type() == QtCore.QEvent.WindowTitleChange:
if self.titleOpt:
self.updateTitleBar()
def showEvent(self, event):
if not event.spontaneous():
# update the titlebar as soon as it's shown, to ensure that
# most of the title text is visible
self.updateTitleBar()
def resizeEvent(self, event):
super(SpecialTitleWindow, self).resizeEvent(event)
# update the centralWidget contents margins, adding the titlebar height
# to the top margin found before
if (self.centralWidget() and
self.centralWidget().getContentsMargins()[1] + self.__topMargin != self.titleHeight):
l, t, r, b = self.centralWidget().getContentsMargins()
self.centralWidget().setContentsMargins(
l, self.titleHeight + self.__topMargin, r, b)
# resize the width of the titlebar option, and move its buttons
self.titleOpt.rect.setWidth(self.width())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(
QtWidgets.QStyle.CC_TitleBar, self.titleOpt, ctrl, self)
if rect:
btn.setGeometry(rect)
sysRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarSysMenu, self)
if sysRect:
self.systemButton.setGeometry(sysRect)
self.titleOpt.titleBarState = int(self.windowState())
if self.isActiveWindow():
self.titleOpt.titleBarState |= int(QtWidgets.QStyle.State_Active)
self.updateTitleBar()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
self.style().drawComplexControl(QtWidgets.QStyle.CC_TitleBar, self.titleOpt, qp, self)
titleRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
icon = self.windowIcon()
if not icon.isNull():
iconRect = QtCore.QRect(0, 0, titleRect.left(), self.titleHeight)
qp.drawPixmap(iconRect, icon.pixmap(iconRect.size()))
title = self.actualWindowTitle()
titleRect.setLeft(self.menuBar().geometry().right())
if title:
# move left of the rectangle available for the title to the right of
# the menubar; if the title is bigger than the available space, elide it
elided = self.fontMetrics().elidedText(
title, QtCore.Qt.ElideRight, titleRect.width() - 2)
qp.drawText(titleRect, QtCore.Qt.AlignCenter, elided)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = SpecialTitleWindow()
w.setWindowTitle('My window')
w.show()
sys.exit(app.exec_())
Try it:
import sys
from PyQt5.QtCore import pyqtSlot, QPoint, Qt, QRect
from PyQt5.QtWidgets import (QMainWindow, QApplication, QPushButton, QHBoxLayout,
QVBoxLayout, QTabWidget, QWidget, QAction,
QLabel, QSizeGrip, QMenuBar, qApp)
from PyQt5.QtGui import QIcon
class TitleBar(QWidget):
height = 35
def __init__(self, parent):
super(TitleBar, self).__init__()
self.parent = parent
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.menu_bar = QMenuBar()
self.menu_bar.setStyleSheet("""
color: #fff;
background-color: #23272A;
font-size: 14px;
padding: 4px;
""")
self.menu_file = self.menu_bar.addMenu('file')
self.menu_file_open=self.menu_file.addAction('open')
self.menu_file_save=self.menu_file.addAction('save')
self.menu_file_saveas=self.menu_file.addAction('save as...')
self.menu_file_quit=self.menu_file.addAction('exit')
self.menu_file_quit.triggered.connect(qApp.quit)
self.menu_work=self.menu_bar.addMenu('work')
self.menu_analysis=self.menu_bar.addMenu('analysis')
self.menu_edit=self.menu_bar.addMenu('edit')
self.menu_window=self.menu_bar.addMenu('window')
self.menu_help=self.menu_bar.addMenu('help')
self.layout.addWidget(self.menu_bar)
self.title = QLabel("Hello World!")
self.title.setFixedHeight(self.height)
self.layout.addWidget(self.title)
self.title.setStyleSheet("""
background-color: #23272a; /* 23272a #f00*/
font-weight: bold;
font-size: 16px;
color: blue;
padding-left: 170px;
""")
self.closeButton = QPushButton(' ')
self.closeButton.clicked.connect(self.on_click_close)
self.closeButton.setStyleSheet("""
background-color: #DC143C;
border-radius: 10px;
height: {};
width: {};
margin-right: 3px;
font-weight: bold;
color: #000;
font-family: "Webdings";
qproperty-text: "r";
""".format(self.height/1.7,self.height/1.7))
self.maxButton = QPushButton(' ')
self.maxButton.clicked.connect(self.on_click_maximize)
self.maxButton.setStyleSheet("""
background-color: #32CD32;
border-radius: 10px;
height: {};
width: {};
margin-right: 3px;
font-weight: bold;
color: #000;
font-family: "Webdings";
qproperty-text: "1";
""".format(self.height/1.7,self.height/1.7))
self.hideButton = QPushButton(' ')
self.hideButton.clicked.connect(self.on_click_hide)
self.hideButton.setStyleSheet("""
background-color: #FFFF00;
border-radius: 10px;
height: {};
width: {};
margin-right: 3px;
font-weight: bold;
color: #000;
font-family: "Webdings";
qproperty-text: "0";
""".format(self.height/1.7,self.height/1.7))
self.layout.addWidget(self.hideButton)
self.layout.addWidget(self.maxButton)
self.layout.addWidget(self.closeButton)
self.setLayout(self.layout)
self.start = QPoint(0, 0)
self.pressing = False
self.maximaze = False
def resizeEvent(self, QResizeEvent):
super(TitleBar, self).resizeEvent(QResizeEvent)
self.title.setFixedWidth(self.parent.width())
def mousePressEvent(self, event):
self.start = self.mapToGlobal(event.pos())
self.pressing = True
def mouseMoveEvent(self, event):
if self.pressing:
self.end = self.mapToGlobal(event.pos())
self.movement = self.end-self.start
self.parent.move(self.mapToGlobal(self.movement))
self.start = self.end
def mouseReleaseEvent(self, QMouseEvent):
self.pressing = False
def on_click_close(self):
sys.exit()
def on_click_maximize(self):
self.maximaze = not self.maximaze
if self.maximaze: self.parent.setWindowState(Qt.WindowNoState)
if not self.maximaze:
self.parent.setWindowState(Qt.WindowMaximized)
def on_click_hide(self):
self.parent.showMinimized()
class StatusBar(QWidget):
def __init__(self, parent):
super(StatusBar, self).__init__()
self.initUI()
self.showMessage("showMessage: Hello world!")
def initUI(self):
self.label = QLabel("Status bar...")
self.label.setFixedHeight(24)
self.label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
self.label.setStyleSheet("""
background-color: #23272a;
font-size: 12px;
padding-left: 5px;
color: white;
""")
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.layout.addWidget(self.label)
self.setLayout(self.layout)
def showMessage(self, text):
self.label.setText(text)
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setFixedSize(800, 400)
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
self.setStyleSheet("background-color: #2c2f33;")
self.setWindowTitle('Code Maker')
self.title_bar = TitleBar(self)
self.status_bar = StatusBar(self)
self.layout = QVBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.layout.addWidget(self.title_bar)
self.layout.addStretch(1)
self.layout.addWidget(self.status_bar)
self.layout.setSpacing(0)
self.setLayout(self.layout)
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
I did a few patches so that the code provided by #musicamante can work on PySide6, but I found that when moving the window, it's shaking. When I looked at the mouseMoveEvent and read qt's document about event.pos() in QMouseEvent, which says:
If you move the widget as a result of the mouse event, use the global position returned by globalPos() to avoid a shaking motion.
So I replaced
self.move(self.pos() + event.pos() - self.__titleBarMousePos)
with
self.move(event.globalPos() - self.__titleBarMousePos)
in mouseMoveEvent()
Here is the PySide6 version:
import sys
if sys.platform == 'win32':
import win32gui
import win32con
from PySide6 import QtCore, QtGui, QtWidgets
# a "fake" button class that we need for hover and click events
class HiddenButton(QtWidgets.QPushButton):
hover = QtCore.Signal()
def __init__(self, parent):
super(HiddenButton, self).__init__(parent)
# prevent any painting to keep this button "invisible" while
# still reacting to its events
self.setUpdatesEnabled(False)
self.setFocusPolicy(QtCore.Qt.NoFocus)
def enterEvent(self, event):
self.hover.emit()
def leaveEvent(self, event):
self.hover.emit()
class SpecialTitleWindow(QtWidgets.QMainWindow):
__watchedActions = (
QtCore.QEvent.ActionAdded,
QtCore.QEvent.ActionChanged,
QtCore.QEvent.ActionRemoved
)
titleOpt = None
__menuBar = None
__titleBarMousePos = None
__sysMenuLock = False
__topMargin = 0
def __init__(self):
super(SpecialTitleWindow, self).__init__()
self.widgetHelpers = []
# uic.loadUi('titlebar.ui', self)
# enable the system menu
self.setWindowFlags(
QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowSystemMenuHint
)
# set the WindowActive to ensure that the title bar is painted as active
self.setWindowState(self.windowState() | QtCore.Qt.WindowActive)
# create a StyleOption that we need for painting and computing sizes
self.titleOpt = QtWidgets.QStyleOptionTitleBar()
self.titleOpt.initFrom(self)
self.titleOpt.titleBarFlags = (
QtCore.Qt.Window | QtCore.Qt.MSWindowsOwnDC |
QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowMinMaxButtonsHint |
QtCore.Qt.WindowCloseButtonHint
)
self.titleOpt.state |= (QtWidgets.QStyle.State_Active |
QtWidgets.QStyle.State_HasFocus)
self.titleOpt.titleBarState = (int(self.windowState()) |
int(QtWidgets.QStyle.State_Active))
# create "fake" buttons
self.systemButton = HiddenButton(self)
self.systemButton.pressed.connect(self.showSystemMenu)
self.minimizeButton = HiddenButton(self)
self.minimizeButton.hover.connect(self.checkHoverStates)
self.minimizeButton.clicked.connect(self.minimize)
self.maximizeButton = HiddenButton(self)
self.maximizeButton.hover.connect(self.checkHoverStates)
self.maximizeButton.clicked.connect(self.maximize)
self.closeButton = HiddenButton(self)
self.closeButton.hover.connect(self.checkHoverStates)
self.closeButton.clicked.connect(self.close)
self.ctrlButtons = {
QtWidgets.QStyle.SC_TitleBarMinButton: self.minimizeButton,
QtWidgets.QStyle.SC_TitleBarMaxButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarNormalButton: self.maximizeButton,
QtWidgets.QStyle.SC_TitleBarCloseButton: self.closeButton,
}
self.widgetHelpers.extend([self.minimizeButton, self.maximizeButton, self.closeButton])
self.resetTitleHeight()
# *** END OF SETUP ***
fileMenu = self.menuBar().addMenu('File')
fileMenu.addAction('Open')
fileMenu.addAction('Save')
workMenu = self.menuBar().addMenu('Work')
workMenu.addAction('Work something')
analysisMenu = self.menuBar().addMenu('Analysis')
analysisMenu.addAction('Analize action')
# just call the statusBar to create one, we use it for resizing purposes
self.statusBar()
self.resize(400, 250)
def resetTitleHeight(self):
# minimum height for the menu can change everytime an action is added,
# removed or modified; let's update it accordingly
if not self.titleOpt:
return
# set the minimum height of the titlebar
self.titleHeight = max(
self.style().pixelMetric(
QtWidgets.QStyle.PM_TitleBarHeight, self.titleOpt, self),
self.menuBar().sizeHint().height()
)
self.titleOpt.rect.setHeight(self.titleHeight)
self.menuBar().setMaximumHeight(self.titleHeight)
if self.minimumHeight() < self.titleHeight:
self.setMinimumHeight(self.titleHeight)
def checkHoverStates(self):
if not self.titleOpt:
return
# update the window buttons when hovering
pos = self.mapFromGlobal(QtGui.QCursor.pos())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, ctrl, self)
# since the maximize button can become a "restore", ensure that it
# actually exists according to the current state, if the rect
# has an actual size
if rect and rect.contains(pos):
self.titleOpt.activeSubControls = ctrl
self.titleOpt.state |= QtWidgets.QStyle.State_MouseOver
break
else:
# no hover
self.titleOpt.state &= ~QtWidgets.QStyle.State_MouseOver
self.titleOpt.activeSubControls = QtWidgets.QStyle.SC_None
self.titleOpt.state |= QtWidgets.QStyle.State_Active
self.update()
def showSystemMenu(self, pos=None):
# show the system menu on windows
if sys.platform != 'win32':
return
if self.__sysMenuLock:
self.__sysMenuLock = False
return
winId = int(self.effectiveWinId())
sysMenu = win32gui.GetSystemMenu(winId, False)
if pos is None:
pos = self.systemButton.mapToGlobal(self.systemButton.rect().bottomLeft())
self.__sysMenuLock = True
cmd = win32gui.TrackPopupMenu(sysMenu,
win32gui.TPM_LEFTALIGN | win32gui.TPM_TOPALIGN | win32gui.TPM_RETURNCMD,
pos.x(), pos.y(), 0, winId, None)
win32gui.PostMessage(winId, win32con.WM_SYSCOMMAND, cmd, 0)
# restore the menu lock to hide it when clicking the system menu icon
QtCore.QTimer.singleShot(0, lambda: setattr(self, '__sysMenuLock', False))
def actualWindowTitle(self):
# window title can show "*" for modified windows
title = self.windowTitle()
if title:
title = title.replace('[*]', '*' if self.isWindowModified() else '')
return title
def updateTitleBar(self):
# compute again sizes when resizing or changing window title
menuWidth = self.menuBar().sizeHint().width()
availableRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
left = availableRect.left()
if self.menuBar().sizeHint().height() < self.titleHeight:
top = (self.titleHeight - self.menuBar().sizeHint().height()) // 2
height = self.menuBar().sizeHint().height()
else:
top = 0
height = self.titleHeight
title = self.actualWindowTitle()
titleWidth = self.fontMetrics().boundingRect(title).width()
if not title and menuWidth > availableRect.width():
# resize the menubar to its maximum, but without hiding the buttons
width = availableRect.width()
elif menuWidth + titleWidth > availableRect.width():
# if the menubar and title require more than the available space,
# divide it equally, giving precedence to the window title space,
# since it is also necessary for window movement
width = availableRect.width() // 2
if menuWidth > titleWidth:
width = max(left, min(availableRect.width() - titleWidth, width))
# keep a minimum size for the menu arrow
if availableRect.width() - width < left:
width = left
extButton = self.menuBar().findChild(QtWidgets.QToolButton, 'qt_menubar_ext_button')
if self.isVisible() and extButton:
# if the "extButton" is visible (meaning that some item
# is hidden due to the menubar cannot be completely shown)
# resize to the last visible item + extButton, so that
# there's as much space available for the title
minWidth = extButton.width()
menuBar = self.menuBar()
spacing = self.style().pixelMetric(QtWidgets.QStyle.PM_MenuBarItemSpacing)
for i, action in enumerate(menuBar.actions()):
actionWidth = menuBar.actionGeometry(action).width()
if minWidth + actionWidth > width:
width = minWidth
break
minWidth += actionWidth + spacing
else:
width = menuWidth
self.menuBar().setGeometry(left, top, width, height)
# ensure that our internal widget are always on top
for w in self.widgetHelpers:
w.raise_()
self.update()
# helper function to avoid "ugly" colors on menubar items
def __setMenuBar(self, menuBar):
if self.__menuBar:
if self.__menuBar in self.widgetHelpers:
self.widgetHelpers.remove(self.__menuBar)
self.__menuBar.removeEventFilter(self)
self.__menuBar = menuBar
self.widgetHelpers.append(menuBar)
self.__menuBar.installEventFilter(self)
self.__menuBar.setNativeMenuBar(False)
self.__menuBar.setStyleSheet('''
QMenuBar {
background-color: transparent;
}
QMenuBar::item {
background-color: transparent;
}
QMenuBar::item:selected {
background-color: palette(button);
}
''')
def setMenuBar(self, menuBar):
self.__setMenuBar(menuBar)
def menuBar(self):
# QMainWindow.menuBar() returns a new blank menu bar if none exists
if not self.__menuBar:
self.__setMenuBar(QtWidgets.QMenuBar(self))
return self.__menuBar
def setCentralWidget(self, widget):
if self.centralWidget():
self.centralWidget().removeEventFilter(self)
# store the top content margin, we need it later
l, self.__topMargin, r, b = widget.contentsMargins()
super(SpecialTitleWindow, self).setCentralWidget(widget)
# since the central widget always uses all the available space and can
# capture mouse events, install an event filter to catch them and
# allow us to grab them
widget.installEventFilter(self)
def eventFilter(self, source, event):
if source == self.centralWidget():
# do not propagate mouse press events to the centralWidget!
if (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and
event.y() <= self.titleHeight):
self.__titleBarMousePos = event.pos()
event.accept()
return True
elif source == self.__menuBar and event.type() in self.__watchedActions:
self.resetTitleHeight()
return super(SpecialTitleWindow, self).eventFilter(source, event)
def minimize(self):
self.setWindowState(QtCore.Qt.WindowMinimized)
def maximize(self):
if self.windowState() & QtCore.Qt.WindowMaximized:
self.setWindowState(
self.windowState() & (~QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive))
else:
self.setWindowState(
self.windowState() | QtCore.Qt.WindowMaximized | QtCore.Qt.WindowActive)
# whenever a window is resized, its button states have to be checked again
self.checkHoverStates()
def contextMenuEvent(self, event):
if not self.menuBar().geometry().contains(event.pos()):
self.showSystemMenu(event.globalPos())
def mousePressEvent(self, event):
if not self.centralWidget() and (event.type() == QtCore.QEvent.MouseButtonPress and
event.button() == QtCore.Qt.LeftButton and event.position().y() <= self.titleHeight):
self.__titleBarMousePos = event.position()
def mouseMoveEvent(self, event):
super(SpecialTitleWindow, self).mouseMoveEvent(event)
if event.buttons() == QtCore.Qt.LeftButton and self.__titleBarMousePos:
# move the window
self.move((event.globalPosition() - self.__titleBarMousePos).toPoint())
def mouseDoubleClickEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.maximize()
def mouseReleaseEvent(self, event):
super(SpecialTitleWindow, self).mouseReleaseEvent(event)
self.__titleBarMousePos = None
def changeEvent(self, event):
# change the appearance of the titlebar according to the window state
if event.type() == QtCore.QEvent.ActivationChange:
if self.isActiveWindow():
self.titleOpt.titleBarState = (
int(self.windowState()) | int(QtWidgets.QStyle.State_Active))
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Active)
else:
self.titleOpt.titleBarState = 0
self.titleOpt.palette.setCurrentColorGroup(QtGui.QPalette.Inactive)
self.update()
elif event.type() == QtCore.QEvent.WindowStateChange:
self.checkHoverStates()
elif event.type() == QtCore.QEvent.WindowTitleChange:
if self.titleOpt:
self.updateTitleBar()
def showEvent(self, event):
if not event.spontaneous():
# update the titlebar as soon as it's shown, to ensure that
# most of the title text is visible
self.updateTitleBar()
def resizeEvent(self, event):
super(SpecialTitleWindow, self).resizeEvent(event)
# update the centralWidget contents margins, adding the titlebar height
# to the top margin found before
if (self.centralWidget() and
self.centralWidget().getContentsMargins()[1] + self.__topMargin != self.titleHeight):
l, t, r, b = self.centralWidget().getContentsMargins()
self.centralWidget().setContentsMargins(
l, self.titleHeight + self.__topMargin, r, b)
# resize the width of the titlebar option, and move its buttons
self.titleOpt.rect.setWidth(self.width())
for ctrl, btn in self.ctrlButtons.items():
rect = self.style().subControlRect(
QtWidgets.QStyle.CC_TitleBar, self.titleOpt, ctrl, self)
if rect:
btn.setGeometry(rect)
sysRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarSysMenu, self)
if sysRect:
self.systemButton.setGeometry(sysRect)
self.titleOpt.titleBarState = int(self.windowState())
if self.isActiveWindow():
self.titleOpt.titleBarState |= int(QtWidgets.QStyle.State_Active)
self.updateTitleBar()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
self.style().drawComplexControl(QtWidgets.QStyle.CC_TitleBar, self.titleOpt, qp, self)
titleRect = self.style().subControlRect(QtWidgets.QStyle.CC_TitleBar,
self.titleOpt, QtWidgets.QStyle.SC_TitleBarLabel, self)
icon = self.windowIcon()
if not icon.isNull():
iconRect = QtCore.QRect(0, 0, titleRect.left(), self.titleHeight)
qp.drawPixmap(iconRect, icon.pixmap(iconRect.size()))
title = self.actualWindowTitle()
titleRect.setLeft(self.menuBar().geometry().right())
if title:
# move left of the rectangle available for the title to the right of
# the menubar; if the title is bigger than the available space, elide it
elided = self.fontMetrics().elidedText(
title, QtCore.Qt.ElideRight, titleRect.width() - 2)
qp.drawText(titleRect, QtCore.Qt.AlignCenter, elided)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = SpecialTitleWindow()
w.setWindowTitle('My window')
w.show()
sys.exit(app.exec())