JavaFX で Google guice によって FXMLLoader のロードを補助する

JavaFX の Controller (←この呼び方が個人的には嫌い、MVCモデルの Cなんだろうけど)のファクトリの
コールバックに、Google guice でのインジェクションを行う方法は、とても有効な手段だと思う。

前回の投稿、
JavaFX の画面遷移、簡単に書く。 - Oboe吹きプログラマの黙示録

で書いた Controller クラスと fxml 名を指定して画面遷移させるメソッド setPage と書いたものは、以下のように
Google guice でのインジェクトの約束が書ける。。

======
この投稿後、あまり良くないと思って、結局、、
JavaFX のイニシャライズ処理を Google guice で整理 - Oboe吹きプログラマの黙示録

を書くはめになった。
======

/* @see javafx.application.Application#start(javafx.stage.Stage) */
@Override
public void start(Stage primaryStage) throws Exception{
   stage = primaryStage;
   setPage(StartPage.class, "start.fxml");
}
public void setPage(final Class<?> cls, String fxml, Object...params) throws Exception{
   Injector injector = Guice.createInjector(new AbstractModule(){
      @Override
      protected void configure(){
         // TODO bind 定義する
      }
   });
   FXMLLoader loader = new FXMLLoader(cls.getResource(fxml));
   loader.setControllerFactory(new Callback<Class<?>, Object>(){
      @Override
      public Object call(Class<?> type){
         return injector.getInstance(cls);
      }
   });
   Scene scene = new Scene((Parent)loader.load());
   BasePage page = (BasePage)loader.getController();
   page.setApp(this);
   page.loadParameter(params);
   stage.setTitle(WINDOW_TITLE);
   stage.setScene(scene);
   stage.show();
}

FXMLLoader loader の実行は、引数なしの load() の実行になる。
Callback の call メソッドのパラメータ type でなくて、setPage で渡される class を指定する!

Mainクラスの全体は以下のようになる。

import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.util.Callback;
/**
 * MainApp
 */
public class MainApp extends Application{
   static Logger logger = LoggerFactory.getLogger(MainApp.class);
   static String WINDOW_TITLE  = "fxtest2";
   private Stage stage;

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

   /* @see javafx.application.Application#start(javafx.stage.Stage) */
   @Override
   public void start(Stage primaryStage) throws Exception{
      stage = primaryStage;
      setPage(StartPage.class, "start.fxml");
   }
   public void setPage(final Class<?> cls, String fxml, Object...params) throws Exception{
      Injector injector = Guice.createInjector(new AbstractModule(){
         @Override
         protected void configure(){
            // TODO bind 定義する
         }
      });
      FXMLLoader loader = new FXMLLoader(cls.getResource(fxml));
      loader.setControllerFactory(new Callback<Class<?>, Object>(){
         @Override
         public Object call(Class<?> type){
            return injector.getInstance(cls);
         }
      });
      Scene scene = new Scene((Parent)loader.load());
      BasePage page = (BasePage)loader.getController();
      page.setApp(this);
      page.loadParameter(params);
      stage.setTitle(WINDOW_TITLE);
      stage.setScene(scene);
      stage.show();
   }
}

setPage の引数、Object...params 画面遷移時に渡すパラメータ、 page.loadParameter(params);
と書いてはいるが、
guice のインジェクションとして、上の // TODO bind 定義する
で、パラメータ渡しを書く方法もあるだろう。
すると、loadParameter というメソッドは、不要になってくるはずだ。。

JavaFX の画面遷移、簡単に書く。

JavaFX の画面遷移、Webページ閲覧のように切り替え、

いろんなやり方のサンプルを見たけど、どれも釈然としない。もっとシンプルに書けるはずだ。

javafx.application.Application継承の mainメソッドクラス

public class MainApp extends Application{
   static Logger logger = LoggerFactory.getLogger(MainApp.class);
   static int    WINDOW_WIDTH  = 500;
   static int    WINDOW_HEIGHT = 300;
   static String WINDOW_TITLE  = "fxtest";
   private Stage stage;

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

   @Override
   public void start(Stage primaryStage) throws Exception{
      stage = primaryStage;
      setPage(StartPage.class, "start.fxml");
   }

   /* BasePage を継承した Controller は遷移先画面の class と fxml名、任意に渡すパラメータで遷移を実行する */
   public void setPage(Class<?> cls, String fxml, Object...params) throws Exception{
      FXMLLoader loader = new FXMLLoader();
      Scene scene = new Scene((Parent)loader.load(cls.getResourceAsStream(fxml)), WINDOW_WIDTH, WINDOW_HEIGHT);
      BasePage page = (BasePage)loader.getController();
      page.setApp(this);
      page.loadParameter(params);
      stage.setTitle(WINDOW_TITLE);
      stage.setScene(scene);
      stage.show();
   }
}

MainApp の setPage で遷移する画面の抽象クラス

public abstract class BasePage{
   private MainApp application;

