2025, Oct 22 23:17

Надёжное ограничение редактирования в QTreeWidget по столбцам

Разбираем, почему QTreeWidgetItem не ловит Tab, и показываем, как в PyQt5 надёжно ограничить редактирование по столбцам: переопределение edit(), нюансы editItem.

Остановить нежелательное редактирование в QTreeWidget кажется простым, пока не выясняется, что QTreeWidgetItem вовсе не видит клавиатурные события. Если нужно, чтобы Tab переносил фокус, но при этом сразу блокировал редактирование в других столбцах, наивный подход не сработает. Ниже — короткое объяснение, почему так происходит, и надежный способ задать правила редактирования, которые действительно соблюдаются.

Воспроизводим проблему

Конфигурация ниже позволяет добавить новую строку через контекстное меню, запускает редактирование в столбце «Header 1» и включает навигацию клавишей Tab. При нажатии Tab фокус перемещается и оставляет новый столбец редактируемым — чего как раз и не хочется.

from PyQt5 import QtCore, QtGui, QtWidgets
class NodeItem(QtWidgets.QTreeWidgetItem):
    def __init__(self, parent=None):
        QtWidgets.QTreeWidgetItem.__init__(self, parent)
    def keyPressEvent(self, ev):
        if ev.key() == QtCore.Qt.Key_Tab:
            self.setFlags(self.flags() | ~QtCore.Qt.ItemIsEditable)
            print('not working')
def make_view():
    tw = QtWidgets.QTreeWidget()
    tw.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
    tw.customContextMenuRequested.connect(showMenu)
    tw.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
    tw.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
    tw.setTabKeyNavigation(True)
    tw.headerItem().setText(0, "Header 0")
    tw.headerItem().setText(1, "Header 1")
    tw.headerItem().setText(2, "Header 2")
    return tw
def showMenu(pos):
    menu = QtWidgets.QMenu()
    action = menu.addAction('add item', insertNode)
    menu.exec_(view.mapToGlobal(pos))
def insertNode():
    it = NodeItem(view)
    it.setFlags(it.flags() | QtCore.Qt.ItemIsEditable)
    view.editItem(it, 1)
if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    app.setStyle("fusion")
    view = make_view()
    view.show()
    sys.exit(app.exec_())

Что происходит на самом деле

QTreeWidgetItem — не виджет и не обрабатывает пользовательский ввод. Клавиатурные события, такие как Tab, разбираются представлением и делегатами, а не элементом. Поэтому переопределение keyPressEvent() у элемента просто никогда не будет вызвано. Это ключевая причина, по которой попытки перехватить Tab внутри элемента тихо проваливаются.

Есть и тонкость: editItem() запускает редактирование, но не устанавливает текущий индекс. Если вы управляете навигацией или полагаетесь на текущий индекс, его нужно явно задать через setCurrentItem(item, column). И ещё: если требуется гарантированно запретить редактирование других столбцов независимо от того, как пользователь пытается его запустить, сама клавиша Tab — лишь симптом, а не первопричина. Навязывать правило нужно в механизме редактирования представления.

Наконец, если вы пытаетесь сбрасывать флаги, правильная побитовая форма — flags() & ~QtCore.Qt.ItemIsEditable, а не flags() | ~QtCore.Qt.ItemIsEditable.

Решение: контролируйте редактирование на уровне представления

Надёжный подход — унаследовать QTreeWidget и переопределить edit(). Если индекс относится к столбцу, который разрешено редактировать, вызывайте базовую реализацию. В противном случае — блокируйте, возвращая False. Так правило концентрируется в одном месте и одинаково применяется ко всем способам запуска редактирования: двойным щелчком, клавиатурой и программными вызовами.

from PyQt5 import QtCore, QtGui, QtWidgets
class NodeItem(QtWidgets.QTreeWidgetItem):
    def __init__(self, parent=None):
        QtWidgets.QTreeWidgetItem.__init__(self, parent)
class GuardedTree(QtWidgets.QTreeWidget):
    def __init__(self):
        super().__init__()
    def edit(self, index, trigger, event):
        if index.column() == 1:
            return super().edit(index, trigger, event)
        return False
def make_view():
    tw = GuardedTree()
    tw.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
    tw.customContextMenuRequested.connect(showMenu)
    tw.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
    tw.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
    tw.setTabKeyNavigation(True)
    tw.setHeaderLabels(["Header 0", "Header 1", "Header 2"])
    return tw
def showMenu(pos):
    menu = QtWidgets.QMenu()
    action = menu.addAction('add item', insertNode)
    menu.exec_(view.viewport().mapToGlobal(pos))
def insertNode():
    it = NodeItem(view)
    it.setFlags(it.flags() | QtCore.Qt.ItemIsEditable)
    view.editItem(it, 1)
if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    app.setStyle("fusion")
    view = make_view()
    view.show()
    sys.exit(app.exec_())

Здесь есть две мелкие доработки для удобства. Во‑первых, сигнал customContextMenuRequested у прокручиваемых областей передаёт координаты относительно viewport, поэтому для корректного позиционирования меню нужно использовать view.viewport().mapToGlobal(pos). Во‑вторых, заголовки можно задать одной командой через setHeaderLabels().

Почему это важно

Опора на обработку событий на уровне элемента для правил редактирования ненадёжна: элемент этих событий не получает. Ограничение в edit() представления даёт предсказуемость независимо от способа запуска редактирования и устраняет пограничные случаи, связанные с разными методами ввода. Логика остаётся в одном месте — её проще поддерживать и понимать.

Итоги и практические советы

Если нужно ограничить редактирование по столбцам в QTreeWidget, не боритесь с элементом — управляйте представлением. Переопределите edit() и разрешайте редактирование только нужных столбцов. Запуская редактирование программно, помните: editItem() не меняет текущий индекс; вызывайте setCurrentItem(), если важна текущая выборка. Для контекстных меню в прокручиваемых представлениях переводите координаты viewport в глобальные. И при переключении флагов используйте правильную побитовую форму с & ~ для их сброса. В итоге навигация по Tab становится предсказуемой, а нежелательное редактирование надёжно блокируется без побочных эффектов.

Статья основана на вопросе на StackOverflow от EGuy и ответе EGuy.