Skip to content

Commit

Permalink
Adjust icon sizes to account for requested DPI (AvaloniaUI#14564)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
TomEdwardsEnscape committed Mar 7, 2024
1 parent acb91d9 commit cca8914
Show file tree
Hide file tree
Showing 9 changed files with 373 additions and 120 deletions.
6 changes: 3 additions & 3 deletions Avalonia.Desktop.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
81 changes: 71 additions & 10 deletions src/Windows/Avalonia.Win32/IconImpl.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 42 additions & 19 deletions src/Windows/Avalonia.Win32/Interop/TaskBarList.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,40 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using static Avalonia.Win32.Interop.UnmanagedMethods;

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
Expand All @@ -17,34 +43,31 @@ internal class TaskBarList
/// <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);
}
}
}
Expand Down
58 changes: 52 additions & 6 deletions src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -2413,7 +2420,9 @@ public enum HRESULT : uint
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
Expand All @@ -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;
Expand All @@ -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;
}
}
}

Expand Down Expand Up @@ -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
{
Expand Down
Loading

0 comments on commit cca8914

Please sign in to comment.