vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Unity】Windows Store AppでUniRxのIObservableを使う

はじめに

ハマったのでメモ。

※今回の方法でとりあえず動作することは確認しましたが、正しい対処法かどうかは保証できませんのであしからず。

UniRxの一部のinterfaceは、.Net4.0以降のinterfaceと競合するものがあり、Windows Store App(以下WSA)用にビルドする場合にエラーとなる場合があります。

ここでは名前空間などを使用せずに、「UniRx.IObservable」なら「IObservable」としてね、とありました。

ところが、そのような方法をとっても「IObservable」が見つからないとエラーとなり、うまくビルドできませんでした(Unity5.5.0f3を使用)。

あれこれ試したところ、一応これでビルドができるようになりました。

.Net4.0との切り分け

まず.Net4.0の場合に、UniRxではなくSystem.IObservableが呼ばれるようにしてみます。

#if NETFX_CORE || NET_4_0
using System;
#endif

using UnityEngine;
using UniRx;
using UnityEngine.UI;

public class MainCtrl : MonoBehaviour
{
    public Button CompleteButton;
    private void Start ()
    {
        CompleteButton.onClick.AsObservable()
            .Subscribe(_ =>
            {
                // ボタンが押されたら一旦非表示.
                CompleteButton.gameObject.SetActive(false);
                ExecuteSomething().Subscribe(result => {},
                    () =>
                    {
                        // 一定時間経過後に再表示.
                        CompleteButton.gameObject.SetActive(true);
                    });
            });
    }
    private IObservable ExecuteSomething()
    {
        return Observable.Create(observer =>
        {
            Observable.Timer(System.TimeSpan.FromSeconds(2d))
                .Subscribe(tim => { }, () =>
                {
                    observer.OnNext(0);
                    observer.OnCompleted();
                });
            return new CompositeDisposable();
        });
    }
}
  • 「using System;」を分けているのは、「IObservable」の競合を避けるためです。

2017.01.31 7.21更新
ExcecuteSomethingの処理をNETFX_CORE、.Net4.0用に分けて書いていましたが、
内容が全く同じ(実行時に「UniRx.IObservable」と「System.IObservable」が自動で切り替わる)なため削除しました。

ただし、これだけだとまだエラーが発生していました。

で、あれこれ探っていたところ、下記のページを見つけました。

ここの中で、UniRxの「IObservable」を、「NETFX_CORE」環境では除外する提案がされています。
もしや…と思い、Assets/Plugins/UniRx/Scripts/System/IObservable.csを下記のように変更してみました。

IObservable.cs

// defined from .NET Framework 4.0 and NETFX_CORE

using System;

#if !(NETFX_CORE || ENABLE_MONO_BLEEDING_EDGE_EDITOR || ENABLE_MONO_BLEEDING_EDGE_STANDALONE
 || NET_4_0)

namespace UniRx
~省略~
  • 赤字の部分を追加しました。

おわりに

これでエラーが無くなり、無事ビルドすることができるようになりました。

ただしREADMEの内容を私が読み違えている恐れがあるため、これが正しい対処法かどうかは、分からない、という不安は残っています。

今後Unityが.Net4.0以上に対応するときには、この辺の問題も解決済みかもしれませんが、まぁ何かの役に立てば、ということでここに書き残しておきます。

参考

UniRxでObservableを作る・つなげる

はじめに

Observableは作れる!ということで(完全に思いつきで発言しています)、CreateやSelectManyを使ってみます。

なお、本来はRxJavaとUniRxの対応表を作ってみたい 1の続編にするつもりでしたが、今回はUniRxのみ扱います。

Observableを作る

例えば時間のかかる処理を行う関数で、戻り値としてIObservable<T>を返すことで、処理の完了を呼び出し側で受け取れるようにします。

private IObservable ExecuteSomething()
{
        return Observable.Create(observer =>
        {
            // 何らかの処理.
            observer.OnNext(0);
            observer.OnCompleted();
            return new CompositeDisposable();
        });
}

Observable.Create<T>{何らかの処理}でObservableを作ります。

  • <T>の型はintだけでなくstringやTexture2Dなども使用可能ですが、関数の戻り値IObservable<T>と、Observable.Create<T>、observer.OnNext(T)の型を合わせる必要があります。
     (というかOnNextで呼び出し元に値を渡すことができるので、そこで必要な値の型を指定します)
  • {何らかの処理}では戻り値としてDisposableを渡します。
     これは変数に入れて渡すことも可能ですが、Createのときに新規作成する必要があります  (Startなどで1回しか作成しないと、1回Completeした後Observable.Create直後に完了してしまうようになります)。
  • {何らかの処理}の中でobserver.OnNext(T)、observer.OnComplete()、observer.OnError(Exception)を呼び出すことで、呼び出し元に通知することができます。

