概要
タイトルの通りの記事です。次の記事で紹介されていて興味を持ったのと、今でも更新され続けているので信頼できると思ったからですね。
クロスプラットフォームなGUIフレームワーク「Eto.Forms」の紹介 - ごった日記
導入
Visual Studioを起動した後、初手で「ツール(T)→拡張機能と更新プログラム(U)...」を選択して「拡張機能と更新プログラム」ウィンドウを開き、「オンライン」タブからEto.Formsを導入します。
具体的には、次のウィンドウで「ダウンロード」ボタンを押してダウンロードした後、 VisualStudioを再起動 すればEto.Formsが導入されます。
導入後は、新しいプロジェクトを作成する際に、Eto.Formsを利用したプロジェクトをテンプレートとして選択することができます。
プロジェクトの設定画面ですが、私はWPF使いなので、「XAML」でFormを編集することにしました。
ファイル構造をざっと説明
まずフォルダパスがこんな感じです。
- ルートディレクトリにある「HDC2」はアプリ名ですので、プロジェクト名を他の名前にすれば当然別の名前になります
- ルートディレクトリの1個下に、共通コードである「HDC2」フォルダと、UI用の「HDC2.Desktop」フォルダが生成されます。「HDC2」の単語の意味は前述したので説明は省略
- HDC2フォルダは共通(.NET Standard製)ですが、HDC2.Desktopフォルダは見るからに プラットフォーム毎に別バイナリ になりそうです
次に実行ファイルですが、上記の図で言えば「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ですが、サンプルがサンプルなせいで コードビハインド前提 のように見えます。
XAMLでMVVMを使うのは当然 なのでどう対策するかですが、実は「INotifyPropertyChanged
を継承したViewModel
をDataContext
に宛がう」といったベタな戦法で対処することができます。
Data Binding · picoe/Eto Wiki
テンプレートに載っているサンプルコードを、そのままMVVMモデルとして使えるように書き直すとこんな感じ。
※WPFと違い、メニュー部分はコードビハインドする必要があります
<?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&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>
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 があります。
【C#】ReactiveProperty全然分からねぇ!って人向けのFAQ集【修正済】
Eto.Formsはフレームワークとして.NET Standardを使用していますが、ReactivePropertyも同様に.NET Standardに対応しています。
ゆえに使えるはず……と思って試してみたところ、 あっさり成功しました。 変更箇所はこんな感じです。ちなみにReactiveCollectionもちゃんと使えます。
<!-- 本当は「MainForm.xeto」ってファイル名です -->
<Label Text="{Binding LabelText}"/>
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
<Label Text="{Binding LabelText.Value}"/>
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
ところが、この問題について「Eto.Forms は Gtk しか SynchronizationContext に対応していないみたいなので自前でセットしたら動きました」との助言を頂きました。つまり、Eto.Desktop
におけるProgram.cs
について、
[STAThread]
static void Main(string[] args)
{
new Application(Eto.Platform.Detect).Run(new MainForm());
}
とあるところを、
[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」ってファイル名です -->
<ListBox/>
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
<ListBox>
<ListBox.ContextMenu>
<ContextMenu>
<ButtonMenuItem Text="右クリックメニュー" Click="SampleFunc" />
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
なお、後述するメニューにCommandをBindingできない不具合はこのコンテキストメニューでも同様ですので注意!
ドラッグ&ドロップの仕込み方
なんでググってもこれぐらいのことのサンプルソース出てこないんだよと思いながら試行錯誤して見つけました。
以下、ListBoxでの例です(他のコントロールでも可能)。
<!-- 本当は「sample.xeto」ってファイル名です -->
<ListBox/>
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
<ListBox AllowDrop="True" DragDrop="DrapDropEvent"/>
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からという比較的新しい機能なせいか、Linuxではまだ動かないのですけれどねorz
その他小ネタ集
メニューアイテムに関する恐ろしい罠について
Eto.Formsプルダウンメニューの翻訳対応 - ごった日記
この記事によるととんでもないことが書かれています。つまり、
-
MenuBar.ApplicationItems
やMenuBar.QuitItem
を使用すると、File
メニューが勝手に追加される -
MenuBar.AboutItem
を使用すると、Help
メニューが勝手に追加される -
File
メニューが無いとLinuxではアプリが強制終了する
……明らかに ローカライズのことを考えてないクソ仕様 だと思いますが、幸いにも「コードビハインドで上書きする」技が使えます。つまり、何食わぬ顔でXAMLを読み込んだ後、当該文字列をダイレクトに書き換えてやればいいのです。
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型のプロパティを弄った方が楽です 。
UIの癖の違いについて
全体的に見て、表現能力はWPFより明らかに見劣りします。
例えば<TableLayout>
はWPFで言うところの<Grid>
っぽいコンテナですが、<Grid>
ではGrid.Column
およびGrid.Row
プロパティで位置決めしていたものを、<TableLayout>
では``の中に1行づつコンテナを詰め込む方向の実装になります。また、**ColumnSpanやRowSpanが無い**ので、それっぽいものを作りたい場合は
`をネストさせて組むしかありません。
ところが、様々なオブジェクト・コンテナに指定する`Spacing`および`Padding`プロパティは、WPFの`Marin`および`Padding`プロパティと違い、周囲を同じピクセル数でしか空けることができません。一応`"10,5"`のようにカンマ区切りで2個の引数を取れるものもありますがそれでも2個まで。その結果、かなりレイアウト構築に制限を受けることになります。
しかも、これはおま環かもしれませんが、レイアウトを表すXMLファイル(`*.xeto`)を標準のテキストエディター(Eto.Forms preview)で開いて作業していると、しばらくしたらVisualStudioごとフリーズし、強制終了を余儀なくされます。上記のようにプレビュー欄が何の役にも立たないくせにこの体たらくですので、編集する際は「XML(テキスト)エディター」を使用することを推奨します。
とはいえ、Eto.Forms previewじゃないと使えないプロパティがIntelliSenseで表示されることがあるといったふざけた仕様により、Eto.Forms previewの使用を余儀なくされることはあるでしょうorz
Linuxで動かす際の注意事項
Linuxで動かす際は、**Mono**を利用してビルド後のexeファイルを動かします。
ただ、その辺のUbuntu 16.04 LTSなどで何気なく「sudo apt-get install mono-complete」としただけだと、最新のMonoじゃないのでEto.Forms製アプリが動かないといったドツボにはまります。
したがって、公式に記載された手順に従い、事前にリポジトリを追加してからインストールする必要があります(以下はUbuntu 16.04の場合)。
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
型のインスタンスを引き出して叩くのが基本です。画像に画像を合成したい時はこんな感じ。
// 画像データを用意する
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);
}