おでんはじめました。

required ちくわぶ and 巾着,optional はんぺん.

SkiaSharpがXamarin.MacでもWPFでも動くよという話

Xamarin その1 Advent Calendar 2017 - Qiitaの22日目です。

去年のアドベントカレンダーXamarin.Forms+SkiaSharpで縦組みの記事を書いたので、今回はこれを.NET Standard化してAndroidiOSとXamarin.MacWPFで動かしてみたよというお話です。

作成したサンプルはGitHubに置いたのでよければビルドしてみてください。

SkiaSharpとは

SkiaSharpとはC#と.Netで使用できるオープンソースの2Dグラフィックスのライブラリです。

github.com

現在はPCLと以下のプラットフォームに対応しています。

こちらに詳しく書かれています。 qiita.com

去年のプロジェクト構成

1年前はSkiaSharpがSharedプロジェクトのみに対応で、PCLプロジェクトに対応していなかったので途中で対応してたけど書き換える能力がなかったのでXAML用に専用のカスタムビュー(TategumiView)を作成し、カスタムレンダラーをiOSAndroidにそれぞれゴリゴリ書いて実装しました。

f:id:masatoru:20171220193746p:plain

今回のプロジェクト構成

現在のSkiaSharpはPCLプロジェクトにも対応しているので、専用のNativeViewを作る必要がなくなり(カスタムレンダラーが不要になり)XAMLでSkiaSharp標準のビューコントローラ(SKCanvasView)を使って描画まで実装が可能です。Xamarin.Mac / WPFも同様にSkiaSharp標準のビューコントローラが用意されています。

さらに.NET Standardに対応しているので描画ロジックなどを.Net Standard対応のライブラリ化をすることでXamarin.MacWPFからも共通のライブラリとして使用することができます。

f:id:masatoru:20171220194316p:plain

去年のSharedプロジェクトに比べてかなりシンプルな構成になっています。

.NET Standard2.0とは

.NET Standardの詳しい説明は賢者の方々のブログを見ていただくとして、僕の唯一の理解は

NET Standard2.0 は .NET Core 2.0 と .NET Framework 4.6.1 と Mono と Xamarin.iOS と Xamarin.Android と Xamarin.Mac と UWP を(ほぼ)サポートしている(ただし、OS やランタイム依存の処理がある場合を除く)

ということです。

.NET Framework4.5以降と.net standard1.xが共存できるということに関してはよくわかりません勉強中です。

要するに.NET Standardは複数のOSでソースを共有できる仕組みなので、 この仕組みのおかげで最近のXamarin.Formsや.NET Framework4.6.1以上のXamarin.MacWPFから.NET Standard2.0のライブラリを使用することができるというわけです。

「ほぼ」とあるのは.Net Standard2.0で作成したライブラリで例えば.NET Framework4.6.1のライブラリを参照すると完全な互換性がない可能性があると警告が表示されるのでそういうことです。

warning NU1701: パッケージ 'xxxxx 2.0.1' はプロジェクトのターゲット フレームワーク '.NETStandard,Version=v2.0' ではなく '.NETFramework,Version=v4.6.1' を使用して復元されました。このパッケージは、使用しているプロジェクトとの完全な互換性がない可能性があります。

僕の数少ない経験上では完全に互換性があります。

各プロジェクトのポイント

各プロジェクト作成にあたり一番特筆すべきは.NET Starndard2.0対応のために.csprojファイルを編集する必要がない!(2017.12.21時点)ということです。

参考までに現在のバージョンを記載しておきますが、少なくともこのバージョン以降であればIDEが提供するプロジェクト作成の手順だけで作成できます。

.NET Standard2.0ライブラリ

UIはXamarin.Forms / Xamarin.Mac / WPFそれぞれで作る必要がありますが、反対にそれ以外に関しては(上記条件を満たせば)共通の .NetStandard2.0ライブラリとしてまとめられるということになります。

今回のサンプルでは(フォントファイルなどリソースの取得に関する箇所を除く)描画ロジックをすべてをライブラリ化して、Xamarin.Forms / Xamarin.Mac / WPFから使用しています。

