2016年7月22日金曜日

Java/JavaFX 版 Mutter Launcher を Github で公開中

ここ最近の Java/JavaFX の投稿は、
以前に Windows 向けに C/C++ と Win32API で作ったアプリケーション Mutter Launcher を Java/JavaFX に移植するという作業の中から出てきたノウハウでした。

ちなみにもともとの Windows版の Mutter Launcher はこんなもの。

その Java/JavaFX 版の Mutter Launcher は Github で公開しています。
完全移植ではなく、まだまだ作成途中ではありますが、
大分、実装がまとまってきたので、
この blog でも記事として投稿してみました。

ここ最近書いてきたような Java/JavaFX のサンプルとして、
あるいは単純に Java アプリケーションとして
もし興味ある方は覗いてみて下さい。

2016年7月21日木曜日

JavaFX の ListView でアイコンを表示/Cell のカスタマイズ


JavaFX の ListView で String 以外を構成要素にするの拡張的な内容になりますが、
ListView の表示が toString() では収まらない、文字列以外も表示したい、という際には、ListView のリストを構成する Cell をカスタマイズすることになります。
その Cell をカスタマイズした一例として…

やりたかったことは ListView にファイル名と対応するアイコンの表示。
ポイントは ListView の Cell のカスタマイズと Icon の描画方法。

といっても、ほとんど

JavaFX file listview with icon and file name - Stack Overflow
http://stackoverflow.com/questions/28034432/javafx-file-listview-with-icon-and-file-name

のマネです。

Item というクラスがファイル名などを持っていて、ListView<Item> として ListView の要素のクラスとなっているとします。
ListView#setCellFactory() で Cell をカスタマイズする ListCell を拡張したクラス(この場合 ItemFormatCellを返すようにします。
        listView.setCellFactory(new Callback<ListView<Item>, ListCell<Item>>() {
             @Override
             public ListCell<Item> call(ListView<Item> list) {
                 return new ItemFormatCell();
             }
         });

ItemFormatCell クラスで、実際に1つのセルの表示内容をカスタマイズすることになりますが、ここで updateItem() をオーバーライドして実装するのが定番のようです。
次の例では、

  • 空行には何も表示しない(setText(), setGraphic() で null 指定)
  • 対応する Item がある場合には、アイコン(setGraphic())+ファイル名(setText())を表示する

ということをやっています。
    final class ItemFormatCell extends ListCell<Item> {
        public ItemFormatCell() {    }
   
        @Override protected void updateItem(Item item, boolean empty) {
            final boolean bUseJLabel = false;
     
            super.updateItem(item, empty);
     
            if(empty || item == null){
                 setText(null);
                 setGraphic(null);
                 return;
            }
     
            // getItemName() はファイル名を返すメソッドとして実装
            setText(item.getItemName());
            setContentDisplay(ContentDisplay.LEFT); // Graphic は Text の左に表示
            // getIcon() は対応する Icon を返すメソッドとして実装
            // Windows では FileSystemView.getFileSystemView().getSystemIcon() を利用
            Icon icon = item.getIcon();
            BufferedImage bufferedImage = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
            icon.paintIcon(null, bufferedImage.getGraphics(), 0, 0);
            // 最終的に Icon を ImageView の形で Cell に表示
            setGraphic(new ImageView(SwingFXUtils.toFXImage(bufferedImage, null)));    
        }
    }

ListView の Cell のカスタマイズについては、Cell の javadoc も参照してみて下さい。


2016年7月20日水曜日

JavaFX の ListView で String 以外を構成要素にする

ListView では javadoc にもあるように、ListView<String> という形で String 型のものを要素として第一に想定していますが、
もちろん、それ以外の独自のクラスを指定することも可能です。
その際に一番簡単な方法は次の通り。

例として独自の Item クラスというのを ListView で扱いたい場合、

・ObservableList<Item> を ListView<Item> に setItems() で設定する

・リストに表示される文字列は Item クラスの toString に実装

実装イメージを書くとこんな感じになります。
// ListView 側
    @FXML
    private ListView<Item> itemListView;

(略)
        ObservableList<Item> items = FXCollections.observableArrayList();

        // items に項目を追加する処理
        itemListView.setItems(items);

// Item 側
public class Item{
    private String itemName;

