2014年12月25日木曜日

LINQの重箱の隅

C#の便利な言語機能のLINQですが、ちょっと気になったことがあったのでいくつか試したり、まとめてみました。LINQの入門が終わった人が次に読むくらいの中身のつもりです。

AsEnumerable

LINQにはAsEnumerableというメソッドがあります。MSDNには、LINQの拡張メソッドと同じ名前のpublicなメソッドを持つクラスでその拡張メソッドを呼び出したいときにこれを使うと書いています。
例えばListクラスにはReverse()というメソッドがあります。これはLINQのReverse()メソッドと(表記上の)パラメーターが同じなので区別できず、そういう時は優先的にListクラス側のReverse()が呼ばれるようになっています。そういう時に、AsEnumerableを使えばIEnumerableになるので、LINQのReverse()が呼べるようになるよというためのメソッドのようです。
もちろん、AsEnumerableを使わなくても、例えばEnumerable.Reverse(hoge);と書いたり、((IEnumerable<hogeT>)hoge).Reverse();と書いたりすることでLINQ側のReverseを呼び出せるようになりますが、こういう書き方よりAsEnumerableのほうがLINQらしい書き方ですね。



ところで、ここで気になったことが1つありました。
例えば、なんかのクラスでIEnumerable<T>型の読み取り専用プロパティみたいなのを定義することがあったとしましょう。そのようなプロパティの値は、クラス内でprivateなListなどで管理していることがよくあります。そんな時、

List<int> value = new List<int>();

public IEnumerable<int> Value
{
    get { return value; }
}

みたいな書き方をしてしまうと、このクラスの利用者側は、

(List<int>)hoge.Value).Add(13);

というキャストさえすれば本来privateなはずのvalueをクラス外から自由に操作できてしまいます。これはもしもこうやって操作されることを意図していなかったのなら、このようなプロパティの実装のしかたは大問題ですよね。

ここで、AsEnumerableがあるんですよ~。

というのは嘘です。
ここでAsEnumerableを使うことができたらすごくそれらしい書き方で便利なんですが、AsEnumerableの内部実装としては、私が試してみたところ、単にIEnumerable<T>にキャストされているだけっぽいです。
本当に、上記のMSDNに書いてあるような使い方にしか使えないっぽいですね。

List<string> hoge = new List<string>();
IEnumerable<string> foo;

try {
    foo = hoge.AsEnumerable();
    ((List<string>)foo).Add("hoge");
    Console.WriteLine("OK");
}
catch(InvalidCastException e) {
    Console.WriteLine(e.Message);
}

try {
    foo = hoge.Select(p => p);
    ((List<string>)foo).Add("foo");
    Console.WriteLine("OK");
}
catch(InvalidCastException e) {
    Console.WriteLine(e.Message);
}

このコードの実行結果は
OK
型 'WhereSelectListIterator`2[System.String,System.String]' のオブジェクトを型 'System.Collections.Generic.List`1[System.String]' にキャストできません。
となります。AsEnumerableはListへのキャストに成功してしまうんですね。

なので、外から無理やりいじられないプロパティ等を実装したかったら、上記のようにSelect(p => p);を使うか、もしくはyield returnで書いてしまうかになりそうですね。

Cast<T>()とOfType<T>()

比較的マイナーだと思われるメソッドなので取り上げました。Cast<T>()OfType<T>()はLINQの中では珍しく型推論がきかないメソッドです。明示的に型を示してあげなければなりません。
どちらもIEnumerable型(IEnumerable<T>ではない)の要素を型パラメーターの型に変換してIEnumerable<T>にして返すメソッドです。Cast<T>()は変換できない要素があると例外を発生させ、OfType<T>()は変換できない要素は省いたコレクションを返します。

System.Collections.ArrayList nongeneric = new System.Collections.ArrayList();

nongeneric.Add(123);
nongeneric.Add("hoge");
nongeneric.Add("foo");
nongeneric.Add(456);

foreach(var o in nongeneric)
    Console.WriteLine(o);
Console.WriteLine("-----");
foreach(var o in nongeneric.OfType<int>())
    Console.WriteLine(o);
Console.WriteLine("-----");
try {
    foreach(var o in nongeneric.Cast<int>())
        Console.WriteLine(o);
}
catch(InvalidCastException e) {
    Console.WriteLine(e.Message);
}

こんなコードを書いて実行すると、実行結果は下のようになります。
123
hoge
foo
456
-----
123
456
-----
123
指定されたキャストは有効ではありません。
非ジェネリックのコレクションなんて使う機会はほとんど無い気がしますが、例えば正規表現周りのクラスにあるMatchCollectionなんかは非ジェネリックなので(これはジェネリックスが導入される前の.NET1.1の時代からあるクラスだから非ジェネリックのまま今まで来てしまったものだと私は勝手に思っています)、これをLINQで扱いたいときはCast<T>()を使うと便利です。

要素のインデックス

要素のインデックスが欲しいことっていうのも多々あります。ElementAt()を使うと任意のインデックスの要素を取得することはできますが、逆にある要素のインデックスを取得したいときなんかはそれに見合ったメソッドはありません。
で、どうするかと言うと、Selectのオーバーロードを使います。例えば、シーケンスの中から条件に一致する要素のインデックスを取得するには、このような書き方で解決できます。

IEnumerable<string> hoge = new[] {"apple", "orange", "application", "operating system"};

var indeces = hoge
    .Select((p, i) => new { Content = p, Index = i })
    .Where(p => p.Content.StartsWith("app"))
    .Select(p => p.Index);

foreach(int i in indeces)
    Console.WriteLine(i);

Selectメソッドはインデックスも与えてくれるオーバーロードがあるので、それで要素とインデックスを抱き合わせた匿名クラスを作ってあげればあとはWhereなどで任意のフィルターを掛けたりすることができます。

隣り合う要素

これは個人的に自分が情弱だっただけなんでしょうが、長い間、コレクションの隣り合った要素同士の関係に関して何かしら計算するのが、スマートにできずにいました。上に要素のインデックスを得る方法を書いたのはこの記事の布石だったのですが、私は長い間、要素とJoinを使う方法を取っていましたが、とても冗長でパッと見で何をやっているのかがわからないような式になってしまいました。

例えば、パスカルの三角形を計算するプログラムを書くとします。その場合、私は今までずっとこんな式を書いていました。

IEnumerable<int> Row = new[] { 1 };

for(int j = 0; j < 20; j++) {    //20段目まで
    Console.WriteLine(string.Join(" ", Row.Select(p => p.ToString())));    //表示

    var RowWithZero = new[] { 0 }.Concat(Row).Concat(new[] { 0 });    //両端にゼロを付ける

    var RowWithIndex = RowWithZero.Select((p, i) => new { Number = p, Index = i });    //インデックスを抱き合わせる
    var ShiftedRowWithIndex = RowWithZero.Skip(1).Select((p, i) => new { Number = p, Index = i });    //最初の1つ飛ばしたものにもインデックスを抱き合わせる

    Row = RowWithIndex.Join(ShiftedRowWithIndex, p => p.Index, p => p.Index, (o, i) => o.Number + i.Number).ToArray();    //ToArrayしないととても重くなる
}

計算アルゴリズムとしては、段の数列に対して両端に0を付加し、その数列と、その数列の1つ目をSkipした数列の2つに関してそれぞれ上記のようにインデックスを抱き合わせた匿名クラスを作り、その2つの数列に対してインデックスが等しい組み合わせをJoinで抽出し、その組み合わせの要素を足したものを数列化して次の段を求めています。
数列の先頭を1つSkipしてインデックスを付けているので、2つのコレクションのインデックスが同じ要素は隣り合ったものというわけですね。

はい。
自分で説明文書いてて 意味わからなくなってしまうくらい簡潔さに欠けます。そもそも「隣り合う要素の組み合わせをSelectしたい」ってだけなのに、こんなにまどろっこしいことをやっているわけです。

はい。
Zip()を使いましょう。
Zip()というメソッドは、2つのコレクションを同時にSelectする拡張メソッドみたいな感じです。

IEnumerable<int> Row = new[] { 1 };

for(int j = 0; j < 20; j++) {    //20段目まで
    Console.WriteLine(string.Join(" ", Row.Select(p => p.ToString())));    //表示

    var RowWithZero = new[] { 0 }.Concat(Row).Concat(new[] { 0 });    //両端にゼロを付ける

    Row = RowWithZero.Zip(RowWithZero.Skip(1), (p, n) => p + n).ToArray();
}

さっきに比べてだいぶスッキリしましたね。Zip()はSelect()やWhere()ほど頻出ではないですが、かなり有用なメソッドだと思います。

Aggregate()

LINQには一般的な集計処理をするためのAggregate()というメソッドがあります。しかし、このメソッドはオーバーロードがいくつかあり、全体像をつかむのに苦労しました。


