повторно отправить событие во вновь включенный дочерний виджет

ПРИМЕЧАНИЕ: ниже редактируется более полный пример

Я хочу реализовать следующее в Qt (в частности, PyQt, но я считаю, что решение будет похоже как на python, так и на C++):

Я хочу, чтобы виджет имел внутренний виджет, который по умолчанию отключен, и при нажатии виджет будет включен, и нажатие мыши будет распространяться на него. Например, в следующем окне/виджете:

введите здесь описание изображения

Если я нажму между c и d, я хочу, чтобы QLineEdit стал активным, взял фокус, а курсор находился между c и d. Мне удалось повторно включить QLineEdit, но я не могу отправить ему событие обратно.

Это мой код до сих пор:

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)

Это упрощенный пример, я также хочу обернуть таким образом другие виджеты, чтобы изменение внутренних виджетов было практически невозможно.

Насколько я могу судить, проблема в том, что отключенный дочерний виджет отклоняет событие мыши (поскольку он отключен) и отказывается снова принимать его (или любое другое событие) от родительского виджета.

Любая помощь вообще будет принята с благодарностью.

РЕДАКТИРОВАТЬ: ниже приведен более четкий пример того, что я имею в виду:

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)

я хочу, чтобы пользователь мог нажимать отключенный внутренний виджет, тем самым включая его полностью (т.е. как btn1, так и btn2 становятся включенными) и одновременно нажимая соответствующую кнопку. Мне нужно, чтобы это было сделано без изменения ComplexInnerWidget вообще (поскольку пользователь должен иметь возможность ввести любой виджет в качестве параметра для MyWidget)

РЕДАКТИРОВАТЬ 2: решение eyllanesc работает для приведенного примера, но я настроил его для MyWidget, чтобы он мог поддерживать несколько виджетов и быть вложенным в другие виджеты:

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)


person bentheiii    schedule 24.02.2019    source источник


Ответы (2)


Вы не можете отправить объект события, так как Qt удалит его, когда виджет его использует, что вы должны сделать, это создать другое событие с теми же данными. Я создал класс, который позволяет вам регистрировать виджеты, чтобы дать вам это свойство, без необходимости перезаписывать класс.

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_())

Обновление: нет необходимости пересылать событие, достаточно включить виджет.

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)
person eyllanesc    schedule 01.03.2019
comment
Это решение, похоже, не работает с более сложными виджетами (например, QWidget с внутренним макетом). - person bentheiii; 04.03.2019
comment
@bentheiii Я улучшил свою реализацию и добавил пример со сложным виджетом, хитрость заключается в том, чтобы зарегистрировать QLineEdit, поскольку образец - это мой пример. - person eyllanesc; 04.03.2019
comment
Хорошо, но на самом деле это не включает/отключает весь сложный виджет сразу, а только редактирование строк. Это также требует изменения внутреннего класса виджета ComplexWidget. - person bentheiii; 05.03.2019
comment
@bentheiii Если вам нужна дополнительная помощь, вы должны предоставить минимально воспроизводимый пример, где ясно, что вы вызываете ComplexWidget, поскольку кажется, что ваше представление о ComplexWidget сильно отличается от моего. Например, на изображении, которое вы показываете, рассмотрим ComplexWidget — это виджет, который имеет в качестве дочерних элементов QLineEdit и QPushButton и явно не отключен (это то, что я взял за основу для своего ответа). Если да, то скажи мне :-) - person eyllanesc; 05.03.2019
comment
справедливо, я уточнил вопрос. - person bentheiii; 06.03.2019

Попытайся:

from PyQt5.QtWidgets import QWidget, QLineEdit, QVBoxLayout, QPushButton, QApplication

class LineEdit(QLineEdit):                                    # +++
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setText('abcdef')
        self.setStyleSheet('color: blue; font-size: 32px')

    def mousePressEvent(self, event):
        super(LineEdit, self).mousePressEvent(event)
        self.cursor = self.cursorPosition() 

    def mouseReleaseEvent(self, event):
        self.setFocus()
        self.setCursorPosition(self.cursor)  


class MyWidget(QWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        layout    = QVBoxLayout(self)

#        self.edit = QLineEdit('abcdef')
        self.edit = LineEdit()                                  # +++

        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 does 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)

введите здесь описание изображения

person S. Nick    schedule 24.02.2019
comment
Это может работать для редактирования строки, но я хотел бы таким образом включить другие (предоставленные клиентом) виджеты, чтобы их настройка таким образом была неэффективной. - person bentheiii; 26.02.2019
comment
@bentheiii, можете ли вы привести пример, показывающий вашу проблему? - person S. Nick; 26.02.2019
comment
Как в примере выше, за исключением того, что вы можете заменить QLineEdit('abcdef') на предоставленный пользователем виджет (возможно, в качестве параметра для __init__) - person bentheiii; 26.02.2019