Loading [MathJax]/extensions/MathMenu.js

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などで管理していることがよくあります。そんな時、

1
2
3
4
5
6
List<int> value = new List<int>();
 
public IEnumerable<int> Value
{
    get { return value; }
}

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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>()は変換できない要素は省いたコレクションを返します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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のオーバーロードを使います。例えば、シーケンスの中から条件に一致する要素のインデックスを取得するには、このような書き方で解決できます。

1
2
3
4
5
6
7
8
9
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を使う方法を取っていましたが、とても冗長でパッと見で何をやっているのかがわからないような式になってしまいました。

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

1
2
3
4
5
6
7
8
9
10
11
12
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する拡張メソッドみたいな感じです。

1
2
3
4
5
6
7
8
9
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引数がその値になります。

1
2
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では初期値を指定できるので任意の型にすることが可能です。

1
2
3
4
5
6
7
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の計算が終わった後、その値を変換するデリゲートが付いています。

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

1
2
3
4
5
6
7
8
9
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に係るクラスは単一のスレッドでしか動かないのでしようがありませんでした…。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//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インスタンスを用意し、必要なパラメーターを与えるとそのインスタンスを使って縦書きテキストのサイズを測るメソッドを作りました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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);    //回転するので縦横入れ替える
    }
}


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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をインスタンス化していたのですが、そのコストを抑えるためにこうやって再利用するようにしています。もしもすでに用意しているインスタンスより多くの行が必要なときは新たにインスタンスを作る処理を入れています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <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];
}

