Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

QListWidget item editing



  • Qt 5.7. I am using a QListWidget, where its QListWidgetItems have the ItemIsEditable flag. At the moment, at least, the items are the standard strings, with the editor being the built-in QLineEdit, i.e. I am not providing my own editor.

    I need to know when the user starts editing an item and when he has finished editing an item (e.g. while editing, buttons I have attached to the list widget for adding/deleting items should be disabled).

    Call me dumb, but I cannot see signals (or overridable) functions for these?

    • The only signal I can see for starting out is QListWidget::itemDoubleClicked. Is that the only way to start editing, and that's what I'm supposed to use?

    • Worse for finished editing. There is QListWidget::itemChanged, which seems great if the user makes a change and clicks elsewhere or whatever --- I receive that one.

    • But what if the user does not change the item? He might do anything, e.g. click some other widget, tab away, I have no idea. How do I know what the QLineEdit seems to know when it takes the user out of editing mode and no change has been made?



  • @VRonin
    Well, I followed your principle of having the QStyledItemDelegate emit signals with the QModelIndex as a parameter.

    I diverged from yours slightly. Given that I'm now overriding destroyEditor() so as to access the index, it seems to me that I might as well be consistent and override createEditor() for beginning editing. So the following two overrides:

    class JEditableListStyledItemDelegate(QtWidgets.QStyledItemDelegate):
        # class variable for "editStarted" signal, with QModelIndex parameter
        editStarted = QtCore.pyqtSignal(QtCore.QModelIndex, name='editStarted')
        # class variable for "editFinished" signal, with QModelIndex parameter
        editFinished = QtCore.pyqtSignal(QtCore.QModelIndex, name='editFinished')
    
        def createEditor(self, parent: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex):
            editor = super().createEditor(parent, option, index)
            if editor is not None:
                self.editStarted.emit(index)
            return editor
    
        def destroyEditor(self, editor: QtWidgets.QWidget, index: QtCore.QModelIndex):
            self.editFinished.emit(index)
            return super().destroyEditor(editor, index)
    

    The above seems to give me fairly reliable signals for what I want, e.g. disable other buttons for doing things to QListWidget (add row, delete row) while the user is editing a row.

    Seem reasonable to you? Thanks for all your input.



  • That's because it's handled by the delegate.
    Subclass QStyledItemDelegate

    class SignalItemDelegate : public QStyledItemDelegate{
    Q_OBJECT
    Q_DISABLE_COPY(SignalItemDelegate)
    public:
    explicit SignalItemDelegate(QObject* parent = Q_NULLPTR):QStyledItemDelegate(parent){
    QObject::connect(this,&SignalItemDelegate::closeEditor,this,&SignalItemDelegate::editFinished);
    }
    void setEditorData(QWidget *editor, const QModelIndex &index) const Q_DECL_OVERRIDE{
    editStarted();
    return QStyledItemDelegate::setEditorData(editor,index);
    }
    Q_SIGNALS:
    void editStarted();
    void editFinished();
    };
    

    And then call something like:

    SignalItemDelegate* delegate = new SignalItemDelegate(listWidget);
    QObject::connect(delegate,&SignalItemDelegate::editStarted,[](){qDebug("edit started")});
    QObject::connect(delegate,&SignalItemDelegate::editFinished,[](){qDebug("edit finished")});
    listWidget->setItemDelegate(delegate);
    


  • @VRonin
    Ah ha! Your famous QStyledItemDelegate, which I have steadfastly not got into so far! OK, let me go set this up and see where I get. I may have further questions for what I'm trying to actually achieve fully (the above is just one part), I'll come back to your expertise on that if I may....


  • Qt Champions 2017

    Did you get a chance to look other set of signals like.

    currentItemChanged(..)
    clicked(..)
    entered(..)
    pressed(..)
    


  • @dheerendra
    Yep, I looked through all those. None of them pick up, e.g., terminating editing, at least, so no good....


  • Qt Champions 2017

    terminatingEditing has to be custom. You have no choice other the your own delegate :)



  • @dheerendra
    Yep, that's fine, as I wrote to @VRonin above, I am presently having a go at his code (converted to Python/PyQt!) to get me going....



  • Hey, I am newbie

    and I want to know how to edit a QListWidget item without removing it to edit and adding back?

    thanks,

    Kissanime



  • listWidget->item(i)->setData(role,data);



  • @VRonin
    OK, I have come up with the basics which I believe emulates/corresponds to your C++ as best I can. I now have a further question about something which you don't do in your example which I would like.

    First, I'll paste the guts of my Python/PyQt code. You'll see it's rather different in signals/slots/emits. This may help or hinder, I don't know, feel free to ignore if that's best.

    class JEditableListStyledItemDelegate(QtWidgets.QStyledItemDelegate):
        # class variable for "editStarted" signal
        editStarted = QtCore.pyqtSignal(name='editStarted')
        # class variable for "editFinished" signal
        editFinished = QtCore.pyqtSignal(name='editFinished')
    
        def __init__(self, parent: QtCore.QObject=None):
            super().__init__(parent)
    
            self.closeEditor.connect(self.editFinished)
    
        def setEditorData(self, editor: QtWidgets.QWidget, index: QtCore.QModelIndex):
            self.editStarted.emit()
            return super().setEditorData(editor, index)
    
    
    class JEditableListWidget(QtWidgets.QListWidget):
        # class variable for "editStarted" signal
        editStarted = QtCore.pyqtSignal(name='editStarted')
        # class variable for "editFinished" signal
        editFinished = QtCore.pyqtSignal(name='editFinished')
    
        def __init__(self, parent: QtWidgets.QWidget=None):
            super().__init__(parent)
    
            styledItemDelegate = JEditableListStyledItemDelegate(self)
            styledItemDelegate.editStarted.connect(self.editStarted)
            styledItemDelegate.editFinished.connect(self.editFinished)
            self.setItemDelegate(styledItemDelegate)
    
    

    The above "works". The JEditableListStyledItemDelegate emits its own editStarted/Finished signals. The JEditableListWidget "redirects" those signals to its own editStarted/Finished signals, for the outside world to slot onto. So far so good?

    Now, if you look at a QListWidget signal for an item like http://doc.qt.io/qt-5/qlistwidget.html#itemChanged, its signature is QListWidget::itemChanged(QListWidgetItem *item). Hence it sends to the outside world a parameter of the QListWidgetItem which has been changed. My editStarted/Finished signals should do the same.

    Now I get stuck as to how I'm supposed to do that.

    I start by changing my JEditableListWidget signals to:

    class JEditableListWidget(QtWidgets.QListWidget):
        # class variable for "editStarted" signal, including item
        editStarted = QtCore.pyqtSignal(QtWidgets.QListWidgetItem, name='editStarted')
        # class variable for "editFinished" signal, including item
        editFinished = QtCore.pyqtSignal(QtWidgets.QListWidgetItem, name='editFinished')
    

    The extra first parameter of QtWidgets.QListWidgetItem declares the signal as forwarding the item as a parameter.

    Now how do I pass that? I believe that (a) I will need a lambda for the signal and (b) I need to know what item is being edited. I think where I had:

    styledItemDelegate.editStarted.connect(self.editStarted)
    

    I now need something like:

    styledItemDelegate.editStarted.connect(lambda: self.editStarted(someItemWidget))
    

    [Actually I suspect it might be more like:

    styledItemDelegate.editStarted.connect(lambda: self.editStarted.emit(someItemWidget))
    

    ]
    Could you tell me:

    • Is this how you would do it in C++ (e.g. with a lambda)? (If not, I may be doing it wrong.) Could you show me your C++ for how you would do this and I will attempt to figure it to PyQt? (Also, I believe your code omitted an emit because I know that's a NO-OP in C++; but could you include it wherever it should be, because in PyQt you can see we have to use emit as a function.)

    • How do I calculate what the someItemWidget will be, from inside my JEditableListWidget?



  • It's actually super easy.
    In the delegate:

    • add an argument of type QtCore.QModelIndex to the signals.
    • remove self.closeEditor.connect(self.editFinished)
    • change self.editStarted.emit() to self.editStarted.emit(index)
    • add
    def destroyEditor(self, editor: QtWidgets.QWidget, index: QtCore.QModelIndex):
            self.editFinished.emit(index)
            return super().destroyEditor(editor, index)
    

    Now in JEditableListWidget you can connect a slot that accepts a QtCore.QModelIndex. To find the corresponding QListWidgetItem you can just call item(index.row())



  • @VRonin
    TVM. I'll implement next week. You're passing the QtCore.QModelIndex available in the JEditableListStyledItemDelegate class up the signal chain to indicate which QListWidgetItem was acted upon. I thought the JEditableListWidget class would know which item in the list was being edited, but seemingly not?



  • @VRonin
    Well, I followed your principle of having the QStyledItemDelegate emit signals with the QModelIndex as a parameter.

    I diverged from yours slightly. Given that I'm now overriding destroyEditor() so as to access the index, it seems to me that I might as well be consistent and override createEditor() for beginning editing. So the following two overrides:

    class JEditableListStyledItemDelegate(QtWidgets.QStyledItemDelegate):
        # class variable for "editStarted" signal, with QModelIndex parameter
        editStarted = QtCore.pyqtSignal(QtCore.QModelIndex, name='editStarted')
        # class variable for "editFinished" signal, with QModelIndex parameter
        editFinished = QtCore.pyqtSignal(QtCore.QModelIndex, name='editFinished')
    
        def createEditor(self, parent: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex):
            editor = super().createEditor(parent, option, index)
            if editor is not None:
                self.editStarted.emit(index)
            return editor
    
        def destroyEditor(self, editor: QtWidgets.QWidget, index: QtCore.QModelIndex):
            self.editFinished.emit(index)
            return super().destroyEditor(editor, index)
    

    The above seems to give me fairly reliable signals for what I want, e.g. disable other buttons for doing things to QListWidget (add row, delete row) while the user is editing a row.

    Seem reasonable to you? Thanks for all your input.



  • @JonB said in QListWidget item editing:

    Given that I'm now overriding destroyEditor() so as to access the index, it seems to me that I might as well be consistent and override createEditor() for beginning editing.

    Only doubt I have is if the delegate recycles the editor. Try going from editing 1 cell to editing another one directly and see if both create and destroy are called or it just calls setEditorData



  • @VRonin
    OK, thanks! You should have said that concern was why you had overridden different/"inconsistent" methods, I had no idea why! Tried with direct click to edit another item, all is well, does call destroy and then create.

    So I shall mark what I did as the solution to my original question. TVM.

    P.S.
    Your Xmas hat looks good. But a lot of others are also wearing them...! :)


  • Lifetime Qt Champion

    @JonB said in QListWidget item editing:

    Your Xmas hat looks good. But a lot of others are also wearing them...! :)

    Well that is is on purpose :)



  • @JonB said in QListWidget item editing:

    Your Xmas hat looks good

    All credit goes to @mrjj


Log in to reply