takeda_san’s blog

JavaFXと機械学習を頑張る方向。

テーブル内のチェックボックスをクリックしたときに行を選択したことにしたい

おのれ、チェックボックス

こんな画面、ギョームアプリケーションを作ったことがある方なら一度はあると思います。
チェックボックスを選択して、その行に対して操作を行うみたいなやつ。

f:id:takeda_san:20170725225412p:plain

JavaFXでは、CheckBoxTableCellというチェックボックス入りのテーブルセルクラスが標準であるのでそれを使います。
標準であるのはありがたいんですが、このクラス、選択時にテーブルのOnMouseClickedが動かないし、行を選択したことにもならないのよね。

普通ならテーブルの行をクリックすると、こんな感じで青く色が反転して選択状態になるんだけど。

f:id:takeda_san:20170725225943p:plain

チェックボックスをクリックすると

f:id:takeda_san:20170725230044p:plain

行は選択状態にならんのかい… ちなみに、テーブル側でOnMouseClickedを設定しておいても動きません。

チェックボックスを押したときでも、行を選択状態にしたい!!!
というわけで、(ムリヤリ)挙動を変えて使いやすくしましょう。

プログラム

今回もみんな大好きOracleのサンプルプログラムをすこし改変して使います。

docs.oracle.com

また、プログラム全文のせちゃいます。

メインクラス

package application;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;

public class Main extends Application {
    private TableView<Person> table = new TableView<>();
    private ObservableList<Person> data = FXCollections.observableArrayList(
            new Person(false, "Jacob", "Smith", "jacob.smith@example.com"),
            new Person(false, "Isabella", "Johnson", "isabella.johnson@example.com"),
            new Person(false, "Ethan", "Williams", "ethan.williams@example.com"),
            new Person(false, "Emma", "Jones", "emma.jones@example.com"),
            new Person(false, "Michael", "Brown", "michael.brown@example.com"));

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

    @Override
    public void start(Stage stage) {
        Scene scene = new Scene(new Group());
        stage.setTitle("Table View Sample");
        stage.setWidth(515);
        stage.setHeight(500);

        final Label label = new Label("Address Book");
        label.setFont(new Font("Arial", 20));

        CheckBoxColumn checkBoxCol = new CheckBoxColumn();
        checkBoxCol.setMinWidth(50);

        TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
        firstNameCol.setMinWidth(100);
        firstNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));

        TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
        lastNameCol.setMinWidth(100);
        lastNameCol.setCellValueFactory(new PropertyValueFactory<>("lastName"));

        TableColumn<Person, String> emailCol = new TableColumn<>("Email");
        emailCol.setMinWidth(200);
        emailCol.setCellValueFactory(new PropertyValueFactory<>("email"));

        // なぜかテーブル全体をeditableにしないとチェックボックスが押せない…
        table.setEditable(true);

        // マウスクリック時に動作する
        table.setOnMouseClicked(event -> {
            System.out.println("行が選択されたぜ!!");
        });

        table.setItems(data);
        table.getColumns().add(checkBoxCol);
        table.getColumns().add(firstNameCol);
        table.getColumns().add(lastNameCol);
        table.getColumns().add(emailCol);

        final VBox vbox = new VBox();
        vbox.setSpacing(5);
        vbox.setPadding(new Insets(10, 0, 0, 10));
        vbox.getChildren().addAll(label, table);

        ((Group) scene.getRoot()).getChildren().addAll(vbox);

        stage.setScene(scene);
        stage.show();
    }

}

チェックボックスカラムのクラス

package application;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.event.Event;
import javafx.scene.control.TableColumn;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;

public class CheckBoxColumn extends TableColumn<Person, Boolean> {

    public CheckBoxColumn() {
        // Personのcheckedプロパティと紐づける
        this.setCellValueFactory(new PropertyValueFactory<>("checked"));

        this.setCellFactory(column -> {

            // CheckBoxTableCellの挙動を定義する
            CheckBoxTableCell<Person, Boolean> cell = new CheckBoxTableCell<Person, Boolean>(index -> {
                BooleanProperty selected = new SimpleBooleanProperty(
                        this.getTableView().getItems().get(index).isChecked());

                selected.addListener((ov, o, n) -> {
                    // チェックボックスの状態が変わったらPersonのデータも更新する
                    this.getTableView().getItems().get(index).setChecked(n);

                    // チェックボックスが押されたので、行を選択したことにする
                    this.getTableView().getSelectionModel().select(index);

                    // テーブルにクリックイベントを飛ばす
                    Event.fireEvent(column.getTableView(), new MouseEvent(MouseEvent.MOUSE_CLICKED, 0, 0, 0, 0,
                            MouseButton.PRIMARY, 1, true, true, true, true, true, true, true, true, true, true, null));
                });

                return selected;
            });

            return cell;
        });
    }
}

