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
customers/chaos-11.0
Tom Edwards 2 years ago
committed by Tom Edwards
parent
commit
cca8914320
  1. 6
      Avalonia.Desktop.slnf
  2. 81
      src/Windows/Avalonia.Win32/IconImpl.cs
  3. 61
      src/Windows/Avalonia.Win32/Interop/TaskBarList.cs
  4. 58
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  5. 77
      src/Windows/Avalonia.Win32/Interop/Win32Icon.cs
  6. 86
      src/Windows/Avalonia.Win32/TrayIconImpl.cs
  7. 16
      src/Windows/Avalonia.Win32/Win32Platform.cs
  8. 21
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  9. 87
      src/Windows/Avalonia.Win32/WindowImpl.cs

6
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",

81
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();
}
}
}

61
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<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>
/// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc
@ -17,34 +43,31 @@ namespace Avalonia.Win32.Interop
/// <param name="fullscreen">Fullscreen state.</param>
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<HrInit>((*ptr)->HrInit);
s_markFullscreenWindowDelegate ??=
Marshal.GetDelegateForFunctionPointer<MarkFullscreenWindow>((*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<MarkFullscreenWindow>((*ptr)->MarkFullscreenWindow);
s_setOverlayIconDelegate ??=
Marshal.GetDelegateForFunctionPointer<SetOverlayIcon>((*ptr)->SetOverlayIcon);
s_markFullscreenWindowDelegate(s_taskBarList, hwnd, fullscreen);
s_setOverlayIconDelegate(s_taskBarList, hwnd, hIcon, description);
}
}
}

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

@ -1451,6 +1451,9 @@ namespace Avalonia.Win32.Interop
[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 @@ namespace Avalonia.Win32.Interop
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 @@ namespace Avalonia.Win32.Interop
[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 @@ namespace Avalonia.Win32.Interop
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 @@ namespace Avalonia.Win32.Interop
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 @@ namespace Avalonia.Win32.Interop
public enum Icons
{
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
@ -2436,9 +2445,10 @@ namespace Avalonia.Win32.Interop
}
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 @@ namespace Avalonia.Win32.Interop
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 @@ namespace Avalonia.Win32.Interop
SETVERSION = 0x00000004
}
internal enum AppBarMessage : uint
{
ABM_GETSTATE = 0x00000004,
ABM_GETTASKBARPOS = 0x00000005,
}
[Flags]
internal enum NIF : uint
{

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

@ -25,9 +25,11 @@ internal class Win32Icon : IDisposable
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 @@ 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 PixelSize Size { get; }
private readonly byte[]? _bytes;
IntPtr CreateIcon(Bitmap bitmap, PixelPoint hotSpot = default)
{
@ -160,19 +177,15 @@ internal class Win32Icon : IDisposable
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 @@ internal class Win32Icon : IDisposable
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<ICONDIRENTRY>(&dir->idEntries, dir->idCount);
var _bestBytesInRes = 0u;
@ -247,8 +260,8 @@ internal class Win32Icon : IDisposable
}
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 @@ internal class Win32Icon : IDisposable
}
}
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 @@ internal class Win32Icon : IDisposable
}
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 @@ internal class Win32Icon : IDisposable
{
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 @@ internal class Win32Icon : IDisposable
{
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);

86
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.LogicalTree;
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 @@ namespace Avalonia.Win32
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<int, TrayIconImpl> s_trayIcons = new();
@ -36,6 +40,8 @@ namespace Avalonia.Win32
public TrayIconImpl()
{
FindTaskBarMonitor();
_exporter = new Win32NativeToManagedMenuExporter();
_uniqueId = ++s_nextUniqueId;
@ -49,28 +55,47 @@ namespace Avalonia.Win32
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;
}
}
/// <inheritdoc />
public void SetIcon(IWindowIconImpl? icon)
{
_icon = icon as IconImpl;
_iconImpl = (IconImpl?)icon;
_iconStale = true;
UpdateIcon();
}
@ -87,8 +112,30 @@ namespace Avalonia.Win32
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 @@ namespace Avalonia.Win32
{
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 @@ namespace Avalonia.Win32
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)

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

@ -224,13 +224,13 @@ namespace Avalonia.Win32
{
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 @@ namespace Avalonia.Win32
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

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

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

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

@ -5,26 +5,24 @@ using System.Diagnostics.CodeAnalysis;
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 @@ namespace Avalonia.Win32
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 @@ namespace Avalonia.Win32
CreateWindow();
_framebuffer = new FramebufferManager(_hwnd);
if (this is not PopupImpl)
{
UpdateInputMethod(GetKeyboardLayout(0));
@ -257,7 +258,9 @@ namespace Avalonia.Win32
}
}
public Size? FrameSize
Size? ITopLevelImpl.FrameSize => FrameSize;
public Size FrameSize
{
get
{
@ -345,7 +348,7 @@ namespace Avalonia.Win32
{
return _nativeControlHost;
}
if (featureType == typeof(IStorageProvider))
{
return _storageProvider;
@ -623,6 +626,8 @@ namespace Avalonia.Win32
DestroyWindow(_hwnd);
_hwnd = IntPtr.Zero;
}
ClearIconCache();
}
public void Invalidate(Rect rect)
@ -747,11 +752,53 @@ namespace Avalonia.Win32
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 @@ namespace Avalonia.Win32
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 @@ namespace Avalonia.Win32
if (GetDpiForMonitor(
monitor,
MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI,
out var dpix,
out _dpi,
out _) == 0)
{
_scaling = dpix / 96.0;
_scaling = _dpi / 96.0;
}
}
}

Loading…
Cancel
Save