こんな感じですね。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// <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とか必要ありません。

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
[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内完結に収めてやろうということにしたわけです。

1
2
3
4
5
6
7
8
<UserControl x:Class="Game2048.Views.GameTable"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d" >
    <Grid Name="grid" />
</UserControl>

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// <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側でデザインしたセルをこちらで使えるようにしています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/// <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次元配列を持ってどうこうやる陳腐なものなのであえて細かく解説はしませんが、ピンポイントで説明するとしたらここでしょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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をよく読んで作ってください。

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

1
2
3
4
5
6
7
8
9
10
<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の場合は、数字の大きさによって色を変化させるようなコンバーターを実装して、それをかませることで数値を色に変換しています。

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

1
2
3
4
5
6
7
<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種類実装してあげています。
また、キーボードの矢印キーを使っても同様の操作ができるべきなので、それも実装してあげます。

1
2
3
<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は任意のセルを読み出すのにははっきり言って全くもって適していません。でも、まあ一応読み出せるので読み出し方を紹介します。

1
2
3
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>を継承したものとして定義されています。
というわけで、正攻法的に読みだしてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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>()型のほうが使い勝手が良いですね。

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

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

1
2
3
4
5
6
7
public class Person
{
    public string 名前 { get; set; }
    public DateTime 誕生日 { get; set; }
    public int 身長 { get; set; }
    public string 血液型 { get; set; }
}

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

1
2
3
4
5
6
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クラスを作ってみます。

1
2
3
4
5
6
7
public class Person
{
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
    public int Height { get; set; }
    public string BloodType { get; set; }
}

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

1
2
3
4
5
6
7
8
9
10
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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を呼び出しています。これは、指定した範囲内をヘッダーの無いデータとして認識するメソッドです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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のクエリ文はあまり書いていません。まあ、どうせみんなわかってるだろうから解説しても仕方がないっていうのもあるとは思いますが。

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

1
2
3
4
5
6
7
8
9
10
11
12
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メソッドは次のように実装しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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を吐くことがあるようです。

1
2
3
4
5
6
7
8
9
10
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つ入っています。

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

1
2
3
4
5
6
7
8
9
10
11
12
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メソッドそのものが吐いた例外なのか、それとも別スレッド内のユーザーが書いた処理に起因する例外なのかを判別することもできません。そういう意味からも、例外をカプセル化してしまったほうが良い、といった判断なのかもしれません。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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のインスタンスの生成です。このインスタンスに対していろいろいじくり回すので、まずはこれを作らなければなりません。

1
2
3
4
5
6
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で読みだしてみます。

1
2
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にはあります。

1
2
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の導入をやってみました。
ちょっとしたウェブページの情報を読み出すソフトとかを作るときに使えそうな技ですね。

2014年9月11日木曜日

2048のロジックを実装する

私はブームを2周くらい遅れて追いかける性質があるようで、ここ最近、2048というゲームにはまっています。ググればたくさん出てきますし、オープンソースってだけあって亜種も結構あるようなのでまあリンクは張らずにおいておきます。

さて、このようにシンプルなゲーム、ある程度やり始めると「これ、自動でやらせればよくね?」って思うようになってきますよね。えっ、ならないだって?:;(∩´﹏`∩);:なってください~。昔、マインスイーパーを自動で解くプログラムを書いたことがありましたが、今回は2048を解いていきたいと思います。

といってもスマホアプリは完全に守備範囲外ですし、別にゲームをやりたいんじゃなければスマホアプリにする必要もありません。
というわけで、お得意のC#で作ろうと思いましたが、オープンソースの2048はJavascriptで作られているようです。というわけで、2048をC#で再実装してしまいました。


まずは、オートソルバーを想定してインターフェースを実装してみましょう。
要するに、2048で必要な操作や、ゲームに際して提供する必要がある操作をインターフェースで提供してやります。そうすることで、別の人が書いた2048のアプリケーションでもそのインターフェースさえ実装していればオートソルバーが使えるようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public interface I2048
{
    /// <summary>
    /// 行の本数
    /// </summary>
    int RowCount { get; }
 
    /// <summary>
    /// 列の本数
    /// </summary>
    int ColumnCount { get; }
 
    /// <summary>
    /// スコア
    /// </summary>
    uint Score { get; }
 
    /// <summary>
    /// ゲームオーバーかどうか
    /// </summary>
    bool IsGameOver { get; }
 
    /// <summary>
    /// row, columnの値
    /// </summary>
    /// <param name="row">行</param>
    /// <param name="column">列</param>
    /// <returns>その値</returns>
    uint this[uint row, uint column] { get; }
 
    /// <summary>
    /// 指定した行を取得するメソッド
    /// </summary>
    /// <param name="row">行番号</param>
    /// <returns>行の中身</returns>
    IEnumerable<uint> GetRow(int row);
 
    /// <summary>
    /// 指定した列を取得するメソッド
    /// </summary>
    /// <param name="column">列番号</param>
    /// <returns>列の中身</returns>
    IEnumerable<uint> GetColumn(int column);
 
 
    /// <summary>
    /// 左に動かす
    /// </summary>
    /// <returns>動かせたかどうか</returns>
    bool MoveLeft();
 
    /// <summary>
    /// 左に動かせるかどうか
    /// </summary>
    bool CanMoveLeft { get; }
 
    /// <summary>
    /// 右に動かす
    /// </summary>
    /// <returns>動かせたかどうか</returns>
    bool MoveRight();
 
    /// <summary>
    /// 右に動かせるかどうか
    /// </summary>
    bool CanMoveRight { get; }
 
    /// <summary>
    /// 上に動かす
    /// </summary>
    /// <returns>動かせたかどうか</returns>
    bool MoveUp();
 
    /// <summary>
    /// 上に動かせるかどうか
    /// </summary>
    bool CanMoveUp { get; }
 
    /// <summary>
    /// 下に動かす
    /// </summary>
    /// <returns>動かせたかどうか</returns>
    bool MoveDown();
 
    /// <summary>
    /// 下に動かせるかどうか
    /// </summary>
    bool CanMoveDown { get; }
}

これくらいの機能さえあれば2048のゲームの状態を取得することも操作することもできますかね。
2048の行数、列数はともに4が一般的ですが、別に4じゃないとこのゲームができないわけではないので(ゲームバランスの問題はあるでしょうけども)任意の列・行に対応できるような仕様にしています。
インデクサーで値が取得できればGetRowとかGetColumnは必須ではないですが、このゲームは行単位や列単位でものを見ることが多いので実装を義務付けても罰は当たらないでしょう。

さて、このインターフェースを実装していきます。
C言語系の言語は変数名等の最初が数字にできないので「2048」とかいうクラスを作るわけにはいきません。なので、Boardっていうクラスにしてみました。

コンストラクタはゲームの初期化を行っています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Board(int row, int column)
{
    if(row < 2 || column < 2)
        throw new ArgumentOutOfRangeException("The row and the column count must be bigger than or equals 2");
 
    RowCount = row;
    ColumnCount = column;
 
    table = new uint[row, column];
 
    PossibilityOfTwo = 0.75;
 
    MakeNumberAppear();
    MakeNumberAppear();
}


1マスだけではゲームができないので、2x2以上を義務付けています。正方形である必要はなく、例えば5x3などの長方形でもOKです。
PossibilityOfTwoは新しい数字が湧き出してくるときに2が出る確率です。残りは4が出てきます。とりあえず確率75%にしてみました。
MakeNumberAppearは新しい数字を湧き出させるメソッドです。ゲームスタート時点では2ヶ所に数字を表示しますから、2回呼び出しています。

ここでMakeNumberAppearの中身を見てみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// 新しい数字を沸き上がらせるメソッド
/// </summary>
private void MakeNumberAppear()
{
    var zero = Enumerable.Range(0, RowCount).
        SelectMany(row => GetRow(row).Select((val, col) => new { Row = row, Col = col, Val = val })).
        Where(p => p.Val == 0).
        ToArray();
 
    if(zero.Length == 0)
        throw new InvalidOperationException("There are no free space.");
 
    uint cellNumber = (uint)(random.Next() % zero.Length);
 
    table[zero[cellNumber].Row, zero[cellNumber].Col] = (random.NextDouble() < PossibilityOfTwo) ? 2u : 4u;
}

まずは、ゼロのセルを列挙しています。
とりあえず全てのセルを列挙し、匿名クラスでその行番号、列番号とそのセルの値を表すクラスを作り、最後にWhereでセルの値がゼロのもののみを拾ってきています。
そして、そして乱数でそのゼロのセルのなかから1つを選び、さらにそこから指定した確率で2か4を入れています。


指定した行をまるごと引っ張ってくるメソッドGetRowですが、実はとても簡単に実装できます。

1
2
3
4
5
6
7
8
9
10
/// <summary>
/// 指定した行を取得するメソッド
/// </summary>
/// <param name="row">行番号</param>
/// <returns>行の中身</returns>
public IEnumerable<uint> GetRow(int row)
{
    for(int i = 0; i < ColumnCount; i++)
        yield return table[row, i];
}

IEnumerableの概念を最初に勉強した時は「使う分には便利だけど実装はクッソめんどくさいだろうなー」って思いましたが、yield文によって上記のように超簡単に実装できるんですよね。なんていうか、for文から抜けてないかのような表記なのに抜けたような挙動の構文ってことでフローがわかりにくいようにも思えますが、この構文のシンプルさは私は大好きですね。

最後に、上下左右に動かすプログラムを実装しましょう。これは案外複雑です。
  1. 動かした方向に数が入ってるすべてのセルを詰める
  2. 動かした先のほうから反対方向へ順に見て行って、その反対方向側の隣のセルに同じ値があった場合、合体する。合体した場合は、合体した次のセルから隣のセルを見ていき、合体しなかった場合は次のセルを見ていく。
  3. ゲームスコアは、合体した後の値が加算される。
1はそこまで難しくなく実装できそうです。しかし、2が面倒くさいですね。普通に「隣のセルと同じだったら合体する」みたいなロジックで組んでしまうと、例えば4 2 2って並んでいた時に右に動かしたら、1回目で2 2が合体して4 4になり、さらにここで並んでしまうのでこれが合体して8になってしまいます。そのような現象を回避するために、動かした先のほうから反対方向へ順番に見ていく必要があります。結構複雑な感じになりそうです。

あ、ここで2の動作を「縮退」と名付けました。なんか2つのセルが合体して1つのセルに縮まる感じです。量子力学とかサーバー運用等に関して言う縮退の意味を考えるとどうなのかなーって気もしなくもないですが、なんていうか、ほかに良い日本語が見つからなかったので。

さて、ここで左に動かすメソッドを見ていきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <summary>
/// 左に動かす
/// </summary>
/// <returns>動かせたかどうか</returns>
public bool MoveLeft()
{
    if(CanMoveLeft) {
        for(int i = 0; i < RowCount; i++) {
            uint score;
 
            SetRow(i, GetRow(i).Where(p => p > 0).Degenerate(out score).AddZero(ColumnCount));
 
            Score += score;
        }
        MakeNumberAppear();
    }
    return CanMoveLeft;
}

あらかじめ動かせるかを調べてから、動かせた場合に各行について処理をしています。行を取得し、ゼロのセル、すなわち何も数字が無いセルをWhereで省きます。その後、縮退のDegenerateメソッドを呼び、最後に列数になるまでゼロのセルを後ろに付けたします。これだけです。DegenerateとAddZeroは私が実装した拡張メソッドです。中身は後述します。

このDegenerateメソッドのアルゴリズム、動かせなかったら動かさないまま値を返すので一見あらかじめ動かせるかどうかを調べてから動かす必要はなさそうです。ではなぜこれが必要かというと、2か4を湧き上がらせる条件として「動かした」ということが必要だからです。動かせる時のみ動かし、新しい数字を湧き上がらせるという手順を踏んでいます。

さて、上記の例は左へ動かす場合ですが、同様に上へ動かす場合はGetRow(i)の代わりにGetColumn(i)を呼び出してあげれば問題ないことは容易く想像がつくでしょう。もちろん、AddZeroの引数とかには注意してあげる必要がありますが。
じゃあ、右や下へ動かす場合はどうするか。
簡単です。Whereの後とAddZeroの後にReverse()を入れてあげるだけです。

1
GetRow(i).Where(p => p > 0).Reverse().Degenerate(out score).AddZero(ColumnCount).Reverse();

行の中身の順番を入れ替えて縮退させれば現象は左右逆になりますよね。下へ動かす場合も同様です。これでだいぶシンプルに各方向へ動かすメソッドが実装できました。

ここで、Degenerate()とAddZero()の中身を見てみましょう。まずはDegenerateから。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// 行または列をくっつけて成長させるメソッド
/// </summary>
/// <param name="array">成長前の配列</param>
/// <param name="score">点数</param>
/// <returns>成長後の配列</returns>
public static IEnumerable<uint> Degenerate(this IEnumerable<uint> array, out uint score)
{
    score = 0;
    if(array.Count() <= 1)    //要素が1個以下ならそのまま
        return array;
    else if(array.First() == array.Skip(1).First()) {    //次と同じだったら足して、残りをDegenerateする
        uint add = array.First();
        IEnumerable<uint> ret = array.Take(1).Select(p => p * 2).Concat(array.Skip(2).Degenerate(out score));
        score += add;
        return ret;
    } else    //次と同じじゃなかったら、その次のからDegenerateする
        return array.Take(1).Concat(array.Skip(1).Degenerate(out score));
}

3パターンにわかれます。
まず、要素が1個以下ならば縮退は起こりえないのでそのまま返します。
次に、最初の要素と2つ目の要素が同じだったら1つ目の要素をarray.Take(1).Select(p => p * 2)で2倍にしています。さらに、この配列を2つスキップした先の要素をDegenerateで再帰呼び出しします。そして、2倍にした側にConcatで連結しています。このとき、点数計算も同時にしています。
最後のパターンは1つ目と2つ目が同じじゃなかった時、2つ目の要素以降を再帰呼び出しで縮退させています。
これで全ての動作が網羅できますよね。我ながらなかなかシンプルにまとめあげたと思っています

つづいてAddZeroです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <summary>
/// 指定した個数の要素数になるまで末尾にゼロをつなげるメソッド
/// </summary>
/// <typeparam name="T">型</typeparam>
/// <param name="array">元の配列</param>
/// <param name="TotalLength">最終的な長さ</param>
/// <returns>ゼロをつなげた配列</returns>
public static IEnumerable<T> AddZero<T>(this IEnumerable<T> array, int TotalLength)
{
    int count = array.Count();
 
    if(count == TotalLength)
        return array;
    else if(TotalLength < count)
        throw new InvalidOperationException("TotalLength must be bigger than or equals to array length");
    else
        return array.Concat(Enumerable.Range(1, TotalLength - count).Select(p => default(T)));
}

指定した個数より元の配列のほうが長かったら例外を吐くようにしています。
Enumerable.Rangeで追加分の長さの連続した数列を作りますが、これは必要ないのでSelectで使わないで捨てています。ジェネリックメソッドにしているので、default(T)になりますね。


最後に、動かせるかどうかを示すプロパティについて見てみます。
とはいっても、動かした後のデータと動かす前のデータをSequenceEqualで比べてるだけですがね。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// <summary>
/// 左に動かせるかどうか
/// </summary>
public bool CanMoveLeft
{
    get
    {
        for(int i = 0; i < RowCount; i++) {
            if(!GetRow(i).Where(p => p > 0).Degenerate().AddZero(ColumnCount).SequenceEqual(GetRow(i)))
                return true;
        }
        return false;
    }
}

先述した通り、このプロパティで動かせるかを判断した後、改めて実際に動かす動作をしています。2度手間であまり賢い実装じゃないかもしれませんが、まあ、まとまりはいいのでこれでいいでしょう。

これに付随して、ゲームオーバーを表すプロパティ「IsGameOver」も実装しました。ゲームオーバーしたかどうかはすなわち全方向に動かせないということですので、そうなるように実装しました。

1
2
3
4
5
6
7
8
9
10
/// <summary>
/// ゲームオーバーかどうか
/// </summary>
public bool IsGameOver
{
    get
    {
        return !CanMoveLeft && !CanMoveRight && !CanMoveUp && !CanMoveDown;
    }
}


さて、ここまで見てもらってわかると思いますが、C#でデータの集まりを操作しようとするとガチガチのLINQ実装になりました。LINQは遅延評価なので、何度も同じIEnumerable<T>を評価するような式は実効効率の意味ではあまり賢くないかもしれませんが(1度配列などに落としておくと早くなると見込める)、まあ、今の段階では特に考えなくていいでしょう。全部LINQで書いてしまったほうが綺麗ですし。


さて、これでだいたいのロジックが出そろいました。
今回のテーマが「ロジックの実装」で、最終目標が「2048のオートソルバー」なのでUIは今のところ全然本気出して実装していませんが、とりあえず動作を確認するために簡単に実装しました。
こんな感じになっています。


次回はUIの整理かな~?

2014年9月8日月曜日

LINQ to Twitter v3で遊ぶ

今回はLINQ to Twitterのネタです。

C#でTwitterやるんだったらLINQ to Twitterでしょう。TwitterのAPI変更にもしつこくついていってくれて、ユーザーとしてはとても助かります。

久しぶりにLINQ to Twitterをいじったらバージョンが2から3に上がってました。ほうほう。と思って、安易にアップデートしてみたらいろいろと破壊的仕様変更がされていて焦りました。 いろいろといじってみるとバージョン3になってから.NET4.5の非同期処理に対応したようでした。Asyncメソッドの嵐です。まあ、Windowsストアアプリとかに使われることも想定しているライブラリですから、当然といえば当然の流れですよね。

ということで、使い方を一通りまとめてみます。

認証


Twitterのアプリケーションを作成するにはConsumerKeyとConsumerSecretが必要なことは多くの人がご存知かと思います。https://apps.twitter.com/からアプリケーションを登録してこれらのキーを手に入れます。

さて、認証に関して、2通りの方法を紹介します。

すでにAccessToken、AccessTokenSecretを知っているとき

アプリケーションに対応するキーの他、そのアプリケーションでユーザーを認証するために使うキーが2つあります。これらのキーを取得する必要がありますが、これを別経路で知っている場合はこれを直接入力して使うことができます(最近の上記のTwitter開発者向けサイトではアプリケーションを登録した本人のそのアプリケーション用のAccessToken、AccessTokenSecretを教えてくれるようです)。もちろん、その場合は他人にそのアプリケーションを渡せなくなる(渡したら自分のアカウントで好き放題されてしまう)ので注意してください。

1
2
3
4
5
6
7
8
SingleUserAuthorizer auth = new SingleUserAuthorizer() {
    CredentialStore = new SingleUserInMemoryCredentialStore() {
        ConsumerKey = "****",
        ConsumerSecret = "****",
        AccessToken = "****",
        AccessTokenSecret = "****"
    }
};

このように書けばおkです。超簡単です。

PINコードで認証をするとき

さて、では実際に自分のアプリケーションを他人に配布できるようなものにするとき、上記のような方法では不十分ですね。PIN認証する必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PinAuthorizer auth = new PinAuthorizer() {
    CredentialStore = new InMemoryCredentialStore() {
        ConsumerKey = "****",
        ConsumerSecret = "****",
    }
};
 
auth.SupportsCompression = false;
auth.GoToTwitterAuthorization = (link) => {
    Process.Start(link);
    Console.WriteLine(link);
};
auth.GetPin = () => {
    Console.Write("Input PIN :");
    return Console.ReadLine();
};
auth.AuthorizeAsync().Wait();

ConsumerKeyとConsumerSecretを設定し、認証用リンクを受け取り適切に転送するデリゲート、入力されたPINコードを返すデリゲートを設定してやります。そして、AuthorizeAsync関数を呼び出せば非同期で認証を行ってくれます。非同期ですので、これを同期的に処理するためにWaitメソッドを呼び出して終了を待っています。こうすると、linkのところでブラウザを起動して、ReadLineでPINの入力を要求して入力が終わったら認証が完了します。もしも誤ったPINが入力されたらWaitメソッドが例外を吐くようなので、適切に処理してやれば良いでしょう。

各種データの取得

タイムラインを取得する

つづいてタイムラインを取得します。この辺りからLINQ to Twitterの威力が発揮されます。

1
2
3
4
5
6
7
8
9
var q = from p in context.Status
        where p.Type == StatusType.Home && p.Count == 100
        orderby p.CreatedAt
        select p;
 
foreach(var s in q) {
    ShowStatus(s);
    Console.WriteLine();
}

LINQがわかってる人なら超簡単に取得できていることがわかると思います。個人的に、取得する個数の指定で「p.Count == 100」とかいうのはちょっと不満なんですが…。だって、個々の要素についてのことじゃないじゃんってね。

特定のツイートを取得する

今度は特定のIDのツイートを取得します。InReplyToからリプライ元を取得する時などに役に立ちます。

1
2
3
4
5
6
var q = from p in context.Status
        where p.Type == StatusType.Show && p.ID == 508907673895436288
        select p;
 
ShowStatus(q.Single());
Console.WriteLine();

とってもシンプルですね。ただ、単一のツイートってわかりきってるのに列挙可能型になるのは冗長な気はしますが。ちなみに、バージョン2まではp.IDはstring型でしたが、バージョン3ではulong型になってました。個人的には良改変だと思ってます。

ユーザー情報を取得する

今度は任意のユーザーのアカウント情報を取得してみましょう。

1
2
3
4
5
6
7
8
var q = from p in context.User
        where p.Type == UserType.Show && p.ScreenName == "EH500_Kintarou"
        select p;
User u = q.Single();
 
Console.WriteLine("UserID = " + u.UserIDResponse);
Console.WriteLine("UserScreenName = " + u.ScreenNameResponse);
Console.WriteLine();

はい、簡単です。
UserクラスのScreenNameはどうもこの検索時にユーザー名を設定するためのプロパティのようで、サーバーからの応答はScreenNameResponseに入るみたいですね。この辺はかなり直感的じゃなくて使いにくいところですが、改良はなかなかされてないですね…。

自分のアカウント情報を取得する

次は自分のアカウント情報を見てみます。特に、PINコードで認証した時なんかは自分のアカウント情報は自明ではありません。調べる必要があります。

1
2
3
4
5
6
7
8
var q = from p in context.Account
        where p.Type == AccountType.VerifyCredentials
        select p;
Account a = q.Single();
 
Console.WriteLine("UserID = " + a.User.UserIDResponse);
Console.WriteLine("UserScreenName = " + a.User.ScreenNameResponse);
Console.WriteLine();

context.Accountを使ってる点はユーザー情報の取得とは違いますが、あとは概ね同じです。
ていうか、この辺全部返ってくる情報は1つってわかってるのに列挙可能型(ry はい、Singleで決め打ちしちゃってます。

UserStreamを受信する

Twitterにはリアルタイムでタイムラインなどを取得する機能があります。さらに、API1.1の仕様変更のタイミングでタイムラインの取得を頻繁にやりすぎるとすぐにAPIが切れてしまう使用になりました。UserStreamは必須と言えるでしょう。

1
2
3
4
5
6
7
8
9
(from p in context.Streaming where p.Type == StreamingType.User select p).StartAsync(async s => {
    await Task.Run(() => {
        Status status = new Status(LitJson.JsonMapper.ToObject(s.Content));
        if(!string.IsNullOrEmpty(status.Text)) {
            ShowStatus(status);
            Console.WriteLine();
        }
    });
});

なんていうか、もはやUserStreamまでにLINQを使う必要なんてあるのかって気すらしてきてしまいますが、その辺はLINQのプライド?なんでしょうね。
若干の設定をしてStartAsyncを呼び出すと、その中のデリゲートにUserStreamを受信するたびにデータが飛んできます。そのデリゲートがasyncなので、中でawaitメソッドの呼び出しをするためにあえてTask.Runを挟んでいます。
ちなみに、受信するデータはStatusだけではないので、あえてシリアライズされたままのJsonの情報で飛んできますので、そのままではツイートは見られません。なので、 new Status(LitJson.JsonMapper.ToObject(s.Content))でStatusクラスを作ってあげています。ただ、これでは例えばフォローされたとかいったような情報が拾えなくなります。ツイートだけを選り分けるためにTextプロパティが空じゃないかを見ていますが、もしもフォローされたとかの情報をちゃんと取得したければ、Jsonの時点でパースをしてあげる必要があるでしょう。
LINQ to TwitterとしてはUserStreamsParser for LinqToTwitterというサードパーティ製のライブラリの使用を推奨しているっぽいれすが、これを昔導入してみたところ、LINQ to Twitterの仕様変更かなんかで上手く動いてくれなかったと記憶しています。このライブラリもアプデされてないっぽいので、まあ、動かないのでしょう。

ツイートする

普通にツイートをする

ツイートも非同期メソッドになっています。

1
context.TweetAsync("なんていうか、LINQ to Twitter v3で.Wait()の嵐になってる(((").Wait();

非同期メソッドを同期的に使うならば、.Wait()を付ける必要がありますね。

リプライをする

さて、問題はリプライです。というのも、バージョン2ではツイートするためのメソッドであるUpdateStatusのオーバーロードとしてリプライが実装されていました。しかし、同様のオーバーライドがTweetAsyncには見当たりません。
と思って見てみたら、TwitterContextにReplyAsyncという別のメソッドが用意されていました。

1
context.ReplyAsync(508969959364362242, "非同期メソッドばっかりだからな。").Wait();

これは自分自身へのリプライなので@を本文につける必要が無いパターンですね。

位置情報をツイートをする

位置情報はオーバーロードとして実装されています。

1
context.TweetAsync("皇居なう(´へωへ`*)", 35.6855323M, 139.7527346M).Wait();

緯度経度はdecimal型なのでMまたはmのサフィックスを付ける必要があります。

画像をツイートする

画像はTweetWithMediaAsyncというメソッドになります。

1
context.TweetWithMediaAsync("コンソールTL", false, System.IO.File.ReadAllBytes(@"ss001.png")).Wait();

そのまま画像ファイルのバイト列を突っ込めばいいっぽいです。

ツイートを表示する

最後にツイートの表示です。
先ほどのユーザー情報の表示でUserクラスのScreenNameプロパティはデータを読みだした時には使えないなどといった若干のクセがありますが、Statusクラスにも同様にクセがあります。というよりか、C#ユーザーに取って自然な形を目指すよりTwitter APIに忠実にクラスを設計したといった感じなんですかね。

1
2
3
4
5
6
7
8
9
10
11
static void ShowStatus(Status s)
{
    if(s.RetweetedStatus != null && !string.IsNullOrEmpty(s.RetweetedStatus.Text)) {
        ShowStatus(s.RetweetedStatus);
        Console.WriteLine("Retweeted by @{0}", s.User.ScreenNameResponse);
    } else {
        Console.WriteLine("@{0}/{1}", s.User.ScreenNameResponse, s.User.Name);
        Console.WriteLine(s.Text.Replace("&lt;", "<").Replace("&gt;", ">").Replace("&amp;", "&"));
        Console.WriteLine("via {0} at {1}", new Regex(@"\>(?<source>.*)\<").Match(s.Source).Groups["source"].Value, s.CreatedAt);
    }
}

まず、リツイートですが、bool型のRetweetedっていういかにもリツイートされたツイートかどうかを示すプロパティっぽいのがありますが、これでリツイートされたツイートかどうかは見分けられないようです結局、RetweetedStatusに中身があるかどうかを見て判断しています。
ツイートの本文は、Jsonのために<>&などといった記号がエスケープされた状態でそのまま格納されています。なので、これらはReplaceを用いて展開をしてやる必要があります。もしかしたらこれ以外にもエスケープされてる文字があるかもしれません。
最後に、viaを表すSourceプロパティですが、これはそのリンクを含めたHTMLのAタグがまるごと入っています。なので、今回は手抜きで>と<に囲まれた領域を正規表現で抜き出してあげています。ちゃんとやるならタグをパースしてあげたりすると良いでしょう。

結構面倒ですね。



とまあ、概ねこんな感じでしょうか…。 まだ検索とか隅々まで試したわけじゃないですが、いろいろ使えそうです。

今回は非同期メソッドを活かしたコードは書いていませんが、また機会があったら書いてみたいですねえ。
Windowsストアアプリ、XAML/C#で作れるけど、それ以外の敷居が高すぎるからなあ…。

2014年8月26日火曜日

PICkit3のProgrammer-To-Goを試す

さて、前回の記事から結構時間が開いてしまいました。
なにせただの趣味プログラマーですので、別のことに忙しかったり、もしくは何かを作ろうといった発想がないとなかなかプログラミングに手がつかないもの仕方ないことですね。特に何かの言語の基礎といったものを修得するような段階なわけでもないですし。

今回は、ちょっとプログラミングっぽくないかもしれませんが、PICkit3のProgrammer-To-Goを試してみようかと思います。

Programmer-To-Goって一体何?

PICkit3は、実はパソコンが無くてもスタンドアロンでPICの書き込みができてしまうのです。その機能がProgrammer-To-Goです(PICkit2にもProgrammer-To-Goはあります)。
とは言っても、まさかPICkit3の上でソフトウェアを開発するわけにもいきません。何をやってるのかというと、PICkit3が内蔵しているEEPROMにMPLABから転送したhexファイルを保持し、適当な外部電源とPICkit3にあるスイッチでPICにプログラムを転送できちゃうよって話です。

で、なぜこれを使おうと思ったか。
私は例のNTP時計を2つ作って動かしているのですが、1つは開発用PCから遠いところに固定してしまっていて、プログラムの更新をするのが非常に面倒なんです。NTP時計を外して持ってきたり、デスクトップパソコンをNTP時計の場所に持って行ったり、もしくはノートPCにPICkit3周りの環境をセットアップしてその場に持っていったりするのはとても面倒です。そこで、Programmer-To-Goなわけです。

それでは、Programmer-To-Goの準備をしていきましょう。

まずは、MPLAB Xでプロジェクトのプロパティを開き、CategoriesのPICkit3を選択して、Option categoriesからProgrammer-To-Goを選択します。



Image Nameの項目にこのProgrammer-To-Goのイメージの名前を入れてあげます。特にこれが完成したプログラムに影響を与えるといったことはない(と私は認識しています)です。単に、今PICkit3に入ってるプログラムが何なのかを識別できるようにしたいってだけなはずです。


次に、ツールバーの書き込みボタンの三角印を押してドロップダウンメニューを表示させます。この中にProgrammer To Go PICkit3 Main Projectという項目があるので、これを押すとhexファイルがPICkit3に転送されます。もちろん、この段階でPICkit3にPICを接続している必要はありません。

ここで、すでにPICkit3の中にProgrammer-To-Go用のプログラムが書き込まれていた場合、このような警告が出てきます。


読めばわかることですが、「PICkit3はすでにProgrammer-To-Goモードで"NTPCLOCK"っていうイメージが入っています。このデータを保持しますか?(つまり、PICkit3内のイメージを消去しないってことです)」と言っていますね。わざわざ"つまり"でわかりやすく言い換えてくれています。ここで、先ほど設定したイメージ名が役に立つわけです。

プログラムを更新したいので、PICkit3の中のメモリは消去する必要があります。なので、Noを押せばいいですね。

そうすると、出力一覧のPICkit3のところに
Connecting to MPLAB PICkit 3...
PICkit 3 is not in programmer-to-go any more.
Firmware Suite Version.....01.30.09
Firmware type..............dsPIC33F/24F/24H


The following memory area(s) will be programmed:
program memory: start address = 0x0, end address = 0x73ff
configuration memory

Programming...
Programming/Verify complete

PICkit 3 is now in Programmer to go mode. The next time you connect to this unit, you will have the choice to take it out of Programmer to go mode.
このようなメッセージが出てきます。
無事、プログラムの書き込みとベリファイが終了し、Programmer-To-Goモードになったということを示しています。
一方、PICkit3のほうを見るとACTIVEランプが点滅し、STATUSランプが緑色に点灯しています。これが成功の証です。

この後、PICkit3はパソコンから取り外し、モバイルバッテリー等の適当なUSB電源に接続してやります。
そうすると、全ランプが10秒くらい点灯した後に、STATUSランプが消え、ACTIVEランプが点滅し始めます。これが書き込みの準備ができたことを示す表示です。
そうしたら、PICkit3をPICに接続し、タクトスイッチを押すだけです。書き込み中はSTATUSランプがオレンジ色になります。書き込みが終了すると書き込まれたPICは動作をはじめ、PICkit3はACTIVEランプの点灯とSTATUSランプの緑色の点灯をして成功を示します。

これでバッチリですね。パソコンから遠いPICの書き込みもできました。

なお、もしも書き込みにエラーが発生するとACTIVEランプが消え、STATUSランプが特定のパターンで点滅をします。そのパターンからエラーを特定することができるそうですが、そのあたりはこのマニュアルにその説明を譲りします。


このProgrammer-To-Goという機能は実は昔から知っていましたが、使いどころがないだろうと思っていました。たいてい、組み込み機器の製作なんてトライアンドエラーですし、そういった観点からパソコンのすぐそこにそのデバイスを置いて作業しますもんね。
本来は、大量生産などで同じプログラムをたくさんのPICに書き込んだりするときに、そのプログラムを書き込んだPICkit3を手元に置き、流れ作業でどんどん書き込んでいくなどといった用途を想定しているようです。しかし、個人の趣味で大量生産などをするわけもなく、そのような用途では使わなかったわけです。

ですが、「パソコンから遠いところに設置した装置のプログラムを書き換える」ということで、ここでまた1つ、PICkit3の機能を使いこなすことができました。こういう使い方もできるんだなって。

2014年8月2日土曜日

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

さて、前回につづいて縦書き用テキストブロックTategakiの記事です。

[バグフィックス]
  • 比較的短い長さのテキストを表示させようとするとバグる件を修正
  • フォントファミリ名やサイズなどをXAMLで指定しないと表示されないバグを修正
[機能追加]
  • 太字、斜体に対応
  • 複数行(自動折り返し)の縦書きコントロール"TategakiMultiline"を実装
こんな感じですかね。いろいろ問題点があったりバグがまだ眠っていそうだったりしますが、とりあえず公開します。(最初からベータ版とかにしとけばよかったかな…)

まず最初の、比較的短い長さのテキストを入れるとバグる件ですが、これはどうも短いテキストだとグリフインデックスが取得できないようですね…。なぜかよくわかりませんが、Uniscribe側の問題っぽいです。なので、非常にアホらしい回避方法ではありますが、あらかじめ10文字ほど足してからグリフインデックスを取得し、その足した分のグリフインデックスを無視するということをやっています。

1
ushort[] glyphs = Uniscribe.GetGlyphs("ああああああああああ" + Text, FontFamily.Source, (int)FontSize).Skip(10).ToArray();

そしてフォントの設定をしないと正常に表示されなかった問題は、コントロールのコンストラクタでシステムフォントを適当に代入してあげています。

1
2
this.FontSize = SystemFonts.MessageFontSize;
this.FontFamily = SystemFonts.MessageFontFamily;

そして、太字、斜体の対応はこれでおkです。

1
2
3
4
5
6
7
8
9
10
11
12
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
    switch(e.Property.Name) {
        case "FontWeight":
        case "FontStyle":
            this.glyphs1.StyleSimulations =
                ((FontWeight != FontWeights.Normal) ? StyleSimulations.BoldSimulation : StyleSimulations.None) |
                ((FontStyle != FontStyles.Normal) ? StyleSimulations.ItalicSimulation : StyleSimulations.None);
            break;
    }
    base.OnPropertyChanged(e);
}


