diff --git a/Avalonia.sln b/Avalonia.sln
index 8833e7127e..ee43b70f07 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -116,7 +116,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\SkiaSharp.props = build\SkiaSharp.props
build\SourceGenerators.props = build\SourceGenerators.props
build\SourceLink.props = build\SourceLink.props
- build\System.Drawing.Common.props = build\System.Drawing.Common.props
build\System.Memory.props = build\System.Memory.props
build\TrimmingEnable.props = build\TrimmingEnable.props
build\UnitTests.NetFX.props = build\UnitTests.NetFX.props
diff --git a/build/System.Drawing.Common.props b/build/System.Drawing.Common.props
deleted file mode 100644
index 108a0f41e0..0000000000
--- a/build/System.Drawing.Common.props
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
index aef89908c8..1a0d5334f6 100644
--- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
+++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
@@ -14,7 +14,6 @@
-
diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs
index 862e99be19..f6b756b607 100644
--- a/src/Windows/Avalonia.Win32/CursorFactory.cs
+++ b/src/Windows/Avalonia.Win32/CursorFactory.cs
@@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
-using System.Drawing;
-using System.Drawing.Imaging;
+using System.ComponentModel;
using System.IO;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using Avalonia.Input;
+using Avalonia.Media.Imaging;
using Avalonia.Platform;
+using Avalonia.Utilities;
using Avalonia.Win32.Interop;
-using SdBitmap = System.Drawing.Bitmap;
-using SdPixelFormat = System.Drawing.Imaging.PixelFormat;
namespace Avalonia.Win32
{
@@ -34,7 +35,7 @@ namespace Avalonia.Win32
IntPtr cursor = UnmanagedMethods.LoadCursor(mh, new IntPtr(id));
if (cursor != IntPtr.Zero)
{
- Cache.Add(cursorType, new CursorImpl(cursor, false));
+ Cache.Add(cursorType, new CursorImpl(cursor));
}
}
}
@@ -82,8 +83,7 @@ namespace Avalonia.Win32
if (!Cache.TryGetValue(cursorType, out var rv))
{
rv = new CursorImpl(
- UnmanagedMethods.LoadCursor(IntPtr.Zero, new IntPtr(CursorTypeMapping[cursorType])),
- false);
+ UnmanagedMethods.LoadCursor(IntPtr.Zero, new IntPtr(CursorTypeMapping[cursorType])));
Cache.Add(cursorType, rv);
}
@@ -92,89 +92,22 @@ namespace Avalonia.Win32
public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot)
{
- using var source = LoadSystemDrawingBitmap(cursor);
- using var mask = AlphaToMask(source);
-
- var info = new UnmanagedMethods.ICONINFO
- {
- IsIcon = false,
- xHotspot = hotSpot.X,
- yHotspot = hotSpot.Y,
- MaskBitmap = mask.GetHbitmap(),
- ColorBitmap = source.GetHbitmap(),
- };
-
- return new CursorImpl(UnmanagedMethods.CreateIconIndirect(ref info), true);
- }
-
- private static SdBitmap LoadSystemDrawingBitmap(IBitmapImpl bitmap)
- {
- using var memoryStream = new MemoryStream();
- bitmap.Save(memoryStream);
- return new SdBitmap(memoryStream);
- }
-
- private unsafe SdBitmap AlphaToMask(SdBitmap source)
- {
- var dest = new SdBitmap(source.Width, source.Height, SdPixelFormat.Format1bppIndexed);
-
- if (source.PixelFormat == SdPixelFormat.Format32bppPArgb)
- {
- throw new NotSupportedException(
- "Images with premultiplied alpha not yet supported as cursor images.");
- }
-
- if (source.PixelFormat != SdPixelFormat.Format32bppArgb)
- {
- return dest;
- }
-
- var sourceData = source.LockBits(
- new Rectangle(default, source.Size),
- ImageLockMode.ReadOnly,
- SdPixelFormat.Format32bppArgb);
- var destData = dest.LockBits(
- new Rectangle(default, source.Size),
- ImageLockMode.ReadOnly,
- SdPixelFormat.Format1bppIndexed);
-
- try
- {
- var pSource = (byte*)sourceData.Scan0.ToPointer();
- var pDest = (byte*)destData.Scan0.ToPointer();
-
- for (var y = 0; y < dest.Height; ++y)
- {
- for (var x = 0; x < dest.Width; ++x)
- {
- if (pSource[x * 4] == 0)
- {
- pDest[x / 8] |= (byte)(1 << (x % 8));
- }
- }
-
- pSource += sourceData.Stride;
- pDest += destData.Stride;
- }
-
- return dest;
- }
- finally
- {
- source.UnlockBits(sourceData);
- dest.UnlockBits(destData);
- }
+ return new CursorImpl(new Win32Icon(cursor, hotSpot));
}
}
internal class CursorImpl : ICursorImpl, IPlatformHandle
{
- private readonly bool _isCustom;
+ private Win32Icon? _icon;
- public CursorImpl(IntPtr handle, bool isCustom)
+ public CursorImpl(Win32Icon icon) : this(icon.Handle)
+ {
+ _icon = icon;
+ }
+
+ public CursorImpl(IntPtr handle)
{
Handle = handle;
- _isCustom = isCustom;
}
public IntPtr Handle { get; private set; }
@@ -182,9 +115,10 @@ namespace Avalonia.Win32
public void Dispose()
{
- if (_isCustom && Handle != IntPtr.Zero)
+ if (_icon != null)
{
- UnmanagedMethods.DestroyIcon(Handle);
+ _icon.Dispose();
+ _icon = null;
Handle = IntPtr.Zero;
}
}
diff --git a/src/Windows/Avalonia.Win32/IconImpl.cs b/src/Windows/Avalonia.Win32/IconImpl.cs
index 4d2b89b0cc..9fd62b7981 100644
--- a/src/Windows/Avalonia.Win32/IconImpl.cs
+++ b/src/Windows/Avalonia.Win32/IconImpl.cs
@@ -2,23 +2,26 @@
using System.Drawing;
using System.IO;
using Avalonia.Platform;
+using Avalonia.Win32.Interop;
namespace Avalonia.Win32
{
class IconImpl : IWindowIconImpl
{
- private readonly Icon icon;
+ private readonly Win32Icon _icon;
+ private readonly byte[] _iconData;
- public IconImpl(Icon icon)
+ public IconImpl(Win32Icon icon, byte[] iconData)
{
- this.icon = icon;
+ _icon = icon;
+ _iconData = iconData;
}
- public IntPtr HIcon => icon.Handle;
+ public IntPtr HIcon => _icon.Handle;
public void Save(Stream outputStream)
{
- icon.Save(outputStream);
+ outputStream.Write(_iconData, 0, _iconData.Length);
}
}
}
diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
index 6c64603377..65e06b54df 100644
--- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
+++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
@@ -1324,6 +1324,10 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")]
public static extern IntPtr CreateIconIndirect([In] ref ICONINFO iconInfo);
+ [DllImport("user32.dll")]
+ public static extern IntPtr CreateIconFromResourceEx(byte* pbIconBits, uint cbIconBits,
+ int fIcon, int dwVersion, int csDesired, int cyDesired, int flags);
+
[DllImport("user32.dll")]
public static extern bool DestroyIcon(IntPtr hIcon);
@@ -1611,6 +1615,8 @@ namespace Avalonia.Win32.Interop
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern IntPtr CreateDIBSection(IntPtr hDC, ref BITMAPINFOHEADER pBitmapInfo, int un, out IntPtr lplpVoid, IntPtr handle, int dw);
+ [DllImport("gdi32.dll", SetLastError = true)]
+ public static extern IntPtr CreateBitmap(int width, int height, int planes, int bitCount, IntPtr data);
[DllImport("gdi32.dll")]
public static extern int DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll", SetLastError = true)]
@@ -2114,6 +2120,8 @@ namespace Avalonia.Win32.Interop
public enum DEVICECAP
{
HORZRES = 8,
+ BITSPIXEL = 12,
+ PLANES = 14,
DESKTOPHORZRES = 118
}
diff --git a/src/Windows/Avalonia.Win32/Interop/Win32Icon.cs b/src/Windows/Avalonia.Win32/Interop/Win32Icon.cs
new file mode 100644
index 0000000000..05927394b1
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/Interop/Win32Icon.cs
@@ -0,0 +1,337 @@
+using System;
+using System.Buffers;
+using System.ComponentModel;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+
+namespace Avalonia.Win32.Interop;
+
+internal class Win32Icon : IDisposable
+{
+ public Win32Icon(Bitmap bitmap, PixelPoint hotSpot = default)
+ {
+ Handle = CreateIcon(bitmap, hotSpot);
+ }
+
+ public Win32Icon(IBitmapImpl bitmap, PixelPoint hotSpot = default)
+ {
+ using var memoryStream = new MemoryStream();
+ bitmap.Save(memoryStream);
+ memoryStream.Position = 0;
+ using var bmp = new Bitmap(memoryStream);
+ Handle = CreateIcon(bmp, hotSpot);
+ }
+
+ public Win32Icon(byte[] iconData)
+ {
+ Handle = LoadIconFromData(iconData);
+ if (Handle == IntPtr.Zero)
+ {
+ using var bmp = new Bitmap(new MemoryStream(iconData));
+ Handle = CreateIcon(bmp);
+ }
+ }
+
+ public IntPtr Handle { get; private set; }
+
+ IntPtr CreateIcon(Bitmap bitmap, PixelPoint hotSpot = default)
+ {
+
+ var mainBitmap = CreateHBitmap(bitmap);
+ if (mainBitmap == IntPtr.Zero)
+ throw new Win32Exception();
+ var alphaBitmap = AlphaToMask(bitmap);
+
+ try
+ {
+ if (alphaBitmap == IntPtr.Zero)
+ throw new Win32Exception();
+ var info = new UnmanagedMethods.ICONINFO
+ {
+ IsIcon = false,
+ xHotspot = hotSpot.X,
+ yHotspot = hotSpot.Y,
+ MaskBitmap = alphaBitmap,
+ ColorBitmap = mainBitmap
+ };
+
+ var hIcon = UnmanagedMethods.CreateIconIndirect(ref info);
+ if (hIcon == IntPtr.Zero)
+ throw new Win32Exception();
+ return hIcon;
+ }
+ finally
+ {
+ UnmanagedMethods.DeleteObject(mainBitmap);
+ UnmanagedMethods.DeleteObject(alphaBitmap);
+ }
+ }
+
+ static IntPtr CreateHBitmap(Bitmap source)
+ {
+ using var fb = AllocFramebuffer(source.PixelSize, PixelFormats.Bgra8888);
+ source.CopyPixels(fb, AlphaFormat.Unpremul);
+ return UnmanagedMethods.CreateBitmap(source.PixelSize.Width, source.PixelSize.Height, 1, 32, fb.Address);
+ }
+
+ static unsafe IntPtr AlphaToMask(Bitmap source)
+ {
+ using var alphaMaskBuffer = AllocFramebuffer(source.PixelSize, PixelFormats.BlackWhite);
+ var height = alphaMaskBuffer.Size.Height;
+ var width = alphaMaskBuffer.Size.Width;
+
+ if (!source.Format!.Value.HasAlpha)
+ {
+ Unsafe.InitBlock((void*)alphaMaskBuffer.Address, 0xff,
+ (uint)(alphaMaskBuffer.RowBytes * alphaMaskBuffer.Size.Height));
+ }
+ else
+ {
+ using var argbBuffer = AllocFramebuffer(source.PixelSize, PixelFormat.Bgra8888);
+ source.CopyPixels(argbBuffer, AlphaFormat.Unpremul);
+ var pSource = (byte*)argbBuffer.Address;
+ var pDest = (byte*)alphaMaskBuffer.Address;
+
+
+
+ for (var y = 0; y < height; ++y)
+ {
+ for (var x = 0; x < width; ++x)
+ {
+ if (pSource[x * 4] == 0)
+ {
+ pDest[x / 8] |= (byte)(1 << (x % 8));
+ }
+ }
+
+ pSource += argbBuffer.RowBytes;
+ pDest += alphaMaskBuffer.RowBytes;
+ }
+
+ }
+
+ return UnmanagedMethods.CreateBitmap(width, height, 1, 1, alphaMaskBuffer.Address);
+ }
+
+ static LockedFramebuffer AllocFramebuffer(PixelSize size, PixelFormat format)
+ {
+ if (size.Width < 1 || size.Height < 1)
+ throw new ArgumentOutOfRangeException();
+
+ int stride = (size.Width * format.BitsPerPixel + 7) / 8;
+ var data = Marshal.AllocHGlobal(size.Height * stride);
+ if (data == IntPtr.Zero)
+ throw new OutOfMemoryException();
+ return new LockedFramebuffer(data, size, stride, new Vector(96, 96), format,
+ () => Marshal.FreeHGlobal(data));
+ }
+
+ // Needs to be packed to 2 to get ICONDIRENTRY to follow immediately after idCount.
+ [StructLayout(LayoutKind.Sequential, Pack = 2)]
+ public struct ICONDIR
+ {
+ // Must be 0
+ public ushort idReserved;
+ // Must be 1
+ public ushort idType;
+ // Count of entries
+ public ushort idCount;
+ // First entry (anysize array)
+ public ICONDIRENTRY idEntries;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct ICONDIRENTRY
+ {
+ // Width and height are 1 - 255 or 0 for 256
+ public byte bWidth;
+ public byte bHeight;
+ public byte bColorCount;
+ public byte bReserved;
+ public ushort wPlanes;
+ public ushort wBitCount;
+ public uint dwBytesInRes;
+ public uint dwImageOffset;
+ }
+
+
+ private static int s_bitDepth;
+
+ static unsafe IntPtr LoadIconFromData(byte[] iconData, int width = 0, int height = 0)
+ {
+ 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);
+
+
+
+ if (s_bitDepth == 0)
+ {
+ IntPtr dc = UnmanagedMethods.GetDC(IntPtr.Zero);
+ s_bitDepth = UnmanagedMethods.GetDeviceCaps(dc, UnmanagedMethods.DEVICECAP.BITSPIXEL);
+ s_bitDepth *= UnmanagedMethods.GetDeviceCaps(dc, UnmanagedMethods.DEVICECAP.PLANES);
+ UnmanagedMethods.ReleaseDC(IntPtr.Zero, dc);
+
+ // If the bitdepth is 8, make it 4 because windows does not
+ // choose a 256 color icon if the display is running in 256 color mode
+ // due to palette flicker.
+ if (s_bitDepth == 8)
+ {
+ s_bitDepth = 4;
+ }
+ }
+
+ fixed (byte* b = iconData)
+ {
+ var dir = (ICONDIR*)b;
+
+ if (dir->idReserved != 0 || dir->idType != 1 || dir->idCount == 0)
+ {
+ return IntPtr.Zero;
+ }
+
+ byte bestWidth = 0;
+ byte bestHeight = 0;
+
+ if (sizeof(ICONDIRENTRY) * (dir->idCount - 1) + sizeof(ICONDIR) > iconData.Length)
+ return IntPtr.Zero;
+
+ var entries = new ReadOnlySpan(&dir->idEntries, dir->idCount);
+ var _bestBytesInRes = 0u;
+ var _bestBitDepth = 0u;
+ var _bestImageOffset = 0u;
+ foreach (ICONDIRENTRY entry in entries)
+ {
+ bool fUpdateBestFit = false;
+ uint iconBitDepth;
+ if (entry.bColorCount != 0)
+ {
+ iconBitDepth = 4;
+ if (entry.bColorCount < 0x10)
+ {
+ iconBitDepth = 1;
+ }
+ }
+ else
+ {
+ iconBitDepth = entry.wBitCount;
+ }
+
+ // If it looks like if nothing is specified at this point then set the bits per pixel to 8.
+ if (iconBitDepth == 0)
+ {
+ iconBitDepth = 8;
+ }
+
+ // Windows rules for specifing an icon:
+ //
+ // 1. The icon with the closest size match.
+ // 2. For matching sizes, the image with the closest bit depth.
+ // 3. If there is no color depth match, the icon with the closest color depth that does not exceed the display.
+ // 4. If all icon color depth > display, lowest color depth is chosen.
+ // 5. color depth of > 8bpp are all equal.
+ // 6. Never choose an 8bpp icon on an 8bpp system.
+
+ if (_bestBytesInRes == 0)
+ {
+ fUpdateBestFit = true;
+ }
+ else
+ {
+ int bestDelta = Math.Abs(bestWidth - width) + Math.Abs(bestHeight - height);
+ int thisDelta = Math.Abs(entry.bWidth - width) + Math.Abs(entry.bHeight - height);
+
+ if ((thisDelta < bestDelta) ||
+ (thisDelta == bestDelta && (iconBitDepth <= s_bitDepth && iconBitDepth > _bestBitDepth ||
+ _bestBitDepth > s_bitDepth && iconBitDepth < _bestBitDepth)))
+ {
+ fUpdateBestFit = true;
+ }
+ }
+
+ if (fUpdateBestFit)
+ {
+ bestWidth = entry.bWidth;
+ bestHeight = entry.bHeight;
+ _bestImageOffset = entry.dwImageOffset;
+ _bestBytesInRes = entry.dwBytesInRes;
+ _bestBitDepth = iconBitDepth;
+ }
+ }
+
+ if (_bestImageOffset > int.MaxValue)
+ {
+ return IntPtr.Zero;
+ }
+
+ if (_bestBytesInRes > int.MaxValue)
+ {
+ return IntPtr.Zero;
+ }
+
+ uint endOffset;
+ try
+ {
+ endOffset = checked(_bestImageOffset + _bestBytesInRes);
+ }
+ catch (OverflowException)
+ {
+ return IntPtr.Zero;
+ }
+
+ if (endOffset > iconData.Length)
+ {
+ return IntPtr.Zero;
+ }
+
+ // Copy the bytes into an aligned buffer if needed.
+ if ((_bestImageOffset % IntPtr.Size) != 0)
+ {
+ // Beginning of icon's content is misaligned.
+ byte[] alignedBuffer = ArrayPool.Shared.Rent((int)_bestBytesInRes);
+ Array.Copy(iconData, _bestImageOffset, alignedBuffer, 0, _bestBytesInRes);
+
+ try
+ {
+ fixed (byte* pbAlignedBuffer = alignedBuffer)
+ {
+ return UnmanagedMethods.CreateIconFromResourceEx(pbAlignedBuffer, _bestBytesInRes, 1,
+ 0x00030000, 0, 0, 0);
+ }
+ }
+ finally
+ {
+
+ ArrayPool.Shared.Return(alignedBuffer);
+ }
+ }
+ else
+ {
+ try
+ {
+ return UnmanagedMethods.CreateIconFromResourceEx(checked(b + _bestImageOffset), _bestBytesInRes,
+ 1, 0x00030000, 0, 0, 0);
+ }
+ catch (OverflowException)
+ {
+ return IntPtr.Zero;
+ }
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ UnmanagedMethods.DestroyIcon(Handle);
+ Handle = IntPtr.Zero;
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/TrayIconImpl.cs b/src/Windows/Avalonia.Win32/TrayIconImpl.cs
index f2023d37ac..606905d5e8 100644
--- a/src/Windows/Avalonia.Win32/TrayIconImpl.cs
+++ b/src/Windows/Avalonia.Win32/TrayIconImpl.cs
@@ -5,6 +5,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.LogicalTree;
+using Avalonia.Media.Imaging;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Styling;
@@ -15,7 +16,9 @@ namespace Avalonia.Win32
{
internal class TrayIconImpl : ITrayIconImpl
{
- private static readonly IntPtr s_emptyIcon = new System.Drawing.Bitmap(32, 32).GetHicon();
+ private static readonly Win32Icon s_emptyIcon = new(new WriteableBitmap(new PixelSize(32, 32),
+ new Vector(96, 96),
+ PixelFormats.Bgra8888, AlphaFormat.Unpremul));
private readonly int _uniqueId;
private static int s_nextUniqueId;
private bool _iconAdded;
@@ -91,7 +94,7 @@ namespace Avalonia.Win32
{
iconData.uFlags = NIF.TIP | NIF.MESSAGE | NIF.ICON;
iconData.uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE;
- iconData.hIcon = _icon?.HIcon ?? s_emptyIcon;
+ iconData.hIcon = _icon?.HIcon ?? s_emptyIcon.Handle;
iconData.szTip = _tooltipText ?? "";
if (!_iconAdded)
diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs
index 9244da5064..3cc5e67d1c 100644
--- a/src/Windows/Avalonia.Win32/Win32Platform.cs
+++ b/src/Windows/Avalonia.Win32/Win32Platform.cs
@@ -16,6 +16,7 @@ using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.Win32.Input;
+using Avalonia.Win32.Interop;
using static Avalonia.Win32.Interop.UnmanagedMethods;
namespace Avalonia
@@ -242,18 +243,11 @@ namespace Avalonia.Win32
private static IconImpl CreateIconImpl(Stream stream)
{
- try
- {
- // new Icon() will work only if stream is an "ico" file.
- return new IconImpl(new System.Drawing.Icon(stream));
- }
- catch (ArgumentException)
- {
- // Fallback to Bitmap creation and converting into a windows icon.
- using var icon = new System.Drawing.Bitmap(stream);
- var hIcon = icon.GetHicon();
- return new IconImpl(System.Drawing.Icon.FromHandle(hIcon));
- }
+ 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()