2014年9月11日木曜日

2048のロジックを実装する

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

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

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


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

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っていうクラスにしてみました。

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

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の中身を見てみましょう。

/// <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ですが、実はとても簡単に実装できます。

/// <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つのセルに縮まる感じです。量子力学とかサーバー運用等に関して言う縮退の意味を考えるとどうなのかなーって気もしなくもないですが、なんていうか、ほかに良い日本語が見つからなかったので。

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

/// <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()を入れてあげるだけです。

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

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

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

/// <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です。

/// <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で比べてるだけですがね。

/// <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」も実装しました。ゲームオーバーしたかどうかはすなわち全方向に動かせないということですので、そうなるように実装しました。

/// <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を教えてくれるようです)。もちろん、その場合は他人にそのアプリケーションを渡せなくなる(渡したら自分のアカウントで好き放題されてしまう)ので注意してください。

SingleUserAuthorizer auth = new SingleUserAuthorizer() {
    CredentialStore = new SingleUserInMemoryCredentialStore() {
        ConsumerKey = "****",
        ConsumerSecret = "****",
        AccessToken = "****",
        AccessTokenSecret = "****"
    }
};

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

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

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

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の威力が発揮されます。

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からリプライ元を取得する時などに役に立ちます。

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型になってました。個人的には良改変だと思ってます。

ユーザー情報を取得する

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

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コードで認証した時なんかは自分のアカウント情報は自明ではありません。調べる必要があります。

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は必須と言えるでしょう。