さて、今回のメインディッシュの複数行対応縦書きコントロールです。コントロールのXAMLはStackPanelを1つ定義するのみで、コードビハインドのほうで1行の縦書きコントロールを複数生成してそのStackPanelに並べてあげているだけです。ですが、これをコントロールのサイズに合わせて適切に文章を区切り、分散させてやらねばならず、これがとても面倒くさいのです。
Win32APIならGetTextExtentPoint32関数などを使えば文字列の幅が取得できますが、グリフに関してはそのようなものが(多分)無いので、実際にコントロールを作ってその大きさを測定することで文字列の幅を取得しています。1文字ずつ増やしては長さを測って折り返すべき長さに達したらそこで文字列を切り離して~なんてやると文字数分だけ時間が掛かってしまいますが、最初に全体の長さを取得して文字数で割ることで1文字当たりの幅を計算し、おおよそ目標の長さにいきなり合うようにして、後はそこから実際にコントロールの長さを測って調整するという処理をすれば行数に比例する程度の時間で済みます。しかし、それでも結構この処理は重く、このコントロールをリサイズしたりすると結構カクカクします。そういう意味で改善の余地がありそうですが、なんか根本的な方法が無いともうどうしようもないんですよね…。

というわけで結構複雑な処理をしているのでコードがごちゃごちゃしてわかりにくくなっていますが、その部分のコードを貼ります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
void RedrawText()
{
    double height = ParentHeight;
 
    if((beforeHeight == null) || (beforeHeight.Value != height) || !object.Equals(Text, beforeText)) {    //これをやらないと無限ループに陥る
        stackPane1.Children.Clear();
 
        if(!string.IsNullOrEmpty(Text)) {
            string[] Lines = Text.Split('\n');
            Size infinitySize = new Size(double.PositiveInfinity, double.PositiveInfinity);
            Thickness margin = new Thickness(0, 0, LineMargin, 0);
 
            if((height <= 0) || (Lines.Length == 0)) {
                stackPane1.Children.Add(new TategakiText() { Text = Text, FontFamily = FontFamily, FontSize = FontSize, Spacing = Spacing, Margin = margin });
            } else {
                foreach(string line in Lines) {
                    string text = line;
 
                    if(line.Length == 0) {
                        TategakiText tategaki = new TategakiText() { Text = string.Empty, FontFamily = FontFamily, FontSize = FontSize, Spacing = Spacing, Margin = margin };
                        stackPane1.Children.Insert(0, tategaki);
                    } else {
                        while(text.Length > 1) {
                            TategakiText tategaki = new TategakiText() { Text = text, FontFamily = FontFamily, FontSize = FontSize, Spacing = Spacing, Margin = margin };
 
                            tategaki.UpdateLayout();
                            tategaki.Measure(infinitySize);
                            double charHeight = tategaki.DesiredSize.Height / text.Length;    //1文字の平均長(縦書きはだいたい等幅)
 
                            int i;
                            if(charHeight == 0)    //ゼロ除算回避
                                i = text.Length;
                            else {
                                for(i = (int)(height / charHeight) + 1; i < text.Length; i++) {    //平均長から1行のおよその文字長を割り出す
                                    tategaki.Text = text.Substring(0, i);
                                    tategaki.UpdateLayout();
                                    tategaki.Measure(infinitySize);
                                    if(tategaki.DesiredSize.Height > height)    //長さが超えていたらブレーク
                                        break;
                                }
                                i = Math.Max(Math.Min(i - 1, text.Length), 1);    //長さが超えたらその1つ小さくなったものから調べればよく、また、最初に決め打った長さがtext.Lengthを超えてる可能性があるのでそれを合わせ込むが、1より小さくはしない。
                            }
                            for(; i > 0; i--) {
                                tategaki.Text = text.Substring(0, i);
                                tategaki.UpdateLayout();
                                tategaki.Measure(infinitySize);
                                if(tategaki.DesiredSize.Height <= height)    //減らしていって長さが切ったらブレーク
                                    break;
                            }
                            stackPane1.Children.Insert(0, tategaki);
 
                            text = text.Substring(Math.Max(i, 1));    //iが0になってきたらそれは1文字
                        }
                        if(text.Length == 1) {    //文字列が1文字だったら強制的に書きだす
                            TategakiText tategaki = new TategakiText() { Text = text, FontFamily = FontFamily, FontSize = FontSize, Spacing = Spacing };
                            stackPane1.Children.Insert(0, tategaki);
                            text = string.Empty;
                        }
                    }
                }
            }
        }
        beforeText = Text;
        beforeHeight = height;
    }
}