テーブルに表示するモデルのクラス

package application;

import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;

public class Person {

    private SimpleBooleanProperty checked;
    private SimpleStringProperty firstName;
    private SimpleStringProperty lastName;
    private SimpleStringProperty email;

    public Person(boolean checked, String fName, String lName, String email) {
        this.checked = new SimpleBooleanProperty(checked);
        this.firstName = new SimpleStringProperty(fName);
        this.lastName = new SimpleStringProperty(lName);
        this.email = new SimpleStringProperty(email);
    }

    public SimpleBooleanProperty checkedProperty() {
        return checked;
    }

    public boolean isChecked() {
        return checked.get();
    }

    public void setChecked(boolean checked) {
        this.checked.set(checked);
    }

    public String getFirstName() {
        return firstName.get();
    }

    public void setFirstName(String fName) {
        firstName.set(fName);
    }

    public String getLastName() {
        return lastName.get();
    }

    public void setLastName(String fName) {
        lastName.set(fName);
    }

    public String getEmail() {
        return email.get();
    }

    public void setEmail(String fName) {
        email.set(fName);
    }
}

長々とプログラムを載せましたが、大事なのはチェックボックスカラムクラスのここだけ。

// チェックボックスが押されたので、行を選択したことにする
this.getTableView().getSelectionModel().select(index);

// テーブルにクリックイベントを飛ばす
Event.fireEvent(column.getTableView(), new MouseEvent(MouseEvent.MOUSE_CLICKED, 0, 0, 0, 0,
        MouseButton.PRIMARY, 1, true, true, true, true, true, true, true, true, true, true, null));

まずは、上の一行のほうから。
テーブルではSelectionModelというクラスを使って、任意に行の選択状態を変更したり、選択されている行のモデルデータを取得することができます。
これが、地味に便利。
今回は、チェックボックスの選択状態が変更されたセルの行番号indexを使って、それと同じ行を選択したことにしています。

次に、見るからに脳みそ筋肉な次の実装。
これは、第一引数のノードで第二引数のイベントを発生させるという処理を書いています。
テーブルにイベントが飛ばないならば、自分でイベントを生成して飛ばしてしまおうというわけです。

実行結果

f:id:takeda_san:20170725232446p:plain

行が選択されたぜ!!

にしても、テーブル周りの実装が、私には高度すぎて時間がかかる…
androidとかやっている人なら、もうちょっとセンス良く書けるのかしら。

// なぜかテーブル全体をeditableにしないとチェックボックスが押せない…
table.setEditable(true);

これな…

Windowsのコマンドプロンプトの代わりにcmderをつかう

私、Windowsを家でも仕事場でも好んで使っているのですが、コマンドプロンプトの使いにくさに少々困っておりまして、
代替のものはないかなぁと、いろいろ試してみた中で個人的に使いやすいものを見つけたのでメモ。

cmderというもの。
何よりハーフライフ的なロゴが気に入りました。

cmder.net

画面はこんな感じ。フォントも読みやすくて適度に色がついてます。

f:id:takeda_san:20170713224724p:plain

初めから使いやすいようにされてて素敵。
エディタでも何でもあまりカスタマイズして使うという習慣がないので、デフォルトで使いやすいのが良いです。
(世の皆様から、怒られそう)

タブで複数切り替え可能。
gitコマンドもsshも使えます。

というので、ひと記事終わるのも寂しいのでPythonインタプリタを使えるまで、やったこともメモしておきます。

Pythonをやっていく気持ち

今読んでる本だと、Anacondaディストリビューションが実際便利らしいのでインストール。 バージョン4.4.0をデフォルトでインストールしました。

Download Anaconda Now! | Continuum

cmderを立ち上げて「python –version」っと。

λ python  --version
'python' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

