Change QSpinBox Arrow Size Without Changing App Style - python

I'm trying to find a way to alter properties of a widget, such as size of a component or various borders and colours, without changing the default style of the widget (in this case, fusion). Still trying to get my head around style sheets in PyQT, could someone explain how to achieve the bigger arrow buttons without altering the style?
Thanks in advance.
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setStyle("fusion")
#Trying to change the arrow size here, while maintaining their "fusion" style
app.setStyleSheet("QSpinBox::up-button { width: 32px; }"
"QSpinBox::down-button { width: 32px; }")
windowExample = basicWindow()
windowExample.show()
sys.exit(app.exec_())
I'm using a Proxy Style for my sliders too in order to alter the slider size, hoping theres something similar for a spinbox:
class SliderProxyStyle(QProxyStyle):
def pixelMetric(self, metric, option, widget):
if metric == QStyle.PM_SliderThickness:
return 20
elif metric == QStyle.PM_SliderLength:
return 40
return super().pixelMetric(metric, option, widget)

You can't, not with simple stylesheets, as explained at the bottom of the sub-controls documentation:
With complex widgets such as QComboBox and QScrollBar, if one property or sub-control is customized, all the other properties or sub-controls must be customized as well."
Complex widgets also include all subclasses of QAbstractSpinBox, from which QSpinBox inherits, so if you alter any of the properties (except for background and foreground color), you have to provide everything else, as the basic implementation of the style will be ignored by using the basic QCommonStyle as fallback.
The only viable solution is to use a QProxyStyle and implement subControlRect for SC_SpinBoxUp, SC_SpinBoxDown and SC_SpinBoxEditField:
class Proxy(QtWidgets.QProxyStyle):
def subControlRect(self, control, opt, subControl, widget=None):
rect = super().subControlRect(control, opt, subControl, widget)
if control == self.CC_SpinBox:
if subControl in (self.SC_SpinBoxUp, self.SC_SpinBoxDown):
rect.setLeft(opt.rect.width() - 32)
elif subControl == self.SC_SpinBoxEditField:
rect.setRight(opt.rect.width() - 32)
return rect
# ...
app = QtWidgets.QApplication(sys.argv)
app.setStyle(Proxy())
# ...

Related

PyQt6 - custom widget - get default widget colors for text, background etc

I'm developing custom components using PyQt6. I now like to adapt my widgets' colors to the default Palette colors so that they look like the default widgets of PyQt6. I think I found a way to get the default colors of a global palette:
default_palette = self.palette()
self.textColor = default_palette.text().color()
self.backgroudColor = default_palette.window().color()
My question is, how to use them in the best (practice) way. My goal is that colors also change, when I change the global stylesheets or use libraries like qt_materials.
One of the most important aspects to consider when dealing with widget "colors" is that the palette is just a hint.
The style (see QStyle) then decides if and how to use that palette.
For instance, the QPalette documentation says, in the role section:
There are some color roles used mostly for 3D bevel and shadow effects. All of these are normally derived from Window, and used in ways that depend on that relationship. For example, buttons depend on it to make the bevels look attractive, and Motif scroll bars depend on Mid to be slightly different from Window.
Note that the above text is also inherited from very old Qt documentation; while generally still consistent, it's not to be taken as granted.
This is an extremely important aspect that has always to be considered, especially when dealing with system styles (specifically, macOS and Windows), that might completely ignore the palette.
That said, Qt standard UI elements normally follow the palette, meaning that you probably can safely use the palette roles.
Note that setting Qt Style Sheets (QSS) creates some inconsistencies with the palette: just like standard HTML based style sheets, QSS override the default behavior.
This implies the following aspects:
generic properties (like color and background) override the palette (see this related answer);
QSS can use palette roles (see the documentation), but they can only use the QPalette of the parent (widget or application) or any value previously set for the widget that does not override the palette itself (see the point above);
palette roles and groups can be cached, but you also need to ensure that changeEvent() properly checks for PaletteChange and StyleChange in order to clear that cache (and after calling the default implementation of the widget!);
This is a basic example that consider all the above aspects (except for the QSS ones):
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class Test(QWidget):
_background = _foreground = None
_paletteCached = False
def changeEvent(self, event):
super().changeEvent(event)
if event.type() in (event.PaletteChange, event.StyleChange):
self._paletteCached = False
def minimumSizeHint(self):
return QSize(self.fontMetrics().boundingRect('Hello').width() * 2, 40)
def paintEvent(self, event):
if not self._paletteCached:
self._paletteCached = True
palette = self._paletteCached = self.palette()
self._background = palette.color(palette.Base)
self._foreground = palette.color(palette.Text)
qp = QPainter(self)
qp.setBrush(self._background)
qp.setPen(self._foreground)
rect = self.rect().adjusted(0, 0, -1, -1)
qp.drawRect(rect)
qp.drawText(rect, Qt.AlignCenter, 'Hello!')
app = QApplication([])
test = QWidget()
layout = QHBoxLayout(test)
layout.addWidget(Test(enabled=True))
layout.addWidget(Test(enabled=False))
new = test.palette()
new.setColor(QPalette.Base, Qt.darkGreen)
new.setColor(QPalette.Disabled, QPalette.Base, QColor(0, 128, 0, 128))
new.setColor(QPalette.Text, Qt.blue)
new.setColor(QPalette.Disabled, QPalette.Text, QColor(255, 0, 0, 128))
QTimer.singleShot(2000, lambda: test.setPalette(new))
test.show()
app.exec()

