やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

Eto.Formsにおけるカスタム・コントロールの作り方を調べてみた

 概要は把握したが、動作するコードは書けなかった。

対象環境

前回まで

 TextAreaで右クリックするとコンテキストメニューが自動で出る。これを削除したいが、TextAreaやTextControlを継承しても解決できなかった。

Eto.Formsでカスタム・コントロールは作れるのか?

 英語なので翻訳して読む。

カスタムプラットフォームコントロールを作成することができます。

 作ることはできるみたい。

カスタムプラットフォームコントロールを作成することの欠点は、使用する各プラットフォーム用のコントロールのハンドラ実装を作成する必要があることです。

 つまり、Windows, Mac, Linux, Androidといった各種プラットフォーム用のコードを書かねばならないと。WindowsならWindowsForm, WPFなどがあり、LinuxならGTK+2, GTK+3があるのだろう。かなり大変そう。

新しいコントロールを登録するには、そのコントロールPlatformインスタンスに追加する必要があります。

Eto

 自作する前にEtoではどのように実装しているのか見てみる。

プラットフォーム

 Etoではどんなプラットフォームに対応しているのか。全8種。Windows系4(Direct2D, WinForms, WinRT, Wpf)、Mac系2(Mac, iOS)、Linux系(Gtk)、Android

 ちなみに上記はすべて以下の抽象クラスを継承している。

コントロールを追加している場所

 上記リンクの通り、各プラットフォームのディレクトリ配下にPlatform.csファイルがあり、そこでコントロールを追加しているようだ。たとえばEto.Gtk/Platform.csの場合は以下。

namespace Eto.GtkSharp
{
        ...
    public class Platform : Eto.Platform
    {
                ...
        public Platform()
        {
#if GTK2
            if (EtoEnvironment.Platform.IsWindows && Environment.Is64BitProcess)
                throw new NotSupportedException(string.Format(System.Globalization.CultureInfo.CurrentCulture, "Please compile/run GTK in x86 mode (32-bit) on windows"));
#endif

            AddTo(this);
        }

        public static void AddTo(Eto.Platform p)
        {
                        ...
            p.Add<TextArea.IHandler>(() => new TextAreaHandler());
                        ...
        }
    }
}

 TextAreaPlatformに追加している。

 Etoによる共通インタフェースのラップメソッド内に、プラットフォーム固有の処理を実装している。GTKにおけるTextArea相当のUIはGtk.TextViewらしい。

ソースコード

 試しに書いてみたが、エラーが出て失敗する。Gtkが参照できないため作成できない。おそらくGtkパッケージを追加すればいいのだろうが未確認。

ExtendTextControl.Desktop

Program.cs

using System;
using Eto;
using Eto.Forms;
using Eto.Drawing;
using ExtendTextControl;

namespace ExtendTextControl.Desktop
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
//            Eto.Gtk.Platform().Add<MyCustomControl.IMyCustomControl>(() => new MyCustomControlHandler()
            Eto.Platform.Detect.Add<MyCustomControl.IMyCustomControl>(() => new MyCustomControlHandler()
//          p.Add<TextArea.IHandler>(() => new TextAreaHandler());
            Eto.Platform.Detect.Add<TextControl2.IHandler>(() => new TextControl2Handler()
            new Application(Eto.Platform.Detect).Run(new MainForm());
        }
    }
}

ExtendTextControl

MainForm.cs

using System;
using Eto.Forms;
using Eto.Drawing;

namespace ExtendTextControl
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            Title = "Extend TextControl";
            ClientSize = new Size(400, 350);
            //TextControl textcontrol = new TextControl(); // abstract class
            TextControl2 textcontrol2 = new TextControl2();
            DynamicLayout layout = new DynamicLayout();
            //layout.Add(textcontrol);
            layout.Add(textcontrol2);
            Content = layout;
        }
    }
}

TextControl2.cs

using System;
using Eto;
using Eto.Forms;
using Eto.IO;

namespace ExtendTextControl
{
    [Handler(typeof(TextControl2.IHandler))]
    public class TextControl2 : TextControl
    {
        new IHandler Handler { get { return (IHandler)base.Handler; } }
        static TextControl2() {}
        public new interface IHandler : TextControl.IHandler {
        }
    }
}

Gtk/TextControl2Handler.cs

 Gtk/TextAreaHandler.csを丸パクリしてTextAreaTextControl2に置換した。

 しかし、Eto.GtkSharp.Drawingが存在しない。Gtk.TextViewも参照できない。おそらくEtoプロジェクト内でのみ参照できるのだろう。どうしたものか……。

using System;
using Eto.Forms;
using Eto.Drawing;
using Eto.GtkSharp.Drawing;