呼び出し

このExecuteSomething()を呼び出すには、ExecuteSomething().Subscribe()を実行します。
Subscribeしないと{何らかの処理}の内容が実行されないので注意が必要です
(私はたまに忘れます\(^o^)/)。

ExecuteSomething()
    .Subscribe(
        result => {/* observer.OnNext実行時に呼ばれる(resultはOnNext(T)の引数) */},
        error => {/* observer.OnError実行時に呼ばれる(errorはOnError(Exception)の引数) */},
        () => {/* observer.OnComplete実行時に呼ばれる */});
  • 上記はOnNext、OnError、OnComplete全て書いていますが、不要な場合はOnError、OnCompleteを省略できます。

Error

{何らかの処理}の中でErrorが発生した時に呼び出し元に通知が欲しい場合はobserver.OnError(Exception)を実行します。

例えば下記の場合は引数としてFileNotFoundExceptionを渡します。

private IObservable LoadTexture(string path)
{
    return Observable.Create(observer =>
    {
        if (!File.Exists(path))
        {
            observer.OnError(new FileNotFoundException("ファイルがないアルヨ"));
            // OnErrorを実行しても処理自体は止まらないのでreturnで処理を中断.
            return new CompositeDisposable();
        }
~省略~
    });
}

なお、OnErrorを呼び出し元に返すためには呼び出し元、呼び出される側(ExecuteSomething、LoadTexture)の両方でOnErrorの処理を書いておく必要があり、どちらかが書かれていない場合は通常のErrorと同じように扱われます。

ここは注意が必要かもしれません。

private IObservable LoadTexture(string path)
    {
        return Observable.Create(observer =>
        {
            // パスから画像読み込み.
            var newTexture = new Texture2D(1, 1);
            newTexture.LoadImage(File.ReadAllBytes(path));
            observer.OnNext(newTexture);
            observer.OnCompleted();
            return new CompositeDisposable();
        });
    }

Observableをつなげる

例えば画像を読み込んで、それをMaterialのTextureにセットする処理を行う場合。

それぞれ下記のような関数を作ってみました。

private IObservable LoadTexture(string path)
{
    // パスから画像を読み込む.
    return Observable.Create(observer =>
    {
        if (!File.Exists(path))
        {
            observer.OnError(new FileNotFoundException("ファイルがないアルヨ"));
            return new CompositeDisposable();
        }
        var newTexture = new Texture2D(1, 1);
        newTexture.LoadImage(File.ReadAllBytes(path));
        observer.OnNext(newTexture);
        observer.OnCompleted();
        return new CompositeDisposable();
    });
}
private IObservable AttachMainTexture(Texture targetTexture)
{
    // Materialに画像をセットする.
    return Observable.Create(observer =>
    {
        TargetMaterial1.SetTexture("_MainTex", targetTexture);
        observer.OnCompleted();
        return new CompositeDisposable();
    });
}

これを実行する場合に、それぞれをSubscribeすることもできますが、 下記のように一纏めにすることができます。

// LoadTextureを呼び出して画像を読み込む.
LoadTexture(Application.dataPath + @"/files/image1.png")
    // LoadTextureのOnNextで渡された画像を使ってAttachMainTextureを呼び出す.
    .SelectMany(texture => AttachMainTexture(texture))
                .Subscribe(
                    result => Debug.Log("OnNext"),
                    () => Debug.Log("finished"));
  • あるObservableの処理が終わったあと、OnNextで渡された値を使ってそのまま別のObservableをSubscribeしたい場合、「SelectMany」を使うことができます。
  • 同じものを呼び出したい場合などは「Concat」を使います。
  • SubscribeのOnNext、OnError、OnCompleteは後から読んでいるAttachMainTextureのものが反映されるため、例えばLoadTextureのOnCompleteのタイミングで何かをしたい、ということであればSelectManyなどを使わず分割した方が良さそうです。

参考

【Unity】ScriptからMaterialにTextureを設定する

はじめに

