Files
NeitzelLib/fx/docs/quick-start.md
T

204 lines
9.1 KiB
Markdown

# Your first JavaFX application
This walkthrough creates a complete Maven project with one editable name and a label that follows it. It uses Java 21, JavaFX 21.0.7 and the current `1.0-SNAPSHOT` NeitzelLib sources. These are local development artifacts; the instructions do not assume that this snapshot is published in Maven Central.
## Prepare the library
Install a JDK 21. Use the library's Maven wrapper (3.9.11), or a separate Maven installation version 3.8.6 or newer. Check `java -version` and `./mvnw -version` before building; on macOS, select the JDK with `export JAVA_HOME=$(/usr/libexec/java_home -v 21)` if necessary.
```bash
git clone https://gitea.neitzel.de/konrad/NeitzelLib.git
cd NeitzelLib
./mvnw -pl fx-example -am -Dmaven.test.failure.ignore=false install
./mvnw -f fx-example/pom.xml javafx:run
```
The `install` command installs the parent POM, `core`, `injection` and `fx` into your local Maven repository and builds the example. The repository hardcodes Surefire failure-ignore, so Maven success alone does not establish passing tests, even with the explicit flag. Inspect the XML reports as shown below. The `javafx:run` command opens the [person desk example](../../fx-example/README.md). Run `javafx:run` only on the example POM, not with `-am`: the reactor parent is not a JavaFX application.
Check test results after the build (requires Python 3):
```bash
python3 - <<'PYTHON'
from pathlib import Path
import xml.etree.ElementTree as ET
reports = list(Path('.').glob('*/target/surefire-reports/TEST-*.xml'))
assert reports, 'No test reports found'
failures = []
for report in reports:
suite = ET.parse(report).getroot()
if int(suite.get('failures', 0)) or int(suite.get('errors', 0)):
failures.append(str(report))
assert not failures, 'Failed test reports: ' + ', '.join(failures)
print(f'{len(reports)} test suites passed')
PYTHON
```
Use a clean build when qualifying a change so stale reports cannot stand in for tests that did not run.
## Create the application
Create a separate directory `hello-fx` with these files. This is a classpath application; it deliberately has no `module-info.java`.
```text
hello-fx/
pom.xml
src/main/java/example/Main.java
src/main/java/example/HelloApp.java
src/main/java/example/Person.java
src/main/resources/example/Hello.fxml
```
`pom.xml`:
```xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>hello-fx</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>de.neitzel.lib</groupId>
<artifactId>fx</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.17</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration><mainClass>example.Main</mainClass></configuration>
</plugin>
</plugins>
</build>
</project>
```
`src/main/java/example/Person.java`:
```java
package example;
public final class Person {
private String name = "Ada";
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
```
`src/main/java/example/Main.java`:
```java
package example;
import javafx.application.Application;
public final class Main {
public static void main(String[] args) {
Application.launch(HelloApp.class, args);
}
}
```
`src/main/java/example/HelloApp.java`:
```java
package example;
import de.neitzel.fx.view.LoadedView;
import de.neitzel.fx.view.ViewLoader;
import de.neitzel.injection.ApplicationContext;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public final class HelloApp extends Application {
@Override
public void start(Stage stage) throws Exception {
Person person = new Person();
ApplicationContext context = new ApplicationContext("example");
LoadedView view = ViewLoader.load(
HelloApp.class.getResource("Hello.fxml"), person, context);
stage.setScene(new Scene(view.getRoot()));
stage.setTitle("Hello NeitzelLib");
stage.show();
}
}
```
`src/main/resources/example/Hello.fxml`:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import de.neitzel.fx.view.BindingList?>
<?import de.neitzel.fx.view.DirectBinding?>
<VBox xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"
spacing="12" prefWidth="360" prefHeight="120">
<fx:define>
<BindingList>
<DirectBinding control="name" property="text" path="name" direction="both"/>
<DirectBinding control="echo" property="text" path="name" direction="view"/>
</BindingList>
</fx:define>
<TextField fx:id="name"/>
<Label fx:id="echo"/>
</VBox>
```
From `hello-fx`, run:
```bash
mvn clean compile javafx:run
```
Without a separate Maven installation, use the absolute path to the cloned library's `mvnw` with `-f /absolute/path/to/hello-fx/pom.xml clean compile javafx:run`.
Type another name: the label follows immediately, and the `Person` setter receives the new value. No controller is required for this form. `ViewLoader` creates a runtime ViewModel that holds the JavaFX properties. Updating `person.setName(...)` directly later does **not** notify the ViewModel; update `((javafx.beans.property.StringProperty) view.property("name")).set("Grace")` on the JavaFX application thread instead.
## Add behavior and reusable forms
Use an injected controller for event handlers or Computed bindings, and a Component for a reusable FXML form. The [person desk](../../fx-example/README.md) demonstrates both, converters, lists, tables and messages. Read the [architecture guide](architecture.md) before introducing nested Models or sharing windows: Model identity, controller lifetime and list notifications matter.
`fx` brings `core`, `injection` and JavaFX base, graphics, controls, FXML and web transitively. You do not need Lombok or a generated ViewModel to use this example. Java ViewModel generation and the Designer are optional tools, not prerequisites for loading an FXML view.
For a named-module application, see the [module and lifecycle notes](architecture.md#modules-and-reflection). Do not simply add `module-info.java` without opening the packages used through reflection.
## Troubleshooting the first launch
| Symptom | Check |
| --- | --- |
| Maven cannot resolve `de.neitzel.lib:fx:1.0-SNAPSHOT` or its parent | Install the library reactor first; use the same Maven local repository for both projects. These instructions use local snapshots. |
| `No plugin found for prefix 'javafx'` | Invoke the example POM directly, or use the complete application POM above. Do not invoke the goal on the library reactor parent. |
| JavaFX runtime/native library failure | Check the JDK version, architecture and JavaFX dependencies actually resolved; avoid mixing native artifacts from different platforms or JavaFX versions. |
| `getResource(...)` returns null | The FXML belongs under `src/main/resources` in the matching package; verify it was copied to `target/classes`. Resource names are case-sensitive. |
| `No component found for type` | Annotate the controller/converter/Component, include it in the context's scan root, and inspect `ComponentScanner.getErrors()` for unresolved dependencies. |
| Binding cannot find control/property/path | Match `fx:id`, a public JavaFX `nameProperty()` target, and a readable/writable Model Attribute. Use `ViewLoader`, not plain `FXMLLoader`. |
| Initial data shows but a bean setter does not update the UI | Update the observable ViewModel property; the bean is not itself observable. |
| A second window fails with an already-parented node | Create a separate context for that window; avoid reusing cached Component roots and controllers. |
| Reflective access fails only on module path | Add the appropriate `opens` declarations and verify registry service declarations, not only dependencies. |
Do not build a single ordinary JAR and expect it to include JavaFX's platform-specific runtime automatically. The Maven run goal handles the development launch; deployment requires the application's dependencies or a separately built runtime image. The person desk's JAR-plus-`libs` packaging is an example of the former, and the Designer has its own bundled runtime packaging.