読者です 読者をやめる 読者になる 読者になる

Eto.Formsで設定ダイアログ(2)―少し凝った設定ダイアログを作る

C# Eto.Forms

少し凝った設定ダイアログを作る

今回は、もう少し要素の多い設定ダイアログを作ってみます。

作るもの

メインウインドウは、文字列を表示するものとします。

(サンプル画像)

f:id:mokake:20170112211933p:plain

Fileメニューから、Preferences...を選ぶと、設定ダイアログ。 次の設定ができるようにします。

  • メッセージの中身
  • フォントサイズ
  • 背景色
  • 左右配置(左寄せ、中央寄せ、右寄せ)
  • 折り返しの有無

設計

今回は、やや複雑になるので、少しだけ全体構想を練ってみます。

  • 設定ダイアログのうち、「OK」「キャンセル」ボタンなどは共通要素。それを抽象基底クラスとしてみる。
  • 設定ダイアログは設定クラスのために存在するので、基底クラスは設定クラスに基づくジェネリッククラスとする。
  • 設定クラスはダイアログ側で値のクローンと代入をしたいので、インタフェース(ここではIBackupableとする)を継承する。

このため、基礎として次のようなコードが必要となります。

  • インタフェース IBuckupable
  • 設定ダイアログの基底クラス TypicalDialog

これらを前提として、今回の仕様にあわせたコードが必要になります。

  • 設定データ(IBuckupable継承)
  • メインウインドウ
  • 設定ダイアログ(TypicalDialog派生)

では、実際のコードです。

インタフェース

値のクローンと代入を実装するよう規定してみました。

public interface IBackupable<T>
{
    T Clone();
    void Set(T source);
}

設定ダイアログの基底クラス

MakePanel抽象メソッドを準備しました。 派生クラスでは、最低限このメソッドを実装して、ダイアログの「中身」を作る必要があります。

なお、PanelはEto.Formsの基本コンテナなので、中身は何でも入ります。 StackLayoutなどのレイアウトを入れて、その中に複数のコントロールを入れることを想定しています。

なお、コンストラクタではbool useApplyButton = falseとして、「適用」ボタンの有無に対応しています。

コードは、基本的に前回の設定ダイアログと同様です。

using System;
using Eto.Forms;

namespace EtoSample07b
{
    public abstract class TypicalDialog<T> : Dialog
        where T : class, IBackupable<T>
    {
        Button okButton = new Button { Text = "OK" };
        Button cancelButton = new Button { Text = "キャンセル" };
        Button applyButton;

        public T Data
        {
            get { return _data; }
            set
            {
                _dataOriginal = value;
                _data = value?.Clone();
            }
        }
        protected T _data;
        protected T _dataOriginal;

        public abstract Panel MakePanel();

        public TypicalDialog(T data, bool useApplyButton = false)
        {
            if (data == null) throw new ArgumentNullException();
            Data = data;

            okButton.Click += (s, e) => {
                _dataOriginal.Set(_data);
                Close();
            };
            cancelButton.Click += (s, e) => Close();
            if (useApplyButton)
            {
                applyButton = new Button { Text = "適用" };
                applyButton.Click += (s, e) => _dataOriginal.Set(_data);
            }

            Content = new StackLayout
            {
                Orientation = Orientation.Vertical,
                Padding = 4,
                Items = {
                    new StackLayoutItem
                    {
                        Control = MakePanel(),
                        Expand = true,
                        HorizontalAlignment = HorizontalAlignment.Stretch,
                        VerticalAlignment = VerticalAlignment.Stretch
                    },
                    new StackLayoutItem
                    {
                        Control = new StackLayout
                        {
                            Orientation = Orientation.Horizontal,
                            Items= {okButton , cancelButton},
                        },
                        HorizontalAlignment = HorizontalAlignment.Right,
                    },
                },
            };

            DefaultButton = okButton;
            AbortButton = cancelButton;
        }
    }
}