まず1つ目のAggregate<TSource>(IEnumerable<TSource>, Func<TSource, TSource, TSource>)についてです。
このメソッドで指定するデリゲートでは、1つ目の引数で今までの集計値、2つ目の引数で現在の要素が渡されます。ちなみに、現在の要素は2つ目の要素から始まり、その2つ目の要素に関しては今までの集計値として1つ目の要素の値が渡されます。このメソッドが現時点での集計値を返すことによって、次の要素に対する呼び出しの第1引数がその値になります。

IEnumerable<int> array = new[] { 1, 3, 5, 7, 9, 2, 4, 6, 8, 10 };
Console.WriteLine(array.Aggregate((working, now) => Math.Min(working, now)));

このコードはコレクションの最小値を求めるものです。ラムダ式の1回目の呼び出しにはworkingに1が入り、nowに3が入ってきます。Math.Minで1を返すので、次の呼び出しではworkingが1(前回の呼び出しの返却値)、nowに5が入ってきます。
ちなみに、要素数が1個のコレクションだとデリゲートは1回も呼ばれずに1つ目の要素が返され、要素数が0個のコレクションだと例外(InvalidOperationException)が吐かれます。


次に、2つ目のAggregate<TSource, TAccumulate>(IEnumerable<TSource>, TAccumulate, Func<TAccumulate, TSource, TAccumulate>)です。
このメソッドではシード、すなわち初期値を指定することができます。上述のAggregateのほうでは初期値がコレクションの1つ目の要素になるので自動的に集計値が要素の型と同じになりますが、こちらのAggregateでは初期値を指定できるので任意の型にすることが可能です。

IEnumerable<Func<int, int>> calc = new Func<int, int>[]{
    (i) => i * 9,
    (i) => i / 10 + i % 10,
    (i) => i + 1,
};

Console.WriteLine(calc.Aggregate(3, (working, now) => now(working)));

例えば、1~10の任意の整数を与えても答えが10に落ち着く計算みたいなのがあります。子供だまし?のちょっとした電卓遊びですね。
それぞれの計算アクションをFunc<int, int>の配列として用意しておきます。そして、初期値から順にこのメソッドを順に呼び出していきます。
計算結果は1~10の任意の整数を与えても必ず10になります。


最後に、3つ目のAggregate<TSource, TAccumulate, TResult>IEnumerable<TSource>, TAccumulate, Func<TAccumulate, TSource, TAccumulate>, Func<TAccumulate, TResult>)です。
このメソッドは2つ目のAggregateの計算が終わった後、その値を変換するデリゲートが付いています。

int[] array = new[] { 1, 3, 5, 7, 9, 2, 4, 6, 8, 10 };

Console.WriteLine(array.Aggregate(0, (working, now) => working + now, p => (double)p / array.Aggregate(0, (w, n) => w + 1)));

これはAggregateだけで平均を求めるメソッドです。平均は、全要素を足してから最後に要素数で割って求めますよね。(要素をそれぞれ要素数で割ってから足してもいいですが…)
すなわち、全て足した後のアクションとして、要素数で割るというアクションを追加する必要がでてきます。というわけで、処理後のselectorを使うわけです。
ちなみに、要素数を数えるのもAggregateでできますね。


はい、ここまでAggregateを見てきました。ここまで来てお気づきの人も多いかと思いますが、Count()Min()Max()Sum()Average()はこのAggregateの特別な形です。


余談:
intとかdoubleとかの一般的な数値型の元締めとなるような親構造体(or インターフェース)が無いせいで、Min()、Max()などはあらゆる数値型に対してオーバーロードが定義されています。なんていうか、実装した人お疲れ様ですって感じですね…。

ThenBy()

これもまただいぶマイナーなメソッドだと思っています。
ThenBy()は、ソートされたコレクションに対して、そのうちのソート基準が同じだったものを別の条件でソートするメソッドです。
これだけ言われると何を言っているんだ?と思ってしまうかもしれませんが、例えば、ファイルの一覧をまず種類(拡張子)でソートして、そのうち同じ拡張子のファイルは日付順に並べたいなんてことがあると思います。

IEnumerable<FileInfo> files = Directory.GetFiles(@"C:\Windows").Select(p => new FileInfo(p));

files = files
    .OrderBy(p => p.Extension.ToLower())
    .ThenByDescending(p => p.LastWriteTime)
    .ThenBy(p => p.Name);

foreach(FileInfo f in files)
    Console.WriteLine("{0:yyyy/MM/dd hh:mm:ss} {1}", f.LastWriteTime, f.Name);

このコードでは、まず拡張子で昇順にソートし、同じ拡張子のものに関しては最終更新日時で降順にソートし、さらに同じ最終更新日時のものに関してはファイル名で昇順にソートしています。

OrderBy()安定なソートなので、そのままでは同じ拡張子のファイルに関してGetFilesの返却値の順序がそのまま使われます。そこで、ThenByの出番ってわけですね。



ここまで、私の独断と偏見に基づいてLINQの重箱の隅をつついてみました。いかがでしたでしょうか。
一度、一通りEnumerableクラスのメソッドを眺めておくとLINQで書くときに最もスマートな方法で書くことができるようになると思います。

これであなたもLINQ星人♪

2014年12月4日木曜日

WPF用縦書きテキストブロック Tategaki ver.1.1.2

昨日のリリースに引き続いてさっそく次のバージョンを作ってしまいました。

昨日の記事でまた細かいテストは追々とか言っていましたが、さっそくフラグを回収してしまいました。というのも、Meiryo UIの縦書きフォントが取得できない環境で正常に表示できないバグを持っていたんですね。UIの標準のフォントがMeiryo UIになっている環境がほとんどだと思うのですが、最初起動時にそのフォントがどうしても読み出されて、その時にフォントが見つからなくて例外を吐くようでした。

というわけで、フォントが見つからなかったら例外を吐くのではなく、勝手にMSゴシック→MS明朝の優先順位でフォントを勝手に変更するようにしました。もしもそのどちらも読み込めなかったら、読み込めるフォントのうちの最初のものを読み込むようにしました。

そのほか、例の改行位置の計算にかかわるプログラムの整理をしました。高速化はあまりできていないとは思いますが、少なくとも遅くはなっていないし、コードの綺麗さは一気に上がったと思っています。
並列化をしようかと思ったのですが、GlyphsなどのUIに係るクラスは単一のスレッドでしか動かないのでしようがありませんでした…。

//Redrawすべきかどうかを判定するための以前の状態を保持する変数
string beforeText = null;
double? beforeHeight = null;
double? beforeFontSize = null;
double? beforeSpacing = null;
FontFamily beforeFontFamily = null;
string beforeLastForbiddenChars = null;
string beforeHeadForbiddenChars = null;
string beforeLastHangingChars = null;
double AverageCharHeight = 0;

/// <summary>
/// 必要があったらテキストを再描画するメソッド
/// </summary>
void RedrawText()
{        
    double height = ParentHeight;    //コスト削減

    if((beforeHeight != height) || (Text != beforeText) || (beforeSpacing != Spacing) ||
       (beforeLastForbiddenChars != LastForbiddenChars) || (beforeHeadForbiddenChars != HeadForbiddenChars) || (beforeLastHangingChars != LastHangingChars) ||
       (beforeFontFamily != FontFamily) || (beforeFontSize != FontSize)) {
        if((beforeFontSize != FontSize) || (beforeSpacing != Spacing) || (beforeFontFamily != FontFamily))    //この条件が揃えば文字の高さを再計算
            AverageCharHeight = CalcAverageCharHeight(Text.Split('\n').OrderByDescending(p => p.Length).FirstOrDefault());    //最も長い行を実行する

        try {
            if(!string.IsNullOrEmpty(Text) && (height > 0)) {    //空文字じゃなくてウィンドウの高さがあったら実行する
                string[] DisplayText = Text.Split('\n').SelectMany(p => SplitLine(p, height)).Reverse().ToArray();
                //※SelectManyの前にAsParallel入れたくなるけど、GUI関係はGUIのスレッドじゃないとダメよ

                while(stackPane1.Children.Count < DisplayText.Length)    //StackPanelが少なかったら
                    stackPane1.Children.Add(new TategakiText());        //足りない分を追加する
                
                if(stackPane1.Children.Count > DisplayText.Length)    //StackPanelが多かったら
                    stackPane1.Children.RemoveRange(0, stackPane1.Children.Count - DisplayText.Length);    //多い分を捨てる

                Thickness margin = new Thickness(0, 0, LineMargin, 0);
                for(int i = 0; i < DisplayText.Length; i++) {
                    TategakiText tategaki = (TategakiText)stackPane1.Children[i];

                    tategaki.FontFamily = FontFamily;
                    tategaki.FontSize = FontSize;
                    tategaki.Spacing = Spacing;
                    tategaki.Margin = margin;
                    tategaki.Text = DisplayText[i];
                }
            }
        }
        catch(ArgumentException) {
            stackPane1.Children.Clear();    //フォントがダメなときはクリアする
        }
        finally {
            beforeText = Text;
            beforeHeight = height;
            beforeFontSize = FontSize;
            beforeSpacing = Spacing;
            beforeFontFamily = FontFamily;
        }
    }
}

