Как разрешить круговую навигацию по элементам в QComboBox? - PullRequest
0 голосов
/ 03 августа 2020

Настольное приложение Qt, которое я пишу, содержит QCombobox в пользовательском интерфейсе (созданном с помощью Designer). Выбрав QCombobox, я могу изменить выбранный элемент, прокручивая колесико мыши или нажимая стрелки вверх / вниз на клавиатуре. Все это работает нормально.

Например, при навигации с помощью стрелки вниз на клавиатуре, когда я достигаю нижнего элемента в списке, стрелка вниз больше не меняет выбранный элемент. Я понимаю, что это ожидаемое поведение.

Но для этого конкретного QComboBox я хотел бы иметь возможность продолжать нажимать стрелку вниз после достижения последнего элемента в списке и "переносить" обратно к первому элемент, чтобы я мог продолжить цикл по элементам. Я изучил документацию для QComboBox по адресу https://doc.qt.io/qt-5/qcombobox.html и для QAbstractItemModel по адресу https://doc.qt.io/qt-5/qabstractitemmodel.html, но я не нашел никакого способа достичь здесь того, что хочу.

В идеале я бы предпочел решение, которое работает для навигации со стрелками на клавиатуре, для навигации с помощью колеса прокрутки мыши и для любого другого жеста пользовательского интерфейса, который может попытаться активировать «следующий» или «предыдущий» элемент в QComboBox.

Ответы [ 2 ]

1 голос
/ 04 августа 2020

Я не пробовал это решение, но интуитивно догадываюсь, что оно верное. Я думаю, вам нужно сделать:

  1. Переопределить keyPressEvent(QKeyEvent *e) для обнаружения стрелок вверх и вниз.
  2. Если стрелка вниз нажата, проверьте, не последний индекс с использованием функции currentIndex() const по сравнению с размером самого поля со списком.
  3. Если да, измените текущий индекс на первый, используя setCurrentIndex(int index).
  4. Сделайте то же самое для стрелка вверх, если вы достигли первого индекса.

PS Поскольку currentIndex() вернул индекс после нажатия, это может заставить его перейти с предпоследнего индекса на первый. Таким образом, я предлагаю использовать частный логический член для переключения при первом выполнении условия.

Надеюсь, это решение вам поможет.

0 голосов
/ 05 августа 2020

Полное решение этой проблемы имеет несколько различных аспектов.

  1. Когда QComboBox раскрывается, чтобы показать все элементы, элегантным решением semanti c является переопределение метод QAbstractItemView::moveCursor(). Эта часть решения не требует обработчиков событий низкого уровня, потому что moveCursor() инкапсулирует концепцию «следующего» и «предыдущего». К сожалению, это работает только при расширении QComboBox. Обратите внимание, что в этом случае элементы фактически не активируются во время навигации, пока не произойдет другой жест, например щелчок или ввод.

  2. Когда QComboBox сворачивается для отображения одного элемента за раз (обычный случай), мы должны прибегнуть к низкоуровневому подходу к фиксации каждого соответствующего жеста, как обрисовано в ответе Мохаммеда Дейфаллаха. У меня с sh Qt была аналогичная абстракция, аналогичная QAbstractItemView::moveCursor(), но это не так. В приведенном ниже коде мы фиксируем события нажатия клавиш и колеса мыши - единственные жесты, о которых я знаю на данный момент. Если нужны и другие жесты, нам нужно будет реализовать каждый из них независимо. Поскольку архитекторы Qt не обобщили понятия «следующий» и «предыдущий» для этих случаев, как они сделали для QAbstractItemView::moveCursor().

Следующий код определяет класс замены для QComboBox который реализует эти принципы.

from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtCore import Qt


# CircularListView allows circular navigation when the ComboBox is expanded to show all items
class CircularListView(QtWidgets.QListView):
    """
    CircularListView allows circular navigation.
    So moving down from the bottom item selects the top item,
    and moving up from the top item selects the bottom item.
    """

    def moveCursor(
        self,
        cursor_action: QtWidgets.QAbstractItemView.CursorAction,
        modifiers: Qt.KeyboardModifiers,
    ) -> QtCore.QModelIndex:
        selected = self.selectedIndexes()
        if len(selected) != 1:
            return super().moveCursor(cursor_action, modifiers)
        index: QtCore.QModelIndex = selected[0]
        top = 0
        bottom = self.model().rowCount() - 1
        ca = QtWidgets.QAbstractItemView.CursorAction
        # When trying to move up from the top item, wrap to the bottom item
        if index.row() == top and cursor_action == ca.MoveUp:
            return self.model().index(bottom, index.column(), index.parent())
        # When trying to move down from the bottom item, wrap to the top item
        elif index.row() == bottom and cursor_action == ca.MoveDown:
            return self.model().index(top, index.column(), index.parent())
        else:
            return super().moveCursor(cursor_action, modifiers)


class CircularCombobox(QtWidgets.QComboBox):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        view = CircularListView(self.view().parent())
        self.setView(view)

    def _activate_next(self) -> None:
        index = (self.currentIndex() + 1) % self.count()
        self.setCurrentIndex(index)

    def _activate_previous(self):
        index = (self.currentIndex() - 1) % self.count()
        self.setCurrentIndex(index)

    def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
        if event.key() == Qt.Key_Down:
            self._activate_next()
        elif event.key() == Qt.Key_Up:
            self._activate_previous()
        else:
            super().keyPressEvent(event)

    def wheelEvent(self, event: QtGui.QWheelEvent) -> None:
        delta = event.angleDelta().y()
        if delta < 0:
            self._activate_next()
        elif delta > 0:
            self._activate_previous()
...