Search code examples
python-3.xpyqt5

How to show a tooltip image when hovering on a button in pyqt5


I want to show an image when hovering on a button.

But, the pyqt5 tooltip reference only contains text.

How can I do this? I want to do dynamically as for loop element like below.

I need to complete the # code

def createButtons(self):
    for d_name in dic:
        btn = QPushButton(d_name, self)
        btn.clicked.connect(lambda state, x=d_name: self.btn_clicked(x))
        # btn.addTooltipImage(d_name)
        self.button_map[btn.text()] = btn

Solution

  • Qt's tooltip support rich text formatting (only a basic subset of HTML), so the <img> tag is available:

    self.button.setToolTip('<img src="icon.svg">')
    

    Remember that if you are using a local file path, it has to be absolute or relative to the path of the file that loads it.

    The alternative is to use Qt's resource system: you can create a resource file in Designer, then build it using pyrcc myresource.qrc -o myresource.py, import it with import myresource and load the images using the colon prefix path:

    self.button.setToolTip('<img src=":/images/icon.svg">')
    

    Animated tooltips (GIF)

    Tooltips, like any other widget that is based on QTextDocument, don't support animations. The only solution is to create a custom widget that behaves like a tooltip.

    In order to achieve that, the most logical approach is to subclass QLabel, which supports the QMovie class that provides support for animated images.

    Note that this is not easy: while tooltips might seem very simple objects, their behavior follows many aspects that the user gives for granted. To mimic that behavior, a subclass has to be carefully tailored in the same way.

    class ToolTipAnimation(QtWidgets.QLabel):
        def __init__(self, parent, file, width=None, height=None):
            super().__init__(parent, flags=QtCore.Qt.ToolTip)
            self.setMouseTracking(True)
    
            # image loading doesn't happen immediately, as it could require some time;
            # we store the information for later use
            self._file = file
            self._width = width
            self._height = height
            self._shown = False
    
            # a timer that prevents the enterEvent to hide the tip immediately
            self.showTimer = QtCore.QTimer(interval=100, singleShot=True)
    
            # install an event filter for the application, so that we can be notified
            # whenever the user performs any action
            QtWidgets.QApplication.instance().installEventFilter(self)
    
        def load(self):
            movie = QtGui.QMovie(self._file)
            if self._width and not self._height:
                self._height = self._width
            if self._width and self._height:
                size = QtCore.QSize(self._width, self._height)
                movie.setScaledSize(size)
            else:
                size = QtCore.QSize()
                for f in range(movie.frameCount()):
                    movie.jumpToFrame(f)
                    size = size.expandedTo(movie.currentImage().size())
            self.setFixedSize(size)
            self.setMovie(movie)
            self._shown = True
    
        def show(self, pos=None):
            if not self._shown:
                self.load()
            if pos is None:
                pos = QtGui.QCursor.pos()
            # ensure that the tooltip is always shown within the screen geometry
            for screen in QtWidgets.QApplication.screens():
                if pos in screen.availableGeometry():
                    screen = screen.availableGeometry()
                    # add an offset so that the mouse cursor doesn't hide the tip
                    pos += QtCore.QPoint(2, 16)
                    if pos.x() < screen.x():
                        pos.setX(screen.x())
                    elif pos.x() + self.width() > screen.right():
                        pos.setX(screen.right() - self.width())
                    if pos.y() < screen.y():
                        pos.setY(screen.y())
                    elif pos.y() + self.height() > screen.bottom():
                        pos.setY(screen.bottom() - self.height())
                    break
    
            self.move(pos)
            super().show()
            self.movie().start()
    
        def maybeHide(self):
            # if for some reason the tooltip is shown where the mouse is, we should
            # not hide it if it's still within the parent's rectangle
            if self.parent() is not None:
                parentPos = self.parent().mapToGlobal(QtCore.QPoint())
                rect = QtCore.QRect(parentPos, self.parent().size())
                if QtGui.QCursor.pos() in rect:
                    return
            self.hide()
    
        def eventFilter(self, source, event):
            # hide the tip for any user interaction
            if event.type() in (QtCore.QEvent.KeyPress, QtCore.QEvent.KeyRelease, 
                QtCore.QEvent.WindowActivate, QtCore.QEvent.WindowDeactivate, 
                QtCore.QEvent.FocusIn, QtCore.QEvent.FocusOut, 
                QtCore.QEvent.Leave, QtCore.QEvent.Close, 
                QtCore.QEvent.MouseButtonPress, QtCore.QEvent.MouseButtonRelease, 
                QtCore.QEvent.MouseButtonDblClick, QtCore.QEvent.Wheel):
                    self.hide()
            return False
    
        def mouseMoveEvent(self, event):
            QtCore.QTimer.singleShot(100, self.hide)
    
        def enterEvent(self, event):
            # hide the tooltip when mouse enters, but not immediately, otherwise it
            # will be shown right after from the parent widget
            if not self.showTimer.isActive():
                QtCore.QTimer.singleShot(100, self.hide)
    
        def showEvent(self, event):
            self.showTimer.start()
    
        def hideEvent(self, event):
            self.movie().stop()
    
    
    class ButtonIcon(QtWidgets.QPushButton):
        toolTipAnimation = None
        formats = tuple(str(fmt, 'utf8') for fmt in QtGui.QMovie.supportedFormats())
    
        def setToolTipImage(self, image, width=None, height=None):
            if not image or self.toolTipAnimation:
                self.toolTipAnimation.hide()
                self.toolTipAnimation.deleteLater()
                self.toolTipAnimation = None
                self.setToolTip('')
                if not image:
                    return
            if image.endswith(self.formats):
                self.toolTipAnimation = ToolTipAnimation(self, image, width, height)
            else:
                if width and not height:
                    height = width
                if width and height:
                    self.setToolTip(
                        '<img src="{}" width="{}" height="{}">'.format(
                            image, width, height))
                else:
                    self.setToolTip('<img src="{}">'.format(image))
    
        def event(self, event):
            if (event.type() == QtCore.QEvent.ToolTip and self.toolTipAnimation and 
                not self.toolTipAnimation.isVisible()):
                    self.toolTipAnimation.show(event.globalPos())
                    return True
            elif event.type() == QtCore.QEvent.Leave and self.toolTipAnimation:
                self.toolTipAnimation.maybeHide()
            return super().event(event)
    
    
    class Window(QtWidgets.QWidget):
        def __init__(self):
            super().__init__()
            layout = QtWidgets.QHBoxLayout(self)
            buttonFixed = ButtonIcon('fixed image')
            buttonFixed.setToolTipImage('icon.svg')
            layout.addWidget(buttonFixed)
            buttonAnimated = ButtonIcon('animated gif')
            # the size can be set explicitly (if height is not provided, it will
            # be the same as the width)
            buttonAnimated.setToolTipImage('animated.gif', 200)
            layout.addWidget(buttonAnimated)