    @Override
    public String toString() {
        return itemName;
    }

2016年7月16日土曜日

JavaFX の fxml 使用時の Application と Controller の相互参照方法

fxml を使って JavaFX のアプリを作っていると、
Application#start() で渡されるメインとなる Stage(primaryStage)と Controller とのやり取りが、どうしても欲しくなってきます。
その際に、正規のやりとりの口はないらしく、

などを参照すると、自力で相互参照を作らないといけないようです。
  • Application#start() → Controller の参照
    FXMLLoader#getController() で load した fxml に関連した Controller のインスタンスが取得できる
  • Controller → primaryStage の参照
    Controller 側で stage を設定できる public メソッドを用意しておいて、上で取得した Controller に対して primaryStage を設定する
という方法になります。
孫引きになりますが、サンプルソースはこんな感じになります。
// MyGui.fxml のロードと、関連した Controller インスタンスの取得
FXMLLoader loader = new FXMLLoader(getClass().getResource("MyGui.fxml"));
MyController controller = (MyController)loader.getController();

// primaryStage を controller 側へ設定
controller.setStageAndSetupListeners(primaryStage); 



2016年7月12日火曜日

JavaFX の ComboBox で Return/Enter キーの入力をひろう

JavaFX の ComboBox でキーイベント等をひろうの続きっぽい内容ですが、
ComboBox で Return/Enter キーの入力をひろうのに苦労したので、
そこのメモ。

検索すると

  1. OnAction のイベントハンドラーで処理できる
  2. ComboBox の getEditor() で取得できる内部の TextField のキーイベントの getCode() と KeyCode.ENTER を比較
なんて方法が出てくるがどちらもうまく行かなかった。

1. の方法は、それで処理されるときもあれば、処理されないときもあるという動作が不確定。

2. の方法は、確かに OnKeyTyped の際にはイベント通知が来るが(ちなみに OnKeyPressed はダメだった)、ログ出ししてみるとなぜか getCode() した結果が UNDEFINED 値(KeyCode.ENTER との比較が true にならない)。

ということで、最終的にイベントのログ出し結果等から私が書いたコードは次のようになった。
    comboBox.getEditor().setOnKeyTyped((event) -> {
        System.out.println("ComboBox onKeyTyped:" + event);
        if(event.getCode() == KeyCode.ENTER || event.getCharacter().equals("\n") || event.getCharacter().equals("\r")){
            executeSelectedItem();
        }
    });
今のところ、ログやデバックでの動作を見る限り getCharacter() の "\r" との比較で上手く動作しています。
それ以外の比較は、環境や動作が変わった場合の保険。
(event.getCode() 値が KeyCode.ENTER になるのが正しい動作だと思うので)

もっとエレガントで正しい書き方があれば知りたい…

2016年7月8日金曜日

JavaFX で Windows のホットキー登録を行う / JIntellitype の利用

JavaFX で Windows のホットキー登録(Win32API の RegisterHotKey で登録できるもの)を行おうとした場合の方法についてです。

システムトレイの時とは違い、awt 等の既存の UI ライブラリにも無いため、
JNI に頼らなければいけないところですが、
既に Java にラッピングしたライブラリが開発・配布されていて、それが JIntellitype です。
そのため JIntellitype に頼るのが早道かと思われます。

実装コードは次のような感じです。

登録処理(ex. Windowsキー + C で画面を表示させたい場合)
    // register global Windows Hot Key
    JIntellitype.getInstance().registerHotKey(1, JIntellitype.MOD_WIN, (int)'C');
    JIntellitype.getInstance().addHotKeyListener(new HotkeyListener(){
        @Override
        public void onHotKey(int aIdentifier) {
            if(aIdentifier == 1){
                Platform.runLater(() -> {
                    primaryStage.show();
                    primaryStage.toFront();
                });
            }
        }
    });

解除処理
    JIntellitype.getInstance().unregisterHotKey(1);

終了処理
    JIntellitype.getInstance().cleanUp();
キーの登録は registerHotKey() → addHotKeyListener() で Listener 登録となります。
複数のキー登録にも対応していて、register 時の第一引数が識別子となっていて、Listener 側へも引数として渡ります。
また、JavaFX の GUI への処理は、これまたシステムトレイの時と同じく、直接呼び出すのではなく、 Platform.runLater() を使って JAT に対する処理依頼の形を取ります(ここも JAT とスレッドコンテキストが違うため)。

解除処理は登録時の識別子を渡して unregisterHotKey() です。

アプリケーション終了前には cleanUp() を呼び出して後始末処理をするようにして下さい。
システムトレイの時と同じく、Application#stop() に実装をして、 Platform.exit() の呼び出し時に必ず呼び出されるようにすると良いのではないかと思います。

実装は簡単なのですが、実際に Eclipse でライブラリを使う際に注意点があります。

JIntellitype の jar に対して Libraries として Build Path を通すのは当然ですが、
jar → JIntellitype.dll へのパス登録も必要になります。
次のどちらかの方法を使って DLL への関連付けを行います。
  • Project のプロパティ → Java Build Path → Libraries タブ → JIntellitype の jar を開いて "Native library location" を選択 → Edit
  • jar のプロパティ → Native Library 

※ 参考にしたページ

2016年7月7日木曜日

JavaFX で Windows のシステムトレイを使う

JavaFX で Windows のシステムトレイを使おうとした場合、
JavaFX 内での解決方法はなく、
Java 6 で導入された java.awt.TrayIcon を使わないといけないようです。

TrayIcon 自体の使い方は


といったページなどでまとめられているので、そちらを参照して下さい。

ここでは TrayIcon を JavaFX で使う場合の注意点について触れます。