double CalcAverageCharHeight(string Text)
{
    if(Text != null) {
        try {
            Size size = TategakiText.CalcTextSize(Text, FontFamily.Source, FontSize, FontWeights.Normal, FontStyles.Normal, Spacing);
            return size.Height / Text.Length;    //1文字の平均長(縦書きはだいたい等幅)を計算する
        }
        catch(ArgumentException) {
            return 0;
        }
    } else
        return 0;
}

/// <summary>
/// 行を最大の高さ以内に分割するメソッド
/// </summary>
/// <param name="LineText">行のテキスト</param>
/// <param name="MaxHeight">最大高さ</param>
/// <returns>分割した行</returns>
/// <exception cref="ArgumentException">フォントが読み込めるものではない</exception>
string[] SplitLine(string LineText, double MaxHeight)
{
    List<string> ret = new List<string>();

    if(string.IsNullOrEmpty(LineText))
        ret.Add(string.Empty);
    else {
        while(LineText.Length > 1) {
            int i;
            if(AverageCharHeight <= 0)    //ゼロ除算回避
                i = LineText.Length;
            else {
                for(i = (int)(MaxHeight / AverageCharHeight) + 1; i < LineText.Length; i++) {    //平均長から1行のおよその文字長を割り出す
                    Size size = TategakiText.CalcTextSize(LineText.Substring(0, i), FontFamily.Source, FontSize, FontWeights.Normal, FontStyles.Normal, Spacing);

                    if(size.Height > MaxHeight)    //長さが超えていたらブレーク
                        break;
                }
                i = Math.Max(Math.Min(i - 1, LineText.Length), 1);    //長さが超えたらその1つ小さくなったものから調べればよく、また、最初に決め打った長さがLineText.Lengthを超えてる可能性があるのでそれを合わせ込むが、1より小さくはしない。
            }
            for(; i > 0; i--) {
                Size size = TategakiText.CalcTextSize(LineText.Substring(0, i), FontFamily.Source, FontSize, FontWeights.Normal, FontStyles.Normal, Spacing);
                if(size.Height <= MaxHeight)    //減らしていって長さが切ったらブレーク
                    break;
            }
            i = Math.Max(i, 1);    //0になってたら1にする


            //禁則処理等をする
            while((i < LineText.Length) && LastHangingChars.Contains(LineText[i]))    //次の行の行頭がぶら下げ文字じゃないか?
                i++;    //次の行を取り込む

            while((i > 1) && (i < LineText.Length) && HeadForbiddenChars.Contains(LineText[i]))    //次の行の行頭が禁則文字じゃないか?
                i--;    //行末を差し出す
            
            while((i > 1) && LastForbiddenChars.Contains(LineText[i - 1]))    //文末に禁則文字が含まれていないか?
                i--;    //行末を差し出す


            ret.Add(LineText.Substring(0, i));
            LineText = LineText.Substring(i);
        }
        if(LineText.Length == 1) {    //文字列が1文字だったら強制的に書きだす
            ret.Add(LineText);
            LineText = string.Empty;
        }
    }

    return ret.ToArray();
}

大幅に整理したコードはこんな感じになっています。
テキストをまずは\nで分割し、その分割された行をまた表示領域の高さに合わせて分割します。従来は分割しつつコントロールに転送していましたが、今回は分割するだけでまだstringの配列に入れるだけにしています。これをSelectManyでつなげれば最終的に表示する行が1つのコレクションになりますね。縦書きの行は右から順に並べますがStackPanelは左から並べるので、Reverseでその行の順序を逆にしています。

そして出来上がった行の個数に合わせてコントロールをインスタンス化したり消したりして、最後にまとめて行の内容を登録しています。

結構整理できたと思っています。あの忌々しく何段も続くインデントが無くなったので、見通しが良くなったのは確かです。

ちなみに、各行を分割する処理をするSplitLineメソッドの中で禁則処理もしています。ぶら下げ組み、行頭の禁則処理、行末の禁則処理の順にしています。その処理自体はシンプルですが、何も考えずにその文字が文頭もしくは文末にあるかだけをチェックすると文字数ゼロの行ができて無限ループに陥るのでそこは気を使ってあげました。


というわけで、 羅生門を表示するとこんな感じになっています。


ぶら下げ組み、行頭の禁則処理、行末の禁則処理がしっかりできていることが分かるかと思います。

ダウンロードはこちら。
WPF用縦書きテキストブロック Tategaki ver.1.1.2

2014年12月3日水曜日

WPF用縦書きテキストブロック Tategaki ver.1.1.1

久しぶりにTategakiのプログラムをブラッシュアップしてみました。前回は8月の頭でしたね。

というわけでこんな感じになりました。


