From e882ae27fcc60254623c32b2bfee6d7630b7dfec Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 20 Jan 2026 20:38:34 +0100 Subject: [PATCH] Fix Win32 clipboard not returning bitmap in some cases --- .../Avalonia.Win32/ClipboardFormatRegistry.cs | 47 +++++----------- .../DataTransferToOleDataObjectWrapper.cs | 53 +++++++++++-------- .../Interop/UnmanagedMethods.cs | 2 +- .../Avalonia.Win32/OleDataObjectHelper.cs | 52 +++++++++++------- .../OleDataObjectToDataTransferWrapper.cs | 8 +-- 5 files changed, 80 insertions(+), 82 deletions(-) diff --git a/src/Windows/Avalonia.Win32/ClipboardFormatRegistry.cs b/src/Windows/Avalonia.Win32/ClipboardFormatRegistry.cs index 9b551332b3..5cf0304f2c 100644 --- a/src/Windows/Avalonia.Win32/ClipboardFormatRegistry.cs +++ b/src/Windows/Avalonia.Win32/ClipboardFormatRegistry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using Avalonia.Input; using Avalonia.Media.Imaging; using Avalonia.Utilities; @@ -12,19 +13,15 @@ namespace Avalonia.Win32 { private const int MaxFormatNameLength = 260; private const string AppPrefix = "avn-app-fmt:"; - public const string PngFormatMimeType = "image/png"; - public const string PngFormatSystemType = "PNG"; - public const string BitmapFormat = "CF_BITMAP"; - public const string DibFormat = "CF_DIB"; - public const string DibV5Format = "CF_DIBV5"; private static readonly List<(DataFormat Format, ushort Id)> s_formats = []; - public static DataFormat PngSystemDataFormat = DataFormat.FromSystemName(PngFormatSystemType, AppPrefix); - public static DataFormat PngMimeDataFormat = DataFormat.FromSystemName(PngFormatMimeType, AppPrefix); - public static DataFormat HBitmapDataFormat = DataFormat.FromSystemName(BitmapFormat, AppPrefix); - public static DataFormat DibDataFormat = DataFormat.FromSystemName(DibFormat, AppPrefix); - public static DataFormat DibV5DataFormat = DataFormat.FromSystemName(DibV5Format, AppPrefix); + public static DataFormat PngSystemDataFormat = new DataFormat(DataFormatKind.Platform, "PNG"); + public static DataFormat PngMimeDataFormat = new DataFormat(DataFormatKind.Platform, "image/png"); + public static DataFormat HBitmapDataFormat = new DataFormat(DataFormatKind.Platform, "CF_BITMAP"); + public static DataFormat DibDataFormat = new DataFormat(DataFormatKind.Platform, "CF_DIB"); + public static DataFormat DibV5DataFormat = new DataFormat(DataFormatKind.Platform, "CF_DIBV5"); + // Ordered from the most preferred to the least preferred public static DataFormat[] ImageFormats = [PngMimeDataFormat, PngSystemDataFormat, DibDataFormat, DibV5DataFormat, HBitmapDataFormat]; static ClipboardFormatRegistry() @@ -44,12 +41,12 @@ namespace Avalonia.Win32 var buffer = StringBuilderCache.Acquire(MaxFormatNameLength); if (UnmanagedMethods.GetClipboardFormatName(id, buffer, buffer.Capacity) > 0) return StringBuilderCache.GetStringAndRelease(buffer); - if (Enum.IsDefined(typeof(UnmanagedMethods.ClipboardFormat), (int)id)) - return Enum.GetName(typeof(UnmanagedMethods.ClipboardFormat), (int)id)!; + if (Enum.IsDefined(typeof(UnmanagedMethods.ClipboardFormat), id)) + return Enum.GetName(typeof(UnmanagedMethods.ClipboardFormat), id)!; return $"Unknown_Format_{id}"; } - public static DataFormat GetFormatById(ushort id) + public static DataFormat GetOrAddFormat(ushort id) { lock (s_formats) { @@ -66,30 +63,12 @@ namespace Avalonia.Win32 } } - public static ushort GetFormatId(DataFormat format) + public static ushort GetOrAddFormat(DataFormat format) { + Debug.Assert(format != DataFormat.Bitmap); // Callers must pass an effective platform type + lock (s_formats) { - if (DataFormat.Bitmap.Equals(format)) - { - (DataFormat, ushort)? pngFormat = null; - (DataFormat, ushort)? dibFormat = null; - - foreach (var currentFormat in s_formats) - { - if (currentFormat.Id == (ushort)UnmanagedMethods.ClipboardFormat.CF_DIB) - dibFormat = currentFormat; - else if (currentFormat.Format.Identifier == PngFormatMimeType) - pngFormat = currentFormat; - } - var imageFormatId = pngFormat?.Item2 ?? dibFormat?.Item2 ?? 0; - - if (imageFormatId != 0) - { - return imageFormatId; - } - } - for (var i = 0; i < s_formats.Count; ++i) { if (s_formats[i].Format.Equals(format)) diff --git a/src/Windows/Avalonia.Win32/DataTransferToOleDataObjectWrapper.cs b/src/Windows/Avalonia.Win32/DataTransferToOleDataObjectWrapper.cs index 672899cb9f..f8f1784180 100644 --- a/src/Windows/Avalonia.Win32/DataTransferToOleDataObjectWrapper.cs +++ b/src/Windows/Avalonia.Win32/DataTransferToOleDataObjectWrapper.cs @@ -30,25 +30,9 @@ internal class DataTransferToOleDataObjectWrapper(IDataTransfer dataTransfer) _current = current; } - public FormatEnumerator(IReadOnlyList dataFormats) + public FormatEnumerator(ushort[] formatIds) { - List formats = new List(); - - if (dataFormats.Contains(DataFormat.Bitmap)) - { - // We add extra formats for bitmaps - formats.Add(OleDataObjectHelper.ToFormatEtc(ClipboardFormatRegistry.PngMimeDataFormat)); - formats.Add(OleDataObjectHelper.ToFormatEtc(ClipboardFormatRegistry.PngSystemDataFormat)); - formats.Add(OleDataObjectHelper.ToFormatEtc(ClipboardFormatRegistry.DibDataFormat)); - formats.Add(OleDataObjectHelper.ToFormatEtc(ClipboardFormatRegistry.DibV5DataFormat)); - formats.Add(OleDataObjectHelper.ToFormatEtc(ClipboardFormatRegistry.HBitmapDataFormat, true)); - } - else - { - formats.AddRange(dataFormats.Select(x => OleDataObjectHelper.ToFormatEtc(x))); - } - - _formats = formats.ToArray(); + _formats = formatIds.Select(OleDataObjectHelper.ToFormatEtc).ToArray(); _current = 0; } @@ -98,6 +82,9 @@ internal class DataTransferToOleDataObjectWrapper(IDataTransfer dataTransfer) public bool IsDisposed => DataTransfer is null; + private ushort[] FormatIds + => field ??= CalcFormatIds(); + public event Action? OnDestroyed; unsafe int Win32Com.IDataObject.DAdvise(FORMATETC* pFormatetc, int advf, void* adviseSink) @@ -115,7 +102,7 @@ internal class DataTransferToOleDataObjectWrapper(IDataTransfer dataTransfer) throw new COMException(nameof(COR_E_OBJECTDISPOSED), unchecked((int)HRESULT.E_NOTIMPL)); if ((DATADIR)direction == DATADIR.DATADIR_GET) - return new FormatEnumerator(DataTransfer.Formats); + return new FormatEnumerator(FormatIds); throw new COMException(nameof(HRESULT.E_NOTIMPL), unchecked((int)HRESULT.E_NOTIMPL)); } @@ -166,7 +153,8 @@ internal class DataTransferToOleDataObjectWrapper(IDataTransfer dataTransfer) { dataFormat = null; - if (!(format->tymed.HasAllFlags(TYMED.TYMED_HGLOBAL) || format->cfFormat == (ushort)ClipboardFormat.CF_BITMAP && format->tymed == TYMED.TYMED_GDI)) + if (!(format->tymed == TYMED.TYMED_HGLOBAL || + (format->tymed == TYMED.TYMED_GDI && format->cfFormat == (ushort)ClipboardFormat.CF_BITMAP))) { result = DV_E_TYMED; dataFormat = null; @@ -185,17 +173,38 @@ internal class DataTransferToOleDataObjectWrapper(IDataTransfer dataTransfer) return false; } - dataFormat = ClipboardFormatRegistry.GetFormatById(format->cfFormat); - if (!DataTransfer.Contains(dataFormat) && !ClipboardFormatRegistry.ImageFormats.Contains(dataFormat)) + if (!FormatIds.Contains(format->cfFormat)) { result = DV_E_FORMATETC; return false; } + dataFormat = ClipboardFormatRegistry.GetOrAddFormat(format->cfFormat); result = (uint)HRESULT.S_OK; return true; } + private ushort[] CalcFormatIds() + { + if (DataTransfer is null) + return []; + + var formatIds = new List(DataTransfer.Formats.Count); + + foreach (var dataFormat in DataTransfer.Formats) + { + if (DataFormat.Bitmap.Equals(dataFormat)) + { + // We add extra formats for bitmaps + formatIds.AddRange(ClipboardFormatRegistry.ImageFormats.Select(ClipboardFormatRegistry.GetOrAddFormat)); + } + else + formatIds.Add(ClipboardFormatRegistry.GetOrAddFormat(dataFormat)); + } + + return formatIds.ToArray(); + } + unsafe uint Win32Com.IDataObject.SetData(FORMATETC* pformatetc, STGMEDIUM* pmedium, int fRelease) => (uint)HRESULT.E_NOTIMPL; diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 100df30511..b4436f3a3f 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -2297,7 +2297,7 @@ namespace Avalonia.Win32.Interop MDT_DEFAULT = MDT_EFFECTIVE_DPI } - public enum ClipboardFormat + public enum ClipboardFormat : ushort { /// /// A handle to a bitmap diff --git a/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs b/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs index 73edab2d02..d603b3497b 100644 --- a/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs +++ b/src/Windows/Avalonia.Win32/OleDataObjectHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.InteropServices; @@ -24,39 +25,26 @@ internal static class OleDataObjectHelper { private const int SRCCOPY = 0x00CC0020; - public static FORMATETC ToFormatEtc(this DataFormat format, bool isGdi = false) + public static FORMATETC ToFormatEtc(ushort formatId) => new() { - cfFormat = ClipboardFormatRegistry.GetFormatId(format), + cfFormat = formatId, dwAspect = DVASPECT.DVASPECT_CONTENT, ptd = IntPtr.Zero, lindex = -1, - tymed = isGdi ? TYMED.TYMED_GDI : TYMED.TYMED_HGLOBAL + tymed = formatId == (ushort)ClipboardFormat.CF_BITMAP ? TYMED.TYMED_GDI : TYMED.TYMED_HGLOBAL }; public static unsafe object? TryGet(this Win32Com.IDataObject oleDataObject, DataFormat format) { - var formatEtc = format.ToFormatEtc(); - - if (oleDataObject.QueryGetData(&formatEtc) != (uint)HRESULT.S_OK) + if (TryGetContainedFormat(oleDataObject, format) is not { } formatId) return null; var medium = new STGMEDIUM(); + var formatEtc = ToFormatEtc(formatId); var result = oleDataObject.GetData(&formatEtc, &medium); if (result != (uint)HRESULT.S_OK) - { - if (result == DV_E_TYMED) - { - formatEtc.tymed = TYMED.TYMED_GDI; - - if (oleDataObject.GetData(&formatEtc, &medium) != (uint)HRESULT.S_OK) - { - return null; - } - } - else - return null; - } + return null; try { @@ -82,6 +70,32 @@ internal static class OleDataObjectHelper return null; } + private static ushort? TryGetContainedFormat(Win32Com.IDataObject oleDataObject, DataFormat format) + { + // Bitmap is not a real format, find the first matching platform format, if any. + if (DataFormat.Bitmap.Equals(format)) + { + foreach (var imageFormat in ClipboardFormatRegistry.ImageFormats) + { + if (TryGetContainedFormatCore(oleDataObject, imageFormat) is { } formatId) + return formatId; + } + + return null; + } + + return TryGetContainedFormatCore(oleDataObject, format); + + static unsafe ushort? TryGetContainedFormatCore(Win32Com.IDataObject oleDataObject, DataFormat format) + { + Debug.Assert(format != DataFormat.Bitmap); + + var formatId = ClipboardFormatRegistry.GetOrAddFormat(format); + var formatEtc = ToFormatEtc(formatId); + return oleDataObject.QueryGetData(&formatEtc) == (uint)HRESULT.S_OK ? formatId : null; + } + } + private static unsafe object? ReadDataFromGdi(nint bitmapHandle) { var bitmap = new BITMAP(); diff --git a/src/Windows/Avalonia.Win32/OleDataObjectToDataTransferWrapper.cs b/src/Windows/Avalonia.Win32/OleDataObjectToDataTransferWrapper.cs index e3aebfbc94..969f13e39c 100644 --- a/src/Windows/Avalonia.Win32/OleDataObjectToDataTransferWrapper.cs +++ b/src/Windows/Avalonia.Win32/OleDataObjectToDataTransferWrapper.cs @@ -36,11 +36,7 @@ internal sealed class OleDataObjectToDataTransferWrapper(Win32Com.IDataObject ol foreach (var format in formats) { - if (format.Identifier is ClipboardFormatRegistry.DibFormat - or ClipboardFormatRegistry.BitmapFormat - or ClipboardFormatRegistry.PngFormatMimeType - or ClipboardFormatRegistry.PngFormatSystemType) - { + if (ClipboardFormatRegistry.ImageFormats.Contains(format)) { hasSupportedImageFormat = true; break; } @@ -65,7 +61,7 @@ internal sealed class OleDataObjectToDataTransferWrapper(Win32Com.IDataObject ol if (formatEtc.ptd != IntPtr.Zero) Marshal.FreeCoTaskMem(formatEtc.ptd); - return ClipboardFormatRegistry.GetFormatById(formatEtc.cfFormat); + return ClipboardFormatRegistry.GetOrAddFormat(formatEtc.cfFormat); } }