Browse Source

Adjust icon sizes to account for requested DPI (#14564)

Refresh icons when window DPI changes
Provide ICON_SMALL icons
Fixed Win32Icon.Size being incorrect when the exact size couldn't be found in the icon
pull/14621/head
Tom Edwards 2 years ago
committed by GitHub
parent
commit
aeb24470cd
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      Avalonia.Desktop.slnf
  2. 81
      src/Windows/Avalonia.Win32/IconImpl.cs
  3. 55
      src/Windows/Avalonia.Win32/Interop/TaskBarList.cs
  4. 56
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  5. 77
      src/Windows/Avalonia.Win32/Interop/Win32Icon.cs
  6. 7
      src/Windows/Avalonia.Win32/PopupImpl.cs
  7. 86
      src/Windows/Avalonia.Win32/TrayIconImpl.cs
  8. 37
      src/Windows/Avalonia.Win32/Win32Platform.cs
  9. 21
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  10. 91
      src/Windows/Avalonia.Win32/WindowImpl.cs

2
Avalonia.Desktop.slnf

@ -47,7 +47,7 @@
"src\\tools\\DevAnalyzers\\DevAnalyzers.csproj", "src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj", "src\\tools\\DevGenerators\\DevGenerators.csproj",
"src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj", "src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
"src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj", "src\\Windows\\Avalonia.Win32.Interoperability\\Avalonia.Win32.Interoperability.csproj",
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj", "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",

81
src/Windows/Avalonia.Win32/IconImpl.cs

@ -1,27 +1,88 @@
using System; using System;
using System.Drawing;
using System.IO; using System.IO;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Win32.Interop; using Avalonia.Win32.Interop;
namespace Avalonia.Win32 namespace Avalonia.Win32
{ {
class IconImpl : IWindowIconImpl internal class IconImpl : IWindowIconImpl, IDisposable
{ {
private readonly Win32Icon _icon; private readonly Win32Icon _smallIcon;
private readonly byte[] _iconData; 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; var scaled = (int)Math.Ceiling(baseSize * factor);
_iconData = iconData; 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();
} }
} }
} }

