F# を使ってRaspberryPi+BrickPi+LOGO Mindstorms EV3 を動かす

F# に限らないのですが、試しにモーター制御の部分だけ .NET 版の BrickPi を作りました。

https://github.com/moonmile/BrickPiNet

– BrickPi.h と brickpinet.c で .NET に公開可能なインターフェースを作る
– BrickPi.cs と BPi.cs で .NET で包む
– sample.cs、sampleF.fs のように C#/F# で制御する

というパターンです。BrickPi_Python などを見ると、それぞれの言語でシリアルポートに直接アクセスしているのですが、C# からポートアクセスは面倒だし、BlickPi.h 自体で十分なインターフェースになっているのでこれをそのまま使います。BrickPi.h 自体は C言語で掛かれているので、そのまま shared で公開して、これを BrickPi.cs で DllImport するというスタイルになっています。

sample.cs と sampleF.fs の場合は、ほとんど生の BrickPi クラスを使っているので他のプログラム言語との対応が取りやすくなっています。が、そのままでオブジェクト指向的に扱いにくいので、BPi クラスを作ってオブジェクト化しています。複数のモーター制御のところで MVVM パターンを使っているのはお試しです。BrickPi の場合は、ポート呼び出しの後に戻りがないので、INotifyPropertyChanged と相性がよいかもしれません。次はセンサーから入って来る操作なので、event 的に作るか、reactive 的に作るか、といったところです。

以下、ざっとファイルの解説を。

■Makefile

Raspberry Pi 上でビルドする必要があるので、Makefile を作っています。先のブログにも書きましたが、RasPi の現状のレポジトリにある mono では F# 3.1 が動かないので、mono のビルドが必須になります。それだと相当大変なので、単に試したい方は、F# のほう(fshaprcのところ)は削除して、apt-get install mono-complete の後、ビルドを試してみてください。

all: sample.exe 
	sampleF.exe 
	libbrickpinet.so 
	sampleCs.exe 
	sampleF2.exe

sample.exe: libbrickpinet.so BrickPi.dll sample.cs
	gmcs /out:sample.exe -sdk:4.5 /r:BrickPi.dll sample.cs 
sampleCs.exe: libbrickpinet.so BrickPi.dll sampleCs.cs
	gmcs /out:sampleCs.exe -sdk:4.5 /r:BrickPi.dll sampleCs.cs 
sampleF.exe: libbrickpinet.so BrickPi.dll
	fsharpc /out:sampleF.exe /r:BrickPi.dll sampleF.fs 
sampleF2.exe: libbrickpinet.so BrickPi.dll
	fsharpc /out:sampleF.exe /r:BrickPi.dll sampleF2.fs 

libbrickpinet.so: brickpinet.o 
	gcc -fPIC -shared -o libbrickpinet.so brickpinet.c -lrt -lm -L/usr/local/lib -lwiringPi
BrickPi.dll: BrickPi.cs 
	gmcs /target:library /out:BrickPi.dll 
	-sdk:4.5 
	BindableBase.cs  
	BrickPi.cs 
	BPi.cs

■brickpinet.c

libbrickpinet.so で BrickPi API を公開させています。BrickPi 構造体へのアクセスがいちいち書いてるのは、C# から C言語の構造体の手間を省くためです。box してもよいのですが、Mono の場合 box は使えるのか?とか調べるのが面倒だったので。

/*
 * BrickPi Interface for .NET 
 */
#include "tick.h"
#include "BrickPi.h"

void SetTimeout(int time) {
	BrickPi.Timeout = time;
}
int GetTimeout() {
	return BrickPi.Timeout;
}
/*
  Motors
*/
void SetMotorSpeed(int motor, int speed) {
	BrickPi.MotorSpeed[motor] = speed;
}
int GetMotorSpeed(int motor) {
	return BrickPi.MotorSpeed[motor];
}
void SetMotorEnable(int motor, int b) {
	BrickPi.MotorEnable[motor] = b == 0 ? 0 : 1;
}
int GetMotorEnable(int motor) {
	return BrickPi.MotorEnable[motor] == 0 ? 0 : 1;
}
...

■BrickPi.cs

単純な.NET版のラッパークラスです。他のプログラム言語との対応がとりやすい形にします。

namespace BrickPiNet
{
    public class BrickPi
    {
        public const int PORT_A = 0;
        public const int PORT_B = 1;
        public const int PORT_C = 2;
        public const int PORT_D = 3;

        public const int PORT_1 = 0;
        public const int PORT_2 = 1;
        public const int PORT_3 = 2;
        public const int PORT_4 = 3;

        public const int MASK_D0_M = 0x01;
        public const int MASK_D1_M = 0x02;
        public const int MASK_9V = 0x04;
        public const int MASK_D0_S = 0x08;
        public const int MASK_D1_S = 0x10;

~~~
        [DllImport("libbrickpinet", EntryPoint = "BrickPiSetup")]
        public static extern int Setup();
        [DllImport("libbrickpinet", EntryPoint = "BrickPiSetupSensors")]
        public static extern int SetupSensors();
        [DllImport("libbrickpinet", EntryPoint = "BrickPiUpdateValues")]
        public static extern void UpdateValues();
        [DllImport("libbrickpinet", EntryPoint = "BrickPiSetTimeout")]
        public static extern void InitTimeout();

■BPi.cs

いちいち関数を呼び出すのは面倒なので、オブジェクト指向っぽく直したのが BPi クラスです。
値を設定した後に、BrickPi.UpdateValues() を呼び出さなくてもよいようにしてあります。ただ、プロパティの変更にセンシティブなのは MVVM の欠点で、このあたりは別なところで回避しようかなと思ってます。例えば、2つのモーターの向きを同時に変える、というようなパターンがそれに相当します。

public class BPi
{
    public ObservableCollection<BPiMotor> Motors;
    public bool AutoUpdate { get; set; }

    private int _timeout = 3000;
    public int Timeout
    {
        get { return _timeout; }
        set
        {
            if (_timeout != value)
            {
                _timeout = value;
                BrickPi.SetTimeout(value);
                BrickPi.InitTimeout();
            }
        }
    }

    /// <summary>
    /// Constructor
    /// </summary>
    public BPi()
    {
        this.Motors = new ObservableCollection<BPiMotor>();
        this.Motors.CollectionChanged += Motors_CollectionChanged;
        this.AutoUpdate = false;
    }
    public void Setup()
    {
        int res = BrickPi.Setup();
        if (res != 0)
        {
            Console.WriteLine(&quot;&quot;);
            throw new Exception(string.Format(&quot;Error: BrickPi.Setup: {0}&quot;, res ));
        }
    }

    void Motors_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        // var m = sender as BPiMotor;
        switch ( e.Action ) {
            case NotifyCollectionChangedAction.Add:
                foreach (BPiMotor it in e.NewItems)
                {
                    it.PropertyChanged += it_PropertyChanged;
                    BrickPi.SetMotorEnable(it.Port, it.Enabled);
                }
                break;
            case NotifyCollectionChangedAction.Remove:
                foreach (BPiMotor it in e.OldItems)
                {
                    it.PropertyChanged -= it_PropertyChanged;
                    BrickPi.SetMotorEnable(it.Port, false);
                }
                break;
            case NotifyCollectionChangedAction.Reset:
                break;
        }
        int res = BrickPi.SetupSensors();
        if (res != 0)
        {
            Console.WriteLine(&quot;&quot;);
            throw new Exception(string.Format(&quot;Error: BrickPi.SetupSensors: {0}&quot;, res));
        }
    }

