2016-02-27

JavaFX: CheckBox 付き ListView

JavaFX の ListView の要素に、チェックボックスとテキストを一緒に表示するにはそうすればよいのか調べていたところ、Java 8 から導入された CheckBoxListCell というクラスを利用すればできることが判りました。Stack Overflow のに掲載されていた例を参考にしてサンプルを作りました。

(2016-2-29) 一部問題が見つかったため、ソースコードを修正して差し替えました。

動作環境は次の通りです。

  • OS: Fedora 23 (x86_64)
  • Java: jdk1.8.0_74-1.8.0_74-fcs.x86_64 (Oracle)
  • IDE: NetBeans IDE 8.1
リスト:Sample_CheckBoxListCell.java 
package sample_checkboxlistcell;

import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class Sample_CheckBoxListCell extends Application {

    String[] prefData = {
        "北海道", "青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県",
        "茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県",
        "山梨県", "長野県", "新潟県", "富山県", "石川県", "福井県", "岐阜県",
        "静岡県", "愛知県", "三重県", "滋賀県", "京都府", "大阪府", "兵庫県",
        "奈良県", "和歌山県", "鳥取県", "島根県", "岡山県", "広島県", "山口県",
        "徳島県", "香川県", "愛媛県", "高知県", "福岡県", "佐賀県", "長崎県",
        "熊本県", "大分県", "宮崎県", "鹿児島県", "沖縄県"};

    @Override
    public void start(Stage primaryStage) {
        ListView<Item> lv = new ListView<>();
        for (String prefName : prefData) {
            Item pref = new Item(prefName, false);
            pref.onProperty().addListener((ov, old_val, new_val) -> {
                System.out.println(pref.getName() + "のチェックボックスが '"
                        + old_val + "' から '" + new_val + "' に変更されました。");
            });
            lv.getItems().add(pref);
        }

        lv.setCellFactory(CheckBoxListCell.forListView((Item item) -> item.onProperty()));
        lv.setPrefHeight(200);
        lv.setPrefWidth(150);

        StackPane root = new StackPane();
        root.getChildren().add(lv);

        Scene scene = new Scene(root);

        primaryStage.setTitle(getClass().getSimpleName());
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static class Item {
        private final StringProperty name = new SimpleStringProperty();
        private final BooleanProperty on = new SimpleBooleanProperty();

        public Item(String name, boolean on) {
            setName(name);
            setOn(on);
        }

        public final StringProperty nameProperty() {
            return this.name;
        }

        public final String getName() {
            return this.nameProperty().get();
        }

        public final void setName(final String name) {
            this.nameProperty().set(name);
        }

        public final BooleanProperty onProperty() {
            return this.on;
        }

        public final boolean isOn() {
            return this.onProperty().get();
        }

        public final void setOn(final boolean on) {
            this.onProperty().set(on);
        }

        @Override
        public String toString() {
            return getName();
        }
    }

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

実行例を以下に示します。

宮城県のチェックボックスが 'false' から 'true' に変更されました。
秋田県のチェックボックスが 'false' から 'true' に変更されました。
山形県のチェックボックスが 'false' から 'true' に変更されました。

参考サイト

  1. java - JavaFX 8, ListView with Checkboxes - Stack Overflow

 

2016-02-20

JavaFX: LineChart を使いこなそう (4) - ズーム

JavaFX には基本的なグラフ(チャート)を作成するクラスが標準で用意されているので、ある程度使い込めば、自分の作成したいグラフを自由自在に作成できるようになります…、と言いたいところですが、それでも標準で備わっていてもらいたい機能がいくつかあります。そのひとつがズーム機能です。必要であれば自分で作ってしまうべきなのですが、世の中には同じようなことを考える人がいて、既に紹介されているサンプルがあります。

今回は、GitHub Gist で公開されている ZoomableLineChart クラスを紹介します。

  1. Example of a LineChart that can be zoomed via mouse.

実行例を下記に示しました。動作を確認した環境は次の通りです。

  • OS: Fedora 23 (x86_64)
  • Java: jdk1.8.0_74-1.8.0_74-fcs.x86_64 (Oracle)
  • IDE: NetBeans IDE 8.1

このサンプルでは、欲しかったズーム機能が実現されています。願わくば、ボタンをクリックしなくとも、マウスの操作だけでスームや解除をしたいところです。また、クラスを汎用目的に使えるようにしたいのですが、必要であれば自分でやるべきことだと思うので、ソースの内容をよく咀嚼して、自分が使いやすい汎用クラスへ改良しようと思います。

[1] のソースを下記に埋め込みました。

2016-02-19

10年ぶりのメジャーリリース「ReactOS 0.4」がついに登場 | OSDN Magazine

ReactOS は、Microsoft Windows NT 系とアプリケーション及びドライバに於けるバイナリ互換性を目指すオープンソースなオペレーティングシステムです。

2 月 16 日付(米国時間)で ReactOS 0.4 がリリースされました。バージョン 0.3 からなんと 10 年を経てのメジャーリリースです。早速、GNOME Boxes で動作確認をしようとしましたが、私の環境ではうまく動きませんでした。仮想マシンを変えて、うまく動作させられたらあらためて紹介します。

  1. 10年ぶりのメジャーリリース「ReactOS 0.4」がついに登場 | OSDN Magazine

追記

VirtualBox で試したところ、ReactOS の Live CD を難なく起動できましたので。スクリーンショットをいくつか掲載します。

2016-02-18

Oracle、「Java SE 8 Update 73」を公開。1件の脆弱性を修正 - 窓の杜

Java のリリースがこの時期に更新されるとは思っていなかったので、すっかり気づくのが遅れてしまいました。業務で使っている Windows 上の JDK を慌てて 8u74 へアップデートしました。JavaFX は変わっていないんですね。

2016-02-16

【備忘録】SysV ランレベルと systemd ターゲット

Fedora は、リリース 21 から WORKSTATION、SERVER、CLOUD の 3 つのエディションが配布されるようになりましたが、以来、WORKSTATION 版をダウンロードして使っています。

最近、古くなったデスクトップ PC を買い換える検討を始めていますが、新調した際に SERVER 版を使用した方が、サーバの動作確認や評価・実験をする際に都合がいいかもしれないと考え、とりあえず仮想環境で Fedora 23 の SERVER 版をインストールしてみることにしました。

インストールイメージが 2.0GB もあったので、ダウンロード時間を節約するために、ネットインストールイメージ (415MB) をダウンロードすることにしました。仮想環境へのインストールは無事終わったのですが、再起動後にデスクトップが起動しません。

あれっ、systemd でいわゆるランレベルの変更をするには…??

どうするのかが判らなかったので調べました [1]

まず、systemd では「ランレベル」とは呼ばずに「ターゲット」と呼ぶのですが、ランレベルの番号と対応しています。詳細は参考サイト [1] の表 6.6 を参照してください。

現在のターゲットをいわゆるランレベル 5 へ変更するには次のようにします。

# systemctl isolate graphical.target

起動後のデフォルトのターゲットになるように変更するには次のようにします。

# systemctl set-default graphical.target

さて、件の仮想環境では、まずデスクトップ環境をインストールする必要がありましたので、乱暴にも dnf groupinstall "Fedora Workstation" でデスクトップに必要なパッケージをインストールしました。その後で、上記の systemctl set-default graphical.target を実行、再起動して GNOME 3 のログイン画面が表示されるのを確認しました。

すぐ忘れてしまいそうなので、備忘録としました。

参考サイト

  1. Red Hat Customer Portal 6.3. SYSTEMD ターゲットでの作業

2016-02-14

GNOME Boxes と Haiku

Fedora の GNOME 3 デスクトップ環境で利用できる GNOME Boxes は、仮想マシンおよびリモートシステムを表示およびアクセスするために使用する、グラフィカル・デスクトップ仮想化ツールです。GNOME Boxes は、簡単な設定でデスクトップから異なるオペレーティングシステムとアプリケーションをテストする方法を提供します[1]

Fedora 23 では、GNOME Boxes 3.18.1 が利用できます(下図)。

このところ VirtualBox を利用していましたが、カーネルと入手できる VirtualBox のカーネルモジュールのバージョンが合わないことが多く、Linux での VirtualBox の利用頻度が減っていました。しかし、この Boxes を使えば、簡単にテストができそうだということで、以下の三種の OS を試してみました。結局、最後の Haiku のみ起動に成功したので、Haiku の例を紹介します。

  • Automotive Grade Linus (agl-demo-platform-qemux86-64.vmdk)
  • React OS (ReactOS-LiveCD.iso)
  • Haiku (haiku-r1alpha4.iso)

動作を確認した環境は次の通りです。

  • OS: Fedora 23 (x86_64)
  • Desktop: GNOME 3

Haiku を仮想マシンへインストール

Haiku は、オープンソースで開発されているデスクトップ向けオペレーティングシステムで、BeOS を再現することを目指しており、以前は OpenBeOS と呼ばれていました [2]。BeOS については参考サイト [3] に詳しく説明があります。

まず、GNOME Boxes を起動します。

左上の「新規 (N)」をクリックして、ダウンロードした Haiku の iso ファイル (haiku-r1alpha4.iso) を選択します。レビューの画面で、Boxes の示すプロパティをカスタマイズせず、そのまま右上の「作成 (R)」ボタンをクリックします。

言語を選択(この例では日本語)し、「インストーラーを実行」ボタンをクリックします。ややあやしい日本語のウェルカムメッセージが出ますので「続ける」ボタンをクリックして続行します。

インストール可能なパーティションが見つからないというメッセージが表示されますので、「OK」ボタンをクリックして警告メッセージを閉じ、「パーティションの設定」ボタンをクリックして、この仮想マシンで確保した領域をフォーマットして、OS のインストール先に割り当てます。下記のスクリーンショットを参考にしてください。

「開始」ボタンをクリックしてインストールを開始します。インストール終了後、「再起動」ボタンをクリックして再起動します。

画面が大きくならないままで再起動が始まりますので、クリックして画面の大きさを戻します。

ログイン画面は無く、再起動後、Haiku のデスクトップ画面が表示されます。試しにインターネット用のブラウザと思われる WebPositive を起動してみました。

Haiku については、日本語化などローカライゼーションが不足しているものの、ちょっといじってみた限りにおいては、仮想マシン上にもかかわらず、比較的きびきびとしたレスポンスのように感じました。もう少し触ってみた後、機能をまとめてみたいと思います。一方、Boxes については、ひきつづき、いろいろな OS を試してみて成功あるいは失敗事例を紹介していきたいと思います。

参考サイト

  1. Boxes
  2. Home | Haiku Project
  3. BeOS - Wikipedia

2016-02-13

JavaFX: チャートの重ね合わせ

JavaFX で棒グラフ (BarChart) と折れ線グラフ (LineChart) を合わせて表示したい場合、StackPane を用いてそれぞれを重ねて表示することで簡単に実現できます。Jewelsea (John Smith) 氏が GitHub Gist でサンプルを公開していますので、その実行例を紹介します [1]

動作を確認した環境は次の通りです。

  • OS: Fedora 23 (x86_64)
  • Java: Java SE 1.8.0_72 (jdk1.8.0_72-1.8.0_72-fcs.x86_64)
  • IDE: NetBeans IDE 8.1

パレート図(もどき)への応用

StackPane を用いたチャートの重ね合わせを応用して、BarChart の y 軸が 左側、LineChart の y 軸が右側にあるパレート図を描画するサンプルを紹介します。ただし、QC七つ道具のパレート図で定義されている累積比率の折れ線と、プロットの仕方が異なっていることをご了承ください [2]

とある会社で品証部門に在籍していたことがあり、そこには日本の品質管理の手法を大切にする気風がありました。何かの時に R で作成したパレート図を自慢気に披露したところ、これはパレート図ではない、と指摘されて赤面したことがあるのです。それが、ここで紹介するパレート図と同じ書き方でしたので、自戒をこめて(もどき)と入れています。個人的には、定義から逸れているからダメだという議論より、このチャートから何を読み取るべきかが大切だとは思うのですが、間違いを間違いだと認めなければなりません。

リスト:Sample_Pareto.java
package sample_pareto;

import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Scene;
import javafx.scene.chart.BarChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class Sample_Pareto extends Application {

    Boolean first = true;

    BarChart<String, Number> barchart;
    LineChart<String, Number> linechart;

    // X data
    String category1 = "A";
    String category2 = "B";
    String category3 = "C";
    String category4 = "D";
    String category5 = "E";
    String category6 = "Others";
    // Y data
    ObservableList<XYChart.Data> data1 = FXCollections.observableArrayList(
            new XYChart.Data(category1, 34),
            new XYChart.Data(category2, 19),
            new XYChart.Data(category3, 12),
            new XYChart.Data(category4, 9),
            new XYChart.Data(category5, 6),
            new XYChart.Data(category6, 10)
    );
    // Y2 data
    ObservableList<XYChart.Data> data2 = FXCollections.observableArrayList(
            new XYChart.Data(category1, 37.8),
            new XYChart.Data(category2, 58.9),
            new XYChart.Data(category3, 72.2),
            new XYChart.Data(category4, 82.2),
            new XYChart.Data(category5, 88.9),
            new XYChart.Data(category6, 100.0)
    );
    // chart title
    String gTitle = "パレート図のサンプル  ";
    String gXTitle = "カテゴリ";
    String gYTitle = "件  数";
    String gY2Title = "累積比率 (%)";
    // geometry
    double sizeX = 600;
    double sizeY = 500;
    double offsetX = 60;

    @Override
    public void start(Stage stage) {
        // X axis
        CategoryAxis xAxis = new CategoryAxis();
        xAxis.setLabel(gXTitle);

        // barchart
        barchart = createBarChart(xAxis, data1);
        barchart.setTitle(""); // ダミー
        barchart.getStylesheets().addAll(getClass().getResource("BaseChart.css").toExternalForm());
        // linechart
        linechart = createLineChart(xAxis, data2);
        linechart.setTitle(gTitle);
        // set chart width
        updateChartWidth();

        // stackpane for overlay
        StackPane pane = new StackPane();
        pane.getChildren().addAll(barchart, linechart);
        pane.setAlignment(Pos.BOTTOM_LEFT);

        Scene scene = new Scene(pane, sizeX, sizeY);
        scene.widthProperty().addListener(
                (ObservableValue<? extends Number> observableValue, Number oldSceneWidth, Number newSceneWidth) -> {
                    if (!first) {
                        sizeX = (double) newSceneWidth;
                        updateChartWidth();
                    } else {
                        first = false;
                    }
                }
        );
        scene.getStylesheets().add(getClass().getResource("ParetoChart.css").toExternalForm());
        stage.setScene(scene);
        stage.show();
    }

    BarChart<String, Number> createBarChart(CategoryAxis xAxis, ObservableList<XYChart.Data> data) {
        NumberAxis yAxis = createYaxis(0, 90, 10, 5);
        yAxis.setLabel(gYTitle);

        BarChart<String, Number> chart = new BarChart<>(xAxis, yAxis);
        chart.setLegendVisible(false);

        XYChart.Series series = new XYChart.Series(data);
        series.setName("カテゴリ");
        chart.getData().add(series);

        return chart;
    }

    LineChart<String, Number> createLineChart(CategoryAxis xAxis, ObservableList<XYChart.Data> data) {
        NumberAxis y2Axis = createYaxis(0, 100, 10, 2);
        y2Axis.setSide(Side.RIGHT);
        y2Axis.setLabel(gY2Title);

        LineChart<String, Number> chart = new LineChart<>(xAxis, y2Axis);
        chart.setLegendVisible(false);
        chart.setHorizontalGridLinesVisible(false);
        chart.setVerticalGridLinesVisible(false);

        XYChart.Series series = new XYChart.Series(data);
        series.setName("累積");
        chart.getData().add(series);

        return chart;
    }

    NumberAxis createYaxis(double min, double max, int major, int minor) {
        final NumberAxis axis = new NumberAxis(min, max, major);
        axis.setMinorTickCount(minor);
        axis.setPrefWidth(offsetX);
        return axis;
    }

    void updateChartWidth() {
        barchart.setLayoutX(0);
        barchart.setTranslateX(0);
        barchart.setMaxWidth(sizeX - offsetX);

        linechart.setLayoutX(0);
        linechart.setTranslateX(offsetX);
        linechart.setMaxWidth(sizeX - offsetX);
    }

    public static void main(String[] args) {
        launch(args);
    }
}
リスト:ParetoChart.css
.chart {
    -fx-padding: 10px;
}

.chart-content {
    -fx-padding: 10px;    
}

.chart-title {
    -fx-font-size: 16pt;    
}

.chart-plot-background { 
    -fx-background-color: transparent; 
}

.axis-label {
    -fx-font-size: 14pt;
}

.axis {
    AXIS_COLOR: #888;
    -fx-tick-label-font: 12pt system;
    -fx-tick-label-fill: derive(-fx-text-background-color, 30%);
}

.chart-bar {
    -fx-border-color: rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.4) transparent rgba(0, 0, 0, 0.4);
}

.series0.chart-bar {
    -fx-background-color: lightskyblue;
}

.default-color0.chart-series-line {
    -fx-stroke: #f44;
}

.default-color0.chart-line-symbol {
    -fx-background-color: #f44, #f88;
    -fx-background-radius: 6px;
    -fx-padding: 6px;
}
リスト:BaseChart.css
.chart-plot-background { 
    -fx-background-color: white; 
}

実行例

参考サイト

  1. Uses JavaFX to draw layers of XYCharts.
  2. (株)日科技研:パレート図とは(QC七つ道具)|製品案内

2016-02-11

ASUS EeeBook X205TA と Linux

Amazon.jp で ASUS ノートパソコン EeeBook X205TA が 31,010 円で売っていたので、これは安い!と思い買ってしまいました。

商品の詳細は右下の通りです。なお Amazon.jp のサイトに記載されていた詳細をベースにしていますが、ASUS から公開されている製品仕様と較べて、修正、追加してあります。

ブランドASUSTek
商品重量0.98 kg
商品の寸法19.3 x 28.6 x 1.8 cm
メーカー型番X205TA-WHITE10
カラーホワイト
フォームファクタLaptop
商品の寸法 幅 × 高さ28.6 x 18 mm
画面サイズ11.6 インチ
解像度1,366×768 ドット (WXGA)
外部ディスプレイ出力最大 1,920×1,080 ドット
CPUブランドIntel
CPUタイプIntel Atom
CPU速度1.33GHz(最大 1.83GHz)
RAM容量2 GB
最大メモリ容量32 GB
ストレージ容量32 GB
ステレージ種類eMMC
グラフィックアクセラレータ インテル HD
内蔵スピーカー2 W × 2(ステレオ)
内蔵マイクアナログマイク
内蔵 Web カメラ30万画素
無線 LAN 802.11a/b/g/n
Bluetooth® 機能Bluetooth® 4.0
USB2.0 用ポート数2
搭載光学ドライブ種類CD-ROM なし
OSWindows 10 32bit
同梱ソフトMS Office 搭載なし
充電時間約 2.3 時間
バッテリー寿命約 12.8 時間
リチウムイオン電池数2 セル

この PC のスペックは自分が使う用途としては貧弱すぎます。しかし、つれあいが PC を使う用途、すなわちメールのチェックと Web 閲覧、カードゲームのような CPU に負荷の少ないアプリケーションで遊ぶ程度であれば十分と考え、加えて、あまりにも古いノート PC(なんと IBM ThinkPad X31 / Fedora)を今でも使い続けていることを不憫に思い、購入したのでした。機能満載の高額なタブレットやスマホに較べれば、確かに動作は遅いのでしょうが、業務用途ではなく個人的な限られた用途に使うのであれば十分な動作速度です。

Linux 導入は断念

我が家では、会社から支給されている Windows 7 搭載のノート PC の他に 3 台の PC がありますが、それらには全て Linux (Fedora) をインストールしています。昨年購入した、同じく ASUS の X200MA に ubuntu や Fedora をインストールして使っていますので、今回もなんなく Linux をインストールして使おうと軽く考えていました。

Fedora 23 Workstation の iso イメージを USB メモリに焼いて、BIOS の設定を変更して起動しようとしましたが起動できません。このときになってはじめて、X205TA に Linux をインストールした事例がないかインターネットで調べました。すると、こんな記事がありました。

上記記事によると、ubuntu で WiFi のドライバが対応していないそうです。この X205TA にはイーサーネット用 RJ45 ジャックが付いていないので、どうしても有線のネットワークに繋ぎたければ USB を介するしかありません。WiFi に対応していなければ、ほとんど使い物にならないと言えます。

なお、kernel のバージョンが 4.0 以上であれば、WiFi に対応できるらしいが、記事では 4.0.4 にアップデートしてもダメだったとあります。Fedora 23 では kernel のバージョンは 4.3 台になっていますので、Fedora をインストールするのであれば、もしかすると WiFi の不具合の件は乗り越えられるかもしれません。

しかしながら、最近ではすっかり kernel の詳しいバージョンアップの内容を追いかけなくなってしまったので、よく調べないと見込みが立たず、依然、失敗するリスクは高いです。

また、次の記事によると USB からイメージを起動するには 32 bit UEFI ブート用ファイルが必要らしい。さらにオーディオもダメらしいです。

前回、X200MA に ubuntu や Fedora をインストールした時と較べれば、インストールのはるかに面倒くさそうです。下記の記事では視点を変えて 32 bit 版を試していますが 32 bit 用の ubuntu であっても、不具合の事情はそれほど変わらないようです。

Windows 10 に触れるのは初めてでもありましたので、しばらくこのままにして少し調べてからにしようと、軟弱にも判断したのでした。

以下に X205TA 上の Windows 10 の画面を示しました。

Windows 10 の評価については、既にいろいろなメディアで触れている中で、今更ではありますが感想をちょっとだけ書きます。

Windows もバージョンが進んだだけあって、さすがに UI は洗練されてきたと思いました。Windows 10 になってスタートメニューが復活したことを喜ぶニュースが多かったことを覚えていますが、Windows 8/8.1 時代の「スタート画面」の雰囲気が色濃く残っている印象を受けました。

個人的には、Windows 8/8.1 時代の「スタート画面」は斬新で悪くはなかったと思っています。しかし悲しいかな、全てのユーザーがこの変化に追従できたわけではなく、結果として Windows 8/8.1 は短命になってしまいました。

そもそも、いろいろなデスクトップをユーザーは選択できない、というのが Windows という商用 OS の限界なのかもしれません。もちろん、デスクトップを統一することによって、Windows のオペレーションを学習するコースを容易に商売にすることが出来るので、結果として、いわゆるパソコン教室のような産業が発達したり、Windows の操作という側面で資格認定のシステムを確立しやすくもなります。

ちなみに、Windows の売上が上がれば、関連する業界も栄えるという互恵関係のしくみを「エコシステム」と呼ぶらしいです。どこまでも商用な OS です。

しかし Linux ユーザーにしてみれば、とあるデスクトップに不満があれば、他のデスクトップに変えれば良いだけなので、事情は違います。例えば Fedora の標準デスクトップは GNOME 3 ですが、気に入らなければ KDE や他のデスクトップに切り替えることが、たとえ OS をインストールした後でも可能です。はじめから GNOME 3 以外のデスクトップを使いたければ、Spin と呼ばれる、他のデスクトップが標準として組み込まれたインストール・イメージを利用することも出来ます。

たかがデスクトップ、されどデスクトップです。Wikipedia によると、デスクトップおよびラップトップ PC の OS シェアでは、Linux は僅か 1% 台です。ただ、時代は変化しています。Android が Linux Kernel をベースとして使っていることから、Web のクライアント OS のシェアでは Linux ベースの OS も Windows に次いで健闘しています。Google の挑戦は、じわじわと Microsoft の首を絞めているのかもしれません。

いずれにしても、トップシェアの OS である Windows を、IT 関係に疎いつれあいにしばらく使ってもらって、感想を聞いてみようと思います。

2016-02-10

ControlsFX の SpreadsheetView クラスを試す

ControlsFX 1.は 高品質な UI やその他のツールを提供することで、JavaFX に足りない機能を補完することを目的としたオープンソースのプロジェクトです(修正 BSD ライセンス)。たとえば JDK 8u40 のリリース (2015/3/3) より前は、ダイアログを簡単に作成するクラスが同梱されている JavaFX で提供されていませんでしたが、ControlFX がそこを補っていました。

JavaFX に簡単にダイアログを作成するクラスがなかった頃に ControlsFX を知りましたが、その当時はなぜかあまり興味を持ちませんでした。しかし、よく見ると、いろいろ面白そうな UI が揃っています。今回はその中から、SpreadsheetView クラスのサンプルを紹介します。

動作を確認した環境は次の通りです。

  • OS: Fedora 23 (x86_64)
  • Java: Java SE 1.8.0_72 (jdk1.8.0_72-1.8.0_72-fcs.x86_64)
  • ControlsFX 8.40.10 (controlsfx-8.40.10.jar)
  • IDE: NetBeans IDE 8.1

デモプログラム

ControlsFX をダウンロードして展開すると、サンプルプログラム controlsfx-samples-8.40.10.jar が同梱されていますので、まずはこのプログラムを紹介します。

$ ls
controlsfx-8.40.10.jar          fxsampler-1.0.9.jar
controlsfx-samples-8.40.10.jar  license.txt
$ /usr/java/latest/bin/java -jar controlsfx-samples-8.40.10.jar
Initialising FXSampler sample scanner...
 Discovering projects...
  Found project 'ControlsFX', with sample base package 'org.controlsfx.samples'

このサンプルでは、各コントロールなどの実行例、ソース、JavaDoc のマニュアルを見ることができます。

SpreadsheetView クラス

ControlFX の JavaDocs の Class SpreadsheetView 2. に掲載されているコードサンプルをベースにして、簡単なサンプルを作りました。今後、機能を追加して、シンプルなスプレッドシートのアプリケーションにまで改良を重ねていきたいので、名前を SpreadSheet として、単なるサンプル扱いにはしていません。参考サイト [3] からソースコードを入手できるようにしています。

リスト:SpreadSheet.java 
package spreadsheet;

import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.controlsfx.control.spreadsheet.GridBase;
import org.controlsfx.control.spreadsheet.SpreadsheetCell;
import org.controlsfx.control.spreadsheet.SpreadsheetCellType;
import org.controlsfx.control.spreadsheet.SpreadsheetView;

public class SpreadSheet extends Application {

    double defaultWidth = 60;

    @Override
    public void start(Stage stage) {
        int rowCount = 100;
        int columnCount = 100;
        GridBase grid = new GridBase(rowCount, columnCount);

        ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
        for (int row = 0; row < grid.getRowCount(); row++) {
            final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
            for (int col = 0; col < grid.getColumnCount(); col++) {
                SpreadsheetCell cell = SpreadsheetCellType.STRING.createCell(row, col, 1, 1, (String) null);
                list.add(cell);
            }
            rows.add(list);
        }
        grid.setRows(rows);

        SpreadsheetView sheet = new SpreadsheetView(grid);
        sheet.getColumns().stream().filter((column) -> (column.isColumnFixable())).forEach((column) -> {
            column.setPrefWidth(defaultWidth);
        });

        StackPane root = new StackPane();
        root.getChildren().add(sheet);

        Scene scene = new Scene(root, 400, 400);
        stage.setTitle(getClass().getSimpleName());
        stage.setScene(scene);
        scene.getStylesheets().add(getClass().getResource(getClass().getSimpleName() + ".css").toExternalForm());
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
リスト:SpreadSheet.css 
.spreadsheet-cell {
    -fx-border-color: gray;
    -fx-border-insets: 0;
    -fx-border-width: 0.2;
    -fx-border-style: dashed;
}

.spreadsheet-cell:selected {
    -fx-background-color: #ddd;
}

.spreadsheet-cell:hover {
    -fx-background-color: #ccf;
}

実行例

スプレッドシートと言えば Microsoft Office の Excel が思い付きますが、そんなに高機能でなくても、ちょっとしたデータの表示や操作にスプレッドシート状の UI を使いたい時が結構あります。この SpreadsheetView クラスは、基本的なコピーペーストなどの機能が既に実装されていますので、自分にとっては手軽に、しかもかなり使い甲斐がありそうなコントロール UI になりそうです。

参考サイト

  1. ControlsFX // JavaFX News, Demos and Insight // FX Experience
  2. SpreadsheetView (ControlsFX Project 8.40.10)
  3. bitwalk123/SpreadSheet

2016-02-03

Java でメッセージダイジェストを生成 (2)

Java でメッセージダイジェストを生成 (2016-01-31) で紹介した GUI プログラムは、本ブログに掲載するためになるべく短いプログラムであることを意識するあまり、肝心な使い心地まで考えたものではありませんでした。これを反省し、もう少し手を加えました。つまり、確かに 1.5 GB 程度の iso ファイルのメッセージダイジェストを計算する時間は、許容できる=我慢できる程度のものだったかもしれませんが、それでも計算中は GUI が反応しなくなります。せっかくスレッドを制御する仕組みが用意されているので、今回は、メッセージダイジェストを計算する部分を別スレッドで処理するように変更しました。

動作を確認した環境は次の通りです。

  • OS: Fedora 23 (x86_64)
  • Java: Java SE 1.8.0_72 (jdk1.8.0_72-1.8.0_72-fcs.x86_64)
  • IDE: NetBeans IDE 8.1

JavaFX で単発的に別スレッドの処理をさせる時には、Task クラスを利用します。参考サイト [1] にこのクラスの使い方が丁寧に説明されています。しかし、別スレッドの call メソッドから返す値をどのようにして本スレッドで受け取ればよいかは、きちんと説明がされていません。もちろん、void setOnSucceeded(EventHandler<WorkerStateEvent> value) メソッドの説明があるのだから、そのぐらいは気づけよと言われるかもしれません。

読む側としては、最初に例を上げて長々と Task の説明があるのですから、もう一歩踏み込んだ説明や例があればいいのにと愚痴りたくなります。クラスやメソッドの説明を読み込んで深く理解する努力をせずに(手っ取り早く)いつも参考にしてしまう Stack Overflow にぴったりな質疑があったので、参考サイト [2] にリンクを載せました。

さて今回は、実行例を最初に紹介しましょう。以下のようになります(画像をクリックすると大きくなります)。

プログラムを以下に示しました。参考サイト [3] の GitHub にイメージを含む全ソースをアップしてあります。

リスト:HashCalc.java 
package hashcalc;

import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.geometry.HPos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.RowConstraints;
import javafx.stage.FileChooser;
import javafx.stage.Stage;

public class HashCalc extends Application {

    Stage stage;
    String baseDir = getClass().getResource("image/").toString();
    Image icoApp = new Image(baseDir + "Binary.png");
    Image icoFile = new Image(baseDir + "File.png");
    Image icoPen = new Image(baseDir + "Pencil.png");

    File file;
    Button button_file;
    Button button_sha256;
    TextField field_file;
    TextField field_sha256;

    @Override
    public void start(Stage primaryStage) {
        this.stage = primaryStage;

        GridPane root = new GridPane();
        root.setId("pane");

        ColumnConstraints col1 = new ColumnConstraints();
        ColumnConstraints col2 = new ColumnConstraints(600);
        ColumnConstraints col3 = new ColumnConstraints();
        root.getColumnConstraints().addAll(col1, col2, col3);

        RowConstraints row1 = new RowConstraints();
        row1.setFillHeight(true);
        RowConstraints row2 = new RowConstraints();
        row2.setFillHeight(true);
        root.getRowConstraints().addAll(row1, row2);

        // File Name label
        Label label_file = new Label("File Name");
        GridPane.setHalignment(label_file, HPos.RIGHT);
        root.add(label_file, 0, 0);

        // File Name field
        field_file = new TextField();
        field_file.setEditable(false);
        GridPane.setHalignment(field_file, HPos.LEFT);
        root.add(field_file, 1, 0);

        // File Name button
        button_file = new Button();
        button_file.setGraphic(new ImageView(icoFile));
        button_file.setOnAction((ActionEvent t) -> {
            FileChooser fileChooser = new FileChooser();
            file = fileChooser.showOpenDialog(stage);
            field_file.setText(file.toString());
            clearMessageDigest();
        });
        root.add(button_file, 2, 0);

        // SHA-256 label
        Label label_sha256 = new Label("SHA-256");
        GridPane.setHalignment(label_sha256, HPos.RIGHT);
        root.add(label_sha256, 0, 1);

        // SHA-256 field
        field_sha256 = new TextField();
        field_sha256.setEditable(false);
        GridPane.setHalignment(field_sha256, HPos.LEFT);
        root.add(field_sha256, 1, 1);

        // SHA-256 button
        button_sha256 = new Button();
        button_sha256.setGraphic(new ImageView(icoPen));
        button_sha256.setOnAction((ActionEvent t) -> {
            try {
                calcMessageDigest("SHA-256", field_sha256);
            } catch (NoSuchAlgorithmException | IOException ex) {
                Logger.getLogger(HashCalc.class.getName()).log(Level.SEVERE, null, ex);
            }
        });
        root.add(button_sha256, 2, 1);
         
        Scene scene = new Scene(root);
        scene.getStylesheets().add(getClass().getResource(getClass().getSimpleName() + ".css").toExternalForm());

        setAppTitle();
        stage.setResizable(false);
        stage.getIcons().add(icoApp);
        stage.setScene(scene);
        stage.show();
    }

    void clearMessageDigest() {
        field_sha256.setText("");
    }

    void calcMessageDigest(String algorithm, TextField field) throws NoSuchAlgorithmException, IOException {
        CalcMessageDigest md = new CalcMessageDigest(file, algorithm);
        md.setOnSucceeded((WorkerStateEvent t) -> {
            field.setText(md.getValue());
            field.requestFocus();
            setAppTitle();
            button_file.setDisable(false);
            button_sha256.setDisable(false);
        });

        Thread th = new Thread(md);
        th.setDaemon(true);
        th.start();

        setAppTitleRun();
        button_file.setDisable(true);
        button_sha256.setDisable(true);
    }

    void setAppTitle() {
        stage.setTitle(getClass().getSimpleName());
    }

    void setAppTitleRun() {
        stage.setTitle(getClass().getSimpleName() + " - calculating");
    }

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

今回は、CSS ファイルを加えて少し修飾していますが、TextField の文字サイズは(たぶん)反映されていません。やり方が間違っているかもしれません。

リスト:HashCalc.css 
#pane {
    -fx-padding: 5px; 
    -fx-hgap: 5px;
    -fx-vgap: 2px;
}

.textfield {
    -fx-font-size: 12px;  
}

.button {
    -fx-padding: 3px; 
    -fx-border-insets: 0px;
    -fx-border-width: 0.5px;
    -fx-border-color: black gray gray black;
    -fx-border-radius: 8px;
    -fx-background-radius: 8px;
}

メッセージダイジェストの計算を別スレッドで処理するようにしました。今回はハッシュ値のアルファベットを小文字にして返すようにしました。

リスト:CalcMessageDigest.java 
package hashcalc;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javafx.concurrent.Task;
import javax.xml.bind.DatatypeConverter;

/**
 * CalcMessageDigest
 *
 * @author bitwalk
 */
public final class CalcMessageDigest extends Task<String> {

    File file;
    String algorithm;

    public CalcMessageDigest(File file, String algorithm) throws NoSuchAlgorithmException, IOException {
        this.file = file;
        this.algorithm = algorithm;
    }

    @Override
    protected String call() throws Exception {
        return calcMD();
    }

    String calcMD() throws NoSuchAlgorithmException, FileNotFoundException, IOException {
        if (isEmpty(file)) {
            return "[empty!]";
        }
        if (!checkBeforeReadfile(file)) {
            return "[The file is not readable]";
        }

        MessageDigest md = MessageDigest.getInstance(algorithm);
        FileInputStream in = new FileInputStream(file);

        byte[] dataBytes = new byte[1024];

        int nread = 0;
        while ((nread = in.read(dataBytes)) != -1) {
            md.update(dataBytes, 0, nread);
        }
        byte[] mdbytes = md.digest();

        return DatatypeConverter.printHexBinary(mdbytes).toLowerCase();
    }

    boolean isEmpty(File value) {
        return value == null || value.length() == 0;
    }

    static boolean checkBeforeReadfile(File read) {
        if (read.exists()) {
            if (read.isFile() && read.canRead()) {
                return true;
            }
        }

        return false;
    }
}

参考サイト

  1. Task (JavaFX 8)
  2. How to use the return value of call method of Task class in Javafx - Stack Overflow
  3. bitwalk123/HashCalc