チャレンジ！クロスプラットフォーム開発（Eto.Forms編）

この記事は[リンク情報システム](https://www.lis.co.jp/)の[2021新春アドベントカレンダー Tech Connect](https://qiita.com/dgkz/items/eabde6515457d8acec9b)のリレー記事です。
engineer.hanzomon のグループメンバによってリレーされます。

※リンク情報システムのSNSです。よければフォローお願いします。
[Facebook](https://ja-jp.facebook.com/lis.co.jp/)、[Twitter](https://twitter.com/liscojp)
#　
寒い日が続いていますが、皆さんはいかがお過ごしでしょうか。
本日1月25日は『中華まんの日』です。

#1. はじめに
複数の異なる仕様の機種やOSで同じように動作するソフトウェアの作成を目指すクロスプラットフォーム開発。近年は新たなフレームワークが続々登場し、選択肢が増えて嬉しいかぎりですね。
今回はそうした中から Eto.Forms に挑戦します！

#2. Eto.Formsとは？
Eto.Formsは、MonoおよびXamarinを基盤としたクロスプラットフォームGUIフレームワークです。
単一のUIコードベースで、全ての対応プラットフォームで動作するネイティブアプリケーションを開発できるのが特徴です(もちろんXAMLを使うこともできます)。
プラットフォーム毎のUIの差異を吸収するため、各フレームワークは様々なアプローチを試みていますが、
「フレームワークの独自UIでなくネイティブなGUI要素を使いたい！」「XAMLはどうも好きになれないなぁ……」といった場合に
Eto.Formsは良い選択肢の一つとなるでしょう。

#3. Eto.Formsを試してみよう
VisualStudio上でEto.Formsを利用することは簡単にできます。さっそく試してみましょう。


##3.1. 準備
最初に開発環境を整えます。

- VisualStudio

VisualStudio2017以降が必要です。
VisualStudioInstallerのワークロードにて「.NETによるモバイル開発」にチェックを入れます。

- .NET Framework

.NET Framework 4.5以上が必要です。
必要ならば、[Download .NET SDKs for Visual Studio](https://aka.ms/msbuild/developerpacks)から予めインストールしておきましょう。

- Mono

LinuxやMac上で.NETアプリケーションを動かすためには Monoランタイム3.0以上が必要です。
[Download Mono](https://www.mono-project.com/download/stable/#download-lin)に記述されたプラットフォーム毎の手順に従い、インストールしておきましょう。

- Eto.Forms

Eto.Forms Visual Studio Addin をインストールする必要があります。
下記の箇所から「Eto」で検索し、NuGetパッケージマネージャからアドインを追加しましょう。
VisualStudio2017の場合、「ツール(T)」→「拡張機能と更新プログラム(U)」から
VisualStudio2019の場合、「拡張機能(X)」→「拡張機能の管理(M)」から
2021年01月現在の最新版は2.5.9でした。

##3.2. プロジェクトの作成
準備ができたら、いよいよ作成開始です。

VisualStudioを起動したら、「ファイル(F)」→「新規作成(N)」→「プロジェクト(P)」の順に選択し、新しいプロジェクトを作成します。
新しいプロジェクトの作成ダイアログにて「Eto」で検索し、「Eto.Forms Application」のプロジェクトテンプレートを選択します。
任意のプロジェクト名をつけましょう(記事内では「EtoApp1」とします)。
「Eto.Forms Application Properties」ダイアログが表示されます。

「Framework」ドロップダウンリスト
開発ターゲットを選択します(.Net Framework 4.xx/.Net Core/.Net 5)。
今回は.Net Framework 4.7.2にしました(プラットフォームによっては.NET Framework 4.7以降が必須の場合があるため)。

「Launcher」ラジオボタン
自動でプラットフォームを判別するSingleと、スタートアップルーチンを各プラットフォーム毎に分離するSeparateとが選べます。
それぞれのプラットフォームに依存したライブラリを利用する予定があるなら、Separateを選びましょう。
Singleを選んだ場合、WindowsではWPFモジュールが起動します。
今回はSeparateにしました。

「Include Xamarin.Mac Project」チェックボックス
チェックを入れるとMono形式に加えてXamarin形式のプロジェクトを生成してくれるらしいです。
チェックを入れなくてもMac用のソースコードは生成されます。

「Form」ラジオボタン
レイアウトの生成形式を選択します。

| 生成形式        | 概要| 
|:----------------------|:------------|
| Code　               | デスクトップアプリのような、メインページのみを持つアプリのサンプルプロジェクトです。|
| Xaml                 | レイアウトをXAML形式で保存します。|
| Json　               | レイアウトをJSON形式で保存します。|
| Code Preview         | レイアウトやイベントハンドラを別ファイルに分離します。プレビュー可能です。|



以下のような構成でソリューションが作成されました。

・EtoApp1
プラットフォーム特有の機能を使わない共有ロジックを記述するプロジェクトです。

・EtoApp1.Gtk
・EtoApp1.Mac
・EtoApp1.Wpf
各プラットフォーム専用のネイティブAPIを呼び出すコードを記述するプロジェクトです。
初期状態ではスタートアップ用のコードのみ組み込まれています。
アプリケーションの雛型プロジェクトが完成しました！

##3.3. プロジェクトの追加
残念ながらテンプレートに含まれているのはGtk・Mac・Wpfの3プラットフォームのみです。
いったんVisualStudioを終了し、他のプラットフォームを追加しましょう。

### 3.3.1. Win
(1) 通常のWindowsアプリケーション用のプロジェクトを作成します。
「EtoApp1.Wpf」フォルダをまるごとコピーし、「EtoApp1.Win」に名前を変更します。
「EtoApp1.Win」直下のbin・objフォルダを削除します。
「EtoApp1.Wpf.csproj」ファイルを「EtoApp1.Win.csproj」にリネームします。
「EtoApp1.Win.csproj」内のPlatform文字列を「Eto.Platform.Windows」に置換します。

```C#:EtoApp1.Win.csproj
  <ItemGroup>
    <PackageReference Include="Eto.Platform.Windows" Version="2.5.9" />	//Windowsに変更
  </ItemGroup>
 ```

「Program.cs」を以下のように書き換えます。

```C#:Program.cs
using System;
using Eto.Forms;

//namespace EtoApp1.Wpf
namespace EtoApp1.Win
{
	class MainClass
	{
		[STAThread]
		public static void Main(string[] args)
		{
			//new Application(Eto.Platforms.Wpf).Run(new MainForm());
			new Application(Eto.Platforms.WinForms).Run(new MainForm());
		}
	}
}
 ```

(2) ソリューションへWindowsアプリケーション用のプロジェクトを追加します。
VisualStudioを起動し、ソリューションエクスプローラにてEtoApp1ソリューションを選択し、
右クリックメニュー→「追加(D)」→「既存のプロジェクト(E)...」の順に選択します。
先ほど作った「EtoApp1.Win.csproj」を選択し、「開く」ボタンを押下します。

最後にビルドしてエラーが出ないことを確認しましょう。
これでWindowsデスクトップ用の雛型プロジェクトが完成しました！

### 3.3.2. Gtk
(1) GTK用のプロジェクトを作成します。
アイエエエ!? GTK!? モウアルノニGTKナンデ!? 
(アドインで生成したプロジェクトのままだと参照が足らず、実行時エラーになるためです。)

「EtoApp1.Wpf」フォルダをまるごとコピーし、「EtoApp1.Gtk」に名前を変更します。
「EtoApp1.Gtk」直下のbin・objフォルダを削除します。
「EtoApp1.Wpf.csproj」ファイルを「EtoApp1.Gtk.csproj」にリネームします。
「EtoApp1.Gtk.csproj」内のPlatform文字列を「Eto.Platform.Gtk」に置換します。
また、Gtk.dll内部から呼び出されるGtk3.dllへの参照を追加します。


```C#:EtoApp1.Gtk.csproj
<Project Sdk="Microsoft.NET.Sdk">
	
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net472</TargetFramework>
    <Platforms>x86</Platforms>	//「AnyCPU」→「x86」に変更
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\EtoApp1\EtoApp1.csproj" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Eto.Platform.Gtk" Version="2.5.9" />	//Gtkに変更
    <PackageReference Include="Eto.Platform.Gtk3" Version="2.5.9" />	//Gtk3の参照追加
  </ItemGroup>
	
</Project>
 ```

「Program.cs」を以下のように書き換えます。

```C#:Program.cs
using System;
using Eto.Forms;

namespace EtoApp1.Gtk	//Gtkに変更
{
	class MainClass
	{
		[STAThread]
		public static void Main(string[] args)
		{
			//Gtk2
			//new Application(Eto.Platforms.Gtk2).Run(new MainForm());
			//Gtk3
			new Application(Eto.Platforms.Gtk).Run(new MainForm());
		}
	}
}
 ```

(2) ソリューションへWindowsアプリケーション用のプロジェクトを追加します。
VisualStudioを起動し、ソリューションエクスプローラにてEtoApp1ソリューションを選択し、
右クリックメニュー→「追加(D)」→「既存のプロジェクト(E)...」の順に選択します。
先ほど作った「EtoApp1.Gtk.csproj」を選択し、「開く」ボタンを押下します。

最後にビルドしてエラーが出ないことを確認しましょう。
これでGTK用の雛型プロジェクトが完成しました！

※"おま環"かもですが、VisualStudio2019ではEto.Platforms.Gtk2しか動きませんでした。
VisualStudio2017ではEto.Platforms.Gtk2・Eto.Platforms.Gtk3両方とも動くのですが……
実行時にエラーが出る場合、Nugetパッケージマネージャでいったんアンインストール後、再度インストールしてみてください。

※"おま環"かもですが、AnyCPUでビルドした場合、32bit/64bit版共用のモジュールになるはずが機能していません。
構成マネージャでx86用の構成を作ってビルドしたところ、とりあえず実行できるようになりました。

### 3.3.3. iOS
iOS用のプロジェクトも同じノリで作成できますが、プロジェクトファイルのターゲットフレームワークに「MonoTouch」を指定しろとビルド時に怒られます。
「VisualStudio＋.Net Framework」でなく「MonoDevelopIDE＋Mono」「XamarinStudio＋Mono」で開発すればいけるのかもしれません。
プラットフォーム識別子は「Eto.Platforms.Ios」です。


### 3.3.4. Android
Android用のプロジェクトも同じノリで作成できますが、Eto.Platforms.Androidが存在しないためビルド時に怒られます。
プラットフォーム識別子はすでに定義されていますが、まだ開発中のようです。残念！
プラットフォーム識別子は「Eto.Platforms.Android」です。

※Eto.Formsは他のGUIフレームワークと共存させることもできます。
こちらのかたはXamarin.FormsとEto.Formsを悪魔合体させて[7プラットフォーム対応のアプリケーションを作成されてます](http://www.kurigohan.com/article/453909030.html)。すごい！


##3.4 実行
各プラットフォームのプロジェクトが問題なくビルドできたら、実行してみましょう。
Gtk・WinForm・WPFの順です。
![01_Hello.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456694/fa1badf2-40eb-8edc-6bd3-2007b835da38.png)
*** Eto.Formsの世界へようこそ！ ***

### 3.4.1. 画像ビューアー
無事に雛型プロジェクトでつくったアプリケーションを動かすことができたので、今度は画像ビューアーを作ってみます。
「MainForm.cs」を以下のように書き換えます。

```C#:MainForm.cs
using Eto.Drawing;
using Eto.Forms;
using System;

namespace EtoApp1
{
    public partial class MainForm : Form
    {
        ImageView myImage = new ImageView
        {
            BackgroundColor = Color.FromRgb(0)
        };
        Scrollable container;

        public MainForm()
        {
            Title = "Image Viewer";
            MinimumSize = new Size(600, 400);
            BackgroundColor = myImage.BackgroundColor;

            container = new Scrollable
            {
                Content = myImage,
                BackgroundColor = myImage.BackgroundColor,
                MinimumZoom = 1F,
                MaximumZoom = 1F,
                ExpandContentHeight = false,
                ExpandContentWidth = false,
            };
            Content = container;
            //Content = myImage;

            var openCommand = new Command
            {
                MenuText = "開く (&O)",
                ToolBarText = "Open",
                Shortcut = Application.Instance.CommonModifier | Keys.O
            };
            openCommand.Executed += (s, e) => {
                var dialog = new OpenFileDialog();
                if (dialog.ShowDialog(this) != DialogResult.Ok) return;
                try
                {
                    var img = new Bitmap(dialog.FileName);
                    if (img != null)
                    {
                        myImage.Image = img;
                        ClientSize = new Size(
                            img.Width + 2, img.Height + 2);
                        //ClientSize = img.Size;
                        return;
                    }
                }
                catch (Exception) { }
                MessageBox.Show("Error!");
            };

            var quitCommand = new Command { MenuText = "終了 (&E)" };
            quitCommand.Executed += (sender, e) => Application.Instance.Quit();

            var aboutCommand = new Command { MenuText = "About..." };
            aboutCommand.Executed += (sender, e) => MessageBox.Show(this, "About my app...");
            Menu = new MenuBar
            {
                Items =
                {
                    new ButtonMenuItem { Text = "&File", Items = { openCommand } },
                },
                QuitItem = quitCommand,
                AboutItem = aboutCommand
            };
            Menu.ApplicationMenu.Text = "ファイル (&F)";
            Menu.HelpMenu.Text = "ヘルプ (&H)";
        }
    }
}
 ```

完成したら画像を読み込ませてみましょう。
みふねたかし様のフリー素材集サイト『いらすとや』から[サンプル画像](https://www.irasutoya.com/2020/10/blog-post_25.html)をお借りしました。

![02_FileDialog.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456694/d3f00b25-ebfc-d8af-64a7-b2d09b1c2fef.png)
プラットフォームによってファイルダイアログのデザインが全く異なることがわかりますね。

![03_ImageViewer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456694/0a5cb82e-471b-1edc-99d1-61e88800732b.png)
どのプラットフォームでもサンプル画像を表示することができました！

### 3.4.2. 画像コンバーター
今度はOpenCVを組み込んで画像をグレースケールに変換してみます。
まずNugetパッケージマネージャで「EtoApp1」プロジェクトに「OpenCvSharp3-AnyCPU」パッケージを追加します。
次に「MainForm.cs」を以下のように書き換えます(オリジナルの画像を上書きするので注意してください)。

```C#:MainForm.cs
using Eto.Drawing;
using Eto.Forms;
using System;
using OpenCvSharp;

namespace EtoApp1
{
    public partial class MainForm : Form
    {
        ImageView myImage = new ImageView
        {
            BackgroundColor = Color.FromRgb(0)
        };
        Scrollable container;

        public MainForm()
        {
            Title = "Image Viewer";
            MinimumSize = new Eto.Drawing.Size(400, 400);
            BackgroundColor = myImage.BackgroundColor;

            container = new Scrollable
            {
                Content = myImage,
                BackgroundColor = myImage.BackgroundColor,
                MinimumZoom = 1F,
                MaximumZoom = 1F,
                ExpandContentHeight = false,
                ExpandContentWidth = false,
            };
            Content = container;
            //Content = myImage;

            var openCommand = new Command
            {
                MenuText = "開く (&O)",
                ToolBarText = "Open",
                Shortcut = Application.Instance.CommonModifier | Keys.O
            };
            openCommand.Executed += (s, e) => {
                var dialog = new OpenFileDialog();
                if (dialog.ShowDialog(this) != DialogResult.Ok) return;
                try
                {
                    string fname = dialog.FileName;

                    // 画像の読込
                    using (Mat mat = new Mat(fname))
                    // 画像の変換
                    using (Mat matGray = mat.CvtColor(ColorConversionCodes.BGR2GRAY))
                    {
                        // 画像の書込
                        Cv2.ImWrite(fname, matGray);

                        var img = new Bitmap(fname);
                        if (img != null)
                        {
                            myImage.Image = img;
                            ClientSize = new Eto.Drawing.Size(
                                img.Width + 2, img.Height + 2);
                            //ClientSize = img.Size;
                            return;
                        }
                    }

                }
                catch (Exception ex) {
                    Console.WriteLine(ex.Message);
                }
                MessageBox.Show("Error!");
            };

            var quitCommand = new Command { MenuText = "終了 (&E)" };
            quitCommand.Executed += (sender, e) => Application.Instance.Quit();

            var aboutCommand = new Command { MenuText = "About..." };
            aboutCommand.Executed += (sender, e) => MessageBox.Show(this, "About my app...");

            Menu = new MenuBar
            {
                Items =
                {
                    new ButtonMenuItem { Text = "&File", Items = { openCommand } },
                },
                QuitItem = quitCommand,
                AboutItem = aboutCommand
            };
            Menu.ApplicationMenu.Text = "ファイル (&F)";
            Menu.HelpMenu.Text = "ヘルプ (&H)";
        }
    }
}
 ```

完成したら画像を変換してみましょう。
![04_ImageConverter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456694/301e7692-900e-8334-184d-a461732cd647.png)
どのプラットフォームでもサンプル画像を変換することができました！


#4. おわりに
Eto.Formsなどの.NET Standard規格に従ったライブラリは、.NET Framework／.NET Core／Xamarin といった全ての.NET 実装に組み込めるため、いろいろ応用がきいて夢が広がりますね。
マイクロソフト社の開発者向けカンファレンス「Microsoft Build 2020」でXamarin後継の新たなクロスプラットフォームフレームワーク「MAUI(.NET Multi-platform App UI）」が発表されるなど、今年も.Netから目を離せません。

#5. 参考文献
[「ごった日記」(http://mokake.hatenablog.com/archive/category/Eto.Forms)](http://mokake.hatenablog.com/archive/category/Eto.Forms)
[「元PG主婦の備忘録。」(http://www.kurigohan.com/category/26489450-1.html)](http://www.kurigohan.com/category/26489450-1.html)