   public void setApp(MainApp application){
      this.application = application;
   }

   public void setPage(Class<? extends BasePage> cls, String fxml, Object...params){
      try{
         application.setPage(cls, fxml, params);
      }catch(Exception e){
         e.printStackTrace();
         throw new RuntimeException(e);
      }
   }

   public void loadParameter(Object[] params){
   }
}

これを継承する側で、パラメータを受信したければ、loadParameterをオーバーライドする。

最初に表示する画面、→ fxml ボタンなどの Action に紐つかせて動くようにする nextPage の実行で
、setPage をパラメータ付けて呼ぶ。→ AlphaPage に遷移する。

public class StartPage extends BasePage{

   public void nextPage(){
      setPage(AlphaPage.class, "alpha.fxml", "ABC");
   }
}

遷移先の AlphaPage → loadParameter を Override して受信したパラメータを Label にセットして表示する

public class AlphaPage extends BasePage{

   @FXML private Label status;

   public void backPage(){
      setPage(StartPage.class, "start.fxml");
   }

   @Override
   public void loadParameter(Object[] params){
      status.setText(params[0].toString());
   }
}

fxml ソースの配置

JavaFX の画面デザインレイアウトは一度決定してリリースしたらそんなに変更差し替えるものではなかろう。

Maven で基本アーキタイプ生成のサンプル、

GitHub - javafx-maven-plugin/javafx-basic-archetype: A Maven archetype for generating a basic JavaFX starter project.

これによって作った構造は、以下のようになる。
f:id:posturan:20170304180521j:plain

そして、起動画面での fxml 読込みの部分は、(MainAppクラスでは)

String fxmlFile = "/fxml/hello.fxml";
FXMLLoader loader = new FXMLLoader();
Parent rootNode = (Parent)loader.load(getClass().getResourceAsStream(fxmlFile));

Mavenで生成されてくる。いまいちである。

JavaFX で画面数もそんなに作らないならこれでもいいだろう。でもやはり複数画面になるなら、

Wicket のスタイルのように、画面を構成するクラスと画面を定義するソースを同じ場所に置き、
画面毎のパッケージにしたいものだ。

そこで、以下のように配置換えする。
f:id:posturan:20170304181109j:plain

そして、fxml 読込みも画面のクラスである HelloController から参照して持ってくるように
指定した方が、直感的であるし「画面クラス→画面クラスの fxml 名指定」という命名規則も作りやすい。

FXMLLoader loader = new FXMLLoader();
Parent rootNode = (Parent)loader.load(HelloController.class.getResourceAsStream("hello.fxml"));

コードも見やすいはずだ。


Maven アーキタイプカタログXMLを作ればいいんだろうけど、まだカタログXMLの作り方を習得してない。

JavaFX Mavenサンプル

JavaFXMaven 基本的なアーキタイプを探していて、

GitHub - javafx-maven-plugin/javafx-basic-archetype: A Maven archetype for generating a basic JavaFX starter project.

というのを見つけたので、ここに紹介されたとおり mvn コマンドを以下のように実行して

mvn archetype:generate -DarchetypeGroupId=com.zenjava -DarchetypeArtifactId=javafx-basic-archetype

作成した fxml を JavaFX Scene Builder 2.0 で開こうとすると、

java.io.IOException: javafx.fxml.LoadException: 
   C:/work/sample/src/main/resources/fxml/hello.fxml

at com.oracle.javafx.scenebuilder.kit.fxom.FXOMLoader.load(FXOMLoader.java:92)
at com.oracle.javafx.scenebuilder.kit.fxom.FXOMDocument.<init>(FXOMDocument.java:80)

     :
... 22 more
Caused by: java.lang.ClassNotFoundException: org.tbee.javafx.scene.layout.fxml.MigPane

とエラーなる。つまり標準SDKで配布されてない Pane がデザインコンテナとして使われており

fxml から、

import org.tbee.javafx.scene.layout.fxml.MigPane

の記述を削除して、別の Pane で書き直す。

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<Pane id="rootPane" prefHeight="207.0" prefWidth="322.0" styleClass="main-panel" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8" fx:controller="yip.fxtest.HelloController">
   <children>

       <Label layoutX="25.0" layoutY="45.0" text="First Name:" /> <TextField fx:id="firstNameField" layoutX="112.0" layoutY="73.0" prefColumnCount="30" prefHeight="23.0" prefWidth="201.0" />
       <Label layoutX="25.0" layoutY="77.0" text="Last Name:" />	<TextField fx:id="lastNameField" layoutX="112.0" layoutY="42.0" prefColumnCount="30" prefHeight="23.0" prefWidth="201.0"  />

       <Button layoutX="191.0" layoutY="104.0" onAction="#sayHello" text="Say Hello" />

       <Label fx:id="messageLabel" layoutX="25.0" layoutY="145.0" prefHeight="36.0" prefWidth="274.0" styleClass="hello-message" />
   </children>

</Pane>