設定データクラス

設定情報クラスを作ります。 骨格は前回と同様ですが、今回は項目が色々あります。

項目の型も、列挙型に構造体、クラスとバリエーションをもたせてみました。 なお、FontSizeの型がfloat?、つまりNullable<float>なのは、サイズ不定の「デフォルト」を表現するためです。

また、フォントはFont MyFontfloat? FontSize、そしてstring FontSizeStringと多数項目を用意していますが、これは設定ダイアログでの変換用です。 WPFと違ってConverterがないので、「使う場所ごとに実装が散らばる」のを避けるためにつけてみました。 あまりきれいではないかも。

using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Eto.Forms;
using Eto.Drawing;

namespace EtoSample07b
{
    public class AppConfig : INotifyPropertyChanged , IBackupable<AppConfig>
    {
        static Font defaultFont = new Font(SystemFont.Default, null);

        private float? _fontSize = defaultFont.Size;
        public float? FontSize
        {
            get { return _fontSize; }
            set
            {
                _myFont = null;
                TrySetProperty(ref _fontSize, value, "FontSize FontSizeString MyFont");
            }
        }
        public string FontSizeString
        {
            get { return (_fontSize == null) ? "" : _fontSize.ToString(); }
            set
            {
                float f = 0F;
                if (string.IsNullOrEmpty(value))
                {
                    if (_fontSize == null) return;
                    FontSize = null;
                }
                else if (float.TryParse(value, out f))
                {
                    if (f == _fontSize) return;
                    FontSize = f;
                }
            }
        }

        private Font _myFont;
        public Font MyFont
        {
            get
            {
                if (_myFont == null)
                {
                    _myFont = new Font(SystemFont.Default, FontSize);
                }
                return _myFont;
            }
        }

        private TextAlignment _alignment = TextAlignment.Center;
        public TextAlignment Alignment
        {
            get { return _alignment; }
            set { TrySetProperty(ref _alignment, value); }
        }

        private bool _wrap = false;
        public bool Wrap
        {
            get { return _wrap; }
            set { TrySetProperty(ref _wrap, value); }
        }

        private string _message = "Hello, World!";
        public string Message
        {
            get { return _message; }
            set { TrySetProperty(ref _message, value); }
        }

        private Color _bgColor = SystemColors.ControlBackground;
        public Color BgColor
        {
            get { return _bgColor; }
            set { TrySetProperty(ref _bgColor, value); }
        }

       #region コンストラクタ、クローン関連

        public AppConfig()
        {
            Message = "Hello, World!";
        }

        public AppConfig Clone()
        {
            var obj = new AppConfig();
            obj.Set(this);
            return obj;
        }

        public void Set(AppConfig c)
        {
            Alignment = c.Alignment;
            FontSize = c.FontSize;
            BgColor = c.BgColor;
            Message = c.Message;
            Wrap = c.Wrap;
        }

       #endregion

       #region VMの基礎

        public event PropertyChangedEventHandler PropertyChanged;

        protected void RaisePropertyChanged(
          [CallerMemberName] string propertyName = null)
        {
            if (PropertyChanged == null) return;
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        protected static char[] SEPARATORS = new char[] { ' ' };

        protected bool TrySetProperty<T>(
          ref T storage,
          T value,
          [CallerMemberName] string propertyName = null)
        {
            if (object.Equals(storage, value)) return false;
            storage = value;
            foreach(var name in propertyName.Split(SEPARATORS,StringSplitOptions.RemoveEmptyEntries))
            {
                RaisePropertyChanged(name);
            }
            return true;
        }

       #endregion
    }
}

メインウインドウ

次にメインウインドウです。 今回はXAMLではなくコードで定義していますが、データバインドを使っているのでXAMLへの置き換えも難しくはありません。

using Eto.Forms;
using Eto.Drawing;

namespace EtoSample07b
{
    public class MainForm : Form
    {
        AppConfig config = new AppConfig();