(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の仕様変更かなんかで上手く動いてくれなかったと記憶しています。このライブラリもアプデされてないっぽいので、まあ、動かないのでしょう。

ツイートする

普通にツイートをする

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

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

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

リプライをする

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

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

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

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

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

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

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

画像をツイートする

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

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

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

ツイートを表示する

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

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文字ほど足してからグリフインデックスを取得し、その足した分のグリフインデックスを無視するということをやっています。

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

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

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

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

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文字当たりの幅を計算し、おおよそ目標の長さにいきなり合うようにして、後はそこから実際にコントロールの長さを測って調整するという処理をすれば行数に比例する程度の時間で済みます。しかし、それでも結構この処理は重く、このコントロールをリサイズしたりすると結構カクカクします。そういう意味で改善の余地がありそうですが、なんか根本的な方法が無いともうどうしようもないんですよね…。

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

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行の縦書きを表示するのに要求されるサイズですね。こうやって測っています。なんかコントロールを一旦インスタンス化して測定してるあたり、結構時間の掛かりそうな処理ですよね…。

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

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

<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°回転させてやるだけです。

<UserControl x:Class="Tategaki.TategakiText"
             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>
        <Glyphs Name="glyphs1" Fill="Black" UnicodeString=" " IsSideways="true">
            <Glyphs.LayoutTransform>
                <RotateTransform Angle="90"/>
            </Glyphs.LayoutTransform>
        </Glyphs>
    </Grid>
</UserControl>

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

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

/// <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で実現しています

<Window x:Class="Frontend.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        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を使ってください)

2014年7月15日火曜日

PIC24FJ64GB002でUSBメモリーにアクセスする

最近、秋月電子でPIC24FJ64GB002の取り扱いが始まりました。
USB-OTGに対応したPICとしてはかなり有名どころで、多くの書籍やウェブページでも取り上げられており、資料は豊富にあります。にもかかわらず入手性は最悪で、国内の通販では共立電子くらいでしか取り扱っていなくて、さもなくばDigiKeyとかで海外から取り寄せるくらいしか方法はありませんでした。
しかし、ついに秋月電子で取り扱いが始まりました。秋葉原に行くだけで買える!しかも、共立電子の半額以下という破格の値段です。

というわけで、早速買ってきました。

 
手前がPIC24FJ64GB002で、奥がPIC32MX250F128Bです。
この2つ、前者は16bitマイコン、 後者は32bitマイコンでアーキテクチャも何もかも全く違うのかなと思ったら、なんとまあピンコンパチ(多分)でした。少なくとも電源やUSB周りは同じで、PIC32MXのほうをいじってみたときに使った回路をそのまま転用することができました。
というわけで、そのPIC32MXをいじったときに最終的にいきついたFatFsを動かすお話に用意したプログラムをPIC24FJ64GB002に移植してみました。


と言ってもほとんど苦労はしません。

そもそもこのデモプログラムはPIC24FJ64GB004でも動くように作られてて、多分これと002の違いはピン数とかパッケージとかその程度です。また、FatFsは完全にデバイス非依存で、型の大きさとかに気をつけておく程度でちゃんと動作はしてくれるはずです。というわけで、プログラムの修正はほとんど必要ありません。


まずは前回のプロジェクトのプロパティからDeviceをPIC24FJ64GB002に変更します。
…と言いたいところですが、PIC32MXのほうの設定を消すのはもったいないです。かと言ってバックアップとってゴニョゴニョやるのもめんどくさい。というわけで、プロジェクトのコンフィグレーションを追加してしまいましょう。MPLAB Xのプロジェクトはコンフィグレーションと呼ばれるものを複数作っておくことで、同じ構成のプログラムを別のデバイスに簡単に移植できるようになっているようです。もちろん、コードがそれに対応していたらの話ですが。

プロパティのカテゴリーの欄の下にある「Manage Configurations...」というボタンを押すとこんな画面が出てきます。


Duplicateで既存のコンフィグレーションをコピーできますが、まあコピーしてもコンパイラが違うとコンパイラの設定が全部吹っ飛んじゃうんであまり意味無いでしょう。ということで、Newを押して、適当な名前のコンフィグレーションを作ります。そして、Set Activeボタンを押せば、そのコンフィグレーションがアクティブになります。


そして、作ったPIC24FJ64GB002のほうのコンフィグレーションに対して、設定をしていきます。デバイスが未設定なので、右上のデバイスの欄からPIC24FJ64GB002を選択します。そして、コンパイラをXC16に設定します。そして前回同様、インクルードファイルのパスとヒープ領域の容量を設定してあげます。


次に、ソースコードの修正を若干します。修正箇所は、コンフィグレーションビットの設定とマイコンの初期化のプログラムのみです。

とは言っても、デモプログラムのほうにPIC24FJ64GB004のコードが入っているので、プリプロセッサの__PIC24FJ64GB004__を__PIC24FJ64GB002__に変更してやるだけです。というより、#if definedで追加してやる形にしました(具体的なコードはMicrochipの著作物なので掲載は遠慮しておきます。各自MLAのサンプルコードからコピーしてください)。

#if defined(__PIC24FJ64GB002__) || defined(__PIC24FJ64GB004__)
    //中略
#elif defined( __PIC32MX__ )
    //中略
#else
    #error Unsupposed Processor
#endif

コンパイルは、クリーンビルドをしてください。PIC32MXのほうの何かが残っているとリンクで謎のエラーが起きて焦ります。

そして、書き込みが終わればUSBメモリーを挿して、めでたくサンプルファイルの書き込みがされて完成です。

めでたしめでたし。



ここまで書いてなんですが、値段で見ると、PIC24FJ64GB002が340円、 PIC32MX250F128Bが360円です。20円差でより高性能なPICが買えるなら、別にPIC32MXのほうでいいんじゃないかなーって気はします。
まあでも最初に言った通り、PIC24のほうは資料の豊富さは格別です。気が向いたらBluetooth Stackとかに手を出してみようかなー。

2014年7月14日月曜日

NTP時計がフリーズするバグの特定

さて、ここのところあまりブログでは話題にしていなかったNTP時計ですが、全く何もやっていたわけではありません。実は、恐怖の「数日~十数日に1回フリーズするバグ」が起きていました。はい、バグの中では最もタチの悪いやつですね…。

このバグの原因がわかるまでは、いろいろな原因を疑っていました。算術エラー割り込みだとか、A/D変換完了待ちループにおける不具合だとか…。
症状はこんな感じでした。
  • 時計のカウントが止まる(時計の値が進まなくなる)
  • 7セグは正常に表示されている(ダイナミック点灯制御は生きている)
ここからわかることは、メインルーチンがどこかで止まっているであろうってことと、PICのクロックが停止したとかそういうハードウェア寄りの問題ではないことです。
NTPサーバーより取得した時刻はタイマ1でカウントしていますが、 「タイマ1の値を取得して時刻に変換して7セグ表示値として設定する」という作業はメインルーチンでやっています。そして、ダイナミック点灯制御は割り込みでやっています。ダイナミック点灯制御が生きていて時刻が進まないということは、割り込み自体は起こり続けているということなので、CPUが止まっているわけではありませんが、何かしらの理由でメインルーチンが動いていないということです。そのため、何かソフトウェアに由来する問題であろうことが予想されます。

頻度ですが、割とすぐに起こるようなものではなく、止まった日時は
6/26 18:30頃
7/1 7:00頃
7/4 10:00頃
7/14 6:00頃
でした。一部記録が曖昧ですが、まあおおまかな日時はこれであっているはずです。フリーズするバグだけどもダイナミック点灯制御は生きているバグだったので、止まった時刻が容易にわかるのがせめてもの救いです。
しかし、これだけ間隔があいていると、なかなかデバッグも容易ではないですね…。


まずは、算術演算エラーを疑いました。
いわゆる、ゼロ除算エラーです。PIC24Fシリーズはゼロで除算すると無限ループにはまったりするわけでもなく、ちゃんとエラーとして割り込みを起こしてくれるようです。
この割り込みのハンドラは作っていなかったので、何かの拍子にゼロ除算が起こって割り込みが発生した上で、 ハンドラで割り込みフラグをリセットしていないから割り込みから抜け出せず、しかし、PIC24Fは多重割り込みが受け付けられるので、ダイナミック点灯制御の割り込みが算術演算エラーの上で割り込まれて動作していると考えました。
しかし、この予想には、1つ重大な間違いがありました。算術演算エラー割り込みは、割り込みの優先度が11に設定されています(ユーザー変更不可)。しかし、周辺モジュールや外部割込みの優先度は0~7にしか設定できず、すなわち、どんな周辺モジュール割り込みよりも先に算術演算エラー割り込みは起こり、算術演算エラー割り込み中に周辺モジュールが割り込みを起こすことはありえません。
すなわち、この予想は間違っていると言えます。

次に、A/D変換の終了待ちの問題を疑いました。
A/D変換は時間がかかるので、通常、開始の指示を出してからしばらくして終了しているかを確認し、値を読み出します。その終了の認識の方法は、無限ループでフラグをポーリングし続けて確認するだとか、割り込みで確認するだとか、いくらかあります。今回、NTP時計では無限ループの方法を使用していました。

AD1CON1bits.SAMP = 1;
while(!AD1CON1bits.DONE);

これ自体は何もおかしくないコードです。実際、MicrochipのA/D変換のドキュメンテーションにもこのように無限ループでポーリングし続けて変換完了を待つサンプルプログラムが書いてあります。
もしも、これが何かしらの問題で変換が終わってもAD1CON1bits.DONEが1になってくれなかったら、ここでプログラムが止まってしまいます。 要するにハードのバグを疑いました。しかし、そんな記事ググっても無ければ、実際にここで止まっている保証もありません。しかし、原因がわからないと疑心暗鬼になってしまって、特にこういう条件次第では無限ループになりそうなところを疑ってみたくなってしまうんですね…。


フリーズの原因を特定するために、いわゆるprintfデバッグと呼ばれるものの類のコードを実装してみました。
液晶は電源さえ入っていれば表示内容を保持するので、マイコンが何かしらの原因で止まっても、液晶は表示を保ってくれます。なので、特定のコードを通過したときに特定の何かしらの文字列を表示させるプログラムを実行しておけば、何が表示されているかでフリーズ箇所を絞れるというものです。

実際には文字列を出力したわけではなく、点を液晶に打ちました。実際に時計として使いながらのデバッグなので、あまり表示内容を乱したくないですしね。
最初はある一定の処理を終えるごとに点を1つずつずらしていくプログラムにしましたが、いざフリーズしてみると、確かに点が表示されているのはわかるものの、それが何段目のピクセルなのか判別がとてもじゃないですがつきません。それくらい予想してプログラム書けばよかった…。

というわけで、次は線にして、線の長さを順に伸ばしていくようにしました。それなら、一生懸命ドット数を数えればエラー箇所がわかります。
実際は、小型液晶ゆえにドットの個数を肉眼で数えるのは厳しいです。なので、フリーズしたら写真で撮って拡大して見ました。

はい、その写真がこちらになります。


画面の右下にドットを打ってありますが、6つのドットが表示されていることがわかります。

で、6ドット分の線が表示されているときに実行中のプログラムはどこかなと思ってソースコードを見てみたら、A/D変換を終えて7セグの明るさの設定値をセットする関数でした。A/D変換の完了待ちではありません。
で、問題のコードがどうなっていたかというと、こうなっていました。

void LED7SEG_SetBrightness(uint8_t value)
{
    uint32_t buf = value + 1ul;

    buf = (uint64_t)buf * buf * buf * PWM_PERIOD / (256ul * 256 * 256);

    PWMOnStart = PWM_PERIOD - buf;
    PWMOffStart = buf;

    PWMOnStart = min(PWMOnStart, PWM_PERIOD);
    PWMOffStart = min(PWMOffStart, PWM_PERIOD);

    Brightness = value;
}

この中で疑わしいところを探します。
掛け算とか割り算とかキャストとかは実際XC16コンパイラがどんなふうに展開しているかが見えないところなのでそういうところについつい目が行ってしまいますが、そこは信用するとした場合、もう残っているのはPWMOnStart変数等に代入しているところくらいしかありません。

( ˘⊖˘) 。o(待てよ…)

このPWMOnStartやPWMOffStartという変数は、7セグのダイナミック点灯用の割り込み中にも呼び出されるものです。
こういう、メインルーチンと割り込みルーチンの両方から参照される変数には細心の注意を払う必要があります。過去から現在にわたってプログラマーを苦しめ続けているマルチスレッドのトラウマ的存在としてとても有名なものです。

void _ISR __attribute__((__auto_psv__)) _T2Interrupt(void)
{
    static uint8_t cnt;
    IFS0bits.T2IF = 0;

    if(cnt++ & 0x01) {
        TMR2 = PWMOnStart;

        Show7SEG();
        LED_COLON = Colon;
    } else {
        TMR2 = PWMOffStart;

        LATA &= ~(MASK_7SEG_RA | MASK_7SEGTR_RA);
        LATB &= ~(MASK_7SEG_RB | MASK_7SEGTR_RB);
        LATC &= ~(MASK_7SEG_RC | MASK_7SEGTR_RC);
        LED_COLON = 0;
    }
}

こうやって割り込み中に7セグのON時間やOFF時間を制御するのに使っています。
PWMOnStartとPWMOffStartは0~PWM_PERIODの値になり、TMR2はPWM_PERIODになると割り込みが入るようにコンパレーターをセットしています。PWMOnStartやPWMOffStartが極端にPWM_PERIODに近かった場合、割り込み処理中に次の割り込みが入ってしまい、すなわち、_T2Interrupt関数を抜けた瞬間に再び_T2Interrupt関数に入ってしまいますが、PWMOnStartとPWMOffStartは足してPWM_PERIODになるようにしてありますので、片方が極端にPWM_PERIODに近くてももう片方は近くなくなるので、そっち側でメインルーチンにCPUを割くことができます。

はい、上の表現、めちゃくちゃ強調している意味、わかるひとにはわかりますね。

本当に、常に足してPWM_PERIODになるの?


こういう複数のスレッドにまたがって使われる変数は、一連の変数書き換えシーケンス中に割り込みが起こった場合を想定する必要があります。

もうお分かりでしょう。
LED7SEG_SetBrightness関数の5行目、PWMOnStartの値が代入されてから、次の行でPWMOffStartの値が代入されるまでの間にタイマ2の割り込みが入ったらどうなるでしょう?
しかも、もともとはPWMOffStartが極端にPWM_PERIODに近い値で、 次にPWMOnStartを極端にPWM_PERIODに近い値にしようとしたタイミングでの割り込みだったらどうなるでしょう?

両方とも、極端にPWM_PERIODに近い値で割り込みが入っちゃうんです。

そうなった場合、もうおしまいです。7セグのON表示処理をし終わったら直ちにOFF表示処理がされ、それが終わったら直ちにON表示処理を…といった状態になってしまい、二度と割り込みルーチンから抜けることがなくなってしまいます

これが今回のバグの原因だったんですね。


さて、ここまででこれがバグの原因だったということで片がつきました。しかしそれは情況証拠であって検証したわけではありません。そもそも、数日経ってやっと発動するかどうかのバグなのに、安定動作してると主張するには一体何日待てば良いのよって話です。
しかし、ラッキーなことに、この変数書き換え中の割り込みがフリーズの原因だった場合、フリーズの状態からそれを裏付けることができます。

それは、7セグの明るさです。

バグがあったとされたのは7セグのON処理時とOFF処理時の時間の長さを変えることで7セグの明るさを調節するプログラムですが、このバグが発動して割り込みルーチンから抜けられていないときは、ON処理とOFF処理の時間がともに最小で終わってしまっているということです。すなわち、デューティー比はおよそ1:1、少なくとも「明るいモード」や「暗いモード」の間の明るさに見えるはずです。

はい、実際に見比べてみましょう。
そのためには、外が真っ暗な時間に部屋の蛍光灯を付けて、同じ位置から同じ露出設定で写真を撮ってみれば、写真として客観的に明るさを見られますね。


上からフリーズ状態、輝度最高、輝度最低のモードです。
本当は三脚使って完全に定点撮影にすればよかったんでしょうが、面倒だったので同じ位置に立って手持ちで撮影しました。

もう一目瞭然ですね。写真でも明るさの違いがよくわかります。

というわけで、原因はこれで確定ってことで良いでしょう。

対策は簡単です。PWMOnStartとPWMOffStartの書き換え中は割り込みを禁止してやればいいだけです。

void LED7SEG_SetBrightness(uint8_t value)
{
    uint32_t buf = value + 1ul;

    buf = (uint64_t)buf * buf * buf * PWM_PERIOD / (256ul * 256 * 256);

    LED7SEG_DisableInterrupt();

    PWMOnStart = PWM_PERIOD - buf;
    PWMOffStart = buf;

    PWMOnStart = min(PWMOnStart, PWM_PERIOD);
    PWMOffStart = min(PWMOffStart, PWM_PERIOD);

    Brightness = value;

    LED7SEG_EnableInterrupt();
}

はい、これでPICを書き換えて完成です。
このように、途中でスレッドが切り替わってはいけない部分のことを、プログラミング用語で「クリティカルセクション」と言います。C言語にはそのような機能はありませんが、言語によってはそれを実現するための機能があるものもあります。
マルチスレッドでは、片っ端から「ここでもし割り込みが起こったらどうなる?」ということを念頭に置いてプログラムを書かなければなりません。すごく大変なことですが、それを怠ると今回みたいなバグを誘発してしまいますし、何しろこの手のバグは再現性がなかなか無いので原因を探るだけでも一苦労です。こういうバグを出さない、もしくは出してもこれが原因だと断定できるようになるまでは結構なプログラミングの習熟度が必要かと思いますが、まあ、この記事を読んでくださった皆さんももしこのような再現性の低いバグに出会った時は、この記事のことを思い出してくれればなと思います。


これで安定して動作してくれ~~~~~~