SIN@SAPPOROWORKSの覚書

C#を中心に、夜な夜な試行錯誤したコードの記録です。

Xamarin.FormsでListViewコントロールを使用するには?

【 Xamarin 記事一覧 】

BuildInsiderで連載されている「Xamarin逆引きTips」に寄稿させて頂きました。


Error

使用したコードは、下記にあります。


furuya02/XamarinTips.ListViewSample · GitHub





【 Xamarin 記事一覧 】

Xamarin.Forms 機械翻訳

【 Xamarin 記事一覧 】

f:id:furuya02:20150302035651p:plain:w400:left
今回は、機械翻訳を実装してみました。

WebAPIでかなり高速な翻訳や読み上げが可能なので、色々面白そうです。

図は、Webページの英文をコピペして翻訳しているようす。

Microsoft Translator

f:id:furuya02:20150302035809p:plain:w400:left
現時点での翻訳サービスは、googleとMicrosoftの2択のようです。googleの方は有料になってしまったようなので、今回は、Microsoftのサービスを使用しました。

Microsoftのオンライン機械翻訳サービスはエンドユーザー向けに「Bing Translator」という名前で提供されており、そのバックで動作しているのが「Microsoft Translator」のようです。

f:id:furuya02:20150302035827p:plain:w400:left



「Microsoft Translator」は、1 ヶ月あたり200万文字まで無料で利用可能です。

利用手順

「Microsoft Translator」の利用するための手順は、下記のとおりです。

Live ID 取得
Windows Live IDが必要です。既に取得している場合は、この作業は必要ありません。

サブスクリプション登録
Microsoft Translator データに対して、サブスクリプション登録をします。
f:id:furuya02:20150302035843p:plain:w400:left
Microsoft Translator | Microsoft Azure Marketplace


アプリケーション登録
f:id:furuya02:20150302035855p:plain:w400:left
https://datamarket.azure.com/developer/applications/
「顧客の秘密(シークレットキー)」は自由に決定することができるようです。また、クライアントアプリを作成する場合「リダイレクトURL」には、無効なURLを指定します。

ここで設定した、「クライアントID(ClientId)」と「顧客の秘密(ClientSecret)」が、実装時に必要になります。

翻訳クラスの実装

「Microsoft Translator」には、読み上げや、辞書引きなど、いくつかの機能がありますが、今回は「テキスト翻訳」のみを実装しています。
Xamarin.FormsのPCLから利用するために、HTTP通信用のHttpClientとJSON処理用のJson.NETをインストールしました。
※HttpClientはデフォルト非同期対応で、非常にシンプルに記述できるので惚れます。


PM> Install-Package Microsoft.Net.Http
PM> Install-Package Newtonsoft.Json

f:id:furuya02:20150302040006p:plain

実装したコードは、下記のとおりです。ざっくり言ってしまうと、アクセストークを取得して、WebAPIをコールしているだけです。
詳しくは、下記のリンクをご参照ください。(最後の方にサンプルコードもあります)
https://msdn.microsoft.com/ja-jp/library/ff512421.aspx

class Translator {
    private const string ClientId = "TranslatorSample001";
    private const string ClientSecret = "iYbcPHxxxxxxxxxxxx7IVE=";

    private string _token;

    private async Task<bool> InitializeToken() {
        if (_token == null) {
            var client = new HttpClient();
            const string url = "https://datamarket.accesscontrol.windows.net/v2/OAuth2-13";
            var content = new FormUrlEncodedContent(new Dictionary<string, string> {
                {"client_id", ClientId},
                {"client_secret", ClientSecret},
                {"scope", "http://api.microsofttranslator.com"},
                {"grant_type", "client_credentials"},
            });
            var response = await client.PostAsync(url, content);
            //JSON形式のレスポンスからaccess_toneを取得する
            var adm = JsonConvert.DeserializeObject<AdmAccessToken>(await response.Content.ReadAsStringAsync());
            _token = adm.access_token;
            return true;
        }
        return false;
    }

    
    //JSON形式のレスポンスをデシリアライズするためのクラス
    [DataContract]
    public class AdmAccessToken {
        [DataMember]
        public string access_token { get; set; }

