vaguely

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

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を入れたときの処理について追うことができませんでしたが、
こちらについては別途調べてまとめたいと思います。

妻とのこれまで。そしてこれから。 - 妻・夫を愛してるITエンジニア Advent Calendar 2016

※2016.12.18
後半慌てて書いたため、「終わりに」追記・修正しました。

はじめに

この記事は妻・夫を愛してるITエンジニア Advent Calendar 2016の17日目の記事です。

タイトル通り、妻とのこれまでとこれからの話をつらつらと。

略歴

私が大学三回生の頃だったので、もう10年経過しているのですねぇ。
中国の南通という街に短期留学したときに会ったのがきっかけです。

妻は日本語が話せず、私は英語が得意ではなかったので、今からするとどうしてこうなったと思わなくもないww

私が大学卒業後に 上海留学 -> 現地で就職 を経て一緒に暮らすようになり、仕事の都合で大連に引っ越して入籍。
3年ほど前に日本に帰国し、去年子どもが生まれて今は3人で暮らしている、といった流れです。

会話

語学留学の結果私が多少は中国語が話せることと、日本語を真面目に教えていない、ということもあって普段の会話は中国語です。
とはいえ私は話をするのが得意ではないので、基本的には妻が延々と話してくれるのをうんうんそうだね(全然理解できてない)ということが大半(苦笑)。

時々私が英語を勉強したい!と思って英語で話しかけたりしますが、大抵は無視されて中国語で帰ってきますorz
ま、いいんですけどね。

食事

文化が違うことで問題になりがちなのが食事だと思います。

が、私達の場合は嫌いな物が少ないからか、あまり問題にはなっていません。
定番の納豆も、むしろ妻のほうが積極的に買ってきたりして驚いた覚えがあります。

私の方は、香菜(パクチー)がちょっと苦手でしたが、真夏の朝から麻辣烫(雑な説明:辛いスープに具を入れて食べる、簡易の鍋みたいな食べ物)に別盛り香菜を食べるうちに慣れてきましたw
蘭州拉麺には香菜が入っていないと物足りないですねぇ。

唯一食べ物に絡んで困ったことといえば、妻が辛いもの好きで私はそうでない、ということでしょうか。

特に日本だとお店でピリ辛とか辛いとか書いていても食べてみるとそうでなかったりするので、そういうときは私が怒られます(解せぬ)。

家事

私は基本的に妻がやらないことをやる、というスタンスで、具体的に言えば朝のゴミ捨てとか食器洗いなどが私の作業となります。
ただ、明示的に決まっているわけでもないので妻がやってくれることもあれば、私が上記以外をやることもあります。

家事に限らず、なんとな~くそれなりにうまく回っている気がすることが、長く一緒にいられる理由なのかもと思ったりはします(向こうはガマンしてくれてるかもですが)。

あ、料理については妻が炒め物を始めとした中華料理しか作らないので、 日本料理的なものとかスパゲッティ(ほぼインスタントの調理と変わらないレベルですが)とかが食べたい、となると外食または私が作ることになります。

終わりに

皆さん惚気けている中で、惚気けているのだかそうでないのかわかりづらい内容になってしまいました…。

なんだかんだ10年一緒にいるので、なんとなくいつも当たり前にそばにいる、空気のような存在になっているような気もします。
しかし空気がなければ死んでしまうわけで。

もう少し恋人らしいあれこれもやってみても良いかな、と思いつつ、私としてはこんな生活がこれからも続いていってくれたらな、と思っています。

アドベントカレンダーのテーマであるITエンジニアにはほぼ理解がなく、私の勉強会参加とかで(尊重はしてくれますが)喧嘩したりはしますが😅

我爱你、老婆😁

【Unity】AndroidでNativeのGUIやら何やら追加したい - Androidその2 Advent Calendar 2016

はじめに

この記事はAndroidその2 Advent Calendar 2016の16日目の記事です。

UnityでAndroid用のアプリを作る場合に、Unity側で用意されている機能だけでは足りないなどの理由で、ネイティブのプラグインを作成したい場合があります。

今回はこの辺りの記事で書いたようなClassのファイルだけでなく、Layoutや画像などを追加する方法を調べてみました。

jarに画像を追加する

この記事と同じように、jarファイルをまず作成してプラグインとして利用する方法を試してみました。

例えば プロジェクトのルートフォルダ/app/src/main/assets に「images」というディレクトリを作成して、その中に入っているJPGファイルをjarファイルに追加したい場合は プロジェクトのルートフォルダ/app/src/build.gradle を下記のように作成します。

