Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get all widgets under cursor

The widgetAt function gives me the widget directly under the cursor, at the highest z-order.

pos = QtGui.QCursor.pos()
widget = QtGui.qApp.widgetAt(pos)

But how can I get all widgets under the cursor? Including those behind the top-most one? Something like:

pos = QtGui.QCursor.pos()
widgets = QtGui.qApp.widgetsAt(pos)

Example

As an example of use, picture an overlay which adds animated graphics, like ripples on the surface of water, wherever the user clicked. Naturally, the overlay would need to receive and respond to clicks, but it must do so without interfering with clicks propagating to widgets underneath it.

Another example is usage tracking; to intercept and record clicks for analysis, the same overlay can be used but this time nothing is drawn, only collected.

In both cases, if only one widget is getting the click event, I'll need a way of distinguishing what widgets reside underneath it so as to pass the event on.

like image 963
Marcus Ottosson Avatar asked Dec 08 '14 17:12

Marcus Ottosson


2 Answers

Here's another possible solution, calling the existing QApplication.widgetAt multiple times.

import sys
from PySide import QtGui, QtCore


def widgets_at(pos):
    """Return ALL widgets at `pos`

    Arguments:
        pos (QPoint): Position at which to get widgets

    """

    widgets = []
    widget_at = QtGui.qApp.widgetAt(pos)

    while widget_at:
        widgets.append(widget_at)

        # Make widget invisible to further enquiries
        widget_at.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
        widget_at = QtGui.qApp.widgetAt(pos)

    # Restore attribute
    for widget in widgets:
        widget.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, False)

    return widgets

Example

image

class Overlay(QtGui.QWidget):
    def __init__(self, parent=None):
        super(Overlay, self).__init__(parent)
        self.setAttribute(QtCore.Qt.WA_StyledBackground)
        self.setStyleSheet("QWidget { background-color: rgba(0, 255, 0, 50) }")

    def mousePressEvent(self, event):
        pos = QtGui.QCursor.pos()
        print [w.objectName() for w in widgets_at(pos)]
        return super(Overlay, self).mousePressEvent(event)


app = QtGui.QApplication(sys.argv)
window = QtGui.QWidget()
window.setObjectName("Window")
window.setFixedSize(200, 100)

button = QtGui.QPushButton("Button 1", window)
button.setObjectName("Button 1")
button.move(10, 10)
button = QtGui.QPushButton("Button 2", window)
button.setObjectName("Button 2")
button.move(50, 15)
overlay = Overlay(window)
overlay.setObjectName("Overlay")
overlay.setFixedSize(window.size())

window.show()
app.exec_()

Here is some output.

[u'Overlay', u'Window'] # Clicking an empty area
[u'Overlay', u'Button 1', u'Window'] # Button 1
[u'Overlay', u'Button 2', u'Window'] # Button 2
[u'Overlay', u'Button 2', u'Button 1', u'Window'] # Overlap

It looks like the results are O(1), with the added benefit of returning each widget in the order in which they overlap. It does however cause overlapping windows to flash (Windows 8) as their attribute is changed and restored.

like image 195
Marcus Ottosson Avatar answered Oct 01 '22 15:10

Marcus Ottosson


Not pretty, but off the top of my head: You'll need to cycle through all of your widgets and use their coordinates against the mouse coordinates.

EDIT: If you want O(1), you can do this but it will require more overhead, and you'll need to store the coordinates after each move. I actually have a class that does this, as I want movable (by user) widgets to come up in their previous state, and I don't want to go through the hassle of coding this in all of my projects. As such, I've subclassed QDialog (etc), and I add it to a convenience class which is essentially a glorified QSettings object. When QDialog emits anything useful, the convenience class's slots will store those values (Geometry(), basically) in QSettings. For any project to use this behavior, a simple ConvenienceClass::addWidget() call w/ the QWidget, and a QString as an argument (as the key) and it's all stored under the sheets. If you wanted to expand on this, you can store all of your QWidget's coordinates in this convenience class, and simply grap them O(1) when you deem necessary.

The storing of the coordinates is trivial, as the work done to calculate them is done anyway. The convenience class which holds these isn't too difficult, but does add some complexity.

like image 44
kiss-o-matic Avatar answered Oct 01 '22 15:10

kiss-o-matic