  1. TrayIcon に登録した Listner から、JavaFX の UI に対して操作を行う場合には、直接操作を行うのではなく、 Platform.runLater() を使って JAT コンテキスト内で処理されるようにする
  2. アプリケーションの終了は System.exit() ではなく、JavaFX で Platform.setImplicitExit(false) した上で Platform.exit() を使う。
    その上で exit する際には必ず明示的に SystemTray.getSystemTray().remove() を呼んで SystemTrayIcon を消すようにする
という2点です。

ズバリ、この点については次のページを参考にしています。

が、一応、私なりの補足を書いておくと…

注意点の1点目は JavaFX の GUI スレッド用の対応です。

JavaFX の解説書や JavaFX でスレッドを扱う場合の記事などで良く触れられますが、 JavaFX は GUI 用に独自のスレッド JavaFX Application Thread (JAT) を持っていて、GUI の操作は全て JAT のスレッドのコンテキスト内から行わないといけません。
しかし TrayIcon は、 JavaFX ではなく awt のためスレッドのコンテキストが分かれてしまっています。 そのため、TrayIcon に登録した Listner 側からは JavaFX GUI の操作が直接できません。

そういう場合には Platform.runLater() を使うと、JAT とは別のスレッドコンテキストから JAT に対して処理依頼が出来るようになります。
(runLater() で依頼された処理は、JAT 側のキューにためられて順次処理がされます)

例えば、TrayIcon をダブルクリックしたら、JavaFX のメインウィンドウ(primaryStage)を隠したい(hide() したい)場合などは次のような感じになります。
SystemTray tray = SystemTray.getSystemTray();
TrayIcon icon = new TrayIcon(ImageIO.read(getClass().getResourceAsStream("hoge.png")));
icon.setImageAutoSize(true); // hoge.png を trayicon サイズに自動調整
icon.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        Platform.runLater(() -> {
            primaryStage.hide();
        });
    }
});
tray.add(icon);
注意点の2点目は、JavaFX での終了処理の作法に絡んだ対応です。

JavaFX アプリは Platform.exit() を使うのが作法であります。
Application の Javadoc によれば
JavaFXアプリケーションを明示的に終了するには、Platform.exit()を呼び出すことをお薦めします。代替方法としてSystem.exit(int)を直接呼び出すことが許容されますが、Applicationはstop()メソッドを実行できなくなります。 
とありますので、stop() に何も実装していないのであれば、System.exit() でも良さそうではあります...
ではありますが、副作用も読み切れないので、Platform.exit() を使うという前提で話を進めます(笑

TrayIcon を使うのであれば、JavaFX のメインウィンドウを閉じる場面が多々あると思われます。
それでアプリケーション自体が終了しないための指定として Platform.setImplicitExit(false) を呼び出して、 JavaFX アプリの終了は内部で Platform.exit() を呼び出した場合だけにします。

ただ、Platform.exit() 呼び出し後に TrayIcon をそのままにしておくと、awt のスレッドが残ってしまい、アプリケーションが終了しません。
そこで、Platform.exit() と一緒に必ず SystemTray.getSystemTray().remove() を呼び出して TrayIcon の後始末=awt スレッドの終了もされるようにします。

私自身は、Application#stop() に実装をして、 Platform.exit() の呼び出し時に必ず呼び出されるようにしてみました。
これでうまく行く感じです。
ざっと実装の概要だけ書くとこんな感じです。
public class Main extends Application {
    private TrayIcon icon;
    @Override
    public void start(Stage primaryStage) {
        // start で TrayIcon の登録処理を実装
        Platform.setImplicitExit(false);
        SystemTray tray = SystemTray.getSystemTray();
        icon = new TrayIcon(ImageIO.read(getClass().getResourceAsStream("reload_12x14.png")));
        // 本当はここで Listener 登録をしているが、ここでは省略
   
        tray.add(icon);
    }
    @Override
    public void stop() throws Exception {
        // stop で TrayIcon の登録処理を実装
        SystemTray.getSystemTray().remove(icon);
        super.stop();
    }
}