「SceneBuilderで独自クラスを使うには」の版間の差分
(→対処方法 追記) |
(→対処方法 追記2) |
||
88行: | 88行: | ||
==対処方法 追記2 == | ==対処方法 追記2 == | ||
+ | |||
+ | 以下のような簡単なアプリを経由して SceneBuilderを起動すると、FXMLの中に SceneBuilderが利用する | ||
+ | クラスパスを記述できるようになります。 | ||
package info.nakamuri.app.pub.sclauncher; | package info.nakamuri.app.pub.sclauncher; |
2016年7月15日 (金) 12:41時点における版
メインページ>コンピュータの部屋#JavaFX>JavaFX Tips
いずれ改善されるとは思いますが、現在この記事に執筆時点のSceneBuilder(8.2.0)では
JDK付属のクラス以外を簡単には扱えません。SceneBuilderが探索するクラスパスを拡張する方法が用意されていないからです。大変不便です。
かつては クラスパスを指定する方法があったらしいのですが、現在では不可能です。
何が問題かというと、例えば ChoiceBox や ComboBox レベルの比較的単純なコントロールでさえ、 少々凝ったことをしようとすると、独自のリストセルクラス、独自のセルファクトリクラスやコンバータークラスが 必要になります。
ChoiceBox や ComboBoxのリスト項目として、String型だけではなく、独自の Enum型などの独自クラスのオブジェクトを セットできるようになっています。
こうしたことをFXMLで記述することは問題なく可能なのですが、FXML内に JDK以外のクラスが混じると、 SceneBuilder で FXMLファイルが開けなくなってしまいます。これは大変困ります。
この記事では、この問題の対処方法を記述します。
カスタムコンポーネントの追加機能
実は SceneBuilder には利用者が独自に開発した JAR や FXML を登録する機能があります。SceneBuilder の左上の部分にカスタムコンポーネントを表示するエリアがあります。
ここに JAR を登録するには下図のメニューから、JAR/FXML Manager を使います。 メニューを選ぶとファイルの選択ダイアログが現れ、JARを登録することができます。
ここで登録した 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が利用する クラスパスを記述できるようになります。
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("アプリ終了"); } }