【问题标题】:re-send an event to a newly enabled child widget将事件重新发送到新启用的子小部件
【发布时间】:2019-07-18 01:55:42
【问题描述】:

注意:下面正在编辑一个更完整的示例

我想在 Qt 中实现以下内容(特别是 PyQt,但我相信解决方案在 python 和 C++ 中都是相似的):

我希望小部件具有默认禁用的内部小部件,当单击时,小部件将被启用,并且鼠标按下将传播到它。例如,在以下窗口/小部件中:

如果我在cd 之间单击,我希望QLineEdit 启用,获得焦点,并且光标位于cd 之间。我已经重新启用了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)

【问题讨论】:

    标签: python pyqt pyqt5 qevent qmouseevent


    【解决方案1】:

    您不能发送事件对象,因为当小部件使用它时 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)
    

    【讨论】:

    • 此解决方案似乎不适用于更复杂的小部件(例如具有内部布局的 QWidget
    • @bentheiii 我改进了我的实现,并添加了一个带有复杂小部件的示例,诀窍是将 QLineEdit 注册为示例是我的示例。
    • 好的,但这实际上并没有立即启用/禁用整个复杂的小部件,只有行编辑。这也需要修改内部小部件类ComplexWidget
    • @bentheiii 如果您需要更多帮助,您应该提供minimal reproducible example,很明显您调用了 ComplexWidget,因为您对 ComplexWidget 的想法似乎与我的非常不同。例如,在您的节目考虑的图像中,ComplexWidget 是具有 QLineEdit 和 QPushButton 作为子项的小部件,并且显然没有被禁用(这是我作为答案的基础)。如果你这样做了,那么你告诉我:-)
    • 很公平,我澄清了这个问题。
    【解决方案2】:

    试试看:

    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)
    

    【讨论】:

    • 这可能适用于 lineedit,但我想以这种方式启用其他(客户端提供的)小部件,因此以这种方式配置它们是无效的。
    • @bentheiii 你能举个例子说明你的问题吗?
    • 和上面的例子一样,除了你可以用用户提供的小部件替换QLineEdit('abcdef')(可能作为__init__的参数)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-01
    • 2022-06-13
    • 2011-10-02
    • 2019-06-18
    • 1970-01-01
    相关资源
    最近更新 更多