タグ「JavaFX」が付けられているもの

この記事は JavaFX Advent Calendar 2016 の 20 日目です。昨日はひらおかゆみ (@yumix_h) の「JavaFXで小惑星を描いてみよう」です。

2~3 年前、JavaFX のフロントエンドを持つアプリケーションのテキスト入力欄に、Google 検索のような入力補完 (履歴参照) 機能を追加する要望が持ち上がって、会社の後輩と 2 人で対応したことがあります。後輩の退職後に機能のメンテナンスを重ねるうちに、アプリケーションから独立した部品として使用できるようになりました。

実際のアプリケーションで使用しているソースコードは使用できませんが、今回は機能設計から新たに書き起こしたものをご紹介します (Ethereal と Wireshark の関係と同じ位置づけだと認識しています)。

はじめに、入力補完 (履歴参照) 機能の実行サンプルを figure 1 に示します。

javafx-textfield-history.png
figure 1 - 入力補完 (履歴参照) 機能のサンプル

この機能を実装するのに必要なコンポーネントは、以下の 2 つです。

いずれも Snapshot ですが私の Nexus https://repo1.haswell.jp/ にも載せてあります。

  • groupId: jp.coppermine.tools
  • artifactId: coppermine-tools-javafx-history
  • version: 現時点ではともに 0.2.0

前者はメモリやファイルなどをストアとして利用する履歴参照機能を提供するモジュールです。今回はそのうちファイルをストアとするものを使用しました。また、後者は前者を利用した JavaFX のコントロールもどきで、実態は TextField の入力内容と連動する ListView です。前者は私のオリジナルです。後者は後輩がアルゴリズムと初期実装を担当し、私が Lambda と Stream API による再実装とアプリケーション本体からの分離を担当しました。

使い方

詳しくは https://github.com/khasunuma/javafx-history-sample を参照して頂きたいのですが、大雑把には次のようになります。

まず、履歴参照機能を取り付ける TextField と紐付くコントローラー・クラスに FileHistoryOperation インタフェースを実装します。ほとんどの機能はメソッドのデフォルト実装にて提供しています。ただし、getHistory メソッドだけは純粋な抽象メソッドであるため以下のように実装する必要があります。

private History history = new FileHistory(getPath());

@Override
public History getHistory() {
    return history;
}

続いて、HistoryView クラスのインスタンスを生成します。

private HistoryView historyView = new HistoryView();

コントローラー・クラスの initialize メソッドに以下の記述を追加します。

initializeHistory();
loadKeywords();
historyView.attach(textField, () -> getKeywords());

最後にトリガーとなるボタンやキーイベントを適切に実装します。これで、最初のスクリーンショットのような入力補完とコマンド履歴が使えるようになります。

当初、私は過去の入力履歴を保存して編集可能なコンボボックスで選択できるような実装で十分だと考えていて、後輩にもそう伝えていたのですが、彼はあくまで Google 検索のような入力補完にこだわり、2 人で試行錯誤を重ねて最初の実装を実現しました。今回はオリジナルをリバースエンジニアリングしたものであり、Java SE 8 で追加された機能を用いて実装の大部分を書き換えたため、最初の実装からは大きく姿を変えています。しかし、入力補完のアルゴリズムは後輩が考案したものを基本的に踏襲しており、私の修正は複雑なループを Stream API に置き換えたことだけでした。現在、後輩は遠い異国の地にいますが、そこでソフトウェア技術者として大いに活躍していることと思います。

明日は深井さん (@fukai_yas) です。

この記事は、Payara Advent Calendar 2016JavaFX Advent Calendar 2016Java EE Advent Calendar 2016 の 15 日目です。昨日は、

です。

Payara Micro は Java EE の埋め込みサーバーとして使用することが可能です。Java EE の埋め込みサーバーは、Java SE アプリケーションに対して Java EE API を提供するもので、別途 Java EE サーバーを構築しなくても Java SE アプリケーション上で Web アプリケーションをはじめとする Java EE アプリケーションを実行させることができるようになります。また、Java EE サーバーを準備しなくて良いことから、Java EE アプリケーションの単体テストを実行するための組み込みコンテナとしても活用することができます。

埋め込みサーバーとしての Payara Micro は、Embedded GlassFish Web Profile を使いやすくラップしたものであり、API としては Web Profile に加えて JBatch、Concurrency Utilities、JCache が追加されています (ただし、クラスタリングを無効にすると Hazelcast が使われなくなるため、それによって提供される JCache も同時に使用できなくなります)。

Payara Micro を埋め込みサーバーとして使用する場合、通常は Java SE のアプリケーションに組み込んで使うことになりますが、JavaFX のアプリケーションに組み込むことも可能です。ただし、いくつか制約事項がありますので、以下に示します。

1. スタートアップ

PayaraMicro のスタートアップ・コードは、javafx.application.Application#init メソッドに記述します。この場所でないと Payara Micro を初期化することができません。

private PayaraMicroRuntime runtime;

@Override
public void init() throws Exception {
    runtime = PayaraMicro.getInstance().setNoCluster(true).bootStrap();
}

2. アプリケーション実行時

このセクションは適当に流すつもりだったのですが、3 つの Advent Calendar に対する共通エントリとしたため気が引けて、当日未明になってから 5 時間がかりで全面的に書き直しました。何らかの参考になれば幸いです。

FXML と Controller クラスを使用しているのであれば、Java SE から Web アプリケーションに対してリクエストを送信し、そのレスポンスを受信して UI に反映させる処理となります。ここでは JAX-RS Client と WebSocket の使用例を重点的に示します。

2.1. JAX-RS Client を使用する場合

以下に JAX-RS のクライアント/サーバーを利用した例を示します。

Step 1 - JAX-RS のエンドポイントを定義する

まず、Web アプリケーション側に JAX-RS のエンドポイントを定義します。サンプルコードを以下に示します。

