2025, Oct 22 23:00

Prevent Unwanted Edits in PyQt5 QTreeWidget: Restrict Columns by Overriding edit() and Fix Tab Navigation

Prevent unwanted edits in PyQt5 QTreeWidget: QTreeWidgetItem misses key events—restrict column edits via edit() override and fix Tab navigation reliably.

Stopping unwanted edits in a QTreeWidget often looks trivial until you discover that QTreeWidgetItem doesn’t see keyboard events at all. If you want Tab to move focus but immediately prevent editing in other columns, the naive approach won’t work. Here’s a concise walkthrough of why this happens and a robust way to enforce editing rules that actually stick.

Reproducing the issue

The setup below allows creating a new row via context menu, starts editing on column “Header 1”, and enables tab navigation. Pressing Tab moves focus and keeps the new column editable, which is not desired.

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_())

What’s really going on

QTreeWidgetItem is not a widget and doesn’t handle user input. Keyboard events like Tab are processed by the view and delegates, not by the item, so redefining keyPressEvent() on the item will never be called. That is the core reason attempts to intercept Tab within the item silently fail.

There is another subtlety: editItem() starts editing but does not set the current index. If you intend to manage navigation or rely on the current index, you must explicitly set it with setCurrentItem(item, column). Also, if you want to ensure other columns cannot be edited regardless of how the user tries to trigger editing, the Tab key is merely a symptom, not the root cause. The enforcement point is the view’s editing pipeline.

Finally, if you try to unset flags, the correct bitwise form is flags() & ~QtCore.Qt.ItemIsEditable, not flags() | ~QtCore.Qt.ItemIsEditable.

The fix: enforce editability at the view level

The reliable solution is to subclass QTreeWidget and override edit(). If the index belongs to the column you allow to be edited, call the base implementation. Otherwise, block by returning False. This centralizes the rule and applies it uniformly to all edit triggers, including double click, keyboard, and programmatic calls.

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_())

There are two small quality-of-life adjustments here. First, customContextMenuRequested on scroll areas uses viewport coordinates, so showing the menu at the correct position requires view.viewport().mapToGlobal(pos). Second, header labels can be set in one call with setHeaderLabels().

Why this matters

Relying on item-level event handling for editing rules is fragile because the item never receives those events. Placing the constraint in the view’s edit() ensures consistency regardless of the edit trigger path and avoids edge cases caused by different input methods. It also keeps the logic in a single place that’s easy to maintain and reason about.

Summary and practical advice

If editing must be restricted by column in a QTreeWidget, don’t fight the item; control the view. Override edit() and authorize only the columns that should be editable. When starting edits programmatically, remember that editItem() doesn’t change the current index; call setCurrentItem() if current selection matters. For context menus in scrollable views, map the viewport position to global coordinates. And when toggling flags, use the correct bitwise form with & ~ to clear them. This combination results in predictable navigation with Tab and reliably prevents unwanted edits without side effects.

The article is based on a question from StackOverflow by EGuy and an answer by EGuy.