Pyqt5 window full screen does not show border

I create a pyqt window without any title bar and transparent. Also added blue border for my window but when on running the app I can't see any blue border for the window.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import Qt
import sys
class Window(QMainWindow):
def __init__(self):
super().__init__()
# this will hide the title bar
self.setWindowFlag(Qt.FramelessWindowHint)
self.setStyleSheet("border : 3px solid blue;")
self.setWindowOpacity(0.01)
# setting the geometry of window
self.setGeometry(100, 100, 400, 300)
self.showFullScreen()
# create pyqt5 app
App = QApplication(sys.argv)
# create the instance of our Window
window = Window()
window.show()
# start the app
sys.exit(App.exec())
How can I show the border for my window?
You can use the TranslucentBackground attribute and paint the border/background in paintEvent.
class Window(QMainWindow):
def __init__(self):
super().__init__()
# this will hide the title bar
self.setWindowFlag(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
# setting the geometry of window
self.setGeometry(100, 100, 400, 300)
self.showFullScreen()
def paintEvent(self, event):
qp = QPainter(self)
qp.setPen(QPen(Qt.blue, 6))
qp.drawRect(self.rect())
qp.setOpacity(0.01)
qp.setPen(Qt.NoPen)
qp.setBrush(self.palette().window())
qp.drawRect(self.rect())
The main problem with the original code is that the whole window gets almost transparent (having an alpha channel value of 0.01); this makes any content of the window as much as transparent, including the border, which becomes practically invisible unless the desktop background (or underlying windows) have enough contrast with the color set.
While the proposed solution works as expected and properly answer the OP question, there's another possibility that doesn't directly involve overriding paintEvent().
The issue of setting the border in the stylesheet and setting the WA_TranslucentBackground attribute is that only explicit opaque parts of the window are visible (child widgets, and any other painting implemented in paintEvent()): the background is automatically ignored, and, normally, the border along with it[1].
A possibility is to add a child widget to the main one and set the border for that widget only using a proper style sheet selector:
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowFlag(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
borderWidget = QWidget(objectName='borderWidget')
self.setCentralWidget(borderWidget)
bgd = self.palette().color(QPalette.Window)
bgd.setAlphaF(.01)
self.setStyleSheet('''
#borderWidget {{
border: 3px solid blue;
background: {bgd};
}}
'''.format(bgd=bgd.name(bgd.HexArgb)))
self.showFullScreen()
Note: in the code above (and that below) the background is still visible, but with a very low alpha channel value. If you don't need it, the background value could be ignored, but, for consistency it's better to explicitly declare it: background: transparent;.
That said, consider that QMainWindow has its own private layout, which could add unwanted margins on some systems (and trying to override them might not work at all).
Unless you really need any of the QMainWindow features (menu bar, status bar, tool bars and dock widgets), you should use a basic QWidget instead, which will give you direct control over the layout:
class Window(QWidget):
def __init__(self):
super().__init__()
self.setWindowFlag(Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground)
borderWidget = QWidget(objectName='borderWidget')
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(borderWidget)
bgd = self.palette().color(QPalette.Window)
bgd.setAlphaF(.01)
self.setStyleSheet('''
#borderWidget {{
border: 3px solid blue;
background: {bgd};
}}
'''.format(bgd=bgd.name(bgd.HexArgb)))
self.showFullScreen()
Remember that setting the basic background/border properties of a QWidget stylesheet only works for actual QWidget instances: Qt subclasses implement it on their own way, and if you are going to create the child widget as a custom QWidget subclass the above will NOT work (see this question and this note in the documentation).
[1] This depends on the implementation of the platform and how Qt deals with it through its QPlatformPlugin. For instance, using older versions of xcompmgr I can get the border just by unsetting the WA_NoSystemBackground attribute (which, as the documentation reports, is automatically set when WA_TranslucentBackground is). While properly implementing specific-platform issues would be better, it's almost impossible as their behavior is often inconsistent across different versions, and the combinations between the window and composition managers are almost infinite. The proposed solution should work in all the situations, and with a minimal, possible, overhead.

How to dissable the focus indication in stylesheet

When my QDoubleSpinBox is focused, it gets a blue outline to it (in "Fusion" style):
How do I turn this off?
Doing this with stylesheets only is doable, but has an important drawback: styling complex widgets like a QSpinBox requires to correctly set all sub control properties.
The basic solution is to set the border for the widget:
QSpinBox {
border: 1px inset palette(mid);
border-radius: 2px;
}
Keep in mind that offering proper visible response of the focus is really important; you might not like the "glow" (and color) the Fusion style offers, but nonetheless it should always be visible when a widget has focus or not, even if it has a blinking text cursor. You can do that by specifying a slightly different color with the :focus selector:
QSpinBox:focus {
border: 1px inset palette(dark);
}
Unfortunately, as explained in the beginning, this has an important drawback: as soon as the stylesheet is applied, the widget painting falls back to the basic primitive methods (the spinbox on the right uses the stylesheet above):
Unfortunately, there's almost no direct way to restore the default painting of the arrows, as using the stylesheet prevents that. So, the only solution is to provide the properties for the controls as explained in the examples about customizing QSpinBox.
There is an alternative, though, using QProxyStyle. The trick is to intercept the control in the drawComplexControl() implementation and remove the State_HasFocus flag of the option before calling the default implementation.
In the following example, I also checked the focus before removing the flag in order to provide sufficient visual feedback, and I also removed the State_MouseOver flag which shows the glowing effect when hovering.
class Proxy(QtWidgets.QProxyStyle):
def drawComplexControl(self, cc, opt, qp, widget=None):
if cc == self.CC_SpinBox:
opt = QtWidgets.QStyleOptionSpinBox(opt)
if opt.state & self.State_HasFocus:
opt.palette.setColor(QtGui.QPalette.Window,
opt.palette.color(QtGui.QPalette.Window).darker(100))
else:
opt.palette.setColor(QtGui.QPalette.Window,
opt.palette.color(QtGui.QPalette.Window).lighter(125))
opt.state &= ~(self.State_HasFocus | self.State_MouseOver)
super().drawComplexControl(cc, opt, qp, widget)
# ...
app = QtWidgets.QApplication(sys.argv)
app.setStyle(Proxy())
# ...
Note that the above "color correction" only works for Fusion style and other styles that use the Window palette role for painting the border. For instance, the Windows style doesn't consider it at all, or you might want to use higher values of darker() or lighter() in order to provide better differentiation.

How to set QStackedWidget size to child widgets minimum size?

Unable to get the QLabel in this example to be of the minimum size to contain its text. I need the layout and stacked widget to then size themselves to the minimum required to fit the label.
I have used code from https://www.tutorialspoint.com/pyqt/pyqt_qstackedwidget.htm to demonstrate my issue.
Setting the size policies seems to work when the application starts, but increasing the application width eventually causes the label to expand after the list reaches a certain width.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class stackedExample(QWidget):
def __init__(self):
super(stackedExample, self).__init__()
self.rightlist = QListWidget()
self.rightlist.setSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding)
self.rightlist.insertItem(0, 'Contact')
self.stack1 = QWidget()
self.stack1.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Minimum)
self.stack1UI()
self.Stack = QStackedWidget(self)
self.Stack.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Minimum)
self.Stack.addWidget(self.stack1)
hbox = QHBoxLayout(self)
hbox.addWidget(self.Stack)
hbox.addWidget(self.rightlist)
self.setLayout(hbox)
self.rightlist.currentRowChanged.connect(self.display)
self.setGeometry(300, 50, 10, 10)
self.setWindowTitle('StackedWidget demo')
self.show()
def stack1UI(self):
layout = QVBoxLayout()
label = QLabel("Hello World")
label.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Minimum)
label.setStyleSheet("QLabel { background-color : red; color : blue; }")
layout.addWidget(label)
self.stack1.setLayout(layout)
def display(self, i):
self.Stack.setCurrentIndex(i)
def main():
app = QApplication(sys.argv)
ex = stackedExample()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
tl;dr
Remove the size policy settings for everything but the QStackedWidget only (for which you'll have to set the horizontal policy to Maximum), and everything should be fine.
Explanation
I have to admit: I always felt that QSizePolicy enum names are confusing (I know that I'm not the only one), so I sympathize with your doubts.
Setting the stretch resolves the issue only partially, because sometime you might need or want to manually set the stretches, and that will possibly mess around with some size policies.
The problem is that you're setting the size policy to "Minimum", which, as the QSizePolicy documentation explains, says that:
The sizeHint() is minimal, and sufficient. The widget can be expanded [...]
And that's because Minimum uses the GrowFlag.
This means that, if the layout "thinks" that there's some available space for a widget, it will let it expand: Minimum does not mean that the widget will use it's minimal size (or, better, its minimumSizeHint()), but that it will use the sizeHint() as a minimum size for the layout, while it keeping its capacity to expand; if there's available space, it will use it.
What you actually need is to set the horizontal policy to Maximum instead, and, specifically, to the Stack object only (the QStackWidget, not the QWidget container, nor the QLabel).
That's because Maximum actually uses the ShrinkFlag (again, from the QSizePolicy docs):
The sizeHint() is a maximum. The widget can be shrunk any amount
without detriment if other widgets need the space (e.g. a separator
line). It cannot be larger than the size provided by sizeHint().
That said, be aware that there are known issues with QLabels in certain cases, specifically if the label has word wrapping.
Not sure if it is the correct approach, but adding in a stretch factor seems to have achieved what I was looking for.
hbox.addWidget(self.rightlist, stretch=1)

remove padding/margin from QTabBar in QLayout

I have an application where I want the QTabBar to be in a separate VBoxLayout from the QTabWidget area. It sort of works using the code below but I'm having styling problems. Before I separated the QTabBar from the QTabWidget I didn't have any problems but now I can't figure out how to style it the way I want.
#!/usr/bin/env python2
from PyQt4 import QtGui, QtCore
from peaks import *
class mainWindow(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.setWindowFlags(QtCore.Qt.Dialog)
self.tabWidget = QtGui.QTabWidget()
self.tabBar = QtGui.QTabBar()
self.tabBar.setContentsMargins(0,0,0,0)
self.tabWidget.setTabBar(self.tabBar)
self.tabWidget.setTabPosition(QtGui.QTabWidget.West)
self.tabWidget.setIconSize(QtCore.QSize(35, 35))
self.tab1 = QtGui.QWidget()
self.tab2 = QtGui.QWidget()
tabLayoutBox = QtGui.QVBoxLayout()
tabLayoutBox.setContentsMargins(0,0,0,0)
tabLayoutBox.addWidget(self.tabBar)
mainHBox = QtGui.QHBoxLayout()
mainHBox.setContentsMargins(0,0,0,0)
mainHBox.setSpacing(0)
mainHBox.setMargin(0)
mainHBox.addLayout(tabLayoutBox)
mainHBox.addWidget(self.tabWidget)
mainVBox = QtGui.QVBoxLayout()
mainVBox.addWidget(QtGui.QWidget())
mainVBox.addLayout(mainHBox)
self.setLayout(mainVBox)
self.tabWidget.addTab(self.tab1, 'tab1')
self.tabWidget.addTab(self.tab2, 'tab2')
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
app.setStyleSheet(
"QTabBar { alignment: right; }"
"QTabBar::tear { width:0; border: none; }"
"QTabBar::scroller { width:0; border: none; }"
)
main = mainWindow()
main.show()
sys.exit(app.exec_())
However there are a couple of things that I want that I can't figure out how to do:
I want to eliminate the gap between the QTabWidget and the QTabBar. I've been trying various things like setContentsMargins(0,0,0,0) and setting stylesheets but nothing I've tried has worked.
I want QTabBar to be flush with the top of the QTabWidget. Interesting to note that the tabs seem to rapidly switch back and forth between the top whenever the window is resized.
stuff I've looked at:
Qt Use QTabBar in a Different QLayout
http://doc.qt.io/qt-5/stylesheet-examples.html
https://wiki.qt.io/Adjust_Spacing_and_Margins_between_Widgets_in_Layout
update: I can emulate my desired behavior by setting the QTabBar miminumSizeHint() to (15,15) and setting QTabBar::tab { margin-right: -15px; } but this doesn't let me actually click the tabs. there's a space underneath (ie to the right of) the tabs for some reason and I've no idea how to get rid of it.
second update: I've identified the main problem I think. my code uses
self.tabWidget.setTabPosition(QtGui.QTabWidget.West)
to move the tab to the left side but the QTabWidget assumes that there is a tabBar there, hence the extra space. If I do
self.tabWidget.setTabPosition(QtGui.QTabWidget.East)
that blank space shows up at the right. So one thing I can do is set the tabShape directly on the tabBar:
self.tabBar.setShape(QtGui.QTabBar.RoundedWest)
however this leaves a blank space at the top where the QTabWidget expects the QTabBar to be. I can move that space to the right using setTabPosition before setShape but that doesn't solve the problem of actually getting rid of it.
I wasn't able to figure out how to hide the empty space so instead I'm just using a QTabBar + QStackedWidget which is quite easy to implement. In order to make one like a QTabWidget all you need to do is connect QTabBar.currentChanged to QStackedWidget.setCurrentIndex:
self.stacked = QtGui.QStackedWidget()
self.tabBar = QtGui.QTabBar()
self.tabBar.currentChanged.connect(self.stacked.setCurrentIndex)
self.tabBar.setShape(QtGui.QTabBar.RoundedWest)
self.tabBar.updateGeometry()
self.tabBar.setContentsMargins(0,0,0,0)
I also wrote a convenience function that emulates QTabWidget.addTab:
def addTab(self, widget, name):
self.stacked.addWidget(widget)
self.tabBar.addTab(name)
Your problem is that you override the tab bar positioning by adding the tab bar to a layout.
The tab bar is no longer in the tab widget when you use the lines.
tabLayoutBox = QtGui.QVboxLayout()
tabLayoutBox.addWidget(self.tabBar)
These lines re-parent the tab bar. It looks like you just want to use setTabPosition() instead of creating your own tab bar. You don't need to set a new tab bar unless you create a custom tab bar class and want to use that.
I don't know why you would want it in a separate layout, but the other option is use
tabLayoutBox.setSpacing(0)
This is the spacing in between widgets to separate them. That spacing is for widgets. You have a layout in a layout, so setSpacing(0) may not apply to the spacing on a layout. If not you may have to sub-class QVBoxLayout and create your own layout.
EDIT
I've found that insertSpacing works a lot better.
mainHBox.insertSpacing(1, -25)

Categories

Resources