build.gradle

apply plugin: 'com.android.library'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.0"
    defaultConfig {
        minSdkVersion 19
        targetSdkVersion 25
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    
    sourceSets {
        main {
            resources{
                srcDir "src/main/assets/images"
                resources.includes = ['**.jpg', '**.JPG', '**.png', '**.PNG']
            }
        }
    }
    
}
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.0.1'
    compile files('libs/classes.jar')
}
task clearJar(type: Delete) {
    delete 'release/' + 'androidplugin01.jar'
}
task exportJar(type: Copy) {
    from('build/intermediates/bundles/release/')
    into('release/')
    include('classes.jar')
    rename('classes.jar', 'androidplugin01.jar')
}
exportJar.dependsOn(clearJar, build)

ビルド後に作成されるandroidplugin01.jarの容量が、画像分増えていれば成功で、
あとは「AssetManager assetManager = currentActivity.getResources().getAssets();」などでアクセスできます。

ただし、layout.xmlだのあれこれ追加したいと思い始めると、やっぱりこれは面倒な気がします…。
(これはこれで使い方によっては便利なのだとは思うのですが)

UnityでAndroidProjectをエクスポートする

Unity5から(多分)Androidのビルドとして、apkファイルを直接出力するだけでなく、AndroidProjectをエクスポートすることができるようになりました。

方法はBuild Settingsで2つの設定をするだけでできます。

  1. 「Build System」を「Gradle(New)」に変更する
  2. 「Export Project」にチェックを入れて「Export」ボタンを押す

するとフォルダ指定を求められるので、適当にフォルダを作成して、それを指定します。
なおUnityProjectと同じフォルダにすることはできません(UnityProject内のフォルダとかならOK)。

f:id:mslGt:20161216233047j:plain

ネイティブの関数を呼ぶ

さて、出力されたAndroidProjectを開いてみると、 プロジェクトのルートフォルダ/src/main/java/jp/masanori/plugintest (jp/masanori/plugintestはパッケージ名)に、UnityPlayerActivity.javaがあります。

これはWindowsだと下記の場所にあるファイルと同じ内容です。

C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Source\com\unity3d\player UnityPlayerActivity.java

プラグインの関数を呼ぶときと同じく、このUnityPlayerActivityをUnity側から呼んでやればアクセスできそうです。

Unity

ということでまずはUnity側。

PluginCtrl.cs

using UnityEngine;

namespace Assets.Scripts
{
    public class PluginCtrl : MonoBehaviour
    {
        private readonly string PluginClassPath = "jp.masanori.plugintest.UnityPlayerActivity";

        public void RequestFilePathList()
        {
            using (var androidJavaClass = new AndroidJavaClass(PluginClassPath))
            {
                androidJavaClass.CallStatic("requestFilePathList");
            }
        }
    }
}

このような関数を、例えばボタンが押されたなどのタイミングで呼ぶことにします。

MainCtrl.cs

using UnityEngine;
using UnityEngine.UI;

namespace Assets.Scripts
{
    public class MainCtrl : MonoBehaviour
    {
        public GameObject PluginCtrlObject;
        public Button ImageButton;
        private PluginCtrl pluginCtrl;

        private void Start ()
        {
            pluginCtrl = PluginCtrlObject.GetComponent();
            ImageButton.onClick.AddListener(OnImageButtonClicked);
        }

        private void OnImageButtonClicked()
        {
            pluginCtrl.RequestFilePathList();
        }
    }
}

AndroidManifest.xml

< ?xml version="1.0" encoding="utf-8"? >
< manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="jp.masanori.plugintest"
    xmlns:tools="http://schemas.android.com/tools"
    android:installLocation="preferExternal"
    android:versionCode="1"
    android:versionName="1.0" >
    < permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/ >
    < permission android:name="android.permission.READ_EXTERNAL_STORAGE" / >
    < supports-screens
        android:smallScreens="true"
        android:normalScreens="true"
        android:largeScreens="true"
        android:xlargeScreens="true"
        android:anyDensity="true"/ >
    < application
        android:theme="@style/UnityThemeSelector"
        android:icon="@drawable/app_icon"
        android:label="@string/app_name"
        android:debuggable="true" >
        < activity android:name=".UnityPlayerActivity"
                  android:label="@string/app_name" >
            < intent-filter >
                < action android:name="android.intent.action.MAIN" / >
                < category android:name="android.intent.category.LAUNCHER" / >
            < /intent-filter>
            < meta-data android:name="unityplayer.UnityActivity" android:value="true" / >
        < /activity >
    < /application >