[バグフィックス]
  • 一部、プロパティを変更しても画面に反映されないバグを修正(多くのプロパティでそうなっていましたorz
[機能追加]
  • ライブラリ自体は特になし(フロントエンドはかなり強化した)
[破壊的変更]
  • 利用できるフォントを取得するGetAvailableFontsメソッドをAvailableFontsプロパティに変更した
[その他]
  • 行のサイズを計算するプログラムをリファクタリングして2倍くらい高速化した

バグはバグとして今回の一番の工夫はそのリファクタリングですかね。
複数行の縦書きでは、例えばフォントサイズを変えたりウィンドウを変えたりしたら1行に表示できる文字数が変わってしまいます。というわけで、そういう1行の文字数が変わりうるイベントが発生したら行の文字数を計算し直す処理を入れているわけですが、そこの処理が重いとウィンドウサイズやフォントサイズの変更がのっそりしてしまいます。

今までも、およそ縦書きは固定幅なので、適当な長さの文字列から1文字あたりの平均長を計算してそこから1行の文字数をおおよそ決定して後は実際にその長さを計算して合わせこみをするといった処理を導入して高速化をしてきましたが、まだまだ改善の余地がありそうな状態でした。

今までの処理では、サイズが変わるたびに行を全てリセットして、行ごとにTategakiTextクラスのインスタンスを作って、そのTategakiTextインスタンスに実際に適当な長さの文字列を与えてみてそのコントロールがどれくらいのサイズになるかを計算していました。
しかし、わざわざコントロールのインスタンスを作って大きさを測っていたんじゃ処理が重そうなので、1つstaticなGlyphsインスタンスを用意し、必要なパラメーターを与えるとそのインスタンスを使って縦書きテキストのサイズを測るメソッドを作りました。

static Glyphs GlyphForGetLength = new Glyphs();

internal static Size CalcTextSize(string Text, string FontFamilyName, double FontSize, System.Windows.FontWeight FontWeight, FontStyle FontStyle, double Spacing)
{
    if(string.IsNullOrEmpty(Text))
        return Size.Empty;
    else {
        Size infinitySize = new Size(double.PositiveInfinity, double.PositiveInfinity);

        try {
            GlyphForGetLength.FontUri = new Uri(FontPathDictionary[FontFamilyName]);
        }
        catch(KeyNotFoundException) {
            throw new ArgumentException("Cannot use this font.");
        }

        GlyphForGetLength.FontRenderingEmSize = FontSize;

        GlyphForGetLength.StyleSimulations =
            ((FontWeight != FontWeights.Normal) ? StyleSimulations.BoldSimulation : StyleSimulations.None) |
            ((FontStyle != FontStyles.Normal) ? StyleSimulations.ItalicSimulation : StyleSimulations.None);

        GlyphForGetLength.Indices = GetIndices(Text, FontFamilyName, (int)FontSize, Spacing);
        GlyphForGetLength.UnicodeString = Text;

        GlyphForGetLength.UpdateLayout();
        GlyphForGetLength.Measure(infinitySize);

        return new Size(GlyphForGetLength.DesiredSize.Height, GlyphForGetLength.DesiredSize.Width);    //回転するので縦横入れ替える
    }
}


さらに、現在表示している行がある場合はそのインスタンスを再利用して、新たにインスタンス化するコストを抑えました。

string beforeText = null;
double? beforeHeight = null;
double? beforeFontSize = null;
double? beforeSpacing = null;
FontFamily beforeFontFamily = null;

void RedrawText()
{
    double height = ParentHeight;

    if((beforeHeight == null) || (beforeHeight != height) || (beforeFontSize != FontSize) || (beforeSpacing != Spacing) || (beforeFontFamily != FontFamily) || !object.Equals(Text, beforeText)) {
        int lineIndex = 0;
                        
        if(!string.IsNullOrEmpty(Text)) {
            string[] Lines = Text.Split('\n');
            Size infinitySize = new Size(double.PositiveInfinity, double.PositiveInfinity);

            if((height <= 0) || (Lines.Length == 0))    //そのまま表示
                GetLineControl(lineIndex++).Text = Text;
            else {
                foreach(string line in Lines) {
                    string text = line;

                    if(line.Length == 0)
                        GetLineControl(lineIndex++).Text = string.Empty;    //行が空なら空にする
                    else {
                        while(text.Length > 1) {
                            Size size = TategakiText.CalcTextSize(text, FontFamily.Source, FontSize, FontWeights.Normal, FontStyles.Normal, Spacing);
                            double charHeight = size.Height / text.Length;    //1文字の平均長(縦書きはだいたい等幅)を計算する

                            int i;
                            if(charHeight == 0)    //ゼロ除算回避
                                i = text.Length;
                            else {
                                for(i = (int)(height / charHeight) + 1; i < text.Length; i++) {    //平均長から1行のおよその文字長を割り出す
                                    size = TategakiText.CalcTextSize(text.Substring(0, i), FontFamily.Source, FontSize, FontWeights.Normal, FontStyles.Normal, Spacing);

                                    if(size.Height > height)    //長さが超えていたらブレーク
                                        break;
                                }
                                i = Math.Max(Math.Min(i - 1, text.Length), 1);    //長さが超えたらその1つ小さくなったものから調べればよく、また、最初に決め打った長さがtext.Lengthを超えてる可能性があるのでそれを合わせ込むが、1より小さくはしない。
                            }
                            for(; i > 0; i--) {
                                size = TategakiText.CalcTextSize(text.Substring(0, i), FontFamily.Source, FontSize, FontWeights.Normal, FontStyles.Normal, Spacing);
                                if(size.Height <= height)    //減らしていって長さが切ったらブレーク
                                    break;
                            }
                            GetLineControl(lineIndex++).Text = text.Substring(0, i);
                            
                            text = text.Substring(Math.Max(i, 1));    //iが0になってきたらそれは1文字
                        }
                        if(text.Length == 1) {    //文字列が1文字だったら強制的に書きだす
                            GetLineControl(lineIndex++).Text = text;
                            text = string.Empty;
                        }
                    }
                }
            }
        }
        beforeText = Text;
        beforeHeight = height;
        beforeFontSize = FontSize;
        beforeSpacing = Spacing;
        beforeFontFamily = FontFamily;

        FixLineControls(lineIndex);
    }
}

こんな感じです。あんまりインデントが深くなるコードは好ましくないんですがね。という意味でまだ改良の余地はありそうな気もします。
GetLineControl()メソッドが、行インデックスを与えるとそのコントロールを返してくれるメソッドです。今までは最初にstackPanel1.Children.Clear()をして、GetLineControl()の代わりに新たにTategakiTextをインスタンス化していたのですが、そのコストを抑えるためにこうやって再利用するようにしています。もしもすでに用意しているインスタンスより多くの行が必要なときは新たにインスタンスを作る処理を入れています。

/// <summary>
/// 行のコントロールを取得するメソッド
/// </summary>
/// <param name="LineIndex">行番号</param>
/// <returns>その行のインスタンス</returns>
TategakiText GetLineControl(int LineIndex)
{
    if(LineIndex < 0)
        throw new ArgumentOutOfRangeException("The index must not be negative.");

    int index = stackPane1.Children.Count - LineIndex - 1;

    if(index < 0) {
        TategakiText tategaki = new TategakiText();
        stackPane1.Children.Insert(0, tategaki);
        return tategaki;
    } else
        return (TategakiText)stackPane1.Children[index];
}

こんな感じですね。

最後に、もしも更新前から用意されていた行より少ない行のテキストだった場合にその余ったインスタンスを消す作業と、あと、全体に共通なフォント等の更新を行っています。

/// <summary>
/// 行のコントロールを整理するメソッド
/// </summary>
/// <param name="LineCount">行の総数</param>
void FixLineControls(int LineCount)
{
    if(LineCount < 0)
        throw new ArgumentOutOfRangeException("The index must not be negative.");

    //まず、いらない行を削除する
    int deleteCnt = stackPane1.Children.Count - LineCount;

    if(deleteCnt > 0)
        stackPane1.Children.RemoveRange(0, deleteCnt);
    
    //次に、パラメーターを調整する
    Thickness margin = new Thickness(0, 0, LineMargin, 0);
    foreach(TategakiText c in stackPane1.Children) {
        c.FontFamily = FontFamily;
        c.FontSize = FontSize;
        c.Spacing = Spacing;
        c.Margin = margin;
    }
}

これで、実測で1回のサイズ計算処理が8msから4msに短くなりました。


さて、それ以外にもフロントエンドは相当の機能追加をしています。
まずはExtended WPF ToolkitをNugetから参照しDoubleUpDownとColorPickerを活用しています。
そして、GUIでGUIを変更する処理をしているので、もはやXAMLですべてを表現してしまっています。ViewModelとか必要ありません。

多くはバインディングで実現できています。例えば、

<Slider Grid.Row="2" Grid.Column="1" Name="slider_fontsize" Minimum="5" Maximum="72" Value="18"/>

このようにスライダーでフォントサイズを適当な範囲で動かせるようにした上で

<tg:TategakiMultiline FontSize="{Binding ElementName=slider_fontsize, Path=Value}" />

このようにエレメント名とパスを指定してやることでバインディングできます。

また、ColorPickerはColor構造体を返しますが、文字の色(Foreground)はBrushになるので、その変換をしてやる必要があります。同様に、BoldやItalicのチェックボックスもFontWeightやFontStyleに変換してやる必要がありますね。
それは、ValueConverterという技を使えば実現することができます。

[ValueConversion(typeof(bool), typeof(FontStyle))]
public class BooleanToFontStyleConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return (bool)value ? FontStyles.Italic : FontStyles.Normal;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return (FontStyle)value != FontStyles.Normal;
    }
}

このようににIValueConverterインターフェースを実装してあげて、あとはXAMLで適当にインスタンス化してConverterに登録してあげるだけですね。

こんな調子で適当に各プロパティの実装をしてあげればおkです。

最後に、TategakiTextやTategakiMultilineを入れたStackPanelをScrollViewerに入れてやりました。これでスクロールができるようになります。が、VerticalScrollBarVisibilityをDisaabledにしないと任意の縦方向の長さになってしまうので、ここだけは注意する必要があります。


こんな感じで、どんどん縦書きテキストブロックに磨きがかかってきています。
まだまだテストが不十分なところもありそうな気もしますが、それは追々またやっていくということで。

ダウンロードはこちらからどうぞ。
WPF用縦書きテキストブロック Tategaki ver.1.1.1
((2015/1/22)都合により削除しました。ver1系はver.1.1.2を使ってください)

2014年11月14日金曜日

2048のGUIを実装する

今朝から.NET周りの非常に大きなニュースがたくさん出回っていて、いろいろとアツいですね。
その中でも、我々WPFerにとっても胸の熱いニュースが出ています。

The Roadmap for WPF

最近どうなってるのかよく分からなかったWPFですが、今後もまたさらなる発展が期待できそうな文面ですね。高まります。

というわけで、長らく放置にしてしまっていた2048の件ですが、やっと前回の続きのGUIの作成を行いました。もちろんWPF+Livetを使っての作成です。


前回の記事では、2048のマトリックスを表示する部分をサボってテキストボックスにし、そこにテキストとしてマトリックスとスコアを表示するプログラムにしていました。さすがにそれではみすぼらしすぎますね。というわけで、行列を表示するGUIを設計してみたいと思います。

行列状のGUIの配列と言ったらパッと思い浮かぶのはGridでしょうか。比較的簡単に綺麗に等間隔にGUIコンポーネントを配置できるので、普段からGUI設計では多用しているかと思います。

一方、今回の2048ではこのマトリックスのサイズを自由に設定できるような設計をしていました。
一般的に出回ってるゲームでは4x4ですが、別に4x4にしなければならない理由はどこにも無いので、そういったことにとらわれないコードを書いています。
しかし、Gridは通常設計段階でどのように分割するかがわかっているものです。そもそもXAMLがそこまでプログラムの条件に合わせて柔軟に動的に何かを生成するものでもないですしね。
となると、ListViewDataGridなどが思い浮かぶわけですが、いまいちピンときませんね。

というわけで、今回はGridベースにマトリックスを表示するコントロールを自作してみようと思います。
コードビハインドでいじってGridのRowDefinitionsなどをC#からいじれば動的に行の追加等はできます。が、ViewModelとViewのコードビハインドは連携しにくいので、というより、コードビハインドでViewに完結しない操作をするのは越権行為ですね。というわけで、マトリックスを表示するコントロールという形でその機能を分離し、View内完結に収めてやろうということにしたわけです。

<UserControl x:Class="Game2048.Views.GameTable"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" >
    <Grid Name="grid" />