UnityのScript(C#)からMaterialにNormalmapなどのTextureを設定する方法をメモっておきます。

MaterialにTextureをセットする

例えばMaterialにNormalmapのTextureをセットするには下記を実行します。

public Texture NormalmapTexture;
public Material TargetMaterial;

private void Start(){
    TargetMaterial.SetTexture("_BumpMap", NormalmapTexture);
}

「SetTexture」の第一引数にプロパティ名を、第二引数に設定するTextureを指定します。

他にもDiffuse(オブジェクトのベースカラー)を設定したい場合は「MainTex」、Specularなら「SpecGlossMap」(Materialの種類を「Standard(Specular setup)」などに変更する必要があります)で設定できます。
※Diffuseについては「Material.mainTexture」でも設定できます。

さて、このプロパティ名は何を参照すればよいでしょうか。

ShaderのEditorを開く

  1. まずInspector上で変更対象のMaterialを開き、Material名の上で右クリック -> Edit Shader を開きます。

f:id:mslGt:20170118073249j:plain

  1. 「Properties」から名前を探します(左側がプロパティ名です)。

f:id:mslGt:20170118073303j:plain

参考

2016 -> 2017

今週のお題「2017年にやりたいこと」

大晦日ハッカソンの休憩がてら今年の振り返りと来年の目標をば。

atnd.org

2016年の振り返りと2017年について

発表について

Twitterなどで書いていたかは覚えていませんが、2016年は発表の場にどんどん挑戦してみよう、と考えていました。

ブログでの記録を見ると7回程。

発表の機会をくれた関西モバイルアプリ研究会、Kansai.kt、umeda.apkの方々には改めて感謝するとともに、
来年もどんどん挑戦させてほしいなと考えています。

また、これまで話を聴いたり見ているだけだった勉強会にも、発表者として参加できるチャンスを狙っていきたいと思います。

一方で課題として、あまり処理の深い部分にたどり着けていないのでは、というところがあります。

Kansai.kt #2でDecompileに触れましたが、2017年は処理が裏でどのように動いているのか、なぜそうなっているのか、
という部分にも突っ込んでみたいと思います。

公開したアプリについて

これもブログ頼りですが、個人で作成したアプリをストアに公開したのも今年ですね。

【Android】リリースしました(和歌山トイレマップで遊んでみる 8) - vaguely

随分ほったらかしなので、これの更新もしなきゃですが、もう一つWebで何か公開してみたいと思っています。
(というか去年もWebやりたいとか言ってたような気はしますが...)

ただ、何を作るのかというのが決まっていないため、まずはそこからかなぁ。

Advent Calendarについて

2015年と比較して、結構たくさん参加させてもらいました。
ブログに書いたのは下記ですが、Twitterなどで参加した朝こざけ #asakzk Advent CalendarBeer Advent Calendarもあります。

日付の縛りがあるため大変なときもありますが、参加すると楽しいですね。

今年は前もって参加登録していたものもありますが、大部分は12月に入って空きがあって、
かつ自分が記事を書けそうなところに登録する、というスタンスで進めていました。

このやり方は来年も続けようかな。

そこそこ埋まっている(ただし空きはある)ようなところに参加して、「今日を埋めたら続くかな?」とちょっとドキドキしつつ書くのも楽しかったので。

2017年もよろしくお願いいたします

というわけで、相変わらず思いつくまま全力でぶつかっていきたいと思います。

このブログをご覧の皆様、2016年は大変お世話になりました。

そして2017年も何卒よろしくお願いいたしますm( )m

JavaFX + Apache POIでSpreadsheet操作 2

はじめに

前回の続きです。

今回は名前付きのセルから値を取得するなど、Apache POIを使ってSpreadsheetから値を取得したり、書き込んだりした内容をまとめます。

セルの名前からWorkbook上のセルの列番号を取得する

まずはWorkbookを開いて、セルの名前(「ID」)を元に対象セルの列番号を取得します。
さらに値が含まれる最大の行番号を取得して、IDの最大値を取得してみます。

FileInputStream fileStream = new FileInputStream(filePath);
// Workbookを開く.
Workbook workbook = WorkbookFactory.create(fileStream);
if(workbook == null){
    observer.onError(new Throwable("Workbookの取得に失敗しました。"));
}
// とりあえずSheet名は固定.
Sheet sheet = workbook.getSheet("Sheet1");
if(sheet == null){
    observer.onError(new Throwable("Sheetの取得に失敗しました。"));
}
// 「ID」という名前がつけられたセルを取得する.
Name name = targetWorkbook.getName("ID");
CellReference reference = new CellReference(name.getRefersToFormula());

//  対象セルの列番号を取得.
int idColumnNum = reference.getCol();
// 対象Sheetの値が入った最終行番号を取得する.
int lastRowNum = sheet.getLastRowNum();
int newId = 1;

// 0行目は項目名を入力しているため値は取得しない.
if(lastRowNum > 0){
    Row lastRow = sheet.getRow(lastRowNum);
    if(lastRow != null){
        Cell lastCell = lastRow.getCell(idColumnNum);
        // 数値を取得すると、「1.0」のような形になるため一旦float型で受取り、その後intに変換する.
        float lastId = NumParser.TryParseFloat(lastCell.toString());
        newId = (int)lastId + 1;
    }
}
else{
    // IDが未登録の場合は1を付与する.
    newId = 1;
}

セルの値を取得する

以前触れましたが、セルの値を取得するには「Cell.getStringCellValue」のように型を指定した上で行います。

この型が実際のセルの型と違っている場合はエラーが発生するため、「Cell.getCellType」で型を確認した上で値を取得していました。

しかし、POI ver.3.15ではこの「getCellType」がdeprecatedで表示されます。

ver.3系ではintが返るのですが、ver.4系ではこれが廃止され、enumが返る「getCellTypeEnum」が「getCellType」に改名される、という変更のためのようです。
とりあえずver.3.15では「getCellType」への対処方法が見つからなかったため、deprecatedでも気にせず使うか、上記のように「Cell.toString」とString型として一旦受け取るなどする必要がありそうです。

指定シート上のセルの名前からセルの列番号を取得する

上記のコードは、セルの名前がWorkbook全体に適用されている、またはWorkbook内に該当の名前が一つしかない場合は問題なく動作します。
しかし、セルの名前がSheet単位で登録されていて、Workbook全体で見ると同一の名前が複数登録されている場合は一番左にあるSheetの値が返ってしまいます。

CellReferenceがWorkbookではなくSheetから取得できれば良さそうですが、調べた限り難しいようでした。

あれこれ試した結果、Workbookに付与されている名前を一旦全て取得して、対象のシートのものだけを取り出すことで解決できました。
(もう少しスマートな方法があるような気はするのですが)

CellValueGetter.java

public class CellValueGetter {
    private int columnNum;
    public List getTargetCellValueList(Workbook workbook, String sheetName){
        // 指定されたSheet内のセルの名前を全て取得する.
        List targetNames = workbook.getAllNames();
        return targetNames.stream()
                .filter(filteredName -> filteredName.getSheetName().equals(sheetName))
                .map(object -> (Name)object)
                .collect(Collectors.toList());
    }
    public int getTargetCellColumnNum(List cellNameList, String targetCellName){
        // targetCellNameの名前が付けられたセルの列番号を返す.
        // 対象のNameを取得.
        Optional foundCellName = cellNameList.stream().filter(name -> name.getNameName().equals(targetCellName)).findFirst();

        // Nullでなければ列番号を取得.
        Optional foundColumnNum = foundCellName.map(cellName -> {
            CellReference reference = new CellReference(cellName.getRefersToFormula());
            if(reference == null){
                return -1;
            }
            int gotColmnNum = (int)reference.getCol();
            return gotColmnNum;
        });
        columnNum = -1;
        foundColumnNum.ifPresent(num -> columnNum = num);
        return columnNum;
    }
}

下記のように呼び出します。

// 指定したシート内の名前を全て取得する.
List allCellNameList = cellValueGetter.getTargetCellValueList(workbook, sheet.getSheetName());
// 「ID」と名付けられたセルの列番号を取得する.
int idColumnNum = cellValueGetter.getTargetCellColumnNum(allCellNameList, "ID");

セルの追加

取得したIDから次のIDを発行し、書き込みます。

// 現在値が入っている行番号最大値を取得する.
int lastRowNum = sheet.getLastRowNum();
int newId = 1;
if(lastRowNum > 0){
    Row lastRow = sheet.getRow(lastRowNum);
    if(lastRow != null){
        Cell lastCell = lastRow.getCell(idColumnNum);

        float lastId = NumParser.TryParseFloat(lastCell.toString());
        newId = (int)lastId + 1;
    }
}
else{
    // IDが未登録の場合は1を付与する.
    newId = 1;
}
// 行を追加する.
Row newRow = sheet.createRow(lastRowNum + 1);

if(newRow != null){
    // セルを追加する.
    newRow.createCell(idColumnNum).setCellValue(newId);
    newRow.createCell(titleColumnNum).setCellValue(title);

    // 現在の日付を取得.
    LocalDate currentDate = LocalDate.now();
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
    newRow.createCell(lastUpdateDateColumnNum).setCellValue(currentDate.format(formatter));

    FileOutputStream outputStream = new FileOutputStream(filePath);
    // Workbookへの書き込み.
    workbook.write(outputStream);
    outputStream.close();
}
  • 新しい行(値の入ったセルが一つもない行)、新しいセルに値を追加する場合はCreateを行う必要があります。
  • 今回更新日はString型でセットしていますが、POIでセルに値をセットする場合に扱うことのできる型にCalendarがあるため、日付型を使いたい場合はCalendar型にしてあげると良いかと思います。

終わりに

さて、これでID取得部分はできるようになりました。

次はこれを使ってもう一つのSpreadsheetに値を入れたり、シートの表示・非表示切り替えを行いたいと思います。

参考

Apache POI

Stream

Optional

Date

JavaFX + Apache POIでSpreadsheet操作 - JavaFX Advent Calendar 2016

はじめに

この記事はJavaFX Advent Calendar 2016の24日目の記事です。

以前下記のような記事を書いており、JavaFXApache POIを使ってSpreadsheet(Excel)を操作する、という意味ではその続編となります。

違いとしては以下のような操作を行うことにあります。

Spreadsheet

  • 複数のファイル(Book)にアクセスして値の入出力を行う。
  • 1つのファイル内にあるSheetの表示、非表示を切り替える。

JavaFX

  • もう一度SceneBuilderを使う。
  • JavaFXにTabPaneを追加し、Tab切り替えのEnable、Disableを行う。

なお今回の使用言語がJavaになっているのは、ただ単にJava8を使ってみたかったからです。

build.gradle

build.gradle

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.11'
    compile 'io.reactivex.rxjava2:rxjava:2.0.3'
    compile group: 'org.apache.poi', name : 'poi', version: '3.15'
    compile(group: 'org.apache.poi', name : 'poi-ooxml', version: '3.15') {
        exclude group: 'stax', module: 'stax-api'
    }
}
  • Rx系ではRxJavaFXも存在するのですが、とりあえず今回はRxJavaのみを使うことにします。
  • Spreadsheetの操作は以前と同じくApachePOIを使います。

