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()