2

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
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
4rigener
  • 346
  • 3
  • 18

2 Answers2

2

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)
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • gif file just appear like png, how can i fix it? Add .gif to file path not working, too – 4rigener Mar 04 '20 at 08:54
  • @4rigener what do you mean? If you are asking about animated gif, they are not supported in tooltips (or in any QTextDocument based widget), you'd need another way to show them. – musicamante Mar 04 '20 at 09:02
  • 1
    @4rigener I've edited the answer to include the animated gif support. – musicamante Mar 04 '20 at 10:52
  • I made auto file extension func, but takes long time. How can I reduce file extension time? [link](https://pastebin.com/yJZKRZQD) – 4rigener Mar 04 '20 at 14:18
  • @4rigener why do you do that?!? there's absolutely no need to check for the contents of the images before loading them. – musicamante Mar 04 '20 at 14:30
  • Because, if not add '.gif' then gif not working just image. And if I add '.gif' then png and jpg not working. my dic don't save file extension. just file name – 4rigener Mar 04 '20 at 14:34
  • @4rigener then just use [`os.path.exists(path)`](https://docs.python.org/3.8/library/os.path.html#os.path.exists) or [`QFile.exists(path)`](https://doc.qt.io/qt-5/qfile.html#exists) in a cycle that checks between available extensions. – musicamante Mar 04 '20 at 14:41
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/209002/discussion-between-4rigener-and-musicamante). – 4rigener Mar 04 '20 at 14:48
1

PySide: Add images to tooltips

This solution applies to my question:

btn.setToolTip('<br><img src="%s">' % (iconpath))
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
4rigener
  • 346
  • 3
  • 18