作成方法はとても簡単で、プロジェクト作成→.NET Standard→クラスライブラリ(.NET Standard)から作成できます。

f:id:masatoru:20171221111721p:plain

Xamarin.Formsプロジェクト

プロジェクト作成に関しては特筆すべきことはありません。プロジェクト作成から、Cross-Platform→Cross-Platform App(Xamarin Forms)→.NET Standardを選択で作成できます。

f:id:masatoru:20171221112156p:plain

nugetからSkiaSharpとSkiaSharp.Views.Formsを取得し、XAMLにSKCanvasViewを配置します。

<views:SKCanvasView x:Name="canvas" PaintSurface="OnPaintSample"/>

SKCanvasViewのPaintSurfaceのイベントハンドラに描画処理を記述します。

void OnPaintSample(object sender, SKPaintSurfaceEventArgs e)
{
  // SKCanvasを取得する
  var canvas = e.Surface.Canvas;
  // 描画処理
  DrawText(canvas);
}

Xamarin.Macプロジェクト

Visual Studio For Macで、Macアプリ→Cocoaアプリを選択します。

SkiaSharpを使用する場合でのはまりどころとしては、ターゲットフレームワーク(オプション→全般)をXamarin.Mac Modernを選択するということです。 .Net Framework4.6.1を選択すると、パッケージの取得でWindowsと認識してしまうのか(SkiaSharp.Mac.dllではなく)SkiaSharp.WPF.dllを持ってきてしまうので(仕組みがよくわからないので誰か教えてください)。

f:id:masatoru:20171221113820p:plain

nugetからSkiaSharpとSkiaSharp.Viewsを取得します。

Storyboardを使用してSKCanvasViewを配置してプロパティを関連付けます。

  1. Main.Storyboardをダブルクリック(XCodeが開く)
  2. ViewControllerにCustomViewを配置する
  3. CustomClassをSKCanvasViewにする
  4. 右上のAssitant editorを開く
  5. 作成したCustomViewをCtrlキーを押しながらViewController.hにドラッグ&ドロップする
  6. Nameをcanvasにする

f:id:masatoru:20171221121513p:plain

上記作業後にXCodeを保存するとコードビハインド(ViewController.designer.cs)にコントロールとプロパティが関連付けされていることがわかります。

[Register ("ViewController")]
partial class ViewController
{
    [Outlet]
    SkiaSharp.Views.Mac.SKCanvasView canvas { get; set; }
    
    void ReleaseDesignerOutlets ()
    {
        if (canvas != null) {
            canvas.Dispose ();
            canvas = null;
        }
    }
}

Xamarin.Forms同様に、ViewController.csにイベントハンドラを記述します。

// 画面が初期化される時に呼ばれる
public override void ViewWillAppear()
{
  base.ViewWillAppear();

  // SKCanvasViewにイベントハンドラを関連付ける
  canvas.PaintSurface += OnPaintCanvas;
}
private void OnPaintCanvas(object sender, SKPaintSurfaceEventArgs e)
{
  // SKCanvasを取得する
  var canvas = e.Surface.Canvas;
  // 描画処理
  DrawText(canvas);
}

Storyboardの操作方法などXamarin.Macのことが詳しく書かれているEssential Xamarinをぜひご一読ください。

WPFプロジェクト

Windowsクラッシックデスクトップ→WPFアプリ(.NET Framework)→.NET Framework4.6.1以上で作成します。

XAMLへの記述以降はほとんどXamarin.Formsと同じです(クラス名が少し違うので注意)。 nugetからSkiaSharpとSkiaSharp.Viewsを取得し、XAMLにSKElementを配置します。

<views:SKElement x:Name="canvas" PaintSurface="OnPaintSample"/>

SKCanvasViewのPaintSurfaceのイベントハンドラに描画処理を記述します。

void OnPaintSample(object sender, SKPaintSurfaceEventArgs e)
{
  // SKCanvasを取得する
  var canvas = e.Surface.Canvas;
  // 描画処理
  DrawText(canvas);
}

