質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.62%

JavaFXの画面遷移について

解決済

回答 1

投稿

  • 評価
  • クリップ 1
  • VIEW 2,113

yogurt

score 12

前提・実現したいこと

JavaFXでサーバー側とクライアント側に分かれたチャットソフトを作っています
クライアント側からユーザー情報を入力(サインイン画面)→サーバー側が受信して応答→クライアント側の画面遷移(チャット画面)
といったようなことをやりたいのですが、実行時エラーが出てしまい、どのように対処したらいいのかわかりません…

自力でもJavaFXの画面遷移についての色々なWebページを見て解決を試みたのですがわかりませんでした…

どの点が悪いのか教えていただけないでしょうか…

画面遷移を実装中に以下のエラーメッセージが発生しました。

発生している問題・エラーメッセージ

文字数制限にかかってしまったのでリンクを貼ります!
http://plsk.net/JavafxErrorMessage

Main.java

package sample;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
import java.io.*;
import java.net.Socket;

public class Main extends Application implements Runnable {
    @FXML TextField HostField;
    @FXML TextField PortField;
    @FXML TextField IdField;
    @FXML Label StatusField;
    @FXML PasswordField passwordField;
    Stage SignStage;

    @Override
    public void start(Stage Stage) throws Exception {
        this.SignStage = new Stage();
        StageChange("UserSign.fxml", "Sign in");
        this.SignStage.setResizable(false);
    }
    @Override
    public void stop() {
        if(socket == null) return;
        try {
            close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //サインインボタンのイベント定義
    @FXML
    private void Sign_in(ActionEvent a) {
        //入力フォームに対する空白の禁止
        if(HostField.getText().equals("") || PortField.getText().equals("") || IdField.getText().equals("") || passwordField.getText().equals("")) {
            StatusField.setText("入力項目に空欄が存在します");
            return;
        }
        this.HOST = HostField.getText();
        this.PORT = Integer.valueOf(PortField.getText());
        if(connectServer()) {
            System.out.println("サーバー接続完了");
            StageChange("test.fxml", "Test");
            //((Node)a.getSource()).getScene().getWindow().hide();
        }
        passwordField.setText("");
        IdField.setText("");
    }
    public static void main(String[] args) {
        launch(args);
    }
    //接続先サーバーのホスト名
    private static String HOST;
    //接続先ポート番号
    private static int PORT;
    //このアプリケーションのクライアントソケット
    private Socket socket;
    //メッセージ受信監視用スレッド
    private Thread thread;

    private Parent root;

    public Main() {
    }
    public void ThreadCreate() {
        //メッセージ受信用に
        System.out.println("Main開始");
        thread = new Thread(this);
        System.out.println("スレッド作成完了");
        thread.start();
        System.out.println("スレッドスタート");
    }
     //サーバーに接続する
     public boolean connectServer() {
        try {
            socket = new Socket(HOST, PORT);
            sendMessage("sign " + passwordField.getText() + " " + IdField.getText());
            ThreadCreate();
            return true;
        } catch(Exception err) {
            socket = null;
            StatusField.setText("サーバーに接続不能な状況か、\n" +
                    "ホスト名かポート番号が間違っています");
        }
        return false;
     }
    //サーバーから切断する
    public void close() throws IOException {
        sendMessage("close");
        socket.close();
    }
    //メッセージをサーバーに送信する
    public void sendMessage(String msg) {
        try {
            OutputStream output = socket.getOutputStream();
            PrintWriter writer = new PrintWriter(output);
            System.out.println(msg);
            writer.println(msg);
            writer.flush();
        }
        catch(Exception err) {
        //    msgTextArea.append("ERROR>" + err + "\n");
        }
    }
    //メッセージ監視用のスレッド
    public void run() {
        try {
            InputStream input = socket.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(input));
            while(!socket.isClosed()) {
                System.out.println("while開始");
                String line = reader.readLine();
                System.out.println("受信完了");
                String[] msg = line.split(" ", 2);
                System.out.println("分割完了");
                String msgName = msg[0];
                String msgValue = (msg.length < 2 ? "" : msg[1]);
                reachedMessage(msgName, msgValue);
                System.out.println("メッセージ処理開始");
            }
        }
        catch(Exception err) { }
    }
    //サーバーから送られてきたメッセージの処理
    public void reachedMessage(String name, String value) {
        //チャットルームのリストに変更が加えられた
        if (name.equals("rooms")) {
            if (value.equals("")) {
            }
            else {
                String[] rooms = value.split(" ");
            }
        }
        //ユーザーが入退室した
        else if (name.equals("users")) {
            if (value.equals("")) {
            }
            else {
                String[] users = value.split(" ");
            }
        }
        //メッセージが送られてきた
        else if (name.equals("msg")) {
        }
        //処理に成功した
        else if (name.equals("successful")) {
            if (value.equals("setName")) {
            } else if(value.equals("sign")) {
                System.out.println("ログイン要求が承認されました");
            }
        }
        else if (name.equals("failed")) {
            if (value.equals("sign")) {
                try {
                    close();
                } catch (IOException err) {
                    err.printStackTrace();
                }
            }
        }
        //エラーが発生した
        else if (name.equals("error")) {
        }
    }
    public void StageChange(String fxml, String title) {
        try {
            root = FXMLLoader.load(getClass().getResource(fxml));
            System.out.println("リソース遷移");
            this.SignStage.setScene(new Scene(root));
            System.out.println("画面遷移");
            this.SignStage.setTitle(title);
            System.out.println("タイトル遷移");
            this.SignStage.show();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

UserSign.fxml

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

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<?import javafx.scene.text.Text?>

<GridPane alignment="center" hgap="10.0" maxHeight="-Infinity" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="-Infinity" prefHeight="279.0" prefWidth="280.0" vgap="15.0" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Main">
   <padding><Insets bottom="5.0" left="25.0" right="25.0" top="5.0" /></padding>
   <Text text="Welcome!" GridPane.columnSpan="2" />
   <Label fx:id="StatusField" prefHeight="38.0" prefWidth="145.0" textFill="#e10000" GridPane.columnIndex="1">
      <font>
         <Font size="9.0" />
      </font></Label>
   <Label prefHeight="17.0" prefWidth="66.0" text="HostName :" GridPane.rowIndex="1" />
   <TextField fx:id="HostField" prefHeight="25.0" prefWidth="149.0" GridPane.columnIndex="1" GridPane.rowIndex="1" />

   <Label prefHeight="17.0" prefWidth="75.0" text="PortNumber :" GridPane.rowIndex="2" />
   <TextField fx:id="PortField" prefHeight="25.0" prefWidth="149.0" GridPane.columnIndex="1" GridPane.rowIndex="2" />


   <Label prefHeight="9.0" prefWidth="45.0" text="User ID :" GridPane.rowIndex="4" />
   <TextField fx:id="IdField" prefHeight="25.0" prefWidth="149.0" GridPane.columnIndex="1" GridPane.rowIndex="4" />

   <Label text="Password:" GridPane.rowIndex="5" />
   <PasswordField fx:id="passwordField" GridPane.columnIndex="1" GridPane.rowIndex="5" />
   <HBox alignment="bottom_right" spacing="10" GridPane.columnIndex="1" GridPane.rowIndex="6">
      <Button fx:id="Sign_Start" onAction="#Sign_in" text="Sign In" />
   </HBox>

   <rowConstraints>
      <RowConstraints maxHeight="34.0" minHeight="13.0" prefHeight="26.0" />
      <RowConstraints maxHeight="0.0" minHeight="0.0" prefHeight="0.0" />
   </rowConstraints>
   <columnConstraints>
   </columnConstraints>
</GridPane>

test.fxml

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

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

<AnchorPane xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="sample.Main"
            prefHeight="400.0" prefWidth="600.0">

</AnchorPane>

補足情報(言語/FW/ツール等のバージョンなど)

Java SE8・IntelliJ IDEA Community Edition 2017.1.5 x64

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

直接的な例外の内容と発生個所

スタックトレースからMain.javaの188行目でNPE(NullPointerException)が発生していることはお判りでしょうか?閲覧者はスタックトレースを見るとMain.javaの188行目でNPEが発生していることはすぐに分かるのですが、コード上のどの行が188行目かは分からない点を認識しておいてください。推察は可能ですが確認するためにはコードを実行して間違いなさそうかを確認せねばなりませんので、多くの閲覧者は例外が発生している行が書いてないと回答が面倒になってしまうと思います。ゆえに質問文には188行目がどの行かを最初から明示しておくべきと思います。

さて、おそらく188行目は次の行と思います。

public void StageChange(String fxml, String title) {
    ...
    this.SignStage.setScene(new Scene(root)); //おそらくこの行が188行目
    ...
}

NPEが発生する原因

NPEが発生している直接的な原因はSignStageがnullであるためです。おそらく質問者さんはFXMLLoader#loadはFXML上に記述されたコントローラークラスのインスタンスを「自動的に生成する」という点を把握しておられないように思います。ひょっとするとクラスを定義しているが、インスタンスがいつ生成されるかを意識しておられないのかも知れません。実際には次のようにNPE発生時までにMainのインスタンスが3つ生成されます。

(1) 最初のインスタンス
JavaFXアプリケーション開始時にApplication#launchにてアプリケーションクラスのインスタンスとして生成されます。

(2) 2つ目のインスタンス
StageChangeの最初の呼び出しでUserSign.fxmlをロードする際にコントローラークラスとしてFXMLLoader#loadがインスタンスを生成します。StageChangeメソッドの最初の呼び出しではthisは(1)で生成されたインスタンスなのでSignStageはstartメソッドで設定された値が用いられ最初の画面は出ます。

(3) 3つ目のインスタンス
UserSign.fxmlの画面でSign_Startボタンのハンドラー内からのStageChangeの呼び出し時に(2)と同様に生成されます。

さて(3)のStageChangeの2回目の呼び出しの際にはthisは(2)で生成されたUserSign.fxmlのコントローラーとしてのインスタンスでありSignStageは何も設定されてないのでnullになっています。そのためNPEが発生します。

対処

質問者さんはアプリケーションインスタンスと異なる2つの画面のコントローラーを全て単一のクラス・インスタンスとして扱おうとしているように見えます。個人的な意見としては、そのような実装は平易な設計ではないと思います。(なぜ平易でないかは回答が長くなりすぎるので省略。)よってアプリケーションクラス、個々の画面用のコントローラークラスはそれぞれ独立なクラスとして定義することをお勧めします。個別のクラスとして定義するとインスタンスも当然別々になりますのでなんらかの方法でコントローラーインスタンス間で情報のやりとりをする必要があります。

様々な方法でそれができると思いますが「こうするのが一番よい」というのが回答しにくいので質問者さんのコードの雰囲気から「アプリケーションクラスに画面遷移用の共通のメソッドを用意する」「Stageは常に単一のものを使う」という前提で考えてみました。なお、以下の点にご注意ください。

  • クラス構成そのものが変わるので元のコードとはクラス名などかなり変わっている
  • Stageインスタンスはstartメソッドへ渡されるものをそのまま使うようにした
  • 画面の遷移についてのみ実装
  • import文は省略。ソースは全て同じパッケージ(transition)にあると仮定しています
// transition.MyApp.java (アプリケーションクラス)
package transition;
...
public class MyApp extends Application {
  private static MyApp theApp;
  private Stage primaryStage;

  @Override public void start(Stage primaryStage) throws Exception {
    theApp = this;
    this.primaryStage = primaryStage;
    this.primaryStage = new Stage();
    transitionTo("Login.fxml", (stage, loginController) -> {
      stage.setTitle("Login");
    });
  }

  // ロードしてからでないとコントローラーインスタンスが決まらないため、
  // 呼び出し元で個別処理をするためにコールバックを引数へ渡すようにしてみました
  public static <T extends Initializable> void transitionTo(String fxmlFilename, BiConsumer<Stage, T> callback) {
    try {
      FXMLLoader loader = new FXMLLoader(MyApp.class.getResource(fxmlFilename));
      Parent root = loader.load();
      T controller = loader.getController();
      Stage stage = theApp.primaryStage;
      stage.setScene(new Scene(root));
      callback.accept(stage, controller);
      stage.show();
    } catch (Exception e) {
      e.printStackTrace();
      Platform.exit();
    }
  }
}

// transition.Login.java (ログイン画面のコントローラークラス)
package transition;
...
public class LoginController implements Initializable {
  @FXML private TextField userText;
  @FXML private Button loginButton;

  @Override public void initialize(URL location, ResourceBundle resources) {
    loginButton.disableProperty().bind(userText.textProperty().isEmpty());
  }

  public void setDefaultUser(String user) {
    userText.setText(user);
  }

  @FXML private void onLogin() {
    MyApp.transitionTo("Session.fxml", (Stage stage, SessionController sessionController) -> {
      sessionController.setUser(userText.getText());
      stage.setTitle("Session");
    });
  }
}

// transition.Session.java (チャット画面のコントローラークラス)
package transition;
...
public class SessionController implements Initializable {
  @FXML private Label userLabel;
  @Override public void initialize(URL location, ResourceBundle resources) {}

  public void setUser(String user) {
    userLabel.setText(user);
  }

  @FXML private void onLogout() {
    MyApp.transitionTo("Login.fxml", (Stage stage, LoginController loginController) -> {
      stage.setTitle("Login again");
      loginController.setDefaultUser(userLabel.getText());
    });
  }
}
<!-- Login.fxml -->
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.VBox?>

<VBox xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1" fx:controller="transition.LoginController">
  <TextField fx:id="userText" />
  <Button fx:id="loginButton" mnemonicParsing="false" onAction="#onLogin" prefHeight="100.0" prefWidth="300.0" text="Login" />
</VBox>
<!-- Session.fxml -->
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.BorderPane?>

<BorderPane prefHeight="200.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1" fx:controller="transition.SessionController">
  <top>
    <Label fx:id="userLabel" BorderPane.alignment="CENTER" />
  </top>
  <bottom>
    <Button mnemonicParsing="false" onAction="#onLogout" text="Logout" BorderPane.alignment="CENTER" />
  </bottom>
  <center>
    <TextArea prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER" />
  </center>
</BorderPane>

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2017/07/25 13:08

    例外発生の行の件、申し訳ありませんでした!
    そして迅速で丁寧な回答を本当にどうもありがとうございます!
    まさかここまで詳細に書いてくださるとは思いませんでした…

    まだまだコントローラークラスについての理解が足りていないと痛感しました

    また、一つのクラスで様々なことを記述してしまうと、どこの処理がどの画面に対応しているのか、などがわかりづらくて仕方ありませんね…

    KSwordOfHaste様が書いてくださったソースコードを参考に画面遷移を実装していきたいと思います!

    そして、NPEの原因について一つひとつ順を追って説明して頂いたこと本当に感謝してます!

    ありがとうございました!

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.62%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る