PyQtDarkTheme
PyQtDarkTheme copied to clipboard
Theme customization
I like this theme because it's simpler and lighter, but is or will there be a way to easily customize it? Perhaps the generated stylesheets can still have placeholders which are formatted on load after the user of the library passes the wanted customizations.
Mainly the default accent color and maybe the borders and padding of widgets. QFrame
and QGroupBox
for example have border and padding even with the frame set to NoFrame
which makes grouping widgets a bit cluttered. Otherwise manually applying a stylesheet after qdarktheme creates issues with spacing and borders near scrollbars.
Hi, thanks for the idea! Unfortunately, there is no way to easily customize stylesheets currently. You need to hack generated stylesheets. But I'm interested in it.
Perhaps the generated stylesheets can still have placeholders which are formatted on load after the user of the library passes the wanted customizations.
Yes, it is easy to add another placeholders. Do you have any ideas what kind of placeholders we should include?
I have an another idea to improve QFrame
style. A smart way was introduced with stackoverflow. (QT Stylesheet for HLine/VLine color). This way can also be used for NoFrame.
Sample code
import sys
from PyQt5.QtWidgets import QApplication, QFrame, QGridLayout, QLabel, QMainWindow, QWidget
import qdarktheme
app = QApplication(sys.argv)
# Widgets
main_win = QMainWindow()
panel_frame = QFrame()
box_frame = QFrame()
no_frame = QFrame()
# Setup widgets
panel_frame.setFrameShape(QFrame.Panel)
box_frame.setFrameShape(QFrame.Box)
no_frame.setFrameShape(QFrame.NoFrame)
# Layout
layout = QGridLayout()
layout.addWidget(QLabel("Panel"), 0, 0)
layout.addWidget(QLabel("Box"), 0, 1)
layout.addWidget(QLabel("NoFrame"), 0, 2)
layout.addWidget(panel_frame, 1, 0)
layout.addWidget(box_frame, 1, 1)
layout.addWidget(no_frame, 1, 2)
central_widget = QWidget()
central_widget.setLayout(layout)
main_win.setCentralWidget(central_widget)
# Setup style
stylesheet = qdarktheme.load_stylesheet()
custom_stylesheet = """
QFrame {
border: 1px solid gray;
padding: 1px;
border-radius: 4px;
}
/* NoFrame */
QFrame[frameShape="0"] {
border: none;
padding: 0;
}
/* Panel */
QFrame[frameShape="2"] {
background-color: gray
}
"""
app.setStyleSheet(stylesheet + custom_stylesheet)
main_win.show()
app.exec()

But this code is not working in Qt6. Maybe this is bug of Qt6. So we need to use custom property(See qt documentation).
Sample code
import sys
from PySide6.QtWidgets import QApplication, QFrame, QGridLayout, QLabel, QMainWindow, QWidget
import qdarktheme
app = QApplication(sys.argv)
# Widgets
main_win = QMainWindow()
panel_frame = QFrame()
box_frame = QFrame()
no_frame = QFrame()
# Setup widgets
panel_frame.setFrameShape(QFrame.Panel)
box_frame.setFrameShape(QFrame.Box)
no_frame.setFrameShape(QFrame.NoFrame)
# Setup custom property
panel_frame.setProperty("shape", "panel")
no_frame.setProperty("shape", "no_frame")
# Layout
layout = QGridLayout()
layout.addWidget(QLabel("Panel"), 0, 0)
layout.addWidget(QLabel("Box"), 0, 1)
layout.addWidget(QLabel("NoFrame"), 0, 2)
layout.addWidget(panel_frame, 1, 0)
layout.addWidget(box_frame, 1, 1)
layout.addWidget(no_frame, 1, 2)
central_widget = QWidget()
central_widget.setLayout(layout)
main_win.setCentralWidget(central_widget)
# Setup style
stylesheet = qdarktheme.load_stylesheet()
custom_stylesheet = """
QFrame {
border: 1px solid gray;
padding: 1px;
border-radius: 4px;
}
/* NoFrame */
QFrame[shape="no_frame"] {
border: none;
padding: 0;
}
/* Panel */
QFrame[shape="panel"] {
background-color: gray
}
/* Override QFrame style */
QLabel {
border: none;
}
"""
app.setStyleSheet(stylesheet + custom_stylesheet)
main_win.show()
app.exec()