55
src/Windows/Avalonia.Win32/Interop/TaskBarList.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using static Avalonia.Win32.Interop.UnmanagedMethods; using static Avalonia.Win32.Interop.UnmanagedMethods;
namespace Avalonia.Win32.Interop namespace Avalonia.Win32.Interop
@ -7,8 +8,30 @@ namespace Avalonia.Win32.Interop
internal class TaskBarList internal class TaskBarList
{ {
private static IntPtr s_taskBarList; private static IntPtr s_taskBarList;
private static bool s_initialized;
private static object s_lock = new();
private static HrInit? s_hrInitDelegate; private static HrInit? s_hrInitDelegate;
private static MarkFullscreenWindow? s_markFullscreenWindowDelegate; private static MarkFullscreenWindow? s_markFullscreenWindowDelegate;
private static SetOverlayIcon? s_setOverlayIconDelegate;
private static unsafe IntPtr Init()
{
int result = CoCreateInstance(in ShellIds.TaskBarList, IntPtr.Zero, 1, in ShellIds.ITaskBarList2, out IntPtr instance);
var ptr = (ITaskBarList3VTable**)instance.ToPointer();
s_hrInitDelegate ??= Marshal.GetDelegateForFunctionPointer<HrInit>((*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);
/// <summary> /// <summary>
/// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc
@ -17,31 +40,31 @@ namespace Avalonia.Win32.Interop
/// <param name="fullscreen">Fullscreen state.</param> /// <param name="fullscreen">Fullscreen state.</param>
public static unsafe void MarkFullscreen(IntPtr hwnd, bool fullscreen) public static unsafe void MarkFullscreen(IntPtr hwnd, bool fullscreen)
{ {
if (s_taskBarList == IntPtr.Zero) LazyInit();
{
int result = CoCreateInstance(in ShellIds.TaskBarList, IntPtr.Zero, 1, in ShellIds.ITaskBarList2, out s_taskBarList);
if (s_taskBarList != IntPtr.Zero) if (s_taskBarList != IntPtr.Zero)
{ {
var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer(); var ptr = (ITaskBarList3VTable**)s_taskBarList.ToPointer();
s_hrInitDelegate ??= Marshal.GetDelegateForFunctionPointer<HrInit>((*ptr)->HrInit); s_markFullscreenWindowDelegate ??=
Marshal.GetDelegateForFunctionPointer<MarkFullscreenWindow>((*ptr)->MarkFullscreenWindow);
if (s_hrInitDelegate(s_taskBarList) != HRESULT.S_OK) s_markFullscreenWindowDelegate(s_taskBarList, hwnd, fullscreen);
{
s_taskBarList = IntPtr.Zero;
}
}
} }
}
public static unsafe void SetOverlayIcon(IntPtr hwnd, IntPtr hIcon, string? description)
{
LazyInit();
if (s_taskBarList != IntPtr.Zero) if (s_taskBarList != IntPtr.Zero)
{ {
var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer(); var ptr = (ITaskBarList3VTable**)s_taskBarList.ToPointer();
s_markFullscreenWindowDelegate ??= s_setOverlayIconDelegate ??=
Marshal.GetDelegateForFunctionPointer<MarkFullscreenWindow>((*ptr)->MarkFullscreenWindow); Marshal.GetDelegateForFunctionPointer<SetOverlayIcon>((*ptr)->SetOverlayIcon);
s_markFullscreenWindowDelegate(s_taskBarList, hwnd, fullscreen); s_setOverlayIconDelegate(s_taskBarList, hwnd, hIcon, description);
} }
} }
} }

56
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -1454,6 +1454,9 @@ namespace Avalonia.Win32.Interop
[DllImport("shell32", CharSet = CharSet.Auto)] [DllImport("shell32", CharSet = CharSet.Auto)]
public static extern int Shell_NotifyIcon(NIM dwMessage, NOTIFYICONDATA lpData); 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)] [DllImport("user32.dll", EntryPoint = "SetClassLongPtrW", ExactSpelling = true)]
private static extern IntPtr SetClassLong64(IntPtr hWnd, ClassLongIndex nIndex, IntPtr dwNewLong); private static extern IntPtr SetClassLong64(IntPtr hWnd, ClassLongIndex nIndex, IntPtr dwNewLong);
@ -1604,6 +1607,10 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")] [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "PostMessageW")]
public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); 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")] [DllImport("gdi32.dll")]
public static extern int SetDIBitsToDevice(IntPtr hdc, int XDest, int YDest, uint public static extern int SetDIBitsToDevice(IntPtr hdc, int XDest, int YDest, uint
dwWidth, uint dwHeight, int XSrc, int YSrc, uint uStartScan, uint cScanLines, dwWidth, uint dwHeight, int XSrc, int YSrc, uint uStartScan, uint cScanLines,
@ -1716,14 +1723,14 @@ namespace Avalonia.Win32.Interop
[DllImport("dwmapi.dll", SetLastError = false)] [DllImport("dwmapi.dll", SetLastError = false)]
public static extern int DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind); public static extern int DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind);
[Flags] [Flags]
public enum LayeredWindowFlags public enum LayeredWindowFlags
{ {
LWA_ALPHA = 0x00000002, LWA_ALPHA = 0x00000002,
LWA_COLORKEY = 0x00000001, LWA_COLORKEY = 0x00000001,
} }
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, LayeredWindowFlags dwFlags); public static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, LayeredWindowFlags dwFlags);
@ -1820,7 +1827,7 @@ namespace Avalonia.Win32.Interop
private static extern int IntMsgWaitForMultipleObjectsEx(int nCount, IntPtr[]? pHandles, int dwMilliseconds, private static extern int IntMsgWaitForMultipleObjectsEx(int nCount, IntPtr[]? pHandles, int dwMilliseconds,
QueueStatusFlags dwWakeMask, MsgWaitForMultipleObjectsFlags dwFlags); 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) QueueStatusFlags dwWakeMask, MsgWaitForMultipleObjectsFlags dwFlags)
{ {
int result = IntMsgWaitForMultipleObjectsEx(nCount, pHandles, dwMilliseconds, dwWakeMask, dwFlags); int result = IntMsgWaitForMultipleObjectsEx(nCount, pHandles, dwMilliseconds, dwWakeMask, dwFlags);
@ -2388,7 +2395,9 @@ namespace Avalonia.Win32.Interop
public enum Icons public enum Icons
{ {
ICON_SMALL = 0, ICON_SMALL = 0,
ICON_BIG = 1 ICON_BIG = 1,
/// <summary>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.</summary>
ICON_SMALL2 = 2,
} }
public static class ShellIds public static class ShellIds
@ -2411,9 +2420,10 @@ namespace Avalonia.Win32.Interop
} }
public delegate void MarkFullscreenWindow(IntPtr This, IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fullscreen); 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 delegate HRESULT HrInit(IntPtr This);
public struct ITaskBarList2VTable public struct ITaskBarList3VTable
{ {
public IntPtr IUnknown1; public IntPtr IUnknown1;
public IntPtr IUnknown2; public IntPtr IUnknown2;
@ -2424,6 +2434,36 @@ namespace Avalonia.Win32.Interop
public IntPtr ActivateTab; public IntPtr ActivateTab;
public IntPtr SetActiveAlt; public IntPtr SetActiveAlt;
public IntPtr MarkFullscreenWindow; 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;
}
} }
} }
@ -2517,6 +2557,12 @@ namespace Avalonia.Win32.Interop
SETVERSION = 0x00000004 SETVERSION = 0x00000004
} }
internal enum AppBarMessage : uint
{
ABM_GETSTATE = 0x00000004,
ABM_GETTASKBARPOS = 0x00000005,
}
[Flags] [Flags]
internal enum NIF : uint internal enum NIF : uint
{ {

77
src/Windows/Avalonia.Win32/Interop/Win32Icon.cs

@ -25,9 +25,11 @@ internal class Win32Icon : IDisposable
Handle = CreateIcon(bmp, hotSpot); 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) if (Handle == IntPtr.Zero)
{ {
using var bmp = new Bitmap(new MemoryStream(iconData)); using var bmp = new Bitmap(new MemoryStream(iconData));
@ -35,7 +37,22 @@ internal class Win32Icon : IDisposable
} }
} }
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 IntPtr Handle { get; private set; }
public PixelSize Size { get; }
private readonly byte[]? _bytes;
IntPtr CreateIcon(Bitmap bitmap, PixelPoint hotSpot = default) IntPtr CreateIcon(Bitmap bitmap, PixelPoint hotSpot = default)
{ {
@ -160,19 +177,15 @@ internal class Win32Icon : IDisposable
private static int s_bitDepth; 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)) if (iconData.Length < sizeof(ICONDIR))
return IntPtr.Zero; return default;
// 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);
if (s_bitDepth == 0) if (s_bitDepth == 0)
{ {
@ -196,14 +209,14 @@ internal class Win32Icon : IDisposable
if (dir->idReserved != 0 || dir->idType != 1 || dir->idCount == 0) if (dir->idReserved != 0 || dir->idType != 1 || dir->idCount == 0)
{ {
return IntPtr.Zero; return default;
} }
byte bestWidth = 0; byte bestWidth = 0;
byte bestHeight = 0; byte bestHeight = 0;
if (sizeof(ICONDIRENTRY) * (dir->idCount - 1) + sizeof(ICONDIR) > iconData.Length) if (sizeof(ICONDIRENTRY) * (dir->idCount - 1) + sizeof(ICONDIR) > iconData.Length)
return IntPtr.Zero; return default;
var entries = new ReadOnlySpan<ICONDIRENTRY>(&dir->idEntries, dir->idCount); var entries = new ReadOnlySpan<ICONDIRENTRY>(&dir->idEntries, dir->idCount);
var _bestBytesInRes = 0u; var _bestBytesInRes = 0u;
@ -247,8 +260,8 @@ internal class Win32Icon : IDisposable
} }
else else
{ {
int bestDelta = Math.Abs(bestWidth - width) + Math.Abs(bestHeight - height); int bestDelta = Math.Abs(bestWidth - size.Width) + Math.Abs(bestHeight - size.Height);
int thisDelta = Math.Abs(entry.bWidth - width) + Math.Abs(entry.bHeight - height); int thisDelta = Math.Abs(entry.bWidth - size.Width) + Math.Abs(entry.bHeight - size.Height);
if ((thisDelta < bestDelta) || if ((thisDelta < bestDelta) ||
(thisDelta == bestDelta && (iconBitDepth <= s_bitDepth && iconBitDepth > _bestBitDepth || (thisDelta == bestDelta && (iconBitDepth <= s_bitDepth && iconBitDepth > _bestBitDepth ||
@ -268,14 +281,9 @@ internal class Win32Icon : IDisposable
} }
} }
if (_bestImageOffset > int.MaxValue) if (_bestImageOffset > int.MaxValue || _bestBytesInRes > int.MaxValue)
{
return IntPtr.Zero;
}
if (_bestBytesInRes > int.MaxValue)
{ {
return IntPtr.Zero; return default;
} }
uint endOffset; uint endOffset;
@ -285,14 +293,16 @@ internal class Win32Icon : IDisposable
} }
catch (OverflowException) catch (OverflowException)
{ {
return IntPtr.Zero; return default;
} }
if (endOffset > iconData.Length) if (endOffset > iconData.Length)
{ {
return IntPtr.Zero; return default;
} }
var bestSize = new PixelSize(bestWidth, bestHeight);
// Copy the bytes into an aligned buffer if needed. // Copy the bytes into an aligned buffer if needed.
if ((_bestImageOffset % IntPtr.Size) != 0) if ((_bestImageOffset % IntPtr.Size) != 0)
{ {
@ -304,8 +314,8 @@ internal class Win32Icon : IDisposable
{ {
fixed (byte* pbAlignedBuffer = alignedBuffer) fixed (byte* pbAlignedBuffer = alignedBuffer)
{ {
return UnmanagedMethods.CreateIconFromResourceEx(pbAlignedBuffer, _bestBytesInRes, 1, return (UnmanagedMethods.CreateIconFromResourceEx(pbAlignedBuffer, _bestBytesInRes, 1,
0x00030000, 0, 0, 0); 0x00030000, 0, 0, 0), bestSize);
} }
} }
finally finally
@ -318,17 +328,22 @@ internal class Win32Icon : IDisposable
{ {
try try
{ {
return UnmanagedMethods.CreateIconFromResourceEx(checked(b + _bestImageOffset), _bestBytesInRes, return (UnmanagedMethods.CreateIconFromResourceEx(checked(b + _bestImageOffset), _bestBytesInRes,
1, 0x00030000, 0, 0, 0); 1, 0x00030000, 0, 0, 0), bestSize);
} }
catch (OverflowException) 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() public void Dispose()
{ {
UnmanagedMethods.DestroyIcon(Handle); UnmanagedMethods.DestroyIcon(Handle);

7
src/Windows/Avalonia.Win32/PopupImpl.cs

@ -129,6 +129,13 @@ namespace Avalonia.Win32
private PopupImpl(IWindowBaseImpl parent, bool dummy) private PopupImpl(IWindowBaseImpl parent, bool dummy)
{ {
_windowProperties = new WindowProperties
{
ShowInTaskbar = false,
IsResizable = false,
Decorations = SystemDecorations.None,
};
_parent = parent; _parent = parent;
PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize));
} }

86
src/Windows/Avalonia.Win32/TrayIconImpl.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
@ -8,7 +9,6 @@ using Avalonia.LogicalTree;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Win32.Interop; using Avalonia.Win32.Interop;
using static Avalonia.Win32.Interop.UnmanagedMethods; using static Avalonia.Win32.Interop.UnmanagedMethods;
@ -19,8 +19,12 @@ namespace Avalonia.Win32
private static readonly Win32Icon s_emptyIcon; private static readonly Win32Icon s_emptyIcon;
private readonly int _uniqueId; private readonly int _uniqueId;
private static int s_nextUniqueId; private static int s_nextUniqueId;
private static nint s_taskBarMonitor;
private bool _iconAdded; private bool _iconAdded;
private IconImpl? _icon; private IconImpl? _iconImpl;
private bool _iconStale;
private Win32Icon? _icon;
private string? _tooltipText; private string? _tooltipText;
private readonly Win32NativeToManagedMenuExporter _exporter; private readonly Win32NativeToManagedMenuExporter _exporter;
private static readonly Dictionary<int, TrayIconImpl> s_trayIcons = new(); private static readonly Dictionary<int, TrayIconImpl> s_trayIcons = new();
@ -36,6 +40,8 @@ namespace Avalonia.Win32
public TrayIconImpl() public TrayIconImpl()
{ {
FindTaskBarMonitor();
_exporter = new Win32NativeToManagedMenuExporter(); _exporter = new Win32NativeToManagedMenuExporter();
_uniqueId = ++s_nextUniqueId; _uniqueId = ++s_nextUniqueId;
@ -49,28 +55,47 @@ namespace Avalonia.Win32
internal static void ProcWnd(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) 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)) switch (msg)
{
value.WndProc(hWnd, msg, wParam, lParam);
}
if (msg == WM_TASKBARCREATED)
{ {
foreach (var tray in s_trayIcons.Values) case (uint)CustomWindowsMessage.WM_TRAYMOUSE:
{ if (s_trayIcons.TryGetValue(wParam.ToInt32(), out var value))
if (tray._iconAdded)
{ {
tray.UpdateIcon(true); value.WndProc(hWnd, msg, wParam, lParam);
tray.UpdateIcon();
} }
} 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;
} }
} }
/// <inheritdoc /> /// <inheritdoc />
public void SetIcon(IWindowIconImpl? icon) public void SetIcon(IWindowIconImpl? icon)
{ {
_icon = icon as IconImpl; _iconImpl = (IconImpl?)icon;
_iconStale = true;
UpdateIcon(); UpdateIcon();
} }
@ -87,8 +112,30 @@ namespace Avalonia.Win32
UpdateIcon(!_iconAdded); 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) 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 var iconData = new NOTIFYICONDATA
{ {
hWnd = Win32Platform.Instance.Handle, hWnd = Win32Platform.Instance.Handle,
@ -99,7 +146,7 @@ namespace Avalonia.Win32
{ {
iconData.uFlags = NIF.TIP | NIF.MESSAGE | NIF.ICON; iconData.uFlags = NIF.TIP | NIF.MESSAGE | NIF.ICON;
iconData.uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE; 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 ?? ""; iconData.szTip = _tooltipText ?? "";
if (!_iconAdded) if (!_iconAdded)
@ -118,6 +165,13 @@ namespace Avalonia.Win32
Shell_NotifyIcon(NIM.DELETE, iconData); Shell_NotifyIcon(NIM.DELETE, iconData);
_iconAdded = false; _iconAdded = false;
} }
if (_iconStale)
{
_icon?.Dispose();
_icon = newIcon;
_iconStale = false;
}
} }
private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)

37
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -225,13 +225,13 @@ namespace Avalonia.Win32
{ {
using (var stream = File.OpenRead(fileName)) using (var stream = File.OpenRead(fileName))
{ {
return CreateIconImpl(stream); return new IconImpl(stream);
} }
} }
public IWindowIconImpl LoadIcon(Stream stream) public IWindowIconImpl LoadIcon(Stream stream)
{ {
return CreateIconImpl(stream); return new IconImpl(stream);
} }
public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) public IWindowIconImpl LoadIcon(IBitmapImpl bitmap)
@ -239,38 +239,7 @@ namespace Avalonia.Win32
using (var memoryStream = new MemoryStream()) using (var memoryStream = new MemoryStream())
{ {
bitmap.Save(memoryStream); bitmap.Save(memoryStream);
return new IconImpl(memoryStream);
var iconData = memoryStream.ToArray();
return new IconImpl(new Win32Icon(iconData), iconData);
}
}
private static IconImpl CreateIconImpl(Stream stream)
{
if (stream.CanSeek)
{
stream.Position = 0;
}
if (stream is MemoryStream memoryStream)
{
var iconData = memoryStream.ToArray();
return new IconImpl(new Win32Icon(iconData), iconData);
}
else
{
using (var ms = new MemoryStream())
{
stream.CopyTo(ms);
ms.Position = 0;
var iconData = ms.ToArray();
return new IconImpl(new Win32Icon(iconData), iconData);
}
} }
} }