SceneBuilderでGUIを作る

まずはGUI部分の作成から。

IntellliJ IDEAではFile -> Settings -> Languages & Frameworks -> JavaFXにSceneBuilderのexeを設定しておくと、 Textとして編集ができる他、GUIでボタンなどの追加ができるようになります。

MainForm.fxml

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

< ?import java.lang.String? >
< ?import javafx.collections.FXCollections? >
< ?import javafx.scene.control.Button? >
< ?import javafx.scene.control.ComboBox? >
< ?import javafx.scene.control.Tab? >
< ?import javafx.scene.control.TabPane? >
< ?import javafx.scene.control.TextArea? >
< ?import javafx.scene.control.TextField? >
< ?import javafx.scene.layout.AnchorPane? >

< TabPane maxHeight="768" maxWidth="1024" minHeight="768" minWidth="1024" prefHeight="768" prefWidth="1024" tabClosingPolicy="UNAVAILABLE" xmlns="http://javafx.com/javafx/8.0.66" xmlns:fx="http://javafx.com/fxml/1" fx:controller="MainCtrl" >
    < Tab fx:id="mainTab" text="Main" >
        < AnchorPane fx:id="mainPane" maxHeight="708" maxWidth="1024" minHeight="708" minWidth="1024" prefHeight="708" prefWidth="1024" >
            < TextField fx:id="idSheetPathField" layoutX="57.0" layoutY="35.0" prefHeight="51.0" prefWidth="760.0" />
            < Button fx:id="searchIdSheetButton" layoutX="884.0" layoutY="35.0" mnemonicParsing="false" onAction="#onSearchIdSheetButtonClicked" text="参照" />
            < TextField fx:id="docTitleField" layoutX="57.0" layoutY="160.0" prefHeight="51.0" prefWidth="760.0" />
            < ComboBox fx:id="docTypeCombobox" layoutX="57.0" layoutY="276.0" onAction="#onDocTypeComboboxSelected" prefWidth="150.0" / >
            < TextField fx:id="outputSheetPathField" layoutX="57.0" layoutY="389.0" prefHeight="51.0" prefWidth="760.0" / >
            < Button fx:id="searchOutputSheetButton" layoutX="878.0" layoutY="389.0" mnemonicParsing="false" onAction="#onSearchOutputSheetButtonClicked" text="参照" / >
            < Button fx:id="createDocButton" layoutX="765.0" layoutY="601.0" mnemonicParsing="false" onAction="#onCreateDocButtonClicked" text="生成" / >
        < /AnchorPane >
    < /Tab >
    < Tab fx:id="sheet1Tab" text="Sheet1" >
        < AnchorPane maxHeight="708" maxWidth="1024" minHeight="708" minWidth="1024" prefHeight="708" prefWidth="1024" >
            < TextArea fx:id="sheet1InputTextArea" layoutX="56.0" layoutY="59.0" prefHeight="200.0" prefWidth="900.0" / >
        < /AnchorPane >
    < /Tab >
    < Tab fx:id="sheet2Tab" text="Sheet2" >
        < AnchorPane maxHeight="708" maxWidth="1024" minHeight="708" minWidth="1024" prefHeight="708" prefWidth="1024" >
            < TextArea fx:id="sheet2InputTextArea" layoutX="56.0" layoutY="59.0" prefHeight="200.0" prefWidth="900.0" / >
        < /AnchorPane >
    < /Tab >