namespace Eto.GtkSharp.Forms.Controls
{
    public class TextControl2Handler : TextControl2Handler<Gtk.TextView, TextControl2, TextControl2.ICallback>
    {
    }

    public class TextControl2Handler<TControl, TWidget, TCallback> : GtkControl<TControl, TWidget, TCallback>, TextControl2.IHandler
        where TControl: Gtk.TextView, new()
        where TWidget: TextControl2
        where TCallback: TextControl2.ICallback
    {
        int suppressSelectionAndTextChanged;
        readonly Gtk.ScrolledWindow scroll;
        Gtk.TextTag tag;

        public override Gtk.Widget ContainerControl
        {
            get { return scroll; }
        }

        public override Size DefaultSize { get { return new Size(100, 60); } }

        public TextControl2Handler()
        {
            scroll = new Gtk.ScrolledWindow();
            scroll.ShadowType = Gtk.ShadowType.In;
            Control = new TControl();
            Size = new Size(100, 60);
            scroll.Add(Control);
            Wrap = true;
        }

        public override void AttachEvent(string id)
        {
            switch (id)
            {
                case TextControl.TextChangedEvent:
                    Control.Buffer.Changed += Connector.HandleBufferChanged;
                    break;
                case TextControl2.SelectionChangedEvent:
                    Control.Buffer.MarkSet += Connector.HandleSelectionChanged;
                    break;
                case TextControl2.CaretIndexChangedEvent:
                    Control.Buffer.MarkSet += Connector.HandleCaretIndexChanged;
                    break;
                default:
                    base.AttachEvent(id);
                    break;
            }
        }

        protected new TextControl2Connector Connector { get { return (TextControl2Connector)base.Connector; } }

        protected override WeakConnector CreateConnector()
        {
            return new TextControl2Connector();
        }

        protected class TextControl2Connector : GtkControlConnector
        {
            Range<int> lastSelection;
            int? lastCaretIndex;

            public new TextControl2Handler<TControl, TWidget, TCallback> Handler { get { return (TextControl2Handler<TControl, TWidget, TCallback>)base.Handler; } }

            public void HandleBufferChanged(object sender, EventArgs e)
            {
                var handler = Handler;
                if (handler.suppressSelectionAndTextChanged == 0)
                    handler.Callback.OnTextChanged(Handler.Widget, EventArgs.Empty);
            }

            public void HandleSelectionChanged(object o, Gtk.MarkSetArgs args)
            {
                var handler = Handler;
                var selection = handler.Selection;
                if (handler.suppressSelectionAndTextChanged == 0 && selection != lastSelection)
                {
                    handler.Callback.OnSelectionChanged(handler.Widget, EventArgs.Empty);
                    lastSelection = selection;
                }
            }

            public void HandleCaretIndexChanged(object o, Gtk.MarkSetArgs args)
            {
                var handler = Handler;
                var caretIndex = handler.CaretIndex;
                if (handler.suppressSelectionAndTextChanged == 0 && caretIndex != lastCaretIndex)
                {
                    handler.Callback.OnCaretIndexChanged(handler.Widget, EventArgs.Empty);
                    lastCaretIndex = caretIndex;
                }
            }

            public void HandleApplyTag(object sender, EventArgs e)
            {
                var buffer = Handler.Control.Buffer;
                var tag = Handler.tag;
                buffer.ApplyTag(tag, buffer.StartIter, buffer.EndIter);
            }
        }

        public override string Text
        {
            get { return Control.Buffer.Text; }
            set
            {
                var sel = Selection;
                suppressSelectionAndTextChanged++;
                Control.Buffer.Text = value;
                if (tag != null)
                    Control.Buffer.ApplyTag(tag, Control.Buffer.StartIter, Control.Buffer.EndIter);
                Callback.OnTextChanged(Widget, EventArgs.Empty);
                suppressSelectionAndTextChanged--;
                if (sel != Selection)
                    Callback.OnSelectionChanged(Widget, EventArgs.Empty);
            }
        }

        public virtual Color TextColor
        {
            get { return Control.GetForeground(); }
            set
            {
                Control.SetForeground(value);
                Control.SetTextColor(value);
            }
        }

        public override Color BackgroundColor
        {
            get
            {
                return Control.GetBase();
            }
            set
            {
                Control.SetBackground(value);
                Control.SetBase(value);
            }
        }

        public bool ReadOnly
        {
            get { return !Control.Editable; }
            set { Control.Editable = !value; }
        }

        public bool Wrap
        {
            get { return Control.WrapMode != Gtk.WrapMode.None; }
            set { Control.WrapMode = value ? Gtk.WrapMode.WordChar : Gtk.WrapMode.None; }
        }

        public void Append(string text, bool scrollToCursor)
        {
            var end = Control.Buffer.EndIter;
            Control.Buffer.Insert(ref end, text);
            if (scrollToCursor)
            {
                var mark = Control.Buffer.CreateMark(null, end, false);
                Control.ScrollToMark(mark, 0, false, 0, 0);
            }
        }

        public string SelectedText
        {
            get
            {
                Gtk.TextIter start, end;
                if (Control.Buffer.GetSelectionBounds(out start, out end))
                {
                    return Control.Buffer.GetText(start, end, false);
                }
                return string.Empty;
            }
            set
            {
                suppressSelectionAndTextChanged++;
                Gtk.TextIter start, end;
                if (Control.Buffer.GetSelectionBounds(out start, out end))
                {
                    var startOffset = start.Offset;
                    Control.Buffer.Delete(ref start, ref end);
                    if (value != null)
                    {
                        Control.Buffer.Insert(ref start, value);
                        start = Control.Buffer.GetIterAtOffset(startOffset);
                        end = Control.Buffer.GetIterAtOffset(startOffset + value.Length);
                        Control.Buffer.SelectRange(start, end);
                    }
                }
                else if (value != null)
                    Control.Buffer.InsertAtCursor(value);
                if (tag != null)
                    Control.Buffer.ApplyTag(tag, Control.Buffer.StartIter, Control.Buffer.EndIter);
                Callback.OnTextChanged(Widget, EventArgs.Empty);
                Callback.OnSelectionChanged(Widget, EventArgs.Empty);
                suppressSelectionAndTextChanged--;
            }
        }

        public Range<int> Selection
        {
            get
            {
                Gtk.TextIter start, end;
                if (Control.Buffer.GetSelectionBounds(out start, out end))
                {
                    return new Range<int>(start.Offset, end.Offset - 1);
                }
                return Range.FromLength(Control.Buffer.CursorPosition, 0);
            }
            set
            {
                suppressSelectionAndTextChanged++;
                var start = Control.Buffer.GetIterAtOffset(value.Start);
                var end = Control.Buffer.GetIterAtOffset(value.End + 1);
                Control.Buffer.SelectRange(start, end);
                Callback.OnSelectionChanged(Widget, EventArgs.Empty);
                suppressSelectionAndTextChanged--;
            }
        }

        public void SelectAll()
        {
            Control.Buffer.SelectRange(Control.Buffer.StartIter, Control.Buffer.EndIter);
        }

        public int CaretIndex
        {
            get { return Control.Buffer.GetIterAtMark(Control.Buffer.InsertMark).Offset; }
            set
            {
                var ins = Control.Buffer.GetIterAtOffset(value);
                Control.Buffer.SelectRange(ins, ins);
            }
        }

        public bool AcceptsTab
        {
            get { return Control.AcceptsTab; }
            set { Control.AcceptsTab = value; }
        }

        bool acceptsReturn = true;

        public bool AcceptsReturn
        {
            get { return acceptsReturn; }
            set
            {
                if (value != acceptsReturn)
                {
                    if (!acceptsReturn)
                        Widget.KeyDown -= HandleKeyDown;
                    //Control.KeyPressEvent -= PreventEnterKey;
                    acceptsReturn = value;
                    if (!acceptsReturn)
                        Widget.KeyDown += HandleKeyDown;
                    //Control.KeyPressEvent += PreventEnterKey;
                }
            }
        }

        void HandleKeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyData == Keys.Enter)
                e.Handled = true;
        }

        static void PreventEnterKey(object o, Gtk.KeyPressEventArgs args)
        {
            if (args.Event.Key == Gdk.Key.Return)
                args.RetVal = false;
        }

        public override Font Font
        {
            get { return base.Font; }
            set
            {
                base.Font = value;
                if (value != null)
                {
                    if (tag == null)
                    {
                        tag = new Gtk.TextTag("font");
                        Control.Buffer.TagTable.Add(tag);
                        Control.Buffer.Changed += Connector.HandleApplyTag;
                        Control.Buffer.ApplyTag(tag, Control.Buffer.StartIter, Control.Buffer.EndIter);
                    }
                    value.Apply(tag);
                }
                else
                {
                    Control.Buffer.RemoveAllTags(Control.Buffer.StartIter, Control.Buffer.EndIter);
                }
            }
        }

        public TextAlignment TextAlignment
        {
            get { return Control.Justification.ToEto(); }
            set { Control.Justification = value.ToGtk(); }
        }

        public bool SpellCheck
        {
            get { return false; }
            set { }
        }

        public bool SpellCheckIsSupported { get { return false; } }

        public TextReplacements TextReplacements
        {
            get { return TextReplacements.None; }
            set { }
        }

        public TextReplacements SupportedTextReplacements
        {
            get { return TextReplacements.None; }
        }
    }
}

所感

 Etoのコードを書く前に、Gtk#だけでコードが書けるようになるべき。