はいはい、環境変数ね。
インストール時に環境変数を設定するのはやめて、スタートメニューから選べるAnaconda Promptから起動しよう。な!
と、言われていた気がするので言われたとおりにしてみる。

f:id:takeda_san:20170713230922p:plain

うん、できたね。
でもね、コマンドプロンプトだと非常に見づらいのね。

この、Anaconda Promptが実行時に何を指定しているのか見てみる。

f:id:takeda_san:20170713231216p:plain

%windir%\system32\cmd.exe "/K" C:\Users\takeda\Anaconda3\Scripts\activate.bat C:\Users\takeda\Anaconda3

これをcmderに入れてやればよいのだろうか。
おもむろにcmderでWinキー + Alt + Tを押し、起動時の設定を入れる。

左下の「+」ボタンで新規追加して適当に名前を付ける。
(今回はcmd::Python)

んで、デフォルト設定っぽい、「cmd::Cmder」の設定をコピペしてくる。

f:id:takeda_san:20170713233150p:plain

んで一番下の段が、コマンドプロンプト実行時の各オプション指定。

cmd /k "%ConEmuDir%\..\init.bat & C:\Users\takeda\Anaconda3\Scripts\activate.bat"  -new_console:d:%USERPROFILE% C:\Users\takeda\Anaconda3

すでに、/kで一つバッチファイルが指定されているので、&で挟んでAnaconda Promptで指定されていたバッチファイルパス。 んで、末尾にファイルパスを指定する。(これなんだろうか…、ユーザ指定?)
最後に左下のSave settingsを押して設定完了。

右下の緑色の「+」ボタンから、先ほどの設定を使ってコマンドプロンプトを起動してみる。
そして、気合をいれて「python –version」!!

f:id:takeda_san:20170713233811p:plain

やりましたよ。奥様。

JavaFXとNettyでソケット通信アプリケーションを作ってみよう その2

前回のあらすじ

takeda-san.hatenablog.com

Nettyを使った簡単なソケット通信プログラムを動かしたぞ!

今回やること

好きな文字列が送れるようにする。
前回は決まったフォーマットの固定長のデータだったので、データのエンコード、デコード部分を直接コードに書いていた。

前回記事、ClientHandlerクラスのこの辺ですね。

long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
System.out.println(new Date(currentTimeMillis));

しかしながら、可変長のデータもあるじゃろ。
ということで、前回のサンプルプログラムを少しいじって、好きな文字列が送れるようにする。

また、今回もJavaFXは出てきません。

プログラム

今回も懲りずに全文掲載。

まずは、クライアント側から

package application.socket.part2;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

public class Client {

    public static void main(String[] args) throws Exception {
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup);
            b.channel(NioSocketChannel.class);
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();

                    // outbound(サーバへの送信時に実行される)
                    pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
                    pipeline.addLast("stringEncoder", new StringEncoder(CharsetUtil.UTF_8));

                    // ハンドラ(送受信両方で実行される)
                    pipeline.addLast(new ClientHandler());
                }
            });

            ChannelFuture f = b.connect(host, port).sync();

            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}
package application.socket.part2;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class ClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(final ChannelHandlerContext ctx) {
        String message = "メッセージを送ったぞ!";

        final ChannelFuture f = ctx.writeAndFlush(message);
        f.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) {
                assert f == future;
                ctx.close();
            }
        });
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

続いてサーバ側

package application.socket.part2;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.util.CharsetUtil;

public class Server {

    private int port;

    public Server(int port) {
        this.port = port;
    }

    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();

                            // inbound(サーバへの送信時に実行される)
                            pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(8192, 0, 4, 0, 4));
                            pipeline.addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8));

                            // ハンドラ(送受信両方で実行される)
                            ch.pipeline().addLast(new ServerHandler());
                        }
                    }).option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture f = b.bind(port).sync();

            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new Server(port).run();
    }
}
package application.socket.part2;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class ServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        String m = (String) msg;
        System.out.println(m);
        ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

んで、サーバとクライアントを起動して、サーバ側のコンソールに出力されるのがこれ。

メッセージを送ったぞ!

今回、プログラムを変更したところは、ここ。

Client.java

// outbound(サーバへの送信時に実行される)
pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
pipeline.addLast("stringEncoder", new StringEncoder(CharsetUtil.UTF_8));

