[PyQt] Suppress departure from cell in QTableView

Maurizio Berti maurizio.berti at gmail.com
Mon Feb 18 02:57:06 GMT 2019


Il giorno dom 17 feb 2019 alle ore 23:19 John F Sturtz <john at sturtz.org> ha
scritto:

> [...]
>
There are still a few things I don't fully understand.
>
> I actually did experiment with reimplementing closeEditor() awhile back.
> But I reimplemented it in the *delegate*, not in the *view*.  I thought I
> understood closeEditor() to be a member of QAbstractItemDelegate, not
> QTableView.  I don't quite understand how it is possible to override this
> method in the QTableView definition.  (Still, empirically, it seems to
> work, so I must be wrong).
>

Well, closeEditor is a *signal* in delegates, which is automatically
connected to the closeEditor *slot* of the item view.
The signal is sent anyway from the delegate, whenever the focus is actually
changed by the item view (or any other widget, including the widget's
window losing focus), or when a "change cell" is internally requested for
the delegate by the view (via tab navigation or any way of commit/ignore is
supported by it): for example, if you use a QComboBox as an editor for the
itemDelegate, the closeEditor won't be sent if the item of the combo has
changed, *but* will be sent if the combo is editable and you "submit" any
data or ignore it.

As explained in the QItemDelegate details (
https://doc.qt.io/archives/qt-4.8/qitemdelegate.html#details ):

The closeEditor
> <https://doc.qt.io/archives/qt-4.8/qabstractitemdelegate.html#closeEditor>()
> signal indicates that the user has completed editing the data, and that the
> editor widget can be destroyed.


The stress is on the "*can* be destroyed".
The fact that the signal is emitted does not actually mean that the editor
will actually be closed: it only means that the editing is theoretically
finished, and it's a way of telling the view (and the delegate) that it
*should* close the editor.
In fact, you can actually disconnect the signal of the delegate from the
relative slot of the view:

self.myTable.itemDelegate().closeEditor.disconnect(self.myTable.closeEditor)

This will result in an editor kept alive even when trying to commit (or
ignore) the data by using keyboard events.
In spite of this, though, the view.closeEditor() slot will obviously be
called anyway from the view the first time you click on any other cell, due
to the currentChanged() slot called from the mousePressEvent(); note that
if you return the closeEditor() slot as above - without actually destroying
the editor -, the slot won't be called another time even if you change the
current index again by clicking on another item or outside the table
contents (I suppose that it's because the closeEditor signal has been
already fired once and the delegate is still there), unless the focus is
restored to the editor again first. Also, in the same scenario, changing
the focus to another widget won't result in the closeEditor() slot called,
but the signal will be fired anyway.

As a side note, be aware that if you implement the closeEditor slot, you'll
need to properly set the slot signature to correctly allow the auto
connection the view creates for the delegate to work and, eventually,
disconnect from it.
In PyQt4, using PyQt objects was fine enough:

@QtCore.Slot(QtWidgets.QWidget, QtWidgets.QAbstractItemDelegate.EndEditHint)
def closeEditor(self, widget, hint):
    [...]

With PyQt5 (at least on my old version, 5.7.1) it seems that the actual C++
string signature is required as reported from the official Qt docs (not
PyQt/PySide), so it will need to be like this in order to allow the
original signal disconnection to work, since the connection was not
"created by Python":

@QtCore.Slot('QWidget*', 'QAbstractItemDelegate::EndEditHint')
def closeEditor(self, widget, hint):
    [...]


That said, since the type of control you need can be required whenever any
event type (tab navigation, editor commit/ignore, mouse events, etc) and
source (the delegate, the view or the application) occurs, it's better to
implement the closeEditor in the view itself, which is not only the
"simplest" and most logic solution, but also the way Qt actually deals with
item editing.


One slight problem with your code as written:  I found that if a delegate
> editor is active and I mouse click on a different cell, the
> mousePressEvent actually registers before the closeEditor() method is
> called.  You assign self.currentEditor inside closeEditor(), so on the
> first mouse click it isn't set yet.  However, if I assign currentEditor
> when the editor is *created* (i.e., in the delegate's createEditor()
> method), then it seems to work.
>
> And with that, I find I don't seem to need to reimplement closeEditor()
> at all.  Just catching a mousePressEvent, checking that self.state() is
> EditState and calling self.currentEditor.setFocus() to set the focus back
> to the editor widget seems to be enough.
>

It works because you've set the currentEditor in the createEditor() slot.
I didn't know you were already using a delegate, so your solution can work
too.

Just a small "conceptual" note about this. Remember that, even if your code
works fine and makes perfect sense, from the modularity point of view,
that's not "theoretically" good, as item delegates can be used
interactively and more than once. A much more "elegant" way to do so would
be to request the actual view from the parent argument of createEditor();
keep in mind, though, that the parent of the editor is the viewport of the
view (since an item view is a QAbstractScrollArea and its contents are in
an embedded "scrollable" QWidget), so the reference to the view is the
parent() of the parent argument of createEditor() method, actually.
Anyway, as said, that's not an actual "error", and in most cases your
implementation is fine enough (I've done something similar lots of times
myself).


I will also need to similarly catch keystrokes that try to change to focus
> to a different cell.  But that is easier, because those events go to the
> delegate, so can be handled within the delegate's eventFilter() method.
>

Luckily enough, Qt's implementation correctly handles QKeyEvents of
delegates and views in the right "levels". Tab navigation and return/escape
keys are catched by the view, so you can take care of everything in the
closeEditor() slot.


I *still* don't really understand why it didn't work when I tried catching
> and suppressing the FocusOut event.  I feel like I followed the
> documentation properly.  But I guess it's somehow related to the fact that
> there are multiple levels of widgets that the events are passing through.
>

As I mentioned, working with focus events in widgets as complex as views
can be very hard to deal with.
You have an inherited QAbstractItemView which is inherited from
QAbstractScrollArea, which might even have one or two QHeaderViews and
QScrollBars (and other scrollbar additional widgets, optionally), an
optional cornerWidget, and a QWidget that works as a viewport, containing
the actual view contents; *then* you can have an editor, which can also be
a complex widget like a combobox (with its internal widgets handling focus
on their own, including the popup view). Moreover, a QTableView also has a
cornerButton too.
So, you'll have to exactly understand the hierarchy of all those widget
levels, remembering that any widget reacts differently according to it's
level in the tree hierarchy and the event type, by accepting or ignoring
the event (which doesn't mean that they won't deal with it) and eventually
sending it "back" to the whole parent widget tree if required.
Finally, whenever an editor is active, it has a focusProxy set to the
view's focusProxy, which means that that widget handles focus events of
another one, thus reacting to those events in its place first.



> In any event, it looks like I have a workaround.  Thank you!
>

Glad to be of any help! :-)

Cheers,
Maurizio

PS: in your code, you've been using model.setData() inside the
setEditorData() method of the itemDelegate, which not only doesn't makes
not much sense, but might also result in an infinite recursion.

-- 
È difficile avere una convinzione precisa quando si parla delle ragioni del
cuore. - "Sostiene Pereira", Antonio Tabucchi
http://www.jidesk.net
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://www.riverbankcomputing.com/pipermail/pyqt/attachments/20190218/79974f91/attachment.html>


More information about the PyQt mailing list