Components, Listen und Table
Eine Component verbindet eine wiederverwendbare JavaFX-Node mit einem Model und einer eigenen FXML-Datei. Repeat und Table erzeugen daraus mehrere Darstellungen für eine Collection.
Die Klasse muss vom Injection-Kontext erzeugt werden können und Parent erweitern, meist über VBox oder HBox. Neben ihr liegt ClassName.fxml im selben Paket. Diese Datei verwendet fx:root; die Component ist zugleich Root und Controller und darf daher kein zusätzliches fx:controller enthalten.
Die Klasse stellt eine öffentliche modelProperty() bereit, die eine JavaFX-Property liefert. Passende Bean-Getter und -Setter ermöglichen gewöhnliche FXML-Zuweisungen. Eine vereinfachte Form sieht so aus:
package example;
@jakarta.inject.Singleton
public class AddressForm extends javafx.scene.layout.VBox {
private final javafx.beans.property.ObjectProperty<Address> model =
new javafx.beans.property.SimpleObjectProperty<>(this, "model");
public javafx.beans.property.ObjectProperty<Address> modelProperty() { return model; }
public Address getModel() { return model.get(); }
public void setModel(Address value) { model.set(value); }
}
Address ist das eigene Model. Die vollständige FXML-Variante steht in fx-example. Die Eltern-View enthält beispielsweise:
<DirectBinding control="homeForm" property="model" path="home" direction="both"/>
und im Layout ein <AddressForm fx:id="homeForm"/>. Die Deklaration gehört in eine BindingList, das Element in den Scene Graph.
Der Container konstruiert die Component. Ihr eigenes FXML und initialize() laufen vor der Übernahme der Eigenschaften aus dem umgebenden Dokument. Eigene Attribute und geerbte Layout-Eigenschaften wie prefWidth verwenden die normale FXML-Konvertierung. Unbekannte Eigenschaften und ungültige Werte werden abgewiesen. $-Objektreferenzen behalten ihre Identität. Statische Layout-Constraints wie GridPane.rowIndex verarbeitet der äußere FXMLLoader.
Die Model-Listener sind vor der Zuweisung der äußeren Werte installiert. Die eigenen Bindings laufen einmal beim ersten nicht-null Model. Ein späterer Austausch bindet das Formular nicht neu. initialize darf das Model noch nicht voraussetzen. Ein HasViewModel erhält den Callback nach der Model-Zuweisung und vor den Bindings, aber nur für ein tatsächliches ViewModel beziehungsweise dessen Unterklasse.
Pro Ladevorgang verwendet das erste Vorkommen einer Klasse getComponent; weitere Vorkommen verwenden createComponent. Repeat und Table erzeugen ihre Zeilenroots immer neu. Ein weiterer unabhängiger Ladevorgang beginnt wieder beim gecachten Singleton. Ein bereits geparenteter Root kann dadurch nicht gleichzeitig in einer zweiten View verwendet werden. Eigene Kontexte für unabhängige Fenster vermeiden diese Kollision.
AddressForm.fxml enthält NoteForm und bindet dessen Model an note. Die Text-Property des inneren Feldes bindet an Note.text. Eine Bearbeitung verändert den Text des bestehenden Note-Objekts, nicht Address.note selbst. Bei null bleibt das innere Formular ohne angewendete Model-Bindings.
Das Beispiel nutzt im Repeat eine einfachere VisitForm ohne NoteForm. Eine frisch erzeugte äußere Repeat-Component bedeutet nicht automatisch, dass jede verschachtelte Component einen neuen Kontext erhält. Sonst könnte eine innere NoteForm erneut die bereits im Hauptformular verwendete Singleton-Node sein.
<Repeat pane="visitPane" component="de.neitzel.fx.example.view.VisitForm"
collection="visits"/>
pane muss eine Pane mit dieser fx:id sein; component ist der vollständige Klassenname. collection bezeichnet direkt eine Property des aktuellen ViewModels, keinen beliebigen punktierten DirectBinding-Pfad. Der Wert muss eine ObservableList sein. Null oder ein ungeeigneter Wert lässt den Aufbau scheitern.
Für jedes Element entsteht eine neue Component in Listenreihenfolge. Hinzufügen und Entfernen werden nachgeführt; set(index, replacement) wird als Entfernen/Hinzufügen verarbeitet. Permutationen und reine Update-Ereignisse werden ignoriert. Die Pane sollte ausschließlich die von Repeat verwalteten Kinder enthalten, weil Repeat mit Listenindizes arbeitet.
<DirectBinding control="tagList" property="items" path="tags" direction="view"/>
<DirectBinding control="visitChoice" property="items" path="visits" direction="view"/>
<DirectBinding control="visitChoice" property="value" path="selectedVisit" direction="both"/>
<DirectBinding control="visitTable" property="selection" path="selectedVisit" direction="both"/>
Listen von Strings können direkt in die items-Property einer ListView gehen. Die Liste wird geteilt; direction="view" verhindert keine Änderungen an diesem gemeinsamen Objekt. Für eine Auswahl wird ein eigenes komplexes Attribut wie selectedVisit verwendet. So können ComboBox und Table dasselbe Model-Objekt auswählen.
NeitzelLibs Table ist eine VBox-basierte eigene Implementierung, keine JavaFX TableView. Sie hält einen Component-Root pro Model-Identität und ist nicht virtualisiert. Große Datenmengen erfordern deshalb eine eigene Leistungsprüfung.
<Table fx:id="visitTable" collection="visits"
row="de.neitzel.fx.example.view.VisitRow"
prefWidth="496" prefHeight="160">
<Column header="Street" attribute="street" width="300"/>
<Column header="City" attribute="city"/>
<Column header="Visited" attribute="visitedOn" width="120"/>
</Table>
columns ist die Default-Property. collection bezeichnet ein direktes les- und schreibbares Collection-Attribut; row die wiederverwendbare Row-Component. Die ersten direkten Kinder der Row entsprechen den Columns nach Position. Weitere Controls, etwa ein Remove-Button, dürfen folgen. Die Runtime prüft nicht automatisch, ob ein Binding im ersten Feld auch zum ersten Column-Attribut gehört.
Mindestens eine Column, nicht leere Header, vorhandene einfache Attribute und eindeutige Attributnamen sind erforderlich. Komplexe Attribute, char/void und doppelte Columns sind ungeeignet. Unterstützt werden unter anderem Strings, Boolean, Enums, Zahlen und Datum/Zeit-Werte. Die Body-Zeilen scrollen; der feste Header folgt der horizontalen Position.
Ein primärer Klick wählt das Model der Zeile. Ein Klick auf freien Body-Bereich löscht die Auswahl; das Entfernen des ausgewählten Models ebenfalls. selectionProperty, getSelection und setSelection arbeiten mit dem Model, nicht mit der Row-Node.
Eine angegebene Breite muss positiv und endlich sein. Columns ohne Breite teilen den beim ersten Aufbau verbleibenden Platz. Mindestbreite ist die Header-Textbreite plus acht Pixel; ohne brauchbare Gesamtbreite gilt ein Fallback von 80 Pixeln pro Column. Ziehen am Resize-Griff verändert nur diese Column. Eine spätere Änderung der Table-Breite verteilt die Columns nicht neu.
Ein Header-Klick wechselt zwischen aufsteigend und absteigend. Eine andere Column ersetzt die bisherige Sortierung; das Menü kann sie löschen. Null bleibt in beiden Richtungen zuletzt. Strings vergleichen ohne Beachtung der Großschreibung, Zahlen über doubleValue, Boolean und Enums über ihre Comparable-Reihenfolge; LocalDate chronologisch. Gleiche Werte behalten die Reihenfolge der Collection. Die zugrunde liegende Liste wird nicht umsortiert.
Das Filtermenü zeigt unterschiedliche Werte der Zeilen, die alle anderen Column-Filter passieren; der eigene Filter schränkt seine Auswahl nicht ein. Eine Teilmenge aktiviert einen Filter. Alle abgewählten Werte schließen auch künftig auftauchende Werte aus; alle ausgewählten Werte bedeuten einen inaktiven Filter und erlauben neue Werte.
Strings unterstützen Contains, Starts with und Ends with; Zahlen Greater than und Less than; Datum/Zeit Before und After. Boolean und Enums verwenden nur die Werteliste. Sind Operator oder Vergleichswert ausgefüllt, hat der Vergleich Vorrang vor den Checkboxen. Beide Angaben müssen gültig sein; sonst bleibt das Menü offen und der bisherige Filter bestehen.
Ganzzahlen werden als Long, andere Zahlen als Double gelesen. LocalDate, LocalDateTime und Instant nutzen ihr ISO-Parsing; Date und Calendar verwenden Instant-Parsing. 2020-01-02 ist beispielsweise ein LocalDate, 02.01.2020 nicht. Numerische und zeitliche Grenzen sind exklusiv; null passt zu keinem Vergleich. Stringvergleiche verwenden Locale.ROOT ohne Beachtung der Großschreibung.
Das Suchfeld filtert nur die sichtbaren Auswahlwerte, nicht unmittelbar die Zeilen. Erst Confirm übernimmt den Filter. * ist ein gewöhnliches Zeichen. Beim erneuten Öffnen ist die Suche leer. Select all berücksichtigt auch ausgeblendete Checkboxen; eine gemischte Auswahl wird unbestimmt angezeigt.
Änderungen in TextInputControls einer Row lösen Filter-/Sortieraktualisierung aus. Andere Controls tun das derzeit nicht automatisch, auch wenn ihre DirectBindings Werte zurückschreiben.
Die folgenden CSS-Klassen stehen für eigene Styles bereit:
| Bereich | Klassen |
|---|---|
| Table und Scrollbereiche | table, table-scroll, table-header-scroll |
| Header und Zellen | column-header-row, column-header, column-cell |
| Breite und Menüöffnung | column-resize, column-opener |
| Sortierung und Auswahl | sort-mark, selected |
| Filtermenü | column-popup, column-search, column-select-all |
| Vergleich | column-comparison-operator, column-comparison-value |
| Aktiver Filter | filtered auf dem zugehörigen Opener |
selected ist eine Style-Klasse der ausgewählten Row, keine zugesicherte Pseudoklasse. Eigene Styles dürfen die Zuordnung zwischen Column-Positionen und Row-Kindern nicht verändern.