        //access_token以外は、取得しないので省略
    }

    public async Task<string> Get(string str) {

        //初回のみaccess_tokenを取得する
        if (_token == null) {
            await InitializeToken();
        }
        var client = new HttpClient();
        var url = string.Format("http://api.microsofttranslator.com/v2/Http.svc/Translate?text={0}&to=ja"
            , WebUtility.UrlEncode(str));
        client.DefaultRequestHeaders.Add("Authorization", "Bearer " + _token);
        var response = await client.GetStringAsync(url);
        //XML解釈
        var doc = XDocument.Parse(response);
        return doc.Root.FirstNode.ToString();
    }
}

メイン画面

先の翻訳機能を使用するメインの画面は、入力欄(Editor)と出力欄(Label)と翻訳開始のボタン(Button)を配置しただけです。

namespace Translator {
    public class App : Application {
        public App() {
            MainPage = new MyPage();
        }

    // ・・・省略・・・

    }

    internal class MyPage : ContentPage {

        private readonly Translator _translator = new Translator();

        public MyPage() {
            //翻訳前のテキスト入力
            var editor = new Editor {
                HeightRequest = 200,
                Text = "Hello world"
            };
            //WindowsPhoneの場合だけ、フォーカスが無いときに文字が見えないので背景色を指定する
            if (Device.OS == TargetPlatform.WinPhone) {
                editor.BackgroundColor = Color.Teal;
            }
            //翻訳後のテキスト表示
            var label = new Label();
            //翻訳開始のボタン
            var button = new Button {
                Text = "Translate",
                Command = new Command(async () => {
                    label.Text = await _translator.Get(editor.Text);
                })
            };

            Content = new StackLayout() {
                Padding = new Thickness(10, Device.OnPlatform(30, 10, 10), 10, 0),
                Children = {editor, button, label}
            };

        }
    }
}

図は、WindowsPhoneとAndroidで動作しているようすです。

f:id:furuya02:20150302040101p:plain:w400f:id:furuya02:20150302040107p:plain:w400

コードはGithubに置きました。


furuya02/Xamarin.Forms.Translator · GitHub

参考資料

Microsoft Translator - Web localization
第38回 使ってみようMicrosoft Translator:使ってみよう! Windows Live SDK/API|gihyo.jp … 技術評論社
Translate Method



【 Xamarin 記事一覧 】

Xamarin.Formsでツールバーアイテムによるメニューを設置するには?

【 Xamarin 記事一覧 】

BuildInsiderで連載されている「Xamarin逆引きTips」に寄稿させて頂きました。


Xamarin.Formsでツールバーアイテムによるメニューを設置するには? - Build Insider

使用したコードは、下記にあります。


furuya02/XamarinTips.ToolbarItemSample · GitHub





【 Xamarin 記事一覧 】

Xamarin.Formsでタッチイベントを処理するには?(iOS/Androidの各種ジェスチャー対応)

【 Xamarin 記事一覧 】

BuildInsiderで連載されている「Xamarin逆引きTips」に寄稿させて頂きました。


Error

使用したコードは、下記にあります。


furuya02/XamarinTips.GestureSample · GitHub





【 Xamarin 記事一覧 】

Xamarin.Forms ビイヘイビア

【 Xamarin 記事一覧 】

Behaviors

Xamarin.Forms 1.3において新しく追加された機能の一つに、ビイヘイビアがありあます。

ビイヘイビアを使用すると、UIコントロールをサブクラス化することなく、そのコントロールに機能を追加することができます。

f:id:furuya02:20150227154055p:plain:w200f:id:furuya02:20150227154057p:plain:w200

 ビヘイビアを実装するには、Behaviorクラスを継承した独自のビヘイビアクラスを定義します。この時、Behaviorクラスには対象とするUIコントロール(ここではEntry)の型を指定します。
 
 ビヘイビアクラスの中では、OnAttachedTo及びOnDetachingFromをオーバーライドし、そこで、追加する機能メソッド(サンプルではCheckEmail)を追加・削除するだけです。

最後に、このビヘイビアクラス(EmailBehavior)を、対象となるEntryのBehaviorsプロパティに追加しています。