// ハンドラ(送受信両方で実行される)
pipeline.addLast(new ClientHandler());

ClientHandler.java

@Override
public void channelActive(final ChannelHandlerContext ctx) {
    String message = "メッセージを送ったぞ!";
    final ChannelFuture f = ctx.writeAndFlush(message);
    f.addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) {
            assert f == future;
            ctx.close();
        }
    });
}

Server.java

// inbound(サーバへの送信時に実行される)
pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(8192, 0, 4, 0, 4));
pipeline.addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8));

// ハンドラ(送受信両方で実行される)
ch.pipeline().addLast(new ServerHandler());

ServerHandler.java

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    String m = (String) msg;
    System.out.println(m);
    ctx.close();
}

で、どの順番で動いてるのよ

  1. サーバが起動する
  2. クライアントが起動する
  3. クライアントがサーバに接続する
  4. クライアントの接続時にClientHandlerクラスのchannelActiveメソッドが実行される
  5. StringのデータをUTF-8エンコード(バイト列にします)
  6. 文字列のバイト長(4バイト)を先頭にくっつける
  7. サーバへ送信する
  8. サーバのデータ受信時にバイト長を元にデータ部(Stringのデータ)を取り出す
  9. データ部をUTF-8でデコードしてStringデータに戻す
  10. コンソールに表示する

パイプライン

パイプラインとは、受け取ったメッセージに対して処理をするキューのようなもの?
データを送受信するときにパイプラインに登録した、ハンドラが順番に実行されます。

クライアントだと

  1. ClientHandler
  2. StringEncoder
  3. LengthFieldPrepender
// outbound(サーバへの送信時に実行される)
pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
pipeline.addLast("stringEncoder", new StringEncoder(CharsetUtil.UTF_8));

// ハンドラ(送受信両方で実行される)
pipeline.addLast(new ClientHandler());

サーバだと

  1. LengthFieldBasedFrameDecoder
  2. StringDecoder
  3. ServerHandler
// inbound(サーバへの送信時に実行される)
pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(8192, 0, 4, 0, 4));
pipeline.addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8));

// ハンドラ(送受信両方で実行される)
ch.pipeline().addLast(new ServerHandler());

の順番で実行されているように見える。
outbound時には、パイプライン登録した後ろのほうから実行される。
inbound時には、パイプライン登録した前のほうから実行される。
で、良いのかな?

LengthFieldPrependerの引数に指定した数値が、データ長を示すヘッダ長になる。
StringDecoderとStringEncoderに指定した引数の文字コードでデコード、エンコードをかける。

次回はようやく、JavaFXに組み込んで、任意の文字列を送受信できるようにします。

JavaFXとNettyでソケット通信アプリケーションを作ってみよう その1

ちょっとNettyを使う用事があったので、忘れないうちにメモ。
せっかくなのでJavaFXでソケット通信アプリケーションを作ってみる。

今回使っているバージョンは、「4.1.12-Final」です。

まずは、JavaFXを使わないで、サンプルプログラムを動かして動作を確認してみる。
[サーバ] → [クライアント] のような通信をするプログラムです。

ここの「Writing a Time Server」と「Writing a Time Client」からコピペです。

Netty.docs: User guide for 4.x

クライアントの接続を待つ、サーバ側のプログラム

package application.socket;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class Server {

    private int port;

    public Server(int port) {
        this.port = port;
    }

    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new ServerHandler());
                        }
                    }).option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture f = b.bind(port).sync();

            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new Server(port).run();
    }
}

つぎが、送信する任意の情報をバイト列に変換するサーバハンドラ。

package application.socket;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class ServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(final ChannelHandlerContext ctx) {
        final ByteBuf time = ctx.alloc().buffer(4);
        time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));

        final ChannelFuture f = ctx.writeAndFlush(time);
        f.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) {
                assert f == future;
                ctx.close();
            }
        });
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

んで、つぎがサーバに接続しに行くクライアント。

package application.socket;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class Client {

    public static void main(String[] args) throws Exception {
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            Bootstrap b = new Bootstrap();
            b.group(workerGroup);
            b.channel(NioSocketChannel.class);
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ClientHandler());
                }
            });

            ChannelFuture f = b.connect(host, port).sync();

            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

最後にサーバから送信されてきたバイト列を元の状態に変換する、クライアントハンドラ。

