当一个小部件失去焦点时,我该如何拦截?

7
我有一个QPlainTextEdit,希望在失去焦点时处理内容。我看到可以使用focusChanged事件或focusOutEvent虚函数来实现这一点。
我不知道如何使用新语法传递参数(即my_app.focusChanged.connect(my_handler),其中my_handler是本地定义的函数)。因此,我尝试使用虚函数。
由于界面是使用QT Designer创建的,继承QPlainTextEdit会过度杀伤,所以我尝试通过简单地使用my_text_edit.focusOutEvent = my_handler覆盖虚拟函数来重写它。这确实拦截了我想要的消息,但显然覆盖了QPlainTextEdit中的一些内置功能,导致文本编辑器的光标在失去焦点时不会消失。我想到应该以某种方式调用原始事件,对我有效的方法如下:
在我的__init__方法中:
    self.original_handler = self.my_text_edit.focusOutEvent
    self.my_text_edit.focusOutEvent = self.my_handler

my_handler的定义如下:

    def my_handler(self, event):
        self.original_handler(event)
        # my own handling follows...

我基本上是在复制我希望库为我做的事情。我觉得这太笨拙了,而且我可以看到在维护过程中它可能会有很多反效果。有人能建议一种更简洁的方法吗?谢谢!

这是我做事的方式,所以我也对任何回应感兴趣。 - derricw
1个回答

12

更新:

下面的原始答案主要涉及PySide/PyQt4,并且部分已经过时-因此我现在添加了一个实现事件处理程序, 事件过滤器全局信号的PySide2/PyQt5演示文稿。但是,关于小部件促进的最后一节仍然有效,并且如果您正在使用Qt Designer,则值得考虑。

PySide2/PyQt5:

from PySide2 import QtCore, QtWidgets
# from PyQt5 import QtCore, QtWidgets

class PlainTextEdit(QtWidgets.QPlainTextEdit):
    def focusInEvent(self, event):
        print('event-focus-in:', self.objectName())
        super().focusInEvent(event)

    def focusOutEvent(self, event):
        print('event-focus-out:', self.objectName())
        super().focusOutEvent(event)

class Window(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.textEdit = PlainTextEdit(objectName='textEdit')
        self.dateEdit = QtWidgets.QDateEdit(objectName='dateEdit')
        self.button = QtWidgets.QPushButton('Test', objectName='button')
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.textEdit)
        layout.addWidget(self.dateEdit)
        layout.addWidget(self.button)
        self.dateEdit.installEventFilter(self)
        QtWidgets.QApplication.instance().focusObjectChanged.connect(
            self.handleFocusChange)

    def handleFocusChange(self, source):
        print(f'signal-focus-in:', source.objectName())

    def eventFilter(self, source, event):
        if event.type() == QtCore.QEvent.FocusIn:
            print('filter-focus-in:', source.objectName())
        elif event.type() == QtCore.QEvent.FocusOut:
            print('filter-focus-out:', source.objectName())
        return super().eventFilter(source, event)

if __name__ == '__main__':

    app = QtWidgets.QApplication(['Test'])
    window = Window()
    window.setGeometry(600, 100, 300, 200)
    window.show()
    app.exec_()

PySide/PyQt4:

个人而言,我从不使用猴子补丁(monkey-patching)的方式来覆盖虚拟方法,正确保留原始行为的方法是直接调用基类方法,像这样:

def my_handler(self, event):
    QtGui.QPlainTextEdit.focusOutEvent(self.my_text_edit, event)
    # my own handling follows...

我不明白为什么您不能使用focusChanged信号。它的处理程序将简单地是:
def my_handler(self, old, new):
    if old is self.my_text_edit:
        print('focus out')
    elif new is self.my_text_edit:
        print('focus in')

然而,我个人更倾向于使用事件过滤器

class Window(QtGui.QMainWindow)
    def __init__(self):
        ...
        self.my_text_edit.installEventFilter(self)

    def eventFilter(self, source, event):
        if (event.type() == QtCore.QEvent.FocusOut and
            source is self.my_text_edit):
            print('eventFilter: focus out')
            # return true here to bypass default behaviour
        return super(Window, self).eventFilter(source, event)

这是一种更加灵活的解决方案,可以提供一种通用的方式来处理通过Qt Designer导入的任何小部件的任何事件类型(或者实际上是您不想子类化的任何小部件)。

Widget Promotion:

还有一种可能性是widget promotion,它可以用于直接将生成的UI模块中的小部件替换为您自己的子类。这样就可以通过继承来覆盖任何虚拟方法,而不是对单个实例进行猴子补丁。如果您从Qt Designer导入多个相同类型的小部件,这些小部件共享许多自定义功能,那么这可以提供非常干净的解决方案。

如何在PyQt中执行小部件推广的简单说明可以在我的答案中找到:Replace QWidget objects at runtime


正如我在问题中提到的,我的问题与“focusChanged”信号有关,我不知道注册它的语法是什么。我该如何传递两个小部件参数(请参见问题的第二段)? - mapto
@mapto。我不知道你所说的“注册”是什么意思。信号是由QApplication发出的,它会自动发送正确的参数-您只需要连接一个适当的处理程序即可。如果您想自己发出信号,可以执行my_app.focusChanged.emit(old, new)。但是我在您的问题中没有看到任何提示为什么您需要这样做。您是否尝试过我的答案中的任何解决方案?如果是,请告诉我为什么它们对您不起作用。 - ekhumoro
我按照你的建议调用了基线方法,它对我起作用了。我还实现了事件过滤器,也成功了。 - mapto
然而,由于与我的其他回调函数的一致性,我更喜欢使用信号。通过“注册它”,我指的是与您所说的“连接适当的处理程序”完全相同。可能的混淆来自于我提供了一个对我无效的示例。我无法弄清楚在my_app.focusChanged.connect(my_handler)这一行中应该放什么代替my_app。我已经尝试过使用我的QApplicationQWidget容器,但会抛出AttributeError: 'PySide.QtCore.Signal' object has no attribute 'connect'错误。 - mapto
1
@mapto。应用程序实例可以通过QtGui.qApp进行访问。因此,请尝试使用QtGui.qApp.focusChanged.connect(my_handler) - ekhumoro

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接