Das demo2a_crud-Plugin verteilt seine Aufgaben auf drei QML-Dateien. Öffnen Sie jede davon in Ihrem Editor — am besten nebeneinander — während Sie diese Seite durcharbeiten.
d2a_data_model.qml — FormDataModel (Singleton)d2a_plugin_component.qml — Viewd2a_form_controller.qml — ControllerDas FormDataModel beantwortet eine einzige Frage: Welche Felder hat das Formular? Es ist ein pragma Singleton, d.h. es gibt genau eine Instanz, die im gesamten Plugin geteilt wird. Es enthält keine Logik und keinen Laufzeitzustand — nur ein JS-Array mit Feldbeschreibungs-Objekten.
pragma Singleton
import QtQuick
QtObject {
property var fields: [
{ "name": "name", "label": "Eintragsname" },
{ "name": "s1", "label": "Gedanken" },
{ "name": "s2", "label": "Gefühle" }
]
}
Jedes Objekt im Array hat zwei Schlüssel:
name — der Attributname im GeoPackage-Layer. Wird vom Controller verwendet, um Feature-Attribute zu lesen und zu schreiben.label — der lesbare Text, der neben dem Eingabefeld im Formular angezeigt wird.Um ein neues Feld zum Formular hinzuzufügen, muss hier nur ein Eintrag ergänzt werden. Der Rest des Plugins passt sich automatisch an — das ist der Sinn des Musters.
Der View beantwortet: Wie sieht das Formular aus, und wie werden Benutzeraktionen verdrahtet? Er besitzt das Layout und erstellt eine FormController-Instanz. Er ruft die QField-API niemals direkt auf.
Der View instanziiert den Controller als untergeordnetes Objekt und stellt ihn über einen Alias bereit, damit Unterkomponenten ihn leicht erreichen können:
FormController {
id: formController
plotId: pluginFrame.plotId
onSelect: function(f_uid) { /* ComboBox-Auswahl synchronisieren */ }
}
property var controller: formController
Der View liest FormDataModel.fields einmalig in eine lokale Eigenschaft. Dies ist dasselbe Array, über das der Repeater iteriert:
property var fieldModel: FormDataModel.fields
Die Eintrags-Auswahlliste ist an controller.entryListModel gebunden — ein ListModel, das der Controller aus der Datenbank befüllt. Wenn der Benutzer eine Auswahl trifft, ruft der View controller.loadEntry(f_uid) auf:
ComboBox {
id: entrySelector
model: controller.entryListModel
textRole: "name"
onCurrentIndexChanged: {
if (currentIndex >= 0) {
var f_uid = model.get(currentIndex).f_uid
controller.loadEntry(f_uid)
}
}
}
Anstatt für jedes Feld manuell ein TextField zu schreiben, verwendet der View einen Repeater über fieldModel. Die component FormField-Deklaration ist das Delegate — die Vorlage, die der Repeater für jeden Eintrag im Model-Array instanziiert.
Jedes TextField innerhalb von FormField:
controller.fieldValues[fieldData.name] — eine Property-Bindung, die sich automatisch aktualisiert, wenn der Controller eine Änderung signalisiert.controller.updateField(fieldData.name, text) zurück.component FormField: Rectangle {
property var fieldData
RowLayout {
Text {
text: fieldData.label // aus FormDataModel
}
TextField {
// LESEN: Controller ist die einzige Datenquelle
text: controller.fieldValues[fieldData.name] !== undefined
? controller.fieldValues[fieldData.name] : ""
// SCHREIBEN: jeder Tastendruck aktualisiert den Controller
onTextChanged: {
controller.updateField(fieldData.name, text)
}
}
}
}
Repeater {
model: fieldModel // FormDataModel.fields
delegate: FormField {
fieldData: modelData // ein Feldbeschreibungs-Objekt pro Iteration
}
}
Die drei Aktionsschaltflächen rufen lediglich Controller-Funktionen auf. Der View weiß nicht, wie diese Operationen intern funktionieren:
// Schaltfläche „Neuer Eintrag"
onClicked: { controller.createEntry() }
// Schaltfläche „Eintrag speichern"
onClicked: { controller.saveEntry() }
Der Controller beantwortet: Welche Werte sind gerade in den Feldern gespeichert, und wie werden sie persistiert? Er ist die einzige Komponente, die die QField-API aufruft (LayerUtils, FeatureUtils, layer.startEditing() usw.).
fieldValues ist eine einfache JavaScript-Map. Sie ist die Laufzeit-Datenquelle für jedes Feld im Formular. Wenn ein Feature aus der Datenbank gelesen wird, werden seine Werte in diese Map kopiert. Beim Speichern werden die Werte aus der Map zurückgelesen und in den Layer geschrieben.
property var fieldValues: ({}) // z.B. { "name": "Plot A", "s1": "...", "s2": "..." }
function updateField(fieldName, value) {
fieldValues[fieldName] = value
fieldValuesChanged() // QML-Bindungen im View benachrichtigen
}
Der Aufruf von fieldValuesChanged() ist wichtig: Da fieldValues ein JS-Objekt ist (kein nativer QML-Property-Typ), kann die QML-Binding-Engine Änderungen an seinen Inhalten nicht automatisch erkennen. Das manuelle Auslösen des Change-Signals zwingt die text-Bindungen des View zur erneuten Auswertung.
entryListModel ist ein ListModel. Es wird durch eine Abfrage des entries-Layers befüllt und für jedes Ergebnis eine Zeile angehängt. Die ComboBox im View ist an dieses Model gebunden.
property ListModel entryListModel: ListModel {}
function loadEntries(plotId) {
entryListModel.clear()
var expression = "plot_id = '" + plotId + "'"
let it = LayerUtils.createFeatureIteratorFromExpression(layer, expression)
while (it.hasNext()) {
let feature = it.next()
entryListModel.append({
"name": feature.attribute("name"),
"f_uid": feature.attribute("f_uid")
})
}
it.close() // Iterator IMMER schließen
}
Wenn der Benutzer einen Eintrag auswählt, holt loadEntry() das Feature und kopiert seine Attribute in fieldValues. Dabei wird über FormDataModel.fields iteriert, sodass nur die bekannten Felder geladen werden — unabhängig davon, welche weiteren Spalten die Tabelle enthalten mag:
function loadEntry(f_uid) {
let it = LayerUtils.createFeatureIteratorFromExpression(
layer, "f_uid = '" + f_uid + "'")
let feature = it.next()
it.close()
var map = {}
for (var i = 0; i < FormDataModel.fields.length; i++) {
var name = FormDataModel.fields[i].name
map[name] = feature.attribute(name) || ""
}
fieldValues = map
fieldValuesChanged()
currentFuid = f_uid
}
saveEntry() liest alle Werte aus fieldValues und schreibt sie mit changeAttributeValue in den Layer. Beachten Sie, wie fields.indexOf(key) verwendet wird, um den Attributindex anhand des Namens nachzuschlagen — niemals als hart kodierte Zahl:
function saveEntry() {
let it = LayerUtils.createFeatureIteratorFromExpression(
layer, "f_uid = '" + currentFuid + "'")
let feature = it.next()
it.close()
layer.startEditing()
var fid = feature.id
var fields = feature.fields
for (var key in fieldValues) {
var fieldIndex = fields.indexOf(key)
if (fieldIndex >= 0) {
layer.changeAttributeValue(fid, fieldIndex, fieldValues[key])
}
}
layer.commitChanges()
}
controller.plotId → Controller ruft loadEntries() auf → entryListModel wird befüllt → ComboBox aktualisiert sich automatisch über Bindung.onCurrentIndexChanged → View ruft controller.loadEntry(f_uid) auf → Controller holt Feature und befüllt fieldValues → TextFields aktualisieren sich automatisch über Bindung.onTextChanged → controller.updateField(name, text) → fieldValues wird im Speicher aktualisiert. Noch kein Datenbankschreibzugriff.controller.saveEntry() auf → Controller liest fieldValues, ruft QField-API auf, schreibt Änderungen in den Layer.controller.createEntry() auf → Controller schreibt neues Feature, aktualisiert entryListModel, wählt den neuen Eintrag aus.