SceneBuilderで独自クラスを使うには

提供: tknotebook
移動: 案内検索

メインページ>コンピュータの部屋#JavaFX>JavaFX Tips


いずれ改善されるとは思いますが、現在この記事に執筆時点のSceneBuilder(8.2.0)では JDK付属のクラス以外のクラスを簡単には扱えません。SceneBuilderにクラスを探索するクラスパスを拡張する簡単な方法が用意されていないからです。大変不便です。

かつては FXMLの中でクラスパスを指定する方法があったらしいのですが、現在では不可能です。

何が問題かというと、例えば ChoiceBox や ComboBox レベルの比較的単純なコントロールでさえ、 少々凝ったことをしようとすると、独自のリストセルクラス、独自のセルファクトリクラスやコンバータークラスが 必要になります。

ChoiceBox や ComboBoxのリスト項目として、String型だけではなく、独自の Enum型などの独自クラスのオブジェクトも セットできるようになっているからです。

こうしたことをFXMLで記述することは問題なく可能なのですが、FXML内に JDK以外のクラスが混じると、 SceneBuilder で FXMLファイルが開けなくなってしまいます。これは大変困ります。

この記事では、この問題の対処方法を記述します。

カスタムコンポーネントの追加機能

実は SceneBuilder には利用者が独自に開発した JAR や FXML を登録する機能があります。SceneBuilder の左上の部分にカスタムコンポーネントを表示するエリアがあります。

SceneBuilder Library.png

ここに JAR を登録するには下図のメニューから、JAR/FXML Manager を使います。 メニューを選ぶとファイルの選択ダイアログが現れ、JARを登録することができます。

SceneBuilder Jar FXML MANAGER.png

ここで登録した JAR は

C:\Users\<ユーザ名>\AppData\Roaming\Scene Builder\Library

にコピーされます(Windows版 8.2.0の場合)。

つまりここに SceneBuilder で使いたいクラスが入った JAR を置けばよいわけです。

対処方法

以下の対処方法は Windows の場合です。

まず、JavaFX のアプリをビルドする際、Jarを作るようにします。IntelliJ IDEA では Artifact を追加すればよいですし、Gradleベースのプロジェクトなら最初からJarは作られるようになっているでしょう。

次に、 C:\Users\<ユーザ名>\AppData\Roaming\Scene Builder\Library にその Jar へのシンボリックリンクを置きます。 作成は mklink コマンドがお手軽です。

シンボリックリンクの作成例(IntelliJ IDEA の場合)

cd C:\Users\<ユーザ名>\AppData\Roaming\Scene Builder\Library
mklink Effects.jar C:\javafx_svn_projects\Effects\out\artifacts\Effects_jar\Effects.jar

mklink の実行には管理者権限が必要です。

これで、アプリ独自クラスを含む FXMLファイルが SceneBuilder で開けるようになります。

様々なアプリの Jar が一つのフォルダに混在することになりますが、パッケージ名をきちんと管理していれば クラスが衝突して混乱することはないはずです。

かなり面倒ではありますが、編集できなくなるよりはずっとましです。シンボリックリンクを GUI で簡単に作れる ツールやシェルエクステンションを導入しておくと少しは楽ができると思います。

対処方法 追記

Windows版 8.2.0 では、 C:\Users\<ユーザ名>\AppData\Local\SceneBuilder\app\SceneBuilder.cfg に設定ファイルがあり、クラスパスを設定できます。ここに使いたいクラスのJARのパスを設定すれば 同じことができます。残念ながらクラスファイルのフォルダは指定しても効果ないようです。

いずれにしても、FXMLの中でクラスパスを指定したり、起動オプションでクラスパスを起動できればよいのですが、 未だ発見できてません。

[Application]
app.name=SceneBuilder
app.mainjar=dist.jar
app.version=8.2.0
app.preferences.id=com/oracle/javafx/scenebuilder/app
app.mainclass=com/oracle/javafx/scenebuilder/app/SceneBuilderApp
app.classpath= ←ここにクラスパスを追加する
app.runtime=$APPDIR\runtime
app.identifier=com.oracle.javafx.scenebuilder.app 

[JVMOptions]

[JVMUserOptions]

[ArgOptions]

対処方法 追記2

以下のような簡単なアプリを経由して SceneBuilderを起動すると、FXMLの中に SceneBuilderが利用する クラスパスを記述できるようになります。

このプログラムは FXML内の処理命令 classpath を読み込んでクラスパスを取得し、scenebuilder.cfg の app.classpathの記述を一時的に書き換えて SceneBuilder を起動します。

SceneBuilderが終了すると、scenebuilder.cfgを元に戻します。


package info.nakamuri.app.pub.sclauncher;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
import java.util.Properties;

/***
 * メインクラス
 */
public class Main {
    /**
     * ロガー
     */
    private static final Logger logger = LoggerFactory.getLogger(Main.class);