< /TabPane >
  • 「onAction」でイベント関数を登録するため、対象のGUI(ボタンとか)の親となるPaneに対して、「fx:controller」を指定しておく必要があります(今回はMainCtrlというクラスを指定しています)。

なおTabPaneの子どもとしてAnchorPaneを追加しているのですが、Androidでいう「match_parent」のようなものは無いのでしょうか。
それとも、その辺りの細かいUIの設定はCSSを使う、ということなのかしら。この辺りはまた次回にでも。

Main Class

まずApplicationを継承しているメインクラスからです。

処理の開始とfxmlのロードだけを行っています。

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.stage.Stage;
import javafx.fxml.FXMLLoader;
import java.io.IOException;

public class MainForm extends Application {
    public static void main(String[] args) {
        launch(args);
    }
    @Override
    public void start(Stage primaryStage) {
        FXMLLoader loader = new FXMLLoader();
        Alert alert = new Alert(Alert.AlertType.NONE , "問題が発生しました。" , ButtonType.CLOSE);
        try {
            Parent root = loader.load(getClass().getResourceAsStream("MainForm.fxml"));
            primaryStage.setScene(new Scene(root));
            primaryStage.show();
        }catch (IOException ex){
            System.out.println(ex.getMessage());
            alert.showAndWait()
                    .filter(response -> response == ButtonType.CLOSE)
                    .ifPresent(response -> Platform.exit());
        }
    }
}
  • エラーが起きたらAlertを出してアプリを閉じるようにしています。