</UserControl>

自作したコントロール、GameTableはこんな感じです。Gridを中に作って、あとはコードビハインドからいじれるように名前を定義してあげるだけです。

メインディッシュはこのXAMLのコードビハインドですね。3つの依存関係プロパティを用意してあげます。
あ、依存関係プロパティって何?って方もいるかもしれませんが、まあ、これを使うとXAMLからバインディングできるようになるって理解で良いと思います。多分。

/// <summary>
/// グリッドを表示するかどうか
/// </summary>
public bool ShowGridLines
{
    get { return (bool)this.GetValue(ShowGridLinesProperty); }
    set { this.SetValue(ShowGridLinesProperty, value); }
}
public static DependencyProperty ShowGridLinesProperty = DependencyProperty.Register("ShowGridLines", typeof(bool), typeof(GameTable), new UIPropertyMetadata((d, e) => {
    ((GameTable)d).grid.ShowGridLines = (bool)e.NewValue;
}));

/// <summary>
/// セルのテンプレート
/// </summary>
public DataTemplate ItemTemplate
{
    get { return (DataTemplate)this.GetValue(ItemTemplateProperty); }
    set { this.SetValue(ItemTemplateProperty, value); }
}
public static DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(GameTable));

まずはこの2つの依存関係プロパティです。比較的やっていることが軽いのでまとめて紹介します。
1つ目はグリッドを表示するかどうかです。このプロパティをそのままGridに転送しています。
2つ目はセルのテンプレートです。このままいくとマトリックスのセルをそのままユーザーコントロールのコードビハインドから生成することになってしまって、XAML側からの編集性に欠けます。というわけで、テンプレートをこのプロパティを介して渡すことで、XAML側でデザインしたセルをこちらで使えるようにしています。

/// <summary>
/// 表示するマトリックス
/// </summary>
public ObservableMatrix2D<int> Matrix
{
    get { return (ObservableMatrix2D<int>)this.GetValue(MatrixProperty); }
    set { this.SetValue(MatrixProperty, value); }
}
public static DependencyProperty MatrixProperty = DependencyProperty.Register("Matrix", typeof(ObservableMatrix2D<int>), typeof(GameTable), new UIPropertyMetadata((d, e) => {
    INotifyCollectionChanged NewValue = e.NewValue as INotifyCollectionChanged;
    INotifyCollectionChanged OldValue = e.OldValue as INotifyCollectionChanged;

    GameTable me = (GameTable)d;

    if(NewValue != null)
        NewValue.CollectionChanged += me.CollectionChanged;

    if(OldValue != null)
        OldValue.CollectionChanged -= me.CollectionChanged;

    me.MatrixSizeChanged();
}));

void CollectionChanged(object o, NotifyCollectionChangedEventArgs e)
{
    if(e.Action == NotifyCollectionChangedAction.Replace)
        MatrixContentChanged();
    else
        MatrixSizeChanged();
}

/// <summary>
/// マトリックスのサイズが変更された
/// </summary>
void MatrixSizeChanged()
{
    grid.RowDefinitions.Clear();
    grid.ColumnDefinitions.Clear();
    grid.Children.Clear();
    if(Matrix != null) {
        for(int i = 0; i < Matrix.RowCount; i++)
            grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) });
        for(int i = 0; i < Matrix.ColumnCount; i++)
            grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) });

        for(int i = 0; i < Matrix.RowCount; i++) {
            for(int j = 0; j < Matrix.ColumnCount; j++) {
                FrameworkElement fe = (FrameworkElement)ItemTemplate.LoadContent();

                fe.DataContext = Matrix[i, j];
                fe.SetValue(Grid.RowProperty, i);
                fe.SetValue(Grid.ColumnProperty, j);

                grid.Children.Add(fe);
            }
        }
    }
}

/// <summary>
/// マトリックスの中身が変更された
/// </summary>
void MatrixContentChanged()
{
    foreach(FrameworkElement child in grid.Children) {
        int r = (int)child.GetValue(Grid.RowProperty);
        int c = (int)child.GetValue(Grid.ColumnProperty);

        child.DataContext = Matrix[r, c];
    }
}

最後に、表示するマトリックスを保管する依存関係プロパティと、それ関連のメソッドです。
マトリックスはそのままint[,]型で与えたんじゃ中身の変更に対しての通知機能が一切ないので、 INotifyCollectionChangedインターフェースを実装したObservableMatrix2D<T>クラスを用意してそれを使っています。変数の変更は依存関係プロパティの登録時に与えるUIPropertyMetadataのコンストラクタの引数に与えるデリゲートで受け取れますが、これはあくまでもこの変数の変更であって、INotifyCollectionChangedが引き起こす変更通知イベントは拾わないので、これに新たにイベントハンドラを付け加えて受信できるようにしています。
マトリックスのサイズが変更されるといった大きな変更では、Gridの行と列を新たに作り直し、その後にそのGridのそれぞれのセルにDataTemplateのインスタンスを作って設定します。DataTemplateはLoadContentメソッドでそのインスタンスを作れます。インスタントを作った後は、DataContextにマトリックスのデータを埋め込み、あとはGridの添付プロパティで行と列のインデックスを指定してやっています。
マトリックスの中身が変更された時は、逆にGridの子コントロールからその添付プロパティを抽出して座標を選択し、DataContextを更新することで対応しています。

これで、汎用的なマトリックス表示コントロールができました。

なお、上で出てきたObservableMatrix2D<T>ですが、まあ、この実装は内部で2次元配列を持ってどうこうやる陳腐なものなのであえて細かく解説はしませんが、ピンポイントで説明するとしたらここでしょう。

public T this[int row, int column]
{
    get { return data[row, column]; }
    set
    {
        if(!object.Equals(data[row, column], value)) {
            T old = data[row, column];

            data[row, column] = value;
            if(CollectionChanged != null)
                CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, old));
        }
    }
}

マトリックスのある要素が変更された時はINotifyCollectionChangedで定義されたCollectionChangedイベントを発生させています。NotifyCollectionChangedEventArgsではどのような変化が起こったのかを示す列挙型を指定しますが、これは、NotifyCollectionChangedEventArgsのコンストラクタによって指定できる列挙型のメンバが決まっているので、MSDNをよく読んで作ってください。

さて、実際にこの作ったコントロールを使っていきます。

<v:GameTable Matrix="{Binding Matrix}" ShowGridLines="False" >
    <v:GameTable.ItemTemplate>
        <DataTemplate>
            <Grid>
                <Rectangle Stroke="Black" StrokeThickness="1" Fill="{Binding Converter={StaticResource NumToBrushConv}}" RadiusX="10" RadiusY="10" />
                <TextBlock Text="{Binding Converter={StaticResource ZeroDontSshowConv}}" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="30" />
            </Grid>
        </DataTemplate>
    </v:GameTable.ItemTemplate>
</v:GameTable>

このようにMainWindow.xamlで定義します。Matrixの中身はViewModelのデータをバインディングします。今回は、セルのテンプレートでグリッド枠相当の描写をやるので、ShowGridLinesはFalseにしています。

RectangleとTextBlockでは、それぞれFillとTextのデータにそのMatrixの各要素の値がバインディングされています。
TextBlockの場合は、そのままバインディングするとゼロもそのまま表示されてしまうので、ゼロの場合は表示しないようにするようなコンバーターを実装してそれを読ませています。
Rectangleの場合は、数字の大きさによって色を変化させるようなコンバーターを実装して、それをかませることで数値を色に変換しています。

さて、これを周囲の上下左右のボタンで操作できるようにしています。

<Button DockPanel.Dock="Left" Content="←" IsEnabled="{Binding CanMoveLeft}" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="MoveLeft" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

これは左の実装ですが、上下左右4種類実装してあげています。
また、キーボードの矢印キーを使っても同様の操作ができるべきなので、それも実装してあげます。

<ei:KeyTrigger Key="Left">
    <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="MoveLeft" />
</ei:KeyTrigger>

Livetでは、このようにKeyTriggerをi:Interaction.Triggersの中に入れてあげれば、キー入力から直接メソッドをバインディングして呼び出せるので便利です。

これで完成です。


どうです?それっぽくなりましたよね?
ですがまあまだスコアの表示の実装とか、設定、リセット等の実装ができていません。
その辺りが煮詰まってきたら、オートソルバーの実装かな?

どれだけ時間のかかることやら。

2014年10月23日木曜日

LINQ to Excelを使う

今回はLINQ to Excelを使ってみたいと思います。

ExcelのファイルをC#で読み込もうとしたら、少しググればCOM相互運用を使って使う方法が出てきます。でもこの方法、Excelを実際に起動させて自分のプログラムとやり取りするので画面がとても鬱陶しいことになりますし、おそらくExcelが無いと使えません。

