最終更新日:2020/07/21 原本2017-07-24

前提・実現したいこと

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

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>