diff --git a/src/Eto.Gtk/Forms/Controls/NativeControlHandler.cs b/src/Eto.Gtk/Forms/Controls/NativeControlHandler.cs index bd1827690..536c3aa21 100644 --- a/src/Eto.Gtk/Forms/Controls/NativeControlHandler.cs +++ b/src/Eto.Gtk/Forms/Controls/NativeControlHandler.cs @@ -1,11 +1,43 @@ -namespace Eto.GtkSharp.Forms.Controls + +namespace Eto.GtkSharp.Forms.Controls { - public class NativeControlHandler : GtkControl + public class NativeControlHandler : GtkControl, NativeControlHost.IHandler { + + Gtk.EventBox _eventBox = new Gtk.EventBox(); + public override Gtk.Widget ContainerControl => _eventBox; + public NativeControlHandler(Gtk.Widget nativeControl) { Control = nativeControl; } + + public NativeControlHandler() + { + } + + public void Create(object controlObject) + { + if (controlObject == null) + { + Control = _eventBox; + } + else if (controlObject is Gtk.Widget widget) + { + Control = widget; + _eventBox.Child = widget; + } + else if (controlObject is IntPtr handle) + { + widget = GLib.Object.GetObject(handle) as Gtk.Widget; + if (widget == null) + throw new InvalidOperationException("Could not convert handle to Gtk.Widget"); + Control = widget; + _eventBox.Child = widget; + } + else + throw new NotSupportedException($"controlObject of type {controlObject.GetType()} is not supported by this platform"); + } } } diff --git a/src/Eto.Gtk/Platform.cs b/src/Eto.Gtk/Platform.cs index 3fd02e4e7..26a9a2b67 100644 --- a/src/Eto.Gtk/Platform.cs +++ b/src/Eto.Gtk/Platform.cs @@ -220,6 +220,7 @@ public static void AddTo(Eto.Platform p) p.Add(() => new DataObjectHandler()); p.Add(() => new DataFormatsHandler()); p.Add(() => new WindowHandler()); + p.Add(() => new NativeControlHandler()); if (EtoEnvironment.Platform.IsLinux) { p.Add(() => new LinuxTrayIndicatorHandler()); diff --git a/src/Eto.Mac/Forms/Controls/NativeControlHandler.cs b/src/Eto.Mac/Forms/Controls/NativeControlHandler.cs index 35ea739c0..51af2e0d9 100644 --- a/src/Eto.Mac/Forms/Controls/NativeControlHandler.cs +++ b/src/Eto.Mac/Forms/Controls/NativeControlHandler.cs @@ -1,6 +1,6 @@ namespace Eto.Mac.Forms.Controls { - public class NativeControlHandler : MacView + public class NativeControlHandler : MacView, NativeControlHost.IHandler { NSViewController controller; @@ -8,6 +8,10 @@ public NativeControlHandler(NSView nativeControl) { Control = nativeControl; } + + public NativeControlHandler() + { + } public override SizeF GetPreferredSize(SizeF availableSize) { @@ -21,6 +25,27 @@ public NativeControlHandler(NSViewController nativeControl) } public override NSView ContainerControl { get { return Control; } } + + public void Create(object controlObject) + { + if (controlObject == null) + { + Control = new NSView(); + } + else if (controlObject is NSView view) + { + Control = view; + } + else if (controlObject is IntPtr handle) + { + view = Runtime.GetNSObject(handle) as NSView; + if (view == null) + throw new InvalidOperationException("supplied handle is invalid or does not refer to an object derived from NSView"); + Control = view; + } + else + throw new NotSupportedException($"controlObject of type {controlObject.GetType()} is not supported by this platform"); + } } } diff --git a/src/Eto.Mac/Platform.cs b/src/Eto.Mac/Platform.cs index 54a27dd02..ab39b2744 100644 --- a/src/Eto.Mac/Platform.cs +++ b/src/Eto.Mac/Platform.cs @@ -208,6 +208,7 @@ public static void AddTo(Eto.Platform p) p.Add(() => new ToggleButtonHandler()); p.Add(() => new ThemedPropertyGridHandler()); p.Add(() => new ThemedCollectionEditorHandler()); + p.Add(() => new NativeControlHandler()); // Forms.Menu p.Add(() => new CheckMenuItemHandler()); diff --git a/src/Eto.WinForms/Forms/Controls/NativeControlHandler.cs b/src/Eto.WinForms/Forms/Controls/NativeControlHandler.cs index b80d4f81e..76a718019 100644 --- a/src/Eto.WinForms/Forms/Controls/NativeControlHandler.cs +++ b/src/Eto.WinForms/Forms/Controls/NativeControlHandler.cs @@ -1,11 +1,56 @@ -namespace Eto.WinForms.Forms.Controls + + +namespace Eto.WinForms.Forms.Controls { - public class NativeControlHandler : WindowsControl + public class NativeControlHandler : WindowsControl, NativeControlHost.IHandler { + swf.IWin32Window _win32Window; public NativeControlHandler(swf.Control nativeControl) { Control = nativeControl; } + + public NativeControlHandler() + { + } + + + public void Create(object controlObject) + { + if (controlObject == null) + { + Control = new swf.UserControl(); + } + else if (controlObject is swf.Control control) + { + Control = control; + } + else if (controlObject is IntPtr handle) + { + CreateWithHandle(handle); + } + else if (controlObject is swf.IWin32Window win32Window) + { + // keep a reference so it doesn't get GC'd + _win32Window = win32Window; + CreateWithHandle(win32Window.Handle); + } + else + throw new NotSupportedException($"controlObject of type {controlObject.GetType()} is not supported by this platform"); + } + + private void CreateWithHandle(IntPtr handle) + { + Control = new swf.Control(); + Win32.GetWindowRect(handle, out var rect); + Win32.SetParent(handle, Control.Handle); + Control.Size = rect.ToSD().Size; + Widget.SizeChanged += (sender, e) => + { + var size = Control.Size; + Win32.SetWindowPos(handle, IntPtr.Zero, 0, 0, size.Width, size.Height, Win32.SWP.NOZORDER); + }; + } } } diff --git a/src/Eto.WinForms/Platform.cs b/src/Eto.WinForms/Platform.cs index a21619c3f..7a894871b 100644 --- a/src/Eto.WinForms/Platform.cs +++ b/src/Eto.WinForms/Platform.cs @@ -115,6 +115,7 @@ public static void AddTo(Eto.Platform p) p.Add(() => new ToggleButtonHandler()); p.Add(() => new ThemedPropertyGridHandler()); p.Add(() => new ThemedCollectionEditorHandler()); + p.Add(() => new NativeControlHandler()); // Forms.Menu p.Add(() => new CheckMenuItemHandler()); diff --git a/src/Eto.WinForms/Win32.cs b/src/Eto.WinForms/Win32.cs index 438d083db..372af85c1 100755 --- a/src/Eto.WinForms/Win32.cs +++ b/src/Eto.WinForms/Win32.cs @@ -620,5 +620,17 @@ public struct SCROLLINFO [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetScrollInfo(IntPtr hwnd, int fnBar, ref SCROLLINFO lpsi); + + [DllImport("user32.dll")] + public static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); + + [DllImport("user32.dll")] + public static extern IntPtr CreateWindowEx(uint dwExStyle, string lpClassName, string lpWindowName, uint dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam); + + [DllImport("user32.dll")] + public static extern bool DestroyWindow(IntPtr hWnd); + + [DllImport("User32.dll", SetLastError = true)] + public static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw); } } diff --git a/src/Eto.Wpf/Forms/Controls/NativeControlHandler.cs b/src/Eto.Wpf/Forms/Controls/NativeControlHandler.cs old mode 100644 new mode 100755 index 97e4bf8fe..5c5b02d4e --- a/src/Eto.Wpf/Forms/Controls/NativeControlHandler.cs +++ b/src/Eto.Wpf/Forms/Controls/NativeControlHandler.cs @@ -1,22 +1,161 @@ -namespace Eto.Wpf.Forms.Controls +using Eto.Forms; +using System.Windows.Interop; +using IWin32WindowWinForms = System.Windows.Forms.IWin32Window; +using IWin32WindowInterop = System.Windows.Interop.IWin32Window; + +namespace Eto.Wpf.Forms.Controls; +public class NativeControlHandler : WpfFrameworkElement, NativeControlHost.IHandler { - public class NativeControlHandler : WpfFrameworkElement + public override IntPtr NativeHandle + { + get + { + if (Control is EtoHwndHost host) + return host.Handle; + return base.NativeHandle; + } + } + + public NativeControlHandler() + { + } + + public NativeControlHandler(sw.FrameworkElement nativeControl) + { + Control = nativeControl; + } + + public override Color BackgroundColor { - public NativeControlHandler(sw.FrameworkElement nativeControl) + get => throw new NotSupportedException("You cannot get this property for native controls"); + set => throw new NotSupportedException("You cannot set this property for native controls"); + } + + public void Create(object controlObject) + { + if (controlObject == null) + { + var host = new EtoHwndHost(null); + host.GetPreferredSize += () => Size.Round(UserPreferredSize.ToEto() * (Widget.ParentWindow?.Screen?.LogicalPixelSize ?? 1)); + Control = host; + } + else if (controlObject is sw.FrameworkElement element) + { + Control = element; + } + else if (controlObject is IntPtr handle) + { + Control = new EtoHwndHost(new HandleRef(this, handle)); + } + else if (controlObject is IWin32WindowWinForms win32Window) + { + // keep a reference to the win32window object + var host = new EtoHwndHost(new HandleRef(win32Window, win32Window.Handle)); + host.GetPreferredSize += () => Size.Round(UserPreferredSize.ToEto() * (Widget.ParentWindow?.Screen?.LogicalPixelSize ?? 1)); + Control = host; + } + else if (controlObject is IWin32WindowInterop win32WindowWpf) { - Control = nativeControl; + // keep a reference to the win32window object + var host = new EtoHwndHost(new HandleRef(win32WindowWpf, win32WindowWpf.Handle)); + host.GetPreferredSize += () => Size.Round(UserPreferredSize.ToEto() * (Widget.ParentWindow?.Screen?.LogicalPixelSize ?? 1)); + Control = host; } + else + throw new NotSupportedException($"controlObject of type {controlObject.GetType()} is not supported by this platform"); + } +} + +sealed class EtoHwndHost : HwndHost +{ + HandleRef? _hwnd; + swc.ScrollViewer _parentScrollViewer; + sd.Rectangle _regionRect = sd.Rectangle.Empty; + + public Func GetPreferredSize { get; set; } - public override Eto.Drawing.Color BackgroundColor + public EtoHwndHost(HandleRef? hwnd) + { + _hwnd = hwnd; + } + + protected override HandleRef BuildWindowCore(HandleRef hwndParent) + { + if (_hwnd == null) { - get - { - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "You cannot get this property for native controls")); - } - set - { - throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "You cannot set this property for native controls")); - } + // create a new WinForms Usercontrol to host whatever we want + var size = GetPreferredSize?.Invoke() ?? new Size(100, 100); + var ctl = new swf.UserControl(); + ctl.Size = size.ToSD(); + _hwnd = new HandleRef(ctl, ctl.Handle); } + Win32.SetParent(_hwnd.Value.Handle, hwndParent.Handle); + HookParentScrollViewer(); + return _hwnd.Value; } + + protected override void OnVisualParentChanged(sw.DependencyObject oldParent) + { + base.OnVisualParentChanged(oldParent); + HookParentScrollViewer(); + } + + void HookParentScrollViewer() + { + if (_parentScrollViewer != null) + _parentScrollViewer.ScrollChanged -= ParentScrollViewerScrollChanged; + _regionRect = sd.Rectangle.Empty; + _parentScrollViewer = this.GetVisualParent(); + if (_parentScrollViewer != null) + _parentScrollViewer.ScrollChanged += ParentScrollViewerScrollChanged; + } + + protected override void DestroyWindowCore(HandleRef hwnd) + { + } + + private void ParentScrollViewerScrollChanged(object sender, swc.ScrollChangedEventArgs e) + { + UpdateRegion(); + } + + double LogicalPixelSize => sw.PresentationSource.FromVisual(this)?.CompositionTarget.TransformToDevice.M11 ?? 1.0; + + sd.Size ScaledSize(double w, double h) + { + var pixelSize = LogicalPixelSize; + return new sd.Size((int)Math.Round(w * pixelSize), (int)Math.Round(h * pixelSize)); + } + + void UpdateRegion() + { + if (_parentScrollViewer == null || !IsVisible) + return; + + if (_parentScrollViewer.Content is not sw.FrameworkElement content) + return; + + var transform = content.TransformToDescendant(this); + var offset = transform.Transform(new sw.Point(_parentScrollViewer.HorizontalOffset, _parentScrollViewer.VerticalOffset)); + + + var loc = ScaledSize(offset.X, offset.Y); + var size = ScaledSize(_parentScrollViewer.ViewportWidth, _parentScrollViewer.ViewportHeight); + + var rect = new sd.Rectangle(new sd.Point(loc), size); + if (rect != _regionRect) + { + SetRegion(rect); + _regionRect = rect; + } + } + + private void SetRegion(sd.Rectangle rect) + { + using (var graphics = sd.Graphics.FromHwnd(Handle)) + Win32.SetWindowRgn(Handle, new sd.Region(rect).GetHrgn(graphics), true); + } + + + } diff --git a/src/Eto.Wpf/Platform.cs b/src/Eto.Wpf/Platform.cs index e24a615c4..d0f9e6aed 100755 --- a/src/Eto.Wpf/Platform.cs +++ b/src/Eto.Wpf/Platform.cs @@ -144,6 +144,7 @@ public static void AddTo(Eto.Platform p) p.Add(() => new ToggleButtonHandler()); p.Add(() => new ThemedPropertyGridHandler()); p.Add(() => new ThemedCollectionEditorHandler()); + p.Add(() => new NativeControlHandler()); // Forms.Menu p.Add(() => new CheckMenuItemHandler()); diff --git a/src/Eto.Wpf/WpfHelpers.cs b/src/Eto.Wpf/WpfHelpers.cs index 0c2ddbbe4..f206f2405 100644 --- a/src/Eto.Wpf/WpfHelpers.cs +++ b/src/Eto.Wpf/WpfHelpers.cs @@ -54,7 +54,7 @@ public static Control ToEto(this sw.FrameworkElement nativeControl) { if (nativeControl == null) return null; - return new Control(new NativeControlHandler(nativeControl)); + return new NativeControlHost(nativeControl); } /// diff --git a/src/Eto/Forms/Controls/NativeControlHost.cs b/src/Eto/Forms/Controls/NativeControlHost.cs new file mode 100755 index 000000000..3f5035008 --- /dev/null +++ b/src/Eto/Forms/Controls/NativeControlHost.cs @@ -0,0 +1,50 @@ +namespace Eto.Forms; + +/// +/// Control to host a native control within Eto +/// +/// +/// This can be used as a cross platform way to convert a native control to an eto control without having to reference +/// Eto's platform-specific assemblies and using its ToNatve() method or creating custom handlers. +/// +/// The type of the supplied controlObject supported depends on the platform you are running on. +/// - Wpf: FrameworkElement, HWND (IntPtr), System.Windows.Forms.Control, or IWin32Window +/// - WinForms: HWND, System.Windows.Forms.Control, or IWin32Window +/// - Mac64/macOS: AppKit.NSView, handle to NSView (IntPtr) +/// - Gtk: Gtk.Widget, or handle to Gtk.Widget (IntPtr) +/// +[Handler(typeof(IHandler))] +public class NativeControlHost : Control +{ + new IHandler Handler => (IHandler)base.Handler; + + /// + /// Initializes a new instance of the native control host with the specified native controlObject + /// + /// ControlObject to host, of null to create a native hosting control that the caller can use + public NativeControlHost(object controlObject) + { + Handler.Create(controlObject); + Initialize(); + } + + /// + /// Initializes a new instance of the native control host with a native hosting control that the caller can use directly. + /// + public NativeControlHost() : this(null) + { + } + + /// + /// Handler interface for the + /// + [AutoInitialize(false)] + public new interface IHandler : Control.IHandler + { + /// + /// Initializes a new instance of the native control host with the specified native controlObject + /// + /// ControlObject to host, of null to create a native hosting control that the caller can use + void Create(object controlObject); + } +} diff --git a/test/Eto.Test.Gtk/NativeHostControls.cs b/test/Eto.Test.Gtk/NativeHostControls.cs new file mode 100755 index 000000000..cde655ba4 --- /dev/null +++ b/test/Eto.Test.Gtk/NativeHostControls.cs @@ -0,0 +1,11 @@ +namespace Eto.Test.Gtk; + +class NativeHostControls : INativeHostControls +{ + public IEnumerable GetNativeHostTests() + { + yield return new NativeHostTest("Gtk.Button", () => new global::Gtk.Button { Child = new global::Gtk.Label { Text = "A Gtk.Button" } }); + yield return new NativeHostTest("IntPtr", () => new global::Gtk.Button { Child = new global::Gtk.Label { Text = "An IntPtr handle button" } }.Handle); + } +} + diff --git a/test/Eto.Test.Gtk/Startup.cs b/test/Eto.Test.Gtk/Startup.cs index cab1e934f..fc8c7d480 100644 --- a/test/Eto.Test.Gtk/Startup.cs +++ b/test/Eto.Test.Gtk/Startup.cs @@ -7,9 +7,10 @@ class Startup [STAThread] static void Main(string[] args) { - var generator = new Eto.GtkSharp.Platform(); + var platform = new Eto.GtkSharp.Platform(); + platform.Add(() => new NativeHostControls()); - var app = new TestApplication(generator); + var app = new TestApplication(platform); app.TestAssemblies.Add(typeof(Startup).Assembly); app.Run(); } diff --git a/test/Eto.Test.Mac/NativeHostControls.cs b/test/Eto.Test.Mac/NativeHostControls.cs new file mode 100755 index 000000000..be728a815 --- /dev/null +++ b/test/Eto.Test.Mac/NativeHostControls.cs @@ -0,0 +1,12 @@ +namespace Eto.Test.Mac +{ + class NativeHostControls : INativeHostControls + { + public IEnumerable GetNativeHostTests() + { + yield return new NativeHostTest("NSView", () => new NSButton { Title = "A NSButton"}); + yield return new NativeHostTest("IntPtr", () => new NSButton { Title = "An IntPtr handle button"}.Handle); + } + } +} + diff --git a/test/Eto.Test.Mac/Startup.cs b/test/Eto.Test.Mac/Startup.cs index ebe09a3ba..c0055df1d 100644 --- a/test/Eto.Test.Mac/Startup.cs +++ b/test/Eto.Test.Mac/Startup.cs @@ -13,6 +13,7 @@ static void Main(string[] args) var stopwatch = new Stopwatch(); stopwatch.Start(); var platform = new Eto.Mac.Platform(); + platform.Add(() => new NativeHostControls()); stopwatch.Stop(); var app = new TestApplication(platform); diff --git a/test/Eto.Test.WinForms/NativeHostControls.cs b/test/Eto.Test.WinForms/NativeHostControls.cs new file mode 100755 index 000000000..165fd835c --- /dev/null +++ b/test/Eto.Test.WinForms/NativeHostControls.cs @@ -0,0 +1,12 @@ +namespace Eto.Test.WinForms +{ + class NativeHostControls : INativeHostControls + { + public IEnumerable GetNativeHostTests() + { + yield return new NativeHostTest("HWND", () => new swf.UserControl { AutoScaleMode = swf.AutoScaleMode.Dpi, Controls = { new swf.Button { Text = "A HWND button" } } }.Handle); + yield return new NativeHostTest("WinForms", () => new swf.Button { Text = "A WinForms button" }); + } + } +} + diff --git a/test/Eto.Test.WinForms/Startup.cs b/test/Eto.Test.WinForms/Startup.cs index 124f9b58d..e763ac68f 100644 --- a/test/Eto.Test.WinForms/Startup.cs +++ b/test/Eto.Test.WinForms/Startup.cs @@ -9,6 +9,8 @@ class Startup static void Main(string[] args) { var platform = new Eto.WinForms.Platform(); + platform.Add(() => new NativeHostControls()); + var app = new TestApplication(platform); app.TestAssemblies.Add(typeof(Startup).Assembly); app.Run(); diff --git a/test/Eto.Test.Wpf/NativeHostControls.cs b/test/Eto.Test.Wpf/NativeHostControls.cs new file mode 100755 index 000000000..f0ad8d1cf --- /dev/null +++ b/test/Eto.Test.Wpf/NativeHostControls.cs @@ -0,0 +1,13 @@ +namespace Eto.Test.Wpf +{ + class NativeHostControls : INativeHostControls + { + public IEnumerable GetNativeHostTests() + { + yield return new NativeHostTest("FrameworkElement", () => new System.Windows.Controls.Button { Content = "A WPF button"}); + yield return new NativeHostTest("HWND", () => new System.Windows.Forms.Button { Text = "A HWND button"}.Handle); + yield return new NativeHostTest("WinForms", () => new System.Windows.Forms.Button { Text = "A WinForms button"}); + } + } +} + diff --git a/test/Eto.Test.Wpf/Startup.cs b/test/Eto.Test.Wpf/Startup.cs index 3de79f4fa..7f47bc5c8 100644 --- a/test/Eto.Test.Wpf/Startup.cs +++ b/test/Eto.Test.Wpf/Startup.cs @@ -9,6 +9,7 @@ class Startup static void Main(string[] args) { var platform = new Eto.Wpf.Platform(); + platform.Add(() => new NativeHostControls()); // optional - enables GDI text display mode /** diff --git a/test/Eto.Test/NativeHostControls.cs b/test/Eto.Test/NativeHostControls.cs new file mode 100755 index 000000000..6a357cf04 --- /dev/null +++ b/test/Eto.Test/NativeHostControls.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Eto.Test +{ + public interface INativeHostControls + { + public IEnumerable GetNativeHostTests(); + } + + public class NativeHostTest + { + Func _createControl; + + public NativeHostTest(string name, Func create) + { + Name = name; + _createControl = create; + } + public string Name { get; } + public object CreateControl() => _createControl(); + + public override string ToString() => Name; + } + + public static class NativeHostControls + { + static INativeHostControls Handler => Platform.Instance.CreateShared(); + + public static IEnumerable GetNativeHostTests() => Handler.GetNativeHostTests(); + } +} \ No newline at end of file diff --git a/test/Eto.Test/UnitTests/Forms/NativeControlHostTests.cs b/test/Eto.Test/UnitTests/Forms/NativeControlHostTests.cs new file mode 100755 index 000000000..c613f1458 --- /dev/null +++ b/test/Eto.Test/UnitTests/Forms/NativeControlHostTests.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using NUnit.Framework; + +namespace Eto.Test.UnitTests.Forms +{ + [TestFixture] + public class NativeControlHostTests : TestBase + { + + [ManualTest] + [TestCaseSource(typeof(NativeHostControls), nameof(NativeHostControls.GetNativeHostTests))] + public void NativeHostShouldShowControl(NativeHostTest test) + { + ManualForm("Control should show something", form => + { + form.Resizable = true; + return new NativeControlHost(test.CreateControl()); + }); + } + + [ManualTest] + [TestCaseSource(typeof(NativeHostControls), nameof(NativeHostControls.GetNativeHostTests))] + public void NativeHostInScrollableShouldBeClipped(NativeHostTest test) + { + ManualForm("Native control should not show when scrolled out of view", form => + { + form.Resizable = true; + var scrollable = new Scrollable + { + Size = new Size(400, 400), + Content = new TableLayout + { + Rows = { + new Panel { Content = "Before", Size = new Size(600, 400) }, + new NativeControlHost(test.CreateControl()), + new Panel { Content = "After", Size = new Size(600, 400) }, + } + } + }; + + return scrollable; + }); + } + } +} \ No newline at end of file