JavaFX

Java用のモダンなデスクトップアプリケーションフレームワーク。リッチなUI、2D/3Dグラフィックス、メディア再生機能を提供。FXMLによるMVCアーキテクチャとCSS スタイリングをサポート。Swingの後継として位置付けられる。

デスクトップJavaGUIクロスプラットフォームScene GraphFXML

GitHub概要

openjdk/jfx

JavaFX mainline development

スター3,018
ウォッチ88
フォーク525
作成日:2019年5月6日
言語:C++
ライセンス:GNU General Public License v2.0

トピックス

javajavafxopenjdkopenjfx

スター履歴

openjdk/jfx Star History
データ取得日時: 2025/8/13 01:43

フレームワーク

JavaFX

概要

JavaFXは、Javaプラットフォーム上でリッチなデスクトップアプリケーションを開発するための現代的なGUIツールキットです。OpenJDKプロジェクトの一部として開発されており、高性能なグラフィックス、メディア処理、Webコンテンツの埋め込み、豊富なUIコントロールを提供します。

詳細

JavaFXは2025年現在、OpenJDK 21をベースとした安定したGUIフレームワークとして提供されています。SwingやAWTの後継として位置づけられ、現代的なデスクトップアプリケーション開発に必要な機能を包括的に提供しています。

JavaFXの主要な特徴:

  • Scene Graph Architecture: 階層的なシーングラフによる効率的なUI管理
  • CSS Styling: Web標準のCSSによるスタイリング機能
  • FXML: XMLベースの宣言的UI定義
  • Modern Controls: リッチなUIコントロールとチャート
  • Hardware Acceleration: GPUアクセラレーションによる高速描画
  • Media Support: 音声・動画の再生とメディア処理

2025年現在、多くの企業レベルアプリケーションやツールがJavaFXで開発されており、Javaエコシステムにおける標準的なデスクトップUIフレームワークとしての地位を確立しています。

メリット・デメリット

メリット

  • 強力なJavaエコシステム: Javaの豊富なライブラリとツールを活用
  • クロスプラットフォーム: Windows、macOS、Linuxで動作
  • 宣言的UI: FXMLによる保守性の高いUI設計
  • CSSスタイリング: Web開発者に馴染みのあるスタイリング手法
  • 高性能グラフィックス: GPU加速による滑らかなアニメーション
  • 豊富なコントロール: チャート、表、メディアプレーヤーなど多彩なUI部品
  • Scene Builder: ビジュアルなUIデザインツール
  • 企業サポート: OpenJDKプロジェクトとして長期サポート

デメリット

  • 起動時間: JVMの起動によるアプリケーション開始の遅延
  • メモリ使用量: ネイティブアプリと比較して大きなメモリフットプリント
  • 配布サイズ: JREを含んだ配布パッケージの大きさ
  • モダンUI: 最新のUIデザイントレンドへの対応の遅れ
  • 学習コスト: Scene Graph、FXML、CSSの理解が必要
  • プラットフォーム固有機能: OS固有の機能へのアクセスが制限される

主要リンク

書き方の例

基本的なJavaFXアプリケーション

