概要は把握したが、動作するコードは書けなかった。
対象環境
- Raspbierry pi 3 Model B+
- Raspbian stretch(9.0) 2018-06-27
- Mono 5.16.0
- MonoDevelop 7.6 build 711
- Eto.Forms 2.4.1 拡張機能, NuGetパッケージ
- .NET Core 2.2, MonoDevelop参照方法
前回まで
TextAreaで右クリックするとコンテキストメニューが自動で出る。これを削除したいが、TextAreaやTextControlを継承しても解決できなかった。
- http://ytyaru.hatenablog.com/entry/2020/02/27/000000
- http://ytyaru.hatenablog.com/entry/2020/02/28/000000
- http://ytyaru.hatenablog.com/entry/2020/02/29/000000
- http://ytyaru.hatenablog.com/entry/2020/03/03/000000
Eto.Formsでカスタム・コントロールは作れるのか?
英語なので翻訳して読む。
カスタムプラットフォームコントロールを作成することができます。
作ることはできるみたい。
カスタムプラットフォームコントロールを作成することの欠点は、使用する各プラットフォーム用のコントロールのハンドラ実装を作成する必要があることです。
つまり、Windows, Mac, Linux, Androidといった各種プラットフォーム用のコードを書かねばならないと。WindowsならWindowsForm, WPFなどがあり、LinuxならGTK+2, GTK+3があるのだろう。かなり大変そう。
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());
                        ...
        }
    }
}
 TextAreaをPlatformに追加している。
 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を丸パクリしてTextAreaをTextControl2に置換した。
 しかし、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#だけでコードが書けるようになるべき。