ちなみに、コントロールのサイズを測るには、まずUpdateLayoutメソッドを呼び、そのあとにMeasureレイアウトを呼ぶことで、DesiredSizeが更新されます。これが、要するにその1行の縦書きを表示するのに要求されるサイズですね。こうやって測っています。なんかコントロールを一旦インスタンス化して測定してるあたり、結構時間の掛かりそうな処理ですよね…。

何かいい方法知ってる人がいたらぜひ教えてください。

ちなみに、この辺苦労しただけあって(?)割と簡単に複数行の書き出しはできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="100" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" Name="TategakiRow" />
    </Grid.RowDefinitions>
 
    <TextBox Grid.Row="0" Name="textBox1" HorizontalScrollBarVisibility="Auto"  VerticalScrollBarVisibility="Visible" AcceptsReturn="True"
             Text="{Binding ElementName=tategakiMul1, Path=Text, UpdateSourceTrigger=PropertyChanged}" />
    <Slider Grid.Row="1" Name="slider1" Minimum="0" Maximum="30" Value="10"/>
    <StackPanel Grid.Row="2" Orientation="Horizontal" FlowDirection="RightToLeft" Margin="0,0,0,10">
        <tg:TategakiText Text="羅生門"
                        FontFamily="メイリオ" FontSize="36" Spacing="200" FontWeight="Bold"
                        HorizontalAlignment="Center" VerticalAlignment="Center"
                        RenderTransformOrigin="0.5,0.5">
            <tg:TategakiText.RenderTransform>
                <ScaleTransform ScaleX="-1" />
            </tg:TategakiText.RenderTransform>
        </tg:TategakiText>
        <tg:TategakiText Text="芥川龍之介"
                        FontFamily="メイリオ" FontSize="20" Spacing="100"
                        HorizontalAlignment="Center" VerticalAlignment="Bottom"
                        RenderTransformOrigin="0.5,0.5">
            <tg:TategakiText.RenderTransform>
                <ScaleTransform ScaleX="-1" />
            </tg:TategakiText.RenderTransform>
        </tg:TategakiText>
        <tg:TategakiMultiline Text="すると、老婆は、見開いていた眼を、一層大きくして、じっとその下人の顔を見守った。まぶたの赤くなった、肉食鳥のような、鋭い眼で見たのである。それから、皺で、ほとんど、鼻と一つになった唇を、何か物でも噛んでいるように動かした。細い喉で、尖った喉仏(のどぼとけ)の動いているのが見える。その時、その喉から、鴉(からす)の啼くような声が、喘(あえ)ぎ喘ぎ、下人の耳へ伝わって来た。&#xa;「この髪を抜いてな、この髪を抜いてな、鬘(かずら)にしようと思うたのじゃ。」&#xa; 下人は、老婆の答が存外、平凡なのに失望した。そうして失望すると同時に、また前の憎悪が、冷やかな侮蔑(ぶべつ)と一しょに、心の中へはいって来た。すると、その気色(けしき)が、先方へも通じたのであろう。老婆は、片手に、まだ死骸の頭から奪った長い抜け毛を持ったなり、蟇(ひき)のつぶやくような声で、口ごもりながら、こんな事を云った。"
                            FontFamily="MS 明朝" FontSize="18" Spacing="100" LineMargin="{Binding ElementName=slider1, Path=Value}" FontStyle="Normal" FontWeight="Normal" Foreground="Black"
                            RenderTransformOrigin="0.5,0.5" Name="tategakiMul1">
            <tg:TategakiMultiline.RenderTransform>
                <ScaleTransform ScaleX="-1" />
            </tg:TategakiMultiline.RenderTransform>
        </tg:TategakiMultiline>
    </StackPanel>
