Home > Back-end >  How to implement a Virtual Tab Key in PyQt5 – nextInFocusChain() returns QFocusFrame
How to implement a Virtual Tab Key in PyQt5 – nextInFocusChain() returns QFocusFrame

Time:01-09

I'm trying to implement a button in PyQt5 which acts identically to pressing the Tab-Key. For this, I get the focused item, and call nextInFocusChain() to get the next item in tab order and set it to focus. If it is not a QLineEdit object, I repeat.

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

Sadly this snipped does not wrap around. After the last QLineEdit, it gets stuck inside the loop while continuously printing

...
<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>

Here a reproducing example...

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)

CodePudding user response:

There are two important aspects to consider when trying to achieve "manual" focus navigation:

  1. the focus chain considers all widgets, not only those that can "visibly" accept focus; this includes widgets that have a NoFocus policy, the parent widget (including the top level window), hidden widgets, and any other widget that can be "injected" by the style, including "helpers" like QFocusFrame;
  2. widgets can have a focus proxy which can "push back" the focus to the previous widget, and this can cause recursion issues like in your case;

Besides that, there are other issues with your implementation:

  • the button accepts focus, so whenever it's pressed it resets the focus chain;
  • to compare the class you should use isinstance and not type;
  • form should not have any argument since it's going to be added as a nested layout, and in any case it shouldn't be self since a layout has already been set;
  • the tab order must be set after both widgets are added to parents that share the same a common ancestor widget/window;
  • the tab order is generally automatically set based on when/where widgets are added to the parent: for a form layout is generally unnecessary as long as all fields are inserted in order;
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()

If you want to keep the focus policy for the button so that it can be reached through Tab, the only possibility is to keep track of the focus change of the application, since as soon as the button is pressed with the mouse button it will already have received focus:

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()
  •  Tags:  
  • Related