PyQGIS – Unexpected Behavior in Custom Feature Form with Python Logic

pyqgisqgis-3

I'm using QGIS 3.22 trying to go through a tutorial (3.16 training manual) and I followed the popular Nathan Woodrow's blog setting custom form with python logic where I changed the PyQt4 imports for PyQt5. I set up a vector layer exactly as described by Woodrow and an UI form that works seamlessly.

from PyQt5.QtWidgets import QLineEdit, QDialogButtonBox, QMessageBox

nameField = None
myDialog = None

def formOpen(dialog, layerid, featureid):
    global myDialog
    myDialog = dialog
    global nameField
    nameField = dialog.findChild(QLineEdit, "Name")
    buttonBox = dialog.findChild(QDialogButtonBox, "buttonBox")
    
    # change buttonBox.accepted.disconnect(myDialog.accept)
    buttonBox.accepted.disconnect()
    
    buttonBox.accepted.connect(validate)

    # change buttonBox.rejected.connect(myDialog.reject)
    buttonBox.rejected.connect(myDialog.resetValues)
    
def validate():
    if not len(nameField.text()) > 0:
        QMessageBox.warning(None, "Attention", "Name can't be null", QMessageBox.Ok)
    else:
        # change myDialog.accept()
        myDialog.save()

To get the Python logic working I figure out that QgsAttibuteForm's accept and reject methods were deprecated and replaced by save and resetValues so I changed then accordingly.
Furthermore, when tried to place buttonBox.disconnect(myDialog.save) I got

TypeError: disconnect failed beetween 'accepted' and 'save'

I just disconnect all signals by using disconnect().
Finally I got the form running and the validation working but even after showing the invalid entry, the feature is save with a null value after clicking the ok button.
I tried to place a myDialog.resetValues() or myDialog.close() at the validation method with the same results.

I'd like the form to revert to the feature values and stay open for the user to correct the entry.

Best Answer

I concur that there seems to be an issue with disconnecting the save() and resetValues() slots from the accepted and rejected signals of the QDialogButtonBox of QgsAttributeForm. Indeed, perusing the docs, I saw that the QgsAttributeForm class has a method disconnectButtonBox() which should take care of this nicely. Inspecting the c++ source code you can see that this method should simply disconnect both those slots from the button box signals. Unfortunately, I was just not able to get it to work.

Below is an effective workaround which makes use of the QgsAttributeForm.hideButtonBox() method which does work. The basic idea is that we add two new, separate QPushButtons to the custom .ui, which we connect to our own slot functions. We also hide the existing button box (which is the workaround for disconnecting the slots from it's signals).

It takes a little more effort to set up the custom .ui file in QtDesigner. Instead of using the 'create dialog with buttons bottom' option, I created a dialog without buttons. I then added the two QPushButton objects (with a horizontal box layout); the rest of the form dialog is much the same. You still need to make sure the line edit object names match your field names. Additionally, I renamed the two push buttons btn_OK and btn_Cancel as we will be accessing them in the Python logic later. To make things look nice, I used a form layout for the whole dialog, set a maximum width for the two push buttons of 75, and added a couple of spacers. My dialog in QtDesigner looks like this:

enter image description here

The object names are:

Segment ID line edit --> Segment_ID

Name line edit --> Name

OK push button --> btn_OK

Cancel push button --> btn_Cancel

The Python file to add the logic looks like this:

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
 
nameField = None
myDialog = None
 
def formOpen(dialog, layerid, featureid):
    global myDialog
    myDialog = dialog
    global nameField
    nameField = dialog.findChild(QLineEdit,'Name')
    buttonOK = dialog.findChild(QPushButton, 'btn_OK')
    buttonCancel = dialog.findChild(QPushButton, 'btn_Cancel')
    myDialog.hideButtonBox()
    buttonOK.clicked.connect(validate)
    buttonCancel.clicked.connect(close_dialog)
    
def validate():
  # Make sure that the name field isn't empty.
    if not len(nameField.text()) > 0 or nameField.text() == 'NULL':
        msgBox = QMessageBox()
        msgBox.setText('Name field can not be null.')
        msgBox.exec_()
        myDialog.resetValues()
    else:
        # Return the form as accpeted to QGIS.
        myDialog.save()
        close_dialog()
            
def close_dialog():
    if isinstance(myDialog.parent(), QDialog):
        myDialog.parent().close()

After enabling macros in Settings-> Options-> General-> Project Files, I recorded the following two short screencasts showing the resulting behavior when adding and editing features.

Adding a new feature:

enter image description here

Editing an existing feature:

enter image description here

Related Question