Fixing the Drop Prohibited Icon in QTableView Drag and Drop

How to enable row reordering with drag and drop in QTableView using a custom model
Heads up! You've already completed this tutorial.

If you've tried to implement drag and drop row reordering in a QTableView using a custom QAbstractTableModel, you've probably run into the prohibited icon — the red circle with a slash through it that appears when you try to drop a row. You can see the drag start, the row follows your cursor, but the table refuses to accept the drop.

This is one of the most common stumbling blocks when working with drag and drop in Qt's model/view framework. The good news is that once you understand why it happens, the fix is straightforward.

In this tutorial, we'll walk through what causes the prohibited drop icon, how to fix it, and build a complete working example of a QTableView where you can drag and drop rows to reorder them.

Why the drop is prohibited

When you drag a row in a QTableView, Qt's drag and drop system goes through a series of checks before it will accept a drop. Setting properties on the view like setDragEnabled(True) and setDragDropMode(QAbstractItemView.InternalMove) is only half the story. The model behind the view also needs to cooperate.

Specifically, your QAbstractTableModel subclass needs to do three things:

  1. Return the right flags
  2. Implement mimeData() and mimeTypes() — so Qt knows how to serialize the dragged data.
  3. Implement dropMimeData() — so Qt knows how to handle the incoming data when it's dropped.

Without all three, the view has no way to complete the drop, and it shows the prohibited icon instead.

Let's go through each of these in detail.

Getting the flags right

Here's the part that trips most people up. You might expect that you need Qt.ItemIsDropEnabled on every item so that rows can be dropped onto them. But for row reordering, the opposite is true.

When you drag a row and want to drop it between other rows (to reorder), you're dropping onto the root item of the model — the blank, invisible parent that contains all the top-level rows. You are not dropping onto an individual item. If individual items have Qt.ItemIsDropEnabled, Qt interprets the drop as "drop this data into that item" (like dropping a file into a folder), which isn't what you want for reordering.

So the correct setup is:

PyQt/PySide Development Services — Stuck in development hell? I'll help you get your project focused, finished and released. Benefit from years of practical experience releasing software with Python.

Find out More

  • Items (valid indexes) should have Qt.ItemIsDragEnabled but not Qt.ItemIsDropEnabled.
  • The root item (invalid index) should have Qt.ItemIsDropEnabled.

In your flags() method, that looks like this:

python
def flags(self, index):
    default_flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable

    if index.isValid():
        # Items can be dragged, but NOT dropped onto
        return default_flags | Qt.ItemIsDragEnabled
    else:
        # The root (empty area) accepts drops
        return default_flags | Qt.ItemIsDropEnabled

When flags() is called with an invalid index, Qt is asking about the root item. By returning Qt.ItemIsDropEnabled only for the root, you're telling Qt: "drops are allowed between rows, but not onto rows." That's exactly what row reordering needs.

Implementing MIME data methods

Qt's drag and drop system uses MIME data to transfer information. Even for internal moves within the same table, the model needs to tell Qt how to package up the dragged row and how to unpack it at the drop site.

You need to implement three methods on your model:

mimeTypes()

This returns a list of MIME types your model supports. You can use any string as your custom MIME type — it just needs to be consistent between mimeData() and dropMimeData().

python
def mimeTypes(self):
    return ["application/x-tableview-dragrow"]

mimeData()

This is called when a drag starts. It receives a list of QModelIndex objects for the items being dragged, and you need to package them into a QMimeData object.

python
def mimeData(self, indexes):
    mime_data = QMimeData()
    # Get the row numbers being dragged
    rows = sorted(set(index.row() for index in indexes if index.isValid()))
    # Encode the row numbers as bytes
    data = QByteArray()
    stream = QDataStream(data, QIODevice.WriteOnly)
    for row in rows:
        stream.writeInt32(row)
    mime_data.setData("application/x-tableview-dragrow", data)
    return mime_data

dropMimeData()

This is called when the drop happens. It receives the MIME data, the drop action, and the target row and column. For row reordering, you need to decode the source row from the MIME data, remove it from the old position, and insert it at the new position.