</Grid>

XAMLの中のテキストで改行をするには、\nではなく&#xa;を使えば良いようです。改行コードそのままです。例によってStackPanelは右から左に並べるとコントロールの中身が左右反転してしまうので(おそらくアラビア語とかのためのコントロール)、あらかじめコントロールをRenderTransformで左右反転してからStackPanelに与えています。

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

2014年7月22日火曜日

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

(2024/05/11追記)
Google等からこのページに直接来られる方が多いようですが、最新版はver.3.2.2になっていますので、こちらからどうぞ。

今回はWPFでの縦書きテキストについてです。

Win32API等を叩いたことがある人に「縦書きのテキストを表示したいときはどうする?」と聞いたら、おそらく即答で「縦書きフォント(通常のフォント名の頭に@を付けたもの)で90°回転させて文字列を描画する」と言うかと思います。LOGFONT構造体に文字列の角度を指定するメンバがあるので、そこに90°を指定して縦書きフォントを選択したうえでTextOut関数などを呼びだせば縦書きのテキストが完成します。

しかし、WPFではそう簡単にいきません。

というのも、なぜか縦書きフォントが封印されています。まあそもそもWPFはGDIベースではないので仕組みが全く違うと言えばそれまでなんですが、フォントファミリ名に@を付けたところで縦書きフォントになってくれません。