Controller Class

次はコントロール用のクラスです。

ボタンやComboboxのイベントの検出などを行います。

MainCtrl.java

import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.stage.FileChooser;

import java.io.File;
import java.net.URL;
import java.util.ResourceBundle;

public class MainCtrl implements Initializable {
    @FXML private AnchorPane mainPane;
    @FXML private TextField idSheetPathField;
    @FXML private ComboBox docTypeCombobox;
    @FXML private TextField outputSheetPathField;
    @FXML private TextField docTitleField;
    @FXML private Tab sheet1Tab;
    @FXML private Tab sheet2Tab;
    @FXML private TextField sheet1InputTextArea;
    @FXML private TextField sheet2InputTextArea;

    private FileChooser fileChooser;

    private final static String FileTypeA = "TypeA";
    private final static String FileTypeB = "TypeB";

    private IdManager idManager;
    private int newId;

    @FXML
    public void onSearchIdSheetButtonClicked(){
        // ダイアログを表示して、指定されたファイルのパスをTextFieldにセット.
        File selectedFile = fileChooser.showOpenDialog(mainPane.getScene().getWindow());
        idSheetPathField.setText(selectedFile.getPath());
    }
    @FXML
    public void onSearchOutputSheetButtonClicked(){
        // ダイアログを表示して、指定されたファイルのパスをTextFieldにセット.
        File selectedFile = fileChooser.showOpenDialog(mainPane.getScene().getWindow());
        outputSheetPathField.setText(selectedFile.getPath());
    }
    @FXML
    public void onCreateDocButtonClicked(){
        // 必須項目が空ならそのまま.
        if(idSheetPathField.getText().isEmpty()
                || docTitleField.getText().isEmpty()
                || outputSheetPathField.getText().isEmpty()
                || docTypeCombobox.getSelectionModel().getSelectedIndex() < 0){
            return;
        }
        // Book1からIDを取得して、Book2にGUIで入力された値とともに挿入する.
        idManager.addNewId(docTitleField.getText(), idSheetPathField.getText())
                .subscribe(new Observer() {
                    @Override
                    public void onSubscribe(Disposable d) {
                    }
                    @Override
                    public void onNext(Integer integer) {
                        newId = integer;
                    }
                    @Override
                    public void onError(Throwable e) {
                        Alert alert = new Alert(Alert.AlertType.NONE, e.getMessage(), ButtonType.CLOSE);
                        alert.show();
                    }
                    @Override
                    public void onComplete() {
                        // TODO: Book2.xlsxに値を挿入.
                    }
                });
    }
    @FXML
    public void onDocTypeComboboxSelected(){
        switch(docTypeCombobox.getSelectionModel().getSelectedItem()){
            case FileTypeA:
                sheet1Tab.setDisable(false);
                sheet2Tab.setDisable(true);
                break;
            case FileTypeB:
                sheet1Tab.setDisable(true);
                sheet2Tab.setDisable(false);
                break;
            default:
                sheet1Tab.setDisable(true);
                sheet2Tab.setDisable(true);
                break;
        }
    }
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // ComboBoxにセットするListの生成.
        ObservableList fileTypeList = FXCollections.observableArrayList();
        fileTypeList.add(FileTypeA);
        fileTypeList.add(FileTypeB);
        docTypeCombobox.setItems(fileTypeList);