// HelloWorldApp.java
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class HelloWorldApp extends Application {
    
    @Override
    public void start(Stage primaryStage) {
        // UIコンポーネントの作成
        Label label = new Label("Hello, JavaFX World!");
        
        // レイアウトコンテナの作成
        StackPane root = new StackPane();
        root.getChildren().add(label);
        
        // シーンの作成
        Scene scene = new Scene(root, 400, 300);
        
        // ステージの設定
        primaryStage.setTitle("JavaFX Hello World");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

FXML を使用したMVCアーキテクチャ

<!-- MainView.fxml -->
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox xmlns="http://javafx.com/javafx/11.0.1" 
      xmlns:fx="http://javafx.com/fxml/1" 
      fx:controller="com.example.MainController">
   
   <children>
      <HBox spacing="10" alignment="CENTER">
         <children>
            <Label text="名前:" />
            <TextField fx:id="nameField" promptText="名前を入力してください" />
            <Button fx:id="greetButton" text="挨拶" onAction="#handleGreetAction" />
         </children>
      </HBox>
      
      <TextArea fx:id="outputArea" prefRowCount="10" editable="false" />
      
      <HBox spacing="10" alignment="CENTER">
         <children>
            <Button text="クリア" onAction="#handleClearAction" />
            <Button text="終了" onAction="#handleExitAction" />
         </children>
      </HBox>
   </children>
   
</VBox>
// MainController.java
package com.example;

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.application.Platform;
import java.net.URL;
import java.util.ResourceBundle;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class MainController implements Initializable {
    
    @FXML private TextField nameField;
    @FXML private Button greetButton;
    @FXML private TextArea outputArea;
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // 初期化処理
        outputArea.appendText("アプリケーションが開始されました。\n");
        
        // テキストフィールドが空の時はボタンを無効化
        greetButton.disableProperty().bind(
            nameField.textProperty().isEmpty()
        );
    }
    
    @FXML
    private void handleGreetAction() {
        String name = nameField.getText().trim();
        if (!name.isEmpty()) {
            String timestamp = LocalDateTime.now()
                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            outputArea.appendText(String.format("[%s] こんにちは、%sさん!\n", 
                timestamp, name));
            nameField.clear();
        }
    }
    
    @FXML
    private void handleClearAction() {
        outputArea.clear();
        outputArea.appendText("ログがクリアされました。\n");
    }
    
    @FXML
    private void handleExitAction() {
        Platform.exit();
    }
}
// MainApp.java
package com.example;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class MainApp extends Application {
    
    @Override
    public void start(Stage primaryStage) throws Exception {
        FXMLLoader loader = new FXMLLoader(
            getClass().getResource("/fxml/MainView.fxml")
        );
        
        Scene scene = new Scene(loader.load(), 600, 400);
        
        // CSSスタイルシートの適用
        scene.getStylesheets().add(
            getClass().getResource("/css/application.css").toExternalForm()
        );
        
        primaryStage.setTitle("JavaFX FXML アプリケーション");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

高度なUIコンポーネントの実装

// DataManagementApp.java
package com.example.advanced;

import javafx.application.Application;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.*;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.*;
import javafx.stage.Stage;

public class DataManagementApp extends Application {
    
    private TableView<Person> tableView;
    private ObservableList<Person> data;
    private PieChart pieChart;
    
    @Override
    public void start(Stage primaryStage) {
        // メインレイアウト
        BorderPane root = new BorderPane();
        root.setPadding(new Insets(10));
        
        // 上部:フォーム
        VBox topSection = createFormSection();
        
        // 中央:テーブル
        VBox centerSection = createTableSection();
        
        // 右側:チャート
        VBox rightSection = createChartSection();
        
        root.setTop(topSection);
        root.setCenter(centerSection);
        root.setRight(rightSection);
        
        Scene scene = new Scene(root, 1000, 600);
        scene.getStylesheets().add(getClass().getResource("/css/advanced.css").toExternalForm());
        
        primaryStage.setTitle("高度なJavaFXアプリケーション");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    
    private VBox createFormSection() {
        VBox formBox = new VBox(10);
        formBox.setPadding(new Insets(10));
        
        HBox inputBox = new HBox(10);
        
        TextField nameField = new TextField();
        nameField.setPromptText("名前");
        
        Spinner<Integer> ageSpinner = new Spinner<>(1, 100, 25);
        ageSpinner.setEditable(true);
        
        ComboBox<String> cityCombo = new ComboBox<>();
        cityCombo.setItems(FXCollections.observableArrayList(
            "東京", "大阪", "名古屋", "福岡", "札幌", "仙台"
        ));
        cityCombo.setPromptText("都市を選択");
        
        Button addButton = new Button("追加");
        addButton.setOnAction(e -> {
            if (!nameField.getText().trim().isEmpty() && 
                cityCombo.getValue() != null) {
                
                Person person = new Person(
                    nameField.getText().trim(),
                    ageSpinner.getValue(),
                    cityCombo.getValue()
                );
                
                data.add(person);
                updateChart();
                
                // フォームをクリア
                nameField.clear();
                ageSpinner.getValueFactory().setValue(25);
                cityCombo.setValue(null);
            } else {
                showAlert("エラー", "すべてのフィールドを入力してください。");
            }
        });
        
        inputBox.getChildren().addAll(
            new Label("名前:"), nameField,
            new Label("年齢:"), ageSpinner,
            new Label("都市:"), cityCombo,
            addButton
        );
        
        formBox.getChildren().add(inputBox);
        return formBox;
    }
    
    private VBox createTableSection() {
        VBox tableBox = new VBox(10);
        tableBox.setPadding(new Insets(10));
        
        Label tableLabel = new Label("人員データ");
        tableLabel.setStyle("-fx-font-size: 16px; -fx-font-weight: bold;");
        
        tableView = new TableView<>();
        data = FXCollections.observableArrayList();
        tableView.setItems(data);
        
        // テーブル列の定義
        TableColumn<Person, String> nameCol = new TableColumn<>("名前");
        nameCol.setCellValueFactory(new PropertyValueFactory<>("name"));
        nameCol.setMinWidth(150);
        
        TableColumn<Person, Integer> ageCol = new TableColumn<>("年齢");
        ageCol.setCellValueFactory(new PropertyValueFactory<>("age"));
        ageCol.setMinWidth(80);
        
        TableColumn<Person, String> cityCol = new TableColumn<>("都市");
        cityCol.setCellValueFactory(new PropertyValueFactory<>("city"));
        cityCol.setMinWidth(120);
        
        // アクション列
        TableColumn<Person, Void> actionCol = new TableColumn<>("操作");
        actionCol.setCellFactory(param -> new TableCell<Person, Void>() {
            private final Button deleteBtn = new Button("削除");
            
            {
                deleteBtn.setOnAction(event -> {
                    Person person = getTableView().getItems().get(getIndex());
                    data.remove(person);
                    updateChart();
                });
                deleteBtn.setStyle("-fx-background-color: #ff6b6b; -fx-text-fill: white;");
            }
            
            @Override
            public void updateItem(Void item, boolean empty) {
                super.updateItem(item, empty);
                if (empty) {
                    setGraphic(null);
                } else {
                    setGraphic(deleteBtn);
                }
            }
        });
        actionCol.setMinWidth(80);
        
        tableView.getColumns().addAll(nameCol, ageCol, cityCol, actionCol);
        
        // 検索機能
        TextField searchField = new TextField();
        searchField.setPromptText("検索...");
        searchField.textProperty().addListener((observable, oldValue, newValue) -> {
            filterTable(newValue);
        });
        
        tableBox.getChildren().addAll(tableLabel, searchField, tableView);
        return tableBox;
    }
    
    private VBox createChartSection() {
        VBox chartBox = new VBox(10);
        chartBox.setPadding(new Insets(10));
        chartBox.setPrefWidth(300);
        
        Label chartLabel = new Label("都市別分布");
        chartLabel.setStyle("-fx-font-size: 16px; -fx-font-weight: bold;");
        
        pieChart = new PieChart();
        pieChart.setTitle("都市別人員分布");
        
        chartBox.getChildren().addAll(chartLabel, pieChart);
        return chartBox;
    }
    
    private void updateChart() {
        // 都市別の人数をカウント
        java.util.Map<String, Long> cityCount = data.stream()
            .collect(java.util.stream.Collectors.groupingBy(
                Person::getCity,
                java.util.stream.Collectors.counting()
            ));
        
        // チャートデータを更新
        ObservableList<PieChart.Data> chartData = FXCollections.observableArrayList();
        cityCount.forEach((city, count) -> {
            chartData.add(new PieChart.Data(city, count));
        });
        
        pieChart.setData(chartData);
    }
    
    private void filterTable(String searchText) {
        if (searchText == null || searchText.isEmpty()) {
            tableView.setItems(data);
        } else {
            ObservableList<Person> filteredData = data.filtered(person ->
                person.getName().toLowerCase().contains(searchText.toLowerCase()) ||
                person.getCity().toLowerCase().contains(searchText.toLowerCase())
            );
            tableView.setItems(filteredData);
        }
    }
    
    private void showAlert(String title, String message) {
        Alert alert = new Alert(Alert.AlertType.ERROR);
        alert.setTitle(title);
        alert.setHeaderText(null);
        alert.setContentText(message);
        alert.showAndWait();
    }
    
    public static void main(String[] args) {
        launch(args);
    }
    
    // Personクラス
    public static class Person {
        private final SimpleStringProperty name;
        private final SimpleIntegerProperty age;
        private final SimpleStringProperty city;
        
        public Person(String name, int age, String city) {
            this.name = new SimpleStringProperty(name);
            this.age = new SimpleIntegerProperty(age);
            this.city = new SimpleStringProperty(city);
        }
        
        public String getName() { return name.get(); }
        public void setName(String value) { name.set(value); }
        public SimpleStringProperty nameProperty() { return name; }
        
        public int getAge() { return age.get(); }
        public void setAge(int value) { age.set(value); }
        public SimpleIntegerProperty ageProperty() { return age; }
        
        public String getCity() { return city.get(); }
        public void setCity(String value) { city.set(value); }
        public SimpleStringProperty cityProperty() { return city; }
    }
}

CSSスタイリング

/* application.css */
.root {
    -fx-background-color: #f5f5f5;
    -fx-font-family: "Yu Gothic UI", "Meiryo UI", sans-serif;
}

.button {
    -fx-background-color: #3498db;
    -fx-text-fill: white;
    -fx-background-radius: 5px;
    -fx-padding: 8px 16px;
    -fx-font-size: 14px;
}

.button:hover {
    -fx-background-color: #2980b9;
}

.button:pressed {
    -fx-background-color: #21618c;
}

.text-field {
    -fx-background-color: white;
    -fx-border-color: #bdc3c7;
    -fx-border-radius: 3px;
    -fx-padding: 5px;
}

.text-field:focused {
    -fx-border-color: #3498db;
    -fx-border-width: 2px;
}

.table-view {
    -fx-background-color: white;
    -fx-table-cell-border-color: transparent;
}

.table-view .column-header {
    -fx-background-color: #ecf0f1;
    -fx-font-weight: bold;
}

.table-row-cell:selected {
    -fx-background-color: #3498db;
    -fx-text-fill: white;
}

.table-row-cell:hover {
    -fx-background-color: #ebf3fd;
}

.chart-pie {
    -fx-border-color: #95a5a6;
    -fx-border-width: 1px;
}

.chart-title {
    -fx-font-size: 16px;
    -fx-font-weight: bold;
}

カスタムコントロールの作成

// CustomSlider.java
package com.example.custom;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.layout.VBox;

public class CustomSlider extends VBox {
    
    private final DoubleProperty value = new SimpleDoubleProperty();
    private final Label titleLabel;
    private final Label valueLabel;
    private final Slider slider;
    
    public CustomSlider(String title, double min, double max, double initial) {
        this.setPadding(new Insets(10));
        this.setSpacing(5);
        
        // タイトルラベル
        titleLabel = new Label(title);
        titleLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 14px;");
        
        // 値表示ラベル
        valueLabel = new Label(String.format("%.2f", initial));
        valueLabel.setStyle("-fx-font-size: 12px; -fx-text-fill: #666;");
        
        // スライダー
        slider = new Slider(min, max, initial);
        slider.setShowTickLabels(true);
        slider.setShowTickMarks(true);
        slider.setMajorTickUnit((max - min) / 4);
        
        // バインディング設定
        value.bind(slider.valueProperty());
        valueLabel.textProperty().bind(
            slider.valueProperty().asString("%.2f")
        );
        
        this.getChildren().addAll(titleLabel, valueLabel, slider);
        
        // スタイル設定
        this.setStyle("-fx-background-color: white; " +
                     "-fx-border-color: #ddd; " +
                     "-fx-border-radius: 5px; " +
                     "-fx-background-radius: 5px;");
    }
    
    public double getValue() {
        return value.get();
    }
    
    public void setValue(double value) {
        slider.setValue(value);
    }
    
    public DoubleProperty valueProperty() {
        return value;
    }
    
    public Slider getSlider() {
        return slider;
    }
}

アニメーションとトランジション

// AnimationDemo.java
package com.example.animation;

import javafx.animation.*;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import javafx.util.Duration;

public class AnimationDemo extends Application {
    
    private Circle animatedCircle;
    private Rectangle animatedRectangle;
    private Timeline timeline;
    
    @Override
    public void start(Stage primaryStage) {
        BorderPane root = new BorderPane();
        root.setPadding(new Insets(20));
        
        // アニメーション対象のシェイプ
        Pane animationPane = createAnimationPane();
        
        // コントロールパネル
        VBox controlPanel = createControlPanel();
        
        root.setCenter(animationPane);
        root.setBottom(controlPanel);
        
        Scene scene = new Scene(root, 800, 600);
        
        primaryStage.setTitle("JavaFX アニメーションデモ");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    
    private Pane createAnimationPane() {
        Pane pane = new Pane();
        pane.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc;");
        pane.setPrefSize(700, 400);
        
        // アニメーション用の円
        animatedCircle = new Circle(50, 50, 30);
        animatedCircle.setFill(Color.BLUE);
        
        // アニメーション用の矩形
        animatedRectangle = new Rectangle(100, 100, 60, 40);
        animatedRectangle.setFill(Color.RED);
        
        pane.getChildren().addAll(animatedCircle, animatedRectangle);
        
        return pane;
    }
    
    private VBox createControlPanel() {
        VBox controlBox = new VBox(10);
        controlBox.setPadding(new Insets(10));
        
        HBox buttonBox = new HBox(10);
        
        Button moveButton = new Button("移動アニメーション");
        moveButton.setOnAction(e -> startMoveAnimation());
        
        Button rotateButton = new Button("回転アニメーション");
        rotateButton.setOnAction(e -> startRotateAnimation());
        
        Button scaleButton = new Button("スケールアニメーション");
        scaleButton.setOnAction(e -> startScaleAnimation());
        
        Button colorButton = new Button("色変更アニメーション");
        colorButton.setOnAction(e -> startColorAnimation());
        
        Button complexButton = new Button("複合アニメーション");
        complexButton.setOnAction(e -> startComplexAnimation());
        
        Button stopButton = new Button("停止");
        stopButton.setOnAction(e -> stopAnimation());
        
        buttonBox.getChildren().addAll(
            moveButton, rotateButton, scaleButton, 
            colorButton, complexButton, stopButton
        );
        
        controlBox.getChildren().add(buttonBox);
        
        return controlBox;
    }
    
    private void startMoveAnimation() {
        stopAnimation();
        
        TranslateTransition moveTransition = new TranslateTransition(Duration.seconds(2), animatedCircle);
        moveTransition.setFromX(0);
        moveTransition.setToX(300);
        moveTransition.setFromY(0);
        moveTransition.setToY(200);
        moveTransition.setCycleCount(Timeline.INDEFINITE);
        moveTransition.setAutoReverse(true);
        
        moveTransition.play();
    }
    
    private void startRotateAnimation() {
        stopAnimation();
        
        RotateTransition rotateTransition = new RotateTransition(Duration.seconds(3), animatedRectangle);
        rotateTransition.setFromAngle(0);
        rotateTransition.setToAngle(360);
        rotateTransition.setCycleCount(Timeline.INDEFINITE);
        
        rotateTransition.play();
    }
    
    private void startScaleAnimation() {
        stopAnimation();
        
        ScaleTransition scaleTransition = new ScaleTransition(Duration.seconds(1.5), animatedCircle);
        scaleTransition.setFromX(1.0);
        scaleTransition.setToX(2.0);
        scaleTransition.setFromY(1.0);
        scaleTransition.setToY(2.0);
        scaleTransition.setCycleCount(Timeline.INDEFINITE);
        scaleTransition.setAutoReverse(true);
        
        scaleTransition.play();
    }
    
    private void startColorAnimation() {
        stopAnimation();
        
        timeline = new Timeline();
        
        KeyFrame keyFrame1 = new KeyFrame(Duration.ZERO,
            new KeyValue(animatedRectangle.fillProperty(), Color.RED)
        );
        
        KeyFrame keyFrame2 = new KeyFrame(Duration.seconds(1),
            new KeyValue(animatedRectangle.fillProperty(), Color.GREEN)
        );
        
        KeyFrame keyFrame3 = new KeyFrame(Duration.seconds(2),
            new KeyValue(animatedRectangle.fillProperty(), Color.BLUE)
        );
        
        KeyFrame keyFrame4 = new KeyFrame(Duration.seconds(3),
            new KeyValue(animatedRectangle.fillProperty(), Color.RED)
        );
        
        timeline.getKeyFrames().addAll(keyFrame1, keyFrame2, keyFrame3, keyFrame4);
        timeline.setCycleCount(Timeline.INDEFINITE);
        
        timeline.play();
    }
    
    private void startComplexAnimation() {
        stopAnimation();
        
        // 複数のアニメーションを組み合わせ
        TranslateTransition moveTransition = new TranslateTransition(Duration.seconds(4), animatedCircle);
        moveTransition.setToX(400);
        moveTransition.setToY(250);
        
        RotateTransition rotateTransition = new RotateTransition(Duration.seconds(4), animatedCircle);
        rotateTransition.setToAngle(720);
        
        ScaleTransition scaleTransition = new ScaleTransition(Duration.seconds(4), animatedCircle);
        scaleTransition.setToX(1.5);
        scaleTransition.setToY(1.5);
        
        FadeTransition fadeTransition = new FadeTransition(Duration.seconds(4), animatedCircle);
        fadeTransition.setFromValue(1.0);
        fadeTransition.setToValue(0.3);
        
        ParallelTransition parallelTransition = new ParallelTransition(
            moveTransition, rotateTransition, scaleTransition, fadeTransition
        );
        
        parallelTransition.setCycleCount(Timeline.INDEFINITE);
        parallelTransition.setAutoReverse(true);
        
        parallelTransition.play();
    }
    
    private void stopAnimation() {
        // すべてのトランジションを停止
        animatedCircle.getTransforms().clear();
        animatedRectangle.getTransforms().clear();
        
        if (timeline != null) {
            timeline.stop();
        }
        
        // 元の状態にリセット
        animatedCircle.setTranslateX(0);
        animatedCircle.setTranslateY(0);
        animatedCircle.setScaleX(1.0);
        animatedCircle.setScaleY(1.0);
        animatedCircle.setRotate(0);
        animatedCircle.setOpacity(1.0);
        
        animatedRectangle.setTranslateX(0);
        animatedRectangle.setTranslateY(0);
        animatedRectangle.setScaleX(1.0);
        animatedRectangle.setScaleY(1.0);
        animatedRectangle.setRotate(0);
        animatedRectangle.setFill(Color.RED);
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

Module システムとプロジェクト設定

// module-info.java
module com.example.javafxapp {
    requires javafx.controls;
    requires javafx.fxml;
    requires javafx.web;
    requires javafx.media;
    
    // FXMLController用にパッケージを開放
    opens com.example to javafx.fxml;
    opens com.example.controllers to javafx.fxml;
    
    // メインクラスをエクスポート
    exports com.example;
}
<!-- pom.xml (Maven) -->
<?xml version="1.0" encoding="UTF-8"?>
<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 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>javafx-app</artifactId>
    <version>1.0-SNAPSHOT</version>
    
    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <javafx.version>21.0.1</javafx.version>
        <javafx.maven.plugin.version>0.0.8</javafx.maven.plugin.version>
    </properties>
    
    <dependencies>
        <!-- JavaFX Controls -->
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>${javafx.version}</version>
        </dependency>
        
        <!-- JavaFX FXML -->
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-fxml</artifactId>
            <version>${javafx.version}</version>
        </dependency>
        
        <!-- JavaFX Web (オプション) -->
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-web</artifactId>
            <version>${javafx.version}</version>
        </dependency>
        
        <!-- JavaFX Media (オプション) -->
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-media</artifactId>
            <version>${javafx.version}</version>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <!-- Maven Compiler Plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>21</source>
                    <target>21</target>
                </configuration>
            </plugin>
            
            <!-- JavaFX Maven Plugin -->
            <plugin>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-maven-plugin</artifactId>
                <version>${javafx.maven.plugin.version}</version>
                <configuration>
                    <mainClass>com.example.MainApp</mainClass>
                </configuration>
            </plugin>
            
            <!-- JLink Plugin for creating custom runtime images -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jlink-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <addModules>java.base,javafx.controls,javafx.fxml</addModules>
                    <launcher>app=com.example.javafxapp/com.example.MainApp</launcher>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

ビルドとデプロイメント

# Maven を使用したプロジェクトの実行
mvn clean compile
mvn javafx:run

# JARファイルの作成
mvn clean package

# カスタムランタイムイメージの作成
mvn clean javafx:jlink

# 実行可能JARファイルの実行 (JavaFXが含まれている場合)
java -jar target/javafx-app-1.0-SNAPSHOT.jar

# モジュールパスを指定した実行
java --module-path "lib" --add-modules javafx.controls,javafx.fxml -cp target/classes com.example.MainApp

# JPackage を使用したネイティブパッケージの作成 (JDK 17+)
jpackage --input target/classes \
         --name "JavaFX App" \
         --main-jar javafx-app-1.0-SNAPSHOT.jar \
         --main-class com.example.MainApp \
         --type dmg \
         --java-options '--enable-preview'

プラットフォーム別の特別な考慮事項

Windows

  • Windows 10以降を推奨
  • JDK 21 LTS の使用を推奨
  • ダークモードサポートの考慮
  • MSIインストーラーでの配布が可能

macOS

  • macOS 10.15 (Catalina) 以降をサポート
  • Apple Siliconネイティブサポート
  • Notarization対応が必要(App Store配布時)
  • DMGファイルでの配布が一般的

Linux

  • OpenJDK + OpenJFXの組み合わせを推奨
  • GTK依存関係の管理が必要
  • AppImageやSnapパッケージでの配布
  • 複数のLinuxディストリビューションでのテスト推奨