如何在 PyQt5 中实现虚拟 Tab 键 – nextInFocusChain() returns QFocusFrame

How to implement a Virtual Tab Key in PyQt5 – nextInFocusChain() returns QFocusFrame

我正在尝试在 PyQt5 中实现一个按钮,它的作用与按 Tab 键相同。为此,我获得了焦点项目,并调用 nextInFocusChain() 以按 Tab 键顺序获取下一个项目并将其设置为焦点。如果它不是 QLineEdit 对象,我重复。

self.focusWidget().nextInFocusChain().setFocus()
while type(self.focusWidget()) != QLineEdit:
    print(str(self.focusWidget()) + "" + str(self.focusWidget().nextInFocusChain()))
    self.focusWidget().nextInFocusChain().setFocus()

遗憾的是,这个片段没有环绕。在最后一个 QLineEdit 之后,它在连续打印时卡在循环中

...
<PyQt5.QtWidgets.QLabel object at 0x1114d2b80><PyQt5.QtWidgets.QFocusFrame object at 0x1114d2ca0>
<PyQt5.QtWidgets.QLabel object at 0x1114d2b80><PyQt5.QtWidgets.QFocusFrame object at 0x1114d2ca0>
<PyQt5.QtWidgets.QLabel object at 0x1114d2b80><PyQt5.QtWidgets.QFocusFrame object at 0x1114d2ca0>

这是一个重现的例子...

from collections import OrderedDict
from PyQt5.QtWidgets import QDialog, QFormLayout, QLineEdit, QCheckBox, QDialogButtonBox, QVBoxLayout, QPushButton

class AddressEditor(QDialog):
    def __init__(self,fields:OrderedDict,parent=None):
        super(AddressEditor, self).__init__(parent=parent)
        layout = QVBoxLayout(self)
        form = QFormLayout(self)
        self.inputs = OrderedDict()
        last = None
        for k,v in fields.items():
            self.inputs[k] = QLineEdit(v)
            if last is not None:
                self.setTabOrder(self.inputs[last],self.inputs[k])
            last = k
            form.addRow(k, self.inputs[k])
        layout.addLayout(form)
        button = QPushButton("TAB")
        layout.addWidget(button)
        button.clicked.connect(self.tabpressed)

    def tabpressed(self):
        self.focusWidget().nextInFocusChain().setFocus()
        while type(self.focusWidget()) != QLineEdit:
            print(str(self.focusWidget()) + "" + str(self.focusWidget().nextInFocusChain()))
            self.focusWidget().nextInFocusChain().setFocus()

if __name__ == '__main__':
    from PyQt5.QtWidgets import QApplication
    import sys
    app = QApplication(sys.argv)
    dialog = AddressEditor({"Text":"Default","Empty":"","True":"True","False":"False"})
    if dialog.exec():
        pass
    exit(0)

尝试实现“手动”焦点导航时需要考虑两个重要方面:

  1. 焦点链考虑 所有 小部件,而不仅仅是那些可以“明显”接受焦点的小部件;这包括具有 NoFocus 策略的小部件、父小部件(包括顶级 window)、隐藏的小部件以及可以通过样式“注入”的任何其他小部件,包括“助手”,例如QFocusFrame;
  2. 小部件可以有一个 focus proxy 可以将焦点“推回”到前一个小部件,这可能会导致递归问题,就像您的情况一样;

除此之外,您的实施还有其他问题:

  • 该按钮接受焦点,因此无论何时按下它都会重置焦点链;
  • 要比较 class 你应该使用 isinstance 而不是 type;
  • form 不应该有任何参数,因为它将作为嵌套布局添加,并且在任何情况下都不应该是 self,因为已经设置了布局;
  • Tab 键顺序必须在将两个小部件添加到具有相同祖先widget/window;
  • 的父级后设置
  • 通常会根据 when/where 将小部件添加到父级来自动设置选项卡顺序:只要按顺序插入所有字段,表单布局通常是不必要的;
class AddressEditor(QDialog):
    def __init__(self, fields:OrderedDict, parent=None):
        super(AddressEditor, self).__init__(parent=parent)
        layout = QVBoxLayout(self)
        form = QFormLayout() # <- no argument
        layout.addLayout(form)
        self.inputs = OrderedDict()
        last = None
        for k, v in fields.items():
            new = self.inputs[k] = QLineEdit(v)
            form.addRow(k, new) # <- add the widget *before* setTabOrder
            if last is not None:
                self.setTabOrder(last, new)
            last = new
        button = QPushButton("TAB")
        layout.addWidget(button)
        button.clicked.connect(self.tabpressed)
        button.setFocusPolicy(Qt.NoFocus) # <- disable focus for the button

    def tabpressed(self):
        nextWidget = self.focusWidget().nextInFocusChain()
        while not isinstance(nextWidget, QLineEdit) or not nextWidget.isVisible():
            nextWidget = nextWidget.nextInFocusChain()
        nextWidget.setFocus()

如果要保持按钮的焦点策略以便可以通过 Tab 到达,唯一的可能是跟踪应用程序的焦点变化,因为一旦用鼠标按钮按下按钮,它就已经获得焦点:

class AddressEditor(QDialog):
    def __init__(self, fields:OrderedDict, parent=None):
        # ...
        button = QPushButton("TAB")
        layout.addWidget(button)
        button.clicked.connect(self.tabpressed)

        QApplication.instance().focusChanged.connect(self.checkField)
        self.lastField = tuple(self.inputs.values())[0]

    def checkField(self, old, new):
        if isinstance(new, QLineEdit) and self.isAncestorOf(new):
            self.lastField = new

    def tabpressed(self):
        nextWidget = self.lastField.nextInFocusChain()
        while not isinstance(nextWidget, QLineEdit) or not nextWidget.isVisible():
            nextWidget = nextWidget.nextInFocusChain()
        nextWidget.setFocus()