package application.socket;

import java.util.Date;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class ClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg;
        try {
            long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        } finally {
            m.release();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

んで、サーバ→クライアントの順番で起動する。
(クライアントの実行時引数にサーバ側の待ち受けホスト名:localhost、ポート:8080を指定するのをお忘れなく)

実行結果(クライアント側)

Wed Jul 05 22:24:06 JST 2017

うむ、ちゃんと動いた。

ハンドラって何さ

送信時、受信時のデータを変換するための、クラスっぽい。
パイプラインという処理の流れでデータを変換していて、その一つの処理の塊がハンドラらしい。
公式のこの図が素晴らしくわかりやすい。

ChannelPipeline (Netty API Reference (4.1.12.Final))

                                                 I/O Request
                                            via Channel or
                                        ChannelHandlerContext
                                                      |
  +---------------------------------------------------+---------------+
  |                           ChannelPipeline         |               |
  |                                                  \|/              |
  |    +---------------------+            +-----------+----------+    |
  |    | Inbound Handler  N  |            | Outbound Handler  1  |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  |               |
  |               |                                  \|/              |
  |    +----------+----------+            +-----------+----------+    |
  |    | Inbound Handler N-1 |            | Outbound Handler  2  |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  .               |
  |               .                                   .               |
  | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
  |        [ method call]                       [method call]         |
  |               .                                   .               |
  |               .                                  \|/              |
  |    +----------+----------+            +-----------+----------+    |
  |    | Inbound Handler  2  |            | Outbound Handler M-1 |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  |               |
  |               |                                  \|/              |
  |    +----------+----------+            +-----------+----------+    |
  |    | Inbound Handler  1  |            | Outbound Handler  M  |    |
  |    +----------+----------+            +-----------+----------+    |
  |              /|\                                  |               |
  +---------------+-----------------------------------+---------------+
                  |                                  \|/
  +---------------+-----------------------------------+---------------+
  |               |                                   |               |
  |       [ Socket.read() ]                    [ Socket.write() ]     |
  |                                                                   |
  |  Netty Internal I/O Threads (Transport Implementation)            |
  +-------------------------------------------------------------------+
 

自分のアプリケーション→別のアプリケーションへの送信時のハンドラをアウトバウンド。
別のアプリケーション→自分のアプリケーションの受信時はインバウンド。

つまり、今回の例だと次のような流れでデータがやり取りされていたということだろう。

(時刻情報)→ [ServerHandler] →(バイト列)→ [ClientHandler] →(時刻情報)
       ↑ここで変換          ↑ここで復元

動作のシーケンスを教えなさいよ

  1. サーバ起動 8080ポートで接続を待つ
  2. クライアント起動 ホスト名:localhost ポート:8080のサーバに接続しに行く
  3. サーバ、接続時に動作する「channelActive」メソッドを実行
    3.1. 時刻情報を取得する
    3.2. 時刻情報をint型としてバイト列に変換する
    3.3. バイト列を送信する
  4. サーバ、送信終了後に接続を切断する
  5. クライアント、データ受信時に動作する「channelRead」メソッドを実行
    5.1. 受信したバイト列をint型として読み込む
    5.2. 元の時刻情報(long型)に復元する
    5.3. コンソールに時刻情報(Date型として)を表示する

ざっくりとこんな感じだろうか。
次回はちょっとコードに手を入れて、任意の文字数のメッセージをやり取りできるようにします。

コレクションにファクトリーメソッドが追加されたらしい

まだまだ続くよJava9お試し。
今回はコレクションフレームワークのお話。

List<String> list = new ArrayList<>();
list.add("ひとつめ");
list.add("ふたつめ");

Map<String, String> map = new HashMap<>();
map.put("きー1", "ばりゅー1");
map.put("きー2", "ばりゅー2");

Set<String> set = new HashSet<>();
set.add("いち");
set.add("に");

リスト、マップ、セットの実装数あれど、使うのは大体この3クラス。
右辺の総称型の中身を書かなくてよくなって、少し楽になりましたが、初期化もちょっと簡単に書けるようになりませんかね・・・?
というわけで、Java9からファクトリーメソッドが用意されました。

Javadocのofメソッドが、該当します。

List (Java Platform SE 9 [build 171])

Map (Java Platform SE 9 [build 171])

Set (Java Platform SE 9 [build 171])

まぁ、なんだ…引数多いな。
言いたいことはよくわかるが…引数多いな。特にmap。
コード補完みてると、圧倒的な存在感だぞ。

f:id:takeda_san:20170526233448p:plain

で、書き直したのがこちら。

List<String> list = List.of("ひとつめ", "ふたつめ");
Map<String, String> map = Map.of("きー1", "ばりゅー1", "きー2", "ばりゅー2");
Set<String> set = Set.of("いち", "に");

すっきり。
ちなみに、ofで作成したコレクションは不変になるとのことで変更しようとすると実行時に怒られます。

List<String> list = List.of("ひとつめ", "ふたつめ");
list.add("みっつめ");
Exception in thread "main" java.lang.UnsupportedOperationException
    at java.base/java.util.ImmutableCollections.uoe(Unknown Source)
    at java.base/java.util.ImmutableCollections$AbstractImmutableList.add(Unknown Source)
    at Main.main(Main.java:12)

Java9から匿名クラスの総称型も省略して書けるって本当ですか!?

わかった気になっているうちにやっておこうシリーズ第二弾。
EclipseでJDK9-eaの設定をする記事はこちら。

takeda-san.hatenablog.com

Java9から匿名クラスの総称型もダイヤモンド演算子(って言い方でいいの?)で省略して書けるらしい。
試してみようじゃないの。

というわけで、よくみるJavaFXのカラムクラス内でセルの定義を匿名クラスで作るコードで検証。
(大人気のJavaFXだからね、よくみるよね、ね?)

というわけで、テーブルカラムを継承して自分のテーブルカラムを作る。
そしてその中で、テーブルセルの挙動を変えたい!みたいなコード。

package application;

import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;

public class TestColumn extends TableColumn<Person, String> {

    public TestColumn() {
        // セルをつくるぞ!
        this.setCellFactory(column -> {
            return new TableCell<Person, String>() {
                @Override
                protected void updateItem(String item, boolean empty) {
                    super.updateItem(item, empty);

                    if (!empty) {
                        // なんか処理
                    }
                }
            };
        });
    }
}

で、ここを毎回、えーと型はなんだったかな…?とクラスの先頭までスクロールしてコピペして書くのがめんどくさい。

       this.setCellFactory(column -> {
            return new TableCell<Person, String>() {

でも、書かないと怒られる。
匿名クラスじゃなきゃできるんだからさっ。ねっ?

で、書き換えてみました。

       this.setCellFactory(column -> {
            return new TableCell<>() {

お、エラーが出ない。
素晴らしい。

念のため、コンパイルのレベルを1.8に戻してみる。

f:id:takeda_san:20170525231644p:plain

やっぱり怒られるよね。

今回の内容とは関係ないけど、Eclipseなんだかjavafxパッケージの下にあるクラスがコード保管されない・・・?
久しぶりにimport文を自分で書いたよね。

EclipseでJigsawしてみる その2

というわけで続きです。

takeda-san.hatenablog.com

jar作成死闘編

ようやくjdk9-eaの設定に成功したので、コードを書いていきましょう。

今回は、ライブラリ用のプロジェクトとそれを使うアプリケーションのプロジェクトの二つを準備しました。
まずは、ライブラリ用のプロジェクト。

f:id:takeda_san:20170523225308p:plain

Lib.java … 外部公開API用のクラス

package lib;

import lib.util.LibUtil;

public class Lib {

    public static void outputTextLib() {
        System.out.println("Libですよー。");
        LibUtil.outputTextUtil();
    }
}

LibUtil.java … 外部には公開したくない、Lib.javaが参照するユーティリティクラス(という設定

package lib.util;

public class LibUtil {

    public static void outputTextUtil() {
        System.out.println("LibUtilですよー。");
    }
}

module-info.java … 公開パッケージを記載する
これ、module lib のlibのところはモジュール名なので、適当な名前をつけてよいらしい。
(偶然パッケージ名とおなじになっているだけ)

module lib {
  exports lib;
}

んで、module-info.javaをsrc直下に置くわけだけども、EclipseのNew→New Java Classで作成すると
『Type name is not vaild』って怒られるのね。(クラス名に"-“が入っているから当たり前か)
なので、New→New Fileで作りました。

このページにも、そうせよと書いてある。

Eclipse and Jigsaw modules - Eclipsepedia

で一通り、ファイルはできたのでコンパイルしてjarにまとめたいんだけど jarのExportが、下のエラーでどうあがいてもできないわけですよ。

『JAR creation failed. See details for additional information. Class files on classpath not found or not accessible for: ‘Lib/src/module-info.java'』

クラスファイルがないだと・・・?
と、binフォルダの下を見るとしっかりmodule-info.classがあるわけですよ。
しばらく調べてもよくわからなかったので、悔しいけれど、コマンドでjarを作ります。

いつものjarコマンドとは違うやり方が必要だった気がするので、jar –helpでコマンドを調べてみます。

 # 2つのクラス・ファイルを含むclasses.jarというアーカイブを作成する:
 jar --create --file classes.jar Foo.class Bar.class
 # foo/のすべてのファイルを含む、既存のマニフェストを使用したアーカイブを作成する:
 jar --create --file classes.jar --manifest mymanifest -C foo/ .
 # モジュラjarアーカイブを作成する。モジュール・ディスクリプタはclasses/module-info.classに
 # ある:
 jar --create --file foo.jar --main-class com.foo.Main --module-version 1.0
     -C foo/ classes resources
 # 既存の非モジュラjarをモジュラjarに更新する:
 jar --update --file foo.jar --main-class com.foo.Main --module-version 1.0
     -C foo/ module-info.class
 # 複数リリースjarを作成し、一部のファイルをMETA-INF/versions/9ディレクトリに配置する:
 jar --create --file mr.jar -C foo classes --release 9 -C foo9 classes

うーん、ようわからん。
とりあえず–createを付ければいいんかな。

C:\Users\takeda\eclipse-workspace\Lib\bin>"C:\Program Files\Java\jdk-9\bin\jar.exe" --create --file=lib.jar .

動作確認編

とりあえず、jarはできたので、アプリケーションのプロジェクトにインポートしてみる。

f:id:takeda_san:20170523231602p:plain

App.java … メインクラス

package app;

import lib.Lib;

public class App {

    public static void main(String[] args) {
        Lib.outputTextLib();
    }
}

module-info.java … 依存するモジュール名を記載する (偶然パッケージ名とおなじになっているだけ)

module app {
  requires lib;
}

で、コンパイル後に実行するわけだけれどもlibパッケージが見えなくてエラーが出ている。
そういえば、設定を一つ忘れていたな…

Java 9 Support (BETA) for Oxygen | Eclipse Plugins, Bundles and Products - Eclipse Marketplace

added to the .classpath manually: <classpathentry kind="con" path="org.eclipse.jdt.MODULE_PATH"/>

モジュールの依存関係をクラスパスに追加せよとのこと。
プロジェクトフォルダ直下の.classpathファイルに追記する。

<?xml version="1.0" encoding="UTF-8"?>
<classpath>
    <classpathentry kind="src" path="src"/>
    <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-9"/>
    <classpathentry kind = "con" path = "org.eclipse.jdt.MODULE_PATH" />
    <classpathentry kind="lib" path="C:/Users/takeda/eclipse-workspace/App/lib/lib.jar"/>
    <classpathentry kind="output" path="bin"/>
</classpath>

んで、読み込みなおしのためワークスペース開きなおし。

f:id:takeda_san:20170523233236p:plain

お、若干参照するライブラリの表示が増えている。
もう一度、コンパイル&実行。

Libですよー。
LibUtilですよー。

長い、戦いだった… 肝心のLibUtilのほうが参照できないか確認してみましょう。

f:id:takeda_san:20170523233535p:plain

お、参照できない。
素晴らしい。

まとめ

2017/5/23時点での内容です。
Java 9、Eclipse Oxygen共に正式リリースまでには、もっと使いやすくなると思います。

Eclipseの現行バージョンNEONでは、Jigsawできないので開発中の次バージョンOxygenを使う
・Oxygen単体では、Java9が使用できないので『Java 9 Support (BETA) for Oxygen』プラグインを入れる
・そのうえで、モジュール化するなら、クラスパスに追加設定が必要
Eclipseからは、module-infoを使ってのjar作成は無理(?)なので、コマンドでつくる
・コマンドでjarを作成する場合は、いつものコマンドに『–create』をつける