// ApplicationPath クラス
@ApplicationPath("api")
public class ApplicationConfig extends Application { }

// SearchResource クラス
@Path("search")
@RequestScoped
public class SearchResource {
  @GET
  @Path
  @Produces("application/xml")
  public List<List> search(@NotNull @QueryParam("q") String query) {
    List result = new ArrayList<>();
    
    // (ここで検索処理を行い、結果の文字列を result へ格納する)
    
    // JAX-RS は List<String> でも適切な XML にアンマーシャリングする
    return result;
  }
}

Step 2 - JavaFX から JAX-RS エンドポイントへのアクセスを実装する

JavaFX の Controller に対して JAX-RS エンドポイントにアクセスするコードを記述します。JAX-RS に関しては、これだけで JavaFX からデータを取得して ListView などへの表示を行えるようになります。


public class SearchController implements Initializable {
  
  //////////////// 共通処理 ////////////////
  
  /**
   * 検索条件を入力するテキストフィールドです。
   */
  @FXML
  private TextField queryField;
  
  /**
   * 検索結果の文字列を格納するリストです。
   */
  private List results = new ArrayList<>();
  
  //////////////// 検索処理 (非同期) の定義 ////////////////

  /**
   * 検索実行可能な場合に true となるプロパティです。
   */
  private BooleanProperty ready = new SimpleBooleanProperty(true);
  
