Visual Studio 2017+Eto.Formsでクロスプラットフォームアプリを作る際のコツ

# 概要
　タイトルの通りの記事です。次の記事で紹介されていて興味を持ったのと、今でも更新され続けているので信頼できると思ったからですね。
　　[クロスプラットフォームなGUIフレームワーク「Eto.Forms」の紹介 - ごった日記](http://mokake.hatenablog.com/entry/2016/12/25/120259)

# 導入
　Visual Studioを起動した後、初手で「ツール(T)→拡張機能と更新プログラム(U)...」を選択して「拡張機能と更新プログラム」ウィンドウを開き、「オンライン」タブから[Eto.Forms](https://marketplace.visualstudio.com/items?itemName=CurtisWensley.EtoFormsVisualStudioAddin)を導入します。
　具体的には、次のウィンドウで「ダウンロード」ボタンを押してダウンロードした後、 **VisualStudioを再起動** すればEto.Formsが導入されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/d9ecd743-1850-e359-8d4b-f4c3967f7dc9.png)

　導入後は、新しいプロジェクトを作成する際に、Eto.Formsを利用したプロジェクトをテンプレートとして選択することができます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/4682eee0-481f-fab1-569c-d28dd6c31314.png)

　プロジェクトの設定画面ですが、私はWPF使いなので、「XAML」でFormを編集することにしました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/02283777-1e09-c592-198f-22b537b91e47.png)

# ファイル構造をざっと説明

　まずフォルダパスがこんな感じです。

- ルートディレクトリにある「HDC2」はアプリ名ですので、プロジェクト名を他の名前にすれば当然別の名前になります
- ルートディレクトリの1個下に、共通コードである「HDC2」フォルダと、UI用の「HDC2.Desktop」フォルダが生成されます。「HDC2」の単語の意味は前述したので説明は省略
- HDC2フォルダは共通(.NET Standard製)ですが、HDC2.Desktopフォルダは見るからに **プラットフォーム毎に別バイナリ** になりそうです

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/d2f4342d-33df-7a61-ddcd-91f91e1d1685.png)

　次に実行ファイルですが、上記の図で言えば「`HDC2\HDC2.Desktop\bin\Debug\net461`」フォルダ内に「`HDC2.Desktop.exe`」といった名前で収められています(リリースビルドだと`Debug`が`Release`になる)。
　フォルダ比較したところ、「`HDC2\HDC2.Desktop\bin\Debug\net461`」フォルダの中身は「`HDC2\HDC2.Desktop\bin\Debug\net461\HDC2.Desktop.app\Contents\MonoBundle`」フォルダとほぼ同じだと分かりました……重複して存在する意味は？？

# 「XAML」って選んだけどどれぐらいWPFに近いの？

　……確かにXAMLですが、サンプルがサンプルなせいで **コードビハインド前提** のように見えます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/d6a84b78-9387-9ada-3d99-9912fa6e8725.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/29802/ef786865-aa76-32f7-739b-8f83c830c8b0.png)

