diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 6089a06d4fb..036484609e2 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -40,13 +40,13 @@ "src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj", "src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj", "src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj", - "src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj", - "src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj", - "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", "src\\tools\\Avalonia.Analyzers\\Avalonia.Analyzers.csproj", "src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj", "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj", + "src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj", + "src\\Windows\\Avalonia.Win32.Interoperability\\Avalonia.Win32.Interoperability.csproj", + "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", diff --git a/src/Windows/Avalonia.Win32/IconImpl.cs b/src/Windows/Avalonia.Win32/IconImpl.cs index 9fd62b7981e..8897fb610e7 100644 --- a/src/Windows/Avalonia.Win32/IconImpl.cs +++ b/src/Windows/Avalonia.Win32/IconImpl.cs @@ -1,27 +1,88 @@ using System; -using System.Drawing; using System.IO; using Avalonia.Platform; using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - class IconImpl : IWindowIconImpl + internal class IconImpl : IWindowIconImpl, IDisposable { - private readonly Win32Icon _icon; - private readonly byte[] _iconData; + private readonly Win32Icon _smallIcon; + private readonly Win32Icon _bigIcon; - public IconImpl(Win32Icon icon, byte[] iconData) + private static readonly int s_taskbarIconSize = Win32Platform.WindowsVersion < PlatformConstants.Windows10 ? 32 : 24; + + public IconImpl(Stream smallIcon, Stream bigIcon) + { + _smallIcon = CreateIconImpl(smallIcon); + _bigIcon = CreateIconImpl(bigIcon); + } + + public IconImpl(Stream icon) + { + _smallIcon = _bigIcon = CreateIconImpl(icon); + } + + private static Win32Icon CreateIconImpl(Stream stream) + { + if (stream.CanSeek) + { + stream.Position = 0; + } + + if (stream is MemoryStream memoryStream) + { + var iconData = memoryStream.ToArray(); + + return new Win32Icon(iconData); + } + else + { + using var ms = new MemoryStream(); + stream.CopyTo(ms); + + ms.Position = 0; + + var iconData = ms.ToArray(); + + return new Win32Icon(iconData); + } + } + + // GetSystemMetrics returns values scaled for the primary monitor, as of the time at which the process started. + // This is no good for a per-monitor DPI aware application. GetSystemMetricsForDpi would solve the problem, + // but is only available in Windows 10 version 1607 and later. So instead, we just hard-code the 96dpi icon sizes. + + public Win32Icon LoadSmallIcon(double scaleFactor) => new(_smallIcon, GetScaledSize(16, scaleFactor)); + + public Win32Icon LoadBigIcon(double scaleFactor) + { + var targetSize = GetScaledSize(s_taskbarIconSize, scaleFactor); + var icon = new Win32Icon(_bigIcon, targetSize); + + // The exact size of a taskbar icon in Windows 10 and later is 24px @ 96dpi. But if an ICO file doesn't have + // that size, 16px can be selected instead. If this happens, fall back to a 32 pixel icon. Windows will downscale it. + if (s_taskbarIconSize == 24 && icon.Size.Width < targetSize.Width) + { + icon.Dispose(); + icon = new(_bigIcon, GetScaledSize(32, scaleFactor)); + } + + return icon; + } + + private static PixelSize GetScaledSize(int baseSize, double factor) { - _icon = icon; - _iconData = iconData; + var scaled = (int)Math.Ceiling(baseSize * factor); + return new(scaled, scaled); } - public IntPtr HIcon => _icon.Handle; + public void Save(Stream outputStream) => _bigIcon.CopyTo(outputStream); - public void Save(Stream outputStream) + public void Dispose() { - outputStream.Write(_iconData, 0, _iconData.Length); + _smallIcon.Dispose(); + _bigIcon.Dispose(); } } } diff --git a/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs b/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs index 7eb457480b9..316be69d970 100644 --- a/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs +++ b/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using System.Threading; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32.Interop @@ -7,8 +8,33 @@ namespace Avalonia.Win32.Interop internal class TaskBarList { private static IntPtr s_taskBarList; + private static bool s_initialized; + private static object s_lock = new(); + private static HrInit? s_hrInitDelegate; private static MarkFullscreenWindow? s_markFullscreenWindowDelegate; + private static SetOverlayIcon? s_setOverlayIconDelegate; + + private static unsafe IntPtr Init() + { + Guid clsid = ShellIds.TaskBarList; + Guid iid = ShellIds.ITaskBarList2; + + int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var instance); + + var ptr = (ITaskBarList3VTable**)instance.ToPointer(); + + s_hrInitDelegate ??= Marshal.GetDelegateForFunctionPointer((*ptr)->HrInit); + + if (s_hrInitDelegate(instance) != HRESULT.S_OK) + { + return IntPtr.Zero; + } + + return instance; + } + + private static IntPtr LazyInit() => LazyInitializer.EnsureInitialized(ref s_taskBarList, ref s_initialized, ref s_lock, Init); /// /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc @@ -17,34 +43,31 @@ internal class TaskBarList /// Fullscreen state. public static unsafe void MarkFullscreen(IntPtr hwnd, bool fullscreen) { - if (s_taskBarList == IntPtr.Zero) - { - Guid clsid = ShellIds.TaskBarList; - Guid iid = ShellIds.ITaskBarList2; + LazyInit(); - int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out s_taskBarList); - - if (s_taskBarList != IntPtr.Zero) - { - var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer(); + if (s_taskBarList != IntPtr.Zero) + { + var ptr = (ITaskBarList3VTable**)s_taskBarList.ToPointer(); - s_hrInitDelegate ??= Marshal.GetDelegateForFunctionPointer((*ptr)->HrInit); + s_markFullscreenWindowDelegate ??= + Marshal.GetDelegateForFunctionPointer((*ptr)->MarkFullscreenWindow); - if (s_hrInitDelegate(s_taskBarList) != HRESULT.S_OK) - { - s_taskBarList = IntPtr.Zero; - } - } + s_markFullscreenWindowDelegate(s_taskBarList, hwnd, fullscreen); } + } + + public static unsafe void SetOverlayIcon(IntPtr hwnd, IntPtr hIcon, string? description) + { + LazyInit(); if (s_taskBarList != IntPtr.Zero) { - var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer(); + var ptr = (ITaskBarList3VTable**)s_taskBarList.ToPointer(); - s_markFullscreenWindowDelegate ??= - Marshal.GetDelegateForFunctionPointer((*ptr)->MarkFullscreenWindow); + s_setOverlayIconDelegate ??= + Marshal.GetDelegateForFunctionPointer((*ptr)->SetOverlayIcon); - s_markFullscreenWindowDelegate(s_taskBarList, hwnd, fullscreen); + s_setOverlayIconDelegate(s_taskBarList, hwnd, hIcon, description); } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 65e06b54dfc..f9f3fd30bac 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1451,6 +1451,9 @@ public enum ClassLongIndex : int [DllImport("shell32", CharSet = CharSet.Auto)] public static extern int Shell_NotifyIcon(NIM dwMessage, NOTIFYICONDATA lpData); + [DllImport("shell32", CharSet = CharSet.Auto)] + public static extern nint SHAppBarMessage(AppBarMessage dwMessage, ref APPBARDATA lpData); + [DllImport("user32.dll", EntryPoint = "SetClassLongPtrW", ExactSpelling = true)] private static extern IntPtr SetClassLong64(IntPtr hWnd, ClassLongIndex nIndex, IntPtr dwNewLong); @@ -1485,8 +1488,8 @@ public static IntPtr GetClassLongPtr(IntPtr hWnd, int nIndex) internal static extern IntPtr SetCursor(IntPtr hCursor); [DllImport("ole32.dll", PreserveSig = true)] - internal static extern int CoCreateInstance(ref Guid clsid, - IntPtr ignore1, int ignore2, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown), Out] out object pUnkOuter); + internal static extern int CoCreateInstance(in Guid clsid, + IntPtr ignore1, int ignore2, in Guid iid, [MarshalAs(UnmanagedType.IUnknown), Out] out object pUnkOuter); [DllImport("ole32.dll", PreserveSig = true)] internal static extern int CoCreateInstance(ref Guid clsid, @@ -1605,6 +1608,10 @@ int cbSize [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")] public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + [return: MarshalAs(UnmanagedType.Bool)] + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "SendMessageW")] + public static extern bool SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + [DllImport("gdi32.dll")] public static extern int SetDIBitsToDevice(IntPtr hdc, int XDest, int YDest, uint dwWidth, uint dwHeight, int XSrc, int YSrc, uint uStartScan, uint cScanLines, @@ -1724,7 +1731,7 @@ public enum LayeredWindowFlags LWA_ALPHA = 0x00000002, LWA_COLORKEY = 0x00000001, } - + [DllImport("user32.dll")] public static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, LayeredWindowFlags dwFlags); @@ -1829,7 +1836,7 @@ internal enum MsgWaitForMultipleObjectsFlags private static extern int IntMsgWaitForMultipleObjectsEx(int nCount, IntPtr[]? pHandles, int dwMilliseconds, QueueStatusFlags dwWakeMask, MsgWaitForMultipleObjectsFlags dwFlags); - internal static int MsgWaitForMultipleObjectsEx(int nCount, IntPtr[]? pHandles, int dwMilliseconds, + internal static int MsgWaitForMultipleObjectsEx(int nCount, IntPtr[]? pHandles, int dwMilliseconds, QueueStatusFlags dwWakeMask, MsgWaitForMultipleObjectsFlags dwFlags) { int result = IntMsgWaitForMultipleObjectsEx(nCount, pHandles, dwMilliseconds, dwWakeMask, dwFlags); @@ -2413,7 +2420,9 @@ public enum HRESULT : uint public enum Icons { ICON_SMALL = 0, - ICON_BIG = 1 + ICON_BIG = 1, + /// The small icon, but with the system theme variant rather than the window's own theme. Requested by other processes, e.g. the taskbar and Task Manager. + ICON_SMALL2 = 2, } public static class ShellIds @@ -2436,9 +2445,10 @@ public struct COMDLG_FILTERSPEC } public delegate void MarkFullscreenWindow(IntPtr This, IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fullscreen); + public delegate void SetOverlayIcon(IntPtr This, IntPtr hWnd, IntPtr hIcon, [MarshalAs(UnmanagedType.LPWStr)] string? pszDescription); public delegate HRESULT HrInit(IntPtr This); - public struct ITaskBarList2VTable + public struct ITaskBarList3VTable { public IntPtr IUnknown1; public IntPtr IUnknown2; @@ -2449,6 +2459,36 @@ public struct ITaskBarList2VTable public IntPtr ActivateTab; public IntPtr SetActiveAlt; public IntPtr MarkFullscreenWindow; + public IntPtr SetProgressValue; + public IntPtr SetProgressState; + public IntPtr RegisterTab; + public IntPtr UnregisterTab; + public IntPtr SetTabOrder; + public IntPtr SetTabActive; + public IntPtr ThumbBarAddButtons; + public IntPtr ThumbBarUpdateButtons; + public IntPtr ThumbBarSetImageList; + public IntPtr SetOverlayIcon; + public IntPtr SetThumbnailTooltip; + public IntPtr SetThumbnailClip; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct APPBARDATA + { + private static readonly int s_size = Marshal.SizeOf(typeof(APPBARDATA)); + + public int cbSize; + public nint hWnd; + public uint uCallbackMessage; + public uint uEdge; + public RECT rc; + public int lParam; + + public APPBARDATA() + { + cbSize = s_size; + } } } @@ -2542,6 +2582,12 @@ internal enum NIM : uint SETVERSION = 0x00000004 } + internal enum AppBarMessage : uint + { + ABM_GETSTATE = 0x00000004, + ABM_GETTASKBARPOS = 0x00000005, + } + [Flags] internal enum NIF : uint { diff --git a/src/Windows/Avalonia.Win32/Interop/Win32Icon.cs b/src/Windows/Avalonia.Win32/Interop/Win32Icon.cs index 05927394b1b..d0da2226cd5 100644 --- a/src/Windows/Avalonia.Win32/Interop/Win32Icon.cs +++ b/src/Windows/Avalonia.Win32/Interop/Win32Icon.cs @@ -25,9 +25,11 @@ public Win32Icon(IBitmapImpl bitmap, PixelPoint hotSpot = default) Handle = CreateIcon(bmp, hotSpot); } - public Win32Icon(byte[] iconData) + public Win32Icon(byte[] iconData, PixelSize size = default) { - Handle = LoadIconFromData(iconData); + _bytes = iconData; + + (Handle, Size) = LoadIconFromData(iconData, ReplaceZeroesWithSystemMetrics(size)); if (Handle == IntPtr.Zero) { using var bmp = new Bitmap(new MemoryStream(iconData)); @@ -35,7 +37,22 @@ public Win32Icon(byte[] iconData) } } + public Win32Icon(Win32Icon original, PixelSize size = default) + { + _bytes = original._bytes ?? throw new ArgumentException("Original icon was created from a bitmap and cannot be copied.", nameof(original)); + + (Handle, Size) = LoadIconFromData(_bytes, ReplaceZeroesWithSystemMetrics(size)); + if (Handle == IntPtr.Zero) + { + using var bmp = new Bitmap(new MemoryStream(_bytes)); + Handle = CreateIcon(bmp); + } + } + public IntPtr Handle { get; private set; } + public PixelSize Size { get; } + + private readonly byte[]? _bytes; IntPtr CreateIcon(Bitmap bitmap, PixelPoint hotSpot = default) { @@ -160,19 +177,15 @@ public struct ICONDIRENTRY private static int s_bitDepth; - static unsafe IntPtr LoadIconFromData(byte[] iconData, int width = 0, int height = 0) + private static PixelSize ReplaceZeroesWithSystemMetrics(PixelSize pixelSize) => new( + pixelSize.Width == 0 ? UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXICON) : pixelSize.Width, + pixelSize.Height == 0 ? UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CYICON) : pixelSize.Height + ); + + private static unsafe (IntPtr, PixelSize) LoadIconFromData(byte[] iconData, PixelSize size) { if (iconData.Length < sizeof(ICONDIR)) - return IntPtr.Zero; - - // Get the correct width and height. - if (width == 0) - width = UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXICON); - - if (height == 0) - height = UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CYICON); - - + return default; if (s_bitDepth == 0) { @@ -196,14 +209,14 @@ static unsafe IntPtr LoadIconFromData(byte[] iconData, int width = 0, int height if (dir->idReserved != 0 || dir->idType != 1 || dir->idCount == 0) { - return IntPtr.Zero; + return default; } byte bestWidth = 0; byte bestHeight = 0; if (sizeof(ICONDIRENTRY) * (dir->idCount - 1) + sizeof(ICONDIR) > iconData.Length) - return IntPtr.Zero; + return default; var entries = new ReadOnlySpan(&dir->idEntries, dir->idCount); var _bestBytesInRes = 0u; @@ -247,8 +260,8 @@ static unsafe IntPtr LoadIconFromData(byte[] iconData, int width = 0, int height } else { - int bestDelta = Math.Abs(bestWidth - width) + Math.Abs(bestHeight - height); - int thisDelta = Math.Abs(entry.bWidth - width) + Math.Abs(entry.bHeight - height); + int bestDelta = Math.Abs(bestWidth - size.Width) + Math.Abs(bestHeight - size.Height); + int thisDelta = Math.Abs(entry.bWidth - size.Width) + Math.Abs(entry.bHeight - size.Height); if ((thisDelta < bestDelta) || (thisDelta == bestDelta && (iconBitDepth <= s_bitDepth && iconBitDepth > _bestBitDepth || @@ -268,14 +281,9 @@ static unsafe IntPtr LoadIconFromData(byte[] iconData, int width = 0, int height } } - if (_bestImageOffset > int.MaxValue) - { - return IntPtr.Zero; - } - - if (_bestBytesInRes > int.MaxValue) + if (_bestImageOffset > int.MaxValue || _bestBytesInRes > int.MaxValue) { - return IntPtr.Zero; + return default; } uint endOffset; @@ -285,14 +293,16 @@ static unsafe IntPtr LoadIconFromData(byte[] iconData, int width = 0, int height } catch (OverflowException) { - return IntPtr.Zero; + return default; } if (endOffset > iconData.Length) { - return IntPtr.Zero; + return default; } + var bestSize = new PixelSize(bestWidth, bestHeight); + // Copy the bytes into an aligned buffer if needed. if ((_bestImageOffset % IntPtr.Size) != 0) { @@ -304,8 +314,8 @@ static unsafe IntPtr LoadIconFromData(byte[] iconData, int width = 0, int height { fixed (byte* pbAlignedBuffer = alignedBuffer) { - return UnmanagedMethods.CreateIconFromResourceEx(pbAlignedBuffer, _bestBytesInRes, 1, - 0x00030000, 0, 0, 0); + return (UnmanagedMethods.CreateIconFromResourceEx(pbAlignedBuffer, _bestBytesInRes, 1, + 0x00030000, 0, 0, 0), bestSize); } } finally @@ -318,17 +328,22 @@ static unsafe IntPtr LoadIconFromData(byte[] iconData, int width = 0, int height { try { - return UnmanagedMethods.CreateIconFromResourceEx(checked(b + _bestImageOffset), _bestBytesInRes, - 1, 0x00030000, 0, 0, 0); + return (UnmanagedMethods.CreateIconFromResourceEx(checked(b + _bestImageOffset), _bestBytesInRes, + 1, 0x00030000, 0, 0, 0), bestSize); } catch (OverflowException) { - return IntPtr.Zero; + return default; } } } } + public void CopyTo(Stream stream) + { + stream.Write(_bytes ?? throw new InvalidOperationException("Icon was created from a bitmap, not Win32 icon data"), 0, _bytes.Length); + } + public void Dispose() { UnmanagedMethods.DestroyIcon(Handle); diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs index 9d2b274a2b9..dc67f392466 100644 --- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs +++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Platform; @@ -8,7 +9,6 @@ using Avalonia.Media.Imaging; using Avalonia.Metadata; using Avalonia.Platform; -using Avalonia.Styling; using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods; @@ -19,8 +19,12 @@ internal class TrayIconImpl : ITrayIconImpl private static readonly Win32Icon s_emptyIcon; private readonly int _uniqueId; private static int s_nextUniqueId; + private static nint s_taskBarMonitor; + private bool _iconAdded; - private IconImpl? _icon; + private IconImpl? _iconImpl; + private bool _iconStale; + private Win32Icon? _icon; private string? _tooltipText; private readonly Win32NativeToManagedMenuExporter _exporter; private static readonly Dictionary s_trayIcons = new(); @@ -36,6 +40,8 @@ static TrayIconImpl() public TrayIconImpl() { + FindTaskBarMonitor(); + _exporter = new Win32NativeToManagedMenuExporter(); _uniqueId = ++s_nextUniqueId; @@ -49,28 +55,47 @@ public TrayIconImpl() internal static void ProcWnd(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { - if (msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.TryGetValue(wParam.ToInt32(), out var value)) - { - value.WndProc(hWnd, msg, wParam, lParam); - } - - if (msg == WM_TASKBARCREATED) + switch (msg) { - foreach (var tray in s_trayIcons.Values) - { - if (tray._iconAdded) + case (uint)CustomWindowsMessage.WM_TRAYMOUSE: + if (s_trayIcons.TryGetValue(wParam.ToInt32(), out var value)) { - tray.UpdateIcon(true); - tray.UpdateIcon(); + value.WndProc(hWnd, msg, wParam, lParam); } - } + break; + case (uint)WindowsMessage.WM_DISPLAYCHANGE: + FindTaskBarMonitor(); + foreach (var tray in s_trayIcons.Values) + { + if (tray._iconAdded) + { + tray._iconStale = true; + tray.UpdateIcon(); + } + } + break; + default: + if (msg == WM_TASKBARCREATED) + { + FindTaskBarMonitor(); + foreach (var tray in s_trayIcons.Values) + { + if (tray._iconAdded) + { + tray.UpdateIcon(true); + tray.UpdateIcon(); + } + } + } + break; } } /// public void SetIcon(IWindowIconImpl? icon) { - _icon = icon as IconImpl; + _iconImpl = (IconImpl?)icon; + _iconStale = true; UpdateIcon(); } @@ -87,8 +112,30 @@ public void SetToolTipText(string? text) UpdateIcon(!_iconAdded); } + private static void FindTaskBarMonitor() + { + var taskBarData = new APPBARDATA(); + if (SHAppBarMessage(AppBarMessage.ABM_GETTASKBARPOS, ref taskBarData) != 0) + { + s_taskBarMonitor = MonitorFromPoint(new() { X = taskBarData.rc.left, Y = taskBarData.rc.top }, MONITOR.MONITOR_DEFAULTTOPRIMARY); + } + } + private void UpdateIcon(bool remove = false) { + Win32Icon? newIcon = null; + if (_iconStale && _iconImpl is not null) + { + var scaling = 1.0; + if ((HRESULT)GetDpiForMonitor(s_taskBarMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpiX, out var dpiY) == HRESULT.S_OK) + { + Debug.Assert(dpiX == dpiY); + scaling = dpiX / 96.0; + } + + newIcon = _iconImpl.LoadSmallIcon(scaling); + } + var iconData = new NOTIFYICONDATA { hWnd = Win32Platform.Instance.Handle, @@ -99,7 +146,7 @@ private void UpdateIcon(bool remove = false) { iconData.uFlags = NIF.TIP | NIF.MESSAGE | NIF.ICON; iconData.uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE; - iconData.hIcon = _icon?.HIcon ?? s_emptyIcon.Handle; + iconData.hIcon = (_iconStale ? newIcon : _icon)?.Handle ?? s_emptyIcon.Handle; iconData.szTip = _tooltipText ?? ""; if (!_iconAdded) @@ -118,6 +165,13 @@ private void UpdateIcon(bool remove = false) Shell_NotifyIcon(NIM.DELETE, iconData); _iconAdded = false; } + + if (_iconStale) + { + _icon?.Dispose(); + _icon = newIcon; + _iconStale = false; + } } private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index 451ebd45007..543df7ae44b 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -224,13 +224,13 @@ public IWindowIconImpl LoadIcon(string fileName) { using (var stream = File.OpenRead(fileName)) { - return CreateIconImpl(stream); + return new IconImpl(stream); } } public IWindowIconImpl LoadIcon(Stream stream) { - return CreateIconImpl(stream); + return new IconImpl(stream); } public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) @@ -238,20 +238,10 @@ public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) using (var memoryStream = new MemoryStream()) { bitmap.Save(memoryStream); - var iconData = memoryStream.ToArray(); - return new IconImpl(new Win32Icon(iconData), iconData); + return new IconImpl(memoryStream); } } - private static IconImpl CreateIconImpl(Stream stream) - { - var ms = new MemoryStream(); - stream.CopyTo(ms); - ms.Position = 0; - var iconData = ms.ToArray(); - return new IconImpl(new Win32Icon(iconData), iconData); - } - private static void SetDpiAwareness() { // Ideally we'd set DPI awareness in the manifest but this doesn't work for netcoreapp2.0 diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index d521f1ba328..b7ec568407f 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -128,9 +128,10 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, case WindowsMessage.WM_DPICHANGED: { - var dpi = ToInt32(wParam) & 0xffff; + _dpi = (uint)wParam >> 16; var newDisplayRect = Marshal.PtrToStructure(lParam); - _scaling = dpi / 96.0; + _scaling = _dpi / 96.0; + RefreshIcon(); ScalingChanged?.Invoke(_scaling); using (SetResizeReason(WindowResizeReason.DpiChange)) @@ -148,6 +149,22 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, return IntPtr.Zero; } + case WindowsMessage.WM_GETICON: + if (_iconImpl == null) + { + break; + } + + var requestIcon = (Icons)wParam; + var requestDpi = (uint) lParam; + + if (requestDpi == 0) + { + requestDpi = _dpi; + } + + return LoadIcon(requestIcon, requestDpi)?.Handle ?? default; + case WindowsMessage.WM_KEYDOWN: case WindowsMessage.WM_SYSKEYDOWN: { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 5b62b2afc88..0ba294a9e65 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -5,26 +5,24 @@ using System.Linq; using System.Runtime.InteropServices; using Avalonia.Collections.Pooled; -using Avalonia.Controls.Platform; using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; -using Avalonia.Input; -using Avalonia.Metadata; using Avalonia.OpenGL.Egl; -using Avalonia.Platform.Storage; using Avalonia.Platform; +using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; -using Avalonia.Rendering; using Avalonia.Win32.DirectX; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; -using Avalonia.Win32.OpenGl.Angle; using Avalonia.Win32.OpenGl; -using Avalonia.Win32.WinRT.Composition; +using Avalonia.Win32.OpenGl.Angle; using Avalonia.Win32.WinRT; +using Avalonia.Win32.WinRT.Composition; using static Avalonia.Win32.Interop.UnmanagedMethods; -using Avalonia.Input.Platform; using System.Diagnostics; namespace Avalonia.Win32 @@ -82,9 +80,12 @@ internal partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindow private IntPtr _hwnd; private IInputRoot? _owner; protected WindowProperties _windowProperties; + private IconImpl? _iconImpl; + private readonly Dictionary<(Icons type, uint dpi), Win32Icon> _iconCache = new(); private bool _trackingMouse;//ToDo - there is something missed. Needs investigation @Steven Kirk private bool _topmost; private double _scaling = 1; + private uint _dpi = 96; private WindowState _showWindowState; private WindowState _lastWindowState; private OleDropTarget? _dropTarget; @@ -151,7 +152,7 @@ public WindowImpl() CreateWindow(); _framebuffer = new FramebufferManager(_hwnd); - + if (this is not PopupImpl) { UpdateInputMethod(GetKeyboardLayout(0)); @@ -257,7 +258,9 @@ public Size ClientSize } } - public Size? FrameSize + Size? ITopLevelImpl.FrameSize => FrameSize; + + public Size FrameSize { get { @@ -345,7 +348,7 @@ private set { return _nativeControlHost; } - + if (featureType == typeof(IStorageProvider)) { return _storageProvider; @@ -623,6 +626,8 @@ public void Dispose() DestroyWindow(_hwnd); _hwnd = IntPtr.Zero; } + + ClearIconCache(); } public void Invalidate(Rect rect) @@ -747,11 +752,53 @@ public void SetCursor(ICursorImpl? cursor) public void SetIcon(IWindowIconImpl? icon) { - var impl = icon as IconImpl; + _iconImpl = (IconImpl?)icon; + ClearIconCache(); + RefreshIcon(); + } - var hIcon = impl?.HIcon ?? IntPtr.Zero; - PostMessage(_hwnd, (int)WindowsMessage.WM_SETICON, - new IntPtr((int)Icons.ICON_BIG), hIcon); + private void ClearIconCache() + { + foreach (var icon in _iconCache.Values) + { + icon.Dispose(); + } + _iconCache.Clear(); + } + + private Win32Icon? LoadIcon(Icons type, uint dpi) + { + if (_iconImpl == null) + { + return null; + } + + if (type == Icons.ICON_SMALL2) + { + type = Icons.ICON_SMALL; + } + + var iconKey = (type, dpi); + if (!_iconCache.TryGetValue(iconKey, out var icon)) + { + var scale = dpi / 96.0; + _iconCache[iconKey] = icon = type switch + { + Icons.ICON_SMALL => _iconImpl.LoadSmallIcon(scale), + Icons.ICON_BIG => _iconImpl.LoadBigIcon(scale), + _ => throw new NotImplementedException(), + }; + } + + return icon; + } + + private void RefreshIcon() + { + SendMessage(_hwnd, (int)WindowsMessage.WM_SETICON, (nint)Icons.ICON_SMALL, LoadIcon(Icons.ICON_SMALL, _dpi)?.Handle ?? default); + SendMessage(_hwnd, (int)WindowsMessage.WM_SETICON, (nint)Icons.ICON_BIG, LoadIcon(Icons.ICON_BIG, _dpi)?.Handle ?? default); + + TaskBarList.SetOverlayIcon(_hwnd, default, null); // This will prompt the taskbar to redraw the icon } public void ShowTaskbarIcon(bool value) @@ -806,15 +853,15 @@ public unsafe void SetFrameThemeVariant(PlatformThemeVariant themeVariant) DwmSetWindowAttribute( _hwnd, (int)DwmWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE, - &pvUseBackdropBrush, - sizeof(int)); + &pvUseBackdropBrush, + sizeof(int)); if (TransparencyLevel == WindowTransparencyLevel.Mica) { SetTransparencyMica(Win32Platform.WindowsVersion); } } } - + protected virtual IntPtr CreateWindowOverride(ushort atom) { return CreateWindowEx( @@ -883,10 +930,10 @@ private void CreateWindow() if (GetDpiForMonitor( monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, - out var dpix, + out _dpi, out _) == 0) { - _scaling = dpix / 96.0; + _scaling = _dpi / 96.0; } } }