ですが、このLINQ to Excel、若干癖があって、というよりか、使い方が一部直観的じゃなくてリファレンス無しにいろいろ使えるわけでもないんですよね(使い方がわかればとても便利だと思います)。一方、COM相互運用側からExcelを使う方法もあるため、特に日本語ではLINQ to Excelの記事はあまり見かけません(例によって自分のググり力が足りないだけかもしれませんが)。


とりあえず、適当なデータを用意しないとサンプルを示すに示せないので、こんなエクセルシートを用意してみました。にゃんぱすーฅ(๑'Δ'๑)
ファイルはxlsでもxlsxでもcsvでも読めるっぽいですが、どうもExcel2013で作ったxlsxは読み込めないっぽいです。Excel2010なら読み込めました。また、読み込もうとしているファイルをExcelで開いていたりすると途中で例外を吐いてしまいますので注意してください。

任意のセルを読み出す

LINQ to Excelは任意のセルを読み出すのにははっきり言って全くもって適していません。でも、まあ一応読み出せるので読み出し方を紹介します。

var excel = new ExcelQueryFactory(@"test.xls");
excel.ReadOnly = true;
Console.WriteLine(excel.WorksheetRangeNoHeader("B3", "B3", "Sheet1").First().First().Value.ToString());

とりあえず呪文のように思ってください。この記事を最後まで読むと意味がわかると思います。読み出すデータはSheet1のB3です。これを実行するとしっかりと
2002/05/28 0:00:00
と表示されます。

正攻法的に読み出す

さて、このLINQ to Excelは、基本的に上のエクセルシートのように1行目がヘッダーで、2行目以降がデータになっているデータを読み出すことに特化し、様々な機能が作られているようです。そのため、WorksheetはRowクラスの列挙可能型として定義されており、RowはList<Cell>を継承したものとして定義されています。
というわけで、正攻法的に読みだしてみます。

var excel = new ExcelQueryFactory(@"test.xls");
excel.ReadOnly = true;
var worksheet = excel.Worksheet("Sheet1");

var q = from p in worksheet
        select new {
            Name = p["名前"].Cast<string>(),
            Birthday = p["誕生日"].Cast<DateTime>(),
            Height = p["身長"].Cast<int>(),
            BloodType = p["血液型"].Cast<string>(),
        };

foreach(var p in q)
    Console.WriteLine("{0}, {1}, {2}cm, {3}", p.Name, p.Birthday, p.Height, p.BloodType);

Sheet1のワークシートを読み込んでからLINQのクエリ文で匿名クラスを定義し、各行をデータ化しています。ワークシートは行の列挙可能型で、行からはインデクサーのオーバーロードでヘッダー名でその列に相当するセルを読み出しています。セルはobject型のValueプロパティを持っていますのでこれを適当にキャストして読み出してもいいのですが、例えばdoube型がobjectにキャストされているときはいきなりintにキャストできませんし、例えば越谷卓は血液型が空欄ですので、そのデータ型はDBNullになっているのでstringにキャストはできません。そういう関係で、Cast<T>()型のほうが使い勝手が良いですね。

データをヘッダーと同名のプロパティを持つクラスに読み込む

さて、続いてこのようなクラスを作ってみます。

public class Person
{
    public string 名前 { get; set; }
    public DateTime 誕生日 { get; set; }
    public int 身長 { get; set; }
    public string 血液型 { get; set; }
}

ヘッダーと同じ名前のプロパティを持ったクラスですね。違和感はとてもありますが、C#はUnicodeなので日本語プロパティも作ることができます。

var excel = new ExcelQueryFactory(@"test.xls");
excel.ReadOnly = true;
var worksheet = excel.Worksheet<Person>("Sheet1");

foreach(var p in worksheet)
    Console.WriteLine("{0}, {1}, {2}cm, {3}", p.名前, p.誕生日, p.身長, p.血液型);

こうすることで、直接ワークシートをPersonの列挙可能型として読み込めるようになりました。超便利です。Cast<T>()も必要ないので、非常にすっきりしました。

データをヘッダーと同名のプロパティを持たないクラスに読み込む

しかし、常にヘッダーと同名のプロパティを持つクラスを用意できるかと言ったらそうとは限りません。ヘッダー名は実行するまでわからないかもしれませんし、そもそも上記のような日本語プロパティは回避したいときなどもあるでしょう。
大丈夫です、そのようなときも、極力簡単に特定のクラスにデータを読み込むことができます。

まずは同様にPersonクラスを作ってみます。

public class Person
{
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
    public int Height { get; set; }
    public string BloodType { get; set; }
}

さて、当然のことながら、ヘッダーとプロパティ名が異なってしまったので、それを対応付ける作業をしなければいけません。それを「マッピング」と呼んでいるようです。

var excel = new ExcelQueryFactory(@"test.xls");
excel.ReadOnly = true;
excel.AddMapping<Person>(p => p.Name, "名前");
excel.AddMapping<Person>(p => p.Birthday, "誕生日");
excel.AddMapping<Person>(p => p.Height, "身長");
excel.AddMapping<Person>(p => p.BloodType, "血液型");
var worksheet = excel.Worksheet<Person>("Sheet1");

foreach(var p in worksheet)
    Console.WriteLine("{0}, {1}, {2}cm, {3}", p.Name, p.Birthday, p.Height, p.BloodType);

このような形でマッピングを追加することでプロパティ名とヘッダー名の対応付けをすることができます。これでめでたくヘッダーと違う名前のプロパティでも適当に代入してくれるようになりました。

ちなみに、AddMapping<T>()の1つ目の引数のラムダ式が何これ?って思う人もいるかもしれませんが、これはいわゆる式木を使った技法で、こうすることでこのラムダ式を受け取った側はプロパティの名前が取得できるようになります。AddMappingのオーバーロードを見るとわかりますが、このような方法を使わずとも単にプロパティ名をstringで与えるオーバーロードもありますが、そのようなものに対して、こっちのほうがIDEのサポートを受けられる(サジェスト、リファクタリング等)ので好んで使われる技法のようです。MVVMのViewModelを作るときに通知可能プロパティで同様の技法を使ったりすることもあります。

データをヘッダーと同名のプロパティを持たないクラスに読み込む その2

上記のようにメソッドでマッピングする以外に、属性を使った方法もあります。

public class Person
{
    [ExcelColumn("名前")]
    public string Name { get; set; }

    [ExcelColumn("誕生日")]
    public DateTime Birthday { get; set; }

    [ExcelColumn("身長")]
    public int Height { get; set; }

    [ExcelColumn("血液型")]
    public string BloodType { get; set; }
}

このように、Personクラスのそれぞれのプロパティに属性をつけてやります。それだけでマッピングはしなくても同様に読み込めるようになります。簡単ですね。

ヘッダーが無いデータを読み込む

中にはヘッダーと呼べる行がなく、いきなり1行目からデータになっているエクセルシートもあるでしょう。そのようなものを読み込むこともできます。
本来は、サンプルコードとしてはWorksheetNoHeader()を呼び出すべきなんでしょうが、元のエクセルシートを書き換えるのもアレなので、下記のようにWorksheetRangeNoHeaderを呼び出しています。これは、指定した範囲内をヘッダーの無いデータとして認識するメソッドです。

var excel = new ExcelQueryFactory(@"test.xls");
excel.ReadOnly = true;
var worksheet = excel.WorksheetRangeNoHeader("A2", "D6", "Sheet1");

var q = from p in worksheet
        select new {
            Name = p[0].Cast<string>(),
            Birthday = p[1].Cast<DateTime>(),
            Height = p[2].Cast<int>(),
            BloodType = p[3].Cast<string>(),
        };

foreach(var p in q)
    Console.WriteLine("{0}, {1}, {2}cm, {3}", p.Name, p.Birthday, p.Height, p.BloodType);

NoHeaderがつくメソッドを使った場合、行はRowクラスではなくRowNoHeaderクラスになります。これは、インデクサーにstringを受け取るオーバーロードがなく、セルのインデックスをint型で与えるしかありません。

LINQする

さて、ここまでデータの読み込み方が中心でした。肝心のLINQのクエリ文はあまり書いていません。まあ、どうせみんなわかってるだろうから解説しても仕方がないっていうのもあるとは思いますが。

var worksheet = excel.Worksheet<Person>("Sheet1");

var q = from p in worksheet
        where p.Birthday > new DateTime(2000, 1, 1)
        orderby p.Birthday descending
        select p;

例えばクエリ文はこんな感じで書けますね。もう至って普通です。
前述しましたが、worksheetは行の集まりなので、pは各行になります。と言っても、Worksheet<Person>で行はPersonに変換済みですから、ここから容易にPersonの条件を設定できます。こでは、2000年以降に生まれた人を若い人順に並べていますね。あまり説明はいらないと思います。
特定のクラスに変換しない場合はpがRowになりますので、where文の中では例えばp["誕生日"].Cast<DateTime>()みたいな書き方をすればいいと思います。若干煩わしいので、変換をかましたほうがきれいそうですね。