< /manifest >

C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer\Apk にあるAndroidManifest.xmlをベースにしていますが、UnityProject側に入れる必要は無いかもしれません。

Android

AndroidProjectとして出力したあと、UnityPlayerActivity.javaを編集します。

UnityPlayerActivity.java

package jp.masanori.plugintest;

import com.unity3d.player.UnityPlayer;
import android.app.Activity;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;

public class UnityPlayerActivity extends Activity
{
    protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code


    private static void requestFilePathList(){
        // TODO: 画像ファイルへのアクセス.
    }

    // Setup activity layout
    @Override protected void onCreate (Bundle savedInstanceState)
    {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);

        getWindow().setFormat(PixelFormat.RGBX_8888); // <--- This makes xperia play happy

        mUnityPlayer = new UnityPlayer(this);
        setContentView(mUnityPlayer);
        mUnityPlayer.requestFocus();
    }
~省略~

これでUnity側でボタンが押されたときに「requestFilePathList()」が呼ばれるようになるので、あとは通常のAndroidアプリと同じようにassetsフォルダの画像にアクセスすればOKです。

もし画像をUnity側から操作したい場合は、Filesフォルダなどに画像をコピーする必要があるかもしれません。

DataBindingを使う

エクスポート後のProjectは通常のAndroidアプリのProjectとほぼ同じです。

なので、DataBindingを使うこともできます。

まずはLayoutを作成します。

native_button_layout.xml

< ?xml version="1.0" encoding="utf-8"? >
< layout xmlns:android="http://schemas.android.com/apk/res/android" >
< RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    < Button
        android:text="@string/native_button_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_alignParentStart="true"
        android:layout_marginStart="65dp"
        android:layout_marginTop="82dp"
        android:id="@+id/native_button" / >
< /RelativeLayout >
< /layout >

DataBindingを有効にします。

build.gradle

// GENERATED BY UNITY. REMOVE THIS COMMENT TO PREVENT OVERWRITING WHEN EXPORTING AGAIN
buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'
    }
}

allprojects {
   repositories {
      flatDir {
        dirs 'libs'
      }
   }
}

apply plugin: 'com.android.application'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}
android {
    compileSdkVersion 25
    buildToolsVersion '25.0.0'

    defaultConfig {
        targetSdkVersion 25
    }

    lintOptions {
        abortOnError false
    }
    signingConfigs { release {
        storeFile file('C:/Users/masanori/.android/debug.keystore')
        storePassword 'android'
        keyAlias 'androiddebugkey'
        keyPassword 'android'
    } }

    buildTypes {
        debug {
            jniDebuggable true
        }
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-unity.txt'
            signingConfig signingConfigs.release
        }
    }
    dataBinding {
        enabled = true
    }
}

UnityのViewに追加します。

~省略~

import jp.masanori.plugintest.databinding.NativeButtonLayoutBinding;

public class UnityPlayerActivity extends Activity
{
    protected UnityPlayer mUnityPlayer; // don't change the name of this variable; referenced from native code

    private static void requestFilePathList(){
        // TODO: 画像ファイルへのアクセス.
    }
    // Setup activity layout
    @Override protected void onCreate (Bundle savedInstanceState)
    {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);

        getWindow().setFormat(PixelFormat.RGBX_8888); // <--- This makes xperia play happy

        // DataBindingを取得してmUnityPlayerに追加.
        NativeButtonLayoutBinding binding = DataBindingUtil.setContentView(this, R.layout.native_button_layout);
        mUnityPlayer.addView(binding.getRoot());

        mUnityPlayer = new UnityPlayer(this);
        setContentView(mUnityPlayer);
        mUnityPlayer.requestFocus();
    }

ポイントとしてはDataBindingはmUnityPlayerに追加する、ということでしょうか。

「setContentView(mUnityPlayer);」の後にDataBindingの処理を行うと、Unity側の画面が消えてネイティブ側の画面のみが表示され、悲しい気持ちになります。

また、下記はプラグイン作成時の注意ではありますが、今回の場合も気をつけた方が良いかもしれません。

おわりに

iOSのネイティブプラグインは以前からUnityのビルド後に追加する形であったため、Androidでも同じようにできる、というのは便利ですね。

ただ多くの場合jarファイルのように一つにファイルがまとまることがないため、複数プロジェクトで使いまわしする場合などは不便かも?