public class App : Application{
    public App(){
        MainPage = new MyPage();
    }

    //・・・省略・・・
}

class MyPage : ContentPage{
    public MyPage(){
        var button = new Button{
            Text = "Send",
        };
        var entry = new Entry{
            WidthRequest = 200,
            Placeholder = "user@exsample.com",
            Behaviors = { new EmailBehavior() }//ビヘイビアの追加
        };
        Content = new StackLayout{
            Padding = 50,
            Children = { entry, button }
        };
    }
}

public class EmailBehavior : Behavior<Entry> {
    private readonly Regex _regex = new Regex(@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*");

    protected override void OnAttachedTo(Entry bindable) {
        //ハンドラの追加
        bindable.TextChanged += CheckEmail;
    }

    protected override void OnDetachingFrom(Entry bindable) {
        //ハンドラの削除
        bindable.TextChanged -= CheckEmail;
    }

    private void CheckEmail(object sender, TextChangedEventArgs e) {
        //文字列がEmailとして正しいかどうかのチェック
        var m = _regex.Match(e.NewTextValue);
        //正かどうかによってテキストの色を変化させる
        ((Entry) sender).TextColor = m.Success ? Color.Default : Color.Red;
    }
}


別コントロールとの連携

ビヘイビアクラスでの挙動追加は、対象コントロールに閉じた動作になります。
追加機能(ここでは検証結果)を別のコントロール(ここではSendボタン)に連動させるには、ビヘイビアクラス側で、外部に状態を伝える何かが必要です。

次のコードは、ビヘイビアクラスにIsValidというプロパティを追加して、それにより、Sendボタンの有効無効を制御しています。

//・・・省略・・・

class MyPage : ContentPage {
    public MyPage() {
        var button = new Button {
            Text = "Send",
            IsEnabled = false //デフォルトで無効にした
        };
	//ビヘイビアクラスを変数として保持した
        var emailBehavior = new EmailBehavior();
        var entry = new Entry {
            WidthRequest = 200,
            Placeholder = "user@exsample.com",
            Behaviors = {emailBehavior}
        };
        entry.TextChanged += (sender, args) => {
            button.IsEnabled = emailBehavior.IsValid; //ビヘイビアクラスの検証結果を使用してボタンを状態を変更する
        };

    //・・・省略・・・

    }
}
public class EmailBehavior : Behavior<Entry> {
    
    //クラス外から検証結果が分かるようにプロパティを追加
    public bool IsValid { get; private set; }
    
    //・・・省略・・・

    private void CheckEmail(object sender, TextChangedEventArgs e) {
        //文字列がEmailとして正しいかどうかのチェック
        var m = _regex.Match(e.NewTextValue);
        //正かどうかによってテキストの色を変化させる
        ((Entry) sender).TextColor = m.Success ? Color.Default : Color.Red;

        IsValid = m.Success;//プロパティ値をセット

    }
}

f:id:furuya02:20150227154124g:plain:w200




【 Xamarin 記事一覧 】

Xamarin.Forms ListViewのコンテキストアクション

【 Xamarin 記事一覧 】

Context Actions

Xamarin.Forms 1.3において、ListViewにコンテキストアクションの機能が追加されました。
Android及びWindowsPhoneにおいては、長押し、iOSの場合は、左にスライドすることによって、メニューが出現します。

iOS Android WindowsPhone
f:id:furuya02:20150227062721p:plain:w200 f:id:furuya02:20150227062727p:plain:w200 f:id:furuya02:20150227062731p:plain:w200

※外観は、プラットフォームごとかなり違います。

public class App : Application{
    public App(){
        MainPage = new MyPage();
    }

    //・・・省略・・・
}

class MyPage : ContentPage {
    public MyPage() {
        var listView = new ListView() {
            ItemsSource = Enumerable.Range(0,10).Select(n=>"item-"+n),
            ItemTemplate = new DataTemplate(() => new MyCell(this))
        };
        Padding = new Thickness(0,Device.OnPlatform(20,0,0),0,0);
        Content = listView;
    }