    /**
     * メイン処理
     * @param args コマンドライン引数
     */
    public void execute(String[] args) {
        try {
            // メイン処理

            // システムパラメータ取得
            Properties props = new Properties();
            props.loadFromXML(Main.class.getClassLoader().getResourceAsStream("systemconfig.xml")); 

            String scpath = getScPath(props);
            logger.debug("scpath = " + scpath);
            String scconfig = getScConfig(props);
            logger.debug("scconfig = " + scconfig);

            // コマンドラインパラメータ取得
            if (args.length != 1) {
                logger.debug("args.lenghth = " + args.length);
                logger.error("usage: sclauncher FXMLファイル");
                System.exit(-1);
            }

            // FXMLファイルよりクラスパスを取得
            String classPath = getClassPath(args[0]);

            File scConfigFile = new File(scconfig);
            String backupName = scConfigFile.getName() + ".backup";
            File backupConfig = new File(scConfigFile.getParent(), backupName);
            logger.debug("backupConfig = " +backupConfig.getAbsolutePath());

            if (classPath != null) {
                // 構成ファイルをバックアップして書き換え
                scConfigFile.renameTo(backupConfig);

                String newline = System.getProperty("line.separator");
                BufferedReader br = new BufferedReader(new FileReader(backupConfig));
                try {
                    FileWriter fw = new FileWriter(scConfigFile);
                    try {
                        String line = br.readLine();
                        while (line != null) {
                            // クラスパスの行を書き換える。
                            if (line.trim().startsWith("app.classpath=")) {
                                line = "app.classpath=" + classPath;
                            }
                            fw.write(line + newline);
                            line = br.readLine();
                        }
                    } finally {
                        fw.close();
                    }

                } finally {
                    br.close();
                }
            }
            try {
                // SceneBuilderの実行。作業用フォルダが FXMLファイルのある場所
                ProcessBuilder pb = new ProcessBuilder(scpath, args[0]);
                Process process = pb.directory(new File(args[0]).getParentFile()).start();
                // 終了まで待つ
                process.waitFor();
            } finally {
                if (classPath != null) {
                    // 構成ファイルを元に戻す
                    scConfigFile.delete();
                    backupConfig.renameTo(scConfigFile);
                }
            }

        } catch (Exception ex) {
            logger.error(ex.getMessage(), ex);
            System.exit(-1);
        }
    }

    /**
     * プロパティから SceneBuilderのパスを取得する。
     * @param props プロパティ
     * @return SceneBuilderの実行ファイルのパス
     * @throws IOException エラー時
     */
    public String getScPath(Properties props) throws IOException {
        String scpath = props.getProperty("scenebuilder.path");
        if (scpath == null) {
            logger.error("systemconfig.xml.save に " +
                    "scenebuilder.path(SceneBuilderの実行パス) " +
                    "が設定されていません。");
            System.exit(-1);
        }
        return scpath;
    } 

    /**
     * プロパティからSceneBuilderの構成ファイルのパスを取得する。
     * @param props プロパティ
     * @return SceneBuilderの構成ファイルのパス
     */
    public String getScConfig(Properties props) {
        String scconfig = props.getProperty("scenebuilder.cfgfile");
        if (scconfig == null) {
            logger.error("systemconfig.xml.save に " +
                    "scenebuilder.cfgfile(SceneBuilderの構成ファイルのパス) が" +
                    "設定されていません。");
            System.exit(-1);
        }
        return scconfig;
    }

    /**
     * FXMLファイルからクラスパスを取得.
     *
     * クラスパスは 絶対、または FXMLファイルに対する相対で指定すること。
     * クラスファイルフォルダは使用不能。jarファイルを指定すること。
     *
     * @param fxml FXMLのファイルパス
     * @return クラスパス
     * @throws SAXException エラー時
     * @throws IOException エラー時
     * @throws ParserConfigurationException エラー時
     */
    public String getClassPath(String fxml) throws SAXException, IOException, ParserConfigurationException {
        // FXMLをパース
        Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new File(fxml)); 

        //最上位ノードの子ノードを探索
        Node child = document.getFirstChild();
        while (child != null) {
            logger.debug("Node Type = " + child.getNodeType());
            logger.debug("Node Name = " + child.getNodeName());
            logger.debug("Node Value = " + child.getNodeValue());

            if ((child.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) &&
                    ("classpath".equals(child.getNodeName()))) {
                // 最初の classpath 処理命令 を取得。後続は無視
                return child.getNodeValue();
            }
            child = child.getNextSibling();
        }
        return null;
    }

    /**
     * メイン関数
     *
     * @param args コマンドライン引数
     */
    public static void main(String[] args) {
        logger.info("アプリ開始");
        Main main = new Main();
        main.execute(args);
        logger.info("アプリ終了");
    }
}

使い方。

このプログラムのクラスパス内に systemconfig.xml というファイルを置き、SceneBuilderのパスと SceneBuilder.cfg のパスを設定します。

記述例

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">


<properties>
    <comment>アプリケーションのプロパティ</comment>
    <entry key="scenebuilder.path">C:\Users\nakamuri\AppData\Local\SceneBuilder\SceneBuilder.exe</entry>
    <entry key="scenebuilder.cfgfile">C:\Users\nakamuri\AppData\Local\SceneBuilder\app\SceneBuilder.cfg</entry>
</properties>

開く FXML ファイルには

<?classpath XXXX/YYYY?>

というようなクラスパスの記述を追加しておきます。残念ながらクラスフォルダーは指定できません。JARのパスのみ指定できます。