2

On the right of the title bar of a PyQt QDialog (see below, next to the "x") there is a "?" that is supposed to help the user query help for any other widget on the Dialog window. PyQt Dialog title bar

What should I do (programmatically) to get it to work. Once the "?" isClicked, one should be able to capture the next widget clicked and provide a ToolTip or something like that. In PyQt, I do not know how to capture the isClicked event on the "?".

I have seen a couple of posts where the question was how to make the "?" disappear, but the discussion there uses Qt, not PyQt, so I do not understand it, and they are not talking about what I need. I need to make it work as intended. See How can I hide/delete the "?" help button on the "title bar" of a Qt Dialog? and PyQt4 QInputDialog and QMessageBox window flags

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Echeban
  • 144
  • 9

2 Answers2

2

You can set the whatsThis property to any widget you want:

    self.someWidget.setWhatsThis('hello!')

From that point on, whenever you click on the "?" button and then click on that widget, a tooltip with that text will be shown.

Since the "what's this" mode is individually set to widgets, there's no easy way to capture it globally (as far as I know of) because if the widget has no whatsthis property set that feature won't be available for it.
Also, whenever you enter the "what's this" mode, the cursor will probably change according to the contents of the whatsthis property: if it's not set, the cursor will probably show a "disabled" icon.

I've created a basic workaround for this issue, which automatically enables any child widget's whatsthis (if none is already set) whenever the mode is activated: as soon as the EnterWhatsThisMode is fired, it automatically installs a custom object that acts as an event filter, and emits a signal if the whatsthis event is called; as soon as the mode exits, the filter is removed.
I used a separate object for the event filter because there's no way to know what event filter have been already installed to a widget, and if you already installed the parent's one, removing it automatically would be an issue.

class WhatsThisWatcher(QtCore.QObject):
    whatsThisRequest = QtCore.pyqtSignal(QtWidgets.QWidget)

    def eventFilter(self, source, event):
        if event.type() == QtCore.QEvent.WhatsThis:
            self.whatsThisRequest.emit(source)
        return super(WhatsThisWatcher, self).eventFilter(source, event)


class W(QtWidgets.QWidget):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        layout = QtWidgets.QVBoxLayout(self)

        hasWhatsThisButton = QtWidgets.QPushButton('Has whatsThis')
        layout.addWidget(hasWhatsThisButton)
        hasWhatsThisButton.setWhatsThis('I am a button!')

        noWhatsThisButton = QtWidgets.QPushButton('No whatsThis')
        layout.addWidget(noWhatsThisButton)

        self.whatsThisWatchedWidgets = []
        self.whatsThisWatcher = WhatsThisWatcher()
        self.whatsThisWatcher.whatsThisRequest.connect(self.showCustomWhatsThis)

        whatsThisButton = QtWidgets.QPushButton('Set "What\'s this" mode')
        layout.addWidget(whatsThisButton)
        whatsThisButton.clicked.connect(QtWidgets.QWhatsThis.enterWhatsThisMode)

    def event(self, event):
        if event.type() == QtCore.QEvent.EnterWhatsThisMode:
            for widget in self.findChildren(QtWidgets.QWidget):
                if not widget.whatsThis():
                    # install the custom filter
                    widget.installEventFilter(self.whatsThisWatcher)
                    # set an arbitrary string to ensure that the "whatsThis" is
                    # enabled and the cursor is correctly set
                    widget.setWhatsThis('whatever')
                    self.whatsThisWatchedWidgets.append(widget)
        elif event.type() == QtCore.QEvent.LeaveWhatsThisMode:
            while self.whatsThisWatchedWidgets:
                widget = self.whatsThisWatchedWidgets.pop()
                # reset the whatsThis string to none and uninstall the filter
                widget.setWhatsThis('')
                widget.removeEventFilter(self.whatsThisWatcher)
        return super(W, self).event(event)

    def showCustomWhatsThis(self, widget):
        widgetPos = widget.mapTo(self, QtCore.QPoint())
        QtWidgets.QWhatsThis.showText(
            QtGui.QCursor.pos(), 
            'There is no "what\'s this" for {} widget at coords {}, {}'.format(
                widget.__class__.__name__, widgetPos.x(), widgetPos.y()), 
            widget)

A couple of notes about this:

  1. I used a button to activate the whatsthis mode, as on my window manager on Linux there's no window title button for that;
  2. Some widgets may contain subwidgets, and you'll get those instead of the "main" one (the most common case are QAbstractScrollArea descendands, like QTextEdit or QGraphicsView, which might return the viewport, the inner "widget" or the scrollbars);
musicamante
  • 41,230
  • 6
  • 33
  • 58
2

By default the task of that button is to enable whatsThis: press "?", then press the widget and you will see the message associated with whatsThis property.

If you want to add other actions(open url, add QToolTip, etc) you can monitor the QEvent::EnterWhatsThisMode and QEvent::LeaveWhatsThisMode events overriding the event() method or using an eventFilter().

from PyQt5 import QtCore, QtGui, QtWidgets


class Dialog(QtWidgets.QDialog):
    def event(self, event):
        if event.type() == QtCore.QEvent.EnterWhatsThisMode:
            print("enter")
            QtGui.QDesktopServices.openUrl(QtCore.QUrl("foo_url"))
        elif event.type() == QtCore.QEvent.LeaveWhatsThisMode:
            print("leave")
        return super().event(event)


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)

    w = Dialog()
    w.setWhatsThis("Whats this")
    w.setWindowFlags(
        QtCore.Qt.WindowContextHelpButtonHint | QtCore.Qt.WindowCloseButtonHint
    )
    w.resize(640, 480)
    w.show()
    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • I marked the 2nd answer as "answered" because in the 2nd line it solved my problem, but I love your code to capture the event and do anything, like go to an external website for help. Probably that is what I will end up using rather than WhatsThis. thumbsUp. – Echeban Nov 29 '19 at 00:27