昨日はJavaのライブラリを利用しSocketの受信機能を作りました。
(今のところ、指定ポートのソケットでデータを受信したかどうか見てるだけですが)
今日は送受信機能を完成させたい所存。
JavaFXのコントロールにイベントハンドラを紐付ける
イベントハンドラの紐付けに必要な処理は以下のステップで行います。
- FXMLに紐付けるコントローラクラスを作成する。
- FXMLとコントローラクラスを紐付ける。
- FXMLのコントロールのイベントとコントローラクラスのメソッドを紐付ける。
参考:Oracle - JavaFX: JavaFXスタート・ガイド 6 FXMLを使用したユーザー・インタフェースの作成
(日本語)
FXMLに紐付けるコントローラクラスを作成する。
これ、特に制限ないっぽいです。
どんなクラスでも設定できるようなので、とりあえず
[送信側ウィンドウをLAN内通信アプリを作ろう その2で作成したMainStage.javaを対象クラスにしてみます。
正直この実装微妙っぽい。
画面を出す処理と、画面の動作を規定する処理は分けるべきかも。
FXMLとコントローラクラスを紐付ける。
Formと異なり、JavaFXでは画面レイアウト/コントロールの情報はXMLという形で保存されているため、このままでは動作と紐付けることが出来ません。
解決方法はシンプルで、FXMLに動作を紐付けるクラスを記述してやればOK。
ってことで早速。
- SceneBuilderを利用する方法
- FXMLファイルを直接変更する方法
の二通りのやり方があるようなのでそれぞれ記載。
SceneBuilderを利用する方法
これ、ものすごく簡単。
- SceneBuilderを起動して、紐付けを行いたいFXMLファイルを開く。
- 画面左下の[Document]アコーディオンの下部にある[Controller]タブをクリックし、表示させる。
- [Controller class]テキストボックスに紐付けたいクラスを入力する。
([▼]ボタンを押下すると、候補のクラスを表示してくれる)
FXMLファイルを直接変更する方法
こっちも簡単。
- rootとなるコンテナ(今回はGridPane)にコントロールクラスを示すパラメータfx:controllerを追記するだけ。具体的には
fx:controller = "紐付けたいコントロールクラスのクラス名(パッケージ名込み)"
具体的にはこんな感じ
<GridPane minHeight="-Infinity" minWidth="-Infinity" prefHeight="200.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="kugui.owd.privateMessenger.stage.MainStage">
FXMLのコントロールのイベントとコントローラクラスのメソッドを紐付ける。
クラスを紐づけたら今度はコントロールの各種イベントと、コントローラクラスのメソッドを紐付け。
こちらはメソッドに以下の制限がある模様。
- メソッド宣言時、アクセスレベルがpublicで無い場合は"@FXML"アノテーションを記述する必要がある。
- 紐付けるイベント毎に適切な型である必要がある。
[Oracle - Javafxドキュメンテーション パッケージ Event (日本語)]https://docs.oracle.com/javase/jp/8/javafx/api/javafx/event/package-summary.html
)
Oracle - Javafxドキュメンテーション クラスEvent (日本語)
今回はこんなメソッドを準備してみました。
@FXML
private void btnSend_Click(ActionEvent event) {
System.out.println( "btnSend is clicked!");
return;
}
こちらも設定方法を二通り記載。
- SceneBuilderを利用する方法
- FXMLファイルを直接変更する方法
SceneBuilderを利用する方法
これもシンプル。
-
SceneBuilderを起動して、紐付けを行いたいFXMLファイルを開く。
-
紐付けたいメソッドを関連するイベントのテキストボックスに入力(今回はOnActionイベント。Clickイベントに相当する)
([▼]ボタンを押下すると、候補のメソッドを表示してくれる)
FXMLファイルを直接変更する方法
これもシンp(ry
- 該当するコントロールに<<該当するイベント名(今回はonAction)>> = "#<<紐付けたいメソッド名(クラス、括弧不要。頭の#は必要)>>"と追記する。
具体的にはこんな感じ
<Button mnemonicParsing="false" onAction="#btnSend_Click" text="Send" GridPane.halignment="CENTER" GridPane.rowIndex="2" GridPane.valignment="CENTER" />
イベントハンドラ 実装結果確認
ここまで出来たら紐付けが出来たか確認。
テキストボックスの中身を取得する
後はメッセージの内容と送信先の情報をとってソケットを送信すれば送信機能はOK。
ってことで、JavaFXで作ったGUIのテキストボックスから内容を取得しましょ。
手順は以下のとおり
- 触りたいJavaFxのコントロールにidを設定する。
- IDを設定したコントロールと同一の型、名前を持つオブジェクトをクラス内で宣言する。
(publicにしない場合。@FXMLアノテーションをつける)
触りたいJavaFxのコントロールにidを設定する。
FXMLとjavaソースの間で整合性を取るためにidを設定しなきゃならない。
idの設定方法も2通り記述。
- SceneBuilderからIDを設定する場合
- FXMLファイルを直接書き換える場合
SceneBuilderからIDを設定する場合
シン(ry
- SceneBuilderを起動して、紐付けを行いたいFXMLファイルを開く。
- イベントを紐付けたいコントローラを選択状態にする、(今回はSendToテキストフィールド)
- [Inspector アコーディオン]-[Codeタブ]をクリック
- [Identity]の項目のfx:idテキストボックスの中身にid(オブジェクト名)を入力
ほとんど上のイベントハンドラの部分と同じなので写真はちょっと省略。
FXMLファイルを直接書き換える場合
- 該当のコントロールにfx:id属性の記述を、値を変数名として追記する。
具体的にはこんな感じ
<TextArea fx:id="txaMessage" prefHeight="200.0" prefWidth="200.0" promptText="Input send message..." GridPane.rowIndex="1" />
IDを設定したコントロールと同一の型、名前を持つオブジェクトをクラス内で宣言する。
クラスの中で宣言するだけなんだけど、具体的にはこんな感じ。
追加したインポート:import javafx.scene.control.*;
public class MainStage extends Application {
@FXML private TextArea txaMessage;
@FXML private TextField txfSendTo;
(後略)
取得結果の確認
あて先となるIPアドレスを入力するTextField:txfSendTo(javafx.control.TextField)と、メッセージを入力するTextArea:txaMessage(javafx.control.TextArea)の取得結果をprintlnで出してみる。
ボタン押下時のイベントハンドラをこんな感じに改変
@FXML
private void btnSend_Click(ActionEvent event) {
System.out.println( "btnSend is clicked!");
System.out.println( String.format("SendTo : %s ", txfSendTo.getText() ));
System.out.println( String.format("Message : %s ", txaMessage.getText() ));
return;
}
以下結果。
これでGUIからの情報はOK。
次行きましょ。
ソケット通信によるメッセージ送受信機能を作成する
前の記事と同じ感じでやっていきましょ。
今回送信側のポートは特に指定しないことにします。
特にこだわりないしね。
ってことでサクッと新規に送信用クラスを作りましょ。
送信用クラスを実装。
処理の流れはこんな感じ
- ソケット作る
- 送信先とTCPコネクションを確立する (clientSocket.connect()のところ)
- DataOutputStreamを介して送信ポートにメッセージを送る(今回は文字列"TestMessage")
- ソケットを閉じる。
targetIPについてはsetTargetIPメソッドで設定するように設定。
package kugui.owd.privateMessenger;
import java.io.*;
import java.net.*;
public class TxThread extends Thread {
private String targetIP;
public void run() {
int port = 80;
int timeout = 10000;
Socket clientSocket = new Socket();
try {
clientSocket.connect(new InetSocketAddress(targetIP, port), timeout);
DataOutputStream dos = new DataOutputStream(clientSocket.getOutputStream());
dos.writeUTF("TestMessage");
dos.close();
}catch(IOException ie) {
ie.printStackTrace();
}
try {
clientSocket.close();
}catch(IOException ie) {
ie.printStackTrace();
}
return;
}
public void setTargetIP(String pIP) {
targetIP = pIP;
return;
}
}
バレてるかもしれませんが、前の記事で作ったRxThread.javaの流用だったりします。
動作確認
では実際に指定したIPアドレスにデータが遅れるか見てみましょ。
前回の状態だと「指定したポートにアクセスがあったか?」くらいしか見てなかったので
受信処理を少し書き足してみました。
以下の機能が追加されてます。
- 一定時間ごとにポートの監視をタイムアウトし、他のスレッドからinterrupt()されていた場合、本スレッドを終了させる。
- 受信したデータをコンソールに出力する。
package kugui.owd.privateMessenger;
import java.io.*;
import java.net.*;
public class RxThread extends Thread {
public void run() {
int port = 80; // test用。
int timeout = 1000;
ServerSocket sSocket;
try {
sSocket = new ServerSocket(port);
sSocket.setSoTimeout(timeout);
System.out.println(String.format("start listening port %d", port));
while (true) {
try {
int length = 0;
DataInputStream dis = new DataInputStream(sSocket.accept().getInputStream());
System.out.println(String.format("IP %d port %d : socket accept", sSocket.getInetAddress().toString() ,port));
while(true) {
byte[] buffer = new byte[1024];
length = dis.read(buffer);
if( length > 0 ) {
System.out.print(new String(buffer));
}else {
break;
}
}
dis.close();
} catch (SocketTimeoutException se) {
if (interrupted() == true) {
break;
} else {
/* no operation */
}
}
}
} catch (IOException ioE) {
ioE.printStackTrace();
}
System.out.println("finished");
return;
}
}
この状態でブラウザからlocalhostに接続するとこんな風になります。
ちゃんとHTTPリクエストの中身が表示されてるのが確認できました。
次はSendToテキストフィールドにlocalhostを入力し、送信ボタンを押下するとこんな結果に。
メッセージの受け取りまでは出来ている様子です。
ただし、たまにメッセージが途切れる問題が発生しています。
まあ、現行だと受信ソケットから情報引き出せない場合問答無用で読み取り終わらせてますからね。
ってことで、少し休んだら残りの作業
- メッセージ部分の受信
- 受信したメッセージの表示(受信ウィンドウの作成)
- 受信処理の改修
をやっていくことにします。
一回区切りましょうかね。今回の作業時間は05:20:20でした。