JavaFXアプリケーションを特定のファイルタイプに関連付ける [Mac編]

パッケージ製品開発担当の大です。こんにちは。

今年の3月に公開されたJava8 Update40から、Javaパッケージャツールに以下のような機能が追加されました。

  • コマンドライン引数を自己完結型アプリケーションに引き渡せるようになった。また、引数のデフォルト値をパッケージングするときに指定できるようになった。
  • ファイルの関連付けを設定できるようになった(拡張子またはMIME Typeで指定)。
  • UserJvmOptionsService APIが追加された。次回起動時に使用されるJVMオプションをプログラムから指定できるみたい?
  • エントリポイントを複数持てるようになった(Macでは非サポート)。

今回はこの中から2番めの「ファイルの関連付け」をMacで試してみようと思います。

準備

関連付けするファイルを用意します。拡張子は「panda」としました(他のアプリで使われてなさそうならなんでもOK)。現時点では何も関連付けられていないので、ファイルの「情報を見る」の「このアプリケーションで開く」は「<なし>」、アイコンも白紙になっています。

関連付け前

関連付け前
(クリックで拡大)

中身はただのUTF-8のテキストファイルです。

$ cat tekito.panda
ファイルの中身です。

アイコンはイチから作るの面倒くさいので、今回はSafari.appにあるアイコンを借りてきました。svg.icnsをファイルタイプ「panda」のアイコンに、compass.icnsをテストアプリのアイコンにそれぞれ使わせていただきます。

compass.icnsとsvg.icns

compass.icnsとsvg.icns(クリックで拡大)

最新版のNetBeans(現時点で8.0.2)と、Java8(現時点でJava8 Update 45)をインストールして、準備は完了です。

JavaFXプロジェクトの作成

NetBeans上で「ファイル>新規プロジェクト>JavaFX>JavaFXアプリケーション」を選択し、適当な名前でプロジェクトとアプリケーションクラスを作成します。

今回はプロジェクト名を「AssocTest」、アプリケーションクラスを「test.AssocTest」としました。

アプリケーションクラスはこんな感じでテキストを表示するだけのものにします。

AssocTest.java

package test;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class AssocTest extends Application {

     @Override
    public void start(Stage primaryStage) {
        Text text = new Text();
        StackPane root = new StackPane();
        root.getChildren().add(text);

        Scene scene = new Scene(root, 300, 100);

        primaryStage.setTitle("関連付けのテスト");
        primaryStage.setScene(scene);
        primaryStage.show();

        text.setText("Hello, World!");
    }

    public static void main(String[] args) {
        launch(args);
    }
}

実行すると、

実行結果

実行結果(クリックで拡大)

こんな風に表示されました。

インストーラを作成

では、ネイティブパッケージャの機能を使用してインストーラを作成していきます。

NetBeansのプロジェクトのプロパティの「ビルド>デプロイメント」に「ネイティブ・パッケージングの有効化」という項目がありますが、今回やろうとしている「ファイルの関連付け」は、まだ設定画面が用意されていなさそうなので、build.xmlに直接書きます。

以前やったときと同様に、「-post-jfx-deploy」に記述します。

  <target name="-post-jfx-deploy">
    <fx:deploy width="${javafx.run.width}" height="${javafx.run.height}" nativeBundles="all"
               outdir="${basedir}/${dist.dir}" outfile="${ant.project.name}">
      <fx:preferences install="true" />
      <fx:info title="${application.title}" vendor="${application.vendor}" description="${application.desc}">
        <fx:icon href="${basedir}/res/compass.icns"/>
        <fx:association extension="panda PANDA" description="関連付けテスト用" icon="${basedir}/res/svg.icns" />
      </fx:info>
      <fx:permissions elevated="true"/>
      <fx:application name="${ant.project.name}" mainClass="${javafx.main.class}" />
      <fx:resources>
        <fx:fileset dir="${basedir}/${dist.dir}" includes="${ant.project.name}.jar"/>
      </fx:resources>
    </fx:deploy>
  </target>

7行目の「fx:association」で関連付けを設定しています。拡張子は空白で区切って複数設定できます。

この設定でビルドしてみます。

作成されたapp、dmg、pkg

作成されたapp、dmg、pkg(クリックで拡大)

app、dmg、pkgが作成されました。appのアイコンに指定されたcompass.icnsが適用されています。

tekito.pandaにもアイコンが適用されている

tekito.pandaにもアイコンが適用されている
(クリックで拡大)

tekito.pandaのアイコンは、私の環境ではビルド直後には反映されなかったのですが、OSのアイコンキャッシュをクリアしたら反映されました。

関連付け後

関連付け後
(クリックで拡大)