        public MainForm()
        {
            Title = "My Eto Form";
            ClientSize = new Size(300, 250);

            var text = new TextArea { };
            text.DataContext = config;
            text.TextBinding.BindDataContext((AppConfig c) => c.Message);
            text.BindDataContext(t => t.BackgroundColor, (AppConfig c) => c.BgColor);
            text.BindDataContext(t => t.Wrap, (AppConfig c) => c.Wrap);
            text.BindDataContext(t => t.Font, (AppConfig c) => c.MyFont);
            text.BindDataContext(t => t.TextAlignment, (AppConfig c) => c.Alignment);

            Content = new StackLayout
            {
                Padding = 10,
                Items =
                {
                    "Your message:",
                    new StackLayoutItem {
                        Control = text,
                        Expand = true,
                        HorizontalAlignment = HorizontalAlignment.Stretch,
                        VerticalAlignment = VerticalAlignment.Stretch,
                    },
                }
            };

            var prefCommand = new Command { MenuText = "&Preferences..." };
            prefCommand.Executed += (sender, e) => {
                var d = new ConfigDialog(config);
                d.ShowModal();
            };

            var quitCommand = new Command { MenuText = "Quit" };
            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 = { } },
                },
                ApplicationItems = { prefCommand, },
                QuitItem = quitCommand,
                AboutItem = aboutCommand
            };
        }
    }
}

設定ダイアログ

最後に派生クラスとして作る設定ダイアログです。

using Eto.Forms;
using Eto.Drawing;

namespace EtoSample07b
{
    public class ConfigDialog : TypicalDialog<AppConfig>
    {
        public ConfigDialog(AppConfig data, bool useApplyButton = false)
            : base(data, useApplyButton)
        {
            Title = "Preferences";
            MinimumSize = new Size(300, 200);
        }

        public override Panel MakePanel()
        {
            // Message
            var message = new TextArea { };
            message.DataContext = Data;
            message.TextBinding.BindDataContext((AppConfig c) => c.Message);

            // Font Size (1)
            var fontSize = new NumericUpDown { MaxValue = 120.0, MinValue = 6.0 };
            fontSize.DataContext = Data;
            fontSize.ValueBinding.BindDataContext(
                Binding.Property((AppConfig c) => c.FontSize)
                .Convert(r => r == null ? 12.0 : (double)r, v => (float?)v)
                );

            // Font Size (2)
            var fontSize2 = new TextBox { };
            fontSize2.DataContext = Data;
            fontSize2.TextBinding.BindDataContext((AppConfig c) => c.FontSizeString);

            // Color
            var bgColor = new ColorPicker { };
            bgColor.DataContext = Data;
            bgColor.ValueBinding.BindDataContext((AppConfig c) => c.BgColor);

            // Text Alignment (1)
            var textAlignment = new EnumRadioButtonList<TextAlignment> { };
            textAlignment.DataContext = Data;
            textAlignment.SelectedValueBinding.BindDataContext((AppConfig c) => c.Alignment);

            // Text Alignment (2)
            var textAlignment2 = new EnumDropDown<TextAlignment> { };
            textAlignment2.DataContext = Data;
            textAlignment2.SelectedValueBinding.BindDataContext((AppConfig c) => c.Alignment);

            // Wrap
            var wrap = new CheckBox { Text = "Wrap" };
            wrap.DataContext = Data;
            wrap.CheckedBinding.BindDataContext((AppConfig c) => c.Wrap);

            return new Panel
            {
                Content = new StackLayout
                {
                    HorizontalContentAlignment = HorizontalAlignment.Stretch,
                    Items =
                    {
                        "Your Message:",
                        message,
                        new StackLayout {
                            Orientation = Orientation.Horizontal,
                            Items = { "Font Size:", fontSize, fontSize2 }
                        },
                        new StackLayout
                        {
                            Orientation = Orientation.Horizontal,
                            Items = { "Background Color:", bgColor },
                        },
                        new StackLayout {
                            Orientation = Orientation.Horizontal,
                            Items = { "Alignment:", textAlignment, textAlignment2 }
                        },
                        new StackLayout {
                            Orientation = Orientation.Horizontal,
                            Items = { "Wrap:", wrap }
                        },
                    },
                }
            };
        }
    }
}

