4rigener
4rigener

Reputation: 386

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

Upvotes: 2

Views: 7320

Answers (2)

4rigener
4rigener

Reputation: 386

PySide: Add images to tooltips

This solution applies to my question:

btn.setToolTip('<br><img src="%s">' % (iconpath))

Upvotes: 2

musicamante
musicamante

Reputation: 48231

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)

Upvotes: 4

Related Questions