　**XAMLでMVVMを使うのは当然** なのでどう対策するかですが、実は「`INotifyPropertyChanged`を継承した`ViewModel`を`DataContext`に宛がう」といったベタな戦法で対処することができます。
　　[Data Binding · picoe/Eto Wiki](https://github.com/picoe/Eto/wiki/Data-Binding)　

　テンプレートに載っているサンプルコードを、そのままMVVMモデルとして使えるように書き直すとこんな感じ。
　**※WPFと違い、メニュー部分はコードビハインドする必要があります**

```MainForm.xeto.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 本当は「MainForm.xeto」ってファイル名です -->
<Form
	xmlns="http://schema.picoe.ca/eto.forms" 
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	Title="My Eto Form"
	ClientSize="400, 350"
	Padding="10"
	>
	<StackLayout>
		<Label Text="{Binding LabelText}"/>
		<Button Text="ClickMe" Command="{Binding ClickMeCommand}"/>
	</StackLayout>

	<Form.Menu>
		<MenuBar>
			<ButtonMenuItem Text="F&amp;ile">
				<ButtonMenuItem Text="Click Me!" Click="HandleClickMe" />
			</ButtonMenuItem>
			<MenuBar.ApplicationItems>
				<ButtonMenuItem Text="Preferences.." Shortcut="{On Control+O, Mac=Application+Comma}" />
			</MenuBar.ApplicationItems>
			<MenuBar.QuitItem>
				<ButtonMenuItem Text="Quit" Shortcut="CommonModifier+Q" Click="HandleQuit" />
			</MenuBar.QuitItem>
			<MenuBar.AboutItem>
				<ButtonMenuItem Text="About..." Click="HandleAbout" />
			</MenuBar.AboutItem>
		</MenuBar>
	</Form.Menu>
	<Form.ToolBar>
		<ToolBar>
			<ButtonToolItem Text="Click Me!" Click="HandleClickMe" />
		</ToolBar>
	</Form.ToolBar>
</Form>
```

```MainForm.xeto.cs
using System;
using Eto.Forms;
using Eto.Serialization.Xaml;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace HDC2
{	
	public class MainForm : Form
	{
		public MainForm() {
			XamlReader.Load(this);
			// ViewModelを登録する
			var model = new MainModel();
			DataContext = model;
		}

		protected void HandleClickMe(object sender, EventArgs e)
		{
			// メニュー(ButtonMenuItem)のCommandプロパティに「"{Binding ClickMeCommand}"」と
			// 設定しても動かなかったので、コードビハインドに見せかけてDataContextを直接叩く荒業
			(DataContext as MainModel).ClickMeCommand.Execute(sender);
		}

		protected void HandleAbout(object sender, EventArgs e)
		{
			new AboutDialog().ShowDialog(this);
		}

		protected void HandleQuit(object sender, EventArgs e)
		{
			Application.Instance.Quit();
		}
	}

	public class MainModel : INotifyPropertyChanged
	{
		// プロパティ
		public ICommand ClickMeCommand { get; private set; }
		public string LabelText { get; private set; }

		// 実行するメソッド
		private void ClickMe(object sender, EventArgs e) {
			MessageBox.Show("I was clicked!");
		}

		private void OnPropertyChanged([CallerMemberName] string memberName = null) {
			PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(memberName));
		}
		public event PropertyChangedEventHandler PropertyChanged;

		// コンストラクタ
		public MainModel() {
			ClickMeCommand = new Command(ClickMe);
			LabelText = "Some Content";
		}
	}
}
```

# ReactivePropertyが使えるか？

　MVVM支援用ライブラリとして個人的に重宝しているものとして **[ReactiveProperty](https://github.com/runceel/ReactiveProperty/blob/master/README-ja.md)** があります。
　　[【C#】ReactiveProperty全然分からねぇ！って人向けのFAQ集【修正済】](https://qiita.com/YSRKEN/items/5a36fb8071104a989fb8)

　Eto.Formsはフレームワークとして[.NET Standard](https://msdn.microsoft.com/ja-jp/magazine/mt842506.aspx)を使用していますが、ReactivePropertyも同様に.NET Standardに対応しています。
　ゆえに使えるはず……と思って試してみたところ、 **あっさり成功しました。** 変更箇所はこんな感じです。ちなみに**ReactiveCollectionもちゃんと使えます**。

```MainForm.xeto.xml
<!-- 本当は「MainForm.xeto」ってファイル名です -->
<Label Text="{Binding LabelText}"/>
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
<Label Text="{Binding LabelText.Value}"/>
```

```MainForm.xeto.cs
public ICommand ClickMeCommand { get; private set; }
public string LabelText { get; private set; }
private void ClickMe(object sender, EventArgs e) {
	MessageBox.Show("I was clicked!");
}
public MainModel() {
	ClickMeCommand = new Command(ClickMe);
	LabelText = "Some Content";
}
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
public ReactiveCommand ClickMeCommand { get; }
public ReactiveProperty<string> LabelText { get; } = new ReactiveProperty<string>("Some Content");
private void ClickMe() {
	MessageBox.Show("I was clicked!");
}
public MainModel() {
	ClickMeCommand = new ReactiveCommand();
	lickMeCommand.Subscribe(ClickMe);
}
```

# Windowsで開発する際の注意点
　Eto.Formsでは、「共通コード」を実現するために裏で「WPF」や「Gtk」などのフレームワークが動いています。
　その辛みで、**プロパティの値を変更する際に例外が出てしまう**といったとんでもない事案を見つけました。
　　※現象が再現する最小コードを作って投げたIssue→[Raise an exception when overwrite the value of ReactiveProperty.Value not in constructor method · Issue #1041 · picoe/Eto](https://github.com/picoe/Eto/issues/1041)
　ところが、この問題について「**[Eto.Forms は Gtk しか SynchronizationContext に対応していないみたいなので自前でセットしたら動きました](https://twitter.com/oigami013/status/970985367426904064)**」との助言を頂きました。つまり、`Eto.Desktop`における`Program.cs`について、

```Program.cs
[STAThread]
static void Main(string[] args)
{
	new Application(Eto.Platform.Detect).Run(new MainForm());
}
```

とあるところを、

```Program.cs
[STAThread]
static void Main(string[] args)
{
	var app = new Application(Eto.Platform.Detect);
	if (app.Platform.IsWpf) {
		SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext());
	}
	app.Run(new MainForm());
}
```

と書き換えてしまえば正常に動くというのです。実際試したら**例外も出ず普通に動くのですがそれはorz**

# 右クリックメニュー(コンテキストメニュー)の仕込み方
　試行錯誤して見つけたのですが、WPFのノリに似た感じでした。以下は`ListBox`での例です。

```sample.xeto.xml
<!-- 本当は「sample.xeto」ってファイル名です -->
<ListBox/>
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
<ListBox>
	<ListBox.ContextMenu>
		<ContextMenu>
			<ButtonMenuItem Text="右クリックメニュー" Click="SampleFunc" />
		</ContextMenu>
	</ListBox.ContextMenu>
</ListBox>
```

　なお、後述する**メニューにCommandをBindingできない不具合**はこのコンテキストメニューでも同様ですので注意！

# ドラッグ＆ドロップの仕込み方
　**なんでググってもこれぐらいのことのサンプルソース出てこないんだよ**と思いながら試行錯誤して見つけました。
　以下、ListBoxでの例です(他のコントロールでも可能)。

```sample.xeto.xml
<!-- 本当は「sample.xeto」ってファイル名です -->
<ListBox/>
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
<ListBox AllowDrop="True" DragDrop="DrapDropEvent"/>
```

```sample.xeto.cs
protected void DrapDropEvent(object sender, DragEventArgs e) {
	// ファイル名一覧をforeachで回しながら、絶対パス名を表示していく
	foreach(var fileUri in e.Data.Uris) {
		Console.WriteLine(fileUri.LocalPath);
	}
}
```
　ただし、Eto.Formsで言えば[Ver.2.4.0から](http://api.etoforms.picoe.ca/html/P_Eto_Forms_DragEventArgs_Data.htm)という比較的新しい機能なせいか、**Linuxではまだ動かない**のですけれどねorz

# その他小ネタ集

## メニューアイテムに関する恐ろしい罠について

　[Eto.Formsプルダウンメニューの翻訳対応 - ごった日記](http://mokake.hatenablog.com/entry/2017/09/23/192310)
　この記事によるととんでもないことが書かれています。つまり、

- `MenuBar.ApplicationItems`や`MenuBar.QuitItem`を使用すると、`File`メニューが勝手に追加される
- `MenuBar.AboutItem`を使用すると、`Help`メニューが勝手に追加される
- `File`メニューが無いとLinuxではアプリが強制終了する

　……明らかに **ローカライズのことを考えてないクソ仕様** だと思いますが、幸いにも「コードビハインドで上書きする」技が使えます。つまり、何食わぬ顔でXAMLを読み込んだ後、当該文字列をダイレクトに書き換えてやればいいのです。

```MainForm.xeto.cs
public MainForm() {
	XamlReader.Load(this);
	// メニューバーを書き変える
	this.Menu.ApplicationMenu.Text = "ファイル(&F)";
	this.Menu.HelpMenu.Text = "ヘルプ(&H)";
	// ViewModelを登録する
	var model = new MainModel();
	DataContext = model;
}
```

## メニュー項目に対するBindingが一部効かない件について
　例えば`ButtonMenuItem`における`Command`プロパティに`Command="Binding SampleCommand"`と**セットしても無駄でした**。つまりコードビハインドでないとメニュー関係は処理できないわけで、看板に偽りありとしか言いようがありませんね。
　なお`Button`とかの`Command`プロパティにはBindingできる謎……。

## About(バージョン情報欄)について
　Eto.Formsのサンプルを読めば気づくと思いますが、`AboutDialog`型のインスタンスに対して`ShowDialog(Window型)`メソッドを発動することにより、バージョン情報のダイアログを表示することができます。
　ソフト名や概要などの設定項目は、プロジェクトのプロパティから「パッケージ」を選ぶことによってある程度は編集できますが、たぶん **直接AboutDialog型のプロパティを弄った方が楽です** 。

![image](https://user-images.githubusercontent.com/3734392/36935541-7468c89c-1f3c-11e8-9219-9b190c9f8d28.png)

## UIの癖の違いについて
　全体的に見て、表現能力は**WPFより明らかに見劣りします**。
　例えば`<TableLayout>`はWPFで言うところの`<Grid>`っぽいコンテナですが、`<Grid>`では`Grid.Column`および`Grid.Row`プロパティで位置決めしていたものを、`<TableLayout>`では``<TableRow>`の中に1行づつコンテナを詰め込む方向の実装になります。また、**ColumnSpanやRowSpanが無い**ので、それっぽいものを作りたい場合は`<StackLayout>`をネストさせて組むしかありません。
　ところが、様々なオブジェクト・コンテナに指定する`Spacing`および`Padding`プロパティは、WPFの`Marin`および`Padding`プロパティと違い、**周囲を同じピクセル数でしか空けることができません**。一応`"10,5"`のようにカンマ区切りで2個の引数を取れるものもありますがそれでも2個まで。その結果、かなりレイアウト構築に制限を受けることになります。
　しかも、これは[おま環](https://hack-le.com/omakan/)かもしれませんが、レイアウトを表すXMLファイル(`*.xeto`)を標準のテキストエディター(Eto.Forms preview)で開いて作業していると、**しばらくしたらVisualStudioごとフリーズし、強制終了を余儀なくされます**。上記のように**プレビュー欄が何の役にも立たない**くせにこの体たらくですので、編集する際は「XML(テキスト)エディター」を使用することを推奨します。
　とはいえ、**Eto.Forms previewじゃないと使えないプロパティがIntelliSenseで表示されることがある**といったふざけた仕様により、Eto.Forms previewの使用を余儀なくされることはあるでしょうorz

![DXYBmoZU0AAWgGL-orig.jpg](https://qiita-image-store.s3.amazonaws.com/0/29802/743513dd-5d4f-6c79-34a5-c505a13f4101.jpeg)

## Linuxで動かす際の注意事項
　Linuxで動かす際は、**[Mono](http://www.mono-project.com)**を利用してビルド後のexeファイルを動かします。
　ただ、その辺のUbuntu 16.04 LTSなどで何気なく「sudo apt-get install mono-complete」としただけだと、**最新のMonoじゃないのでEto.Forms製アプリが動かない**といったドツボにはまります。
　したがって、[公式に記載された手順に従い](http://www.mono-project.com/download/stable/#download-lin)、事前にリポジトリを追加してからインストールする必要があります(以下はUbuntu 16.04の場合)。

```terminal
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF
echo "deb http://download.mono-project.com/repo/ubuntu stable-xenial main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list
sudo apt-get update
```

## Listboxにどう対応するか？
　`Label`コントロールや`Button`コントロールにおける`Text`プロパティや、`ImageView`コントロールにおける`Image`プロパティのように、1要素でOKなプロパティについては`ReactiveProperty<Hoge>`型を割り当てることができます。
　また、`ListBox`コントロールにおける`DataStore`プロパティのように、複数要素を割り当てるプロパティには`ReactiveCollection<Hoge>`型を割り当てることができます。ただ、**WPFとオブジェクト名やプロパティ名が一部異なっているので覚えるのが大変**ですわ……。

## 画像データの加工について
　`System.Drawing.Bitmap`のような名前の`Eto.Drawing.Bitmap`型がありますので、そこから`Eto.Drawing.Graphics`型のインスタンスを引き出して叩くのが基本です。画像に画像を合成したい時はこんな感じ。

```BitmapSample.cs
// 画像データを用意する
var image1 = new Eto.Drawing.Bitmap(w1, h2, PixelFormat.Format24bppRgb);
var image2 = new Eto.Drawing.Bitmap(w2, h2, PixelFormat.Format24bppRgb);
// Eto.Drawing.Graphics型はIDisposeを継承しているのでusingが使える
using(var g = new Eto.Drawing.Graphics(image1)){
  // image2から切り出す範囲を指定(下記例だと左上座標は(x,y)、幅w3・高さh3の範囲)
  var cropRect = new Eto.Drawing.Rectangle(x, y, w3, h3);
  // tempImageはimage2から切り出された範囲
  var tempImage = image2.Clone(cropRect);
  // image1の指定した位置(x2,y2)にtempImageを貼り付ける
  // ちなみにGraphics.DrawImage(Image, float x, float y)メソッドは
  // 見ての通り第2・第3引数がなぜか実数だが、整数を入れてもキャストされるので
  // 問題なく使用できる
  g.DrawImage(tempImage, x2, y2);
}
```

# 参考資料

- [クロスプラットフォームなGUIフレームワーク「Eto.Forms」の紹介](http://mokake.hatenablog.com/entry/2016/12/25/120259)