python
def dropMimeData(self, data, action, row, column, parent):
    if action == Qt.IgnoreAction:
        return True

    if not data.hasFormat("application/x-tableview-dragrow"):
        return False

    # Decode the source row
    encoded = data.data("application/x-tableview-dragrow")
    stream = QDataStream(encoded, QIODevice.ReadOnly)
    source_rows = []
    while not stream.atEnd():
        source_rows.append(stream.readInt32())

    # If dropping on an item, use that item's row
    if parent.isValid():
        target_row = parent.row()
    elif row >= 0:
        target_row = row
    else:
        target_row = self.rowCount(QModelIndex())

    # Move rows (handle one at a time for simplicity)
    for source_row in reversed(source_rows):
        if source_row < target_row:
            target_row -= 1
        item = self._data.pop(source_row)
        self._data.insert(target_row, item)

    self.layoutChanged.emit()
    return True

The row parameter in dropMimeData() tells you where between existing rows the user dropped. A value of -1 with a valid parent means the drop was on a specific item (which we've disabled for regular items, but we handle it as a fallback). A value of -1 with an invalid parent means the drop was at the end of the list.

Implementing supportedDropActions()

You should also tell Qt which drop actions your model supports. For reordering, you want Qt.MoveAction:

python
def supportedDropActions(self):
    return Qt.MoveAction

This ensures Qt uses move semantics (remove from source, insert at target) rather than copy semantics.

Complete working example

Here's a full, runnable example that puts it all together. You can copy this, run it, and immediately drag rows to reorder them:

python
import sys

from PySide6.QtCore import (
    QAbstractTableModel,
    QByteArray,
    QDataStream,
    QIODevice,
    QMimeData,
    QModelIndex,
    Qt,
)
from PySide6.QtWidgets import (
    QAbstractItemView,
    QApplication,
    QHeaderView,
    QMainWindow,
    QTableView,
    QVBoxLayout,
    QWidget,
)


class ReorderTableModel(QAbstractTableModel):
    def __init__(self, data=None):
        super().__init__()
        self._data = data or []

    def rowCount(self, parent=QModelIndex()):
        return len(self._data)

    def columnCount(self, parent=QModelIndex()):
        return 1

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None

        if role in (Qt.DisplayRole, Qt.EditRole):
            return self._data[index.row()]

        return None

    def setData(self, index, value, role=Qt.EditRole):
        if index.isValid() and role == Qt.EditRole:
            self._data[index.row()] = value
            self.dataChanged.emit(index, index, [role])
            return True
        return False

    def flags(self, index):
        default_flags = Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable

        if index.isValid():
            # Items can be dragged but NOT dropped onto
            return default_flags | Qt.ItemIsDragEnabled
        else:
            # The root item accepts drops (between rows)
            return default_flags | Qt.ItemIsDropEnabled

    def supportedDropActions(self):
        return Qt.MoveAction

    def mimeTypes(self):
        return ["application/x-tableview-dragrow"]

    def mimeData(self, indexes):
        mime_data = QMimeData()
        rows = sorted(set(index.row() for index in indexes if index.isValid()))
        data = QByteArray()
        stream = QDataStream(data, QIODevice.WriteOnly)
        for row in rows:
            stream.writeInt32(row)
        mime_data.setData("application/x-tableview-dragrow", data)
        return mime_data

    def dropMimeData(self, data, action, row, column, parent):
        if action == Qt.IgnoreAction:
            return True

        if not data.hasFormat("application/x-tableview-dragrow"):
            return False

        # Decode the source row(s)
        encoded = data.data("application/x-tableview-dragrow")
        stream = QDataStream(encoded, QIODevice.ReadOnly)
        source_rows = []
        while not stream.atEnd():
            source_rows.append(stream.readInt32())

        # Determine target row
        if parent.isValid():
            target_row = parent.row()
        elif row >= 0:
            target_row = row
        else:
            target_row = self.rowCount()

        # Move each dragged row to the target position
        for source_row in sorted(source_rows, reverse=True):
            item = self._data.pop(source_row)
            # Adjust target if source was above it
            if source_row < target_row:
                target_row -= 1
            self._data.insert(target_row, item)

        self.layoutChanged.emit()
        return True

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if role == Qt.DisplayRole and orientation == Qt.Horizontal:
            return "Item"
        return None


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QTableView Drag & Drop Reorder")
        self.setMinimumSize(400, 350)

        # Sample data
        items = [
            "First item",
            "Second item",
            "Third item",
            "Fourth item",
            "Fifth item",
            "Sixth item",
            "Seventh item",
        ]

        # Set up the model
        self.model = ReorderTableModel(items)

        # Set up the table view
        self.table = QTableView()
        self.table.setModel(self.model)
        self.table.setSelectionMode(QAbstractItemView.SingleSelection)
        self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.table.setDragEnabled(True)
        self.table.setAcceptDrops(True)
        self.table.setDragDropMode(QAbstractItemView.InternalMove)
        self.table.setDefaultDropAction(Qt.MoveAction)
        self.table.setDragDropOverwriteMode(False)
        self.table.setDropIndicatorShown(True)

        # Stretch the column to fill the view
        self.table.horizontalHeader().setSectionResizeMode(
            QHeaderView.Stretch
        )

        # Layout
        layout = QVBoxLayout()
        layout.addWidget(self.table)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

When you run this, you'll see a table with seven rows. Click and drag any row, and you'll see a drop indicator line appear between rows as you move the cursor. Release the mouse to drop the row in its new position — no prohibited icon in sight.

QTableView with drag and drop reordering working correctly

What each view property does

The view configuration includes several properties that all work together. Here's what each one does and why it matters:

Property Purpose
setDragEnabled(True) Allows items to be dragged out of the view
setAcceptDrops(True) Allows the view to receive drops
setDragDropMode(InternalMove) Restricts drag and drop to within this view only, and uses move (not copy) semantics
setDefaultDropAction(Qt.MoveAction) Sets move as the default action when dropping
setDragDropOverwriteMode(False) Drops insert between rows instead of overwriting existing rows
setDropIndicatorShown(True) Shows a visual line indicating where the drop will land

Missing any one of these can cause subtle issues — from the prohibited icon to rows being duplicated instead of moved.

Common mistakes to watch for

Putting ItemIsDropEnabled on items instead of the root. This is the single most common cause of the prohibited icon. When items are drop-enabled, Qt tries to drop onto them (like placing something inside a folder). For reordering, only the root should be drop-enabled.

Forgetting to implement mimeData() and dropMimeData(). The base QAbstractTableModel doesn't provide working implementations of these methods. Without them, Qt has no way to serialize or deserialize the dragged data, so the drop silently fails.

Not emitting layoutChanged after modifying the data. After you move items around in your internal data list, the view needs to know the data has changed. Emitting layoutChanged tells the view to refresh everything. For more complex models, you might use beginMoveRows()/endMoveRows() instead, but layoutChanged is simpler and works well for this use case.

Using QStandardItemModel tips with QAbstractTableModel. Many drag and drop tutorials online are written for QStandardItemModel, which has built-in drag and drop support. If you're subclassing QAbstractTableModel, you need to implement the MIME methods yourself.

Wrapping up

Getting drag and drop to work with QTableView and a custom model requires coordination between the view's properties and the model's methods. The view needs to be told to allow dragging and accept drops, and the model needs to provide the right flags, serialize the dragged data, and handle the drop.

The counter-intuitive part — that individual items should not be drop-enabled for row reordering — is what catches most people. Once you understand that drops between rows are handled by the root item, everything else falls into place.

If you're new to Qt's model/view architecture, it's worth reading through the Model View Architecture tutorial to understand how models, views, and delegates work together. For working with tabular data in QTableView using numpy or pandas, see the QTableView with numpy and pandas tutorial. You can also learn more about drag and drop for custom widgets and signals and slots which underpin the layoutChanged signal used in this example.

Create GUI Applications with Python & Qt6 by Martin Fitzpatrick — (PySide6 Edition) The hands-on guide to making apps with Python — Save time and build better with this book. Over 15K copies sold.

Get the book

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak
Martin Fitzpatrick

Fixing the Drop Prohibited Icon in QTableView Drag and Drop was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.