    void it_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        if (this.AutoUpdate == true) 
            Update();
    }

    /// <summary>
    /// call UpdateValues
    /// </summary>
    public void Update()
    {
        BrickPi.UpdateValues();
        System.Threading.Thread.Sleep(10);
    }
}
public class BPiMotor : BindableBase
{
    public int Port { get; set; }

    private bool _enabled = false;
    public bool Enabled
    {
        get { return _enabled; }
        set
        {
            BrickPi.SetMotorEnable(this.Port, value);
            this.SetProperty(ref this._enabled, value);
        }
    }

    private int _speed = 0;
    public int Speed
    {
        get { return _speed; }
        set
        {
            BrickPi.SetMotorSpeed(this.Port, value);
            this.SetProperty(ref this._speed, value );
        }
    }
}

実際に制御するサンプルコードです。

■sampleCs.cs

設定部分がちょっとオブジェクト指向っぽい感じですね。最終的には、Xamarin.iOS/Android を使って、iPad から制御できるようにします。通信部分は、.NET で簡易HTTPサーバー作ってRESTでやり取りするという感じです。

class Program2
{
    void main()
    {
        int speed = 200;
        var bpi = new BPi();
        // initialize 
        bpi.Setup();
        bpi.AutoUpdate = true;
        var motor1 = new BPiMotor() { Port = BrickPi.PORT_B, Enabled = true };
        var motor2 = new BPiMotor() { Port = BrickPi.PORT_C, Enabled = true };
        bpi.Motors.Add(motor1);
        bpi.Motors.Add(motor2);
        bpi.Timeout = 3000;

        Console.WriteLine("start");
        bool loop = true;
        while (loop)
        {
            var k = Console.ReadKey();
            switch (k.Key)
            {
                case ConsoleKey.W:
                    motor1.Speed = speed;
                    motor2.Speed = speed;
                    break;
                case ConsoleKey.A:
                    motor1.Speed = speed;
                    motor2.Speed = -speed;
                    break;
                case ConsoleKey.D:
                    motor1.Speed = -speed;
                    motor2.Speed = speed;
                    break;
                case ConsoleKey.S:
                    motor1.Speed = -speed;
                    motor2.Speed = -speed;
                    break;
                case ConsoleKey.X:
                    motor1.Speed = 0;
                    motor2.Speed = 0;
                    break;
                case ConsoleKey.Q:
                    loop = false;
                    break;
            }
            // bpi.Update();
        }
    }
    static void Main(string[] args)
    {
        var pro = new Program2();
        pro.main();
    }
}

■sampleF2.fs

F# の場合も C# とほぼ同じですが、括弧がない分だけ若干短く書けます。これも HTTP 経由で制御できるようにする予定です。

module sampleF2
open System
open BrickPiNet 

let speed = 200
// main
let bpi = new BPi()
bpi.Setup()
bpi.AutoUpdate <- true
let motor1 = new BPiMotor( Port = BrickPi.PORT_B, Enabled = true )
let motor2 = new BPiMotor( Port = BrickPi.PORT_B, Enabled = true )
bpi.Motors.Add(motor1)
bpi.Motors.Add(motor2)
bpi.Timeout <- 3000 
Console.WriteLine(&quot;start&quot;)
let mutable loop = true
while loop do
    let key = Console.ReadKey()
    match key.Key with
        | ConsoleKey.W -> 
            motor1.Speed <- speed 
            motor2.Speed <- speed 
        | ConsoleKey.A -> 
            motor1.Speed <- speed 
            motor2.Speed <- -speed 
        | ConsoleKey.D -> 
            motor1.Speed <- -speed 
            motor2.Speed <- speed 
        | ConsoleKey.S -> 
            motor1.Speed <- -speed 
            motor2.Speed <- -speed 
        | ConsoleKey.X -> 
            motor1.Speed <- 0
            motor2.Speed <- 0
        | ConsoleKey.Q -> 
            loop <- false
        | _ -> ()

F# の場合、面白いのはインタープリタ(fsharpi)でも動くことです。Raspberry Pi 上のターミナルで、fsharpi でインタープリタを起動した後で、以下を一気に通します。シリアルポートアクセスのため sudo で root 権限で動かしてください。

#r &quot;BrickPi.dll&quot; ;;
open System ;;
open BrickPiNet ;;
let speed = 200 ;;
// main
let bpi = new BPi() ;;
bpi.Setup() ;;
bpi.AutoUpdate <- true ;;
let motor1 = new BPiMotor( Port = BrickPi.PORT_B, Enabled = true ) ;;
let motor2 = new BPiMotor( Port = BrickPi.PORT_B, Enabled = true ) ;;
bpi.Motors.Add(motor1) ;;
bpi.Motors.Add(motor2) ;;
bpi.Timeout <- 3000 ;;

その後、

motor1.Speed <- 200 ;;

のように F# のコードを書くとそのまま車輪が動き出します。このあたりの流れは F# インタープリタならではのところです。制御コマンドっぽい形で動かせるので、テストとか試行錯誤のときに便利そうです。

カテゴリー: C#, F#, RaspberryPi | F# を使ってRaspberryPi+BrickPi+LOGO Mindstorms EV3 を動かす はコメントを受け付けていません

BrickPi でモーターの動作確認まで

Raspberry Pi で F# が動かせたので、今度は BrickPi の設定です。

image

BrickPi Image Setup: Setting up your SD Card For the BrickPi
http://www.dexterindustries.com/BrickPi/getting-started/pi-prep/

にある Modify your own image にしたがって、BrickPi パッケージをインストールしていきます。

git clone https://github.com/DexterInd/BrickPi.git

から始まって、ちまちまと設定をしないといけないので、Download and use our modified Raspbian image on your own SD Card のほうから、BrickPi インストール済みの OS をダウンロードしたほうが良さそうです。今回は、mono, fsharp のインストールが大変なので、ちまちまと modify していきました。sudo nano /etc/inittab のあたりとか、既に Raspberry Pi 配布の Debian では設定済みのところがあるので、あれ?と思うところもありあますが、そのまま進んで完了です。

■サンプルを動かして動作確認する

サンプルは Progam メニューの中に色々あります。グラフィカルにプログラミングができる Scratch も魅力的ではあるのですが、最終的には F# で動かしたいのでここは飛ばし、

Scratch

C ← BrickPi
http://www.dexterindustries.com/BrickPi/program-it/c/

でビルドをします。

BrickPi のボードの MB と MC に EV3 のモーターをつなげて