21
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@ -129,9 +129,10 @@ namespace Avalonia.Win32
case WindowsMessage.WM_DPICHANGED: case WindowsMessage.WM_DPICHANGED:
{ {
var dpi = ToInt32(wParam) & 0xffff; _dpi = (uint)wParam >> 16;
var newDisplayRect = Marshal.PtrToStructure<RECT>(lParam); var newDisplayRect = Marshal.PtrToStructure<RECT>(lParam);
_scaling = dpi / 96.0; _scaling = _dpi / 96.0;
RefreshIcon();
ScalingChanged?.Invoke(_scaling); ScalingChanged?.Invoke(_scaling);
using (SetResizeReason(WindowResizeReason.DpiChange)) using (SetResizeReason(WindowResizeReason.DpiChange))
@ -149,6 +150,22 @@ namespace Avalonia.Win32
return IntPtr.Zero; 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_KEYDOWN:
case WindowsMessage.WM_SYSKEYDOWN: case WindowsMessage.WM_SYSKEYDOWN:
{ {

91
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -5,26 +5,24 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Collections.Pooled; using Avalonia.Collections.Pooled;
using Avalonia.Controls.Platform;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Input.TextInput; using Avalonia.Input.TextInput;
using Avalonia.Input;
using Avalonia.Metadata;
using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Egl;
using Avalonia.Platform.Storage;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.Rendering;
using Avalonia.Win32.DirectX; using Avalonia.Win32.DirectX;
using Avalonia.Win32.Input; using Avalonia.Win32.Input;
using Avalonia.Win32.Interop; using Avalonia.Win32.Interop;
using Avalonia.Win32.OpenGl.Angle;
using Avalonia.Win32.OpenGl; using Avalonia.Win32.OpenGl;
using Avalonia.Win32.WinRT.Composition; using Avalonia.Win32.OpenGl.Angle;
using Avalonia.Win32.WinRT; using Avalonia.Win32.WinRT;
using Avalonia.Win32.WinRT.Composition;
using static Avalonia.Win32.Interop.UnmanagedMethods; using static Avalonia.Win32.Interop.UnmanagedMethods;
using Avalonia.Input.Platform;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Platform.Storage.FileIO; using Avalonia.Platform.Storage.FileIO;
using Avalonia.Threading; using Avalonia.Threading;
@ -84,10 +82,13 @@ namespace Avalonia.Win32
private string? _className; private string? _className;
private IntPtr _hwnd; private IntPtr _hwnd;
private IInputRoot? _owner; private IInputRoot? _owner;
private WindowProperties _windowProperties; 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 _trackingMouse;//ToDo - there is something missed. Needs investigation @Steven Kirk
private bool _topmost; private bool _topmost;
private double _scaling = 1; private double _scaling = 1;
private uint _dpi = 96;
private WindowState _showWindowState; private WindowState _showWindowState;
private WindowState _lastWindowState; private WindowState _lastWindowState;
private OleDropTarget? _dropTarget; private OleDropTarget? _dropTarget;
@ -144,7 +145,7 @@ namespace Avalonia.Win32
CreateWindow(); CreateWindow();
_framebuffer = new FramebufferManager(_hwnd); _framebuffer = new FramebufferManager(_hwnd);
if (this is not PopupImpl) if (this is not PopupImpl)
{ {
UpdateInputMethod(GetKeyboardLayout(0)); UpdateInputMethod(GetKeyboardLayout(0));
@ -247,7 +248,9 @@ namespace Avalonia.Win32
} }
} }
public Size? FrameSize Size? ITopLevelImpl.FrameSize => FrameSize;
public Size FrameSize
{ {
get get
{ {
@ -335,7 +338,7 @@ namespace Avalonia.Win32
{ {
return _nativeControlHost; return _nativeControlHost;
} }
if (featureType == typeof(IStorageProvider)) if (featureType == typeof(IStorageProvider))
{ {
return _storageProvider; return _storageProvider;
@ -602,6 +605,8 @@ namespace Avalonia.Win32
DestroyWindow(_hwnd); DestroyWindow(_hwnd);
_hwnd = IntPtr.Zero; _hwnd = IntPtr.Zero;
} }
ClearIconCache();
} }
public void Invalidate(Rect rect) public void Invalidate(Rect rect)
@ -727,11 +732,53 @@ namespace Avalonia.Win32
public void SetIcon(IWindowIconImpl? icon) public void SetIcon(IWindowIconImpl? icon)
{ {
var impl = icon as IconImpl; _iconImpl = (IconImpl?)icon;
ClearIconCache();
RefreshIcon();
}
var hIcon = impl?.HIcon ?? IntPtr.Zero; private void ClearIconCache()
PostMessage(_hwnd, (int)WindowsMessage.WM_SETICON, {
new IntPtr((int)Icons.ICON_BIG), hIcon); 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) public void ShowTaskbarIcon(bool value)
@ -786,15 +833,15 @@ namespace Avalonia.Win32
DwmSetWindowAttribute( DwmSetWindowAttribute(
_hwnd, _hwnd,
(int)DwmWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE, (int)DwmWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE,
&pvUseBackdropBrush, &pvUseBackdropBrush,
sizeof(int)); sizeof(int));
if (TransparencyLevel == WindowTransparencyLevel.Mica) if (TransparencyLevel == WindowTransparencyLevel.Mica)
{ {
SetTransparencyMica(); SetTransparencyMica();
} }
} }
} }
protected virtual IntPtr CreateWindowOverride(ushort atom) protected virtual IntPtr CreateWindowOverride(ushort atom)
{ {
return CreateWindowEx( return CreateWindowEx(
@ -863,10 +910,10 @@ namespace Avalonia.Win32
if (GetDpiForMonitor( if (GetDpiForMonitor(
monitor, monitor,
MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI,
out var dpix, out _dpi,
out _) == 0) out _) == 0)
{ {
_scaling = dpix / 96.0; _scaling = _dpi / 96.0;
} }
} }
} }
@ -1511,7 +1558,7 @@ namespace Avalonia.Win32
public RECT WindowRect { get; set; } public RECT WindowRect { get; set; }
}; };
private struct WindowProperties protected struct WindowProperties
{ {
public bool ShowInTaskbar; public bool ShowInTaskbar;
public bool IsResizable; public bool IsResizable;

Loading…
Cancel
Save