        fileChooser = new FileChooser();
        // デフォルトでカレントディレクトリが開くようにする.
        fileChooser.setInitialDirectory(new File(System.getProperty("user.dir")));
        fileChooser.setTitle("ファイルを選択");
        fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Spreadsheet", "*.xlsx", "*.ods"));

        idManager = new IdManager();
    }
}

ComboboxにItemをセットする

以前トライしたときはセットしたはずのGUIがNullになる、といった問題がありましたが、どうやらコードからComboboxに値をセットするには、 Initializableを継承して「initialize(URL url, ResourceBundle rb)」の中で実行する必要があるようです。

~省略~
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // ComboBoxにセットするListの生成.
        ObservableList fileTypeList = FXCollections.observableArrayList();
        fileTypeList.add(FileTypeA);
        fileTypeList.add(FileTypeB);
        docTypeCombobox.setItems(fileTypeList);
~省略~

Applicationを継承していないクラスでprimaryStageを取得する

ファイル選択用にFileChooserでダイアログを表示する(showOpenDialogを実行する)ためには、Stageを引数として渡す必要があります。

今回であればApplicationを継承しているメインクラスから受け取っても良いのですが、表示中のPane(AnchorPaneなど)のインスタンスを取得すれば、「mainPane.getScene().getWindow()」を引数として渡すことでshowOpenDialogが実行できるようになります。

FileChooserでデフォルトでカレントディレクトリを開く

FileChooserでダイアログを表示したときのデフォルトのディレクトリは、「setInitialDirectory」で指定することができます。

~省略~
        fileChooser = new FileChooser();
        // デフォルトでカレントディレクトリが開くようにする.
        fileChooser.setInitialDirectory(new File(System.getProperty("user.dir")));
~省略~

TabのEnable/Disable切り替え

javafx.scene.control.Tabのインスタンスを取得しておき、「setDisable」にTrueを渡せばOKです。
個人的にはDisableにするのにTrueを渡す、というのにちょっと違和感が...。

まぁ慣れの問題だとは思いますが。