画面キャプチャ

最後に今回作成したサンプルの画面キャプチャをのせておきます。

f:id:masatoru:20171221103637p:plain

f:id:masatoru:20171221130955p:plain

f:id:masatoru:20171221130554p:plain

f:id:masatoru:20171221103656p:plain

最後に

.NET Standard2.0がより一般的になることでWinFormsなどのレガシーなC#の資産をXamarinやMacで再利用する流れがそろそろより一層広がればいいなと思います。

今後の課題として、(今回はすべてコードビハインドで書いていますが)WPFやXamarinでPrismやReactivePropertyなどのフレームワークを使用した場合に、MacCocoa Bindingなど別のフレームワークを利用することになるので、全体としてどういう構成(落としどころ)が望ましいのか引き続き勉強したいです。

以上です。 今宵もハッピーアドベント!(言わない?)

追伸、 あ!サンプルのボタンがどれも機能してないです(汗。随時実装していきますので。

明日は tafuji さんです。よろしくお願いします!

Xamarin.Macでアプリ起動時にコードでStoryboardを切り替える方法

Xamarin.Macの起動時に複数のストーリーボードからコードで選択する方法がわからなかったのでそのメモ。

デフォルトで起動時に表示されるインターフェイスはinfo.plistのMain Interfaceで指定されています(さらにInterface Builderの[is Initial Controller]にチェックが入っているWindow Controllerが表示対象)。

f:id:masatoru:20170828125045p:plain

この起動時に表示されるインターフェイスをコードで変更する方法です。

そもそもストーリーボードの使い方がよくわかってない*1のでストーリーボードの追加方法から。

  • ストーリーボードを追加する

    • ファイル→新しいファイル→ストーリーボードでSub.storyboardを作成する
    • 作成したSub.storyboardをInterfaceBuilderで開いてWindowControllerを追加する
    • InterfaceBuilderでWindowControllerを選択→IdentityInspectorを選択→StoryboardIDにMainWindowと入力する
    • 保存する

f:id:masatoru:20170828173725p:plain

  • AppDelegate.csでDidFinishLaunchingメソッドをオーバーライド*2して以下を追加する*3
public override void DidFinishLaunching(NSNotification notification)
{
    // 呼んじゃだめ!!
    //base.DidFinishLaunching(notification);

    // StoryBoardを呼び出し
    var storyboard = NSStoryboard.FromName("Sub", null);

    // StoryBoardからViewControllerを呼び出し
    var controller = storyboard.InstantiateControllerWithIdentifier("MainWindow") as NSWindowController;

    // 表示
    controller.ShowWindow(this);
}

以上っす。

*1:Ctrlキー押しながらdragとか無理っと指つりながら作業してます。

*2:インテリセンスで入力するとbase.DidFinishLaunching()も追加されるけどこれは呼んじゃだめ(「陰」のP24参照)

*3:コードは Working with Storyboards - XamarinのLoading from codeそのまま。

iOSのSimulatorからParallels越しのVisualStudioで起動したASP.NETを参照する方法

バックエンドのテストをしていて、Parallels越しのWindowsのVisualStudioで立ち上げたASP.NETIIS)を、XamarinStudioでビルドしたiOSアプリから参照する方法がわからなかったので調べてみました。

www.barelycompetent.co.za