困りました。

じゃあどうするか。グリフというものを使えば良いようです。
フォントはあらゆる字に対しての統一的な書体のことを言いますが、グリフっていうのは個々の字そのものの形を意味する言葉らしいです。初めて聞きました。
C#では一般的にUnicodeで文字列が管理されていますが、そのある字に対して、そのUnicodeを「グリフインデックス」と呼ばれるあるフォント特有のインデックスに変換することで、フォントデータからその字を呼び出し画面に表示するという処理をしているようです。そのレベルでテキストをいじるクラスがGlyphsクラスのようです。普段は手を出さなくてもよさそうな低レイヤーなところですね。

当然、縦書きと横書きなんかで字のUnicode値が変わることはありませんが、グリフでは変わるものが出てきます。
例えば、句読点、括弧、記号などです。横書きの文字をそのまま縦に並べると、句読点の打たれる位置に違和感が出るでしょう。それ以外にも、括弧や記号などの形がおかしくなります。










このように書けば一目瞭然ですね。なので、縦書きは縦書き用のグリフに変換してやる必要があります。

一般的にこのUnicode値とグリフインデックスの変換に関しては法則性は無く、テーブルを介して変換するようです。
例えば、C#ではGlyphTypeface.CharacterToGlyphMapというプロパティを使って文字をグリフインデックスに変換してやることができます。しかしこれは横書き用の変換で、縦書き用の変換に関するものはC#には用意されていないようです。