~省略~
public class MainCtrl implements Initializable {
~省略~
    @FXML private Tab sheet1Tab;
    @FXML private Tab sheet2Tab;
~省略~
    @FXML
    public void onDocTypeComboboxSelected(){
        // Comboboxで選択された値に合わせてTabをEnable/Disable切り替え.
        switch(docTypeCombobox.getSelectionModel().getSelectedItem()){
            case FileTypeA:
                sheet1Tab.setDisable(false);
                sheet2Tab.setDisable(true);
                break;
            case FileTypeB:
                sheet1Tab.setDisable(true);
                sheet2Tab.setDisable(false);
                break;
            default:
                sheet1Tab.setDisable(true);
                sheet2Tab.setDisable(true);
                break;
        }
    }

終わりに

まだSpreadsheetに触れてもいませんが、長くなってきたので一旦切ります。

JavaFXで使えるというCSSや先に触れたRxJavaFXなども触ってみたいところですね。
今回のGUIもこのままだとTextFieldとボタンの高さがずれていたりして結構残念な見栄えになってしまっているので...orz。

それはそれとして、JavaFX Advent Calendar 2016はいよいよ明日が最終日。
skrbさん、よろしくお願いいたしますm( )m

参考

JavaFX

Alert

FileChooser

KotlinとNull安全と - null安全 Advent Calendar 2016

はじめに

この記事はnull安全 Advent Calendar 2016の20日目の記事です。

KotlinをベースにNull安全について思うことをつらつらと。

NullableとNonNull

まずNull安全とは何か、というところからですが、ここではNullを許容する変数(Nullable)と許容しない変数(NonNull)とが明確にわけられている、ということとして話を進めたいと思います。

// Nullable: 問題なし
val nullableVariable: String? = null

// NonNull: コンパイルエラー
val nonnullVariable: String = null

私としては変数がNullになり得る、ということよりも、変数にNullが渡された時点で即エラーとなる、というNonNullの仕様によるインパクトが大きいと感じます。

自分で書くコードについて考えると、NullableとNonNullとが別れていることで、Nullにならないことが明らかなのであればNullチェックをせず、Nullになり得る変数に対してのみNullチェックを行う、ということができるようになります。

こうすることでコード量を安全に減らすことができます。

もう一つ、ライブラリの呼び出しなど、自分で書いていないコードにアクセスする場合。

特にListViewのAdapterのように相手から自分のコードが呼ばれる場合、仕様を理解せずに引数を何でもNonNullにしてしまうと、IllegalStateExceptionなどの例外で苦しむハメになります。

例: http://mslgt.hatenablog.com/entry/2016/07/03/191230#20160703_getview_of_listview

// 第二引数をNonNull(convertView: View)にすると、実行時にNullが渡されてエラーが発生する
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View? {
    return convertView
}

そのためNonNullを使うためには、ただNullを渡さないようにするだけでなく、関数が呼ばれるときにどのような値が渡されるのかといった仕様まで正しく理解することが求められる、ということになります。

これらの理解が合わさると、シンプルに安全なコードが書けるようになるのでは?と考えています。

まぁ私はまだまだ例外起こしまくりですが(・_・;)

なおNullableの方は、状況によっては「if(x == null){ y = x.toString() }」などと書かずに「y = x?.toString()」とシンプルに書ける、というのが良いですね。

特にKotlinでは、エルビス演算子(?:)を使ってNullの場合に値を入れるのではなくreturnを実行できるなど、 シンプルに書くための仕組みがあれこれ用意されているのも良いです。

NonNull変数を追う

ではそんなNonNull変数をDecompileしてみるとどんなコードになるでしょうか。

IntelliJ IDEAの Tools -> Kotlin -> Show Kotlin Bytecode -> Decompile でDecompileしたコードを見ることができます。

まずこのようなclassと関数、変数を用意します。

class NullsafeMain : Application() {
    override fun start(primaryStage: Stage){
        var test: String = "test"
        test = null
    }
}

これをDecompileします。

public final class NullsafeMain extends Application {
   public void start(@NotNull Stage primaryStage) {
      Intrinsics.checkParameterIsNotNull(primaryStage, "primaryStage");
      String test = "test";
      test = (String)null;
   }
}

おや?

start関数の引数である「primaryStage」に対しては「@NotNull」が付与されたり、何やら「Intrinsics.checkParameterIsNotNull」なる関数が呼ばれていますね。

ただし、変数である「test」については特に何も行われていませんね。

何か追いかけ方を激しく間違えている気がしてきましたが、せっかくなので「Intrinsics」というclassを辿ってみます。

~省略~
public class Intrinsics {
    ~省略~
    public static void checkParameterIsNotNull(Object value, String paramName) {
        if (value == null) {
            throwParameterIsNullException(paramName);
        }
    }
    ~省略~
    private static void throwParameterIsNullException(String paramName) {
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();

        // #0 Thread.getStackTrace()
        // #1 Intrinsics.throwParameterIsNullException
        // #2 Intrinsics.checkParameterIsNotNull
        // #3 our caller
        StackTraceElement caller = stackTraceElements[3];
        String className = caller.getClassName();
        String methodName = caller.getMethodName();

        IllegalArgumentException exception =
                new IllegalArgumentException("Parameter specified as non-null is null: " +
                                             "method " + className + "." + methodName +
                                             ", parameter " + paramName);
        throw sanitizeStackTrace(exception);
    }
    ~省略~

渡されてきた引数(primaryStage)がNullの場合に、IllegalArgumentExceptionを投げてStackTraceにメッセージを出力する、ということのようです。

NonNullの引数にNullが渡された場合の例外はこの処理が実行されているのだと思います。

おわりに

Null安全の機能が必須であるか、と言われると、正直そうではないと思います。

ただ、コードをできるだけシンプルに、かつ安全に実装することを考えたときに、Null安全が強力なツールになるのではないかと思っています。

今回はNonNullの変数にNullを入れたときの処理について追うことができませんでしたが、
こちらについては別途調べてまとめたいと思います。