I need to properly display a tooltip with rounded corners using PyQt5. The default behavior whenever using the border-radius: 8px;
in the stylesheet of a QToolTip
keeps the tooltip rendered as a rectangle, and the border-radius
property only impacts the shape of the border, which is rather ugly.
This question is following this question : How to make Qtooltip corner round in pyqt5 which was left partially unanswered. Based on this answer, I tried this code :
from PyQt5 import QtCore, QtGui, QtWidgets
import sip
class ProxyStyle(QtWidgets.QProxyStyle):
def styleHint(self, hint, opt=None, widget=None, returnData=None):
if hint == self.SH_ToolTip_Mask and widget:
if super().styleHint(hint, opt, widget, returnData):
# the style already creates a mask
return True
returnData = sip.cast(returnData, QtWidgets.QStyleHintReturnMask)
src = QtGui.QImage(widget.size(), QtGui.QImage.Format_ARGB32)
src.fill(QtCore.Qt.transparent)
widget.render(src)
mask = QtGui.QRegion(QtGui.QBitmap.fromImage(
src.createHeuristicMask()))
if mask == QtGui.QRegion(opt.rect.adjusted(1, 1, -1, -1)):
# if the stylesheet doesn't set a border radius, create one
x, y, w, h = opt.rect.getRect()
mask = QtGui.QRegion(x + 4, y, w - 8, h)
mask += QtGui.QRegion(x, y + 4, w, h - 8)
mask += QtGui.QRegion(x + 2, y + 1, w - 4, h - 2)
mask += QtGui.QRegion(x + 1, y + 2, w - 2, h - 4)
returnData.region = mask
return 1
return super().styleHint(hint, opt, widget, returnData)
app = QtWidgets.QApplication([])
app.setStyle(ProxyStyle())
palette = app.palette()
app.setStyleSheet('''
QToolTip {
color: black;
background: white;
border: 1px solid black;
border-radius: 8px;
}
''')
test = QtWidgets.QPushButton('Hover me', toolTip='Tool tip')
test.show()
app.exec()
However this does not work as expected : the produced tooltip does not change from the default one.
After some debugging, I noticed that sizeHint()
logic is actually never executed ; super().styleHint(hint, opt, widget, returnData)
always returns True
.
How can I render a tooltip with rounded corners ?
Edit : I am running Windows 11, and it seems that it is important (see answer)
It seems that by default, Windows 11 creates a mask for tooltips ; therefore if super().styleHint(hint, opt, widget, returnData):
is always True
. If we want to run the logic generating the mask, we have to remove that block.
In order to render the tooltip, to get a mask to clip it later, we also have to prevent infinite recursion ; widget.render
would call this styleHint
which would in turn call widget.render
. Therefore we have to add a recursive check.
Lastly, due to the antialiased pixels of the border, the produced mask is a tiny bit too large. To fix that issue, we have to crop the mask of 1px in every direction, which can be done by downscaling the mask by 2 pixels in both directions and then translate the QRegion
by 1px.
Here is a working code (at least on Windows 11, with PyQt5):
class ProxyStyle(QtWidgets.QProxyStyle):
_recursive_check = False
def styleHint(self, hint, opt=None, widget=None, returnData=None):
if hint == self.SH_ToolTip_Mask and widget and not self._recursive_check:
self._recursive_check = True
returnData = sip.cast(returnData, QtWidgets.QStyleHintReturnMask)
src = QtGui.QImage(widget.size(), QtGui.QImage.Format_ARGB32)
src.fill(QtCore.Qt.transparent)
widget.render(src)
image = src.createHeuristicMask()
bitmap = QtGui.QBitmap.fromImage(image)
mask = QtGui.QRegion(bitmap)
x, y, w, h = opt.rect.getRect()
if mask == QtGui.QRegion(opt.rect.adjusted(1, 1, -1, -1)):
mask = QtGui.QRegion(x + 4, y, w - 8, h)
mask += QtGui.QRegion(x, y + 4, w, h - 8)
mask += QtGui.QRegion(x + 2, y + 1, w - 4, h - 2)
mask += QtGui.QRegion(x + 1, y + 2, w - 2, h - 4)
image = image.scaled(image.width()-2, image.height()-2)
bitmap = QtGui.QBitmap.fromImage(image)
mask = QtGui.QRegion(bitmap).translated(1, 1)
returnData.region = mask
self._recursive_check = False
return 1
return super().styleHint(hint, option=opt, widget=widget, returnData=returnData)
app = QtWidgets.QApplication([])
app.setStyle(ProxyStyle())
palette = app.palette()
app.setStyleSheet('''
QToolTip {{
color: {fgd};
background: {bgd};
border: 2px solid black;
border-radius: 13px;
}}
'''.format(
fgd=palette.color(palette.ToolTipText).name(),
bgd=palette.color(palette.ToolTipBase).name(),
border=palette.color(palette.ToolTipBase).darker(110).name()
))
test = QtWidgets.QPushButton('Hover me', toolTip='Tool tip')
test.show()
app.exec()