非常に不便ですね。嫌になっちゃいます。なんでWin32APIであんなに簡単にできていたことでC#でこんなに苦労せにゃならんのだと。ついでに言えば、英語圏の人は別に縦書きなんて必要としないので、そういう意味でもこの縦書きに関する記事は非常に少ないです。


さて、愚痴だらけになってしまいましたが、今回の記事の目標はWPFでテキストブロックの縦書き版みたいなものを作るところにしたいと思います。

結論から言いますと、UniscribeというMicrosoftのテキストレンダリングエンジンを使うことで頑張って縦書き用グリフインデックスを取得しちゃえば良いという話です。これに関しては、えムナウ氏のブログに参考になりそうなコードが載っておりました。

WPF の縦書き
縦書きライブラリ

ネイティブの関数を呼び出さなければいけないのでDllImportだの[StructLayout(LayoutKind.Sequential)]だのC#らしくないところで散々苦労しなければならなくなってしまっています。が、えムナウ氏がその辺のラッパーを書いてMITライセンスで配布してくださっていますので、こちらを今回は使わせていただくことにしました。えムナウ氏のコードは縦書き用グリフインデックスを取得するあたりに特化しており、WPFのコントロール部はほとんど入っておりません。なので、今回はこれを実装しました。

割とXAMLはシンプルで、グリフを1つ用意し、縦書き用に90°回転させてやるだけです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<UserControl x:Class="Tategaki.TategakiText"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d" >
    <Grid>
        <Glyphs Name="glyphs1" Fill="Black" UnicodeString=" " IsSideways="true">
            <Glyphs.LayoutTransform>
                <RotateTransform Angle="90"/>
            </Glyphs.LayoutTransform>
        </Glyphs>
    </Grid>
</UserControl>

GlyphsはUnicodeStringとIndicesの両方を空にしては行けないようなので、初期値でスペースを与えています。

コードビハインドでは結構ごちゃごちゃやっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/// <summary>
/// TategakiText.xaml の相互作用ロジック
/// </summary>
public partial class TategakiText : UserControl
{
    public TategakiText()
    {
        InitializeComponent();
    }
 
    static Dictionary<string, string> fontPathDictionary = null;
    static Dictionary<string, string> FontPathDictionary
    {
        get
        {
            if(fontPathDictionary == null)
                fontPathDictionary = SearchFontNamePathPair(new CultureInfo[] { CultureInfo.CurrentCulture, new CultureInfo("en-US") });
            return fontPathDictionary;
        }
    }
 
    /// <summary>
    /// 有効なフォントの一覧を取得するメソッド
    /// </summary>
    /// <param name="cultures">フォントのカルチャの配列</param>
    /// <returns></returns>
    static Dictionary<string, string> SearchFontNamePathPair(IEnumerable<CultureInfo> cultures)
    {
        Dictionary<string, string> ret = new Dictionary<string, string>();
 
        string FontDir = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
 
        //キーを読み取り専用で開く
        RegistryKey regkey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts", false);
        string[] FontFiles = regkey.GetValueNames().Select(p => (string)regkey.GetValue(p)).ToArray();
 
        foreach(string file in FontFiles) {
            try {
                string path = FontDir + System.IO.Path.DirectorySeparatorChar + file;
                GlyphTypeface typeface = new GlyphTypeface(new Uri(path));
 
                foreach(CultureInfo culture in cultures) {
                    string FamilyName = typeface.FamilyNames[culture];
 
                    if(!string.IsNullOrEmpty(FamilyName) && !ret.ContainsKey(FamilyName))
                        ret.Add(FamilyName, path);
                }
            }
            catch(FileFormatException) { }
            catch(NotSupportedException) { }
        }
 
        return ret;
    }
 
