I have QMainWindow class with QScrollArea with content as central widget. And I created a main_layout with my scroll area as parent.
Thus I have ability to access to background layout (self.scroll_area_layout) as well as a forward layout (self.main_layout).
It works well, except one: When I use splitter and want to interact with my scroll area throught widget with WA_TransparentForMouseEvents attribute in this splitter, seems like splitter handle this events.
Also I tried to handle mouse events from widget and redirect it to my scroll area, but works only wheelEvents (and I need to be able to select the text from labels in scroll area).
This is my widget class to handle and redirect events to scroll area:
class SignalTransmitterWidget(QWidget):
def __init__(self, scroll_area: QScrollArea, parent=None):
super().__init__(parent)
self.scroll_area = scroll_area
self.setStyleSheet('background: rgba(0,200,0,200)')
def event(self, event):
print(event)
QApplication.sendEvent(self.scroll_area.viewport(), event)
return True
I have this code (without SignalTransmitterWidget):
from PySide6.QtWidgets import QMainWindow, QApplication, QVBoxLayout, QWidget, QScrollArea, QLabel, QSplitter
from PySide6.QtCore import Qt
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.resize(500, 400)
self.scroll_area_layout = QVBoxLayout()
messages_widget = QWidget()
messages_widget.setLayout(self.scroll_area_layout)
self.messages_scroll = QScrollArea()
self.messages_scroll.setWidgetResizable(True)
self.messages_scroll.setWidget(messages_widget)
self.setCentralWidget(self.messages_scroll)
self.main_layout = QVBoxLayout(self.messages_scroll)
self.main_layout.setSpacing(0)
for i in range(20):
label = QLabel(f"Selectable Text {i+1}")
label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
self.scroll_area_layout.addWidget(label)
self.splitter = QSplitter(Qt.Orientation.Vertical)
widget1 = QLabel("widget1 no interaction")
widget1.setStyleSheet("background: rgba(0,0,0,100)")
widget1.setAlignment(Qt.AlignmentFlag.AlignCenter)
widget2 = QLabel("This widget2 must be transparent for mouse events as well as widget3")
widget2.setStyleSheet("background: rgba(0,0,0,100)")
widget2.setAlignment(Qt.AlignmentFlag.AlignCenter)
widget2.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
widget3 = QLabel("This widget3 is transparent for mouse events, but it must be in splitter")
widget3.setAlignment(Qt.AlignmentFlag.AlignCenter)
widget3.setFixedHeight(200)
widget3.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
self.splitter.addWidget(widget1)
self.splitter.addWidget(widget2)
self.main_layout.addWidget(self.splitter)
self.main_layout.addWidget(widget3)
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
window.show()
Result:
And I need to interact with my scroll area in widget2 like you can interact from widget3.
A premise: while I may understand the graphic design requirement, allowing mouse interaction for a widget shown behind another that partially obfuscates them is probably not a good choice from the UX perspective. The fact that what's above also allows partial mouse interaction also makes this approach quite unintuitive.
The result of making a widget that is transparent to mouse events is that the QApplication will not consider them in the stacking order of the widget tree. The result is that Qt will try to find what's behind those widgets and eventually send the event to them if they do accept mouse events.
Since QSplitter is a container, and it obviously has a geometry that contains its children, if those children do not receive mouse events they're automatically sent to their parent, the QSplitter.
By default, and like any other standard widget, QSplitter does receive mouse events, it simply doesn't handle them. If Qt finds that a widget can receive mouse events but does not handle them, it will not "pass through" them, it will simply send the event to the parent, recursively up in the widget tree, until some widget eventually does handle it.
There are at least two possible workarounds I could think of, each of them with their pros and cons.
This is probably the simplest solution: use a QSplitter subclass, implement its mouse event handlers, and redirect those events based on a given context.
The concept is to create a QSplitter by giving a reference to the widget that would eventually receive the mouse events: whenever it receives those events, it will redirect them accordingly, ensuring that their coordinates are properly adjusted.
class PassThroughSplitter(QSplitter):
altChild = None
def __init__(self, orientation, altWidget):
super().__init__(orientation)
self.altWidget = altWidget
def _sendAltEvent(self, event):
if self.altChild is None:
return
QApplication.sendEvent(self.altChild, QMouseEvent(
event.type(),
self.altChild.mapFromGlobal(event.globalPos()),
event.button(),
event.buttons(),
event.modifiers()
))
def mousePressEvent(self, event):
self.altChild = self.altWidget.childAt(
self.altWidget.mapFromGlobal(event.globalPos()))
self._sendAltEvent(event)
def mouseMoveEvent(self, event):
self._sendAltEvent(event)
def mouseReleaseEvent(self, event):
self._sendAltEvent(event)
if self.altChild:
self.altChild = None
class MainWindow(QMainWindow):
def __init__(self):
...
self.splitter = PassThroughSplitter(
Qt.Orientation.Vertical, self.messages_scroll.viewport())
...
Pros:
Cons:
While the above issues may be fixed with some workarounds (programmatically setting the focus and redirecting hover events), doing so in a reliable way may be quite difficult, if not impossible.
This approach is much more complex, as it requires subclassing QSplitter and creating two separate QSplitterHandle subclasses: one as the real handle, the other as the displayed one and used as a "proxy" for mouse events.
The QSplitter subclass (which completely ignores mouse events) will create custom QSplitterHandles, used to actually manage QSplitter changes, which, in turn, will create "cousin" handles which are the ones that will be actually displayed and used for mouse movements.
class FakeSplitterHandle(QSplitterHandle):
def __init__(self, realHandle, orientation, parent):
super().__init__(orientation, parent)
self.realHandle = realHandle
self.realHandle.destroyed.connect(self.deleteLater)
def mousePressEvent(self, event):
self.realHandle.mousePressEvent(event)
def mouseMoveEvent(self, event):
self.realHandle.mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
self.realHandle.mouseReleaseEvent(event)
class RealSplitterHandle(QSplitterHandle):
_isMoving = False
fakeParent = None
def __init__(self, orientation, parent):
super().__init__(orientation, parent)
self.fake = FakeSplitterHandle(self, orientation, parent)
self.setUpdatesEnabled(False)
def setFakeParent(self, parent):
self.fake.setParent(parent)
self.fakeParent = parent
if self.isVisible() and parent is not None:
self.fake.show()
self.fake.raise_()
else:
self.fake.hide()
def showEvent(self, event):
super().showEvent(event)
self.fake.show()
self.fake.raise_()
def hideEvent(self, event):
super().hideEvent(event)
self.fake.hide()
def setFakeGeometry(self):
if not self.fakeParent:
return
offset = self.parent().pos()
self.fake.setGeometry(self.geometry().translated(offset))
def moveEvent(self, event):
self._isMoving = True
self.setFakeGeometry()
def resizeEvent(self, event):
super().resizeEvent(event)
if not self._isMoving:
self.setFakeGeometry()
else:
self._isMoving = False
class MouseIgnoreSplitter(QSplitter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def changeEvent(self, event):
super().changeEvent(event)
if event.type() == event.ParentChange:
parent = self.parent()
for i in range(self.count()):
self.handle(i).setFakeParent(parent)
def updateFakeHandles(self):
for i, fake in enumerate(self.fakeHandles):
if i == 0:
fake.hide()
else:
handle = self.handle(i)
fake.setGeometry(handle.geometry())
def createHandle(self):
return RealSplitterHandle(self.orientation(), self)
class MainWindow(QMainWindow):
def __init__(self):
...
self.splitter = MouseIgnoreSplitter(Qt.Orientation.Vertical)
self.splitter.setAttribute(
Qt.WidgetAttribute.WA_TransparentForMouseEvents)
...
The WA_TransparentForMouseEvents
is mandatory and obviously makes it unnecessary setting it for any child added to the splitter.
Pros:
Cons:
First of all, as already said, this UI approach is probably not a good idea to begin with (but the simplicity of your example doesn't clarify what the "stacked" labels would actually show).
When choosing which of the approaches above you'd use, don't consider the "amount" of pros/cons, but how they may affect the usage to the user. Do proper testing.
Finally, while it's common to feel "attached" to an idea, we should always consider that, as developers, our hopes or needs are rarely those of final users. What may seem fine (or even "cool") to us, may just be annoying to others. Consider if that UI approach is actually valid and really improves the UX experience: a good looking UI is worth nothing if it makes user interaction difficult to understand or use. Visual design must make intuitive and effective its usage, not the other way around.