さて、ここまで解説すれば大体のExcelファイルについて「ヘッダー+データ」という構造ならば簡単に読み込めると思います。そういう構造じゃなければ、もしくは、ワークシートに対してセルのデータを読み出す以上の仕事を要求するのならそもそもLINQ to Excelは使えません。
そもそもExcel自体に強力なデータ整理機能があるので、あえてExcelでできるようなことをC#でやる必要はないとは思いますが、複数のExcelファイルにまたがった処理とか、毎回やるような簡単な処理をバッチ化したいとかでLINQ to Excelを活用できるととても便利かもしれません。

2014年10月15日水曜日

非同期処理とAggregateException

以前の記事で2048のGUIを作ろうとかオートソルバーを作ろうとか言っておきながら手が付いていないのですが、その件は一旦保留してまた別の記事を書いてみようと思います。

さて、以前、LINQ to Twitter v3の記事を書きましたが、ちょっとこれについてまたいじっていたら例外処理で躓きましたので、一般化して話をしていこうと思います。
例えばネットにつながっていないPCからLINQ to Twitterでタイムラインを読み込もうとしてしまうようなことがあるかもしれません。そういった場合、例外が吐かれます。 まあWebExceptionとかHttpRequestExceptionとかそのあたりが吐かれるんだろうなーと思って適当に例外処理していると痛い目に遭いました。AggregateExceptionとかいう見慣れない例外が吐かれます。

さて、AggregateExceptionが一体どのような例外なのかを調べるためにMSDNに行ってみます。

AggregateException クラス (System)

「アプリケーションの実行中に発生する1つ以上のエラーを表します。」って書いてありますがその本質はよくわかりません。どうもInnerExceptionsというコレクションがあり、複数の例外をこのクラスで保持することができるようですが、なぜそのような例外が吐かれるのかがわかりません。素直にWebExceptionとかじゃダメなの?複数の場所で例外が発生するような状況だったとしても、1つ目に遭遇した例外が吐かれればそれで問題ないよね?というか、通常の例外処理がそうでしょ?なのになぜ敢えてこのように複数の例外をまとめるような例外が吐かれるのでしょう。


その疑問に答えるべく、まずひとつこの例外を吐くメソッドを挙げてみます。
わかりやすい例としてParallel.Forが挙げられます。 指定した範囲の整数を受け取るデリゲートを同時並行で動かすメソッドです。もっと正確にいえば、同時並行に動かす可能性があるです。詳しい説明は省きますが、スレッドプールを利用して実行するため、環境や設定によっては単一スレッドでの実行になるかもしれませんし、その順序も、どの値同士が並列で実行されるかもわかりません。こんな運要素の大きなメソッドですが、この中で特定の値で例外を吐くようなプログラムを書いて実行してみます。

try {
    Parallel.For(0, 100, (i) => {
        if(i == 0)
            throw new InvalidOperationException();
        if(i == 1)
            throw new ArgumentException();
        Console.WriteLine(i);
    });
}
catch(AggregateException e) {
    ShowAggregateException(e);
}

ちなみにShowAggregateExceptionメソッドは次のように実装しています。

static void ShowAggregateException(AggregateException ae)
{
    ShowAggregateException(ae, 0);
}

static void ShowAggregateException(AggregateException ae, int depth)
{
    Console.WriteLine(new string(' ', depth) + ae.GetType().Name);
    foreach(var e in ae.InnerExceptions) {
        if(e is AggregateException)
            ShowAggregateException((AggregateException)e, depth + 1);
        else
            Console.WriteLine(new string(' ', depth + 1) + e.GetType().Name);
    }
}

いわゆる再帰ですね。AggregateExceptionに含まれるすべての内部例外を表示しますが、その時にその内部例外にAggregateExceptionが含まれていたら再帰的にこいつもこのメソッドで表示させています。そして、階層が深まるたびにインデントを追加しています。new string(' ', depth + 1)がそれにあたります。

さて、これを実行するわけですが、VisualStudioを使う場合は「デバッグなしで開始」で実行する必要があります。「デバッグ開始」でやると、下の図のようにthrow文のところで「ハンドルされていない例外としてVisualStudioにキャッチされてしまい、AggregateExceptionが活躍しているところが見られません。


同じコードでデバッグ時の挙動とデバッグしないときの挙動で違うのも困りものですが、まあ、AggregateException行きになる時点でハンドルされていない例外とも言えるのか…な?なんていうか、単にVisual Studioの対応力不足な気もしますが。
さて、話を戻します。実際にデバッグなしで実行してみると、例えば下図のような実行結果が得られます。


表示されている数字が昇順や降順でないあたり、並列処理が実行されていそうな雰囲気ですね。
そして、InvalidOperationExceptionとArgumentExceptionが同時に吐かれていることがわかると思います。なるほど、確かにこういう場面を想定するとAggregateExceptionも役に立ちそうですね。
ちなみにですが、先ほども言いましたがこのParallel.Forはかなり気まぐれなメソッドです。何回か実行してみると、表示される数字の順番は毎回異なりますし、InvalidOperationExceptionしか例外が吐かれないこともあります。おそらくどこか1つでも例外を吐いたら直ちに新しい処理を開始するのをやめてメインルーチンに処理を返す仕様になっているんでしょうね。たまたま0と1が別スレッドで同時に実行された時のみ、上記のコードではこのような実行結果を吐くでしょう。なので、もしかしたらシングルコアのCPUのマシンとかだったらこれは一生再現できないかもしれませんね。


さてさて、ここではParallel.Forを使ってみましたが、もともとはこれは非同期処理全般に使われている例外のようです。非同期処理の基礎で出てくるこのあたりのメソッドやプロパティでもAggregateExceptionが吐かれるようです。

Task.Wait メソッド  (System.Threading.Tasks)
Task(TResult).Result プロパティ  (System.Threading.Tasks)

Task.Resultのほうには明言されていませんが、これはタスクの終了を待ってから制御を返すプロパティなので、内部的にWaitを呼んでいるのか知りませんが、これもAggregateExceptionを吐くことがあるようです。

Task t = Task.Factory.StartNew(() => {
    throw new InvalidOperationException();
});
Thread.Sleep(1000);
try {
    t.Wait();
}
catch(AggregateException e) {
    ShowAggregateException(e);
}

さて、このように非同期処理中で例外を吐くプログラムを書いてみましょう。
タスクを開始してからメインスレッドでは怪しげな1秒のスリープを入れてみました。こうすることで、t.Wait()が呼ばれる前に例外を吐くスレッドのほうではいかにも例外を吐いたであろう状態にあることは明らかです(もちろん、スレッドプールが2スレッド以上あることが前提ですが)。ですが、その例外は即座にプログラムの実行に影響を及ぼしません。t.Wait()が呼び出されたときに例外が吐かれます。予想通り、AggregateExceptionの中にはInvalidOperationExceptionが1つ入っています。

では、この別スレッドの中でさらに別スレッドにを実行し、そこで例外を吐かせてみたらどうでしょう。

Task t = Task.Factory.StartNew(() => {
    Task.Factory.StartNew(() => {
        throw new ArgumentException();
    }, TaskCreationOptions.AttachedToParent);
    throw new InvalidOperationException();
});
try {
    t.Wait();
}
catch(AggregateException e) {
    ShowAggregateException(e);
}

ここで、子スレッドにはTaskCreationOptions.AttachedToParentオプションを付けていることに気を付けてください。これを付けないと、ArgumentExceptionを吐くスレッドが終わる前にInvalidOperationExceptionを吐くスレッドが終わってしまって、ArgumentExceptionのほうがAggregateExceptionに含まれなくなってしまいます。


この通り、見事にInvalidOperationExceptionとAggregateExceptionを含むAggregateExceptionが吐かれました。


さて、ここまで見てきたら、何となくなぜ非同期処理や並列処理でAggregateExceptionが必要なのかがわかってきたかと思います。非同期処理や並列処理は根本的に複数の例外を同時に吐く可能性をはらんでいる点があるからAggregateExceptionが必要というわけです。
しかし、よくよく考えれば、例えば別スレッド内での例外をそのままWait()呼び出し時に再スローするような仕組みをつくった場合、Waitメソッドが吐く例外の種類をメソッドのドキュメンテーションに定義できなくなりますし、Waitメソッドそのものが吐いた例外なのか、それとも別スレッド内のユーザーが書いた処理に起因する例外なのかを判別することもできません。そういう意味からも、例外をカプセル化してしまったほうが良い、といった判断なのかもしれません。

最後に、少し実用的なコードを書いてみようと思います。

using(WebClient wc = new WebClient()) {
    var t = wc.DownloadStringTaskAsync(@"http://www.google.co.jp");
    try {
        Console.WriteLine(t.Result);
    }
    catch(AggregateException ae) {
        ae.Handle((e)=>{
            if(e is WebException) {
                Console.WriteLine(e.GetType().Name);
                Console.WriteLine(" " + e.Message);
                return true;
            } else
                return false;
        });
    }
}