cd BrickPi_C/Project_Examples/simplebot/
cp ../../Drivers/*.h .
gcc -o sbot simplebot_simple.c -lrt -lm -L/usr/local/lib -lwiringPi
sudo ./sbot

でモーターが動きます。Drivers ディレクトリからコピーしているのは、BrickPi.h と tick.h とい2つのファイルです。

最初、sbot のようにユーザーモードで動かして、なんか BrickPiSetup で -1 を返すし変だなーと思っていたのですが、どうやら root 権限じゃないと動かないようです。シリアルポート ttyAMA0 を読み書きするのでルート権限が必要なようです。

ソース自体は実に単純で https://github.com/DexterInd/BrickPi_C/blob/master/Project_Examples/simplebot/simplebot_simple.c を見ると、

  • BrickPiSetup で初期化
  • BrickPi.Address に通信アドレス設定
  • BrickPi.MotorEnable[motor1] のモーターの設定
  • BrickPiSetupSensors() でセンサーに設定
  • BrickPi.MotorSpeed[motor1] にモーターのスピードを設定
  • BrickPiUpdateValues() で設定をアップロード

のようなパターンでできます。C# のサンプルはないので、適宜 API を DllImport する必要がありそうですが、まあなんとかなるかな。

■WiFi と電源供給の関係

image

モーターの駆動は上の 9V 電池から、WiFi などの動作は下の mini USB から取ってきているようです。BrickPi の 9V 電源を指しただけでも Raspberry Pi は動作するのですが、WiFi を差し込むと電圧が足りないのか止まってしまうんですよね。手元にあるのが、Logitec の Skylink LAN-W300n/U2S というものなのですが、消費電力が多いのかもしれません。

ちなみに、新しく買ってきた elecom WDC-150U2MBK は Raspberry Pi が認識しないというパターンに陥っているので、慎重に選んだほうがよさそうです。いくつかのサイトを見ると、Logitec か bufferlo の製品を使っている場合が多そうです。たしか、基盤は一緒なので、ドングルの名前さえうまく設定してやれば Raspberry Pi で認識するようになったハズなのですが。

■参考

工作と小物のがらくた部屋: BrickPi を入手しました。
http://junkroom2cyberrobotics.blogspot.jp/2014/02/brickpi.html

このサイトを見ると Planex の BT-micro3H2X が高いですがよさそうです。WiFi と Bluetooth が一緒になっているのでよさそうです。

カテゴリー: RaspberryPi | BrickPi でモーターの動作確認まで はコメントを受け付けていません

Raspberry Pi で F# を動かすまで(まとめ)

手元にあるのが Raspberry Pi のメモリが 256MB タイプのためか F# をビルドするときに失敗します。が、別途の Debian でビルドした DLL を install することで回避しています。
手持ちの SD カードは 8GB です。

■下準備

画面は使わずにターミナルだけで作業をします。初期状態で ssh が使える状態になっているので、起動したときの IP だけわかれば OK。初回のみ HDMI でディスプレィをつなげて固定 IP にしておけばよいでしょう。DHCP でも自宅の場合はそう変わらないので、そのままでもいいかなと。

  1. Raspbian Debian Wheezy をインストール
  2. sudo respi-config して Expand Filesystem を実行。こうすると SD カード一杯まで容量を増やしていくれる。
  3. sudo passwd root でパスワードを設定しておく。
  4. adduser でユーザを作る。
  5. visudo で sudo できるようにしておく。
  6. sudo apt-get update する。
  7. sudo apt-get upgrade する。
  8. sudo apt-get install avahi-daemon する。ホスト名を公開するため。
  9. sudo apt-get install samba する。Windows からファイルを送るため

■ビルドの準備

Mono と F# を git から持ってきてビルドします。mono 自体は sudo apt-get mono-complete で持ってこれるのですが、何故か F# 3.1 で使おうとするこけます。また、Rasberry Pi のレポジトリには fsharp パッケージがないので、自前でビルドする必要があります。が、Raspberry Pi 上で F# のコードをビルドしようとすると、途中でテストのエラーでこけます。

という状態なので、Mono 3.2.8 + F# 3.1 の組み合わせの場合は、

  • Mono を Raspberry Pi でダウンロードしてビルド、インストール
  • F# を別の Debian でダウンロードしてビルド
  • 出来上がった F# を Raspberry Pi にコピーしてインストール

という手順になります。F# を別の環境で make install するので、ビルドするディレクトリ名は揃えたほうが無難です(異なる場合は、ln -s すれば ok)。可能ならば ARM 環境で Mono をビルドすればいいんでしょうが。Visual Studio を使えばできるかも。

■ビルドする

http://fsharp.org/use/linux/ にある Option 2: Build and install the F# 3.1 runtime, compiler and tools に従って Mono をビルドします。うちの環境では 12 時間以上かかりました。出来上がったバイナリは大事にとっておきましょう。

sudo make install すると無事 mono が使える状態になります。

image

お次は、F# をビルドするのですが、これは別の Debian でやります。git clone https://github.com/fsharp/fsharp  からダウンロードしてビルドします。これは普通の PC(私の場合は Hyper-V 上の Debian)なのであっという間です。ちなみに、Mono のビルドも結構なスピードで出来上がりますが、中身が ARM ではないので、Raspberry Pi にはコピーできません…と思うのですがどうなんでしょうね?私の場合は 12 時間かけて Raspberry Pi 上でビルドしたのですが。

できあがった F# のバイナリを tar で固めて、Raspberry Pi 上に持っていきます。そして sudo make install すれば、無事 F# が使えるようになります。

■F# を動かす

インタープリタは fsharpi です。コンパイラは fsharpc ですね。Scala の fsc とバッティングしたので名前が長くなっています。つーか、Scala 入れてないから、fsc で ln -s してもいいですね。

image

コマンドラインから fsharpi を打った後にしばらく待たされます。更に let a = … と打った後にしばらく待たされます。どうやら mono ががんばっている模様で、ある程度ライブラリのロードが終わったら早くなります。top でみると結構上に貼りついてしまうのでなかなか大変そうです。

image

ビルドした後は分からないのですが、センサー取り込みのような素早い処理には向かないけど、ロボット制御のようなコマンド送信には十分使えるかなと思っています。まあ、それで BrickPi を購入したわけですが。de:code で MS太田さんが .NET Micro Framework で EV3 を操作できたそうなので、MF のほうでも試してみたいですね。つーか、動くのか? F#
ちょっと古いですが、2010年ごろのブログに Managed、Native、.NET Framework? – デバイスとITの架け橋 – Site Home – MSDN Blogs の冒頭「参加されていたのは組込み業界の皆さんだったにも拘らず、F#を使っている方が何人かいらっしゃって」とあるので、制御系にはうけがいいかもしれません。コンピュテーション式なんてのは、DSL 的に有用ですからね。RSNP あたりでしたっけ?

そんな訳で、お次は BrickPi のセットアップです。

カテゴリー: F#, RaspberryPi | Raspberry Pi で F# を動かすまで(まとめ) はコメントを受け付けていません

Raspberry Pi で F# を動かす(陽性かくにん版)

F# ビルドの続きです。

■fsharp のビルドでこける

正確には F# proto(F# 3.1 をビルドするための F# 3.0)のテストでこけます。

image_thumb

ガベージコレクションのテスト(かな?)をしているところで、alloc 出来なくてこけているので おそらく RasPi のメモリ不足ですね。手元の Raspberry Pi って初期型なので、メモリが少なくって USB が 2 つついているけど 256MB しかないんですよね。

image_thumb[1]

おそらく、ここの GC チェックを外してしまえばビルドできると思うのですが、どうも手軽ではない。そもそも Mono 3.2.7 のメモリでこける件もテスト自体でこけているので実害はないかもしれない。

■Debian でビルドした F# をインストールする

で、そもそもが「 FSharp.Core は環境依存していないだろう」という想定のもとに、別の Debian でビルドした FSharp.Core 等を Raspberry Pi のほうにインストールしてしまいます。これでいいのかっどうか不明なのですが(Debian 構築のほうは Hyper-V 上で x64)、同じ Linux 上だしなんとかなるかもしれません。

Debian でビルドした fsharp を tar で固めて、Raspberry Pi へコピー。その後、フォルダ名を合わせてから sudo make install します。ビルド環境のパスをあわせるのは、makefile 等にビルド環境が書き込まれているからです。

無事インストールして fsharpi します。

image

fsharpi を動かすと30秒ぐらい待たされますが、一応動きます。元ネタは 大事なことは全部MLが教えてくれた ~ Apple の Swift の mutability 周りの件を理解する – Oh, you `re no (fun _ → more) です。どうやら mono の読み込みの時にかなりパワーを使っているらしく、新しい文法を使うたびに動作が重いです。一度、読み込むと動きがスムースになるのでキャッシュあたりですかね。

という訳で例の FsRandom で陽性かくにん! も動かしてみましょう。Windows の NuGet で持ってきた FsRandom.dll を Raspberry Pi にコピーして

fsharpc -r:FsRandom.dll stap.fs

すると、無事 stap.exe が出来上がります。これを実行すると…無事 198599 番目に(でよかったのかな?)陽性かくにんできます。

image

このプログラム自体は結構なスピードで動くので、初回の mono のロードだけ重たいみたいです。

カテゴリー: F#, RaspberryPi | 1件のコメント

Mono と F# を CentOS/Debian 上でビルドしている途中

なぜか、現状の Raspberry Pi 上では

という状態で、このテストエラーは Xamarin Bug  https://bugzilla.xamarin.com/show_bug.cgi?id=17654 を見ると直っているらしい。この現象自体は Mono 3.2.7 で発生しているので、手元の Mono 3.2.8 は大丈夫なはずなんだけど、同じようにダメ。おそらく RasPi にあるレポジトリが何らかのタイミングで古いのではないか?と思のだが…これを、RasPi 上でビルドするとえらい時間がかかる。6時間かかってもまだ Mono のビルドが終わらない。

まだまだ掛かりそうなので、その前に Mono 3.2.7 と F# 3.1 の組みあわせが Linux 上で動くかどうか確認しておこう。RasPi の CPU は、ARM1176JZF-S なので ARM になる。F# のアセンブリがそのまま動けばいいのだが…これはどうなんだろう?中身は機種依存していない感じなんだが。

■CentOS に g++ を入れる

sudo yum install gcc-g++

最新の CentOS には apt-get が使えなくなっているそうなので yum を使う。手元の Raspberry Pi は http://www.raspberrypi.org/downloads/ から RASPBIAN Debian Wheezy を使っているのでそれに揃えればいいのだが…いや、一旦そろえてみよう。

CentOS 上では Mono 3.2.7 はビルドできるのだが(多々エラーがでているのが気になるけど)、その後、F# 側で ./autogen.sh –prefix /usr すると、

[masuda@centos fsharp]$ ./autogen.sh --prefix /usr
checking whether make sets $(MAKE)... yes
checking for pkg-config... /usr/bin/pkg-config
configure: "pkg-config: /usr/bin/pkg-config"
configure: "PKG_CONFIG_LIBDIR: "
configure: error: "You need mono 3.0"
[masuda@centos fsharp]$

になって詰む。ピンポイントで mono 3.0.x が必要なのだろうか?

■Debian で Mono/F# をビルドする。

Debian のレポジトリには F# パッケージがあるのだが、http://fsharp.org/use/linux/ に従って Mono からビルドしていく。コマンド自体も apt-get が使えるのでそのままコピペしながら実行。途中でたらたらと Mono をビルドしている Raspberry Pi を追い抜いて、F# のビルドまで実行している途中。あっさり、Mono 3.2.7 の問題もスルーしていったので、おそらく Raspberry Pi のレポジトリにある Mono が古いバージョンなのかもしれない。

■Respberry Pi で mono のビルドが続く

約15時間ほどかけて Raspberry Pi 上で Mono のビルドが完了。USB メモリ上でやっているので、アクセスが遅くてビルドが遅いという話もある。8GB の SD メモリを使うと幾分はやいかも。

続けて F# のビルドに入るのだが、早々に Mono 3.2.7 の問題をクリアした。やっぱり Raspberry Pi レポジトリの Mono パッケージがおかしいらしい。OS のバージョンとかもあるのかもしれないが、それぞれの最新版で作ると通るので、ひとまず安心。

カテゴリー: F#, RaspberryPi | Mono と F# を CentOS/Debian 上でビルドしている途中 はコメントを受け付けていません

Raspberry Pi で F# を動かすまで…なんだがまだ終わらず

随分前から Raspberry Pi を持っているのですが、しばらく放置中だったので再開。LEGO Mindstorms EV3 を動くところまで続ける予定です。

最初は mono と F# を入れるところまで

■ Mono をインストールする

Mono on RaspberryPi でHelloWorld – 銀の光と碧い空 を参考にして

sudo apt-get install mono-complete

で一発インストールです。私の場合 SD カードが 2GB という小さいメモリだったので、mono を入れた途端に df が 100% になって詰み。仕方がないので、余っていた 4GB の SD カードに OS を入れ直して再インストールする羽目に。

# 追記
# respi-config で Expand Filesystem すると、SD カード一杯に容量を拡張します。初期状態では、ディスクイメージのために 2.6GB ぐらいしかないんですね。実行すると手元の 8GB のメモリを認識できました。

…が、いざ Use F# on Linux | The F# Software Foundation を参考にして、

sudo apt-get install fsharp

しようとしたものの git clone した後に make したところで再びメモリ不足。F# のビルドは結構容量が多いのですね(と思ったけど、間違えて mono をビルドしてた orz、まあ USB メモリのマウントの方法が分かったので良しとするか)

さて、8GB の SD カードがあればいいのですが、手元にないので思案。幸いにして手元に USB メモリならばあるので、USBメモリのマウント : ふじかわ家のページ を参照してマウント。

mount /dev/sda1 /mnt/usbmem

が、更に問題があって、この USB メモリは FA32 フォーマットのために Linux で使う chmod 777 とかが正しく動かない。パーミッションを正しく動作させるためには ext3 あたりでフォーマットしなおさないと駄目なので、CentOS : USBメモリをLinux用にフォーマット « Demence/Cup fdisk でパーティションを削除したのち、mkfs.ext3 でフォーマット

fdisk /dev/sda1
...
mkfs.ext3 /dev/sda1

■F#をビルド…がエラーになるので

やっとこさ準備が整ったので git clone してからビルド…なのだが、make でエラーになる。

FSharp Build error on the Raspberry Pi under Mono 3.2.7 · Issue #260 · fsharp/fsharp
https://github.com/fsharp/fsharp/issues/260

と同じ現象で、手元のバージョンは Mono 3.2.8 で直っているはずなのだが。raspberry pi の mono-complete が何らかのタイミングで古いのかもしれない。仕方がないので、元に戻って mono からビルドをする。

という訳で、mono からビルド中。6時間経ったがまだ終わらず。

image

カテゴリー: F#, RaspberryPi | Raspberry Pi で F# を動かすまで…なんだがまだ終わらず はコメントを受け付けていません

Xamarin.Forms でネイティブのイベントハンドラを拾う(iOS/Android編)

Xamarin.Forms でネイティブのイベントハンドラを拾う(Windows Phone編) | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/5908

の続きです。やり方は同じなのですが、iOS/Androidの場合にはネイティブのコントロールには Name プロパティがないので、Windows Phone のような FindName メソッドはないですね。iOS の場合は Outlet、Android の場合は FindViewById になるので、操作がちょっと違います。このあたりは、似たような操作(あるいは、適当なメソッドで包んでしまう)にしないと、手間がかかるので後でまとめていきましょう。

■iOSのRendererを表示する。

iOS の場合は、画面のルートが UIViewController で、その下の各種の UIView があります。Xamarin製XAMLでPageオブジェクトを作成した後は、CreateViewControllerメソッドでUIViewControllerを作ります。

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    Forms.Init();

    window = new UIWindow(UIScreen.MainScreen.Bounds);
    // window.RootViewController = App.GetMainPage().CreateViewController();
    var page = App.GetMainPage();
    window.RootViewController = page.CreateViewController();
    window.MakeKeyAndVisible();
    Disp(window.RootViewController);

    return true;
}

void Disp(UIViewController vc, string spc = "")
{
    Debug.WriteLine("{0}{1}", spc, vc.GetType().Name);
    Disp(vc.View);
}
void Disp(UIView vi, string spc = "")
{
    Debug.WriteLine("{0}{1}", spc, vi.GetType().Name);
    foreach (var it in vi.Subviews)
    {
        Disp(it, spc + " ");
    }
}

■iOSのネイティブコントロールにイベントをつける

方法は Windows Phone と同じで、レンダラのツリーを探索してネイティブのコントロールを探し出します。Xamarin製XAMLには名前をつけておいて、対応するネイティブコントロールを返す SetNamePageToUIelement メソッドを作ります。

UIControl SetNamePageToUIelement(string name, Xamarin.Forms.Page page)
{
    var el = page.FindByName<View>(name);
    if (el != null)
    {
        var rend = FindRenderer(el);
        if (rend != null)
        {
            // var en = rend as EntryRenderer;
            // en.Control.Name = name;
            // リフレクションで
            var pa = rend as UIView;
            var pi = pa.GetType().GetProperty(&quot;Control&quot;);
            var obj = pi.GetValue(pa);
            //obj.GetType().GetProperty(&quot;Name&quot;).SetValue(obj, name);

            return obj as UIControl;
        }
    }
    return null;
}

UIView FindRenderer(View ent)
{
    return Search(window.RootViewController.View, ent);
}
UIView Search(UIView el, View ent)
{

    var pa = el as UIView;
    if (pa != null)
    {
        var pi = pa.GetType().GetProperty(&quot;Element&quot;);
        if (pi != null)
        {
            var enel = pi.GetValue(pa);
            if (enel == ent)
            {
                return pa;
            }
        }
        foreach (var it in pa.Subviews)
        {
            var ret = Search(it, ent);
            if (ret != null)
            {
                return ret;
            }
        }
    }
    return null;
}

レンダラのツリーでペアになっている、Xamarin製コントロールとiOS謹製コントロールは、それぞれ Element プロパティと Control プロパティで取得できます。ただし、レンダラーで使ってるクラスが ViewRenderer<TView, TNativeView> のようにジェネリックになっているため、各コントロールごとにクラスが作成されています。対応するコントロールに対してのキャストをいちいちやってもいいのですが、所詮プロパティだけが欲しいのですから、リフレクションを使って省力化します。

ネイティブコントロールは UIControl を基底クラスにしているので、これを戻します。各種のイベントを付加したい場合は、もとのクラスにキャストする必要あります。

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    Forms.Init();

    window = new UIWindow(UIScreen.MainScreen.Bounds);
    // window.RootViewController = App.GetMainPage().CreateViewController();
    var page = App.GetMainPage();
    window.RootViewController = page.CreateViewController();
    window.MakeKeyAndVisible();
    Disp(window.RootViewController);

    UIControl uc = SetNamePageToUIelement("textUserName", page);
    var obj = uc as UITextField;
    obj.AllTouchEvents += obj_AllTouchEvents;

    return true;
}

■AndroidでRendererを表示する

Androidの場合は明示的なコンバーターを呼び出していません。SetPage メソッド内で隠蔽化されていて、レンダラのルートが解りづらいのですが、this.Window.DecorView でルートとなるビューが取得できます。ややこしいのですが、Android.Views.View と Xamarin.Forms.View と名前が混在しています。Xamarin.Forms.View のほうは、Xamarin製XAMLで使うViewで、Android.Views.View のほうはレンダリングツリーの構築のための View です。

protected override void OnCreate(Bundle bundle)
{
    base.OnCreate(bundle);

    Xamarin.Forms.Forms.Init(this, bundle);

    // SetPage(App.GetMainPage());
    var page = App.GetMainPage();
    SetPage(page);
    Disp(this.Window.DecorView);
}

void Disp(Android.Views.View vi, string spc = &quot;&quot;)
{
    System.Diagnostics.Debug.WriteLine(&quot;{0}{1}&quot;, spc, vi.GetType().Name);
    var vg = vi as ViewGroup;
    if (vg != null)
    {
        for (int i = 0; i < vg.ChildCount; i++)
        {
            var v = vg.GetChildAt(i);
            Disp(v, spc + &quot; &quot;);
        }
    }
}

Android の場合、子コントロールを取得するためには ViewGroup にキャストをします。Children コレクションを持たせてもいいような気がするのですが、Android はそういう流儀みたいです。

■Androidのネイティブコントロールにイベントを設定する

戻り値のオブジェクトが Android.Views.View になるだけで、iOS と動作は同じです。このあたりは、Windows Phone も含めてライブラリ化したいところですね。

Android.Views.View SetNamePageToUIelement(string name, Xamarin.Forms.Page page)
{
    var el = page.FindByName<Xamarin.Forms.View>(name);
    if (el != null)
    {
        var rend = FindRenderer(el);
        if (rend != null)
        {
            // リフレクションで
            var pa = rend as Android.Views.View;
            var pi = pa.GetType().GetProperty(&quot;Control&quot;);
            var obj = pi.GetValue(pa);

            return obj as Android.Views.View;
        }
    }
    return null;
}

Android.Views.View FindRenderer(Xamarin.Forms.View ent)
{
    return Search(this.Window.DecorView, ent);
}
Android.Views.View Search(Android.Views.View el, Xamarin.Forms.View ent)
{

    var pa = el as Android.Views.View;
    if (pa != null)
    {

        var pi = pa.GetType().GetProperty(&quot;Element&quot;);
        if (pi != null)
        {
            var enel = pi.GetValue(pa);
            if (enel == ent)
            {
                return pa;
            }
        }
        var vg = pa as ViewGroup;
        if (vg != null)
        {
            for (int i = 0; i < vg.ChildCount; i++)
            {
                var ret = Search(vg.GetChildAt(i), ent);
                if (ret != null)
                {
                    return ret;
                }
            }
        }
    }
    return null;
}

イベントの種類が、iOS/Android/WP と随分違うので一概に共通化できませんが、それぞれのネイティブのイベントを使うことができます。

protected override void OnCreate(Bundle bundle)
{
    base.OnCreate(bundle);
    Xamarin.Forms.Forms.Init(this, bundle);

    // SetPage(App.GetMainPage());
    var page = App.GetMainPage();
    SetPage(page);
    Disp(this.Window.DecorView);

    Android.Views.View vi = SetNamePageToUIelement("textUserName", page);
    vi.FocusChange += vi_FocusChange;
}

おそらく、将来的には基本的なタップイベントのようなものは、Xamarin.Forms で実装されるでしょうから、こまめに共通化してもあまり意味はないかなと思っています(まあ、現時点では Click イベントぐらいしかないので、実務的な意味あるんですが)。むしろ、スワイプやピンチのような特有な操作を共通にしておくとライブラリ的に意味があるかもしれません。ちょっとそのあたりは Xamarin.Forms のイベント絡みがどうなるのかが不明なので、なんとも言えませんね。

ただ、自前の TMPuzzle を移植してみた感じでは、圧倒的にイベント絡みの処理は足りなそうなので、なんらかの補完はしないと駄目そうです。ゲームアプリの場合には、ピンチ、スワイプ、コマのスライド、得点のアニメーションなど、通常のコントロールにはない操作が出てくるので、そのあたりが必要です。ええ、もちろん Unity や MonoGame を使えばいいんでしょうが、手軽に作れるパズルアプリってのは考えているので、そのあたりはおいおいと。

カテゴリー: 開発, Android, Xamarin, iOS | Xamarin.Forms でネイティブのイベントハンドラを拾う(iOS/Android編) はコメントを受け付けていません

Xamarin.Forms でネイティブのイベントハンドラを拾う(Windows Phone編)

Xamarin.Forms ではイベント絡みが隠蔽化されていて、ボタンのクリックイベントやテキストボックスの変更イベントぐらいしか発生しません。このため、パズルゲームでは画像(Imageタグ)のタップイベントを Xamarin.Froms でパズルゲームを作る(iOS/Android版) | Moonmile Solutions Blog のように TapGestureRecognizer を使っています。(今後はどうか分からないのですが)TapGestureRecognizer クラスでは、情報が何もわたってこなくてタップなりスワイプなりの操作をしようとすると、ネイティブな UI コントロールに切り替える必要が出てきます。なので、さっくっと作れそうなページであれば Xamarin.Forms で、複雑なコントロールの組み合わせであればネイティブで、ってことになるんでしょうが…いや、ちょっと待てよ。それぞれのプラットフォームでは、Content = TMPuzzleXForms.App.GetMainPage().ConvertPageToUIElement(this); のように、Xamarin.Forms の XAML から各プラットフォームへのコンバータが動いています。Windows Phone の場合は ConvertPageToUIElement、iOS の場合は CreateViewController が使われています。Android の場合は SetPage が直接呼び出されていて中身が不明ですが、たぶん内容は似た感じになっているハズです。

これは、Xamarin.Forms 製の XAML から、MS 製の XAML にコンバートしていることを示しているわけで、何等かの形で内部で Windows Phone 特有のコントロールにして持っています。少し調べていて、レンダリングの部分で Xamarin.Forms の Label から iOS の UILabel を取り出す – Qiita のように LabelRenderer などで変更できることが解りました。更に調べていくと、なんとか Renderer のようなものがいっぱいあります。

image

実は、直前のバージョンでは EntryRenderer が none public になっていて手が出せなかったのですが(iOS と Android の Renderer は public になっていました)。何故か、つい最近公開された、1.1.0.6201 では、ここの Renderer が公開になっていました。ええ、ついでに内部でペアでもっていてる Element プロパティと Control プロパティも public になっています(直前のバージョンでは、名前すら違っていたのは内緒です)。ここで、情報を整理すると、

  • Xamarin.Forms.ContentPage が Xamarin.Forms の XAML ツリーを持っている。
  • ConvertPageToUIElement 等を呼び出すと、MS 製の XAML ツリーを作成する。
  • 同時に、Renderer を含む UIElement のツリーを作成し、Xamarin 製と MS 製の対応をツリー状にして保持する。
  • Xamarin.Forms 製のコントロールは、Element プロパティで取得する。
  • MS 製のコントロールは、Control プロパティで取得する。

のような感じになっています。おおまかに書くとこんな感じです。中身をみるとテキストボックスの場合には、TextBox と Password の2つのコントロールを持っているので、Xamarin.Forms 側では Entry 、MS XAML では Canvs になっています。

image

なので、Xamarin.Forms 側のコントロールから、うまく MS XAML のコントロールを見つけてやれば、Tap や Mouse イベント等のネイティブなイベントを設定できるはずです。

■Renderer のツリー を覗いてみる

ためしに、Windows Phone の MainPage クラスのコンストラクタを弄って、ツリーを書き出してみます。

public MainPage()
{
    InitializeComponent();

    Forms.Init();
    Content = TMPuzzleXForms.App.GetMainPage().ConvertPageToUIElement(this);
    Disp(Content);
}

 

void Disp(UIElement el, string spc = "")
{
    var pa = el as Panel;
    if (pa == null)
    {
        Debug.WriteLine("{0}{1}", spc, el.GetType().Name);
    }
    else
    {
        Debug.WriteLine("{0}{1} '{2}'", spc, pa.GetType().Name, pa.Name);
        foreach (var it in pa.Children)
        {
            Disp(it, spc + " ");
        }
    }
}

これを実行すると、こんな感じに Renderer のツリーが取得できます。Name プロパティを出力してみたのですが、残念ながら空になっています。名前は別途 FindName で検索しないと駄目っぽいです。

Canvas ''
 PageRenderer ''
  ViewRenderer ''
   LabelRenderer ''
    TextBlock
   ViewRenderer ''
    EntryRenderer ''
     Canvas ''
      PhoneTextBox
      PasswordBox
    LabelRenderer ''
     TextBlock
    LabelRenderer ''
     TextBlock
    LabelRenderer ''
     TextBlock
    LabelRenderer ''
     TextBlock
    LabelRenderer ''
     TextBlock
    LabelRenderer ''
     TextBlock
   ViewRenderer ''
    ImageRenderer ''
     Image
    ImageRenderer ''
     Image
    ImageRenderer ''
     Image

■ Entry コントロールを見つけ出して、イベントを設定する

しかし、Element プロパティを調べれば、対応する Control プロパティで Windows Phone のコントロールが見つかることが分かったので、試しに Entry コントロールだけチェックしてみます。

public MainPage()
{
    InitializeComponent();

    Forms.Init();
    this.page = TMPuzzleXForms.App.GetMainPage() as TMPuzzleXForms.MainPage;
    // Content = TMPuzzleXForms.App.GetMainPage().ConvertPageToUIElement(this);
    this.Content = page.ConvertPageToUIElement(this);
    Disp(Content);

    SetNamePageToUIelement("textUserName", this.page);
    var obj = this.FindName("textUserName") as UIElement;
    obj.LostFocus += obj_LostFocus;
}

void obj_LostFocus(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("lost focus");
}

Xamarin.Forms 側で textUserName と名前つけたコントロールに対応する Windows Phone のコントロールを見つけ出します。そして、LostFocus 時にデバッグ出力するコードです。

ツリーから探し出すコードはこんな風になります。コントロールを直接返すのではなく、いちど Xamarin.Forms で付けた名前と同じものを Windows Phone のコントロールにもつけています。こうすると、後から FindName で見つけられるので汎用性があります。

void SetNamePageToUIelement(string name, Xamarin.Forms.Page page )
{
    var el = page.FindByName<Entry>(name);
    if (el != null)
    {
        var rend = FindRenderer( el );
        if (rend != null)
        {
            var en = rend as EntryRenderer;
            en.Control.Name = name;
        }
    }
}

UIElement FindRenderer(Entry ent)
{
    return Search( this.Content, ent );
}
UIElement Search(UIElement el, Entry ent)
{
    var pa = el as Panel;
    if (pa != null)
    {
        var en = pa as EntryRenderer;
        if (en != null && en.Element == ent)
        {
            return en;
        }
        foreach (var it in pa.Children)
        {
            var ret = Search(it, ent);
            if (ret != null)
            {
                return ret;
            }
        }
    }
    return null;
}

まあ、いちいち探索をすると遅くなってしまうので、一度 Xamarin.Forms のツリーを Renderer で探索してしまってから名前を付けるとよいでしょう。このあたりは、後日やる予定。

ネイティブのコントロールイベントが取れるので Image コントロールの Tap イベントも付加できます。おそらく、iOS/Android も同じ方式でできると思うので、このあたりは共通して使えるようにしていきます。ちょっとやっかいなのは、EntryRenderer クラスは共通のインターフェースから継承されていなくて、ViewRenderer<Entry, System.Windows.Controls.Canvas> な感じでひとつひとつジェネリックが使われてるってところですね。取得したいところが、Control プロパティと Element プロパティなので、共通に持っている Canvas では駄目という…ここは各コントロールごとに書くしかないのかな。あるいはリフレクションを使うとうまく作れるかも。

    public class VisualElementRenderer<TElement, TNativeElement> : Canvas, IVisualElementRenderer, IRegisterable
        where TElement : Xamarin.Forms.VisualElement
        where TNativeElement : System.Windows.FrameworkElement
    {
        public VisualElementRenderer();

        protected bool AutoPackage { get; set; }
        protected bool AutoTrack { get; set; }
        public UIElement ContainerElement { get; }
        public TNativeElement Control { get; }
        public TElement Element { get; }
        protected VisualElementTracker Tracker { get; set; }

このテクニックを、月曜日のコンテストに間に合わせるか…どうかは不明。どうせならば汎用的に作っておきたいし、Image コントロールのタップは3機種同じように作りたいので。

カテゴリー: Windows Phone, Xamarin | Xamarin.Forms でネイティブのイベントハンドラを拾う(Windows Phone編) はコメントを受け付けていません

Xamarin.Froms でパズルゲームを作る(Windows Phone版)

昨日の続きで…と言いますか、実装は昨日のうちに済ませました。移植自体は簡単で1時間かかりません。

image

ちょっと、XAMLを書き直して、コマの大きさを変えたりセンタリングしたりしています。

■ファイルアクセスは ApplicationData.Current.LocalFolder を使う

基本的なところは、iOS/Android と同じなのですが、ファイルアクセス部分がちょっとだけ違います。iOS/Android の場合は、System.Environment.GetFolderPath を使って普通のフォルダアクセス(といえ、アプリ内のフォルダだけなのですが…システムフォルダへのアクセスは要調査)と同様に使えますが、Windows Phone の場合は、ApplicationData.Current.LocalFolder を使います。これは WinRT と同じで、アプリからアクセスできるフォルダが OS 上で制限されているためですね。

このサンプルが「共有プロジェクト」を使っているのは、このためでもあって、以下のように #if でコンパイル時に分岐させます。PCL でやると、ここのコードビハイドが委譲を使ったりして面倒なので、そのままコード共有で。

var se = new XmlSerializer(typeof(MyData));
try
{
#if __IOS__ || __ANDROID__ 
    var documents =
        System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments);
    var file = System.IO.Path.Combine(documents, "mydata.xml");
    using (var stream = System.IO.File.OpenRead(file))
    {
        var m = se.Deserialize(stream) as MyData;
        m.CopyTo(_model);
    }
#else
    using (var stream = await Windows.Storage.ApplicationData.Current.LocalFolder.OpenStreamForReadAsync(
        "mydata.xml"))
    {
        var m = se.Deserialize(stream) as MyData;
        m.CopyTo(_model);
    }
#endif
}
catch
{
    // 各スコアを0にする
    this._logic.Reset();
}

stream を取るところは一緒なので、Windows Phone で使っている OpenStreamForReadAsync メソッドを、適当に包んでやれば共有化できるのですが…まあ、このままで。気になるのは、iOS/Android で使っているのは OpenWrite で同期メソッドなんだけど、Windows Phone の OpenStreamForReadAsync は非同期メソッドってところですよね。ここは非同期に揃えたいところです。

■ゲームロジックやAzure Mobile Serivces は、PCL で

image

PCL(移植可能)を使っているのはゲームロジックとAzure Mobile Service のところです。カメラ機能とかは Xamarin.Mobile を使えば共有化できるので、シンプルなものならば結構すんなり共有化できるかなと。ただし、カメラを撮った後の遷移が各プラットフォームで若干異なるので、そのあたりはひと工夫が必要そうです。

■画像リソースはどうする?

このアプリでは、画像ファイルを

_mk[0] = ImageSource.FromFile("MarkNone.png")

な感じでファイル名を指定して直接取ってきています。ここのルートが何処を指すかというと、プラットフォーム毎に違っていて、

iOS /Resources/MarkNone.png
Android /Resources/Drawable/MarkNone.png
WinPhone /MarkNone.png

ってな感じで、Windows Phone はプロジェクト直下を示しているという…画像ファイルがちりばめられるのは嫌なので、このあたり WinPhone のために Images フォルダを作成して、

iOS /Resources/Images/MarkNone.png
Android /Resources/Drawable/Images/MarkNone.png
WinPhone /Images/MarkNone.png

とかにしたいところですね。ちなみに、Windows Phone には Resources というフォルダがあるのですが、そこは参照していません。

ただし、画像がたくさんあったり多言語化を考えると、リソースを使いたいわけで、そのあたりは Xamarin.iOS/Android で、文字列と画像をPCLを使って共有させる方法 を使って PCL にしたいところです。ここでの方法は iOS/Android しか対応していないので、WinPhone と WinRT にも拡張したいところですね。

■サンプルコード

もう少し手を入れますが、現状のは Github に
https://github.com/moonmile/TMPuzzleXForms

カテゴリー: Android, Xamarin, iOS | Xamarin.Froms でパズルゲームを作る(Windows Phone版) はコメントを受け付けていません

Xamarin.Froms でパズルゲームを作る(iOS/Android版)

de:code の直前に発表になった Xamarin.Forms ですが、拙著のサンプルも、それぞれビューを作っているわけで。

日経BP書店|C#によるiOS、Android、Windowsアプリケーション開発入門
http://ec.nikkeibp.co.jp/item/books/P98340.html

そんな訳で、サンプルコードの TMPuzzle を Xamarin,Forms にコンバートしています。サンプル自体は一枚絵(ひとつのビュー)になっているので、比較的コンバートは楽なハズ…なのですが、いくつかコツが要りそうなでメモ的に残しておきます。

まだ途中の段階ですが、iOS/Android版を Github に公開しておきます。
https://github.com/moonmile/TMPuzzleXForms

■ビューを XAML で作る

https://github.com/moonmile/TMPuzzleXForms/blob/master/TMPuzzleXForms/TMPuzzleXForms/MainPage.xaml

サンプルでは、axml/storyboard/XAML で作ったので、今回も XAML で作ります。Xamarin Studio ではデザイナは動かないのですがコード補完ができるのでだいたいの動きが想像できます。デモのサンプル https://github.com/xamarin/xamarin-forms-samples にある ButtonXaml が XAML を使ったコードです。*.xaml と *.xaml.cs の2つがあるので、コードビハイドが書けます。MVVM スタイルにするときはバインドを使えばいいのですが、ここではがりがりとコードを書きつけてしまいます。バインド自体は、MS謹製のXAMLと同じように Text=”{Binding …}” の構文で書けるので、ここは便利。使えるコントロールは http://iosapi.xamarin.com/?link=N%3aXamarin.Forms を参照すれば OK です。TextBox が Editor になっているとか、いくつか違いがありますが、このあたりは iOS/Android/Windows の混合になるので仕方がないところです。

ビュー自体は、レイアウトが異なるので、http://developer.xamarin.com/guides/cross-platform/xamarin-forms/controls/layouts/ を見てベースを決めます。TMPuzzle は Windows ストア寄りに作ってあるので Grid を使ってみたのですが、iOS だとうまくレイアウトができませんね。スマートフォンのような小さ目の画面であれば? Frame を使って iOS のように位置固定(Windows の Xaml ならば canvas)で良いかもしれません。TMPuzzle 自体はゲームアプリなので、たくさんのコントロールを乗せていますが、普通のコンテンツを表示するならば、ContentViewStackLayout でもいいかもしれません。

image

プロジェクトの構成は、Blank App (Xamarin.Forms. Shared) を使っています。PCL でも良いのですが、サンプル自体はコードビハイドを使っているのと、プラットフォームごとの処理が必要になって時には共有プロジェクト内で #if してしまえばよいので、こっちのほうが融通が利きます。

image

https://github.com/moonmile/TMPuzzleXForms/blob/master/TMPuzzleXForms/TMPuzzleXForms/App.cs 共有プロジェクトの App.cs を書き換えます。MainPage.xaml を共有プロジェクトに追加したので、new MainPage() でページを作成します。コードビハイドの場合は、このあとゴリゴリ内容を書いていますが、ひとまず XAML を書いて Andorid か iOS のシミュレーターで動かすとよいでしょう。

public class App
{
	public static Page GetMainPage()
	{
		return new MainPage();
	}
}

画像リソースも多分、共有プロジェクトにおけるハズなのですが、フォルダが異なる(Android の場合は Resources/Drawable、iOS の場合は Resources)となるので、各プラットフォームのフォルダに置いています。画像リソースは一律、

var img = ImageSource.FromFile("MarkNone.png");

のように取り出せるので、iOS/Android を区別する必要はありません。

ちなみに、Xamarin.Forms とは関係ないですが、ファイル呼び出しは

var file = System.IO.Path.Combine(documents, "mydata.xml");

のように共通化できます(Windows Phone の場合は調査中)。

■MainPage.xaml.cs

https://github.com/moonmile/TMPuzzleXForms/blob/master/TMPuzzleXForms/TMPuzzleXForms/MainPage.xaml.cs

ページを表示するときの初期化は、Android では OnCreate、iOS では ViewDidLoad になるのですが、Xamarin.Forms では(多分)OnAppearing です。 ヘルプを見ると When overridden, allows application developers to customize behavior immediately prior to the Xamarin.Forms.Page becoming visible. とあるので、次回表示するときも呼び出されてしまうような気がするのですが、ここは要調査ですね。TMPuzzle は1枚絵なのでこれを使っています。

各プラットフォームで呼び出す MainActiveity や AppDelegate はそのままです。もともと共通化を意識して作ったサンプルプログラムなので、Xamarin.Forms に移植するとプラットフォーム毎の差がほとんどなくなって、画像ファイルの切り替えぐらいになります。

■画像のクリックイベントはTapGestureRecognizerを使う

Button コントロールには Click イベントがあるのですが、ImageやLabelなどには Click イベントがありません…というか、タップイベント全般がありません。ざっと調べてみると、コントロールに対するユーザーイベントは Click と TextChanged ぐらいしかありません。
うーむ、困った。これで万事休すかな、と思ったのですが TapGestureRecognizer ってのがありました。iOS と同じでコントロールに対するイベントを取ってきます。

TapGestureRecognizer tapGestureRecognizer = new TapGestureRecognizer
{
    TappedCallback = img_Click
};
// 表示用マークにイベントをつける
_marks = new Image[DataModel.BOARD_X_MAX * DataModel.BOARD_Y_MAX];
_tags = new Dictionary<Element, int>();
for (int i = 0; i < DataModel.BOARD_X_MAX * DataModel.BOARD_Y_MAX; i++)
{
    var img = _marks[i] = this.FindByName<Image>(string.Format(&quot;mark{0}&quot;, i));
    img.GestureRecognizers.Add(tapGestureRecognizer);
    _tags[img] = i;
}

こんな風に、イベント先の img_Click を設定しておいて、img.GestureRecognizers.Add のように追加していきます。GestureRecognizers 自体は、View クラスにあるので大抵のコントロールにはついています。

いわゆる sender が View オブジェクトで渡されるので、これを目的のコントロールにキャストします。引数自体は object 型なのですが、中身は不明。Image をクリックすると null が来ています…が、ひとまず画像やラベルのタップイベントを取得できます。

public async void img_Click(View view, object args)
{
    // 再入禁止
    if (_flag == true) return;
    _flag = true;
    Image mark = view as Image;

■Android と iPhone の動作

同じ XAML ファイルを使ったのですが、iOS のほうがレイアウトが崩れています。というか、Grid だとちょっと辛いものがありそうですね。Xamarin.Forms のサンプルは、flowlayout を使っているので、どれも同じようになりますが、複雑なレイアウトの場合にはそれぞれの調節が必要っぽいです。

image

image

引き続き Windows Phone を作成。

カテゴリー: Android, Xamarin, iOS | Xamarin.Froms でパズルゲームを作る(iOS/Android版) はコメントを受け付けていません