そもそもUnity側で解決できるならそちらでやったほうが…。

とまぁ微妙なところもありますが、適材適所、便利に問題に対応していくのが良いかと思います。

参考

RiderでUnityアプリ開発するときに詰まったとことか - JetBrains Advent Calendar 2016

はじめに

この記事はJetBrains Advent Calendar 2016の11日目の記事です。

まだEarlyAccessProgramの段階ではあるものの、C#用のIDEであるRiderが一般ユーザーでも使用可能になりました。

早速UnityのScriptを書くのに使ってみたところ、いくつか引っかかったところがあったので書き残しておきます。

※この記事は2016.12.11時点のもの、かつRiderは正式版ではないため、今後の改修で不要になるかもしれません。

Monoのインストール

Windowsの場合だと(おそらく).Net Frameworkが使われるため必要ないかもですが、Macの場合はMonoをインストールする必要があるようです。
(最初開いた瞬間にエラーまみれでクラクラきた思い出…)

http://www.mono-project.com/

MonoはUnityで使うMono Developにも含まれていると思いますので、そちらを使うのも良さそうです。 が、Unityを複数バージョンインストールする場合などはややこしいので、特に理由がなければ別途インストールしてあげる方が良いかと。

プラグインのインストール

Unity用のプラグインが作成されています。

https://github.com/JetBrains/Unity3dRider

使い方はUnityプロジェクトのAssets\Plugins\Editor\JetBrainsに設置するだけ。

これが無いとどうなるか、というと、最初にMono Developなどで生成したソリューションファイルを開いたときは問題なくても、 Unity側で何かを変更した場合などにエラーまみれになったりします。

次のソリューションファイル生成と合わせて、面倒ではあるもののRiderを使うのであれば現状プラグインのインストールは必須と言えそうです。

ソリューションファイルの生成

Mono Develop、Visual Studioを使う場合、Unityエディタ上でスクリプトファイルをダブルクリックするとソリューションファイル(.sln)などが存在しなければ自動で生成した上でそのプロジェクトを開きます。

Riderの場合はその方法だと生成されず、ソリューションが見つからないとエラーになります。

ではどうするか。

上記のUnity用プラグインをインストールすると、メニューのAssets以下に「Open C# Project in Rider」という項目が追加されるので、これをクリックすることでソリューションファイルなどを生成できます。

ソリューションファイルさえ生成しておけば、あとはスクリプトファイルのダブルクリックでもプロジェクトを開くことができるようになります。

Unity Support Plugin

定着しているからなのか、誰も気にしていないからなのかは分かりませんが、Unity用のプラグインはもう一つあります(Resharper用に作成されたもののようです)。

Windowsの場合はFile/Settings/Pluginsの「Install JetBrains plugin」からUnityで検索すると以下が見つかると思います。

https://resharper-plugins.jetbrains.com/packages/JetBrains.Unity/

このプラグインをインストールすると、StartやUpdateといった、Unity標準のイベント関数に対して、 どこからも呼ばれてないよとWarningがでることが防げます。

Sharderの編集

Sharderも編集自体は可能です。

が、カラースキーマは(デフォルトでは)用意されていないようなので、メモ帳などで開いた場合と変わらない感じです。

自作してみようかと思ったのですが方法がわからず…。
ここは今後解決できたら投稿したいと思います。

命名規則

デフォルトの命名規則

命名規則については、Unityエディタからクラスを作った場合は特に以下のような点でWarningが出ます。

namespace: (ファイルがAssets/Scriptsにある場合) namespace Assets.Scripts を指定する。

namespace Assets.Scripts
{
    public class MainCtrl : MonoBehaviour
    {
    }
}

※2017.04.24更新
2017年4月現在では、
プラグインを入れているとWarningが出なくなるようです。

public変数: Pascal方式で書く

public string PublicText;

private変数: _(アンダースコア)始まりのCamelCase

private string _privateText;

enum: Pascal方式で書く

private enum TestEnum{
    Num0 = 0
    , Num1
    , Num2
}

命名規則の変更

namespaceはともかく、変数の命名規則は何がベースになっているのかが少し気になりました。
(特にprefixとしてアンダースコアを付けるprivate変数)

Microsoft命名規則だとprefixは禁止されていますね。

ということでprivate変数を、prefix無しのCamelCaseに変更してみます。

  1. まずprefix無しのprivate変数を用意して、左に出てくる電球をクリックします。

  2. Inspection: “Inconsistent Naming” -> Change naming rule ‘Instance fields (private)’ をクリックします。

  3. Rule Settingsウインドウが開きます(しばらく待っても表示されない場合は、ウインドウ切り替えて開いてないか確認してみてください)。

  4. 「Name Prefix」の「_」を削除して「Save」ボタンをクリックします。