By including these custom properties into template stylesheets, we can easily change the style with setProperty()
. QGroupBox
could be improved as well.
Otherwise manually applying a stylesheet after qdarktheme creates issues with spacing and borders near scrollbars.
Can you provide a minimal code example that replicate this issue?
But this code is not working in Qt6. Maybe this is bug of Qt6.
I found a bug report in Qt Bug Tracker. https://bugreports.qt.io/browse/QTBUG-98928?filter=-4&jql=text%20~%20%22qframe%22%20order%20by%20created%20DESC
Regarding placeholders, I'm not sure which will be useful to include, so the more the better? But since I'm looking for ways to style grouped and nested widgets (QPushButton
s inside QFrame
or QTextEdit
/QPlainTextEdit
together with buttons inside a QFrame
using a layout) it makes sense to be able to remove the borders of most widgets to get a cleaner look, set background color, padding, scrollbar spacing (right of the content, left of the scrollbar, if that's possible) and accent colors for slider grooves, progress bars, enabled/disabled states, etc.
I have an another idea to improve QFrame style. A smart way was introduced with stackoverflow. (QT Stylesheet for HLine/VLine color). This way can also be used for NoFrame.
This or just plain css classes seems interesting and should do the job. But I'm fine with waiting for upstream fix since the Python bindings for Qt6 are still relatively new and have missing features.
Otherwise manually applying a stylesheet after qdarktheme creates issues with spacing and borders near scrollbars.
Can you provide a minimal code example that replicate this issue?
I came across a few issues with the borders of QPlainTextEdit
and QScrollArea
when removing the borders and padding of the parent widget. Here is a quick example:
QPlainTextEdit
import sys
import qdarktheme
from PySide6 import QtCore, QtGui, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
central_widget = QtWidgets.QWidget(self)
text_area = QtWidgets.QPlainTextEdit(central_widget)
text_area.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
for i in range(20):
text_area.appendPlainText(f'Line {i}')
self.text_area = text_area
central_widget_layout = QtWidgets.QVBoxLayout(central_widget)
central_widget_layout.addWidget(self.text_area)
central_widget.setLayout(central_widget_layout)
self.central_widget = central_widget
self.central_widget_layout = central_widget_layout
self.setCentralWidget(self.central_widget)
def main():
app = QtWidgets.QApplication(sys.argv)
stylesheet = qdarktheme.load_stylesheet('dark')
custom_stylesheet = '''
QFrame {
padding: 0;
border: none;
}
'''
app.setStyleSheet(stylesheet + custom_stylesheet)
mw = MainWindow()
mw.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()
produces a missing border around the scrollbar:
QScrollArea
import sys
import qdarktheme
from PySide6 import QtCore, QtGui, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
central_widget = QtWidgets.QWidget(self)
scroll_area = QtWidgets.QScrollArea(central_widget)
scroll_area.setWidgetResizable(True)
scroll_area.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
scroll_area_contents = QtWidgets.QWidget(scroll_area)
scroll_area_contents_layout = QtWidgets.QVBoxLayout(scroll_area_contents)
scroll_area_contents.setLayout(scroll_area_contents_layout)
scroll_area.setWidget(scroll_area_contents)
self.labels = []
for i in range(20):
label = QtWidgets.QLabel(scroll_area_contents, text=f'Label {i}')
self.labels.append(label)
scroll_area_contents_layout.addWidget(label)
self.scroll_area = scroll_area
self.scroll_area_contents = scroll_area_contents
self.scroll_area_contents_layout = scroll_area_contents_layout
central_widget_layout = QtWidgets.QVBoxLayout(central_widget)
central_widget_layout.addWidget(self.scroll_area)
central_widget.setLayout(central_widget_layout)
self.central_widget = central_widget
self.central_widget_layout = central_widget_layout
self.setCentralWidget(self.central_widget)
def main():
app = QtWidgets.QApplication(sys.argv)
stylesheet = qdarktheme.load_stylesheet('dark')
custom_stylesheet = '''
QFrame {
padding: 0;
border: none;
}
QScrollArea {
border: 1px solid #fff;
}
'''
app.setStyleSheet(stylesheet + custom_stylesheet)
mw = MainWindow()
mw.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()
produces missing corners of the border:
I'm not 100% sure if I'm not doing something wrong or it's the platform/version of Qt or something else though. Could also be the selector affecting all QFrame
s.
Running Windows 10 x64, PySide 6.2.2.1, Qt 6.2.2
Thanks for your some ideas.
I'm looking for ways to style grouped and nested widgets (QPushButtons inside QFrame or QTextEdit/QPlainTextEdit together with buttons inside a QFrame using a layout) it makes sense to be able to remove the borders of most widgets to get a cleaner look
I think this can basically be solve with property selector. I found a solution to use property selector in Qt6. It seems that the frameShape
property values are different between Qt5 and Qt6.
Panel style
import sys
from PySide6.QtWidgets import QApplication, QFrame, QGridLayout, QLabel, QMainWindow, QWidget
import qdarktheme
app = QApplication(sys.argv)
# Widgets
main_win = QMainWindow()
panel_frame = QFrame()
box_frame = QFrame()
no_frame = QFrame()
# Setup widgets
panel_frame.setFrameShape(QFrame.Panel)
box_frame.setFrameShape(QFrame.Box)
no_frame.setFrameShape(QFrame.NoFrame)
# Layout
layout = QGridLayout()
layout.addWidget(QLabel("Panel"), 0, 0)
layout.addWidget(QLabel("Box"), 0, 1)
layout.addWidget(QLabel("NoFrame"), 0, 2)
layout.addWidget(panel_frame, 1, 0)
layout.addWidget(box_frame, 1, 1)
layout.addWidget(no_frame, 1, 2)
central_widget = QWidget()
central_widget.setLayout(layout)
main_win.setCentralWidget(central_widget)
# Setup style
stylesheet = qdarktheme.load_stylesheet()
custom_stylesheet = """
QFrame {
border: 1px solid gray;
padding: 1px;
border-radius: 4px;
}
/* NoFrame, Qt5 is "0" */
QFrame[frameShape="NoFrame"] {
border: none;
padding: 0;
}
/* Panel, Qt5 is "2" */
QFrame[frameShape="Panel"] {
background-color: gray
}
"""
app.setStyleSheet(stylesheet + custom_stylesheet)
main_win.show()
app.exec()
QPushButton and QGroupBox have the similar properties for flat style. The flat style of QPushButton has already been included in qdarktheme. You can check using QPushButton.setFlat()
. Widgets that inherit from QFrame, such as QTextEdit and QTableView, can use NoFrame
. Therefore in this case, I think placeholder is no need. It can be included into a template stylesheet.
But this style will not update itself automatically when the value of a property referenced from the stylesheet changes. We need to unpolish and polish the style.
https://wiki.qt.io/Dynamic_Properties_and_Stylesheets
# After change the style
widget.style().unpolish(widget)
widget.style().polish(widget)
set background color, padding, scrollbar spacing (right of the content, left of the scrollbar, if that's possible) and accent colors for slider grooves, progress bars, enabled/disabled states, etc.
These may need placeholders. I think these are very good options, and accent colors and padding are especially useful.
I came across a few issues with the borders of QPlainTextEdit and QScrollArea when removing the borders and padding of the parent widget.
This is the stylesheet problems.
QFrame { padding: 0; border: none; }
border: none
remove the border display area as default. qdarktheme has a style sheet that displays a colored border when highlighting. If QPlainTextEdit display the colored border in this state, it will overlap other widgets because there is no place to display the border. Therefore, you need to prepare a space in advance to display the border when highlighting. Remove border: none
and add border-color: transparent;
to QFrame selector.
However, another problem arises. There is a container widget for the scrollbar below the QScrollBar in the QScrollArea. This container widget edges are sharp. This means that the corners of the container widget will be displayed above the border.
This can be removed with the following code.
QPlainTextEdit > .QWidget {
background-color: transparent;
}
And the second issue is similar to the first issue. scroll_area_contents
edges are sharp. So you need to add border-color: transparent;
to scroll_area_contents
. QScrollArea already has parent container widget. Therefore, you need to add qss as following.
/* scroll_area > QScrollArea widget container(built-in class) > scroll_area_contents */
QScrollArea > .QWidget > .QWidget {
background-color: transparent;
}
Here is the fixed codes.
QPlainTextEdit
import sys
from PySide6 import QtCore, QtGui, QtWidgets
import qdarktheme
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
central_widget = QtWidgets.QWidget(self)
text_area = QtWidgets.QPlainTextEdit(central_widget)
text_area.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
for i in range(20):
text_area.appendPlainText(f"Line {i}")
self.text_area = text_area
central_widget_layout = QtWidgets.QVBoxLayout(central_widget)
central_widget_layout.addWidget(self.text_area)
central_widget.setLayout(central_widget_layout)
self.central_widget = central_widget
self.central_widget_layout = central_widget_layout
self.setCentralWidget(self.central_widget)
def main():
app = QtWidgets.QApplication(sys.argv)
stylesheet = qdarktheme.load_stylesheet("dark")
custom_stylesheet = """
QFrame {
padding: 0;
border-color: transparent;
}
QPlainTextEdit > .QWidget {
background-color: transparent;
}
"""
app.setStyleSheet(stylesheet + custom_stylesheet)
mw = MainWindow()
mw.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
QScrollArea
import sys
from PySide6 import QtCore, QtGui, QtWidgets
import qdarktheme
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
central_widget = QtWidgets.QWidget(self)
scroll_area = QtWidgets.QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
scroll_area_contents = QtWidgets.QWidget(scroll_area)
scroll_area_contents_layout = QtWidgets.QVBoxLayout(scroll_area_contents)
scroll_area_contents.setLayout(scroll_area_contents_layout)
scroll_area.setWidget(scroll_area_contents)
self.labels = []
for i in range(20):
label = QtWidgets.QLabel(scroll_area_contents, text=f"Label {i}")
self.labels.append(label)
scroll_area_contents_layout.addWidget(label)
self.scroll_area = scroll_area
self.scroll_area_contents = scroll_area_contents
self.scroll_area_contents_layout = scroll_area_contents_layout
central_widget_layout = QtWidgets.QVBoxLayout(central_widget)
central_widget_layout.addWidget(self.scroll_area)
central_widget.setLayout(central_widget_layout)
self.central_widget = central_widget
self.central_widget_layout = central_widget_layout
self.setCentralWidget(self.central_widget)
def main():
app = QtWidgets.QApplication(sys.argv)
stylesheet = qdarktheme.load_stylesheet("dark")
custom_stylesheet = """
QFrame {
padding: 0;
border-color: transparent;
}
QScrollArea {
border: 1px solid #fff;
}
QScrollArea > .QWidget {
background-color: transparent;
}
/* scroll_area > QScrollArea widget container(built-in class) > scroll_area_contents */
QScrollArea > .QWidget > .QWidget {
background-color: transparent;
}
"""
app.setStyleSheet(stylesheet + custom_stylesheet)
mw = MainWindow()
mw.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
I will try to add this fix to qdarktheme.
I think this can basically be solve with property selector. I found a solution to use property selector in Qt6. It seems that the
frameShape
property values are different between Qt5 and Qt6.
If the property selector values are consistent for all Qt 5.* and 6.* versions it shouldn't be difficult to support them. As I'm currently focusing on Qt6, I'll try to report any inconsistent behaviour that I find.
Widgets that inherit from QFrame, such as QTextEdit and QTableView, can use NoFrame. Therefore in this case, I think placeholder is no need. It can be included into a template stylesheet. But this style will not update itself automatically when the value of a property referenced from the stylesheet changes. We need to unpolish and polish the style.
That's great, I'll keep it in mind.
If QPlainTextEdit display the colored border in this state, it will overlap other widgets because there is no place to display the border. Therefore, you need to prepare a space in advance to display the border when highlighting. Remove
border: none
and addborder-color: transparent;
to QFrame selector. However, another problem arises. There is a container widget for the scrollbar below the QScrollBar in the QScrollArea. This container widget edges are sharp. This means that the corners of the container widget will be displayed above the border.
I see. I'm still not fully familiar with what child widgets there are and which color & border properties are set by default. But thanks for providing working examples!
There also shouldn't be a need for me to set padding: 0
and border: none
globally for every QFrame
when I can apply it along with NoFrame
shape to QFrame
widgets that are only used as simple containers.
Hi @alexitx. I have created a new PR(#66) that fixes this issue. Can you check it?
Looks good so far. The only small issue I noticed is that frameless widgets still have invisible border that creates 1 pixel gap, where without the theme there's no border at all.
QFrame NoFrame
import sys
import qdarktheme
from PySide6 import QtWidgets
app = QtWidgets.QApplication(sys.argv)
qdarktheme_stylesheet = qdarktheme.load_stylesheet('dark')
custom_stylesheet = '''
/* Uncomment to emulate the default behaviour */
/*
QFrame[frameShape="NoFrame"] {
border: none;
}
*/
#button_1,
#button_2,
#button_3 {
padding: 4px;
color: #fff;
background-color: #606060;
border: none;
}
#container {
background-color: #ff6060;
}
'''
app.setStyleSheet(qdarktheme_stylesheet + custom_stylesheet)
main_window = QtWidgets.QMainWindow()
central_widget = QtWidgets.QWidget(main_window)
central_widget_layout = QtWidgets.QVBoxLayout(central_widget)
central_widget_layout.setContentsMargins(10, 10, 10, 10)
container = QtWidgets.QFrame(central_widget, objectName='container')
container.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
container_layout = QtWidgets.QVBoxLayout(container)
container_layout.setContentsMargins(0, 0, 0, 0)
button_1 = QtWidgets.QPushButton(central_widget, text='QPushButton 1', objectName='button_1')
button_1.setFlat(True)
button_2 = QtWidgets.QPushButton(central_widget, text='QPushButton 2', objectName='button_2')
button_2.setFlat(True)
button_3 = QtWidgets.QPushButton(central_widget, text='QPushButton 3', objectName='button_3')
button_3.setFlat(True)
container_layout.addWidget(button_1)
container_layout.addWidget(button_2)
container_layout.addWidget(button_3)
container.setLayout(container_layout)
central_widget_layout.addWidget(container)
central_widget.setLayout(central_widget_layout)
main_window.setCentralWidget(central_widget)
main_window.show()
sys.exit(app.exec())
Image 1 - QFrame
with NoFrame
frame shape (no theme)
Image 2 - QFrame
with NoFrame
frame shape (qdarktheme)
Image 3 - QFrame
with NoFrame
frame shape and manually set border: none
(qdarktheme)
I think the 3rd image is the expected result, as it also retains the same window size, because there is no border.
Another example on a flat QPushButton
with its border color set to #ffff00
:
Image 1 - Flat QPushButton
with border-color: #ffff00
(no theme)
Image 2 - Flat QPushButton
with border-color: #ffff00
(qdarktheme
Image 3 - Flat QPushButton
with border-color: #ffff00
and manually set border: none
(qdarktheme)
border: none
is dangerous because it may break the style.
I only removed QFrame border with .QFrame
selector only matching instances of QFrame.(#66)
qdarktheme cannot remove flat QPushButton
border because QPushButton.setCheckable(True)
uses the hilight border.
QPushButton.checkable = True
import sys
from PySide6 import QtWidgets
import qdarktheme
app = QtWidgets.QApplication(sys.argv)
qdarktheme_stylesheet = qdarktheme.load_stylesheet("dark")
custom_stylesheet = """
#button_1,
#button_2,
#button_3 {
padding: 4px;
color: #fff;
background-color: #606060;
border: none;
}
#container {
background-color: #ff6060;
}
"""
app.setStyleSheet(qdarktheme_stylesheet + custom_stylesheet)
main_window = QtWidgets.QMainWindow()
central_widget = QtWidgets.QWidget(main_window)
central_widget_layout = QtWidgets.QVBoxLayout(central_widget)
central_widget_layout.setContentsMargins(10, 10, 10, 10)
container = QtWidgets.QFrame(central_widget, objectName="container")
container.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
container_layout = QtWidgets.QVBoxLayout(container)
container_layout.setContentsMargins(0, 0, 0, 0)
for i in range(1, 4):
button = QtWidgets.QPushButton(
central_widget,
text=f"QPushButton {i}",
objectName=f"button_{i}",
flat=True,
checkable=True,
)
container_layout.addWidget(button)
container.setLayout(container_layout)
central_widget_layout.addWidget(container)
central_widget.setLayout(central_widget_layout)
main_window.setCentralWidget(central_widget)
main_window.show()
sys.exit(app.exec())
left: set the line of border: none;
right: comment out the line of border: none;
The QPushButtons of qdarktheme need the border even when style is flat. I think we need to change the base style of button if you want to remove flat QPushButton border.
I think it's a good way to have the button style follow Google material design. In Google material design, border is no need because button toggle uses background color instead of a border.
text-button(flat) > States | Google material design
border: none
is dangerous because it may break the style. I only removed QFrame border with.QFrame
selector only matching instances of QFrame.(#66)qdarktheme cannot remove flat
QPushButton
border becauseQPushButton.setCheckable(True)
uses the hilight border.
Yeah, I didn't think of checkable QPushButton and other widgets that might use the border as highlight. Mainly because I'm used to testing against the default style without a theme.
I think it's a good way to have the button style follow Google material design. In Google material design, border is no need because button toggle uses background color instead of a border.
That sounds good, but it's up to you to decide wether to change a lot of the core style of the theme.
The QPushButtons of qdarktheme need the border even when style is flat. I think we need to change the base style of button if you want to remove flat QPushButton border.
Actually now that I think, I didn't need to change the style of the QPushButton
s in the first example. I was trying to show the container (QFrame
with object name #container
) still having transparent 1 pixel border even with NoFrame
set.
If you remove the block #button_1, #button_2, #button_3 ...
and only comment/uncomment the first block with the selector QFrame[frameShape="NoFrame"]
it demonstrates the example fine.
The flat QPushButton
is fine even if the border is present, because frames used as containers shouldn't have contrasting background color likes in my example. Or if that is a possibility later on when the user can customize color properties, then the border color of flat QPushButton
s can be set to the background color of the button, instead of transparent in base.qss#L406, unless there is a reason for that, which I'm not aware of.
the border color of flat QPushButtons can be set to the background color of the button, instead of transparent in base.qss#L406, unless there is a reason for that, which I'm not aware of.
The background color of the flat QPushButton is set to transparent. Therefore, the border color must also be set to transparent. Flat QPushButton color should always match the color of the widget behind it, so I think transparent is the best choice.
Oh, I'm sorry. I changed the background color of flat QPushButton to transparent in base.qss | PR #66.
This issue resolved on #187.