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).
Related
I am trying to override the paintEvent() of QMenu to make it have rounded corners.
The context menu should look something like this.
Here is the code I have tried But nothing appears:
from PyQt5 import QtWidgets, QtGui, QtCore
import sys
class Example(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Context menu')
self.show()
def contextMenuEvent(self, event):
cmenu = AddContextMenu(self)
newAct = cmenu.addAction("New")
openAct = cmenu.addAction("Open")
quitAct = cmenu.addAction("Quit")
action = cmenu.exec_(self.mapToGlobal(event.pos()))
class AddContextMenu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super(AddContextMenu, self).__init__()
self.painter = QtGui.QPainter(self)
self.setMinimumSize(150, 200)
self.pen = QtGui.QPen(QtCore.Qt.red)
#self.setStyleSheet('color:white; background:gray; border-radius:4px; border:2px solid white;')
def paintEvent(self, event) -> None:
self.pen.setWidth(2)
self.painter.setPen(self.pen)
self.painter.setBrush(QtGui.QBrush(QtCore.Qt.blue))
self.painter.drawRoundedRect(10, 10, 100, 100, 4.0, 4.0)
self.update()
#self.repaint()
#super(AddContextMenu, self).paintEvent(event)
def main():
app = QtWidgets.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Note: setting a style sheet doesn't work for me:
this is what I get when using the style sheet It isn't completely rounded.
This is the paintEvent after #musicamante suggestion(This is just for him/her to check)
def paintEvent(self, event) -> None:
painter = QtGui.QPainter(self)
#self.pen.setColor(QtCore.Qt.white)
#painter.setFont(QtGui.QFont("times", 22))
#painter.setPen(self.pen)
#painter.drawText(QtCore.QPointF(0, 0), 'Hello')
self.pen.setColor(QtCore.Qt.red)
painter.setPen(self.pen)
painter.setBrush(QtCore.Qt.gray)
painter.drawRoundedRect(self.rect(), 20.0, 20.0)
and in the init()
self.pen = QtGui.QPen(QtCore.Qt.red)
self.pen.setWidth(2)
I cannot comment on the paintEvent functionality, but it is possible to implement rounded corners using style-sheets. Some qmenu attributes have to be modified in order to disable the default rectangle in the background, which gave you the unwanted result.
Here is a modified version of your Example using style-sheets + custom flags (no frame + transparent background):
from PyQt5 import QtWidgets, QtCore
import sys
class Example(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Context menu')
self.show()
def contextMenuEvent(self, event):
cmenu = QtWidgets.QMenu()
# disable default frame and background
cmenu.setWindowFlags(QtCore.Qt.FramelessWindowHint)
cmenu.setAttribute(QtCore.Qt.WA_TranslucentBackground)
# set stylesheet, add some padding to avoid overlap of selection with rounded corner
cmenu.setStyleSheet("""
QMenu{
background-color: rgb(255, 255, 255);
border-radius: 20px;
}
QMenu::item {
background-color: transparent;
padding:3px 20px;
margin:5px 10px;
}
QMenu::item:selected { background-color: gray; }
""")
newAct = cmenu.addAction("New")
openAct = cmenu.addAction("Open")
quitAct = cmenu.addAction("Quit")
action = cmenu.exec_(self.mapToGlobal(event.pos()))
def main():
app = QtWidgets.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Setting the border radius in the stylesheet for a top level widget (a widget that has its own "window") is not enough.
While the solution proposed by Christian Karcher is fine, two important considerations are required:
The system must support compositing; while this is true for most modern OSes, at least on Linux there is the possibility that even an up-to-date system does not support it by choice (I disabled on my computer); if that's the case, setting the WA_TranslucentBackground attribute will not work.
The FramelessWindowHint should not be set on Linux, as it may lead to problems with the window manager, so it should be set only after ensuring that the OS requires it (Windows).
In light of that, using setMask() is the correct fix whenever compositing is not supported, and this has to happen within the resizeEvent(). Do note that masking is bitmap based, and antialiasing is not supported, so rounded borders are sometimes a bit ugly depending on the border radius.
Also, since you want custom colors, using stylesheets is mandatory, as custom painting of a QMenu is really hard to achieve.
class AddContextMenu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super(AddContextMenu, self).__init__()
self.setMinimumSize(150, 200)
self.radius = 4
self.setStyleSheet('''
QMenu {{
background: blue;
border: 2px solid red;
border-radius: {radius}px;
}}
QMenu::item {{
color: white;
}}
QMenu::item:selected {{
color: red;
}}
'''.format(radius=self.radius))
def resizeEvent(self, event):
path = QtGui.QPainterPath()
# the rectangle must be translated and adjusted by 1 pixel in order to
# correctly map the rounded shape
rect = QtCore.QRectF(self.rect()).adjusted(.5, .5, -1.5, -1.5)
path.addRoundedRect(rect, self.radius, self.radius)
# QRegion is bitmap based, so the returned QPolygonF (which uses float
# values must be transformed to an integer based QPolygon
region = QtGui.QRegion(path.toFillPolygon(QtGui.QTransform()).toPolygon())
self.setMask(region)
Some side notes about your paintEvent implementation, not necessary in this specific case for the above reason, but still important (some points are related to portions of code that have been commented, but the fact that you tried them makes worth mentioning those aspects):
The QPainter used for a widget must never be instanciated outside a paintEvent(): creating the instance in the __init__ as you did is a serious error and might even lead to crash. The painter can only be created when the paintEvent is received, and shall never be reused. This clearly makes useless to set it as an instance attribute (self.painter), since there's no actual reason to access it after the paint event.
If the pen width is always the same, then just set it in the constructor (self.pen = QtGui.QPen(QtCore.Qt.red, 2)), continuously setting it in the paintEvent is useless.
QPen and QBrush can directly accept Qt global colors, so there's no need to create a QBrush instance as the painter will automatically (internally and fastly) set it: self.painter.setBrush(QtCore.Qt.blue).
self.update() should never be called within a paintEvent (and not even self.repaint() should). The result in undefined and possibly dangerous.
If you do some manual painting with a QPainter and then call the super paintEvent, the result is most likely that everything painted before will be hidden; as a general rule, the base implementation should be called first, then any other custom painting should happen after (in this case it obviously won't work, as you'll be painting a filled rounded rect, making the menu items invisible).
I have implemented round corners menu using QListWidget and QWidget. You can download the code in https://github.com/zhiyiYo/PyQt-Fluent-Widgets/blob/master/examples/menu/demo.py.
I've been trying to learn PyQt5 to reimplement an application I did with Tkinter, but with some design differences. Since it's not a complex application, I'd like to make it have a style similar to this small window from GitHub desktop (options on the left side of the window, and the rest in the remaining space):
I know my colors don't look great now, but I can take care of that later. However, I haven't found out how to draw lines/boxes similar to those, or at lesat in the divisions between my columns/rows.
Here's what I have so far:
import sys
from PyQt5.QtWidgets import QApplication, QLabel, QPushButton, QWidget, QFileDialog, QGridLayout, QFrame
from PyQt5 import QtGui, QtCore
class Window(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle('Some Window Title')
self.app = QApplication(sys.argv)
self.screen = self.app.primaryScreen()
self.screen_size = self.screen.size()
self.screen_width = self.screen_size.width()
self.screen_height = self.screen_size.height()
self.setGeometry(
self.screen_width * 0.1,
self.screen_height * 0.1,
self.screen_width * 0.8,
self.screen_height * 0.8
)
self.setStyleSheet('background: #020a19;')
self.grid = QGridLayout()
self.grid.setVerticalSpacing(0)
self.grid.setContentsMargins(0, 0, 0, 0)
self.option_button_stylesheet = '''
QPushButton {
background: #020a19;
color: #c5cad4;
border-color: #c5cad4;
border: 0px 0px 0px 0px;
border-style: outset;
border-radius: 5px;
font-size: 15px;
font-weight: bold;
padding: 10px;
margin: 15px;
width: 2px;
}
QPushButton:hover {
background-color: #00384d;
}
'''
self.placeholder_button_stylesheet = '''
QPushButton {
background: #020a19;
color: #c5cad4;
border-top: none;
border-right: 1px;
border-left:none;
border-bottom: none;
border-color: #c5cad4;
border-style: outset;
padding: 10px;
margin: 0px;
width: 2px;
height: 100%;
}
QPushButton:hover {
background-color: #020a19;
}
'''
self.header_label = QLabel('Some Application')
self.option_1_button = QPushButton('Option 1')
self.option_2_button = QPushButton('Option 2')
self.option_3_button = QPushButton('Option 3')
self.header_label.setStyleSheet(
'''
font-size: 25px;
color: #c5cad4;
padding-left: 10px;
padding-top: 16px;
padding-bottom: 20px;
height: 10%;
border-bottom: 1px solid #c5cad4;
'''
)
self.header_label.setFixedHeight(120)
self.grid.addWidget(self.header_label, 0, 0, 1, 2)
self.option_1_button.setStyleSheet(self.option_button_stylesheet)
self.option_1_button.setFixedWidth(200)
self.option_1_button.setFixedHeight(100)
self.grid.addWidget(self.option_1_button, 1, 0)
self.option_2_button.setStyleSheet(self.option_button_stylesheet)
self.option_2_button.setFixedWidth(200)
self.option_2_button.setFixedHeight(100)
self.grid.addWidget(self.option_2_button, 2, 0)
self.option_3_button.setStyleSheet(self.option_button_stylesheet)
self.option_3_button.setFixedWidth(200)
self.option_3_button.setFixedHeight(100)
self.grid.addWidget(self.option_3_button, 3, 0)
self.grid.setRowStretch(4, 1)
self.initUI()
self.setLayout(self.grid)
self.show()
def initUI(self):
self.greet_text = QLabel('Welcome')
self.greet_text.setAlignment(QtCore.Qt.AlignCenter)
self.greet_text.setStyleSheet(
"""
font-size: 35px;
color: #c5cad4;
"""
)
self.grid.addWidget(self.greet_text, 1, 1, 5, 1)
def run():
window = Window()
sys.exit(window.app.exec())
run()
As you can see, I'm using QWidget as my window element. I know I could use QMainWindow, but that changes the way widgets are placed and I'm finding it easier to use QWidget. I also don't need a toolbar or anything like that for this app.
How can I draw those lines?
Qt style sheets (QSS) don't provide such a feature, as it's only possible to style specific widgets without being able to consider their position within the layout. This is important in your case, as what you want to do is draw the "separation" between layout items.
It is theoretically possible to achieve this by setting a background for the container widget that will be the line color, have all its child widgets drawing their full contents with opaque colors, and ensure that the layout always has a spacing equal to the width of the line, but if the inner widgets don't respect their full size, they use an alpha channel, or some stretch or further spacing is added, the result would be ugly.
One possibility is to use a QWidget subclass, override its paintEvent() and draw those lines with QPainter.
The idea is that we cycle through all layout items, and draw lines that are placed in the middle between the "current" item and the previous.
In the following example I've created a basic QWidget subclass that implements the above concept, depending on the layout used.
Note that I had to make some changes and corrections to your original code:
as already noted in comments, an existing QApplication is mandatory to allow the creation of a QWidget, and while it's possible to make it an attribute of the object (before calling the super().__init__()), it is still conceptually wrong;
highly hierarchical structures in grid layouts should not use individual rows and columns for their direct child objects, but proper sub-layouts or child widgets should be added instead; in your case, the should be only two rows and columns: the header will have a 2-column-span in the first row, the menu will be on the second row (index 1) and first column, the right side in the second column, and the menu buttons will have their own layout;
setting generic style sheet properties for parent widgets is highly discouraged, as complex widgets (such as QComboBox, QScrollBar and scroll areas children) require that all properties are set to properly work; using setStyleSheet('background: ...') should always be avoided for parents or the application;
style sheets that are shared among many widgets should be set on the parent or the application, and proper selectors should always be used;
the QSS width property should be used with care, as it could make widgets partially invisible and unusable;
if you don't want any border, just use border: none;;
only absolute units are supported for style sheet sizes (see the Length property type), percent values are ignored;
setting fixed heights, paddings and margins can result in unexpected behavior; ensure that you carefully read the box model and do some testing to understand its behavior;
classes should not show themselves automatically during construction, so show() should not be called within the __init__() (this is not specifically "forbidden" or discouraged, but it's still good practice);
an if __name__ == '__main__': block should always be used, especially when dealing with programs or toolkits that rely on event loops (like all UI frameworks, as Qt is);
Here is a rewriting of your original code:
class LayoutLineWidget(QWidget):
_borderColor = QColor('#c5cad4')
def paintEvent(self, event):
# QWidget subclasses *must* do this to properly use style sheets;
# (see doc.qt.io/qt-5/stylesheet-reference.html#qwidget-widget)
opt = QStyleOption()
opt.initFrom(self)
qp = QStylePainter(self)
qp.drawPrimitive(QStyle.PE_Widget, opt)
# end of default painting
layout = self.layout()
if not layout or layout.count() <= 1:
return
if layout.spacing() < 1:
layout.setSpacing(1)
return
qp.setPen(self._borderColor)
if isinstance(layout, QBoxLayout):
lastGeo = layout.itemAt(0).geometry()
if isinstance(layout, QVBoxLayout):
for row in range(1, layout.count()):
newGeo = layout.itemAt(row).geometry()
y = (lastGeo.bottom()
+ (newGeo.y() - lastGeo.bottom()) // 2)
qp.drawLine(0, y, self.width(), y)
lastGeo = newGeo
else:
for col in range(1, layout.count()):
newGeo = layout.itemAt(row).geometry()
x = (lastGeo.right()
+ (newGeo.x() - lastGeo.right()) // 2)
qp.drawLine(x, 0, x, self.height())
lastGeo = newGeo
elif isinstance(layout, QGridLayout):
for i in range(layout.count()):
row, col, rowSpan, colSpan = layout.getItemPosition(i)
if not row and not col:
continue
cellRect = layout.cellRect(row, col)
if rowSpan:
cellRect |= layout.cellRect(row + rowSpan - 1, col)
if colSpan:
cellRect |= layout.cellRect(row, col + colSpan - 1)
if row:
aboveCell = layout.cellRect(row - 1, col)
y = (aboveCell.bottom()
+ (cellRect.y() - aboveCell.bottom()) // 2)
qp.drawLine(cellRect.x(), y, cellRect.right() + 1, y)
if col:
leftCell = layout.cellRect(row, col - 1)
x = (leftCell.right()
+ (cellRect.x() - leftCell.right()) // 2)
qp.drawLine(x, cellRect.y(), x, cellRect.bottom() + 1)
class Window(LayoutLineWidget):
def __init__(self):
super().__init__()
self.setStyleSheet('''
Window {
background: #020a19;
}
QLabel#header {
qproperty-alignment: AlignCenter;
font-size: 25px;
color: #c5cad4;
padding-left: 10px;
padding-top: 16px;
padding-bottom: 20px;
}
QWidget#content {
border: 1px solid #c5cad4;
border-radius: 5px;
}
QPushButton {
background: #020a19;
color: #c5cad4;
border: none;
border-radius: 5px;
font-size: 15px;
font-weight: bold;
padding: 10px;
}
QPushButton:hover {
background-color: #00384d;
}
QWidget#menu > QPushButton {
width: 180px;
height: 80px;
}
''')
mainLayout = QGridLayout(self)
mainLayout.setContentsMargins(0, 0, 0, 0)
mainLayout.setSpacing(0)
self.header_label = QLabel('Some Application', objectName='header')
self.header_label.setMinimumHeight(120)
mainLayout.addWidget(self.header_label, 0, 0, 1, 2)
menuContainer = QWidget(objectName='menu')
mainLayout.addWidget(menuContainer)
menuLayout = QVBoxLayout(menuContainer)
menuLayout.setSpacing(15)
self.option_1_button = QPushButton('Option 1')
self.option_2_button = QPushButton('Option 2')
self.option_3_button = QPushButton('Option 3')
menuLayout.addWidget(self.option_1_button)
menuLayout.addWidget(self.option_2_button)
menuLayout.addWidget(self.option_3_button)
menuLayout.addStretch()
rightLayout = QVBoxLayout()
mainLayout.addLayout(rightLayout, 1, 1)
self.content = QStackedWidget()
self.content.setContentsMargins(40, 40, 40, 40)
rightLayout.addWidget(self.content)
rightLayout.addStretch(1)
self.firstPage = LayoutLineWidget(objectName='content')
self.content.addWidget(self.firstPage)
firstPageLayout = QVBoxLayout(self.firstPage)
spacing = sum(firstPageLayout.getContentsMargins()) // 2
firstPageLayout.setSpacing(spacing)
self.other_option_1_button = QPushButton('Other 1')
self.other_option_2_button = QPushButton('Other 2')
self.other_option_3_button = QPushButton('Other 3')
firstPageLayout.addWidget(self.other_option_1_button)
firstPageLayout.addWidget(self.other_option_2_button)
firstPageLayout.addWidget(self.other_option_3_button)
screen = QApplication.primaryScreen()
rect = QRect(QPoint(), screen.size() * .8)
rect.moveCenter(screen.geometry().center())
self.setGeometry(rect)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec())
This is the visual result:
Note that the above code will cause calling the paintEvent() and its instructions very often, so it's always a good idea to provide some caching. A possibility is to use QPicture, which is a sort of "QPainter recorder"; since it completely relies on the C++ implementation, this allows to optimize the painting by drawing the existing content until it's changed.
class LayoutLineWidget(QWidget):
_borderColor = QColor('#c5cad4')
_paintCache = None
_redrawEvents = QEvent.LayoutRequest, QEvent.Resize
def event(self, event):
if event.type() in self._redrawEvents:
self._paintCache = None
self.update()
return super().event(event)
def paintEvent(self, event):
# QWidget subclasses *must* do the following to properly use style sheets;
# see https://doc.qt.io/qt-5/stylesheet-reference.html#qwidget-widget
qp = QStylePainter(self)
opt = QStyleOption()
opt.initFrom(self)
qp.drawPrimitive(QStyle.PE_Widget, opt)
layout = self.layout()
if not layout or layout.count() <= 1:
return
if layout.spacing() < 1:
layout.setSpacing(1)
return
try:
qp.drawPicture(0, 0, self._paintCache)
except TypeError:
self._rebuildPaintCache()
qp.drawPicture(0, 0, self._paintCache)
def _rebuildPaintCache(self):
layout = self.layout()
self._paintCache = QPicture()
qp = QPainter(self._paintCache)
# from this point, it's exactly the same as above
qp.setPen(self._borderColor)
if isinstance(layout, QBoxLayout):
# ...
Further notes:
the above codes haven't been tested against widgets with different margin/padding settings and complex row/column spans for grid layouts; it might need further fixing;
one of the hidden features of QGridLayout is that it's possible to set more widgets for each grid "cell"; while this feature can be useful for complex layouts, it has an important drawback: whenever a child widget or layout uses spanning, the layout spacing is ignored, so child items might have inconsistent geometries and the above code might not work as expected;
don't underestimate the aspects related to the box model, especially when dealing with sizes;
font sizes should not be set in pixels, as screens can have different DPI settings; always prefer device based units: pt, em or ex;
I am trying to figure out how to add a populated QVBoxLayout into a QFrame.
In the image you see 4 sliders, each with two labels, and a pushbutton. I've been messing around with QIcon and stylesheets which is why they look a bit odd at the moment. Each of these objects is created in my own custom class that produces all 4 objects together in a QVBoxLayout.
I would like to put a border around each QVBoxLayout. I've reeled through pages and pages of guides and other peoples code but i just can't work out what i'm doing wrong here. I've managed to create a QFrame with just a black border that is appearing behind the QVBoxLayout but its not tied to it in anyway, so its off-center. I could probably bodge something together using the geometery but that seems like something i'll regret later on.
You can see in the second image what i'm trying to get at, but everything i'm doing just isn't working. I've only just started learning to code, so i'm a bit out of my depth at the moment - i've read and read and read the QT documentation and hundreds of message board questions similar to this but none of it makes sense and none of it works.
Its going to be something very simple i know, but right now i'm ready to throw the computer out the window.
Here is my code - apologies as it is a bit messy as i've been experimenting with a lot of different things:
#!/usr/bin/env python3
import sys
from PySide2.QtWidgets import *
from PySide2.QtGui import *
from PySide2.QtCore import *
ON_BTN = 0
ON_PRESS_BTN = 1
OFF_BTN = 2
OFF_PRESS_BTN = 3
MUTE_ICONS = {
ON_BTN: "./images/onbtn.png",
ON_PRESS_BTN: "./images/onpressbtn",
OFF_BTN: "./images/offbtn.png",
OFF_PRESS_BTN: "./images/offpressbtn.png"
}
slider_enabled = """
QSlider::groove:vertical {
height: 300px;
width: 7px;
border-radius: 3px;
}
QSlider::handle:vertical {
background: #8d8d8d;
border: 2px solid #444;
height: 30px;
margin: 0 -30px; /* expand outside the groove */
border-radius: 10px;
}
QSlider::add-page:vertical {
background: #31b0c3;
border-radius: 3px;
}
QSlider::sub-page:vertical {
background: black;
border-radius: 3px;
}
QSlider::handle:vertical:pressed {
background: #565656;
}"""
slider_disabled = """
QSlider::groove:vertical {
height: 300px;
width: 7px;
border-radius: 3px;
}
QSlider::handle:vertical {
background: #8d8d8d;
border: 2px solid #444;
height: 30px;
margin: 0 -30px; /* expand outside the groove */
border-radius: 10px;
}
QSlider::add-page:vertical {
background: #3d6166;
border-radius: 3px;
}
QSlider::sub-page:vertical {
background: black;
border-radius: 3px;
}
QSlider::handle:vertical:pressed {
background: #565656;
}"""
class StyleSheets():
def __init__(self, style):
self.style = style
def sliderstyle(self):
self.style.setStyleSheet
class MySlider(QWidget):
def __init__(self, number):
#Defining various items that make up the object
QWidget.__init__(self)
self.number = number #Channel number
#Slider creation and functions that set its value called
self.slider = QSlider(Qt.Vertical)
self.slider.setMaximum(100)
self.send_slider_startup_values()
self.slider.valueChanged.connect(self.slider_value_change)
self.slider.setFixedHeight(300)
self.slider.setFixedWidth(80)
self.setStyleSheet("background: white;")
self.MyStylesheet = """
QSlider::groove:vertical {
height: 300px;
width: 7px;
border-radius: 3px;
}
QSlider::handle:vertical {
background: #8d8d8d;
border: 2px solid #444;
height: 30px;
margin: 0 -30px; /* expand outside the groove */
border-radius: 10px;
}
QSlider::add-page:vertical {
background: #00ddff;
border-radius: 3px;
}
QSlider::sub-page:vertical {
background: black;
border-radius: 3px;
}
QSlider::handle:vertical:pressed {
background: #565656;
}
"""
self.slider.setStyleSheet(self.MyStylesheet)
#Defining the channel number label & volume level label
self.numberlabel = QLabel(str(number))
self.numberlabel.setAlignment(Qt.AlignCenter)
self.volumelabel = QLabel("0")
self.volumelabel.setAlignment(Qt.AlignCenter)
self.volumelabel.setStyleSheet("border-style: solid; border-color: rgba(0,50,100,255);")
#Creating the mute button
self.mutebutton = QPushButton()
#self.mutebutton.setAlignment(Qt.AlignCenter)
self.mutebutton.setFixedSize(QSize(int (77), int (45)))
self.mutebutton.setIconSize(QSize( int(80), int(48)))
self.mutebutton.setFlat(True)
self.mutebutton.pressed.connect(self.button_pressed)
self.mutebutton.released.connect(self.button_released)
self.update_status(OFF_BTN)
#Making the Layout and adding each item
self.sliderframe = QFrame(self)
self.sliderframe.setFixedHeight(450)
self.sliderframe.setFixedWidth(90)
self.sliderframe.setStyleSheet("border: 2px solid black; background: 0%;")
self.setGeometry(50, 50, 450, 90)
#sliderflayout = QVBoxLayout()
#sliderflayout.setAlignment(Qt.AlignCenter)
sliderlayout = QVBoxLayout()
sliderlayout.setAlignment(Qt.AlignCenter)
sliderlayout.addWidget(self.slider, 0, Qt.AlignCenter)
sliderlayout.addWidget(self.numberlabel, Qt.AlignCenter)
sliderlayout.addWidget(self.volumelabel, Qt.AlignCenter)
sliderlayout.addWidget(self.mutebutton, Qt.AlignCenter)
#sliderlayout.addWidget(self.sliderframe, Qt.AlignCenter)
#self.sliderframe = QFrame(sliderlayout)
#self.sliderframe.setFixedHeight(450)
#self.sliderframe.setFixedWidth(100)
#self.sliderframe.setStyleSheet("border: 5px solid black;")
#self.setGeometry(50, 50, 50, 50)
# self.sliderframe.setVisible(False)
self.setLayout(sliderlayout)
#self.setAutoFillBackground(True)
def slider_value_change(self):
value = int(self.slider.value() / 10)
self.volumelabel.setText(str(value))
def send_slider_startup_values(self):
self.slider.setValue(0)
def update_status(self, status):
self.status = status
self.mutebutton.setIcon(QIcon(MUTE_ICONS[self.status]))
def button_pressed(self):
if self.status == ON_BTN:
self.update_status(ON_PRESS_BTN)
elif self.status == OFF_BTN:
self.update_status(OFF_PRESS_BTN)
def button_released(self):
if self.status == ON_PRESS_BTN:
self.send_mute()
self.slider.setStyleSheet(slider_disabled)
self.update_status(OFF_BTN)
elif self.status == OFF_PRESS_BTN:
self.send_mute()
self.slider.setStyleSheet(slider_enabled)
self.update_status(ON_BTN)
def send_mute(self):
pass
class Window2(QWidget):
def __init__(self):
QWidget.__init__(self)
self.setWindowTitle("Hello")
self.setMinimumHeight(480)
self.setMinimumWidth(800)
self.setAutoFillBackground(True)
self.setStyleSheet("""QWidget{background-color: grey;}""")
self.labeltest = QLabel("test")
label = QLabel("Test 1")
label2 = QLabel("Test 2")
label3 = QLabel("Test 3")
label4 = QLabel("Test 4")
label5 = QLabel("Test 5")
radiobutton = QRadioButton("MUTE")
radiobutton.setText("hello")
#self.label5 = QLabel("HELLO!!!!!")
#frame = QFrame()
#frame.setLayout(QVBoxLayout)
self.channel1 = MySlider(1)
self.channel2 = MySlider(2)
self.channel3 = MySlider(3)
self.channel4 = MySlider(4)
#self.sliderframe = QFrame(self.channel1)
#self.sliderframe.setFixedHeight(500)
#self.sliderframe.setFixedWidth(150)
#self.sliderframe.setStyleSheet("border: 2px solid black; background: 0%;")
self.channel2.setDisabled(True)
layout = QHBoxLayout()
layout.setAlignment(Qt.AlignLeft)
layout.addWidget(self.channel1)
layout.addWidget(self.channel2)
layout.addWidget(self.channel3)
layout.addWidget(self.channel4)
self.setLayout(layout)
def password_timeout(self):
pass
#def frame_object(self, channel):
#frame = QFrame()
#frame.setStyleSheet("""
#QFrame::active: {
# background: white;
#}""")
#channelstrip = MySlider(channel)
#frame.setLayout(channelstrip)
#return frame
class PasswordWindow(QWidget):
def __init__(self):
QWidget.__init__(self)
self.setWindowTitle("Password Test")
self.showFullScreen()
layout = QGridLayout()
self.setLayout(layout)
label = QLabel("Please enter password: ")
layout.addWidget(label, 0, 0)
button = QPushButton("Press me!")
button.clicked.connect(self.button_pushed)
layout.addWidget(button, 1, 0)
def button_pushed(self):
self.close()
app = QApplication(sys.argv)
mainwindow = Window2()
mainwindow.show()
sys.exit(app.exec_())
EDIT: Slight update, i've managed to frame the object by creating a seperate class that inherits Qframe and has the MySlider object passed into it. I need to find a way to get it tighter around the objects but its progress somewhat.
I've figured it out. I'm writing a response in case someone else comes across the same problem (as it seems pretty common). Layouts come automatically with a margin and padding. The command to set both the padding and margin to what you want it to be is:
layoutname.setContentsMargins(0, 0, 0, 0)
layoutname.setSpacing(0)
So i thought i was doing this with QFrames, but i didn't and accidently made it work anyway. You can do this by creating a class of widget that you pass a custom made object into, and using a stylesheet to border the entire thing. You also get the benefit of the stylesheet not affecting objects passed into it, as its only concerned with the class its written in. So in my case, i made an object class called "MySlider" that produces 4 objects - a QSlider, 2 QLabels, and a QPushbutton. This is considered a single object as it was made in a class. I then created a second class that requires an argument. I pass that newly made object into the second class as that argument and add that to another QVBoxLayout. Using self.setStyleSheet i can then edit the entire widgets elements. The widget will automatically size to the smallest it can go without cropping its contents. Here is the code, its pretty simple but man did it twist my melon:
class SliderFrame(QFrame):
def __init__(self, myslider):
QFrame.__init__(self)
self.setStyleSheet("border: 1px solid black; margin: 0px; padding: 0px;")
self.layout = QVBoxLayout()
self.layout.addWidget(myslider)
self.layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(self.layout)
And when creating the object on your main window:
self.channel1 = SliderFrame(MySlider(1))
You can see that i pass an argument into SliderFrame, and in turn pass an argument into MySlider.
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_())
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_())