1
  • I have a layer with two attributes: "text" (string) and "int" (integer).
  • I used drag'n'drop form design to have a form with just the "text" attribute in it.
  • I added Python Init Code:
from functools import partial

from PyQt5.QtWidgets import QPushButton

def on_ok(layer, feature):
    field_idx = layer.fields().indexOf("int")
    attribute_changed = layer.changeAttributeValue(feature.id(), field_idx, 12345)
    if not attribute_changed:
        raise Exception("Attribute value could not be changed!")

def my_form_open(dialog, layer, feature):
    ok_button = dialog.findChild(QPushButton)
    ok_button.clicked.connect(partial(on_ok, layer, feature))

So the form does not show an input widget for the "int" field. But when pressing OK, the code should change the edited feature's "int" value to 12345.

This works if I edit existing features.

But if I create a new feature, the "int" value will be empty/NULL instead of 12345. Any pointers on how to properly update a new feature's attribute fields, that exist on a layer, but are not shown the user with the QGIS' widgets in the attribute form?

I have already tried

attribute_changed = feature.setAttribute(field_idx, 12345)

and

feature[attribute_name] = 12345

with no success either.

In my real scenario the value would be dynamic and defined elsewhere, the 12345 is just an example.

4
  • I noticed that the both the id(feature) is always different when I look at it in either function which suggests the object got copied (by the partial() call?). The same is true for feature.id(). As a hack I tried using a global instead of passing the feature to on_ok(). Jan 10 at 9:56
  • The QGIS attribute form (QgsAttributeForm) is made in such a way that only the fields that are included in the form are editable and this behavior doesn't look like it can be modified. Imo your best solution would be to attach to the signals of the QgsVectorLayer to update the value independently of the form. Which in a way is what you do when you change the value of the "int" field at the closing of the attribute form.
    – Kalak
    Jan 11 at 7:31
  • I don't understand. How does the form prevent other fields to be edited? Am I not side-stepping the form by using a method of the layer? Which signal would you suggest and how would I make sure to pass a value from the form (the 12345 in the example) to the layer/feature? Jan 11 at 9:03
  • I mean that you can't use form functions directly from the "dialog" object if the field is removed from the form. So you have to edit directly the layer independently from any form "actions". the way @Matt does it is good (I didn't know about that use of editBuffer()) but it needs to handle update of feature too (probably depending if the id is >0 or not).
    – Kalak
    Jan 11 at 10:57

2 Answers 2

2

If you add a print statement in on_ok to check the feature.id() you will probably find that it is wildly different to what you expect.

I was able to get the correct feature id by querying the editBuffer of the layer for the most recently added feature. While editing, features receive an incrementally decreasing negative feature id, so the most recent is the lowest value.

from PyQt5.QtWidgets import QMessageBox, QPushButton
from functools import partial

def on_ok(layer, feature):
    field_idx = layer.fields().indexOf("int")
    
    print(feature.id()) # <--- not what you expect

    edit_buffer = layer.editBuffer()
    added_feats = edit_buffer.addedFeatures()

    # get the id of the most recently added feature in the edit buffer
    last_added_feat_id = min(list(added_feats.keys()))
    
    attribute_changed = layer.changeAttributeValue(last_added_feat_id, field_idx, 12345)
    if not attribute_changed:
        raise Exception("Attribute value could not be changed!")

def my_form_open(dialog, layer, feature):
    ok_button = dialog.findChild(QPushButton)
    ok_button.clicked.connect(partial(on_ok, layer, feature))
9
  • Nicely done for new features but ValueError: min() arg is an empty sequence on the update feature if the feature wasn't created in the edit block or would modify the wrong feature (change last added feature instead of the feature selected for update).
    – Kalak
    Jan 11 at 10:51
  • 1
    Good catch, but I will leave it like this for now. I have every faith that @bugmenot123 is skilled enough to add the relevant checks specific to their use case. The editBuffer object also has an attributeValueChanged signal which will emit the feature id of the edited feature.
    – Matt
    Jan 11 at 10:57
  • Thank you! This would break as soon as a second feature was added before the form of the first one was closed though, right? Jan 11 at 12:01
  • I just tested and this doesn't seem to be the case. I added a few features (not closing the feature form between edits) and after confirming by clicking 'OK' on all open forms, every feature received 12345 in the "int" field. It would probably require a little more robust testing, however.
    – Matt
    Jan 11 at 12:07
  • 1
    Indeed, I understood that from your question. But, as I have no idea how that should be calculated, I cannot say for sure whether my solution will work or fail. Are you able to test and provide feedback?
    – Matt
    Jan 11 at 13:59
1

A minimal solution, based on @Matt's great answer:

from functools import partial

from qgis.PyQt.QtWidgets import QPushButton, QSpinBox

def on_ok(layer, feature, spinbox):
    value = spinbox.value()
    field_idx = layer.fields().indexOf("int")
    
    feature_id = feature.id()
    if feature_id == -9223372036854775808:
        # this feature was newly added in this "form session"
        edit_buffer = layer.editBuffer()
        added_feats = edit_buffer.addedFeatures()
        # get the id of the most recently added feature in the edit buffer
        feature_id = min(list(added_feats.keys()))
    # otherwise we are looking at an existing feature, committed or not
    
    attribute_changed = layer.changeAttributeValue(feature_id, field_idx, value)
    if not attribute_changed:
        raise Exception("Attribute value could not be changed!")

def my_form_open(dialog, layer, feature):
    layout = dialog.layout()
    spinbox = QSpinBox()
    layout.insertWidget(0, spinbox)

    ok_button = dialog.findChild(QPushButton)
    ok_button.clicked.connect(partial(on_ok, layer, feature, spinbox))

Explanation: When a new feature is "about to be created", the form receives a blank feature with the ID -9223372036854775808 (-2**63). In that case we have to get the actual feature's ID from the edit buffer after it has been created, which happens when the user OKs the form.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.