  /**
   * JAX-RS エンドポイントを呼び出すタスクを作成します。
   * <p>
   * ボタン・クリックのイベントが発生したときに実行することを想定しているため、
   * {@link Service} による非同期処理として実装する必要があります。
   */
  Service<List> searchService = new Service<List>() {
    @Override
    protected Task<List> createTask() {
      return new Task<List>() {
        @Override
        protected List call() throws Exception {
          // 検索条件は queryField に入力される
          String query = queryField.getText();
          
          // 検索結果は listView に設定する
          listView.getItems().clear();
          
          // Web サービスを呼び出し、検索結果を取得する
          List results = client.target(CONTEXT_ROOT).path("api").path("search")
                                       .queryParam("q", query)
                                       .request()
                                       .accept("application/xml")
                                       .get(new GenericType<List>() { });
          // (追加の処理が必要な場合は、ここで実装する)
        
          // 検索結果を設定する
          // このクラス内では getValue() で参照可能になる
          return result;
        }
      };
    }
    
    /**
     * タスク実行時の処理を記述します。
     */
    @Override
    protected void running() {
      // 再検索を行う時は、前回の検索結果を消去する
      tweets.clear();
      
      // 検索中なので ready プロパティの状態を false に変更する
      // これを行わないと検索リクエストが無制限に発生して面倒なことになる
      ready.set(false);
    }
    
    /**
     * タスク成功時の処理を記述します。
     */
    @Override
    protected void succeeded() {
      // 検索結果を取得する
      results.addAll(getValue());
      
      // 検索結果を listView に設定する (重複排除とソートは実施する)
      listView.setItems(FXCollections.observableList(
          getValue().stream().distinct().sort().collect(toList());
      
      // (検索結果のダイアログ表示などは、この場所に記述すること)
    
      // ready プロパティの状態を true に変更して、検索可能な状態に戻す
      ready.set(true);
    }
    
    /**
     * タスクキャンセル時の処理を記述します。
     */
    @Override
    protected void cancelled() {
      // (検索キャンセルのダイアログ表示などは、この場所に記述すること)
      
      // ready プロパティの状態を true に変更して、検索可能な状態に戻す
      ready.set(true);
    }
    
    /**
     * 検索失敗時の処理を記述します。
     */
    @Override
    protected void failed() {
      // (検索失敗のダイアログ表示などは、この場所に記述すること)
      
      // ready プロパティの状態を true に変更して、検索可能な状態に戻す
      ready.set(true);
    }
  };
  
  //////////////// ボタン・イベント処理の定義 ////////////////
  
  /**
   * 検索ボタンをクリックした時の処理を記述します。
   * 
   * @param event アクション実行時のイベント
   */
  @FXML
  public void onSearchAction(ActionEvent event) {
    // searchService による非同期呼び出し
    // もし仮に同期呼び出しを使用すると、JavaFX の UI が硬直するので注意
    searchService.reset();
    searchService.start();
  }
  
  @Override
  public void initialize(URL location, ResourceBundle resources) {
    // 省略
  }  
}

2.2. WebSocket

以下に WebSocket のクライアント/サーバーを利用した例を示します。

Step 1 - WebSocket のエンドポイントを定義する

Web アプリケーション側で WebSocket のエンドポイントを Singleton として実装します。Singleton 以外でも実装可能のはずですが、私の環境では Singleton が最も安定して動作します。

@Singleton
@ServerEndpoint(value = "/api/twitter/publish")
public class TwitterPublisher {
  
  /**
   * WebSocket のセッションを管理するオブジェクトです。
   */
  private Set sessions = new CopyOnWriteArraySet<>();
  
  /**
   * 新たな WebSocket 接続があったときに対応するセッションを追加します。
   *
   * @param 追加されるセッション
   */
  @OnOpen
  public void onOpen(Session session) {
    sessions.add(session);
  }
  
  /**
   * 既存の WebSocket 接続が切断されたときに対応するセッションを削除します。
   *
   * @param 削除されるセッション
   */
  @OnClose
  public void onClose(Session session) {
    sessions.remove(session);
  }
  
  /**
   * エラー発生時の処理を定義します。
   *
   * throws t エラー内容を表す例外オブジェクト
   */
  @OnError
  public void onError(Throwable t) {
    // ログ出力などを行う (省略)
  }
  
  /**
   * 接続済みのすべてのセッションに対してメッセージを配信します。
   * <p>
   * このメソッドは、メッセージ配信のタイミングで外部から呼ばれます。
   * このサンプルでは、外部の EJB タイマーから定期的に呼び出します。
   */
  public void send(String message) {
    sessions.forEach(session -> {
      try {
        session.getBasicRemote().sendText(message);
      } catch (IOException) {
        e.printStackTrace();
      }
    });
  }
  
  @Override
  public void initialize(URL location, ResourceBundle resources) {
    // 省略
  }  
}

Step 2 - Web アプリケーション側に定期配信の仕組み (EJB Timer) を実装する。

WebSocket は定期的に配信するところに強みがあります。エンドポイント自身には定期配信を行うための仕組みがないため、エンドポイントを呼び出して定期配信するためのコードを実装する必要があります。ここでは Web アプリケーション側に EJB Timer を追加して、10 秒おきにエンドポイントからデータを取得するようなコードを用意しました。

@Singleton
public class Scheduler {
  
  /**
   * ポーリング実行時の絞り込み条件を表します。
   */
  private String query;
  
  /**
   * ポーリングの開始・停止を制御します。
   *
   * @pamam flag {@code true} の場合はポーリング実行、{@code false} の場合は停止
   */
  public setPolling(boolean flag) { /* 省略 */ }
  
  /**
   * ポーリングの開始・停止の状態を取得します。
   *
   * @return {@code true} の場合はポーリング実行中、{@code false} の場合は停止済
   */
  public isPolling() { /* 省略 */ }
  
  /**
  * ポーリングを開始します。
  *
  * @param query ポーリング対象の絞り込み条件
  */
  public void startPolling(String query) {
    // ポーリング停止中のみ処理対象 (既に実行中の場合は何もしない)
    if (!isPolling()) {
      this.query = query;
      setPolling(true);
    }
  }
  
  /**
   * ポーリングを停止します。
   */
  public void stopPolling() {
    // ポーリング実行中のみ処理対象 (既に停止中の場合は何もしない)
    if (isPolling()) {
      setPolling(false);
      query = null;
    }
  }
  
  /**
   * Publisher クラス (WebSocket エンドポイント) のインスタンスです。
   */
  @Inject
  private Publisher publisher;
  
  /**
   * 定期的に {@link Publisher#send(String)} を呼び出してデータを配信します。
   * このメソッドは EJB Timer として呼び出されることが補償されているため、
   * {@code private} スコープとしています。
   * <
   * なお、このメソッドは 10 秒おきに EJB Timer として呼び出されます。
   *
   * @see javax.ejb.Scledule
   */
  @Schedule(hour = "*", minute = "*", second = "*/10", persistent = false)
  private void polling() {
    if (isPolling()) {
      // ポーリング実行中: データ配信
      // ここでは getNewResult メソッドで新着メッセージを取得すると仮定
      List result = getNewResult(query);
      result.forEach(s -> publisher.send(s));
    }
  }
}

Step 3 - JavaFX から WebSocket エンドポイントへのアクセスを実装する

JavaFX の Controller に対して WebSocket エンドポイントにアクセスするコードを記述します。WebSocket に関しては、これだけで JavaFX からデータを取得して ListView などへの表示を行えるようになります。

public static class PollingController implements Initializable {

//////////////// 共通処理 ////////////////

/**
 * ポーリング対象条件を入力するテキストフィールドです。
 */
@FXML
private TextField queryField;

/**
 * ポーリングの開始・終了を制御するトグルボタンです。
 */
@FXML
private ToggleButton pollingButton;

/**
 * ポーリング結果の文字列を格納するリストです。
 */
@FXML
private ListView<String> listView;

//////////////// 検索処理 (非同期) の定義 ////////////////

/**
 * ポーリング実行可能な場合に true となるプロパティです。
 */
private BooleanProperty ready = new SimpleBooleanProperty(true);

/**
 * WebSocket エンドポイントを呼び出すタスクを作成します。
 * <p>
 * ボタン・クリックのイベントが発生したときに実行することを想定しているため、
 * {@link Service} による非同期処理として実装する必要があります。
 */
Service<Void> pollingService = new Service<Void>() {
  @Override
  protected Task<Void> createTask() {
    return new Task<Void>() {
      /**
       * This latch works the thread keep awaiting because of accepting cancel request.
       */
      private CountDownLatch messageLatch = new CountDownLatch(1);
      
      /**
       * WebSocket セッションへの参照を表します。
       */
      private Session session;
      
      @Override
      protected Void call() throws Exception {
        // WebSocket のエンドポイント
        final wsEndpoint = URI.create("/api/polling/publish");
        
        // このあたりは WebSocket クライアント API を使うときのほぼ定型コード
        WebSocketContainer container = ContainerProvider.getWebSocketContainer();
        session = container.connectToServer(new Subscriber(listView), wsEndpoint);
        
        messageLatch.await();
        
        return null;
      }
      
      /**
       * ポーリング実行時の処理を記述します。
       */
      @Override
      protected void cancelled() {
        if (session != null) {
          try {
            session.close();
          } catch (IOException e) {
            // ログ出力などを行う (省略)
          }
        }
        
	// pollingButton をクリック可能にする
        pollingButton.setSelected(false);
        
        // ポーリング実行を受け付ける
        ready.set(true);
      }

      /**
       * ポーリングキャンセル時の処理を記述します。
       */
      @Override
      protected void failed() {
        
	// pollingButton をクリック可能にする
        pollingButton.setSelected(false);
        
        // ポーリング実行を受け付ける
        ready.set(true);
      }
    };
  }

  /**
   * タスク失敗時の処理を記述します。
   */
  @Override
  protected void cancelled() {
    // pollingButton をクリック可能にする
    pollingButton.setSelected(false);
    
    // ポーリング実行を受け付ける
    ready.set(true);
  }

  /**
   * タスク失敗時の処理を記述します。
   */
  @Override
  protected void failed() {
    // pollingButton をクリック可能にする
    pollingButton.setSelected(false);
    
    // ポーリング実行を受け付ける
    ready.set(true);
  }
};

/**
 * ポーリング開始・停止ボタンをクリックした時の処理を記述します。
 * 
 * @param event アクション実行時のイベント
 */
@FXML
public void onPollingAction(ActionEvent event) {
  if (pollingButton.isSelected()) {
    pollingButton.setSelected(false);
    
    // 開始要求の GET リクエストを送信するだけ: レスポンスは無視
    client.target(CONTEXT_PATH).path("api").path("polling").path("start")
          .queryParam("q", queryField.getText()).request().get();

    listView.getItems().clear();
    
    // pollingService による非同期呼び出し
    // もし仮に同期呼び出しを使用すると、JavaFX の UI が硬直するので注意
    pollingService.reset();
    pollingService.start();
  } else {
    pollingService.cancel();
    
    // 停止要求の GET リクエストを送信するだけ: レスポンスは無視する
    client.target(CONTEXT_PATH).path("api").path("polling").path("stop")
          .request().get();
  }
}

2.3. その他の手段

JavaServer Faces、Servlet、JSP などに対しては、画面遷移を伴うことから、通常は JavaFX の WebView を経由したアクセスになります。ただし、ユースケースによっては HTTP Client で直接アクセスすることもあり得ます。

JAX-WS については、Java SE に JAX-WS Client が付属しているため、これを使用することができます。

RMI-IIOP などのレガシーな分散処理プロトコルについても、検証こそしていませんが同様の要領で使用できるのではないかと考えています。もっとも、今どき RMI-IIOP なんて (一部の例外を除き) 使用することはないと思いますが。

その他にも Java EE 7 には様々な API が用意されています。JavaFX のアプリケーションに Payara Micro を埋め込むことで、それらの API も気軽に使用することができます。

3. シャットダウン

Payara Micro を JavaFX に組み込むと、通常の Ctrl+C によるシャットダウンができません。これは JavaFX 側でシャットダウン・フックを完全に押さえており、Payara Micro のシャットダウン・コードをシャットダウン・フックに追加する余地がないためだと思われます。この制限のため、Payara Micro のシャットダウンは javafx.application.Application#stop メソッドから PayaraMicro#shutdown または PayaraMicroRuntime#shutdown メソッドを呼び出すことで実現する必要があります。

@Override
public void stop() throws Exception {
    if (runtime != null) {
        runtime.shutdown();
    }
}

なお、Payara Micro を明示的にシャットダウンしなかった場合には、それを動作させていた Java VM (プロセス) が残ったままとなりますので、注意が必要です。

4. まとめ

いくつかの制約事項はありますが、JavaFX と Payara Micro を組み合わせて使用することは可能です。javapackager を使用すれば、Java EE ランタイムを内包した JavaFX アプリケーションをネイティブ・パッケージとして配布することも可能になります。フロントエンドを JavaFX、バックエンドを Payara Micro で構成した、比較的小さな Java EE アプリケーションを開発および配布する場合には、ネイティブ・パッケージも作成できる上記の方法も選択肢のひとつとしてあげられることでしょう。

今回のサンプルコードは一応 https://github.com/khasunuma/payaramicro-on-javafx にあります。急ぎ追加した JAX-RS と WebSocket に関するコードが含まれていないため、あくまでテンプレートとしての扱いでお願いします。

明日は、Payara が「Payara が持つ 2 つのクラスタリング」、JavaFX がさくらばさん (@skrb)、Java EE がタイガーたいぞー (@tigertaizo)さんです。

JavaFX の Color クラスを探る

この記事は JavaFX Advent Calendar 2016 の初日です。誰も書く気配を見せないので、最近の関心事について少しだけ。

JavaFX の Color クラスは、API ドキュメントにも記載があるように sRGB 色空間をモデリングしたものです。実際に、内部実装は red (赤)、green (緑)、blue (青)、opacity (不透明率) の 4 つの 'float' 型フィールドで構成されています (外部的には double 型でのやり取りとなります)。AWT の Color クラスと同様に HSB (HSV) 色空間との相互変換もサポートしますが、使う側としては変換操作を意識することなく、必要に応じて sRGB または HSB として利用することができます。また、HTML/CSS のカラー属性文字列からインスタンスを生成できたり、148 色のプロセスカラーを事前定義するなど、AWT と比較して Web との親和性も強化されています。

AWT と異なり、CIE XYZ 表色系などへの変換はサポートされていません。色の世界は奥が深いため、にわか知識で様々な表色系に手を出すよりは、sRGB と HSB を確実に押さえようという考えでしょうか。@yumix_h が小惑星の色表現で悲鳴を上げているのを見て、色について少し調べてみたのですが (大学で環境工学を専攻していたため空間の色に対する予備知識が救いとなりました)、基準となる光源の選び方やガンマ補正など、迂闊に手を出すと大やけどをする分野でもあります。

Color クラスのインスタンスは以下の方法で生成することができます。

  • コンストラクタ -- sRGB: Red (赤)、Green (緑)、Blue (青)、Opacity (不透明度・オプション)
  • color ファクトリ・メソッド -- sRGB: Red (赤)、Green (緑)、Blue (青)、Opacity (不透明度・オプション)
  • rgb ファクトリ・メソッド -- RGB: Red (赤・離散値)、Green (緑・離散値)、Blue (青・離散値)、Opacity (不透明率・オプション)
  • hsb ファクトリ・メソッド -- HSB: Hue (色相)、saturation (彩度)、brightness (明度)、Opacity (不透明度・オプション)
  • gray ファクトリ・メソッド -- Gray (無彩色の明度)、Opacity (不透明度・オプション)
  • grayRgb ファクトリ・メソッド -- Gray (無彩色の明度・離散値)、Opacity (不透明度・オプション)
  • web ファクトリ・メソッド -- HTML/CSS カラー属性文字列 (rgb、rgba、hsl、hsla)
  • 定数 - プロセスカラー (148 色)

色成分の取得は以下のメソッドで行います。sRGB ↔ HSB 変換を明示的に行うことなく双方の色成分を取得できることが特徴です。具体的には sRGB で作成した色の色相・彩度・明度を取得したり、反対に HSB で作成した色の赤・緑・青成分を取得することが可能です。

  • double getRed() -- 赤 (0.0-1.0)
  • double getGreen() -- 緑 (0.0-1.0)
  • double getBlue() -- 青 (0.0-1.0)
  • double getOpacity() -- 不透明度(0.0-1.0)
  • double getHue() -- 色相 (0.0-360.0)
  • double getSaturation() -- 彩度 (0.0-1.0)
  • double getBrightness() -- 明度

相対的な色変換

「やや明るく」「やや暗く」のような相対的な色変換を行うメソッドが用意されています。

  • Color brighter() -- より明るくする
  • Color darker() -- より暗くする
  • Color saturate() -- 彩度を上げる
  • Color desaturate() -- 彩度を下げる

最初、仕組みがよく分からなかったのですが、内部で deriveColor という色相・彩度・明度をシフトするメソッドを呼んでいるようです。

Color deriveColor(double hueShift, double saturationFactor, double brightnessFactor, double opacityFactor)

ちなみに、色相変化量を 0、彩度・明度および不透明度を 1.0 (倍) に設定すると、全く同じ色になります。

deriveColor(0, 1.0, 1.0, 1.0)

前掲のメソッドは、それぞれ次の deriveColor 呼び出しと等価です。

  • brighter() → deriveColor(0, 1.0, 1.0 / 0.7, 1.0)
  • darker() → deriveColor(0, 1.0, 0.7, 1.0)
  • saturate() → deriveColor(0, 1.0 / 0.7, 1.0, 1.0)
  • desaturate() → deriveColor(0, 0.7, 1.0, 1.0)

彩度・明度を上げるときは約 143 % (1.0 / 0.7)、下げるときは 70 % (0.7) に設定しているようです。

グレースケール化と色反転

特定の色をグレースケールにしたり、あるいは反転することも可能です。

  • Color grayscale() - グレースケール化
  • Color invert() - 色反転

最初、これらについても deriveColor メソッドを使用しているのかと推測しましたが、実際には直接 sRGB の色成分を変更しているようです。グレースケール化の場合は、以下に示すように、赤・緑・青それぞれに異なる重み付けをして、すべての成分を同じ値に設定し直しています。緑のウェイトが大きいのは、CIE XYZ 表色系の Y 成分 (緑に相当) が色成分に加えて可視光のパワーも反映していることと関連があると思われます。また、人間の目は青よりも赤に対する感度の方が高いことが、それぞれの重み付けにも影響しています。

  • grayscale() → color(0.21 * red + 0.71 * green + 0.07 * blue, 0.21 * red + 0.71 * green + 0.07 * blue, 0.21 * red + 0.71 * green + 0.07 * blue, opacity);

色反転の場合、赤・緑・青すべての成分を単純に反転させています。色相を 180°回転させるのかと想像していただけに、意外な実装です。

  • invert() → Color.color(1.0 - red, 1.0 - green, 1.0 - blue, opacity)

sRGB は基本的に D65 光源 (色温度 6500K) における白色を基準とした色の表現ですが、D50 光源 (色温度 5000K) に対応した sRGB も存在しているようです。また sRGB では使用しませんが C 光源 (色温度 7000K) を使用する色表現や、sRGB と同じ D65 光源を採用しつつも補正値が異なる Adobe RGB なども存在するわけで、それらに対する拡張ポイントも欲しかったというのが正直な感想です。

参考まで、http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html には CIE XYZ 表色系における光源ごとの変換行列が掲載されています。C、D50、D65 以外にも様々な光源が存在していると分かります。

p.s. @yumix_h は光源の違いについてもちゃんと考慮しているのだろうか?

この記事は JavaFX Advent Calendar 2015 の 3 日目です。昨日は @boochnich さんの「JavaFXで作ったアナログ時計にタッチパネル操作を追加してみた際のメモ」です。

同様の内容の記事は既に書いているのですが、かなり雑な書き方だったので、今回はちゃんと書きます。

Oracle から JavaFX Scene Builder のバイナリ配布が行われなくなって久しいですが、現在は Gluon が実行可能 JAR と各プラットフォーム向けインストーラのバイナリ配布を行っています。それ自体は非常にありがたいのですが、Windows 向け EXE インストーラ版を用いると、Surface Pro をはじめとする HiDPI 環境でウィンドウが潰れて表示されてしまいます。Surface Pro の場合は拡大率 150% のため、毎回手動でレイアウトを整えればまだ使えます。しかし、筆者が自宅で使用している LaVie (2560×1440) は拡大率が 200% であり、さすがに操作に支障をきたします。Windows でも JDK 8 Update 60 以降では JavaFX の HiDPI 対応がなされているため、最新の JRE と実行可能 JAR 形式の Scene Builder を組み合わせて使用すれば、解像度に関わる問題は解決します。ただし、手軽に使える EXE インストーラ版は同梱の古い JRE 上で動作するため、さすがにお手上げです。

一番楽な方法は素直に実行可能 JAR 形式で利用することです。ただ、筆者はこの件についてなぜか納得がいかないので、自分で MSI と EXE のインストーラを作ってみることにしました。今回はそのレシピのご紹介です。繰り返しになりますが、実行可能 JAR で納得のいく方は真似をしないでください。

JDK には javapackager というツールが含まれており、Java SE アプリケーションをインストーラ形式にパッケージすることができます。Windows の MSI および EXE 形式、Mac の DMG 形式、Linux の RPM 形式がサポートされます。古い JDK には javafxpackager という JavaFX 専用のパッケージツールが含まれていましたが、javapackager はその後継となるものです。

さて、今回やりたいことの本質は単純で、javapackager で実行可能 JAR 形式の Scene Builder をパッケージして、MSI または EXE 形式のインストーラを作成することです。ただし、事前準備が必要になります。javapackager はそれ自身インストーラを作成する機能を持たず、外部のインストーラ作成ツールを呼び出します。そして不幸なことに、Windows にはインストーラを作成するツールは含まれていません。

Case 1. MSI 形式インストーラ

MSI 形式は "Program Files" 以下にアプリケーションをインストールするもので、作成には WiX Toolset を使用します。最近の Windows アプリケーションでは "setup.exe" が MSI 形式のインストーラを呼び出していることが多いようです。MSI 形式の場合、実行には管理者権限が必要です (既定では UAC が働きます)。

まず、MSI 形式インストーラを作成するにはまず WiX Toolset をインストールしてください。インストール直後はパスが設定されていない状態のため、WiX のコマンド群 (例: C:\Program Files (x86)\WiX Toolset v3.10\bin) にパスを通すことも忘れないように。

次に適当なフォルダを用意して (面倒ならばデスクトップでも構いません)、以下の内容でバッチファイルを作成します。

javapackager -deploy -native msi -outdir SceneBuilder -outfile SceneBuilder -srcdir . -srcfiles SceneBuilder-8.0.0.jar  -appclass com.oracle.javafx.scenebuilder.app.SceneBuilderApp -name SceneBuilder -title "JavaFX Scene Builder" -description "JavaFX Scene Builder" -vendor "OpenJFX" -Bicon=SceneBuilder.ico -BappVersion=8.0.0 -BmenuHint=true -BshortcutHint=true -Bwin.menuGroup="JavaFX Scene Builder"

続いてバッチファイルと同じフォルダに実行可能 JAR 形式の Scene Builder (SceneBuilder-8.0.0.jar) とアイコンを置きます。Scene Builder 標準のアイコンは以下のものです。

SceneBuilder.ico

バッチファイル、Scene Builder、アイコンの 3 つが揃ったら、バッチファイルを実行します。成功すれば SceneBuilder というサブフォルダが作成され、SceneBuilder\bundles 以下に MSI インストーラ SceneBuilder-8.0.0.msi が作成されるはずです。

Case 2. EXE 形式インストーラ

EXE 形式はユーザープロファイルの下にアプリケーションをインストールするもので、作成には InnoSetup を使用します。Gluon が配布している EXE 形式インストーラも同様の形式です。この形式はユーザープロファイル以下にインストールするため、管理者権限が不要であるというメリットがあります。

EXE 形式のインストーラを作成するにはまず InnoSetup をインストールしてください。インストール後の設定は特に要らないはずです。その後の手順は WiX の場合とほぼ同じですが、バッチファイルの内容が異なります。

javapackager -deploy -native exe -outdir SceneBuilder -outfile SceneBuilder -srcdir . -srcfiles SceneBuilder-8.0.0.jar  -appclass com.oracle.javafx.scenebuilder.app.SceneBuilderApp -name SceneBuilder -title "JavaFX Scene Builder" -description "JavaFX Scene Builder" -vendor "OpenJFX" -Bicon=SceneBuilder.ico -BappVersion=8.0.0 -BmenuHint=true -BshortcutHint=true -Bwin.menuGroup="JavaFX Scene Builder"

相違点は -native msi の代わりに -native exe を指定しているところです。

Case 3. もう 1 つのオプション -- -native image

ここからはサンプルのバッチファイルは挙げませんが、--native オプションの引数には msi、exe の他に image も指定できます。--native image を指定すると、SceneBuilder\bundles フォルダの下にはインストーラは作成されず、実行形式の EXE ファイルと JRE が展開された形で生成されます。

実はこの JRE には java.exe や javaw.exe は存在せず、必要なライブラリ (JAR およびネイティブ) のみで構成されています。個人的には興味深いものと思っています。

JDK 8 Update 60 から、Windows 版の JavaFX にも HiDPI 対応がしれっと実装されていることは、ご存じの方も少なくないかと思います。この件については青江さん (@aoetk) のエントリ http://aoe-tk.hatenablog.com/entry/2015/08/23/234733 が詳しいので、詳細な考察についてはそちらを参照頂ければと思います。

さて、現在 JavaFX Scene Builder 最新版のバイナリは Oracle から配布されておらず、Gruon などサードパーティーから配布されています。これは非常にありがたいことなのですが、どうやらビルドに使用している JDK8 が旧版のままのようで、JDK8u60 の HiDPI 対応が反映されないという弱点があります。

1 つの方法として、自身の環境を JDK8u60 移行にアップデートした上で配布されている jar ファイルを java -jar ... で起動して解決する方法があるのですが、あまり美しい解決方法とは言えない。そこで、jar 配布版を javapackager でパッケージし直すことを考えました。

ということで、レシピを示します。

用意するもの

  • JDK 8 Update 60 (or later)
  • WiX Toolset (3.0 or later)
  • Scene Builder 8.0.0 の jar ファイル (Gruon 等からダウンロード)
  • SceneBuilder.ico ファイル (どこかからパクる)
  • JDK 8 と WiX への実行パス設定 (意外と忘れる...)

手順

  1. いろいろファイルが出力されるので、1 つ専用のフォルダを作った方が良いでしょう。
  2. Scene Builder の jar ファイルとアイコンファイルを先のフォルダにコピーします。
  3. javapackager を実行します。なお、実行環境に合わせて JDK 8 の 32bit 版/64bit 版をきちんと判別しましょう (特に 64bit 版で作成したものは 32bit 環境では動きません)。
  4. javapackager 実行後、{作成したフォルダ}\SceneBuilder\bundles 以下に SceneBuilder-8.0.0.msi のようなインストーラが作成されていれば、基本的には成功です。

参考

筆者は次のようなスクリプトを作成し、ビルドを行っています。パス等を変更して活用頂ければ幸いです。

javapackager -deploy -native msi -outdir SceneBuilder -outfile SceneBuilder -srcdir . -srcfiles SceneBuilder.jar  -appclass com.oracle.javafx.scenebuilder.app.SceneBuilderApp -name SceneBuilder -title "JavaFX Scene Builder" -description "JavaFX Scene Builder" -vendor "OpenJFX" -Bicon=SceneBuilder.ico -BappVersion=8.0.0 -BmenuHint=true -BshortcutHint=true -Bwin.menuGroup="JavaFX Scene Builder"

この記事は JavaFX Advent Calendar 2014、8 日目の記事です。昨日は @backpaper0 さんの「どうやってApplicationサブクラスの名前取ってきてんの?」でした。

Java SE 8 Update 40 より、JavaFX にダイアログが導入されることになりました。既に Update 40 の Early Access Build のインストーラー配布が公開されていますので、興味のある方はダウンロードして新機能を確かめてみることをお勧めします(ただし Early Access は本番環境への適用が禁止されているため、あくまで個人の範囲で)。

今回ご紹介するサンプルは https://github.com/btnrouge/javafx-dialogs にあります。JDK 8u40 以降でないと実行できないため、実行環境の JDK 8 のバージョンにはくれぐれもご注意ください。

JavaFX 8u40 のダイアログについては、11 月 25 日に開催された JavaFX Night にてセッションを担当しました。導入部分についてはセッション資料をご覧頂くのが一番かと思いますので、以下に掲載します。

JavaFX Night では、時間の都合で十分な量のサンプルを提示できなかったことと、ダイアログのカスタマイズについて「その気になればカスタマイズもできるよ!」のひとことで済ませてしまったため、この記事では主にそれらを中心にお話ししようと思います。

JavaFX 8u40 で導入予定のダイアログは、サードパーティのライブラリ ControlsFXDialog を参考にしていると思われます。その証拠に、画面レイアウトもよく似ています。

1. 2 通りのダイアログ表示方法

JavaFX のダイアログには 2 通りの表示方法があります。JavaFX Night でも取り上げた、Dialog#show() と Dialog#showAndWait() です。どちらもダイアログを表示し入力を待つ動作は同じですが、Dialog#show() はそれ以降に続く処理がすぐに実行されるのに対し、Dialog#showAndWait() はユーザー入力後に続く処理を実行します。

具体例を挙げましょう。まずは Dialog#show() の場合です。

Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("Show");
alert.getDialogPane().setHeaderText("Header Text");
alert.getDialogPane().setContentText("Content Text");
alert.show();
System.out.println("message after alert#show()");

alert.show(); でダイアログを表示し、すぐに次の処理が実行されるため、ユーザーの入力を待つことなくコンソールに "message after alert#show()" と表示されます。

Dialog#show() はユーザー入力を待たないため、ダイアログからの応答を受け取るタイミングが存在せず、従って戻り値も void として宣言されています。

続いて、Dialog#showAndWait() の場合を見てみましょう。

Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("Show and Wait");
alert.getDialogPane().setHeaderText("Header Text");
alert.getDialogPane().setContentText("Content Text");
alert.showAndWait();
System.out.println("message after alert#showAndWait()");

alert.showAndWait(); でダイアログを表示するところまでは同じですが、ユーザーの入力があるまで(ダイアログを閉じるまで)コンソールに "message after alert#showAndWait()" は表示されません。

基本的な使い分けとしては、

  • いわゆる従来型のダイアログとして使用する場合は Dialog#showAndWait()
  • Windows 8 のトースター通知のように処理を止めたくない場合は Dialog#show()

といった形になるでしょうか。

2. DialogPane の使い方

DialogPane は Dialog が保持できる唯一のコンテナーで、Dialog の dialogPane プロパティーを通じて操作することができます。DialogPane にはさらにいくつかのプロパティーが存在し、それらの調整だけでもダイアログのカスタマイズが可能です。

DialogPane のルック・アンド・フィールを制御するプロパティーの一覧を下記に示します。

DialogPane の主なプロパティー
プロパティークラス概要
graphic Node アイコン
headerText String ヘッダー文字列
header Node ヘッダー(headerText より優先)
contentText String メッセージ
content Node コンテンツ(contentText より優先)
expandableContent Node 詳細情報
expanded boolean 詳細情報の表示/非表示

headerText と header の関係、contentText と content の関係については JavaFX Night では触れませんでしたが、headerText と contentText がヘッダーとメッセージをそれぞれテキスト(内部的には Label にラップして header / content に設定)するのに対して、header と content はあらゆる Node ツリーを設定できるため自由なレイアウトが実現できます。header と content はデフォルトでは null となっており、header / content に Node を設定すると headerText / contentText の内容は無視される仕様になっています。多くの場合、凝ったダイアログを必要とするケースは多くないので、headerText と contentText を押さえておけば良いでしょう。

一般にエラーを通知するダイアログでは、初期表示ではエラーの詳細を隠し、必要に応じて詳細を表示する形式が多く見られます。そのようなダイアログを実現するために expandableContent と expanded が用意されています。expandableContent はデフォルトでは null になっています。expandableContent が null でない場合は expanded が有効になり、詳細情報の表示/非表示の制御ができるようになります。

expandableContent の想定される使い方としては、Alert ダイアログの AlertType.ERROR に原因となったエラーの詳細(例外スタックトレースなど)を表示するための領域を追加するような用途です。

なお、Dialog 自体にも headerText / contentText プロパティーが存在しますが、これらは DialogPane の headerText / contentText を呼び出しているだけです。

3. Alert のカスタマイズ

3.1. expandableContent プロパティ

Alert は DialogPane のプロパティを調整することで簡単にカスタマイズすることができます。ここでは expandableContent プロパティを設定して、エラー詳細情報を表示できるようにしてみましょう。

// error message
final String stackTrace = 
    "Exception in thread \"main\" java.lang.UnsupportedOperationException\n"
    + "\tat java.sql.Date.toInstant(Date.java:304)\n"
    + "\tat samples.time.Unsupported.main(Unsupported.java:6)";

Alert alert = new Alert(AlertType.ERROR);

// prepare expandable content
TextArea textArea = new TextArea(stackTrace);
alert.getDialogPane().setExpandableContent(textArea);

alert.setTitle("ERROR");
alert.setHeaderText("Error");
alert.setContentText("An exception was thrown in the application");
Optional result = alert.showAndWait();

ダイアログの実行イメージです。

exception.png

「詳細の表示」をクリックすると、expandableContent プロパティで設定した内容が表示されます。

exception-expanded.png

3.2. header プロパティおよび content プロパティ

headerText や contentText では単純な文字列しか出力できませんでした。しかし、出力したい内容を header または content に設定しておけば、任意の内容をダイアログに表示させることができます。

ここでは Alert の content に TextFlow(リッチテキスト)とイメージを設定したダイアログの例を見てみます。ソースコード中に contentText を設定していますがそれが無視されていることにも注目してください。

Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("Custom content");
alert.setHeaderText("Header Text");
alert.setContentText("Content Text");

// Set content property
Text text1 = new Text("It's content Text ");
Text text2 = new Text("as Rich Text");
text2.setFill(Color.RED);
TextFlow textFlow = new TextFlow(text1, text2);
ImageView imageView = new ImageView(new Image(getClass().getResourceAsStream("someimage.png")));
VBox vbox = new VBox(4.0, textFlow, imageView);
alert.getDialogPane().setContent(vbox);

Optional result = alert.showAndWait();

ダイアログの実行イメージです。

custom-content.png

なお、header の場合も手順は同様です。

3.3. graphic プロパティ

Alert ダイアログのアイコンはコンストラクタ引数(AlertType)によって決まりますが、graphic プロパティを設定することで任意のイメージに差し替えることができます。この方法は AlertType.NONE の場合でアイコンを表示する場合にも用います。

では、アイコンを差し替えてみましょう。以下にソースコードを示します。

Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("Custom icon");
alert.setHeaderText("Header Text");
alert.setContentText("Content Text");

// Set graphic property
ImageView imageView = new ImageView(new Image(getClass().getResourceAsStream("newicon.png")));
imageView.setFitWidth(48.0);
imageView.setFitHeight(48.0);
alert.getDialogPane().setGraphic(imageView);

Optional result = alert.showAndWait();

ダイアログの実行イメージです。

custom-graphic.png

4. TextInputDialog と ChoiceDialog のカスタマイズ

前章で示したように、Alert は比較的簡単にカスタマイズすることができます。しかし、TextInputDialog と ChoiceDialog については content をカスタマイズすることができません(contentText は可能)。これらのダイアログは content にあらかじめレイアウト(GridPane)が設定されており、かつレイアウトの設定タイミングをダイアログ側がコントロールしているため、拡張の余地がないのです。

なお、header プロパティおよび graphic プロパティについては、Alert 同様に設定が可能です。

5. 独自ダイアログの実装

Dialog のサブクラスを独自に作成することで、全く新しいダイアログを作成することもできます。こちらは Dialog クラスのサブクラスを直接作成するため手間はかかりますが、出来合いのダイアログに比べると自由度が高い利点があります。

注意点として、Dialog クラスのメソッドはすべて final 宣言されており、オーバーライドすることができません。ダイアログ内部のイベント取得タイミングを変更するなど、ダイアログの動作そのものを変えてしまうことはできないのです。

当初の予定では、Dialog から直接派生させた独自のダイアログを作成しようと考えていたのですが、ユースケースが思いつかなかったため割愛させていただきます。現在は JavaFX のソースコードも標準で添付されるようになったため、Alert などのソースを参考に各自工夫してみてください。

6. Dialog と Stage の関係

最後に Dialog と Stage の関係についてお話します。

実用上は、Dialog と Stage は似てはいるが別物として扱った方がわかりやすいでしょう。Dialog には Stage にも存在するプロパティーやメソッドをいくつも持っています。新たなウィンドウを開くという点でも同じです。

厳密に言うと、Dialog と Stage は "has-A" の関係にあります。ここから先は Dialog の実装に深く踏み込んだ話になるため、初見の方は読み飛ばしていただいて構いません。

Dialog は下請けクラス FXDialog のインスタンスを保持しています。FXDialog は抽象クラスであり、実際にはサブクラスの HeavyweightDialog のインスタンスとなります。そして HeavyweightDialog が DialogPane を root とする Scene を構築し(dialogPane に null を設定した場合は必ず新規の DialogPane が生成される)、Scene とダイアログの共通属性(リサイズ不可と画面中央への表示)を設定して Stage を作成します。

FXDialog とそのサブクラスは、Stage のメソッドとプロパティのうちダイアログのカスタマイズに利用可能なものを外部に提供しています。Dialog と Stage で共通のプロパティが多いのはこれが理由です。

Dialog は FXDialog(HeavyweightDialog)のコンストラクタに自身のインスタンスを設定し、ダイアログの Stage を生成してもらいます。Dialog のメソッドとプロパティは FXDialog が提供するメソッドとプロパティをほぼそのまま呼び出すだけです。

あまり多くのことを覚えたくなければ、Dialog と Stage は分けて考えた方が分かりやすいでしょう。

7. まとめ

JavaFX 2.x 時代から望まれていたダイアログが、来春には JavaFX 8 に追加されます。仕様策定に予想以上の時間がかかりましたが、まずまずの出来ではないでしょうか。2015 年 3 月には JavaFX 8u40 のリリースと共に標準でダイアログが利用できるようになるため、それまでの間に習熟しておくと良いことがありかもしれません。

最後になりましたが、今回サンプルで提示したソースコードは GitHub https://github.com/btnrouge/javafx-dialogs にホスティングしています。必要に応じて参照してください。


明日は気鋭の学生プログラマー @orekyuu が何か書いてくれると思います。大いに期待しましょう。