    public async void Action(string str) {
        await DisplayAlert("Action", str, "OK");
    }
}

セルのテンプレート

Context Actionsを使用するには、必ず、DataTemplateを自前で用意する必要があります。

ViewCellを継承したテンプレートを作成し、ContextActionsプロパティにMenuItemを追加することでメニューが生成されます。

MenuItemは、Text若しくはIconが表示可能なように見えますが、今のところIconの表示はできませんでした。

iOSにおいて、スライドした部分の背景色は固定(グレー)であり、IsDestructiveをtrueにした時、赤色になります。

DataTemplateは、下記のように、typedefでクラスの型を指定するだけで生成が可能ですが、本サンプルでは、DataTemplate親クラス(MyPage)へのポインタをテンプレートクラスに送るために、わざとMyCellのインスタンスをnewしています。
ItemTemplate = new DataTemplate(typedef(MyCell))

class MyCell : ViewCell {
    public MyCell(MyPage myPage) {

        //テキスト表示用のラベル
        var label = new Label {
            VerticalOptions = LayoutOptions.CenterAndExpand
        };
        label.SetBinding(Label.TextProperty,new Binding("."));//ItemSourceのテキストを表示する
        
        //1つ目のメニュー
        var deleteAction = new MenuItem {
            Text = "Delete",
            Command = new Command( p=>myPage.Action("delete "+ p)),//Commandにセットすることでイベントを取得
            IsDestructive = true, //背景赤色
        }; 
        deleteAction.SetBinding(MenuItem.CommandParameterProperty, new Binding("."));//コマンド実行時のパラメータに表示テキストを使用する
        ContextActions.Add(deleteAction);

        //2つ目のメニュー
        var moreAction = new MenuItem{
            Text = "More",
        };
        moreAction.SetBinding(MenuItem.CommandParameterProperty, new Binding("."));//コマンド実行時のパラメータに表示テキストを使用する
        moreAction.Clicked += (s, e) => { //Clickedによりイベントを取得
            myPage.Action("more " + ((MenuItem)s).CommandParameter);
        };
        ContextActions.Add(moreAction);

        View = new StackLayout{
            Padding = 10,
            Children = {label}
        };
    }
}


アクション

f:id:furuya02:20150227063330p:plain:w200f:id:furuya02:20150227063335p:plain:w200

MenuItemのCommandプロパティ、若しくは、Clickedイベントを処理することで、アクションが記述できます。

先のサンプルコードでは、1つ目が、Commandによるもの、そして2つ目が、Clickedを使用した場合の例になっています。




【 Xamarin 記事一覧 】

Wi-Fi付きのArduino互換機 Spark Core (その5) Xamarin.Formsクライアントによるマトリックス操作

1 ダイナミック点灯

 ダイナミック点灯とは、1行ごとに点灯の処理を行い、それを高速で繰り返すことで、残像で全行が表示されているように見せることです。

 ここで、1行の表示を処理しているのは、void disp(int r,String dat) です。第1パラメータは、表示対象の行です。そして、第2パラメータは、列のOn/OFFを指定するデータです。例えば、datに"f0"を指定すると、ビットに直して"11110000"なので、左から●●●●○○○○のように点灯します。