VisualStudioでASP.NETを起動するとローカルの任意のポート(例:http://localhost:10897/)で実行されます。 このポートはWindowsの外側からは参照できないのでMac側のiOSのSimulator(やAndroidのEmulator)からは使用することができません。そこでSharpProxyというソフトを使ってポート番号を切り替える(すり替える?)ことで実現しています。以下その手順になります。

  • WIndows側でSharpProxyをクローンしてVisualStudioでビルド&起動します。
git clone https://github.com/jocull/SharpProxy.git
  • Windows側のVisualStudioでASP.NETを起動してポート番号を確認します(例:http://localhost:10897/)。

  • WindowsIPアドレスをipconfigで調べます(例:172.xx.xx.xx)。SharpProxyでも[Your IP Address]で表示されています。

  • SharpProxyの[InternalPort]に起動したASP.NETのポート番号(10897)を入力して[Start]をクリックします。

f:id:masatoru:20170404205444p:plain

これでiOS側からParallels越しのWindowsASP.NETを参照できます。

他にもっとスマートな方法があればぜひ教えて下さい。

追伸: Xamarinはいいぞ!!(今回はXamarinに限った話じゃないけど)

Xamarin.Formsで縦組み

Xamarin Advent Calendar 2016(その2)の21日目です。

はじめに

10月にJXUGC #17 お前の Xamarin アプリを見せてみろ!に登壇させていただいたのでそのまとめです。

goo.gl

なぜXamarin.Formsで縦組みか

最近のHTML+CSS縦組み機能が充実しており大抵のブラウザで表示可能です。ただしブラウザによってその実装はまちまちで、端末に依存することなく同じイメージで表示したいというのが始めたきっかけです。自分で実装することで例えば左にルビを表示する(そんな人はほとんどいないでしょうけど)など自由に機能追加できることが(当たり前ですが)魅力の一つです。

SkiaSharp

縦組みの描画は2Dの描画フレームワークSkiaSharpを使用しています。Xamarinが推奨してるわりに日本語情報は少ないのですが、最近になって記事も増えてきたので今後に期待しています。今回のサンプルではRendererを使ってAndroidiOSをそれぞれ実装していますが、最新バージョンではXamarin.Formsに対応しています。ということで早く書き直さないと(汗)。

日本語フォント

日本語フォントを表示するためにIPAIPAex明朝フォントを使用しています。サイトからダウンロードしたipaexm.ttfをリソースフォルダ(AndroidはAssetsフォルダ、iOSはResourcesフォルダ)へ追加しています。

縦組みエンジン(Hanako)

タグの読み込みと組版(Compose)はHanakoという縦組みエンジンで実装しています。この中で行末の折り返しやページごとに組版をおこなうといった処理をおこなっています。

縦組み描画コントロール(TategumiView)

組版されたページをTategumiViewコントロールで表示しています。SkiaSharpで用意されたCanvasにDrawTextで1文字ずつXYを指定して描画しています。

//文字の描画
static void drawHonbunChar(SKCanvas canvas, SKPaint paint, WaterTrans.TypeLoader.TypefaceInfo tfi, HKWaxBase ch)
{
  //文字をGlyphIdに変換
  var glyphs = stringToVerticalGlyphs(ch.Char, paint, tfi);
  //描画の文字サイズを設定
  paint.TextSize = ch.FontSize;
  //本文を描画
  drawText(canvas, glyphs, ch.DevX, ch.DevY, paint);
}
static unsafe void drawText(SKCanvas canvas, ushort[] glyphs, float x, float y, SKPaint paint)
{
  paint.TextEncoding = SKTextEncoding.GlyphId;
  fixed (ushort* p = glyphs)
  {
    canvas.DrawText((IntPtr)p, glyphs.Length * 2, x, y, paint);
  }
}

縦組み用の字形

上記の処理でstringやcharではなくGlyphIdで描画していますが、拗促音(ゃゅょっ)や括弧は縦組み用の字形に切り替える必要があり、これは@espresso3389さんが作った(というかこのために作ってもらった)WaterTrans.TypeLoaderを使用していて、ここでstringをGlyphIdに変換しています。

//縦組み用の文字に変換している
static ushort[] stringToVerticalGlyphs(string text, SKPaint paint
  , WaterTrans.TypeLoader.TypefaceInfo typefaceInfo)
{
  ushort[] glyphs;
  paint.Typeface.CharsToGlyphs(text, out glyphs);
  var conv = typefaceInfo.GetVerticalGlyphConverter();
  for (int i = 0; i < glyphs.Length; i++)
  {
    if (conv.CanConvert(glyphs[i]))
      glyphs[i] = conv.Convert(glyphs[i]);
  }
  return glyphs;
}

XAML

作成したTategumiViewをXAMLで配置してます。WidthやHeightをBindingしていてViewの向きが変わると再処理(Compose)→描画(View)処理がおこなわれます。

<local:TategumiView VerticalOptions="FillAndExpand"
 HorizontalOptions="FillAndExpand"
 Width="{Binding TateviewWidth.Value}"
 Height="{Binding TateviewHeight.Value}"
 CurrentPage="{Binding CurrentPage.Value}"
 PageIndex="{Binding Path=PageIndex.Value,Mode=TwoWay}">

データ形式

HTML形式で<p>タグと<ruby>タグを読み込むようにしています。タグのパースはHtmlAgilityPackを使用しています。

<p><ruby>私<rt>わたくし</rt></ruby>はその人を...</p>

サンプルデータ

青空文庫から直接スクレイピングしようとしたのですが、文字コードやタグの使い方が統一されていなかったり(HtmlAgilityPackのPCLではXPATHが使えなかったり)できれいに取得できなかったので、元のHTMLからルビと本文だけのHTMLに加工しました。作成したHTMLリソースフォルダ(AndroidのAssetフォルダ、iOSのResoucesそれぞれ)に置いて読み込む形になっています。

表示!!

目次から書名をタップするとこんな感じで表示されます。左右のタップでページ移動します。

f:id:masatoru:20161220184545p:plain

GitHub

というわけで(不十分なところが多々ありますが)今回の縦組みサンプルをGitHubに上げました(現在Android版のみ動作します。iOSでなぜか動かないので調査中です)。奇特な方々の優しいダメだしを頂ければ大変幸いです。

最後に

今年もXamarinおよびXamariの関係者の方々と楽しく時間を過ごせました。ありがとうございました。来年もより一層よろしくお願いします。よいお年を。

.Net Framework2.0から4.6.1に上げたけどSystem.BadImageFormatExceptionで動かない

昔のプログラムを.Net Framework2.0から4.6.1に上げたけどSystem.BadImageFormatExceptionとかで動作しない。

下記をチェックしたけどどれも該当しない。

例外のトラブルシューティング : System.BadImageFormatException

わかったことはメインのexeのapp.configに

<supportedRuntime version="v2.0.50727"/>>

とあったのでこれを下記に書き換えたら動いた。

<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>

すっかりはまってしまった。

DbSetのAddをMockでTestする

書きなれないブログでタイトルからつまづいてますが。。。
EF6をServiceとMockを使っていい感じでテスト&モッキューできないかなと。
元ネタはなかじさんのブログ。

blog.nakajix.jp

ここの記事にあるEFのTestをMoqを使ってやる記事があります。

msdn.microsoft.com

この中の「Testing query scenarios」が、サンプルデータをServiceを通してGetAllBlogsで取得するとorder byされて返ってくるというTESTです。

[TestMethod] 
public void GetAllBlogs_orders_by_name() 
{ 
    var data = new List<Blog> 
    { 
        new Blog { Name = "BBB" }, 
        new Blog { Name = "ZZZ" }, 
        new Blog { Name = "AAA" }, 
    }.AsQueryable(); 

    var mockSet = new Mock<DbSet<Blog>>(); 
    mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(data.Provider); 
    mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression); 
    mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType); 
    mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(0 => data.GetEnumerator()); 

    var mockContext = new Mock<BloggingContext>(); 
    mockContext.Setup(c => c.Blogs).Returns(mockSet.Object); 

    var service = new BlogService(mockContext.Object); 
    var blogs = service.GetAllBlogs(); 

    Assert.AreEqual(3, blogs.Count); 
    Assert.AreEqual("AAA", blogs[0].Name); 
    Assert.AreEqual("BBB", blogs[1].Name); 
    Assert.AreEqual("ZZZ", blogs[2].Name); 
} 

