WPFでReactiveProperty入門 ~ Rxを使って検索結果のサムネイル画像を一括ダウンロードする
この記事は、WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ の続きです。
関連記事
- WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る - pierre3のブログ
- WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る (1. 検索バーの実装) - pierre3のブログ
サンプルプロジェクト
今回は、Bing画像検索を実行し、検索結果からサムネイル画像を取得して一覧表示する処理について、Model側に焦点を当てて見てみたいと思います。
目次
今回は、Model側の処理がメインになりますので、Model側の主要クラスについて簡単にまとめておきます。 Bing Search API をAzure Market Placeから購入(無料含む)すると、AzureポータルからBing Searchのプロキシクラス(BingSearchContainer.csファイル)をダウンロードして使用することが出来ます。 今回はこれをそのまま使用させて頂きました。 このプロキシクラスは、WCFデータサービスの仕組みを利用しており、 画像検索の実行には、Image() メソッドを使用します。 引数で検索条件等を色々指定できるのですが、とりあえずは第1引数に検索ワードを入れたら後は ※ パラメータの詳細は、下記リンクの"Optional Parameters" の項目で確認できます。
https://msdn.microsoft.com/en-us/library/dd250942.aspx
画像検索結果が格納される 上記のような非同期操作の実装はいくつか考えられますが、今回はReactive Extensionsを使用した方法で実装を試してみたいと思います。 IE 今回のサンプルでは、検索ワードを渡すとBing画像検索を実行して結果を ここでは、 検索実行中に発生した例外は、そのままでは 上記の例では、検索実行中に発生した例外は Rxで流れてきた検索結果オブジェクト ここでは、 Rxの中で非同期メソッドを実行するには、 こうすることで、 この一連の処理を 以下に、実装例を示します。(実際には進捗通知等の処理が入り、もう少し複雑になっていますが、ここでは説明のため簡略化しています) これで、検索を実行して、結果をサムネイルの一覧で表示するための枠組みが出来ました。 Modelの概要
クラス
説明
Bing.BingSearchContainer
Microsoftが提供するBing Search APIのプロキシクラス
Bing.ImageResult
Bing Search APIが返す画像検索結果のクラス。検索結果の画像1枚の情報が格納されている
WebImage
検索結果の画像1枚を表すクラス。Bing.ImageResultに格納されている画像URLから画像データをダウンロード後、BitmapImageとして保持する
WebImageStore
WebImageのコレクションを管理するクラス。画像検索を実行し、検索結果からWebImageを生成してコレクションに保持する
WebImageHelper
WebImage、WebImageStoreクラスを補助するメソッドを提供するクラス
Bing Search API プロキシクラス
BingSearchContainer
System.Data.Services.Client.DataServiceContext
を継承したBingSearchContainer
クラスを用いて検索を行います。null
でOKです。//Bing Image検索プロキシクラス
public class BingSearchContainer : System.Data.Services.Client.DataServiceContext
{
//画像検索用のクエリを発行
public DataServiceQuery<ImageResult> Image(String Query, String Options,
String Market, String Adult, Double? Latitude, Double? Longitude, String ImageFilters)
{
DataServiceQuery<ImageResult> query;
query = base.CreateQuery<ImageResult>("Image");
//...(中略)...
return query;
}
}
Image()
メソッドは、画像検索用クエリを表す DataServiceQuery<ImageResult>
を返却します。
DataServiceQuery<ImageResult>
のExecute()
メソッドでクエリを実行すると、検索結果がIEnumerable<ImageResult>
のかたちで返却されます。var bing = new Bing.BingSearchContainer(new Uri("https://api.datamarket.azure.com/Bing/search/"));
bing.Credentials = new NetworkCredential("accountKey", accountKey);
//画像検索用のクエリ(DataServiceQuery<Bing.ImageResult>)を発行
var query = bing.Image(searchWord, null, null, null, null, null, null);
//検索実行
IEnumerable<T> result = query.Execute();
ImageResult
ImageResult
クラスは以下のプロパティを持ちます//画像検索の結果を格納するクラス
public class ImageResult
{
public Guid ID { get; set; }
public String Title { get; set; }
public String MediaUrl { get; set; }
public String SourceUrl { get; set; }
public String DisplayUrl { get; set; }
public Int32? Width { get; set; }
public Int32? Height { get; set; }
public Int64? FileSize { get; set; }
public String ContentType { get; set; }
public Thumbnail Thumbnail { get; set; }
}
public class Thumbnail
{
public String MediaUrl { get; set; }
public String ContentType { get; set; }
public Int32? Width { get; set; }
public Int32? Height { get; set; }
public Int64? FileSize { get; set; }
}
MediaUrl
に画像データをダウンロードするためのURLが格納されています。
また、画像サイズを縮小したサムネイル用の画像データを取得することが可能で、その場合はThumbnail
プロパティのMediaUrl
を使用します。検索結果のシーケンスからサムネイル画像をまとめて非同期ダウンロードする。
IEnumerable<ImageResult>
として返される全ての検索結果(最大50件)のサムネイル画像を一度にダウンロードするのですが、(当然ながら)UIをブロックしないよう非同期で行う必要があります。Reactive Extensions で非同期ダウンロード
IEnumerable<ImageResult>
から IObservable<ImageResult>
に変換IObservable<ImageResult>
で返すヘルパーメソッドを WebImageHelper
クラスに用意しています。DataServiceQuery<ImageResult>
をExecute()して得られるIEnumerable<ImageResult>
を ToObservable()
で IObserbable<ImageResult>
に変換たものをreturn しています。static class WebImageHelper
{
//Bing Image 検索を実行して 結果を IObservable<T> で返す
public static IObservable<Bing.ImageResult> SearchImageAsObservable(
string searchWord, string accountKey, int skip, int top)
{
var bing = new Bing.BingSearchContainer(
new Uri("https://api.datamarket.azure.com/Bing/search/"));
bing.Credentials = new NetworkCredential("accountKey", accountKey);
var query = bing.Image(searchWord, null, null, null, null, null, null);
query = query.AddQueryOption("$skip", skip);
query = query.AddQueryOption("$top", top);
try
{
IEnumerable<T> result = query.Execute();
return result.ToObservable();
}
catch (Exception e)
{
return Observable.Throw<Bing.ImageResult>(e);
}
}
}
例外を
IObserbable<T>
に乗せるには Observable.Throw<T>()
IO<T>
の一連の流れの中では補足できませんが、
Observable.Throw<T>()
メソッドを使用することで、例外をIObservable<T>
として扱うことが可能になります。IObservable<ImageResult>
で包まれて、購読側のOnError()
メソッドで補足できるようになります。SelectMeny()で非同期メソッドを実行する
ImageResult
を基にサムネイル画像のダウンロードを行う処理は、以下のメソッドを用いて行います。ImageResult
オブジェクトを引数にWebImage
のインスタンスを生成後そのDownLoadThumbnailAsync()
メソッドでサムネイル画像のダウンロードを非同期に行います。戻り値はTask<WebImage>
となります。public class WebImageStore : IDisposable
{
//検索結果オブジェクト(ImageResult)でWebImageのインスタンスを生成後、
//ImageResultに格納されているサムネイル画像のURLから画像データをダウンロードする
private async Task<WebImage> CreateWebImageAsync(Bing.ImageResult bingResult)
{
var image = new WebImage(bingResult, logger);
await image.DownLoadThumbnailAsync();
return image;
}
}
SelectMeny()
を使用します。
SelectMeny()
に渡すデリゲートをasync
にして、その中で await CreateWebImageAsync()
とします。CreateWebImageAsync()
メソッドは非同期かつ並列的に実行されます。実行の結果は、完了した順に後続へ渡ります。Subscribe()
すると、OnNext()
にサムネイルのダウンロードが完了したWebImage
オブジェクトが渡ってくるので、これを ObservableCollection に逐次追加して行きます。public class WebImageStore : IDisposable
{
private IDisposable disposable;
private ObservableCollection<WebImage> imagesSource;
public ReadOnlyObservableCollection<WebImage> readonlyImages;
public IReadOnlyObservableCollection<WebImage> Images {get {return readonlyImages;}}
public void DownloadSearchResult(string searchWord)
{
disposable = WebImageHelper.SearchImageAsObservable(searchWord, bingAccountKey)
.SelectMany(async bingResult => await CreateWebImageAsync(bingResult))
.Where(webImage => webImage?.Thumbnail != null)
.Subscribe(
onNext: webImage =>
{
//ObservableCollection に追加
imagesSource.Add(webImage);
},
onError: e =>
{
//SearchImageAsObservable()内でObservable.Throw()した例外はここで補足
logger.Error("画像の検索に失敗しました。", e);
},
onCompleted: () =>
{
logger.Info("検索が完了しました。");
});
}
}
まとめ
次回は、URLからサムネイルの画像データをダウンロードする処理と、そのデータを元にBitmapImage
オブジェクトを生成する処理の詳細について書きたいと思います。
(補足) Roslyn で C#のソースコードからPlantUMLのクラス図を生成する の設計メモ
前回の記事の補足資料です。
目次
使い方
前回、PlantUmlClassDiagramGenerator の使い方を書くのを忘れていましたので載せておきます。
PlantUmlClassDiagramGenerator はコンソールアプリケーションです。以下のパラメータを受け付けます。
PlantUmlClassDiagramGenerator.exe SOURCE_PATH [DEST_DIR]
- 第1引数: SOURCE_PATH
- 変換元のソースファイル名または、ソースファイルを格納したフォルダ名を指定します。
- フォルダを指定した場合は、直下にある .cs ファイルをすべて読み込みます。(フォルダの階層は辿りません)
- 第2引数: DEST_DIR
- 結果のPlantUMLファイルの出力先を指定します。省略可能です。
- 省略した場合は、入力ファイルと同じ階層に"uml" というフォルダを作成してそこに出力します。
C:\ > PlantUmlClassDiagramGenerator.exe SOURCE_PATH DEST_DIR
設計メモ
C#からPlantUMLのクラス図に変換する処理の設計メモです。
- UMLに関する知識不足等で、不適切な部分があるかもしれません。 お気づきの点などありましたらご指摘頂けると嬉しいです。
型定義
インターフェース、クラス、構造体などの型定義に関する仕様です。
C#のキーワードとPlantUMLでの記述との対応付けを表にしてみました。
型キーワード
PlantUMLでは、interface
, class
, abstract class
, enum
が使用可能です。
C# | PlantUML | Memo |
---|---|---|
class |
class |
|
struct |
<<struct>> class |
PlantUMLにはstruct で定義できる型が無いので、class にステレオタイプ<<struct>> を付加することで構造体を表現 |
interface |
interface |
|
abstract class |
abstract class |
抽象クラスはabstract class で宣言可能 |
enum |
enum |
修飾子
修飾子は、(基本的には)ステレオタイプで表現します。
C# | PlantUML | Memo |
---|---|---|
abstract |
abstract |
‘abstract‘ は、‘class‘ キーワードと組み合わせて抽象クラスの宣言時に使用する |
static |
<<static>> |
|
partial |
<<partial>> |
|
sealed |
<<sealed>> |
- 型のアクセス修飾子について
UMLに型自体のアクセス修飾子に関する規定がない(?)ため、型に対するアクセス修飾子は無視するようにしました。
必要な場合は、<<internal>> class ClassA
の様にステレオタイプを付加する?
型引数
ジェネリックの型引数は、PlantUMLでもC#と同じように書けます
class GenericsType<string,int> { }
例
クラス定義の変換例です。
//csharp sealed class ClassA{ } abstract class AbstractClass{ } static class StaticClass{ } struct Structure{ } enum EnumType{ }
'plantuml @startuml class ClassA <<sealed>> abstract class AbstractClass class StaticClass <<static>> class Structure <<struct>> enum EnumType class GenericsType<string,int> @enduml
メンバー定義
フィールド、プロパティ、メソッド、Enum定数 など型のメンバー定義に関する仕様です。
アクセス修飾子(共通)
C# | PlantUML | Comment |
---|---|---|
public |
+ |
|
internal |
<<internal>> |
~ (package) が意味合い的に近いと思うが、 protected internal と合わせてステレオタイプにした |
protected internal |
# <<internal>> |
#~ はPlantUMLではエラーになるので internal をステレオタイプで表現することに |
protected |
# |
|
private |
- |
修飾子 (共通)
C# | PlantUML | Comment |
---|---|---|
abstract |
{abstract} |
ステレオタイプではなく{} で括る。イタリック体の表記になる |
static |
{static} |
ステレオタイプではなく{} で括る。下線付きの文字で表現される |
virtual |
<<virtual>> |
|
override |
<<override>> |
|
readonly |
<<readonly>> |
プロパティ
プロパティの表現は悩みどころですが、変換のしやすさを優先して以下の様に出力するようにしました。
- C#のコードに記述されているアクセサー(get,set)をそのままステレオタイプとして出力
//csharp public int PropA {get; set;} public int PropB {get;} public int PropC {get; protected set;}
'prantuml + PropA : int <<get>> <<set>> + PropB : int <<get>> + PropC : int <<get>> <<protected set>>
- 別候補
他にも以下のように変換することも考えたのですが
(1) は protected set
等、アクセサーごとにアクセス修飾子が付けられた際の表現が難しく、
(2) は 変換処理が少し複雑になるのと、もはやプロパティではなくなるので却下しました。
'prantuml '(1) <<property>> ステレオタイプを付け、getterのみの場合はreadonlyの制約を付ける + PropA : int <<property>> + PropB : int <<property>> { readonly } '(2) Java風にメソッドで表現 + getPropA():int + setPropA(value:int) + getPropB():int + getPropC():int # setPropC(value:int)
初期化子(フィールド、プロパティ)
フィールド、プロパティの初期値を初期化子で設定している場合、初期値がリテラルの場合のみ = (初期値)
を付加するようにしています。
リテラルのみにした理由は、初期化子にコンストラクタやメソッドを使った場合に、 PlantUMLが 初期化子の()
を見て フィールド(プロパティ)ではなくメソッドと判断してしまう為です。
'plantuml ''次のようにフィールドを記述しても、PlantUMLはメソッドと判断してしまう class ClassA{ # int : IList<int> = new List<int>() }
初期化子の変換例
//csharp class ClassA { private readonly int intField = 100; protected double X = 0, Y = 1, Z = 2; internal double PropC { get; } = 3.141592; protected IList<int> list = new List<int>(); }
'plantuml class ClassA { - <<readonly>> intField : int = 100 # X : double = 0 # Y : double = 1 # Z : double = 2 <<internal>> PropC : double <<get>> = 3.141592; ' リテラル以外の初期化子は出力しない # list : IList<int> }
未対応
現在対応できていないコードについてまとめます。
ネストクラス
現状、入れ子になった型の定義は、以下の様にそのまま入れ子の状態で出力されるのですが、PlantUMLではエラーとなり変換できません。
'plantuml '' このコードはエラーになります class NestedClass { + A : int <<get>> + B : InnerClass <<get>> class InnerClass { + X : string <<get>> + MethodX() : void } }
内部クラスを外に出して、+--
で結ぶと入れ子を表現可能なので、以下の様に変換すべきなのですが。。。
class NestedClass { + A : int <<get>> + B : InnerClass <<get>> } class InnerClass { + X : string <<get>> + MethodX() : void } NestedClass +-- InnerClass
演算子のオーバーロード
演算子のオーバーロードを定義してもPlantUMLに出力されません。(未検討)
変換例
最後に、いろいろな変換例を載せておきます。
変換前
変換元C#のコードはこちらを参照ください InputClasses.cs
変換後のPlantUML
'plantuml class ClassA { - <<readonly>> intField : int = 100 - {static} strField : string # X : double = 0 # Y : double = 1 # Z : double = 2 - list : IList<int> # PropA : int <<get>> # <<internal>> PropB : string <<get>> <<protected set>> <<internal>> PropC : double <<get>> = 3.141592 + ClassA() {static} ClassA() # <<virtual>> VirtualMethod() : void + <<override>> ToString() : string + {static} StaticMethod() : string } abstract class ClassB { - field_1 : int {abstract} + PropA : int <<get>> <<protected set>> # <<virtual>> VirtualMethod() : string + {abstract} AbstractMethod(arg1:int, arg2:double) : string } class ClassC <<sealed>> { - {static} <<readonly>> readonlyField : string = "ReadOnly" + <<override>> PropA : int <<get>> <<protected set>> = 100 + <<override>> AbstractMethod(arg1:int, arg2:double) : string # <<override>> VirtualMethod() : string } class Vector <<struct>> { + X : double <<get>> + Y : double <<get>> + Z : double <<get>> + Vector(x:double, y:double, z:double) + Vector(source:Vector) } enum EnumA { AA= 0x0001, BB= 0x0002, CC= 0x0004, DD= 0x0008, EE= 0x0010, } ClassB <|-- ClassC
クラス図
Roslyn で C#のソースコードからPlantUMLのクラス図を生成する
前回 に引き続き、PlantUMLの話になります。
ReactiveProperty の記事もまだ途中なのですが、もうちょっと寄り道します。
今回は、Roslynで C# のソースコードから PlantUMLのクラス図を生成するプログラムを作ってみよう!というお話です。
Roslyn を使って C#のソースコードから PlantUML のクラス図を生成してみる
Roslyn でメタプログラミング
Roslynの導入方法、Roslynを使ったプログラミングについては、Build Insiderの記事が参考になります。
サンプルコード
今回も、サンプルコードをGitHubに上げておきました。
CSharpSyntaxTree および CSharpSyntaxWalker
今回のプログラムですが、基本的な仕組みは以下のようになります。
- ソースコードを構文木(
CSharpSyntaxTree
)にパース CSharpSyntaxWalker
を継承したクラスを用意して、必要な要素をVisitorパターンで訪問- 訪問先で、取得した情報を加工して出力
といった流れで、いたってシンプルです。
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; using System; using System.Collections.Generic; using System.IO; using System.Linq; class Program { static void Main(string[] args) { //ソースコード var code = @"class Hoge{ ...(略)"; //SyntaxTree にパース var tree = CSharpSyntaxTree.ParseText(code); var root = tree.GetRoot(); //出力先。今回はStringWriterで書き込みます var output = new StringBuilder(); using (var writer = new StringWriter(output)) { //CSharpSyntaxWalker を継承したクラス //この中で変換処理やら出力やらをやります var gen = new ClassDiagramGenerator(writer, indent:" "); gen.Visit(root); } Console.WriteLine(output); } }
ClassDiagramGenerator
上記のコードで重要なのが、ClassDiagramGenerator
で、このプログラムの心臓部になります。
CSharpSyntaxWalker
を継承しています。
では、ClassDiagramGenerator
をクラス図で見てみましょう。
Visit~ と名前の付いたメソッドがいくつかあります。
これらはCSharpSyntaxWalker
からオーバーライドしたものです。構文木を辿り、対応するノード(ここではクラス、インターフェース、プロパティ、メソッド等の定義部)を訪問した際に実行されます。
このメソッド内に、必要な情報を取得し、適切な文字列に変換して出力する処理を記述します。
細かな実装については、割愛させていただきます。 Roslynのおかげでとてもシンプルなコードになっていると思いますので、詳細は以下のソースコードで確認してみてください。
変換例
実際に、C#のクラス定義がどのように変換されるかの一例を示します。
C# (ClassA.cs)
public class ClassA { private readonly int intField = 100; private static string strField; protected double X = 0, Y = 1, Z = 2; protected int PropA { get; private set; } protected internal string PropB { get; protected set; } internal double PropC { get; } = 3.141592; public ClassA() { } static ClassA() { strField = "static field"; } protected virtual void VirtualMethod() { } public override string ToString() { return intField.ToString(); } public static string StaticMethod() { return strField; } }
PlantUML (ClassA.plantuml)
class ClassA { - <<readonly>> intField : int = 100 - {static} strField : string # X : double = 0 # Y : double = 1 # Z : double = 2 # PropA : int <<get>> # <<internal>> PropB : string <<get>> <<protected set>> <<internal>> PropC : double <<get>> = 3.141592 + ClassA() {static} ClassA() # <<virtual>> VirtualMethod() : void + <<override>> ToString() : string + {static} StaticMethod() : string }
クラス図 (ClassA.svg)
オブジェクト間のリンク
複数オブジェクトの関連付けなどには対応していません。
これは、別途手書きで行います。
@startuml !include ClassA.plantuml !include ClassB.plantuml IDisposable ()-- ClassA ClassBBase <|-- ClassB ClassA o- ClassB : aggregation @enduml
PlantUMLでは、!include [ファイル名]
で別のPlanuUMLファイルを読み込んで使用することが出来ます。
このように、クラス単位で自動生成したファイルをIncludeして、関連のみを別ファイルに記載するとスッキリして良いです。
まとめ
今回の様に、C#のコードから他の言語(DSL)などを生成するといった事が、 Roslyn の力で いとも簡単に出来てしまうことが分かりました。
むしろ生成処理そのものよりも、C#の型定義とUMLとの対応付けをどうするかを決める作業の方が難しいのではないでしょうか。
C#に固有の、UMLでは定義されていない(もしくは微妙に意味が異なる)要素をどう扱うかが非常に悩ましいところでした。
今回は、なるべく変換処理が複雑にならないように、というのも加味して、今の実装に落ち着いたのですが...
この辺りの話は、また別の記事でまとめたいと思います
PlantUMLを自動変換してLiveプレビューしてくれるAtom拡張 PlantUML-Viewer がイイ!
PlantUML とは
テキストベースでUMLのダイアグラムをサクサクかける ドメイン固有言語(DSL)です。
以下のような特徴があります。
参照
- Open-source tool that uses simple textual descriptions to draw UML diagrams.
⇒ PlantUML 公式サイト。リファレンスマニュアルがpdfファイルで公開されています(PlantUML_Language_Reference_Guide.pdf) - http://plantuml-ref-ja.github.io/
⇒ リファレンスマニュアルを日本語訳してくれているサイトです。 - PlantUML の使い方 | プログラマーズ雑記帳
⇒ 分かりやすく豊富な解説記事が参考になります。
Atom Editor でPlantUML
とても便利なPlantUMLなのですが、テキストをUMLに変換して、結果を確認するまでの手順が少々面倒であったりします。
- PlantUMLの書式に従ってテキストを編集する
- テキストファイルをUMLに変換して、画像ファイルとして出力
- できたファイルを画像ビューアやWebブラウザ等で開き、結果を確認する
- 1 に戻る
この辺りの手順を自動化して、結果をプレビュー表示してくれるツールを探していたのですが、最近見つけたAtom Editorのプラグインが素敵だったので、紹介したいと思います。
Atom
plantuml-viewer
- PlantUMLのテキストをUMLに変換して、エディタ右側の分割ペインに表示してくれます。
- テキストの編集に追従して、リアルタイムに図が更新されます。(Live Preview)
- 図はマウス操作で自由に拡大/縮小、移動することができます。
- 図は、PNG, SVG, EPS の3種類の形式で保存可能です。
導入手順
PlantUMLを使う準備
PlantUMLを使用するには、以下がインストールされている必要があります。
Java ランタイム(JRE)
PlanuUMLはJavaで動くので、Javaのランタイムがインストールされている必要があります。
インストールされていない場合は、以下から最新版をインストールすればよいと思います。
Graphviz
シーケンス図以外の図のレンダリングには、 Graphiviz の dot.exe というモジュールが必要になります。
http://www.graphviz.org/Download..php
なお、plantuml本体のモジュール plantuml.jar はplantuml-viewer に組み込まれているので別途インストールする必要はありません
Atomのインストール
以下のリンクからインストーラをダウンロードして、普通にインストールするのが一番簡単です。
Atom プラグインのインストール
Atomを起動したら、[File]メニューのSettings を開き、左側で[Install]を選ぶと、以下の画面になります。
ここで、”plantuml” と入力して表示される一覧からplantuml-viewer
を探して[Install]ボタンをクリックします。
ついでに、PlantUMLで書かれたテキストをシンタックスハイライト表示してくれるパッケージ language-plantuml
も入れておきましょう。
plantuml-viewer の設定
次に、plantuml-viewer の設定を行います。
先ほどのSettingsページで、今度は[Packages]を選択します。
すると、インストールしたプラグインのパッケージ一覧が表示されますので、ここでplantuml-viewer
を探して[Settings]ボタンをクリックします。
設定項目
Charset
変換前のテキストのエンコーディングを指定します。指定しないとシステムのデフォルト(日本語環境のWindowsではCP932(Shift-jis))になります。当初、この設定項目がなく、Shift-jis以外で書くと文字化けしていました。この項目を指定できるようにカスタマイズして、プルリクエストしてみたところ、即日取り込んでいただき、V0.60から使用可能となっています。(感謝)
Config File
変換時に読み込まれる、共通の処理を記載しておくplantUMLファイルのパスを指定します。図の外観のカスタマイズする場合などに使用します。- Grammars
対象となるファイルの拡張子を指定します。 - Graphviz Dot Executable
インストールした Graphivizの dot.exe のパスを指定します。 - Live Update
チェックを外すと、図の自動更新を無効にできます。 - Open in Split Pane
チェックを外すと、図のプレビューを分割ペインではなく、通常のタブに表示するようになります。
最後に
plantuml-viewer はまだリリース間もない(8/9)のと、私も使い始めたばかりなので、もう少し使い込んでみる必要はあるかと思いますが、第一印象としてはかなりいい感じです。
この記事を読んで気になった方は、ぜひ試してみてください!
※ 2016/3/6追記
変換するUMLのサイズが大きいと、極端に重くなったり、入力したテキストの内容がちゃんと反映されなかったりする場合があります。
この辺の話と対処法を以下の記事に書きましたので、こちらもご参照ください。
WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る (検索バーの実装)
前回 WPF+ReactivePropertyでBing画像検索ビューアを作るの続きです。
今回は、検索バー(検索ワードの入力と、検索の実行を行う部分)の実装を通して、ReactiveProperty の基本的な使い方を確認してみたいと思います。
ここで期待される動作は以下の通りです。
- テキストボックスにキーワードを入力して[検索]ボタンをクリックすると検索が開始される
- 検索実行中に[キャンセル]で検索を中止できる。
- [検索]ボタンは「テキストボックスが空ではなくて」「検索実行中ではない」場合のみ押せる
- [キャンセル]ボタンは、「検索実行中」の場合のみ押せる
- [検索]ボタンの代わりに、「Enter」キーを押すことでも検索を実行できる
これを踏まえて、実装を見てみましょう。まずはViewModelから。
ViewModel
ViewModelでは、ViewにバインドするReactiveProperty と ReactiveCommand を定義します。
これらは
MainWindowViewModel クラスのコンストラクタ内で初期化を行っています。
検索ワード
まずは、TextBoxに入力された検索ワードを格納する ReactiveProperty を定義します。
SearchWord = new ReactiveProperty<string>("");
ReactiveProperty のコンストラクタについて
以下の省略可能な2つの引数を受け付けます
- 第1引数でDefault値を設定
- 第2引数は ReactivePropertyMode で細かな挙動を指定
DistinctUntilChanged
: 同じ値は続けて通さないRaiseLatestValueOnSubscribe
: Subscribe 時に最新の値を流す。値が無ければDefault値を流す- 指定しなければどちらも有効になる (
DistinctUntilChanged | RaiseLatestValueOnSubscribe
)
検索コマンド
次に、[検索]を実行する ReactiveCommand、「SearchCommand」 の定義を見てみます。
//バックグラウンド処理の実行状態とか、処理の進捗状態とかを通知するオブジェクト var progress = new ProgressNotifier(); //[検索]コマンド // 検索ボックスに文字が入っていて、検索実行中でない場合に実行可能 this.SearchCommand = new[] { SearchWord.Select(x => string.IsNullOrWhiteSpace(x)), progress.IsProcessingObservable.StartWith(false) } .CombineLatestValuesAreAllFalse() .ToReactiveCommand(); this.SearchCommand.Subscribe(_ => { //検索実行 & サムネイル画像のダウンロード webImageStore.Clear(); webImageStore.DownloadWebImage(SearchWord.Value, progress); }).AddTo(disposables);
ReactiveCommandの初期化について
IObservable<bool>
をToReactiveCommand()
で ReactiveCommand に変換しています。- この
IObservable<bool>
が ICommand でいうところの CanExecute() の役割を担っています。
(これがFalseを返す間は、コマンドが実行されず、バインド先のコントロールもグレーアウトした状態になります)
ここでは、検索ワードが空以外で、検索が実行中でない場合にコマンドが実行可能になるようにしています。
this.SearchCommand = new[] { //検索ワードが 空または空白のみのときTrueを流す SearchWord.Select(x => string.IsNullOrWhiteSpace(x)), //検索実行中になったらTrueを流すIO<bool> progress.IsProcessingObservable.StartWith(false) } //配列中のIO<bool>について、流れてくる最新の値がすべてFalse になっていたら Trueを流す .CombineLatestValuesAreAllFalse() //...
progress.IsProcessingObservable.StartWith(false)
の部分について。
- ここで、検索実行中になったらTrueを、実行中でなくなったらFalse流す
IO<bool>
を取得しています。 取得元(ProgressNotifier という自作のクラス)では、内部で
Reactive.Bindings.Notifiers.CountNotifier
を使用して処理の実行状態の変化を監視しています。CountNotifier
ついては、以下の解説とサンプルコードを見ると良いと思います。
キャンセル コマンド
[キャンセル]実行時のコマンドも、検索コマンドと同様に初期化を行います。
検索実行中の場合のみ コマンドが実行可能になります。
var isProcessing = progress.IsProcessingObservable.StartWith(false) //[キャンセル]コマンド //検索中の場合のみ実行可能 this.CancelCommand = isProcessing.ToReactiveCommand(); this.CancelCommand.Subscribe(_ => { webImageStore.Cancel(); }).AddTo(disposables);
View
続いて、Viewを見てみましょう。
ここまでに定義したReactiveProperty、ReactiveCommand をViewでバインドします。
<!-- 検索バー --> <StackPanel Orientation="Horizontal" DockPanel.Dock="Top"> <TextBox Margin="4,2" Padding="2" Width="240" Text="{Binding SearchWord.Value,Mode=OneWayToSource,UpdateSourceTrigger=PropertyChanged}"> <i:Interaction.Triggers> <i:EventTrigger EventName="KeyDown"> <Interactivity:EventToReactiveCommand Command="{Binding SearchCommand}"> <conv:ReturnKeyDownConverter/> </Interactivity:EventToReactiveCommand> </i:EventTrigger> </i:Interaction.Triggers> </TextBox> <Button Padding="8,2" Margin="4,2" Command="{Binding SearchCommand}">検索</Button> <Button Padding="8,2" Margin="8,2" Command="{Binding CancelCommand}">キャンセル</Button> </StackPanel>
このViewの全体像は、こちら MainWindow.xaml を参照ください
- TextBox の Text に VieModelのSearchWord をバインド
- ReactiveProperty をバインドする場合、
.Value
を忘れずに
- ReactiveProperty をバインドする場合、
- [検索]ボタンに ViewModelの SearchCommand を バインド
- [キャンセル]ボタンに ViewModel の CancelComannd をバインド
ここまでは、特に問題ないかと思います。
残るは <i:Interraction.Triggers>...
の部分ですが、
ViewのイベントをReactiveCommand に変換する EventToReactiveCommand
冒頭に挙げた、期待される動作の最後に、以下の要件がありました。
- [検索]ボタンの代わりに、「Enter」キーを押すことでも検索を実行できる。
これを実現するために、EventToReactiveCommand
を使用しています。
ViewのイベントをReactiveCommandに変換してくれる優れものです。
使い方としては、
- TextBox の Triggers に KeyDownイベントのEventTriggerとしてEventToReactiveCommandを登録し
- KeyDownイベントハンドラをReactiveCommandに変換するためコンバータを定義して
- EventToReactiveCommandの Command プロパティにReactiveCommandをバインドする
という流れになります。
「KeyDownイベントハンドラをReactiveCommandに変換するためコンバータ」は、以下の様に定義しました。
using Reactive.Bindings.Interactivity; using System; using System.Reactive.Linq; using System.Windows.Input; namespace ReactiveBingViewer.Converters { public class ReturnKeyDownConverter : ReactiveConverter<KeyEventArgs, object> { protected override IObservable<object> OnConvert(IObservable<KeyEventArgs> source) { return source.Where(arg => arg.Key == Key.Return) .Select<KeyEventArgs,object>(_ => null); } } }
OnConvertにはKetDownのイベント引数 KeyEventArgs がIObservable<T>
として流れてきます。
この値を見て、Enter(Return)キーが押された場合のみReactiveCommandに値を流すようにしています。
まとめ
これで、検索バーの処理をReactivePropertyで実装することが出来ました。
次回は、Model部分の実装についても少し見てみたいと思います。
WPFでReactiveProperty入門 ~Bing画像検索ビューアを作る
ReactiveProperty を使ってみたい
と思いつつ、なかなか手を付けられなかった ReactiveProperty に入門すべく、WPF + ReactiveProperty で、サンプルアプリケーションを作ってみました。
ReactiveProperty
ReactivePropertyの概要はこちら。この解説を読むだけでもReactivePropertyの素晴らしさが分かります。
MVVMとリアクティブプログラミングを支援するライブラリ「ReactiveProperty v2.0」オーバービュー - かずきのBlog@hatena
作ったもの
Bingで画像検索をして結果を閲覧するWPFアプリケーションです。
検索した画像は Microsoft Project Oxford Computer Vision APIs を使って画像解析も行います。
機能
- 入力した検索ワードでBing画像検索を行います。
- 検索結果をサムネイルで一覧表示します。
- サムネイルを選択すると、その画像をフルサイズでダウンロードしてウィンドウ中央に表示します。
- 同時に、Computer Vision API で画像解析を行い、その結果を画像プロパティとしてテキスト表示します。 ― さらに、Computer Vision APIで人の顔として認識されたものがある場合、顔領域の矩形と、その顔から推定される年齢と性別を画像に重ね合わせて表示します。
- ステータスバーに、検索の進捗と現在の状況を表示します。
- 画像の検索、ダウンロードなどで例外が発生した場合、エラーの内容を通知パネルで表示します。
マナカナ以外の画像も集める!
このアプリケーションでは、以下のAPIを使用しています。
各APIの概要、導入方法等は以下の記事を参考にさせていただきました。
(というか、完全にこちら↓の記事の二番煎じです。)
マナカナの画像からProejctOxfordとimagemagickで顔を切り出す。 - かれ4
ソースコード
今回のサンプルコードは Githubに置いてあります。
ReactiveProperty はもちろん、 WPFやReactive Extensionsについても勉強しつつ探り探り実装しております。
「ここの使い方間違っているよ」や「これもっといい実装方法あるよ」等々ありましたらご指摘いただけると非常に助かります。
開発環境、ライブラリ
- Visual Studio Enterprise 2015
- .Net Framework 4.5.2
- ReactiveProperty 2.2.2
- Reactive Extensions 2.2.5
使用方法
試してみたい方がいらっしゃいましたら、GitHubでClone してコンパイルするか、Release からバイナリのzipをダウンロードしてみてください。
但し、実行するには Bing Search API と Project Oxford Computer Vision API の アクセスキーが必要になります。 Azure Market Place でアカウントを登録して、キーを取得してください (この辺りの手順も、前述のマナカナ画像の記事が詳しいです。)
アクセスキーを入手しましたら、App.config (ReactiveBingViewer.exe.config)のapplicationSettings に入力します。
"BingApiAccountKey" および"VisionApiSubscriptionKey" をそれぞれ入手したアクセスキーで置き換えてください。
<configuration> <applicationSettings> <ReactiveBingViewer.Properties.Settings> <setting name="BingApiAccountKey" serializeAs="String"> <value>Input your account key.</value> </setting> <setting name="VisionApiSubscriptionKey" serializeAs="String"> <value>Input your subscription key.</value> </setting> </ReactiveBingViewer.Properties.Settings> </applicationSettings> </configuration>
次回
次回から、プログラムの解説的なものを 書いて行く予定です。
PowerShell の入力補完にGoogleサジェストの結果を表示する
↑ みたいな事って出来るのかな、となんとなく調べてみたところ、どうやら TabExpansion++ というモジュールを使うと簡単に出来そう!
という事で、ちょっと試してみました。
TabExpansion++
PowerShellのTab補完、インテリセンスをより賢く、便利にするモジュールで、
コンテキストに応じた入力候補を動的に生成してくれるようです。
さらには、入力候補の生成処理を自作して組み込むことが出来るとのことで、今回はこの機能を使ってGoogleサジェストの結果をインテリセンスに表示させてみたいと思います。
インストール
iex (new-object System.Net.WebClient).DownloadString('https://raw.github.com/lzybkr/TabExpansionPlusPlus/master/Install.ps1')
でダウンロードして、Install.ps1 を実行するか、PsGet がインストールされていれば以下でインストールできます。
Install-Module -ModuleUrl https://github.com/lzybkr/TabExpansionPlusPlus/zipball/master/ -ModuleName TabExpansion++ -Type ZIP
後は、プロファイルに Import-Module TabExpansion++
を追加しておけば準備完了です。
Search-Google コマンドレット
まずは、Google でWeb検索するコマンドレットを作成します。
パラメータ($SearchWords
)に検索ワードを渡すと、検索クエリ付のURLを生成してブラウザに投げるだけの簡単なものです。
Search-Google -$SearchWords あ
のように入力して、[Tab]または[Ctrl]+[Space]を押すと、
Googleサジェストから取得した入力候補が表示されるようになればOKです。
入力候補を動的に生成する関数の定義
以下の要件を満たした関数を定義するだけで、自作の入力補完処理が使用できるようになります。
(すごい!)
関数に ArgumentCompleter 属性を付ける。
- ここで、入力補完のターゲットとしたいコマンドとそのパラメータ名を指定します。
- コマンドは配列で複数指定することが可能です。
入力パラメータは以下の様に定義する。
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
$wordToComplete
に補完対象となる入力中の文字列が入ってきます。
System.Management.Automation.CompletionResult
クラスのオブジェクトを出力するNew-ComoletionResult
コマンドを使用して生成します。引数には順に以下を渡します。- CompletionText:補完結果の文字列
- ToolTip: ツールチップに表示される文字列(省略可。省略時は第1引数と同じになる)
- ListItemText: 入力候補一覧に表示される文字列(省略可。省略時は第1引数と同じになる)
- CompletionResultType : 入力候補の種別 (一覧の左側に表示するアイコンを指定できる)(省略可。規定値'ParameterValue')
- NoQuotes : 文字列の中の変数を展開するか否かを指定?(違うかも)(省略可。規定値False)
では、実際に関数を作ってみましょう。
SearchWoerdsパラメータの一部として入力された文字列をGoogleサジェストに投げて、帰ってきた結果から CompletionResult を生成して出力します。
(2015/12/5 追記) PowerShell v5.0からは、Register-ArgumentCompleter コマンドレットで処理を登録できるようになりました。こちらを使用したサンプルコードを追記します。(以下2番目のコード)
後は、これを記述したスクリプトファイルを 以下の何れかに配置しておくだけでOKです。
- $env:PSModulePath に定義されているPath の1階層下のフォルダ
- 例: C:\Users\UserName\Documents\WindowsPowerShell\Modules\MyCompleter
- 任意のフォルダに置いて、そのパスを $env:PSArgumentCompleterPath に設定する
実行結果
"powershell" と入力して[Ctrl]+[Space]
"powershell" と入力して[Tab]
まとめ
思った以上に簡単に実現できてしまって驚きです。
他にも工夫次第でいろいろ便利なものが出来そうな予感がします。
TabExpansion++ が提供する入力補完も相当数あるようですので、自作しなくても十分かもしれませんが。