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