次にServiceでAddBlogすると追加されるTestをおこなうために下記を追加。
ここでなぜか(当然?)4件にならない、ということで本題に入ります。

service.AddBlog("CCC", "http://blogs.msdn.com/adonet");
var blogs = service.GetAllBlogs(); 
Assert.AreEqual(3, blogs.Count); //4件にならない

ServiceでAddBlogしたときに追加されるようにしたいわけですが、見つけたのが下記のAnswerの箇所。
要するに、DbSetでAddされる時のMockがないので追加されないとのこと。

stackoverflow.com

...Callbackがよくわからないのでちょっと勉強してから。

github.com

というわけで試行錯誤した結果一番最後の行ができたやつ。

//コピペのままではコンパイルされず
//mockSet.Setup(m => m.Add(It.IsAny<Blog>())).Callback(blog => data.Add(blog));

//IQueryrableにAddはない
//mockSet.Setup(m => m.Add(It.IsAny<Blog>())).Callback<Blog>(blog => data.Add(blog));   

//AddがないのでConcatで配列を作り直す→なぜかダメ
//mockSet.Setup(m => m.Add(It.IsAny<Blog>())).Callback((Blog blog) => data = data.Concat<Blog>(new[] { blog }))

//ListとIQueryableを上で使い分ける
mockSet.Setup(d => d.Add(It.IsAny<Blog>())).Callback<Blog>((s) => source.Add(s));

