Eto.Formsにおけるカスタム・コントロールの作り方を調べてみた
概要は把握したが、動作するコードは書けなかった。
対象環境
- 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#だけでコードが書けるようになるべき。