diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 76114c385b..340905a7f7 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -136,7 +136,7 @@ SliderPage.xaml - + TreeViewPage.xaml @@ -146,6 +146,7 @@ ToolTipPage.xaml + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index a3b8708c29..0be5d25a09 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -13,11 +13,20 @@ namespace ControlCatalog { this.InitializeComponent(); if (AvaloniaLocator.Current.GetService().GetRuntimeInfo().IsDesktop) - ((IList) this.FindControl("Sidebar").Items).Add(new TabItem() + { + IList tabItems = ((IList)this.FindControl("Sidebar").Items); + tabItems.Add(new TabItem() { Header = "Dialogs", Content = new DialogsPage() }); + tabItems.Add(new TabItem() + { + Header = "Screens", + Content = new ScreenPage() + }); + + } } private void InitializeComponent() diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs new file mode 100644 index 0000000000..34aa85b8aa --- /dev/null +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -0,0 +1,62 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using Avalonia.Platform; + +namespace ControlCatalog.Pages +{ + public class ScreenPage : UserControl + { + private double _leftMost; + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + Window w = (Window)VisualRoot; + w.PositionChanged += (sender, args) => InvalidateVisual(); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + Window w = (Window)VisualRoot; + Screen[] screens = w.Screens.All; + + Pen p = new Pen(Brushes.Black); + if (screens != null) + foreach (Screen screen in screens) + { + if (screen.Bounds.X / 10f < _leftMost) + { + _leftMost = screen.Bounds.X / 10f; + InvalidateVisual(); + return; + } + + Rect boundsRect = new Rect(screen.Bounds.X / 10f + Math.Abs(_leftMost), screen.Bounds.Y / 10f, screen.Bounds.Width / 10f, + screen.Bounds.Height / 10f); + Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f, screen.WorkingArea.Width / 10f, + screen.WorkingArea.Height / 10f); + context.DrawRectangle(p, boundsRect); + context.DrawRectangle(p, workingAreaRect); + + FormattedText text = new FormattedText(); + text.Text = $"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}"; + context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height), text); + + text.Text = $"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}"; + context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 20), text); + + text.Text = $"Primary: {screen.Primary}"; + context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 40), text); + + text.Text = $"Current: {screen.Equals(w.Screens.ScreenFromBounds(new Rect(w.Position, w.Bounds.Size)))}"; + context.DrawText(Brushes.Black, boundsRect.Position.WithY(boundsRect.Size.Height + 60), text); + } + + context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10, w.Bounds.Width / 10, w.Bounds.Height / 10)); + } + } +} \ No newline at end of file diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs index efbc71b808..0b683239fb 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/PopupImpl.cs @@ -36,6 +36,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform _clientSize = value; UpdateParams(); } + + public IScreenImpl Screen { get; } public Point Position { diff --git a/src/Avalonia.Controls/Platform/IScreenImpl.cs b/src/Avalonia.Controls/Platform/IScreenImpl.cs new file mode 100644 index 0000000000..dafa1de473 --- /dev/null +++ b/src/Avalonia.Controls/Platform/IScreenImpl.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Platform +{ + public interface IScreenImpl + { + int ScreenCount { get; } + + Screen[] AllScreens { get; } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index a5c7c60843..0a01cf3df4 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -65,5 +65,10 @@ namespace Avalonia.Platform /// Sets the client size of the toplevel. /// void Resize(Size clientSize); + + /// + /// Gets platform specific display information + /// + IScreenImpl Screen { get; } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Platform/Screen.cs b/src/Avalonia.Controls/Platform/Screen.cs new file mode 100644 index 0000000000..5aa8c2c392 --- /dev/null +++ b/src/Avalonia.Controls/Platform/Screen.cs @@ -0,0 +1,18 @@ +namespace Avalonia.Platform +{ + public class Screen + { + public Rect Bounds { get; } + + public Rect WorkingArea { get; } + + public bool Primary { get; } + + public Screen(Rect bounds, Rect workingArea, bool primary) + { + this.Bounds = bounds; + this.WorkingArea = workingArea; + this.Primary = primary; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Screens.cs b/src/Avalonia.Controls/Screens.cs new file mode 100644 index 0000000000..b8ddce3aea --- /dev/null +++ b/src/Avalonia.Controls/Screens.cs @@ -0,0 +1,54 @@ +using System.Linq; +using Avalonia.Platform; +using Avalonia.Utilities; +using Avalonia.VisualTree; + +namespace Avalonia.Controls +{ + public class Screens + { + private readonly IScreenImpl _iScreenImpl; + + public int ScreenCount => _iScreenImpl.ScreenCount; + public Screen[] All => _iScreenImpl?.AllScreens; + public Screen Primary => All.FirstOrDefault(x => x.Primary); + + public Screens(IScreenImpl iScreenImpl) + { + _iScreenImpl = iScreenImpl; + } + + public Screen ScreenFromBounds(Rect bounds){ + + Screen currMaxScreen = null; + double maxAreaSize = 0; + foreach (Screen screen in All) + { + double left = MathUtilities.Clamp(bounds.X, screen.Bounds.X, screen.Bounds.X + screen.Bounds.Width); + double top = MathUtilities.Clamp(bounds.Y, screen.Bounds.Y, screen.Bounds.Y + screen.Bounds.Height); + double right = MathUtilities.Clamp(bounds.X + bounds.Width, screen.Bounds.X, screen.Bounds.X + screen.Bounds.Width); + double bottom = MathUtilities.Clamp(bounds.Y + bounds.Height, screen.Bounds.Y, screen.Bounds.Y + screen.Bounds.Height); + double area = (right - left) * (bottom - top); + if (area > maxAreaSize) + { + maxAreaSize = area; + currMaxScreen = screen; + } + } + + return currMaxScreen; + } + + public Screen SceenFromPoint(Point point) + { + return All.FirstOrDefault(x=>x.Bounds.Contains(point)); + } + + public Screen ScreenFromVisual(IVisual visual) + { + Point tl = visual.PointToScreen(visual.Bounds.TopLeft); + Point br = visual.PointToScreen(visual.Bounds.BottomRight); + return ScreenFromBounds(new Rect(tl,br)); + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 9c0245a714..64912f20df 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -119,6 +119,7 @@ namespace Avalonia.Controls : base(impl) { _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); + Screens = new Screens(PlatformImpl?.Screen); } /// @@ -135,6 +136,8 @@ namespace Avalonia.Controls remove { _nameScope.Unregistered -= value; } } + public Screens Screens { get; private set; } + /// /// Gets the platform-specific window implementation. /// diff --git a/src/Gtk/Avalonia.Gtk/Avalonia.Gtk.csproj b/src/Gtk/Avalonia.Gtk/Avalonia.Gtk.csproj index 6049869424..1142b32dbf 100644 --- a/src/Gtk/Avalonia.Gtk/Avalonia.Gtk.csproj +++ b/src/Gtk/Avalonia.Gtk/Avalonia.Gtk.csproj @@ -48,6 +48,7 @@ + diff --git a/src/Gtk/Avalonia.Gtk/ScreenImpl.cs b/src/Gtk/Avalonia.Gtk/ScreenImpl.cs new file mode 100644 index 0000000000..a5082ca59a --- /dev/null +++ b/src/Gtk/Avalonia.Gtk/ScreenImpl.cs @@ -0,0 +1,42 @@ +using System; +using Avalonia.Controls; +using Avalonia.Platform; +using Avalonia.Utilities; +using Gdk; +using Screen = Avalonia.Platform.Screen; +using Window = Gtk.Window; + +namespace Avalonia.Gtk +{ + internal class ScreenImpl : IScreenImpl + { + private Window window; + + public int ScreenCount + { + get => window.Display.DefaultScreen.NMonitors; + } + public Screen[] AllScreens { + get + { + Screen[] screens = new Screen[ScreenCount]; + var screen = window.Display.DefaultScreen; + + for (short i = 0; i < screens.Length; i++) + { + Rectangle geometry = screen.GetMonitorGeometry(i); + Rect geometryRect = new Rect(geometry.X, geometry.Y, geometry.Width, geometry.Height); + Screen s = new Screen(geometryRect, geometryRect, false); + screens[i] = s; + } + + return screens; + } + } + + public ScreenImpl(Window window) + { + this.window = window; + } + } +} \ No newline at end of file diff --git a/src/Gtk/Avalonia.Gtk/WindowImpl.cs b/src/Gtk/Avalonia.Gtk/WindowImpl.cs index 2c1826c509..ef8f5f95cb 100644 --- a/src/Gtk/Avalonia.Gtk/WindowImpl.cs +++ b/src/Gtk/Avalonia.Gtk/WindowImpl.cs @@ -1,5 +1,6 @@ using System; using System.Reactive.Disposables; +using Avalonia.Controls; using Avalonia.Platform; using Gdk; @@ -66,6 +67,8 @@ namespace Avalonia.Gtk } } + public IScreenImpl Screen => new ScreenImpl(Window); + public void Resize(Size value) { Window.Resize((int)value.Width, (int)value.Height); diff --git a/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj b/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj index b58a83b3d2..59bd58916b 100644 --- a/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj +++ b/src/Gtk/Avalonia.Gtk3/Avalonia.Gtk3.csproj @@ -53,6 +53,8 @@ + + diff --git a/src/Gtk/Avalonia.Gtk3/GtkScreen.cs b/src/Gtk/Avalonia.Gtk3/GtkScreen.cs new file mode 100644 index 0000000000..7c28dd47b9 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/GtkScreen.cs @@ -0,0 +1,24 @@ +using Avalonia.Platform; + +namespace Avalonia.Gtk3 +{ + public class GtkScreen : Screen + { + private readonly int _screenId; + + public GtkScreen(Rect bounds, Rect workingArea, bool primary, int screenId) : base(bounds, workingArea, primary) + { + this._screenId = screenId; + } + + public override int GetHashCode() + { + return _screenId; + } + + public override bool Equals(object obj) + { + return (obj is GtkScreen screen) ? this._screenId == screen._screenId : base.Equals(obj); + } + } +} \ No newline at end of file diff --git a/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs b/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs index 9766e8ca36..8d14515d28 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/GObject.cs @@ -46,7 +46,30 @@ namespace Avalonia.Gtk3.Interop class GtkImContext : GObject { - + } + + class GdkScreen : GObject + { + public GdkScreen() : base(IntPtr.Zero, false) + { + } + + public GdkScreen(IntPtr handle, bool owned = true) : base(handle, owned) + { + this.handle = handle; + } + } + + class UnownedGdkScreen : GdkScreen + { + public UnownedGdkScreen() : base(IntPtr.Zero, false) + { + } + + public UnownedGdkScreen(IntPtr handle, bool owned = true) : base(IntPtr.Zero, false) + { + this.handle = handle; + } } class GtkDialog : GtkWindow diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs index ffc1d380a3..35aef7b4e5 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs @@ -20,6 +20,27 @@ namespace Avalonia.Gtk3.Interop { public static class D { + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] + public delegate gint16 gdk_display_get_n_screens(IntPtr display); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] + public delegate UnownedGdkScreen gdk_display_get_screen(IntPtr display, gint16 num); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] + public delegate UnownedGdkScreen gdk_display_get_default_screen (IntPtr display); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] + public delegate gint16 gdk_screen_get_n_monitors(GdkScreen screen); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] + public delegate gint16 gdk_screen_get_primary_monitor(GdkScreen screen); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] + public delegate void gdk_screen_get_monitor_geometry(GdkScreen screen, gint16 num, ref GdkRectangle rect); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] + public delegate void gdk_screen_get_monitor_workarea(GdkScreen screen, gint16 num, ref GdkRectangle rect); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] public delegate IntPtr gtk_application_new(Utf8Buffer appId, int flags); @@ -288,6 +309,9 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate bool signal_onevent(IntPtr gtkWidget, IntPtr ev, IntPtr userData); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void monitors_changed(IntPtr screen, IntPtr userData); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate bool signal_commit(IntPtr gtkWidget, IntPtr utf8string, IntPtr userData); @@ -298,7 +322,13 @@ namespace Avalonia.Gtk3.Interop public delegate void GtkClipboardTextReceivedFunc(IntPtr clipboard, IntPtr utf8string, IntPtr userdata); } - + public static D.gdk_display_get_n_screens GdkDisplayGetNScreens; + public static D.gdk_display_get_screen GdkDisplayGetScreen; + public static D.gdk_display_get_default_screen GdkDisplayGetDefaultScreen; + public static D.gdk_screen_get_n_monitors GdkScreenGetNMonitors; + public static D.gdk_screen_get_primary_monitor GdkScreenGetPrimaryMonitor; + public static D.gdk_screen_get_monitor_geometry GdkScreenGetMonitorGeometry; + public static D.gdk_screen_get_monitor_workarea GdkScreenGetMonitorWorkarea; public static D.gtk_window_set_decorated GtkWindowSetDecorated; public static D.gtk_window_set_skip_taskbar_hint GtkWindowSetSkipTaskbarHint; public static D.gtk_window_get_skip_taskbar_hint GtkWindowGetSkipTaskbarHint; diff --git a/src/Gtk/Avalonia.Gtk3/ScreenImpl.cs b/src/Gtk/Avalonia.Gtk3/ScreenImpl.cs new file mode 100644 index 0000000000..0f4fbd54bd --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/ScreenImpl.cs @@ -0,0 +1,57 @@ +using System; +using Avalonia.Controls; +using Avalonia.Gtk3.Interop; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Gtk3 +{ + internal class ScreenImpl : IScreenImpl + { + public int ScreenCount + { + get => _allScreens.Length; + } + + private Screen[] _allScreens; + public Screen[] AllScreens + { + get + { + if (_allScreens == null) + { + IntPtr display = Native.GdkGetDefaultDisplay(); + GdkScreen screen = Native.GdkDisplayGetDefaultScreen(display); + short primary = Native.GdkScreenGetPrimaryMonitor(screen); + Screen[] screens = new Screen[ScreenCount]; + for (short i = 0; i < screens.Length; i++) + { + GdkRectangle workArea = new GdkRectangle(), geometry = new GdkRectangle(); + Native.GdkScreenGetMonitorGeometry(screen, i, ref geometry); + Native.GdkScreenGetMonitorWorkarea(screen, i, ref workArea); + Rect workAreaRect = new Rect(workArea.X, workArea.Y, workArea.Width, workArea.Height); + Rect geometryRect = new Rect(geometry.X, geometry.Y, geometry.Width, geometry.Height); + GtkScreen s = new GtkScreen(geometryRect, workAreaRect, i == primary, i); + screens[i] = s; + } + + _allScreens = screens; + } + + return _allScreens; + } + } + + public ScreenImpl() + { + IntPtr display = Native.GdkGetDefaultDisplay(); + GdkScreen screen = Native.GdkDisplayGetDefaultScreen(display); + Signal.Connect(screen, "monitors-changed", MonitorsChanged); + } + + private unsafe void MonitorsChanged(IntPtr screen, IntPtr userData) + { + _allScreens = null; + } + } +} \ No newline at end of file diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index 4464059efb..5725ee2596 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -341,6 +341,8 @@ namespace Avalonia.Gtk3 return; Native.GtkWindowResize(GtkWidget, (int)value.Width, (int)value.Height); } + + public IScreenImpl Screen { get; } = new ScreenImpl(); public Point Position { diff --git a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs index e6935c4f20..a4bc45c0a1 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Linq.Expressions; using Avalonia.Controls; using Avalonia.Gtk3.Interop; using Avalonia.Platform; diff --git a/src/OSX/Avalonia.MonoMac/Helpers.cs b/src/OSX/Avalonia.MonoMac/Helpers.cs index b4e3f7e0b7..454b775cc0 100644 --- a/src/OSX/Avalonia.MonoMac/Helpers.cs +++ b/src/OSX/Avalonia.MonoMac/Helpers.cs @@ -1,6 +1,8 @@ using System; + using System.ComponentModel; using MonoMac.AppKit; using MonoMac.CoreGraphics; + using MonoMac.OpenGL; namespace Avalonia.MonoMac { @@ -27,5 +29,14 @@ namespace Avalonia.MonoMac return new CGPoint(pt.X, t - pt.Y); } + public static Rect ConvertRectY(this Rect rect) + { + return new Rect(rect.Position.WithY(rect.Y + rect.Height).ConvertPointY(), rect.Size); + } + + public static CGRect ConvertRectY(this CGRect rect) + { + return new CGRect(new CGPoint(rect.X, rect.Y + rect.Height).ConvertPointY(), rect.Size); + } } } diff --git a/src/OSX/Avalonia.MonoMac/MacScreen.cs b/src/OSX/Avalonia.MonoMac/MacScreen.cs new file mode 100644 index 0000000000..3d5ab0769f --- /dev/null +++ b/src/OSX/Avalonia.MonoMac/MacScreen.cs @@ -0,0 +1,25 @@ +using System; +using Avalonia.Platform; + +namespace Avalonia.MonoMac +{ + public class MacScreen : Screen + { + private readonly IntPtr handle; + + public MacScreen(Rect bounds, Rect workingArea, bool primary, IntPtr handle) : base(bounds, workingArea, primary) + { + this.handle = handle; + } + + public override int GetHashCode() + { + return (int)handle; + } + + public override bool Equals(object obj) + { + return (obj is MacScreen screen) ? this.handle == screen.handle : base.Equals(obj); + } + } +} \ No newline at end of file diff --git a/src/OSX/Avalonia.MonoMac/ScreenImpl.cs b/src/OSX/Avalonia.MonoMac/ScreenImpl.cs new file mode 100644 index 0000000000..bf8545a300 --- /dev/null +++ b/src/OSX/Avalonia.MonoMac/ScreenImpl.cs @@ -0,0 +1,51 @@ +using Avalonia.Controls; +using Avalonia.Platform; +using Avalonia.Utilities; +using MonoMac.AppKit; +using MonoMac.Foundation; + +namespace Avalonia.MonoMac +{ + public class ScreenImpl : IScreenImpl + { + private const string NSApplicationDidChangeScreenParametersNotification = "NSApplicationDidChangeScreenParametersNotification"; + + public int ScreenCount + { + get => NSScreen.Screens.Length; + } + + private Screen[] _allScreens; + public Screen[] AllScreens + { + get + { + if (_allScreens == null) + { + NSScreen[] screens = NSScreen.Screens; + Screen[] s = new Screen[screens.Length]; + NSScreen primary = NSScreen.MainScreen; + for (int i = 0; i < screens.Length; i++) + { + Rect bounds = screens[i].Frame.ToAvaloniaRect().ConvertRectY(); + Rect workArea = screens[i].VisibleFrame.ToAvaloniaRect().ConvertRectY(); + s[i] = new MacScreen(bounds, workArea, i == 0, screens[i].Handle); + } + + _allScreens = s; + } + return _allScreens; + } + } + + public ScreenImpl() + { + NSNotificationCenter.DefaultCenter.AddObserver(NSApplicationDidChangeScreenParametersNotification, MonitorsChanged); + } + + private void MonitorsChanged(NSNotification notification) + { + _allScreens = null; + } + } +} \ No newline at end of file diff --git a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs index fdb2434698..71f008e074 100644 --- a/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs +++ b/src/OSX/Avalonia.MonoMac/WindowBaseImpl.cs @@ -153,6 +153,8 @@ namespace Avalonia.MonoMac Position = pos; } + public IScreenImpl Screen { get; } = new ScreenImpl(); + public override Point PointToClient(Point point) { var cocoaScreenPoint = point.ToMonoMacPoint().ConvertPointY(); diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.Shared.projitems b/src/Windows/Avalonia.Win32/Avalonia.Win32.Shared.projitems index 4599a0741e..6f13747cb6 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.Shared.projitems +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.Shared.projitems @@ -20,10 +20,12 @@ + + Properties\SharedAssemblyInfo.cs diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 4fed7a34f4..fb4aefbf36 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -616,6 +616,12 @@ namespace Avalonia.Win32.Interop public const int SizeOf_BITMAPINFOHEADER = 40; + [DllImport("user32.dll")] + public static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, + MonitorEnumDelegate lpfnEnum, IntPtr dwData); + + public delegate bool MonitorEnumDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr dwData); + [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr GetDC(IntPtr hWnd); @@ -903,6 +909,9 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern IntPtr MonitorFromPoint(POINT pt, MONITOR dwFlags); + [DllImport("user32.dll")] + public static extern IntPtr MonitorFromRect(RECT rect, MONITOR dwFlags); + [DllImport("user32.dll")] public static extern IntPtr MonitorFromWindow(IntPtr hwnd, MONITOR dwFlags); @@ -1021,6 +1030,14 @@ namespace Avalonia.Win32.Interop public int top; public int right; public int bottom; + + public RECT(Rect rect) + { + left = (int)rect.X; + top = (int)rect.Y; + right = (int)(rect.X + rect.Width); + bottom = (int)(rect.Y + rect.Height); + } } public struct TRACKMOUSEEVENT diff --git a/src/Windows/Avalonia.Win32/ScreenImpl.cs b/src/Windows/Avalonia.Win32/ScreenImpl.cs new file mode 100644 index 0000000000..4f4331e461 --- /dev/null +++ b/src/Windows/Avalonia.Win32/ScreenImpl.cs @@ -0,0 +1,64 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Platform; +using Avalonia.Utilities; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +#if NETSTANDARD +using Win32Exception = Avalonia.Win32.NetStandard.AvaloniaWin32Exception; +#endif + +namespace Avalonia.Win32 +{ + public class ScreenImpl : IScreenImpl + { + public int ScreenCount + { + get => GetSystemMetrics(SystemMetric.SM_CMONITORS); + } + + private Screen[] _allScreens; + public Screen[] AllScreens + { + get + { + if (_allScreens == null) + { + int index = 0; + Screen[] screens = new Screen[ScreenCount]; + EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, + (IntPtr monitor, IntPtr hdcMonitor, ref Rect lprcMonitor, IntPtr data) => + { + MONITORINFO monitorInfo = new MONITORINFO(); + if (GetMonitorInfo(monitor, monitorInfo)) + { + RECT bounds = monitorInfo.rcMonitor; + RECT workingArea = monitorInfo.rcWork; + Rect avaloniaBounds = new Rect(bounds.left, bounds.top, bounds.right - bounds.left, + bounds.bottom - bounds.top); + Rect avaloniaWorkArea = + new Rect(workingArea.left, workingArea.top, workingArea.right - bounds.left, + workingArea.bottom - bounds.top); + screens[index] = + new WinScreen(avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1, + monitor); + index++; + } + return true; + }, IntPtr.Zero); + _allScreens = screens; + } + return _allScreens; + } + } + + public void InvalidateScreensCache() + { + _allScreens = null; + } + } +} diff --git a/src/Windows/Avalonia.Win32/WinScreen.cs b/src/Windows/Avalonia.Win32/WinScreen.cs new file mode 100644 index 0000000000..2f8dcb0f3e --- /dev/null +++ b/src/Windows/Avalonia.Win32/WinScreen.cs @@ -0,0 +1,25 @@ +using System; +using Avalonia.Platform; + +namespace Avalonia.Win32 +{ + public class WinScreen : Screen + { + private readonly IntPtr _hMonitor; + + public WinScreen(Rect bounds, Rect workingArea, bool primary, IntPtr hMonitor) : base(bounds, workingArea, primary) + { + this._hMonitor = hMonitor; + } + + public override int GetHashCode() + { + return (int)_hMonitor; + } + + public override bool Equals(object obj) + { + return (obj is WinScreen screen) ? this._hMonitor == screen._hMonitor : base.Equals(obj); + } + } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9c13a6d9b8..dab7f0ba04 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -103,6 +103,7 @@ namespace Avalonia.Win32 } } + public IScreenImpl Screen => new ScreenImpl(); public IRenderer CreateRenderer(IRenderRoot root) { @@ -598,6 +599,10 @@ namespace Avalonia.Win32 case UnmanagedMethods.WindowsMessage.WM_MOVE: PositionChanged?.Invoke(new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16))); return IntPtr.Zero; + + case UnmanagedMethods.WindowsMessage.WM_DISPLAYCHANGE: + (Screen as ScreenImpl)?.InvalidateScreensCache(); + return IntPtr.Zero; } #if USE_MANAGED_DRAG