動いたソースを一応全部のっけておきます。

[Test]
public void GetAllBlogs_with_mock()
{
  var source = new List<Blog>
  {
    new Blog { Name = "BBB" },
    new Blog { Name = "ZZZ" },
    new Blog { Name = "AAA" },
  };
  var data = source.AsQueryable();
  
  var mockSet = new Mock<DbSet<Blog>>();
  mockSet.As<IQueryable<Blog>>().Setup(m => m.Provider).Returns(data.Provider);
  mockSet.As<IQueryable<Blog>>().Setup(m => m.Expression).Returns(data.Expression);
  mockSet.As<IQueryable<Blog>>().Setup(m => m.ElementType).Returns(data.ElementType);
  mockSet.As<IQueryable<Blog>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());

  //DbSetのAddをモックする
  //ListとIQueryableを上で使い分ける(これは仕方ない?ここをもうちょいきれいにしたい)
  mockSet.Setup(d => d.Add(It.IsAny<Blog>())).Callback<Blog>((s) => source.Add(s));
  
  var mockContext = new Mock<BloggingContext>();
  mockContext.Setup(c => c.Blogs).Returns(mockSet.Object);
  
  var service = new BlogService(mockContext.Object);
  var blogs = service.GetAllBlogs();
  
  Assert.AreEqual(3, blogs.Count,"追加前");
  Assert.AreEqual("AAA", blogs[0].Name);
  Assert.AreEqual("BBB", blogs[1].Name);
  Assert.AreEqual("ZZZ", blogs[2].Name);
  
  //もう一件追加する
  service.AddBlog("CCC", "http://blogs.msdn.com/adonet");
  blogs = service.GetAllBlogs();

  Assert.AreEqual(4, blogs.Count, "追加後");
  Assert.AreEqual("AAA", blogs[0].Name);
  Assert.AreEqual("BBB", blogs[1].Name);
  Assert.AreEqual("CCC", blogs[2].Name);
  Assert.AreEqual("ZZZ", blogs[3].Name);
}

で、下記に回答がありましたというオチです。

stackoverflow.com

追伸、 もっきゅーもっきゅーって居酒屋のメニューにしか聞こえないのは自分だけ?

Bitmapとbyte[]の変換

いざ書くといつも忘れてしまうので。

画像ファイル(Bitmap)からbyte[]に変換

Bitmap bmp = new Bitmap(画像のPATH);  //using System.Drawing
MemoryStream ms = new MemoryStream();
bmp.Save(ms,ImageFormat.Png);   //using System.Drawing.Imaging;

byte[]から画像ファイル(Bitmap)に変換

MemoryStream ms = new MemoryStream(byte[]のデータ);
Bitmap bmp = new Bitmap(ms);
ms.Close();
bmp.Save(画像のPATH);

byte[]って何者かいまだによく知らない。