pom.xml も以下のように書き直す。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>yip.fxtest</groupId>
<artifactId>fxtest1</artifactId>
<packaging>jar</packaging>
<version>1.0</version>

<properties>
	<slf4j.version>1.7.12</slf4j.version>
	<log4j.version>1.2.17</log4j.version>
</properties>

<dependencies>

	<!-- MigLayout -->
	<dependency>
		<groupId>com.miglayout</groupId>
		<artifactId>miglayout-javafx</artifactId>
		<version>5.0</version>
	</dependency>

	<!-- Apache Commons -->
	<dependency>
		<groupId>commons-lang</groupId>
		<artifactId>commons-lang</artifactId>
		<version>2.6</version>
	</dependency>

	<!-- Logging  -->
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-api</artifactId>
		<version>${slf4j.version}</version>
	</dependency>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>jcl-over-slf4j</artifactId>
		<version>${slf4j.version}</version>
	</dependency>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-log4j12</artifactId>
		<version>${slf4j.version}</version>
	</dependency>
	<dependency>
		<groupId>log4j</groupId>
		<artifactId>log4j</artifactId>
		<version>${log4j.version}</version>
	</dependency>

</dependencies>

<build>
	<finalName>fxtest1</finalName>
	<plugins>
		<plugin>
			<groupId>com.zenjava</groupId>
			<artifactId>javafx-maven-plugin</artifactId>
			<version>8.8.3</version>
			<configuration>
				<mainClass>yip.fxtest.MainApp</mainClass>
			</configuration>
		</plugin>

		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-compiler-plugin</artifactId>
			<configuration>
				<source>1.8</source>
				<target>1.8</target>
			</configuration>
		</plugin>
	</plugins>
</build>

</project>

実行すると以下の画面で、基本的なテキスト入力→ボタンアクション→入力結果を表示という基本的なコードなので
満足する初期段階のソースコードなんだが、標準でない Pane の使用で躓かせてくれる。。

f:id:posturan:20170304141910j:plain

今更、JavaFX の開発環境、

デザインレイアウトの為の fxml を編集するツール、もうバージョンUPされる見込みがなさそうで、何とかORACLEのダウンロードサイトを見つける。

JavaFX Scene Builder 2.0インストール
http://www.oracle.com/technetwork/java/javafxscenebuilder-1x-archive-2199384.html

 

それから Eclipse 環境では、プラグイン e(fx)clipse を入れる。

f:id:posturan:20170304134747j:plain

 

この e(fx)clipse でプロジェクト作成では、以下の様に作成できるけど、、

f:id:posturan:20170304134908j:plain

でも、 Maven を使った開発にするので、このようにプロジェクト生成しない。

他、いろいろ開発環境見たけど皆、癖があって嫌だ。

 

 

 

Wicket 8 の websocket

2017年3月3日時点、Wicket 8.0.0-M4 の Websocket は、残念なことに、Tomcat8用の WebFilter が作られていない。

wicket-native-websocket-tomcat/8.0.0-M3 は Central Repository にあっても
wicket-native-websocket-tomcat/8.0.0-M4 は、まだ公開されていない。

Tomcat 8.5.11 を使いたければ、Wicket の native-Websocket は諦めて、@ServerEndpoint アノテーションによる javax.websocket
を使用した方が良いみたい。

  <dependency>
      <groupId>org.apache.tomcat</groupId>
      <artifactId>tomcat-websocket</artifactId>
      <version>8.5.11</version>
  </dependency>

問題は、Wicket を使う場合の web.xml の WebFilter 設定で /* を指定するケースで、如何に @ServerEndpoint の
url とぶつからず済むかである。

→ 2017-6-25
oboe2uran.hatenablog.com

WebSocket の getOpenSessions は現在開いてるのを返す約束なのに。。

WebSocket の javax.websocket.Session の getOpenSessions は、現在開いてるセッションを全て返してくれるはずなのに、
サンプルを書いていて、どうも endpoint のセッションしか返してこない。。なぜ?

Tomcat8.5.11 で試していた。。

やりたかったのは、こんな感じ、、

@OnMessage
public void onMessage(Session session, String message){
   try{
      session.getOpenSessions().stream().forEach(s->s.getAsyncRemote().sendText(message));
   }catch(Exception e){
      // TODO
   }
}

しかたなく安全策で、、

import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/broadcast")
public class BroadcastSample{

   static Set<Session> sessionList = new CopyOnWriteArraySet<>();

   @OnOpen
   public void open(Session session){
      sessionList.add(session);
   }

   @OnMessage
   public void onMessage(Session session, String message){
      try{
         sessionList.stream().filter(s->s.isOpen()).forEach(s->{
            s.getAsyncRemote().sendText(message);
         });
      }catch(Exception e){
         // TODO
      }
   }

   @OnClose
   public void close( Session session){
      sessionList.remove(session);
   }

   @OnError
   public void error(Session session, Throwable cause){
   }
}