こんな感じでどうでしょうか。
非同期にGoogleのトップページのソースをダウンロードしています。正常にダウンロードできれば画面にそのテキストが表示されます。一方、正常にダウンロードできなかったらAggregateExceptionが発生します。このHandleメソッドでは、InnerExceptionsに含まれてるすべての例外についての例外処理のデリゲートを 書いています。処理した例外はtrueを返し、処理しなかったらelseを返します。
例えばデスクトップPCならLANケーブルを引っこ抜いて、ノートPCなら機内モードにするなどで正常にネットにつながらない状態にしてみましょう。そうすると、見事にAggregateExceptionが吐かれ、そのWebExceptionについての例外情報が画面に表示されます。

LINQ to Twitter v3でもクエリが並列非同期処理となっているためか例外はAggregateExceptionに内包される形で吐かれるようです。なので、上記のようにAggregateExceptionを処理する形に書き直しておけば、各々の例外に対応することができるようになります。


今回は非同期処理の例外処理について紹介しました。
非同期処理や並列処理の簡単さはC#5.0/.NET4.5あたりの目玉機能ですが、この辺りを使うときはぜひとも気を使ってみたいところですね。

2014年10月9日木曜日

Html Agility PackとXPath

今回はC#でHTMLパースをしてみようと思います。

C#でHTMLをパースする方法をググると真っ先に出てくるのがHtml Agility Packというライブラリです。しかし、Documentationは何も書いておらず、ググって出てくる断片的な記事しか無いので、私も1つの断片的な記事を書いてみようと思います(

さて、このライブラリ、どうもXPathというものと切っても切れない関係があるようです。
XPathというものを知らなかった私は、ググりながら、同時にHtml Agility Packを使っていかなければなりませんでした。
XPathというのは、XMLにおいて特定の場所を指し示すための言語みたいですね。
すなわち、このライブラリでは、HTMLの中の特定のタグ等をXPathを使って検索し、そこから目的の属性とか内容を取得するといった流れになっているようです。

まずは、HtmlDocumentのインスタンスの生成です。このインスタンスに対していろいろいじくり回すので、まずはこれを作らなければなりません。

HtmlDocument html = new HtmlDocument();

using(WebClient wc = new WebClient())
using(Stream s = wc.OpenRead(URL)) {
    html.Load(s);
}

このライブラリは直接WebからHTMLを取得するということはしないようなので、このように適当にWebClientを使って読みだしてWebからHTMLを取得しています。ローカルの場合は、パスを指定するだけで読み出せるLoadメソッドのオーバーロードがあるのでそれを使えば良いでしょう。

では、実際にXPathで読みだしてみます。

HtmlNode head = html.DocumentNode.SelectSingleNode(@"//head//title");
Console.WriteLine(head.InnerText);

例えば上記のHtmlDocumentクラスの生成のときのURLをhttp://www.google.co.jpにしてこのプログラムを実行すると「Google」という文字列が表示されます。DocumentNodeプロパティはHTMLの中身全体を含むHtmlNodeクラスのインスタンスです。この子ノードを見てみると例えば<html>などを含んでいるのでそのことからもわかると思います。

HtmlNodeクラスは、一つのタグで表されるノードの内容を保持するクラスです。いろいろなプロパティがありますが、主に使うのは
  • Name(このノードの名前)
  • Attributes(このノードの属性のコレクション)
  • ParentNode(親ノード)
  • ChildNodes(子ノードのコレクション)
  • InnerText(そのノードに含まれるテキスト。子ノードを含む場合はタグなどを差し引いたテキスト部分全体)
  • InnerHtml(そのノードに含まれるHTML。子ノードを含む場合はタグ等もすべて含まれたHTMLがstringで格納されている)
あたりですかね。今回のテーマにXPathというのがありますが、それを完全に無視して、ルートノードからこれらのプロパティをLINQで探索して条件に合うノードのコレクションを抽出することはできなくはないですが、LINQのメソッドチェーンがとても長くなったり、ラムダ式が大量に入ったりして可読性が著しく悪くなります。多分。

次は、XPathでの検索のしかたです。XPathで検索するのによく使うメソッドを紹介します。
HtmlNodeクラスのメソッドでSelectNodes()メソッドとSelectSingleNode()メソッドです。これの唯一の引数に、XPathの構文のテキストを与えればおkです。この2つのメソッドの使い分けは簡単で、前者は条件に合うノードのコレクションを返し、後者は条件に合うコレクションの最初のノードを返します。すなわち、イメージとしてはSelectNodes().FirstOrDefault()って感じですね。はい、ここからもわかるように、SelectSingleNodeメソッドは該当するノードが無い場合はnullを返します。では、該当するノードが無いときにSelectNodes()はどうなるのかと言うと、空のコレクションではなくnullを返します。これは個人的にクソ仕様だと思っています。すなわち、SelectNodes().FirstOrDefault()という書き方をして該当するノードが無かった場合はFirstOrDefault()がnullを返すのではなく、null.FirstOrDefault()になるためにNullReferenceExceptionが発生します。これはぜひとも改善してほしいところですね。いや、こういう仕様にした拡張メソッドでも実装して自分でそれを使ってしまうのが一番なのかもしれませんが。


さて、それでは実際にXPathの中身を見てみましょう。上記のXPathは
//head//title
となっています。 文頭の/はルートノードを表します。文中の/は、その子ノードを表します。WindowsやUNIX系とかのディレクトリ階層の表し方そっくりなので、ここは感覚的にも理解しやすいかと思います。そして、//になると、子だけではなく子孫ノード全体を表します。すなわち、子、孫、ひ孫など、いくら階層が下のノードでも条件に合致するようになるわけですね。

Googleのトップページのソースを見てみるとわかると思いますが、htmlノードの子のheadノードの下にmetaノードが何層かあって、そのあとにtitleノードがあります。丁寧にmetaノードを書くのが面倒なので、「htmlノードの子孫ノードのtitleノード」という意味で、このような構文にしてみました。


さて、今回は簡単にtitleノードを抽出するXPathでしたが、ほかにもいろいろとやりたいことは増えてくると思います。例えば、特定の子ノードを持つノードを抽出したいとか、特定の属性を持つノードを抽出したいとか、そういった話があるかと思います。
大丈夫です。そういった構文もXPathにはあります。

HtmlNode div = html.DocumentNode.SelectSingleNode(@"//div[@id=""hplogo""]");
Console.WriteLine(div.Attributes["style"].Value);

これも同じくGoogleのトップページをパースするプログラムです。これを実行すると
height:110px;width:276px;background:url(/images/srpr/logo9w.png) no-repeat
という出力が得られます。
//余談ですが、エスケープを無視するC#の文字列の@""っていうやつ、ダブルクオーテーション2個「""」で「"」を表すんですね。知りませんでした。
こちらのXPathでは鍵かっこ[]が出てきています。これが条件式です。鍵かっこ外に合致するノードのうち、鍵かっこ内の条件に合致するものが返されます。
ちなみに、鍵カッコ内の@ですが、これは属性を示す記号です。すなわち、@idでidという属性という意味ですね。[@id]でidという属性を持つノードという意味になりますが、="hplogo"でさらにその属性の値がhplogoに一致するという意味になります。何も難しくありません。
ちなみに、属性を示す@ですが、正式にはattribute::と書くことになっているようですが、この省略記法として@が使えるという立ち位置のようです。attribute::とかいちいち書いていると冗長なので、このような省略記法は積極的に使っていきたいですね。
というわけで、このXPathは、ルートノード以下のすべてのdivノードのうちidという属性を持ち、その値がhplogoのノードを検索するといった意味になります。次の行では、divの属性のうち、style属性の値を画面に表示しています。特に説明は無くても、C#erなら普通にわかる表現かと思います。

また、検索条件として、「特定の属性を持つ」だけでなく、「特定の子ノードを持つ」という条件を課したくなることも結構あると思います。それは、[child::hoge]と書くのがいわゆる省略しない記法です。省略記法は「何も書かない」なので[hoge]になります。

ちなみに、この条件式の鍵カッコは入れ子にもできますし([hoge[@id="foo"]]など)、複数並べるとandの意味になります([@id="hoge"][@style="foo"]など)。//node1[@id="hoge"]/node2みたいな書き方をして、特定の条件を満たすノードの子ノードなどを示すこともできます。

ここまでくればだいたいの条件式は書けるようになると思います。まだいろいろな条件の書き方はあるようですが、この辺の基本がわかっていればあとはググればなんとかなるでしょう。はい。

というわけで、XPathの導入をやってみました。
ちょっとしたウェブページの情報を読み出すソフトとかを作るときに使えそうな技ですね。