設定ダイアログの基本機能を基底クラスにおいたので、こちらは具体的なMakePanelメソッドの実装が大半です。

また、パネルの作成にしても、パラメータ側に面倒な変換を入れたので、こちらでは基本的にはGUI要素を作ってバインドさせるのがほとんどです。

今回は、説明のために同一項目を複数のGUI要素で設定可能にしてみました。 実用性はありませんが、比較可能な実装例ということで。

NumericUpDownコントロール

よくある「ボタンで中身を変更可能な、数値専用の1行テキスト」です。

Etoの場合、フォントサイズはfloat型ですがNumericUpDownのValueはdouble型なので、バインド時には型キャストが必要となります。

さらに今回は「サイズ未設定(デフォルト)」を表現するためにフォントサイズをNullableとしたので、それも含めた変換としています。

ColorPickerコントロール

Color構造体(Eto.Drawing名前空間)を簡単にバインドできるコントロールです。

Windowsで使うと、プルダウンで簡単に使えて便利です。 個人的には、わざわざモーダルウインドウを作るColorDialogより優れている印象。

Linuxだと、標準のカラーダイアログを呼び出すボタンになります。

EnumRadioButtonListコントロール

ラジオボタンの集まり(RadioButtonList)の派生ジェネリッククラスで、列挙型をバインドして、自動的に候補も生成するという便利な代物です。 たった3行で定義できます。

EnumDropDownコントロール

いわゆるコンボボックスのうち、選択肢から選ばなければならないタイプ(直接書き換えは不可)なものを、Eto.Formsは2.0からDropDownとしました。 EnumDropDownは、列挙型用の派生ジェネリッククラスで、EnumRadioButtonList同様、3行で選択肢込みの定義ができてしまいます。

CheckBoxコントロール

特筆すべき点はない、普通のチェックボックスです。

実行してみる

起動すると、中央にメッセージ欄が表示されます。

f:id:mokake:20170112190122p:plain

プルダウンメニューのFileの中にあるPreferences...を選ぶと:

f:id:mokake:20170112211921p:plain

設定ダイアログが表示されます。

f:id:mokake:20170112211933p:plain

ちなみに「Font Size」の右や、「Alignment」の右は、同じ要素にバインドしているため、片方を変えれば他方も連動します。

上述のEnumRadioButtonListやEnumDropDownも、これでイメージがつかめるかと思います。

「Background Color」の右側をクリックすると、Windowsの場合は色サンプルが表示されます。 もちろん選択可能です。

f:id:mokake:20170112211949p:plain

設定を色々と変えてみました。

f:id:mokake:20170112212001p:plain

「OK」ボタンで、設定が反映されます。

f:id:mokake:20170112212014p:plain

Linux上で実行

Linux上でも普通に動作します。

f:id:mokake:20170112212026p:plain

設定ダイアログも同様。 ただし、色選択はデフォルトのモーダルダイアログが開きます。

f:id:mokake:20170112212038p:plain

設定を変更してみました。

f:id:mokake:20170112212053p:plain

「OK」ボタンで反映されます。

f:id:mokake:20170112212106p:plain

まとめ

今回はやや長くなりましたが、Eto.Formsを使った設定ダイアログの例を示しました。データバインドやEtoの便利なクラスを使うことで、比較的シンプルな記述ができることがお分かりいただけるかと思います。

なお、(WPFもですが)フォント選択については、統一的に処理しようとすると、複数のコンボボックスやチェックボックスを組み合わせる必要があり、やや煩雑な印象です。コモンダイアログの方が簡単でしょうね。