10

I've got a Qt4 application (using the PyQt bindings) which contains a QListWidget, initialized like so:

class MyList(QtGui.QListWidget):
    def __init__(self):
        QtGui.QListWidget.__init__(self)
        self.setDragDropMode(self.InternalMove)

I can add items, and this allows me to drag and drop to reorder the list. But how do I get notification when the list gets reordered by the user? I tried adding a dropMimeData(self, index, data, action) method to the class, but it never gets called.

Chris B.
  • 85,731
  • 25
  • 98
  • 139
  • The answer from @Chani (not the currently accepted answer) is the easiest solution to this. – pancake Mar 28 '14 at 05:24
  • possible I'm doing something from but the answer from @Chani didn't work in my example for PyQt5. The accepted answer did work as did overriding QDropEvent as in my answer below – Vince W. Oct 03 '17 at 14:55

6 Answers6

15

I have an easier way. :)

You can actually access the listwidget's internal model with myList->model() - and from there there are lots of signals available.

If you only care about drag&drop, connect to layoutChanged. If you have move buttons (which usually are implemented with remove+add) connect to rowsInserted too.

If you want to know what moved, rowsMoved might be better than layoutChanged.

Chani
  • 760
  • 7
  • 11
  • Welcome to StackOverflow. Please take a few minutes to find out how to use [MarkDown](http://daringfireball.net/projects/markdown/basics) formatting. This will allow you to format the code code in your answer and make more readable for everyone else. – marko Nov 13 '12 at 20:41
  • rowsMoved() can't emit signal... I tested here it never enter slot when i move item by drag/drop. but item get change order – vivi Jul 16 '19 at 13:44
  • Note that the `.rowsMoved` signal from this answer tells you which items were moved and where, while the `QEvent.ChildRemoved` eventFilter in the accepted answer does not. – Dragon Mar 07 '20 at 20:40
7

I just had to deal with this and it's a pain in the ass but here's what to do:

You have to install an eventFilter on your ListWidget subclass and then watch for the ChildRemoved event. This event covers moves as well as removal, so it should work for re-arranging items with drag and drop inside a list.

I write my Qt in C++, but here's a pythonification version:

class MyList(QtGui.QListWidget):
    def __init__(self):
        QtGui.QListWidget.__init__(self)
        self.setDragDropMode(self.InternalMove)
        self.installEventFilter(self)

    def eventFilter(self, sender, event):
        if (event.type() == QEvent.ChildRemoved):
            self.on_order_changed()
        return False # don't actually interrupt anything

    def on_order_changed(self):
        # do magic things with our new-found knowledge

If you have some other class that contains this list, you may want to move the event filter method there. Hope this helps, I know I had to fight with this for a day before figuring this out.

Cas
  • 6,123
  • 3
  • 36
  • 35
Trey Stout
  • 6,231
  • 3
  • 24
  • 27
5

I found Trey Stout's answer did work however I was obviously getting events when the list order had not actually changed. I turned to Chani's answer which does work as required but with no code it took me a little work to implement in python.

I thought I would share the code snippet to help out future visitors:

class MyList(QListWidget):
    def __init__(self):
        QListWidget.__init__(self)
        self.setDragDropMode(self.InternalMove)
        list_model = self.model()
        list_model.layoutChanged.connect(self.on_layout_changed)

    def on_layout_changed(self):
        print "Layout Changed"

This is tested in PySide but see no reason it wouldn't work in PyQt.

Community
  • 1
  • 1
Cas
  • 6,123
  • 3
  • 36
  • 35
  • I have tested this in PyQt5, and unless I'm doing something wrong, this approach doesn't work, see answer below – Vince W. Oct 03 '17 at 14:47
2

I know this is old, but I was able to get my code to work using Trey's answer and wanted to share my python solution. This is for a QListWidget inside a QDialog, not one that is sub-classed.

class NotesDialog(QtGui.QDialog):
    def __init__(self, notes_list, notes_dir):
        QtGui.QDialog.__init__(self)
        self.ui=Ui_NotesDialog()
        # the notesList QListWidget is created here (from Qt Designer)
        self.ui.setupUi(self) 

        # install an event filter to catch internal QListWidget drop events
        self.ui.notesList.installEventFilter(self)

    def eventFilter(self, sender, event):
        # this is the function that processes internal drop in notesList
        if event.type() == QtCore.QEvent.ChildRemoved:
            self.update_views() # do something
        return False # don't actually interrupt anything
akehrer
  • 71
  • 1
  • 1
1

Not a solution, but some ideas:

You should probably check what is returned by supportedDropActions method. It might be that you need to overwrite that method, to include Qt::MoveAction or Qt::CopyAction.

You have QListView::indexesMoved signal, but I am not sure whether it will be emitted if you're using QListWidget. It worths checking.

Cătălin Pitiș
  • 14,123
  • 2
  • 39
  • 62
  • 2
    There is a known bug in Qt 4.5.x that the QListView::indexesMoved signal never fires. You are forced to install an eventFilter and handle it yourself. – Trey Stout Oct 06 '09 at 22:34
1

The QListWidget.model() approach seemed the most elegant of the proposed solutions but did not work for me in PyQt5. I don't know why, but perhaps something changed in the move to Qt5. The eventFilter approach did work, but there is another alternative that is worth considering: over-riding the QDropEvent and checking if event.source is self. See the code below which is an MVCE with all of the proposed solutions coded in for checking in PyQt5:

import sys
from PyQt5 import QtGui, QtWidgets, QtCore


class MyList(QtWidgets.QListWidget):
    itemMoved = QtCore.pyqtSignal()

    def __init__(self):
        super(MyList, self).__init__()
        self.setDragDropMode(self.InternalMove)
        list_model = self.model()
        # list_model.layoutChanged.connect(self.onLayoutChanged)  # doesn't work
        # self.installEventFilter(self)  # works
        self.itemMoved.connect(self.onLayoutChanged)  # works

    def onLayoutChanged(self):
        print("Layout Changed")

    def eventFilter(self, sender, event):
        """
        Parameters
        ----------
        sender : object
        event : QtCore.QEvent
        """
        if event.type() == QtCore.QEvent.ChildRemoved:
            self.onLayoutChanged()
        return False

    def dropEvent(self, QDropEvent):
        """
        Parameters
        ----------
        QDropEvent : QtGui.QDropEvent
        """

        mime = QDropEvent.mimeData()  # type: QtCore.QMimeData
        source = QDropEvent.source()

        if source is self:
            super(MyList, self).dropEvent(QDropEvent)
            self.itemMoved.emit()


app = QtWidgets.QApplication([])
form = MyList()
for text in ("one", "two", "three"):
    item = QtWidgets.QListWidgetItem(text)
    item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
    item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
    item.setCheckState(QtCore.Qt.Checked)
    form.addItem(item)

form.show()
sys.exit(app.exec_())
Vince W.
  • 3,561
  • 3
  • 31
  • 59