 以下は、Arduinoのコードですが、このdisp()をloop()の中で、全行分繰り返しています。

int row[8]={A7,A6,A5,A4,A3,A2,A1,A0};
int col[8]={D7,D6,D5,D4,D3,D2,D1,D0};

//点灯対象の文字列データ
String datStr="f0f0f0f08f8f8f8f";

void setup() {
    for(int i=0;i<8;i++){
        //D0..D7の初期化
        pinMode(col[i], OUTPUT); 
        digitalWrite(col[i],1);//HIGHで初期化
        //A0..A7の初期化
        pinMode(row[i], OUTPUT); 
        digitalWrite(row[i],0); //LOWで初期化
    }
}

void loop() {
    for(int i=0;i<8;i++){
        //2文字づつ切り取って、列ごと点灯する
        disp(i,datStr.substring(i*2,i*2 + 2));
    }
}

//文字列(2文字)を数値化する
//"ff" -> 0xff   "1c" -> 0x1c
int toHex(String str){
    char c = str[0];
    int h = ('a' <= c && c <= 'f') ? (c - 'a') + 10 : (c - '0');
    c = str[1];
    int l = ('a' <= c && c <= 'f') ? (c - 'a') + 10 : (c - '0');
    return (h<<4 | l)&0xff;
}

void disp(int r,String dat){
    int d = toHex(dat);//文字列の数値化
    digitalWrite(row[r],0); //当該行の消灯
    for(int c=0;c<8;c++){
        int sw = 1;//HIGHで初期化
        if (((d >> c) & 1)==1) {
            sw = 0;//LOWで点灯
        }
        digitalWrite(col[7-c],sw);
    }
    digitalWrite(row[r],1); //当該行の点灯
    delay(3);
    digitalWrite(row[r],0); //当該行の消灯
}

元となるデータが、String datStr="f0f0f0f08f8f8f8f" であるため、LEDは次のように表示されます。

●●●●○○○○ <- f0
●●●●○○○○ <- f0
●●●●○○○○ <- f0
●●●●○○○○ <- f0
●○○○●●●● <- 8f
●○○○●●●● <- 8f
●○○○●●●● <- 8f
●○○○●●●● <- 8f


実際に点灯しているようすです。

f:id:furuya02:20150223060532j:plain

2 WebAPIの公開

続いて、(その2) で紹介したWebAPIを使用して、この元データであるdatStrを外部から変更できるようにします。

追加したArduinoのコードは、下記のとおりです。

int func(String sw); //WebAPIの型宣

void setup() {
    //・・・省略・・・
    Spark.function("func", func); //WebAPIの公開
}

//公開されたAPI
int func(String s){
    str = s;
    return 1;
}

3 LEDボタン

f:id:furuya02:20150223061751j:plain:w300:left
WebAPIをコールするクライアントは、iPhone6用として決め打ちなのですが、今後の拡張が容易なように、Xamarin.Formsを使用しました。

クライアントでは、LED(赤い丸)をタッチすると、LEDがON/OFFするようになっていますが、この赤い丸(LEDクラス)は、BoxViewを継承して作成しました。
LEDクラスは、レンダラーを記述することで、プラットホーム側で、丸く描画し、併せて影をおきました。

BoxViewを継承したLEDクラスは次のとおりです。

public class Led:BoxView{
    bool _sw;
    public bool Sw {
        set {
            _sw = value;
            Color = _sw ? Color.FromRgba(224, 33, 0, 255) : Color.FromRgba(255, 100, 100, 100);
            OnPropertyChanged(propertyName: "Color");
        }
        get { return _sw; }
    }

    public Led() {
        WidthRequest = 45;
        HeightRequest = 45;
        Sw = false;
    }
}

LEDクラスには、Swというプロパティがあり、これがON/OFFを表現しています。
Swプロパティをセットした時は、同時にColorプロパティが変更され、レンダラークラスにも通知されるようになっています。

レンダラー側のコードは、下記のとおりです。

[assembly: ExportRenderer(typeof(Led), typeof(LedRenderer))]
namespace MatrixLed.iOS
{
    class LedRenderer : BoxRenderer{
        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == "Color"){
                SetNeedsDisplay();      
            }
        }

        public override void Draw(CGRect rect) {
            //base.Draw(rect);

            var led = (Led)Element;         
            using (var context = UIGraphics.GetCurrentContext()){   

                var shadowSize = 3;
                var blur = shadowSize;
                var radius = 20;

                context.SetFillColor(led.Color.ToCGColor());     
                var bounds = Bounds.Inset(2,2); 
                context.AddPath(CGPath.FromRoundedRect(bounds, radius, radius));
                context.SetShadow(new SizeF(shadowSize, shadowSize), blur);
                context.DrawPath(CGPathDrawingMode.Fill);
            }
        }
    }
}

デフォルトの描画を無効とし、「影」及び「丸」を自前で描画しています。
また、再描画は、プロパティ「Color」が変更されたときに、 SetNeedsDisplay()をコールし、間接的にDraw()を呼び出しています。

4 メイン処理

続いて、メインの処理です。

 LEDクラスは、16個生成しAbsoluteLayoutレイアウトで配置しています。また、タッチの検出は、TapGestureRecognizerクラスを LEDクラス(BoxView)のGestureRecognizersに追加することで行っています。

