やる気駆動型エンジニアの備忘録

WPF(XAML+C#)の話題を中心に.NET/Android/CI やたまに趣味に関するブログです

(WPF + MVVM)Material Design In XAML でモダンなダイアログを表示する

先日、業務でMaterial Design In XAML を使用したダイアログ表示機能を作ったのでメモ。

Material Design In XAML って?

GoogleAndroid OS に採用されているデザインシステムである"Materail Design" をMicrosoftXAML で実現しますよというOSS プロダクトです。

参考ページ:http://materialdesigninxaml.net/

使用可能なGUI アーキテクチャにはWPF, UWP があります。
ここまでであればよくあるXAML 向けテーマなのですが、Material Design In XAMLAndroid の外観とほとんど同じ見た目にするだけでなく、XAML 向けアーキテクチャ専用のエラー通知や外観をカスタマイズするためのDataTemplate ももちろん使用できます。
さらに、使用したいスタイルやコントロールはデモプロジェクトを実行すればいつでも閲覧してコピペが可能です。

デモプロジェクトを実行するにはGitHub のMainDemo.Wpf を実行すればOKです。 要ビルドなのでリポジトリをクローンするなり、ZIP でダウンロードするなりでやってみてください。


余談ですが、個人的には上記のMainDemo.Wpf を実行しながらAdobe XD のAndoid のUI キットを使って画面設計したりワイヤーフレームを作ると実際に製作段階に入ったときにシームレスに移行できるのでオススメです。
もちろんXAML の知識は必要ですし、XD で独自のUI を作る場合はその工数も必要ではあります。
XAML + Adobe XD が使えて画面設計書と実際の画面デザインの差を埋めることに困っているという方向けですね。

動作環境

ダイアログの作成

デスクトップアプリにおいて必須といっても過言ではないダイアログ表示ですが、エンタープライズ向けだとこんなことがままあります。

Windows 標準でないダイアログを表示したい」

標準ダイアログでは実現できない使用がある場合は仕方ないですね。
そこでMaterial Design In XAML の登場です。

Material Design In XAML のダイアログは DialogHost を使用します。 DialogHost はダイアログをホストする(表示する領域、パネル)もので、ダイアログを表示するための方法をいくつか提供しています。
ダイアログの表示方法はMainDemo.Wpf を見ればわかりますのでここでは割愛します。
開発時に作るべきはこのダイアログの外観とViewModel です。
外観はUserControl を使用し、ViewModel はバインド可能な情報をプロパティとして持っていればOK です。
以下はダイアログ(UserControl) とそのViewModel です。

gist793013159cfac93d76e930dcfa9525d1

gistb8083d846942569d7e7d6374edc2dd8c

これらのダイアログを表示するためにウィンドウにDialogHost を配置します。
このDialogHost の配置の仕方次第でダイアログの表示領域が決まるのですが、今回はサンプルなので全体を覆うように表示するものとします。

gist17d320f707bb8bcf5bd92f621716c62c

DialogHost のIdentifier プロパティは複数のDialogHost が存在する場合にどこにホストするかの識別子です。
一意な名前であれば何でもいいですが、後で使用するということは覚えておきましょう。
なお、Identifier を使用しない方法もありますが、今回は説明しません。

ViewModel からダイアログを表示する

ダイアログが作成できたのでViewModel から表示してみましょう。
そのまま表示するのではMainDemo.Wpf と同じなので、MVVM アーキテクチャを使用して表示するようにします。
MVVM を使用したダイアログ(メッセージ)表示といえばServices パターンですね。
ということでIDialogService インターフェースとその具象クラスを作っていきます。

gist4df100caa40a1cfd2cc40ae88aecc55d

gist8f129f635c3738c8b6e1c472db367fa4

とりあえずQuestion メソッドを一つ作って、質問メッセージを表示するようにします。
他種別のメッセージを表示したい場合は、新たにメソッドを追加していく形になります。
ここで重要なのは戻り値がTask<bool> であること。
Windows 標準のMessageBox とは異なり、Material Design In XAML のダイアログは非同期で表示されます。
この辺りはUWP とおなじですね。
また、戻り値は以下の方法で返しています。

object result = await DialogHost.Show(dialog, _identifier);
return (result is bool selectedResult) && selectedResult;

DialogHost.Show の戻り値はobject 型ですが、この値はどこから設定されるのか?
正解は先ほど作成したダイアログのXAML にある以下の行です。

<Button Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}"
        Content="はい"
        IsDefault="True"
        Style="{StaticResource MaterialDesignFlatButton}">
    <Button.CommandParameter>
        <system:Boolean>True</system:Boolean>
    </Button.CommandParameter>
