NOTE: below is in edit with a more complete example
I want to implement the following in Qt (specifically PyQt, but I believe that the solution will be similar in both python and C++):
I want a widget to have an internal widget that is disabled by default, and when clicked, the widget will be enabled, and the mouse press will propagate to it. For example, in the following window/widget:
If I click between the c
and d
, I'd like the QLineEdit
to become enabled, take focus, and the cursor to be between the c
and d
. I got as far as re-enabling the QLineEdit
but I can't seem to send the event back to it.
This is my code so far:
from PyQt5.QtWidgets import QWidget, QLineEdit, QVBoxLayout, QPushButton, QApplication
class MyWidget(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QVBoxLayout(self)
self.edit = QLineEdit('abcdef')
self.edit.setEnabled(False)
layout.addWidget(self.edit)
self.disable_btn = QPushButton('disable edit')
self.disable_btn.clicked.connect(self._disable_edit)
layout.addWidget(self.disable_btn)
def _disable_edit(self, *a):
self.edit.setEnabled(False)
def mousePressEvent(self, a0):
if not self.edit.isEnabled() and self.edit.underMouse():
self.edit.setEnabled(True)
QApplication.instance().sendEvent(self.edit, a0) # <-- this doesn't seem to work
super().mousePressEvent(a0)
if __name__ == '__main__':
from PyQt5.QtWidgets import QApplication
app = QApplication([])
w = MyWidget()
w.show()
res = app.exec_()
exit(res)
This is a simplified example, I also want to wrap other widgets in this way, so that modifying the inner widgets is practically impossible.
The problem is, as far as as I can tell, that the disabled child widget rejects the mouse event (since it is disabled), and refuses to take it (or any other event) again from the parent widget.
Any help at all would be greatly appreciated.
EDIT: following is a clearer example of what I mean:
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton
class ComplexInnerWidget(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QVBoxLayout(self)
self.btn1 = QPushButton('button 1')
self.btn1.clicked.connect(self._btn1_click)
layout.addWidget(self.btn1)
self.btn2 = QPushButton('button 2')
self.btn2.clicked.connect(self._btn2_click)
layout.addWidget(self.btn2)
def _btn1_click(self, *a):
print('button 1')
def _btn2_click(self, *a):
print('button 2')
class MyWidget(QWidget):
def __init__(self, inner_widget: QWidget, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QVBoxLayout(self)
self.inner = inner_widget
self.inner.setEnabled(False)
layout.addWidget(self.inner)
if __name__ == '__main__':
from PyQt5.QtWidgets import QApplication
app = QApplication([])
inner = ComplexInnerWidget()
w = MyWidget(inner)
w.show()
res = app.exec_()
exit(res)
what I want is to allow the user to press the disabled inner widget, hereby enabling it in its entirety (i.e. both btn1 and btn2 becoming enabled), and pressing the appropriate button at the same time. I need this done without changing ComplexInnerWidget
at all (since the user should be able to enter any widget as a parameter to MyWidget
)
EDIT 2: eyllanesc's solution works for the example provided, but I have adjusted it for MyWidget
to be able to support multiple widgets, and to be nested in other widgets:
from PyQt5 import QtCore, QtWidgets
class ComplexInnerWidget(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QtWidgets.QVBoxLayout(self)
self.btn1 = QtWidgets.QPushButton('button 1')
self.btn1.clicked.connect(self._btn1_click)
layout.addWidget(self.btn1)
self.btn2 = QtWidgets.QPushButton('button 2')
self.btn2.clicked.connect(self._btn2_click)
layout.addWidget(self.btn2)
self.le = QtWidgets.QLineEdit('abcdef')
layout.addWidget(self.le)
def _btn1_click(self, *a):
print('button 1')
def _btn2_click(self, *a):
print('button 2')
class MyWidget(QtWidgets.QWidget):
class EnableMouseHelper(QtCore.QObject):
def __init__(self, *args, warden):
super().__init__(*args)
self.warden = warden
def eventFilter(self, obj, event):
if obj.isWidgetType() and event.type() == QtCore.QEvent.MouseButtonPress:
if self.warden in obj.window().findChildren(QtWidgets.QWidget) \
and self.warden.underMouse() and not self.warden.isEnabled():
self.warden.setEnabled(True)
obj.setFocus()
return super().eventFilter(obj, event)
def __init__(self, inner_widget: QtWidgets.QWidget, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QtWidgets.QVBoxLayout(self)
self.inner = inner_widget
self.inner.setEnabled(False)
layout.addWidget(self.inner)
self.helper = self.EnableMouseHelper(warden=self.inner)
QtWidgets.QApplication.instance().installEventFilter(self.helper)
class OuterWidget(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(MyWidget(ComplexInnerWidget()))
layout.addWidget(MyWidget(ComplexInnerWidget()))
le = QtWidgets.QLineEdit('hi there')
le.setEnabled(False)
layout.addWidget(le)
le = QtWidgets.QLineEdit('hi there')
layout.addWidget(le)
if __name__ == '__main__':
from PyQt5.QtWidgets import QApplication
app = QApplication([])
w = OuterWidget()
w.show()
res = app.exec_()
exit(res)
You can not send the event object since Qt will delete it when the widget consumes it, what you must do is create another event with the same data. I have created a class that allows you to register widgets to give you this property without having to overwrite the class.
from functools import partial
from PyQt5 import QtCore, QtGui, QtWidgets
class Singleton(type(QtCore.QObject), type):
def __init__(cls, name, bases, dict):
super().__init__(name, bases, dict)
cls.instance=None
def __call__(cls,*args,**kw):
if cls.instance is None:
cls.instance=super().__call__(*args, **kw)
return cls.instance
class EnableMouseHelper(QtCore.QObject, metaclass=Singleton):
def __init__(self, parent=None):
super(EnableMouseHelper, self).__init__(parent)
self._widgets = []
@staticmethod
def addWidget(widget):
if isinstance(widget, QtWidgets.QWidget):
helper = EnableMouseHelper()
helper._widgets.append(widget)
widget.installEventFilter(helper)
return True
return False
@staticmethod
def removeWidget(widget):
helper = EnableMouseHelper()
if widget is helper._widgets:
widget.removeEventFilter(helper)
helper._widgets.remove(widget)
def eventFilter(self, obj, event):
if obj in self._widgets and event.type() == QtCore.QEvent.MouseButtonPress:
if not obj.isEnabled():
new_event = QtGui.QMouseEvent(
event.type(),
event.localPos(),
event.windowPos(),
event.screenPos(),
event.button(),
event.buttons(),
event.modifiers(),
event.source()
)
obj.setEnabled(True)
obj.setFocus()
QtCore.QCoreApplication.postEvent(obj, new_event)
return super(EnableMouseHelper, self).eventFilter(obj, event)
class ComplexWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(ComplexWidget, self).__init__(parent)
le_1 = QtWidgets.QLineEdit(text='abcdef', enabled=False)
btn_le_1 = QtWidgets.QPushButton(text='disable edit', clicked=partial(le_1.setEnabled, False))
EnableMouseHelper.addWidget(le_1) # <---- register widget
le_2 = QtWidgets.QLineEdit(text='abcdef', enabled=False)
btn_le_2 = QtWidgets.QPushButton(text='disable edit', clicked=partial(le_2.setEnabled, False))
EnableMouseHelper.addWidget(le_2) # <---- register widget
flay = QtWidgets.QFormLayout(self)
flay.addRow(le_1, btn_le_1)
flay.addRow(le_2, btn_le_2)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = ComplexWidget()
w.show()
sys.exit(app.exec_())
Update: It is not necessary to forward the event, just enough to enable the widget.
from PyQt5 import QtCore, QtGui, QtWidgets
class EnableMouseHelper(QtCore.QObject):
def eventFilter(self, obj, event):
if obj.isWidgetType() and event.type() == QtCore.QEvent.MouseButtonPress:
for w in obj.window().findChildren(QtWidgets.QWidget):
if not w.isEnabled():
w.setEnabled(True)
obj.setFocus()
return super(EnableMouseHelper, self).eventFilter(obj, event)
class ComplexInnerWidget(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QtWidgets.QVBoxLayout(self)
self.btn1 = QtWidgets.QPushButton('button 1')
self.btn1.clicked.connect(self._btn1_click)
layout.addWidget(self.btn1)
self.btn2 = QtWidgets.QPushButton('button 2')
self.btn2.clicked.connect(self._btn2_click)
layout.addWidget(self.btn2)
self.le = QtWidgets.QLineEdit('abcdef')
layout.addWidget(self.le)
def _btn1_click(self, *a):
print('button 1')
def _btn2_click(self, *a):
print('button 2')
class MyWidget(QtWidgets.QWidget):
def __init__(self, inner_widget: QtWidgets.QWidget, *args, **kwargs):
super().__init__(*args, **kwargs)
layout = QtWidgets.QVBoxLayout(self)
self.inner = inner_widget
self.inner.setEnabled(False)
layout.addWidget(self.inner)
if __name__ == '__main__':
from PyQt5.QtWidgets import QApplication
app = QApplication([])
helper = EnableMouseHelper()
app.installEventFilter(helper)
inner = ComplexInnerWidget()
w = MyWidget(inner)
w.show()
res = app.exec_()
exit(res)