public class App : Application {
    public App() {
        MainPage = new MyPage();
    }

    //・・・省略・・・
}

class MyPage : ContentPage {
    public MyPage() {
        var ar = new List<Led>();

        BackgroundImage = "back.png";//背景画像

        var absoluteLayout = new AbsoluteLayout();

        //送信文字の表示用ラベル
        var label = new Label{
            Text="0x0000000000000000",
            FontFamily = "HelveticaNeue-Thin",
            FontSize = 30,
            TextColor = Color.White,
            BackgroundColor = Color.FromRgba(0,0,0,70),
            WidthRequest = 375,//iPhone6
            HeightRequest = 100,
            XAlign = TextAlignment.Center,
            YAlign = TextAlignment.Center
        };
        absoluteLayout.Children.Add(label, new Point(0, 667-90));

        //タッチイベントの処理
        var gr = new TapGestureRecognizer(); 
        gr.Tapped += (s, e) => {
            var led = (Led) s;
                
            //ON/OFFの反転
            led.Sw = !led.Sw;

            //LEDのON/OFFデータをを文字列として取得する
            var dataStr = GetDataStr(ar);
          
            //ラベルへの表示
            label.Text = "0x" + dataStr;

            //WebAPIのコール
            Api(dataStr);

        };

        //LEDの生成と表示
        const int top = 100;
        const int left = 3;
        const int width = 46;
        for (var y = 0; y < 8; y++){
            for (var x = 0; x < 8; x++) {
                var led = new Led();
                ar.Add(led);
                led.GestureRecognizers.Add(gr); //タッチイベントの検知を追加
                absoluteLayout.Children.Add(led, new Point(left + x*width, top + y*width));
            }
        }

        Content = absoluteLayout;
    }
     // ・・・省略・・・
}

5 WebAPI

最後にWebAPIをコールするコードです。
トリガーは、タッチ時ですが、コールの要領は、(その2)で紹介したものと同じです。

    //タッチイベントの処理
    gr.Tapped += (s, e) => {
        var led = (Led) s;
                
        //LEDのON/OFFデータをを文字列として取得する
        var dataStr = GetDataStr(ar);
          
        //WebAPIのコール
        Api(dataStr);

    };


    //WebAPIのコール
    async void Api(String daataStr){
        var httpClient = new HttpClient();
        const string deviceId = "53ff6xxxxxxxxxxxxxx2567";//デバイスID
        const string accessToken = "50179axxxxxxxxxxxxxxxxxxxxxxxxxxx5413e";//アクセストークン
        const string funcName = "func";//WebAPIのファンクション名
        var data = new FormUrlEncodedContent(new Dictionary<string, string>{
                {"access_token", accessToken},
                {"params", daataStr}
            });
        //URLはデバイスIDとファンクション名で決まる
        var url = string.Format("https://api.spark.io/v1/devices/{0}/{1}", deviceId, funcName);
        await httpClient.PostAsync(url, data);
    }

    //LEDのON/OFFデータをを文字列として取得する
    private string GetDataStr(List<Led> ar) {
        var sb = new StringBuilder();
        for (var r = 0; r < 8; r++) {
            var dat = 0;
            for (var c = 0; c < 8; c++) {
                var index = r*8 + c;
                if (ar[index].Sw) {
                    dat |= 1 << (7 - c);
                }
            }
            sb.Append(Convert.ToString(dat, 16));
        }
        return sb.ToString();
    }

}



Githubにおいた全コードです。

furuya02/Xamarin.Forms.MatrixLed · GitHub

iPhoneで操作しているようすです。
f:id:furuya02:20150223061645j:plain






Wi-Fi付きのArduino互換機 Spark Core (その1) - SIN@SAPPOROWORKSの覚書


Wi-Fi付きのArduino互換機 Spark Core (その2) - SIN@SAPPOROWORKSの覚書


Wi-Fi付きのArduino互換機 Spark Core (その3) - SIN@SAPPOROWORKSの覚書


Wi-Fi付きのArduino互換機 Spark Core (その4) - SIN@SAPPOROWORKSの覚書