</Button>

DialogHost.CloseDialogCommand というコマンドが用意されていて、そのCommandParameter に設定した値がDialogHost.Show の戻り値となります。
今回のケースではXAML に直接bool 型の値を設定し、"はい"ボタンと"いいえ"ボタンのどちらを押下したかを識別できるようにしています。

さて、DialogService が作れたので、これを画面のViewModel で使用できるようにしましょう。

gist793fbb7ca9006cca375a48e92ed4f495

ダイアログの表示はIDialogService.Question メソッドを非同期で呼び出せばOK です。
アプリの終了処理もMVVM で書けばよかったんですが、ちょっと手抜きです。(ゴメンナサイ


余談その2なんですが、WPF はView のDataContext にViewModel を指定する際、Visual Studio の[書式]メニューから一覧を表示してViewModel を選択すると思います。
しかし、このとき一覧に表示されるにはViewModel にデフォルトコンストラクタが実装されていなければなりません。
デフォルトコンストラクタを実装してXAML デザイナーからViewModel を指定したい、けど、必要なインターフェースはコンストラクタの引数から引き渡したいというときは私はいつもデフォルトコンストラクタにObsolete 属性を指定しています。
こうすることでコード上ではViewModel のデフォルトコンストラクタ呼び出しができなくなります。
ただ、なぜXAML デザイナー上ではエラーが発生しないのかはよくわかりませんが、WPF がこれ以上進化しないことを考えればこの回避方法はわりとありな気がしています。


最後にViewModel の構築部分です。
今回の例ではPrism を使用しているのでBootstrapper でViewModel をインスタンス化しています。

gist43bbb1b00cc5e305126b27f38a4f7a25

DialogService のコンストラクタで渡している"MessageDialogHost" はView で指定したDialogHost のIdentifier プロパティの識別子です。

これで実行して、右上の閉じるボタンをクリックするとこんなメッセージダイアログが表示されます。
f:id:iyemon018:20181007191308p:plain

このようにMaterial Design In XAML を使用すれば、独自ダイアログも割と簡単に表示することが可能です。
また、ダイアログとして表示するUserControl、ViewModel をカスタマイズすればいろいろと応用することもできます。
Material Design In XAML はほかにもモダンなUI を構築するための仕組みがそろっているので、WPF を使用するのであれば一度見てみることをお勧めします。

今回使用したプロジェクトはこちら↓↓↓↓↓ github.com

Google Cloud Platform Vision API を.NET で使ってみた

先日、Google Cloud Platform のVision API を使用してOCR を試したんですが、思いのほか簡単にできたのでその使い方についてメモメモ。

※なお、この記事は 2018年5月13日 時点の内容です。最新の情報とは異なる可能性がありますのでご注意ください。

Google Cloud Platform - Vision API について

Google Cloud Platform は、Google 社が提供するクラウドサービスで、Vision API はそのGCP で提供されているサービスの一つです。

GCP のサービスは様々な言語から使用することができ、そのサンプルも充実しています。
今回は.NET(C#) でその機能を試してみます。

OCR の種類

Vision APIOCR を行うにあたって文字認識機能は以下の2種類が存在します。

  • TEXT_DETECTION
  • DOCUMENT_TEXT_DETECTION

これらの2つの最大の違いは解析する文字単位の精度です。
TEXT_DETECTION は単語単位、DOCUMENT_TEXT_DETECTION は文字単位で解析されます。
なので、より高精度な解析が必要な場合は DOCUMENT_TEXT_DETECTION を選択することになります。

料金体系について

クラウドサービスを使用するうえで料金体系は重要な要素の一つです。
Vision API の料金体系は以下のページを参照してください。

https://cloud.google.com/vision/pricingcloud.google.com

料金支払いの単位は"ユニット"と呼び、これは一つの解析機能を使用した場合、"1"ユニットとなります。
そして、大体の機能がひと月1,000 ~ 5,000,000(500万)ユニットまで使用すれば、1,000 ユニットにつき$1.50 です。
計算めんどくせーって方は計算ツールも提供さているので以下のページからどうぞ。
cloud.google.com

続きを読む

(Android)APK をCI ツールで生成する方法メモ

Android アプリのAPK ファイルの生成をCI ツールを使用して自動化する際のメモです。
Android Studio だとメニューからAPK を生成できますが、例えばGit のmaster ブランチへpush されたらAPK を生成する といったことができれば、デプロイも簡単になります。

今回は以下のような環境下でどうすればAPK の生成を自動化できるか?という観点で調査・実践した結果をまとめています。

開発環境

開発用PC
  • OS : Windows10 Enterprise 64bit
  • 開発環境 : Android Studio 3.1.1
  • Gradle : 3.3
サーバー(オンプレミス)

目的

業務用のAndroid アプリ開発で、APK ファイルの生成を自動化したい。

アプリの署名について

Android アプリの全てのAPK は、デジタル署名されている必要があります。
署名に関する情報は以下のページを参照してください。

アプリの署名 | Android Studio

この署名はAndroid Studio から行うことができます。
上記URL の[キーとキーストアを生成する] の手順を行えば、キーストアが作成され、以降はパスワードを入力するとAPK ファイルを作成することができます。

ただし、これはあくまで"手動"でAPK を作成する方法です。

続きを読む

(Android)Lint 解析対象から特定のファイルを除外する方法

Android の開発中にデバッグ用のActivity やもう古くなって使用しないファイルをLint の解析対象外にしたいことがあります。 検索方法が間違っていたのか、調べても簡単には出てこなかったのでメモ。

開発環境

  • 開発環境 : Android Studio 3.0
  • Compile Sdk Version : API26 Android8.0 (O)
  • Min Sdk Version : API23 Android6.0 (Mashmallow)
  • Gradle : 2.3.3

Lint 解析の除外方法

特定のファイルをLint 解析から除外するには"lint.xml" ファイルに除外するファイルを記載する必要があります。
lint.xml を参照するには、/app/build.gradle に以下の内容を記述します。

android {
    lintOptions {
        // 任意のファイル名でOK
        lintConfig file('lint.xml')
    }
}

次に/app フォルダに"lint.xml" ファイルを追加します。 lint.xml の記載例は以下のとおりです。

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <!--  id="all" とすることで全ての警告カテゴリが対象となる。  -->
    <issue id="all">
        <!--  以下のファイルは全てのLint 警告を無視するものとする。  -->
        <ignore path="**/activity_debug_*.xml" />
        <ignore path="**/Debug*.java" />
    </issue>
</lint>

ポイントは以下の2つ

  1. issue タグの id 属性に"all"を設定する。
    今回はデバッグ用のファイルは全てのLint 警告を無視するようにしたかったのでこの様にしています。
    ここは各プロダクトによって適宜変更する必要があるかもしれません。

  2. ignore タグの path 属性に除外対象のフィアルパスを設定する。
    除外対象のファイルは、"**/" で全フォルダが対象となります。
    上記の設定では、activity_debug~.xml のようなデバッグActivity レイアウトファイルを解析対象外としています。
    また、デバッグ用のActivity クラスも同様に解析対象外としています。

何でもかんでもLint 検証対象外にしてしまうのは良くないので、開発が本格的に動き出す前に予めこのあたりのルールは決めておきたいところですね。

(Java)コレクションをプリミティブ型の配列に変換する方法

Java でコレクションをプリミティブ型の配列に変換する際に迷ったのでメモ

開発環境

変換方法

まず、やりたいことは以下の通り。

// 仮にList<Integer> を作る。
List<Integer> target = new ArrayList<>();
for (int i=0; i<100000; i++) {
    target.add(i);
}
// ここをどうにかしたい。
int[] result = target;

List などのコレクションは、Integer などの参照型しかジェネリックに指定できません。
その為、プリミティブ型のint 配列などへ変換することができません。
"java collection to array primitive" などで検索しましたが、先頭に引っかかったStackOverflow では以下のようなコメントが有りました。

stackoverflow.com

Unfortunately, I don't believe there really is a better way of doing this due to the nature of Java's handling of primitive types, boxing, arrays and generics. In particular:

・List.toArray won't work because there's no conversion from Integer to int

・You can't use int as a type argument for generics, so it would have to be an int-specific method (or one which used reflection to do nasty trickery).

I believe there are libraries which have autogenerated versions of this kind of method for all the primitive types (i.e. there's a template which is copied for each type). It's ugly, but that's the way it is I'm afraid :(

Even though the Arrays class came out before generics arrived in Java, it would still have to include all the horrible overloads if it were introduced today (assuming you want to use primitive arrays).

そうかー、良い方法は無いのか…
と、ネガティブな結果となりましたが、それでも2018年だし何かしらベターな方法があるのでは?
と思い調査しました。
その結果、Apache Commons Language3 にプリミティブ型配列への変換機能を見つけました。

Apache Commons Language3 を使う
List<Integer> target = new ArrayList<>();
for (int i=0; i<100000; i++) {
    target.add(i);
} 

// org.apache.commons.lang3.ArrayUtils#toPrimitive(Integer[]) メソッドを使用して
// List<Integer> → Integer[] → int[] の順にキャストする。
int[] result = org.apache.commons.lang3.ArrayUtils.toPrimitive(target.toArray(new Integer[0]));

コレクションを使用した場合、一度参照型の配列へ変換する必要があります。
あまり美しくはないですが、使用頻度を考慮すればこれでも良い気がします。

ただ、

target.toArray(new Integer[0])

この部分がものすごく書き心地が悪いのでもう少し頑張りたいところ。

そこで、org.apache.commons.lang3.ArrayUtils#toPrimitive(Integer[]) メソッドの実装を確認したところ、以下のようになっていました。

public static int[] toPrimitive(final Integer[] array) {
    if (array == null) {
        return null;
    } else if (array.length == 0) {
        return EMPTY_INT_ARRAY;
    }
    final int[] result = new int[array.length];
    for (int i = 0; i < array.length; i++) {
        result[i] = array[i].intValue();
    }
    return result;
}

上記のStackOverflow の質問者も似たようなコードを書いていました。
なので、こちらを参考にした変換メソッドを作ってみました。

独自の変換メソッドを使う
List<Integer> target = new ArrayList<>();
for (int i=0; i<100000; i++) {
    target.add(i);
}

final int[] result = new int[target.size()];
for (int i=0; i<result.length; i++) {
    result[i] = target.get(i);
}

ほんとにそのまま持ってきただけです。
やっぱりfor 文を使用すると途端にわかりにくくなるのは否めません。

とはいえ、上記のどちらもUtils のstatic メソッドにしてしまえば使用者が見ることは無いのでどちらでも良い気がします。
どちらでも良いのですが、個人的事情もあり、今回は実行速度の早い方を採用したいと思います。

どちらが速いか?
  • for Apache Commons Language3
List<Integer> target = new ArrayList<>();
for (int i=0; i<100000; i++) {
    target.add(i);
}

long begin = System.currentTimeMillis();

int[] result = org.apache.commons.lang3.ArrayUtils.toPrimitive(target.toArray(new Integer[0]));

long finish = System.currentTimeMillis();
System.out.println((finish - begin) + "ms");
  • for 独自の変換メソッド
List<Integer> target = new ArrayList<>();
for (int i=0; i<100000; i++) {
    target.add(i);
}

long begin = System.currentTimeMillis();

final int[] result = new int[target.size()];
for (int i=0; i<result.length; i++) {
    result[i] = target.get(i);
}

long finish = System.currentTimeMillis();
System.out.println((finish - begin) + "ms");

実行結果は以下の通り。

  • for Apache Commons Language3 : 8ms
  • for 独自の変換メソッド : 1ms

厳密にベンチマークテストできているわけではないですが、明らかに差があります。
ということで、速度を求めた結果、以下のようになりました。

public final class ArrayUtils {
    
    public static int[] toArray(List<Integer> target) {
    
        final int[] result = new int[target.size()];
        for (int i=0; i<result.length; i++) {
            result[i] = target.get(i);
        }
        
        return result;
    }
}

ブログ用にエラー処理などは記述していません。

ちなみにC#だとどうなるか

C# だと、コレクションから配列への変換は以下のようになります。
といってもC# は言語仕様上、コレクションの型にプリミティブ型を使用することができるため、Java と全く同じではないですが。

var target = new List<int>();

for (int i = 0; i < 100000; i++)
{
    target.Add(0);
}

target.ToArray();

実行速度は1/10 ミリ秒 ~ 2/10 ミリ秒程度でした。