※2017.04.24更新
2017年4月現在のバージョンでは、
右クリックからではなく、

Preferences > Editor > Code Style > C#

にある「Naming」タブから変更ができるようになっています。

その他

スクリプトファイルの追加

ここまでの設定が完了していたら、Unityから追加してもRiderから追加しても問題なく反映されると思います。

ただMonoBehaviourを継承するクラスが必要ならUnityから追加した方が良く、
interfaceなどクラス以外を追加したい場合はRiderから行った方が良いと思います(そもそもUnityから追加できるのがクラスだけなので)。

ショートカット

これはMacの話ですが、ショートカットをVisual Studioなどに設定すると、置換のショートカットが「Command + H」に設定されます。

が、実際にやってみるとウインドウが最小化されますorz (OS側のショートカットと重なっているため)

そのため、設定のKeymapから適当な組み合わせに変更してあげる必要があります。

おわりに

まだ開発版ということで、複数の変数にエラーがあるとそれらを修正しても画面が赤いまま、といったエラーは見受けられるものの、かなり良い感じで使えています。

デバッグの機能はまだ難しいようですが、スクリプトの編集という機能においては、入力補完がバリバリ効いてかなり便利です。

普段IntelliJ IDEAやAndroid Studioなどを使っている方は、ぜひ触ってみると良いのではないかと思います。

参考

子育てとエンジニア -子育てエンジニア Advent Calendar 2016

はじめに

この記事は子育てエンジニア Advent Calendar 2016の6日目の記事です。

現在1歳4ヶ月の息子が生まれてから変わったことをつらつらと書いてみることにします。

変わったこと

一番大きいのは時間でしょうか。

どうしても子供が起きている間は他のことに時間が割きづらいので、プライベートで開発したりするのは寝静まったあとにしています。 もしくは会社帰りにカフェなどに寄るとか。

フラッと立ち寄れるコワーキングスペースがあると良いのですが、ここ和歌山ではそんな便利なものはなく…。

勉強会も同じで、妻に子供を任せて割りと自由に参加させてもらってはいますが(妻に感謝)、
やっぱりもくもく会とか頻度が高くなってきたりすると参加を取りやめたり、ということはあります。

ただ、今は直接勉強会に参加できなかったとしても、Twitterなどに情報を流してくれる方がいたり、
VRを中心としてインターネットを通して参加できるイベントが増えてきていてありがたい限りです。

来年ですが、Swift TweetsTwitterを使って行う勉強会、ということで、どんなことになるのかとても楽しみにしています。
(その前に今のSwiftを勉強しておかなきゃですが)

直接会場に足を運んで、時には発表したり、周りの誰かとお話する。
それが素晴らしいことには間違いありませんが、それ以外にも参加できる方法が増えていってくれると良いな、と思います。

グッズ

最近買ってちょっと便利なものを2つほど。

  1. 静音マウス ま、マウスのクリック音で起きるということはないのですが、やっぱりカチカチいうと気になるので…。

  2. Chromecast 購入後わりとすぐに4K対応版が出てビミョーな気持ちにはなりましたが、初期設定の他はWi-Fi環境で接続している端末なら何もインストールなしで操作できてしまうので結構良いです。

普段はYoutubeばかりですが、PlayStoreでアンパンマンの映画もあるようなので購入しても良いかも。
ちなみに先日買ったMadMaxはまだ観られていません。

終わりに

あまりポジティブな内容にはならなかった気もしますが、この一年で(おそらく)失われているものもたくさんある一方で、得たものがとても多いと感じています。

まぁべったりひっついてくれるのも今だけで、その内「くせーよオヤジ~」とか言って離れていくんだろうなぁ。
ちなみに将来本当に息子がそんなことを言った場合は、地獄のゆりかごを見舞う予定です(๑•̀ㅂ•́)و✧

それはともかく、押し入れにLeapMotionやらArduinoやら眠っているので、息子がもう少し大きくなったら一緒にプログラミングして何か作ってみるのも面白いかも、と思ったりしています(やりたがったらの話ですが)。

とりとめない内容になりましたが、今日も我が家は平和である、ということでした。

明日はseikoudoku2000さんの予定です。
よろしくお願いいたしますm( )m

f:id:mslGt:20161126104937j:plain