    /// <summary>
    /// このコントロールで使えるフォント名を列挙するメソッド
    /// </summary>
    /// <returns>使えるフォントファミリ名</returns>
    public static string[] GetAvailableFonts()
    {
        return FontPathDictionary.Keys.ToArray();
    }
 
    /// <summary>
    /// 表示テキスト
    /// </summary>
    public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
        "Text", typeof(string), typeof(TategakiText), new PropertyMetadata((d, e) => {
            TategakiText me = (TategakiText)d;
            me.RedrawText();
        }));
 
    /// <summary>
    /// 表示テキスト
    /// </summary>
    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { SetValue(TextProperty, value); }
    }
 
    /// <summary>
    /// 文字間隔
    /// </summary>
    public static readonly DependencyProperty SpacingProperty = DependencyProperty.Register(
        "Spacing", typeof(double), typeof(TategakiText), new PropertyMetadata((double)100, (d, e) => {
            TategakiText me = (TategakiText)d;
            me.RedrawText();
        }));
 
    /// <summary>
    /// 文字間隔
    /// </summary>
    public double Spacing
    {
        get { return (double)GetValue(SpacingProperty); }
        set { SetValue(SpacingProperty, value); }
    }
 
    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        switch(e.Property.Name) {
            case "FontFamily":
                try {
                    this.glyphs1.FontUri = new Uri(FontPathDictionary[FontFamily.Source]);
                    RedrawText();
                }
                catch(KeyNotFoundException) {
                    throw new ArgumentException("Cannot use this font.");
                }
                break;
            case "FontSize":
                this.glyphs1.FontRenderingEmSize = FontSize;
                RedrawText();
                break;
            case "Foreground":
                this.glyphs1.Fill = Foreground;
                RedrawText();
                break;
        }
        base.OnPropertyChanged(e);
    }
 
    void RedrawText()
    {
        if(string.IsNullOrEmpty(Text))
            this.glyphs1.UnicodeString = " ";
        else {
            ushort[] glyphs = Uniscribe.GetGlyphs(Text, FontFamily.Source, (int)FontSize);
            string[] IndicesTexts = glyphs.Select((p, i) => {
                StringBuilder sb = new StringBuilder();
                sb.Append(p);
 
                if(i < glyphs.Length - 1)
                    sb.AppendFormat(",{0}", Spacing);
 
                return sb.ToString();
            }).ToArray();
 
            this.glyphs1.UnicodeString = Text;
            this.glyphs1.Indices = string.Join(";", IndicesTexts);
        }
    }
}

まず、フォントファミリ名とフォントファイルの対応付けをやっています。
フォントのディレクトリを取得し、一方、レジストリからインストールされているフォントの一覧を読みだして、その一つ一つについてフォントファミリ名を取得しています。使えないのは省いています。
フォントディレクトリ内のファイルを列挙して総当りする処理もやってみましたが、かなり時間が掛かったので(おそらく例外周りのせい)とりあえずレジストリの方法にしています。

あとは、表示テキストや文字間隔の依存関係プロパティを作り、OnPropertyChangedをオーバーライドして特定のプロパティが変更されたときにグリフの設定を変更する作業などもしています。
RedrawTextメソッドがそのメソッドで、取得したグリフや文字間隔の値を使いながらIndicesをフォーマットし、設定しています。

このコントロールを使用すとこんな表示ができるようになります。


これは、次のようなXAMLで実現しています

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<Window x:Class="Frontend.Views.MainWindow"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:Frontend.Views"
        xmlns:vm="clr-namespace:Frontend.ViewModels"
        xmlns:tg="clr-namespace:Tategaki;assembly=Tategaki"
        Title="Tategaki Sample" Height="640" Width="480">
     
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>
     
    <i:Interaction.Triggers>
     
        <!--Viewに特別な要件が存在しない限りは、トリガーやアクションの自作にこだわらず積極的にコードビハインドを使いましょう -->
        <!--Viewのコードビハインドは、基本的にView内で完結するロジックとViewModelからのイベントの受信(専用リスナを使用する)に限るとトラブルが少なくなります -->
        <!--Livet1.1からはコードビハインドでViewModelのイベントを受信するためのWeakEventLisnterサポートが追加されています -->
         
        <!--WindowのContentRenderedイベントのタイミングでViewModelのInitializeメソッドが呼ばれます-->
        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
        </i:EventTrigger>
 
        <!--Windowが閉じたタイミングでViewModelのDisposeメソッドが呼ばれます-->
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>
 
        <!--WindowのCloseキャンセル処理に対応する場合は、WindowCloseCancelBehaviorの使用を検討してください-->
 
    </i:Interaction.Triggers>
     
    <StackPanel Orientation="Horizontal" FlowDirection="RightToLeft">
        <tg:TategakiText Text="羅生門"
                         FontFamily="メイリオ" FontSize="36" Spacing="200"
                         HorizontalAlignment="Center" VerticalAlignment="Center"
                         RenderTransformOrigin="0.5,0.5">
            <tg:TategakiText.RenderTransform>
                <ScaleTransform ScaleX="-1" />
            </tg:TategakiText.RenderTransform>
        </tg:TategakiText>
        <tg:TategakiText Text="芥川龍之介"
                         FontFamily="メイリオ" FontSize="18" Spacing="100"
                         HorizontalAlignment="Center" VerticalAlignment="Bottom"
                         RenderTransformOrigin="0.5,0.5">
            <tg:TategakiText.RenderTransform>
                <ScaleTransform ScaleX="-1" />
            </tg:TategakiText.RenderTransform>
        </tg:TategakiText>
        <tg:TategakiText Text="すると、老婆は、見開いていた眼を、一層大きくして、じっとその下人の"
                         FontFamily="メイリオ" FontSize="18" Spacing="100"
                         HorizontalAlignment="Center" VerticalAlignment="Top"
                         RenderTransformOrigin="0.5,0.5">
            <tg:TategakiText.RenderTransform>
                <ScaleTransform ScaleX="-1" />
            </tg:TategakiText.RenderTransform>
        </tg:TategakiText>
        <!-- 中略 -->
    </StackPanel>
</Window>

スタイルとか使えばいいじゃんって突っ込まれそうですけど、まあサンプルなのでプロパティの使い方がわかりやすいようにこういう記述のしかたをしています。
このTategakiTextには改行機能は無いので現状1行ずつインスタンスを作っていますが、それを順に並べるのにはStackPanelを使っています。ごく自然なStackPanelの使い方だとは思いますが、右から順に並べたいのでFlowDirectionをRightToLeftに指定しています。しかし、これを指定するとStackPanel内の表示内容も左右反転してしまうようなんですね。アラビア語とかの使用を想定したインターフェースなのでしょうか。とりあえず、左右が逆転していては仕方ないので、各TategakiTextをRenderTransformで左右反転しています。

これでめでたく縦書きをWPFコントロールというコンテナ化された状態で使えるようになりましたとさ。めでたしめでたし。


サンプルコードを含むこのライブラリのダウンロードはこちらからできます。
WPF用縦書きテキストブロック Tategaki ver.1.0.0
((2015/1/22)都合により削除しました。ver1系はver.1.1.2を使ってください)