「このアプリケーションで開く」が「AssocTest.app(デフォルト)」になっています。

tekito.pandaをダブルクリックすると、無事にAssocTestが起動しました。

ダブルクリックされたファイルの情報をアプリに渡す

さて、関連付けられたアプリを起動できるだけではあまり意味がないので、ダブルクリックされたtekito.pandaの情報をアプリに渡すようにしてみます。

JavaFXのApplicationでは、getParameters() というメソッドが用意されていて、通常のコマンドライン引数やjnlpから渡されたパラメータはここから取得できるのですが、関連付けられたファイルのパス情報は、どうもここには入っていないようです。

検索してみると、以下のような情報にぶつかりました。

なるほど、Appleが用意しているAPI経由で取得するのですね。Launcherクラスを追加し、AssocTestクラスも修正します。

Launcher.java:

package test;

import com.apple.eawt.AppEvent;
import com.apple.eawt.OpenFilesHandler;
import java.io.File;
import java.util.List;

public class Launcher implements OpenFilesHandler {

    private static List<File> files = null;

    public static List<File> getOpenFiles() {
        return files;
    }

    @Override
    public void openFiles(AppEvent.OpenFilesEvent e) {
        files = e.getFiles();
    }

    public static void main(String[] args) {
        if (System.getProperty("os.name").contains("OS X")) {
            Launcher launcher = new Launcher();
            com.apple.eawt.Application.getApplication().setOpenFileHandler(launcher);
        }
        javafx.application.Application.launch(AssocTest.class, args);
    }
}

AssocTest.java

package test;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class AssocTest extends Application {
    @Override
    public void start(Stage primaryStage) throws IOException {
        Text text = new Text();
        StackPane root = new StackPane();
        root.getChildren().add(text);

        Scene scene = new Scene(root, 300, 100);

        primaryStage.setTitle("関連付けのテスト");
        primaryStage.setScene(scene);
        primaryStage.show();

        List<File> files = Launcher.getOpenFiles();
        if (files != null && !files.isEmpty()) {
            byte[] bytes = Files.readAllBytes(Paths.get(files.get(0).toURI()));
            text.setText(new String(bytes, "utf-8"));
        } else {
            text.setText("ファイルが指定されていません。");
        }
    }
}

mainメソッドがLauncherに移動していることに注意してください。上記のStackOverflowの回答にもありますが、mainがJavaFXのApplicationクラスにあるとうまくAppleのイベントがハンドラに渡されないようです。

コンパイルしてみると。。。あれ、エラーになっちゃいますね。
「エラー: パッケージcom.apple.eawtは存在しません」とか言われます。

com.apple.eawtというパッケージはjre/lib/rt.jarに入っているのですが、Javaコンパイラはコンパイル時にはrt.jarではなくシンボルファイルというやつを見に行ってしまうらしいです(速度向上のため?)。このシンボルファイルにはrt.jarのすべてのクラスが入っているわけではなく、入っていないクラスはこのようにコンパイルエラーになっちゃうようです。

「プロジェクトのプロパティ>ビルド>コンパイル>追加コンパイルオプション」で
「-XDignore.symbol.file」を追加すると、今度はちゃんとコンパイルできました。

しかし、tekito.pandaをダブルクリックしてみると

実行結果

実行結果(クリックで拡大)

のように、まだファイルが渡されていません。

調べてみると、AssocTest.jarに入っているMANIFEST.MFのMain-Classがtest.AssocTestのままになっていました。

$ unzip -p dist/bundles/AssocTest.app/Contents/Java/AssocTest.jar META-INF/MANIFEST.MF
Manifest-Version: 1.0
Implementation-Title: AssocTest
Implementation-Version: 1.0
Permissions: sandbox
Codebase: *
JavaFX-Version: 8.0
Class-Path:
Created-By: JavaFX Packager
Implementation-Vendor: HOS
Main-Class: test.AssocTest

test.AssocTestにはmainが定義されていませんが、この場合JavaFXはヘルパクラスのmainを呼び出して実行してしまうようです。

明示的にtest.Launcherをメインにしないとダメですね。

いろいろ試してみましたが、NetBeans上の設定で「プロジェクトのプロパティ>実行>アプリケーション・クラス」を「test.Launcher」にして、build.xmlを右クリックして「ターゲットを実行」でビルドするとうまくいきました。ただ、この場合はプロジェクトの右クリック>ビルドが動かなくなってしまうようです。よくわかりませんが、ここらへんはNetBeansの問題な気がします。

ビルドしなおして再びtekito.pandaをダブルクリックしてみると、

実行結果

実行結果(クリックで拡大)

今度はうまくいきました!