按 T​​ab 键时移动到下一个选项卡(并关注相应的小部件)

Move to next tab (and focus on the corresponding widget) when pressing the Tab key

在我的应用程序中,我有一个 QTabWidget,它包含可变数量的看似“相同”的选项卡和可变数量的小部件。 我希望,一旦按下 TAB(或 shift-TAB)按钮,应用程序的焦点就会移动到下一个(或上一个)选项卡,并专注于该选项卡的相应小部件(与小部件对应的那个)直到按下按键才有焦点)。

简单解决此问题的最佳方法是什么?我尝试使用 QShortcut 来捕捉按键,但我似乎无法找到一种方法来在下一个或上一个选项卡中获取相应的小部件并专注于此。

这是代码的最小示例,它只是移动到下一个选项卡而不是相应的小部件:

import sys

from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.QtWidgets import *


class tabdemo(QTabWidget):
    def __init__(self, num_tabs=2):
        super().__init__()

        shortcut = QtWidgets.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Tab), self)
        shortcut.activated.connect(self.on_tab)
        shortcut2 = QtWidgets.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Backtab), self)
        shortcut2.activated.connect(self.on_shift_tab)


        self.tabs = []
        for i in range(num_tabs):
            newtab = QWidget()
            self.tabs.append(newtab)
            self.addTab(newtab, f'Tab {i}')
            self.add_widgets_to(newtab)


    def add_widgets_to(self, tab):
        layout = QVBoxLayout()
        tab.setLayout(layout)

        layout.addWidget(QSpinBox())
        layout.addWidget(QCheckBox())

        gender = QHBoxLayout()
        gender.addWidget(QRadioButton("Male"))
        gender.addWidget(QRadioButton("Female"))
        layout.addLayout(gender)

    @QtCore.pyqtSlot()
    def on_tab(self):
        current_tab = self.currentIndex()
        self.setCurrentIndex((current_tab + 1) % self.count())
        # TODO find current widget in focus, and find the corresponding one in the next tab, and focus on that one... note that widgets could be complex (i.e., not direct children...)




    @QtCore.pyqtSlot()
    def on_shift_tab(self):
        print("do_something")
        current_tab = self.currentIndex()
        self.setCurrentIndex((current_tab - 1) % self.count())


def main():
    app = QApplication(sys.argv)
    ex = tabdemo()
    ex.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

由于 OP 指示每个页面将具有相同的组件,因此可以关联一个索引,以便在更改选项卡之前可以获得选项卡的索引,然后设置小部件的焦点,然后将焦点设置到另一个相应的小部件.

import sys

from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import (
    QApplication,
    QCheckBox,
    QHBoxLayout,
    QRadioButton,
    QShortcut,
    QSpinBox,
    QTabWidget,
    QVBoxLayout,
    QWidget,
)


class Page(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        spinbox = QSpinBox()
        checkbox = QCheckBox()
        male_radio = QRadioButton("Male")
        female_radio = QRadioButton("Female")

        layout = QVBoxLayout(self)
        layout.addWidget(spinbox)
        layout.addWidget(checkbox)

        gender = QHBoxLayout()
        gender.addWidget(male_radio)
        gender.addWidget(female_radio)
        layout.addLayout(gender)

        for i, widget in enumerate((spinbox, checkbox, male_radio, female_radio)):
            widget.setProperty("tab_index", i)


class Tabdemo(QTabWidget):
    def __init__(self, num_tabs=2):
        super().__init__()

        shortcut = QShortcut(QKeySequence(Qt.Key_Tab), self)
        shortcut.activated.connect(self.next_tab)
        shortcut2 = QShortcut(QKeySequence(Qt.Key_Backtab), self)
        shortcut2.activated.connect(self.previous_tab)

        for i in range(num_tabs):
            page = Page()
            self.addTab(page, f"Tab {i}")

    @pyqtSlot()
    def next_tab(self):
        self.change_tab((self.currentIndex() + 1) % self.count())

    @pyqtSlot()
    def previous_tab(self):
        self.change_tab((self.currentIndex() - 1) % self.count())

    def change_tab(self, index):
        focus_widget = QApplication.focusWidget()
        tab_index = focus_widget.property("tab_index") if focus_widget else None
        self.setCurrentIndex(index)
        if tab_index is not None and self.currentWidget() is not None:
            for widget in self.currentWidget().findChildren(QWidget):
                i = widget.property("tab_index")
                if i == tab_index:
                    widget.setFocus(True)


def main():
    app = QApplication(sys.argv)
    ex = Tabdemo()
    ex.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

基于 eyllanesc 的回答,我将功能改进为:

  • 考虑滚动条位置(如果存在)
  • 使用双向字典(implemented here)而不是线性查找
  • 使用 update_map() 方法动态添加所有相关小部件,而不必手动添加每个小部件。

张贴以防有人觉得有用。

from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QWidget, QTabWidget, QShortcut, QApplication, QScrollArea


class BidirectionalDict(dict):
    def __init__(self, *args, **kwargs):
        super(BidirectionalDict, self).__init__(*args, **kwargs)
        self.inverse = {}
        for key, value in self.items():
            self.inverse.setdefault(value, []).append(key)

    def __setitem__(self, key, value):
        if key in self:
            self.inverse[self[key]].remove(key)
        super(BidirectionalDict, self).__setitem__(key, value)
        self.inverse.setdefault(value, []).append(key)

    def __delitem__(self, key):
        self.inverse.setdefault(self[key], []).remove(key)
        if self[key] in self.inverse and not self.inverse[self[key]]:
            del self.inverse[self[key]]
        super(BidirectionalDict, self).__delitem__(key)

    def get_first_inv(self, key):
        return self.inverse.get(key, [None])[0]


class Page(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.widgets_map = BidirectionalDict()
        # ... add your widgets ...
        self.update_map()


    def update_map(self):
        widgets = self.findChildren(QWidget)
        for i, widget in enumerate(widgets):
            self.widgets_map[i] = widget


class MyQTabWidget(QTabWidget):
    def __init__(self):
        super().__init__()

        shortcut = QShortcut(QKeySequence(Qt.Key_Tab), self)
        shortcut.activated.connect(self.next_tab)
        shortcut2 = QShortcut(QKeySequence(Qt.Key_Backtab), self)
        shortcut2.activated.connect(self.previous_tab)


    @pyqtSlot()
    def next_tab(self):
        self.change_tab((self.currentIndex() + 1) % self.count())

    @pyqtSlot()
    def previous_tab(self):
        self.change_tab((self.currentIndex() - 1) % self.count())

    def change_tab(self, new_tab_index):
        old_tab: Page = self.currentWidget()
        focus_widget = QApplication.focusWidget()
        widget_index = old_tab.widgets_map.get_first_inv(focus_widget) if focus_widget else None

        self.setCurrentIndex(new_tab_index)

        new_tab: Page = self.currentWidget()

        if new_tab is not None and widget_index is not None:
            corresponding_widget: QWidget = new_tab.widgets_map[widget_index]
            corresponding_widget.setFocus(True)

            # Move scrollbar to the corresponding position
            if hasattr(old_tab, 'scrollbar'):
                # Tabs are identical so new_tab must have scrollbar as well
                old_y = old_tab.scrollbar.verticalScrollBar().value()
                scrollbar: QScrollArea = new_tab.scrollbar
                scrollbar.verticalScrollBar().setValue(old_y)