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

Posted

技术标签:

【中文标题】按下 Tab 键时移动到下一个选项卡(并关注相应的小部件)【英文标题】:Move to next tab (and focus on the corresponding widget) when pressing the Tab key 【发布时间】:2021-07-27 18:44:47 【问题描述】:

在我的应用程序中,我有一个 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()

【问题讨论】:

添加了一个最小的可重现示例 您能否更详细地解释您的短语:但不是针对相应的小部件。每个标签对应的小部件是什么,或者是否有任何标准可以知道哪些小部件是对应的吗? 选项卡是彼此相同的副本:它们使用相同的功能构建,创建填充选项卡的小部件的新实例。 “相应的小部件”可能与当前小部件位于相同的 (x,y) 位置(在另一个选项卡中除外)?虽然这个定义可能会遇到像组合框这样的小部件的问题......我的想法是找出当前焦点所在的小部件/布局,以某种方式找出它相对于选项卡的“子编号”,然后获取第 i 个子下一个选项卡(对于相同的 i 值)。但是我找不到这样的功能。 据我了解,您的选项卡将具有相同的小部件,如果我们将索引与选项卡中的每个小部件相关联,那么如果它位于第 i 个选项卡中并且第 j 个小部件具有焦点如果TAB被按下,那么它必须更改为第(i + 1)个选项卡并且当前选项卡对应的第j个小部件必须具有焦点? 是的,就是这样。我看到的一个问题是,由于第 i 个小部件实际上可能是“复杂小部件”或包含许多其他小部件的布局,如果没有某种递归解决方案,这可能无法正常工作?也许使用继承并添加功能来跟踪子项作为混合?如果不清楚,我可以扩展这个问题 【参考方案1】:

由于 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()

【讨论】:

【参考方案2】:

基于 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)

【讨论】:

以上是关于按下 Tab 键时移动到下一个选项卡(并关注相应的小部件)的主要内容,如果未能解决你的问题,请参考以下文章

当我按 Enter 键时,选项卡控件的选项卡页会关闭

Javascript代码按CTRL + TAB?

使用 Tab 键时如何更改文本字段选择顺序

Java - 请求关注当前元素

WPF:如何在不禁用箭头键导航的情况下禁用选项卡导航?

在 VC6-MFC 中按 ENTER 键时的 TAB 效果