From 55512c732e29633556e7f155983cc5a7de1d0e44 Mon Sep 17 00:00:00 2001 From: David Hall Date: Thu, 5 Jan 2023 10:28:15 -0700 Subject: [PATCH] Tons of fixes and updates to IDataObject and Clipboard methods and wrapper classes. --- PInvoke/Shared/WinUser/CLIPFORMAT.cs | 14 +- PInvoke/Shell32/Clipboard.cs | 560 +++++++++++++++++++----------- UnitTests/Windows.Shell/ClipboardTests.cs | 264 ++++++++++---- Windows.Shell.Common/NativeClipboard.cs | 334 +++++------------- Windows.Shell/ShellDataObject.cs | 7 +- 5 files changed, 650 insertions(+), 529 deletions(-) diff --git a/PInvoke/Shared/WinUser/CLIPFORMAT.cs b/PInvoke/Shared/WinUser/CLIPFORMAT.cs index 03c0bb17..75757f97 100644 --- a/PInvoke/Shared/WinUser/CLIPFORMAT.cs +++ b/PInvoke/Shared/WinUser/CLIPFORMAT.cs @@ -144,7 +144,7 @@ namespace Vanara.PInvoke public static implicit operator int(CLIPFORMAT value) => value._value; /// A handle to a bitmap (HBITMAP). - [ClipCorrespondingType(typeof(HBITMAP))] + [ClipCorrespondingType(typeof(HBITMAP), TYMED.TYMED_GDI)] public static readonly CLIPFORMAT CF_BITMAP = 2; /// A memory object containing a BITMAPINFO structure followed by the bitmap bits. @@ -205,6 +205,7 @@ namespace Vanara.PInvoke /// A handle to type HDROP that identifies a list of files. An application can retrieve information about the files by passing the /// handle to the DragQueryFile function. /// + [ClipCorrespondingType(typeof(string[]))] public static readonly CLIPFORMAT CF_HDROP = 15; /// @@ -238,7 +239,7 @@ namespace Vanara.PInvoke /// Text format containing characters in the OEM character set. Each line ends with a carriage return/linefeed (CR-LF) combination. A /// null character signals the end of the data. /// - [ClipCorrespondingType(typeof(string), TYMED.TYMED_HGLOBAL, EncodingType = typeof(System.Text.ASCIIEncoding))] + [ClipCorrespondingType(typeof(string), EncodingType = typeof(System.Text.ASCIIEncoding))] public static readonly CLIPFORMAT CF_OEMTEXT = 7; /// @@ -288,7 +289,7 @@ namespace Vanara.PInvoke /// Text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals the end of the data. /// Use this format for ANSI text. /// - [ClipCorrespondingType(typeof(string), TYMED.TYMED_HGLOBAL, EncodingType = typeof(System.Text.ASCIIEncoding))] + [ClipCorrespondingType(typeof(string), EncodingType = typeof(System.Text.UTF8Encoding))] public static readonly CLIPFORMAT CF_TEXT = 1; /// Tagged-image file format. @@ -297,7 +298,7 @@ namespace Vanara.PInvoke /// /// Unicode text format. Each line ends with a carriage return/linefeed (CR-LF) combination. A null character signals the end of the data. /// - [ClipCorrespondingType(typeof(string), TYMED.TYMED_HGLOBAL, EncodingType = typeof(System.Text.UnicodeEncoding))] + [ClipCorrespondingType(typeof(string), EncodingType = typeof(System.Text.UnicodeEncoding))] public static readonly CLIPFORMAT CF_UNICODETEXT = 13; /// Represents audio data in one of the standard wave formats, such as 11 kHz or 22 kHz PCM. @@ -312,10 +313,7 @@ namespace Vanara.PInvoke /// Initializes a new instance of the class. /// The type that corresponds to this entity. /// The medium type used to store the payload. - public ClipCorrespondingTypeAttribute(Type typeRef, TYMED medium = TYMED.TYMED_HGLOBAL) : base(typeRef) - { - Medium = medium; - } + public ClipCorrespondingTypeAttribute(Type typeRef, TYMED medium = TYMED.TYMED_HGLOBAL) : base(typeRef) => Medium = medium; /// Gets the medium type used to store the payload. /// The medium type. diff --git a/PInvoke/Shell32/Clipboard.cs b/PInvoke/Shell32/Clipboard.cs index ef4e8fcf..153d6de2 100644 --- a/PInvoke/Shell32/Clipboard.cs +++ b/PInvoke/Shell32/Clipboard.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -10,6 +11,7 @@ using System.Runtime.Serialization.Formatters.Binary; using System.Text; using System.Text.RegularExpressions; using Vanara.Extensions; +using Vanara.Extensions.Reflection; using Vanara.InteropServices; using static Vanara.PInvoke.Gdi32; using static Vanara.PInvoke.Kernel32; @@ -21,18 +23,6 @@ namespace Vanara.PInvoke { public static partial class Shell32 { - private static readonly Lazy> clipFmtIds = new(() => - { - Type type = typeof(CLIPFORMAT); - Dictionary knownIds = type.GetFields(BindingFlags.Static | BindingFlags.Public).Where(f => f.FieldType == type && f.IsInitOnly).ToDictionary(f => (uint)(CLIPFORMAT)f.GetValue(null), f => (f.Name, f.GetCustomAttributes().FirstOrDefault())); - foreach (FieldInfo f in typeof(ShellClipboardFormat).GetFields(BindingFlags.Public).Where(f => f.FieldType == typeof(string))) - { - string s = (string)f.GetValue(null); - knownIds.Add(RegisterClipboardFormat(s), (s, f.GetCustomAttributes().FirstOrDefault())); - } - return knownIds; - }); - /// /// Values used with the DROPDESCRIPTION structure to specify the drop image. /// @@ -102,6 +92,35 @@ namespace Vanara.PInvoke FD_UNICODE = 0x80000000, } + /// Converts an ANSI string to Unicode. + /// The ANSI string value. + /// The Unicode string value. + public static string AnsiToUnicode(string value) + { + if (string.IsNullOrEmpty(value)) return value; + byte[] ret = null; + var sz = MultiByteToWideChar(0, 0, value, value.Length, ret, 0); + ret = new byte[(int)sz]; + MultiByteToWideChar(0, 0, value, value.Length, ret, sz); + return Encoding.Unicode.GetString(ret); + } + + /// Enumerates the structures that define the formats and media supported by a given data object. + /// The data object. + /// A sequence of structures. + public static IEnumerable EnumFormats(this IDataObject dataObj) + { + IEnumFORMATETC e = null; + try { e = dataObj.EnumFormatEtc(DATADIR.DATADIR_GET); e.Reset(); } + catch { } + if (e is null) yield break; + + FORMATETC[] etc = new FORMATETC[1]; + int[] f = new[] { 1 }; + while (((HRESULT)e.Next(1, etc, f)).Succeeded && f[0] > 0) + yield return etc[0]; + } + /// Takes an HTML fragment and wraps it in the HTML format specification for the clipboard. /// /// The fragment contains pure, valid HTML representing the area the user has selected (to Copy, for example). This contains the @@ -177,6 +196,25 @@ namespace Vanara.PInvoke return Encoding.UTF8.GetBytes(sb.ToString()); } + /// Obtains data from a source data object. + /// The data object. + /// Specifies the particular clipboard format of interest. + /// + /// Indicates how much detail should be contained in the rendering. This parameter should be one of the DVASPECT enumeration values. + /// A single clipboard format can support multiple aspects or views of the object. Most data and presentation transfer and caching + /// methods pass aspect information. For example, a caller might request an object's iconic picture, using the metafile clipboard + /// format to retrieve it. Note that only one DVASPECT value can be used in dwAspect. That is, dwAspect cannot be the result of a + /// Boolean OR operation on several DVASPECT values. + /// + /// + /// Part of the aspect when the data must be split across page boundaries. The most common value is -1, which identifies all of the + /// data. For the aspects DVASPECT_THUMBNAIL and DVASPECT_ICON, lindex is ignored. + /// + /// The object associated with the request. If no object can be determined, a [] is returned. + /// Unrecognized TYMED value. + public static object GetData(this IDataObject dataObj, string format, DVASPECT aspect = DVASPECT.DVASPECT_CONTENT, int index = -1) => + GetData(dataObj, RegisterClipboardFormat(format), aspect, index); + /// Obtains data from a source data object. /// The data object. /// Specifies the particular clipboard format of interest. @@ -195,8 +233,8 @@ namespace Vanara.PInvoke /// Unrecognized TYMED value. public static object GetData(this IDataObject dataObj, uint formatId, DVASPECT aspect = DVASPECT.DVASPECT_CONTENT, int index = -1) { - ClipCorrespondingTypeAttribute attr = clipFmtIds.Value.TryGetValue(formatId, out (string name, ClipCorrespondingTypeAttribute attr) data) ? data.attr : null; - TYMED tymed = attr?.Medium ?? TYMED.TYMED_FILE | TYMED.TYMED_HGLOBAL | TYMED.TYMED_ISTREAM | TYMED.TYMED_ISTORAGE | TYMED.TYMED_GDI | TYMED.TYMED_MFPICT | TYMED.TYMED_ENHMF; + ClipCorrespondingTypeAttribute attr = ShellClipboardFormat.clipFmtIds.Value.TryGetValue(formatId, out (string name, ClipCorrespondingTypeAttribute attr) data) ? data.attr : null; + TYMED tymed = attr?.Medium ?? AllTymed.Value; FORMATETC formatetc = new() { cfFormat = unchecked((short)(ushort)formatId), @@ -204,7 +242,6 @@ namespace Vanara.PInvoke lindex = index, tymed = tymed }; - //STGMEDIUM medium = new() { tymed = attr?.Medium ?? TYMED.TYMED_HGLOBAL }; dataObj.GetData(ref formatetc, out var medium); // Handle TYMED values, passing through HGLOBAL @@ -212,7 +249,7 @@ namespace Vanara.PInvoke if (medium.tymed != TYMED.TYMED_HGLOBAL) return medium.tymed switch { - TYMED.TYMED_FILE => new SafeMoveableHGlobalHandle(medium.unionmember, userFree).ToString(-1, CharSet.Ansi), + TYMED.TYMED_FILE => Marshal.PtrToStringBSTR(medium.unionmember), TYMED.TYMED_ISTREAM => Marshal.GetObjectForIUnknown(medium.unionmember) as IStream, TYMED.TYMED_ISTORAGE => Marshal.GetObjectForIUnknown(medium.unionmember) as IStorage, TYMED.TYMED_GDI when userFree => new SafeHBITMAP(medium.unionmember), @@ -225,6 +262,12 @@ namespace Vanara.PInvoke _ => throw new InvalidOperationException(), }; + // Handle list of shell items + if (formatId == ShellClipboardFormat.Register(ShellClipboardFormat.CFSTR_SHELLIDLIST)) + { + return SHCreateShellItemArrayFromDataObject(dataObj); + } + using SafeMoveableHGlobalHandle hmem = new(medium.unionmember, userFree); try { @@ -233,13 +276,13 @@ namespace Vanara.PInvoke // Handle CF_HDROP since it can't indicate specialty if (CLIPFORMAT.CF_HDROP.Equals(formatId)) { - return ClipboardHDROPFormatter.Instance.Read(hmem); + return new ClipboardHDROPFormatter().Read(hmem); } - // If there's no hint, return bytes + // If there's no hint, return bytes or ISerialized value if (attr is null) { - return hmem.GetBytes(); + return ClipboardSerializedFormatter.IsSerialized(hmem) ? new ClipboardSerializedFormatter().Read(hmem) : hmem.GetBytes(); } // Use clipboard formatter if available @@ -248,27 +291,19 @@ namespace Vanara.PInvoke return ((IClipboardFormatter)Activator.CreateInstance(attr.Formatter)).Read(hmem); } - // Handle strings - if (attr.TypeRef == typeof(string)) + CharSet charSet = GetCharSet(attr); + switch (attr.TypeRef) { - Encoding enc = (Encoding)Activator.CreateInstance(attr.EncodingType ?? typeof(UnicodeEncoding)); - return enc.GetString(hmem.GetBytes()); + // Handle strings + case Type t when t == typeof(string): + return GetEncoding(attr).GetString(hmem.GetBytes()).TrimEnd('\0'); + // Handle string[] + case Type t when t == typeof(string[]): + return hmem.ToStringEnum(charSet).ToArray(); + // Handle other types + default: + return hmem.CallLocked(p => p.Convert(hmem.Size, attr.TypeRef, charSet)); } - - CharSet charSet = CharSet.Auto; - if (attr.EncodingType == typeof(ASCIIEncoding)) - charSet = CharSet.Ansi; - else if (attr.EncodingType == typeof(UnicodeEncoding)) - charSet = CharSet.Unicode; - - // Handle string[] - if (attr.TypeRef == typeof(string[])) - { - return hmem.ToStringEnum(charSet).ToArray(); - } - - // Handle other types - return hmem.DangerousGetHandle().Convert(hmem.Size, attr.TypeRef, charSet); } finally { @@ -276,6 +311,22 @@ namespace Vanara.PInvoke } } + private static Encoding GetEncoding(ClipCorrespondingTypeAttribute attr) => (Encoding)Activator.CreateInstance(attr.EncodingType ?? typeof(UnicodeEncoding)); + + /// Obtains data from a source data object. + /// The type of the object being retrieved. + /// The data object. + /// Specifies the particular clipboard format of interest. + /// + /// Part of the aspect when the data must be split across page boundaries. The most common value is -1, which identifies all of the + /// data. For the aspects DVASPECT_THUMBNAIL and DVASPECT_ICON, lindex is ignored. + /// + /// The character set to use for string types. + /// The object associated with the request. If no object can be determined, default(T) is returned. + /// This format does not support direct type access. - formatId + public static T GetData(this IDataObject dataObj, string format, int index = -1, CharSet charSet = CharSet.Auto) => + GetData(dataObj, RegisterClipboardFormat(format), index, charSet); + /// Obtains data from a source data object. /// The type of the object being retrieved. /// The data object. @@ -296,22 +347,13 @@ namespace Vanara.PInvoke lindex = index, tymed = TYMED.TYMED_HGLOBAL }; - //STGMEDIUM medium = new() { tymed = TYMED.TYMED_HGLOBAL }; dataObj.GetData(ref formatetc, out var medium); if (medium.tymed != TYMED.TYMED_HGLOBAL) throw new ArgumentException("This format does not support direct type access.", nameof(formatId)); if (medium.unionmember == default) return default; using SafeMoveableHGlobalHandle hmem = new(medium.unionmember, medium.pUnkForRelease is null); - try - { - hmem.Lock(); - return hmem.ToType(charSet == CharSet.Auto ? (StringHelper.GetCharSize(charSet) == 1 ? CharSet.Ansi : CharSet.Unicode) : charSet); - } - finally - { - hmem.Unlock(); - } + return hmem.ToType(charSet == CharSet.Auto ? (StringHelper.GetCharSize(charSet) == 1 ? CharSet.Ansi : CharSet.Unicode) : charSet); } /// Gets an HTML string from bytes returned from the clipboard. @@ -346,6 +388,46 @@ namespace Vanara.PInvoke return Encoding.UTF8.GetString(bytes, startFrag, endFrag - startFrag); } + /// + /// Determines whether the data object is capable of rendering the data described in the parameters. Objects attempting a paste or + /// drop operation can call this method before calling GetData to get an indication of whether the operation may be successful. + /// + /// The data object. + /// Specifies the particular clipboard format of interest. + /// if is available; otherwise, . + public static bool IsFormatAvailable(this IDataObject dataObj, uint formatId) + { + FORMATETC formatetc = new() + { + cfFormat = unchecked((short)(ushort)formatId), + dwAspect = DVASPECT.DVASPECT_CONTENT, + lindex = -1, + tymed = AllTymed.Value + }; + + return dataObj.QueryGetData(ref formatetc) == HRESULT.S_OK; + } + + private static readonly Lazy AllTymed = new(() => Enum.GetValues(typeof(TYMED)).Cast().Aggregate((a, b) => a | b)); + + /// Transfer a data stream to an object that contains a data source. + /// The data object. + /// Specifies the particular clipboard format of interest. + /// The object to add. + /// + /// Indicates how much detail should be contained in the rendering. This parameter should be one of the DVASPECT enumeration values. + /// A single clipboard format can support multiple aspects or views of the object. Most data and presentation transfer and caching + /// methods pass aspect information. For example, a caller might request an object's iconic picture, using the metafile clipboard + /// format to retrieve it. Note that only one DVASPECT value can be used in dwAspect. That is, dwAspect cannot be the result of a + /// Boolean OR operation on several DVASPECT values. + /// + /// + /// Part of the aspect when the data must be split across page boundaries. The most common value is -1, which identifies all of the + /// data. For the aspects DVASPECT_THUMBNAIL and DVASPECT_ICON, lindex is ignored. + /// + public static void SetData(this IDataObject dataObj, string format, object obj, DVASPECT aspect = DVASPECT.DVASPECT_CONTENT, int index = -1) => + SetData(dataObj, RegisterClipboardFormat(format), obj, aspect, index); + /// Transfer a data stream to an object that contains a data source. /// The data object. /// Specifies the particular clipboard format of interest. @@ -363,104 +445,98 @@ namespace Vanara.PInvoke /// public static void SetData(this IDataObject dataObj, uint formatId, object obj, DVASPECT aspect = DVASPECT.DVASPECT_CONTENT, int index = -1) { - TYMED tymed = TYMED.TYMED_HGLOBAL; - IntPtr mbr = default; - switch (obj) + ClipCorrespondingTypeAttribute attr = ShellClipboardFormat.clipFmtIds.Value.TryGetValue(formatId, out (string name, ClipCorrespondingTypeAttribute attr) data) ? data.attr : null; + + TYMED tymed = attr?.Medium ?? TYMED.TYMED_HGLOBAL; + CharSet charSet = GetCharSet(attr); + IntPtr mbr = attr?.Formatter is null ? default : ((IClipboardFormatter)Activator.CreateInstance(attr.Formatter)).Write(obj); + if (mbr == default) { - case null: - tymed = TYMED.TYMED_NULL; - break; + switch (obj) + { + case null: + tymed = TYMED.TYMED_NULL; + break; - case byte[] bytes: - ClipboardBytesFormatter.Instance.Write(bytes); - break; + case byte[] bytes: + mbr = ClipboardBytesFormatter.Instance.Write(bytes); + break; - case MemoryStream mstream: - ClipboardBytesFormatter.Instance.Write(mstream.GetBuffer()); - break; + // TODO + //case MemoryStream mstream: + // mbr = ClipboardBytesFormatter.Instance.Write(mstream.GetBuffer()); + // break; - case Stream stream: - byte[] sbytes = new byte[stream.Length]; - stream.Position = 0; - stream.Read(sbytes, 0, sbytes.Length); - ClipboardBytesFormatter.Instance.Write(sbytes); - break; + // TODO + //case Stream stream: + // ComStream cstream = new(stream); + // tymed = TYMED.TYMED_ISTREAM; + // mbr = Marshal.GetIUnknownForObject((IStream)cstream); + // break; - case string str: - if (CLIPFORMAT.CF_UNICODETEXT.Equals(formatId) || - RegisterClipboardFormat(ShellClipboardFormat.CFSTR_INETURLW) == formatId || - RegisterClipboardFormat(ShellClipboardFormat.CFSTR_FILENAMEW) == formatId) - { - ClipboardBytesFormatter.Instance.Write(StringHelper.GetBytes(str, true, CharSet.Unicode)); - } - else if (CLIPFORMAT.CF_TEXT.Equals(formatId) || CLIPFORMAT.CF_OEMTEXT.Equals(formatId) || - RegisterClipboardFormat(ShellClipboardFormat.CF_RTF) == formatId || - RegisterClipboardFormat(ShellClipboardFormat.CFSTR_INETURLA) == formatId || - RegisterClipboardFormat(ShellClipboardFormat.CFSTR_FILENAMEA) == formatId || - RegisterClipboardFormat(ShellClipboardFormat.CF_CSV) == formatId) - { - ClipboardBytesFormatter.Instance.Write(StringHelper.GetBytes(str, true, CharSet.Ansi)); - } - else if (RegisterClipboardFormat(ShellClipboardFormat.CF_HTML) == formatId) - { - ClipboardHtmlFormatter.Instance.Write(str); - } - else - { - ClipboardBytesFormatter.Instance.Write(StringHelper.GetBytes(str, true, CharSet.Auto)); - } + case string str: + //if (CLIPFORMAT.CF_TEXT.Equals(formatId)) + // mbr = ClipboardBytesFormatter.Instance.Write(UnicodeToAnsiBytes(str)); + //else + mbr = ClipboardBytesFormatter.Instance.Write(StringHelper.GetBytes(str, GetEncoding(attr), true)); + break; - break; + case IEnumerable strlist: + // Handle HDROP specifically since its formatter cannot be specified. + if (CLIPFORMAT.CF_HDROP.Equals(formatId)) + mbr = new ClipboardHDROPFormatter().Write(strlist, charSet != CharSet.Ansi); + else + mbr = strlist.MarshalToPtr(StringListPackMethod.Concatenated, MoveableHGlobalMemoryMethods.Instance.AllocMem, out _, charSet, 0, + MoveableHGlobalMemoryMethods.Instance.LockMem, MoveableHGlobalMemoryMethods.Instance.UnlockMem); + break; - case IEnumerable strlist: - if (CLIPFORMAT.CF_HDROP.Equals(formatId) || - formatId == RegisterClipboardFormat(ShellClipboardFormat.CFSTR_FILENAMEMAPA) || - formatId == RegisterClipboardFormat(ShellClipboardFormat.CFSTR_FILENAMEMAPW)) - { - mbr = ClipboardHDROPFormatter.Instance.Write(strlist); - } + case IStream str: + tymed = TYMED.TYMED_ISTREAM; + mbr = Marshal.GetIUnknownForObject(str); + break; - break; + case IStorage store: + tymed = TYMED.TYMED_ISTORAGE; + mbr = Marshal.GetIUnknownForObject(store); + break; - case IStream str: - tymed = TYMED.TYMED_ISTREAM; - mbr = Marshal.GetIUnknownForObject(str); - break; + // TODO + //case FileInfo fileInfo: + // tymed = TYMED.TYMED_FILE; + // mbr = Marshal.StringToBSTR(fileInfo.FullName); + // break; - case IStorage store: - tymed = TYMED.TYMED_ISTORAGE; - mbr = Marshal.GetIUnknownForObject(store); - break; + case System.Runtime.Serialization.ISerializable ser: + mbr = new ClipboardSerializedFormatter().Write(ser); + break; - case System.Runtime.Serialization.ISerializable ser: - mbr = ClipboardSerializedFormatter.Instance.Write(ser); - break; + case SafeMoveableHGlobalHandle hg: + mbr = hg.TakeOwnership(); + break; - case SafeMoveableHGlobalHandle hg: - mbr = hg.TakeOwnership(); - break; + // TODO + //case SafeAllocatedMemoryHandle h: + // mbr = new SafeMoveableHGlobalHandle(h).TakeOwnership(); + // break; - case SafeAllocatedMemoryHandle h: - mbr = new SafeMoveableHGlobalHandle(h).TakeOwnership(); - break; + case HBITMAP: + case SafeHBITMAP: + tymed = TYMED.TYMED_GDI; + mbr = ((IHandle)obj).DangerousGetHandle(); + break; - case HBITMAP: - case SafeHBITMAP: - tymed = TYMED.TYMED_GDI; - mbr = ((IHandle)obj).DangerousGetHandle(); - break; + case HMETAFILE: + case SafeHMETAFILE: + tymed = TYMED.TYMED_MFPICT; + mbr = ((IHandle)obj).DangerousGetHandle(); + break; - case HMETAFILE: - case SafeHMETAFILE: - tymed = TYMED.TYMED_MFPICT; - mbr = ((IHandle)obj).DangerousGetHandle(); - break; - - case HENHMETAFILE: - case SafeHENHMETAFILE: - tymed = TYMED.TYMED_ENHMF; - mbr = ((IHandle)obj).DangerousGetHandle(); - break; + case HENHMETAFILE: + case SafeHENHMETAFILE: + tymed = TYMED.TYMED_ENHMF; + mbr = ((IHandle)obj).DangerousGetHandle(); + break; + } } FORMATETC formatetc = new() { @@ -473,6 +549,18 @@ namespace Vanara.PInvoke dataObj.SetData(ref formatetc, ref medium, true); } + /// Transfer a data stream to an object that contains a data source. + /// The type of the object being passed. + /// The data object. + /// Specifies the particular clipboard format of interest. + /// The object to add. + /// + /// Part of the aspect when the data must be split across page boundaries. The most common value is -1, which identifies all of the + /// data. For the aspects DVASPECT_THUMBNAIL and DVASPECT_ICON, lindex is ignored. + /// + public static void SetData(this IDataObject dataObj, string format, T obj, int index = -1) where T : struct => + SetData(dataObj, RegisterClipboardFormat(format), SafeMoveableHGlobalHandle.CreateFromStructure(obj), DVASPECT.DVASPECT_CONTENT, index); + /// Transfer a data stream to an object that contains a data source. /// The type of the object being passed. /// The data object. @@ -485,6 +573,20 @@ namespace Vanara.PInvoke public static void SetData(this IDataObject dataObj, uint formatId, T obj, int index = -1) where T : struct => SetData(dataObj, formatId, SafeMoveableHGlobalHandle.CreateFromStructure(obj), DVASPECT.DVASPECT_CONTENT, index); + /// Sets a URL with optional title to a data object. + /// The data object. + /// The URL. + /// The title. This value can be . + /// url + public static void SetUrl(this IDataObject dataObj, string url, string title = null) + { + if (url is null) throw new ArgumentNullException(nameof(url)); + dataObj.SetData(CLIPFORMAT.CF_UNICODETEXT, url); + dataObj.SetData(ShellClipboardFormat.CF_HTML, $"{System.Net.WebUtility.HtmlEncode(title ?? url)}"); + dataObj.SetData(ShellClipboardFormat.CFSTR_INETURLA, url); + dataObj.SetData(ShellClipboardFormat.CFSTR_INETURLW, url); + } + /// Obtains data from a source data object. /// The type of the object being retrieved. /// The data object. @@ -497,46 +599,17 @@ namespace Vanara.PInvoke /// if data is available and retrieved; otherwise . public static bool TryGetData(this IDataObject dataObj, uint formatId, out T obj, int index = -1) { - FORMATETC formatetc = new() - { - cfFormat = unchecked((short)(ushort)formatId), - dwAspect = DVASPECT.DVASPECT_CONTENT, - lindex = index, - tymed = TYMED.TYMED_HGLOBAL - }; - obj = default; - if (dataObj.QueryGetData(ref formatetc) != HRESULT.S_OK) - return false; - try - { - STGMEDIUM medium = new() { tymed = TYMED.TYMED_HGLOBAL }; - dataObj.GetData(ref formatetc, out medium); - if (medium.tymed == TYMED.TYMED_HGLOBAL && medium.unionmember != default) - { - using SafeMoveableHGlobalHandle hmem = new(medium.unionmember, medium.pUnkForRelease is null); - obj = hmem.ToType(); + if (IsFormatAvailable(dataObj, formatId)) + try { + var charSet = GetCharSet(ShellClipboardFormat.clipFmtIds.Value.TryGetValue(formatId, out (string name, ClipCorrespondingTypeAttribute attr) data) ? data.attr : null); + obj = GetData(dataObj, formatId, index, charSet); return true; } - } - catch - { - } + catch { } + obj = default; return false; } - /// Converts an ANSI string to Unicode. - /// The ANSI string value. - /// The Unicode string value. - public static string AnsiToUnicode(string value) - { - if (string.IsNullOrEmpty(value)) return value; - byte[] ret = null; - var sz = MultiByteToWideChar(0, 0, value, value.Length, ret, 0); - ret = new byte[(int)sz]; - MultiByteToWideChar(0, 0, value, value.Length, ret, sz); - return Encoding.Unicode.GetString(ret); - } - /// Converts an Unicode string to ANSI. /// The Unicode string value. /// The ANSI string value. @@ -550,6 +623,19 @@ namespace Vanara.PInvoke return ret; } + private static CharSet GetCharSet(ClipCorrespondingTypeAttribute attr) + { + CharSet charSet = CharSet.Auto; + if (attr is not null) + { + if (attr.EncodingType == typeof(UTF8Encoding) || attr.EncodingType == typeof(ASCIIEncoding)) + charSet = CharSet.Ansi; + else if (attr.EncodingType == typeof(UnicodeEncoding)) + charSet = CharSet.Unicode; + } + return charSet; + } + /// /// /// Used with the CFSTR_SHELLIDLIST clipboard format to transfer the pointer to an item identifier list (PIDL) of one or more Shell @@ -864,6 +950,20 @@ namespace Vanara.PInvoke nFileSizeLow = (uint)(value & 0xFFFFFFFF); } } + + /// Performs an implicit conversion from to . + /// The instance. + /// The result of the conversion. + public static implicit operator FILEDESCRIPTOR(System.IO.FileInfo fi) => new FILEDESCRIPTOR() + { + dwFlags = FD_FLAGS.FD_ATTRIBUTES | FD_FLAGS.FD_ACCESSTIME | FD_FLAGS.FD_CREATETIME | FD_FLAGS.FD_WRITESTIME | FD_FLAGS.FD_FILESIZE | (Marshal.SystemDefaultCharSize > 1 ? FD_FLAGS.FD_UNICODE : 0), + dwFileAttributes = (FileFlagsAndAttributes)fi.Attributes, + cFileName = fi.FullName, + ftCreationTime = fi.CreationTime.ToFileTimeStruct(), + ftLastAccessTime = fi.LastAccessTime.ToFileTimeStruct(), + ftLastWriteTime = fi.LastWriteTime.ToFileTimeStruct(), + nFileSize = unchecked((ulong)fi.Length) + }; } /// Defines the CF_FILEGROUPDESCRIPTOR clipboard format. @@ -1039,20 +1139,20 @@ namespace Vanara.PInvoke /// /// If dwScope is not set to RESOURCE_CONNECTED, this field is undefined. /// - public IntPtr lpLocalName; + public StrPtrAuto lpLocalName; /// /// If the enumerated item is a network resource, this field contains a remote network name. This name may be then passed to /// NPAddConnection to make a network connection if dwUsage is set to RESOURCEUSAGE_CONNECTABLE. If the enumerated item is /// a current connection, this field will refer to the remote network name that lpLocalName is connected to. /// - public IntPtr lpRemoteName; + public StrPtrAuto lpRemoteName; /// May be any provider-supplied comment associated with the enumerated item. - public IntPtr lpComment; + public StrPtrAuto lpComment; /// Specifies the name of the provider that owns this enumerated item. - public IntPtr lpProvider; + public StrPtrAuto lpProvider; } /// Defines the clipboard format. @@ -1165,22 +1265,22 @@ namespace Vanara.PInvoke public static class ShellClipboardFormat { /// Comma Separated Value - [ClipCorrespondingType(typeof(string), TYMED.TYMED_HGLOBAL, EncodingType = typeof(ASCIIEncoding))] + [ClipCorrespondingType(typeof(string), EncodingType = typeof(UTF8Encoding))] public const string CF_CSV = "Csv"; /// HTML Format - [ClipCorrespondingType(typeof(string), TYMED.TYMED_HGLOBAL, EncodingType = typeof(UTF8Encoding), Formatter = typeof(ClipboardHtmlFormatter))] + [ClipCorrespondingType(typeof(string), EncodingType = typeof(UTF8Encoding), Formatter = typeof(ClipboardHtmlFormatter))] public const string CF_HTML = "HTML Format"; /// RichEdit Text and Objects public const string CF_RETEXTOBJ = "RichEdit Text and Objects"; /// Rich Text Format - [ClipCorrespondingType(typeof(string), TYMED.TYMED_HGLOBAL, EncodingType = typeof(ASCIIEncoding))] + [ClipCorrespondingType(typeof(string), EncodingType = typeof(UTF8Encoding))] public const string CF_RTF = "Rich Text Format"; /// Rich Text Format Without Objects - [ClipCorrespondingType(typeof(string), TYMED.TYMED_HGLOBAL, EncodingType = typeof(ASCIIEncoding))] + [ClipCorrespondingType(typeof(string), EncodingType = typeof(UTF8Encoding))] public const string CF_RTFNOOBJS = "Rich Text Format Without Objects"; /// Undocumented. @@ -1227,7 +1327,7 @@ namespace Vanara.PInvoke /// to a single file, it could also, for example, represent data extracted by the source from a database or text document. /// /// - [ClipCorrespondingType(typeof(FILEGROUPDESCRIPTOR), TYMED.TYMED_HGLOBAL, EncodingType = typeof(ASCIIEncoding))] + [ClipCorrespondingType(typeof(FILEGROUPDESCRIPTOR), EncodingType = typeof(UTF8Encoding))] public const string CFSTR_FILEDESCRIPTORA = "FileGroupDescriptor"; /// @@ -1246,7 +1346,7 @@ namespace Vanara.PInvoke /// to a single file, it could also, for example, represent data extracted by the source from a database or text document. /// /// - [ClipCorrespondingType(typeof(FILEGROUPDESCRIPTOR), TYMED.TYMED_HGLOBAL, EncodingType = typeof(UnicodeEncoding))] + [ClipCorrespondingType(typeof(FILEGROUPDESCRIPTOR), EncodingType = typeof(UnicodeEncoding))] public const string CFSTR_FILEDESCRIPTORW = "FileGroupDescriptorW"; /// @@ -1254,7 +1354,7 @@ namespace Vanara.PInvoke /// memory object. The structure's hGlobal member points to a single null-terminated string containing the file's fully qualified /// file path. This format has been superseded by CF_HDROP, but it is supported for backward compatibility with Windows 3.1 applications. /// - [ClipCorrespondingType(typeof(string), TYMED.TYMED_HGLOBAL, EncodingType = typeof(ASCIIEncoding))] + [ClipCorrespondingType(typeof(string), EncodingType = typeof(UTF8Encoding))] public const string CFSTR_FILENAMEA = "FileName"; /// @@ -1264,7 +1364,7 @@ namespace Vanara.PInvoke /// the accompanying CF_HDROP format. The format of the character array is the same as that used by CF_HDROP to list the /// transferred files. /// - [ClipCorrespondingType(typeof(string[]), TYMED.TYMED_HGLOBAL, EncodingType = typeof(ASCIIEncoding))] + [ClipCorrespondingType(typeof(string[]), EncodingType = typeof(UTF8Encoding))] public const string CFSTR_FILENAMEMAPA = "FileNameMap"; /// @@ -1274,7 +1374,7 @@ namespace Vanara.PInvoke /// the accompanying CF_HDROP format. The format of the character array is the same as that used by CF_HDROP to list the /// transferred files. /// - [ClipCorrespondingType(typeof(string[]), TYMED.TYMED_HGLOBAL, EncodingType = typeof(UnicodeEncoding))] + [ClipCorrespondingType(typeof(string[]), EncodingType = typeof(UnicodeEncoding))] public const string CFSTR_FILENAMEMAPW = "FileNameMapW"; /// @@ -1282,7 +1382,7 @@ namespace Vanara.PInvoke /// memory object. The structure's hGlobal member points to a single null-terminated string containing the file's fully qualified /// file path. This format has been superseded by CF_HDROP, but it is supported for backward compatibility with Windows 3.1 applications. /// - [ClipCorrespondingType(typeof(string), TYMED.TYMED_HGLOBAL, EncodingType = typeof(UnicodeEncoding))] + [ClipCorrespondingType(typeof(string), EncodingType = typeof(UnicodeEncoding))] public const string CFSTR_FILENAMEW = "FileNameW"; /// @@ -1308,7 +1408,7 @@ namespace Vanara.PInvoke /// UNICODE is not defined, the application retrieves the CF_TEXT/CFSTR_SHELLURL version of the URL. If UNICODE is defined, the /// application retrieves the CF_UNICODE version of the URL. /// - [ClipCorrespondingType(typeof(string), TYMED.TYMED_HGLOBAL, EncodingType = typeof(ASCIIEncoding))] + [ClipCorrespondingType(typeof(string), EncodingType = typeof(UTF8Encoding))] public const string CFSTR_INETURLA = CFSTR_SHELLURL; /// @@ -1317,11 +1417,11 @@ namespace Vanara.PInvoke /// UNICODE is not defined, the application retrieves the CF_TEXT/CFSTR_SHELLURL version of the URL. If UNICODE is defined, the /// application retrieves the CF_UNICODE version of the URL. /// - [ClipCorrespondingType(typeof(string), TYMED.TYMED_HGLOBAL, EncodingType = typeof(UnicodeEncoding))] + [ClipCorrespondingType(typeof(string), EncodingType = typeof(UnicodeEncoding))] public const string CFSTR_INETURLW = "UniformResourceLocatorW"; /// Undocumented. - [ClipCorrespondingType(typeof(string), TYMED.TYMED_HGLOBAL, EncodingType = typeof(UnicodeEncoding))] + [ClipCorrespondingType(typeof(string), EncodingType = typeof(UnicodeEncoding))] public const string CFSTR_INVOKECOMMAND_DROPPARAM = "InvokeCommand DropParam"; /// @@ -1371,7 +1471,7 @@ namespace Vanara.PInvoke /// folders as well as drive letters, the handler must be able to understand both the CSFTR_MOUNTEDVOLUME and CF_HDROP formats. /// /// - [ClipCorrespondingType(typeof(string), TYMED.TYMED_HGLOBAL, EncodingType = typeof(ASCIIEncoding))] + [ClipCorrespondingType(typeof(string), EncodingType = typeof(UTF8Encoding))] public const string CFSTR_MOUNTEDVOLUME = "MountedVolume"; /// @@ -1449,7 +1549,7 @@ namespace Vanara.PInvoke /// CF_HDROP. However, the pFiles member of the DROPFILES structure contains one or more friendly names of printers instead of /// file paths. /// - [ClipCorrespondingType(typeof(string), TYMED.TYMED_HGLOBAL, EncodingType = typeof(ASCIIEncoding))] + [ClipCorrespondingType(typeof(string), EncodingType = typeof(UTF8Encoding))] public const string CFSTR_PRINTERGROUP = "PrinterFriendlyName"; /// Undocumented. @@ -1480,7 +1580,7 @@ namespace Vanara.PInvoke public const string CFSTR_SHELLIDLISTOFFSET = "Shell Object Offsets"; /// This format identifier has been deprecated; use instead. - [ClipCorrespondingType(typeof(string), TYMED.TYMED_HGLOBAL, EncodingType = typeof(ASCIIEncoding))] + [ClipCorrespondingType(typeof(string), EncodingType = typeof(UTF8Encoding))] public const string CFSTR_SHELLURL = "UniformResourceLocator"; /// @@ -1516,6 +1616,66 @@ namespace Vanara.PInvoke /// Undocumented. [ClipCorrespondingType(typeof(uint))] public const string CFSTR_ZONEIDENTIFIER = "ZoneIdentifier"; + + internal static readonly Lazy> clipFmtIds = new(() => + { + Type cftype = typeof(CLIPFORMAT); + var knownIds = cftype.GetFields(BindingFlags.Static | BindingFlags.Public). + Where(f => f.FieldType == cftype && f.IsInitOnly). + ToDictionary(f => (uint)(CLIPFORMAT)f.GetValue(null), f => (f.Name, f.GetCustomAttributes().FirstOrDefault())); + foreach (FieldInfo f in typeof(ShellClipboardFormat).GetConstants().Where(f => f.FieldType == typeof(string))) + { + string s = (string)f.GetValue(null); + uint id = RegisterClipboardFormat(s); + if (!knownIds.ContainsKey(id)) + knownIds.Add(id, (s, f.GetCustomAttributes().FirstOrDefault())); + } + return knownIds; + }); + + /// Retrieves from the clipboard the name of the specified registered format. + /// The type of format to be retrieved. + /// The format name. + public static string GetName(uint formatId) + { + if (clipFmtIds.Value.TryGetValue(formatId, out var value)) + return value.name; + + // Ask sysetm for the registered name + StringBuilder sb = new(80); + int ret; + while (0 != (ret = GetClipboardFormatName(formatId, sb, sb.Capacity))) + { + if (ret < sb.Capacity - 1) + { + clipFmtIds.Value.Add(formatId, (sb.ToString(), null)); + return sb.ToString(); + } + sb.Capacity *= 2; + } + + // Failing all elsewhere, return value as hex string + return string.Format(CultureInfo.InvariantCulture, "0x{0:X4}", formatId); + } + + /// Registers a new clipboard format. This format can then be used as a valid clipboard format. + /// The name of the new format. + /// The registered clipboard format identifier. + /// format + /// + /// If a registered format with the specified name already exists, a new format is not registered and the return value identifies the + /// existing format. This enables more than one application to copy and paste data using the same registered clipboard format. Note + /// that the format name comparison is case-insensitive. + /// + public static uint Register(string format) + { + if (format is null) throw new ArgumentNullException(nameof(format)); + + var id = Win32Error.ThrowLastErrorIf(RegisterClipboardFormat(format), v => v == 0); + if (!clipFmtIds.Value.ContainsKey(id)) + clipFmtIds.Value.Add(id, (format, null)); + return id; + } } internal class ClipboardBytesFormatter : IClipboardFormatter @@ -1529,8 +1689,6 @@ namespace Vanara.PInvoke internal class ClipboardHDROPFormatter : IClipboardFormatter { - public static ClipboardHDROPFormatter Instance { get; } = new(); - public object Read(IntPtr hGlobal) { SafeMoveableHGlobalHandle h = new(hGlobal, false); @@ -1542,15 +1700,18 @@ namespace Vanara.PInvoke public IntPtr Write(object value, bool wideChar) { - SafeMoveableHGlobalHandle dfmem = SafeMoveableHGlobalHandle.CreateFromStructure(new DROPFILES { pFiles = (uint)Marshal.SizeOf(typeof(DROPFILES)), fWide = wideChar }); - new NativeMemoryStream(dfmem) { CharSet = wideChar ? CharSet.Unicode : CharSet.Ansi }.Write((string[])value); + if (value is not string[] vals) throw new ArgumentException(null, nameof(value)); + DROPFILES drop = new() { pFiles = (uint)Marshal.SizeOf(typeof(DROPFILES)), fWide = wideChar }; + SafeMoveableHGlobalHandle dfmem = new(drop.pFiles + vals.Sum(v => (v.Length + 1) * (wideChar ? 2 : 1)) + 2); + dfmem.Write(drop); + dfmem.CallLocked(p => p.Write(vals, StringListPackMethod.Concatenated, wideChar ? CharSet.Unicode : CharSet.Ansi, (int)drop.pFiles, dfmem.Size)); return dfmem.TakeOwnership(); } } internal class ClipboardHtmlFormatter : ClipboardBytesFormatter { - public new static ClipboardHtmlFormatter Instance { get; } = new(); + public new static IClipboardFormatter Instance { get; } = new ClipboardHtmlFormatter(); public override object Read(IntPtr hGlobal) => GetHtmlFromClipboard((byte[])base.Read(hGlobal)); @@ -1559,30 +1720,27 @@ namespace Vanara.PInvoke internal class ClipboardSerializedFormatter : IClipboardFormatter { - private static readonly byte[] guid = new Guid("FD9EA796-3B13-4370-A679-56106BB288FB").ToByteArray(); - public static ClipboardSerializedFormatter Instance { get; } = new(); + private static readonly Guid guid = new(0xFD9EA796, 0x3B13, 0x4370, 0xA6, 0x79, 0x56, 0x10, 0x6B, 0xB2, 0x88, 0xFB); + private static readonly byte[] guidBytes = guid.ToByteArray(); + + public static bool IsSerialized(SafeMoveableHGlobalHandle h) => h.Size >= 16 && h.ToStructure() == guid; public object Read(IntPtr hGlobal) { byte[] bytes = (byte[])ClipboardBytesFormatter.Instance.Read(hGlobal); - if (bytes.Length <= guid.Length || !Equals(bytes, guid, guid.Length)) + if (bytes.Length <= guidBytes.Length || !Equals(bytes, guidBytes, guidBytes.Length)) { throw new InvalidDataException(); } - using MemoryStream mem = new(bytes, false) { Position = guid.Length }; + using MemoryStream mem = new(bytes, false) { Position = guidBytes.Length }; return new BinaryFormatter() { AssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple }.Deserialize(mem); static bool Equals(byte[] bytes, byte[] guid, int length) { for (int i = 0; i < length; i++) - { if (bytes[i] != guid[i]) - { return false; - } - } - return true; } } @@ -1591,7 +1749,7 @@ namespace Vanara.PInvoke { System.Runtime.Serialization.ISerializable ser = (System.Runtime.Serialization.ISerializable)value; MemoryStream ms = new(); - new BinaryWriter(ms).Write(guid); + new BinaryWriter(ms).Write(guidBytes); new BinaryFormatter().Serialize(ms, ser); return ClipboardBytesFormatter.Instance.Write(ms.GetBuffer()); } diff --git a/UnitTests/Windows.Shell/ClipboardTests.cs b/UnitTests/Windows.Shell/ClipboardTests.cs index 582e7aaa..e4df2af5 100644 --- a/UnitTests/Windows.Shell/ClipboardTests.cs +++ b/UnitTests/Windows.Shell/ClipboardTests.cs @@ -1,8 +1,13 @@ -using NUnit.Framework; +using Newtonsoft.Json.Linq; +using NUnit.Framework; using System; +using System.IO; using System.Linq; +using System.Runtime.InteropServices.ComTypes; using System.Threading; using System.Windows.Forms; +using Vanara.Extensions; +using Vanara.InteropServices; using Vanara.PInvoke; using Vanara.PInvoke.Tests; using static Vanara.PInvoke.Shell32; @@ -11,28 +16,13 @@ using WFClipboard = System.Windows.Forms.Clipboard; namespace Vanara.Windows.Shell.Tests { - [TestFixture/*, SingleThreaded*/] + [TestFixture, SingleThreaded] public class ClipboardTests { private const string html = "
“We’ve been here”
"; - - [Test] - public void CtorTest() - { - const string txt = "Test"; - using (var cb = new Clipboard(true, User32.GetDesktopWindow())) - cb.SetText(txt, txt, txt); - using (var cb = new Clipboard(false, User32.GetDesktopWindow())) - Assert.That(cb.GetText(TextDataFormat.UnicodeText), Is.EqualTo(txt)); - using (var cb = new Clipboard()) - Assert.That(cb.GetText(TextDataFormat.UnicodeText), Is.EqualTo(txt)); - using (var cb = new Clipboard(true)) - cb.SetText(txt, txt, txt); - using (var cb = new Clipboard()) - Assert.That(cb.GetText(TextDataFormat.UnicodeText), Is.EqualTo(txt)); - using (var cb = new Clipboard(false, User32.GetDesktopWindow())) - Assert.That(cb.GetText(TextDataFormat.UnicodeText), Is.EqualTo(txt)); - } + readonly string[] files = { TestCaseSources.SmallFile, TestCaseSources.ImageFile, TestCaseSources.LogFile }; + const string txt = @"“0’0©0è0”"; + const string ptxt = "ABC123"; [Test] public void DumpWFClipboardTest() @@ -53,21 +43,15 @@ namespace Vanara.Windows.Shell.Tests [Test] public void EnumFormatsTest() { - using var cb = new Clipboard(); - var fmts = cb.EnumAvailableFormats(); + SHCreateDataObject(ppv: out var ido).ThrowIfFailed(); + ido.SetData(CLIPFORMAT.CF_UNICODETEXT, "Test"); + + var fmts = ido.EnumFormats().ToArray(); Assert.That(fmts, Is.Not.Empty); - TestContext.Write(string.Join(", ", fmts.Select(f => Clipboard.GetFormatName(f)))); + TestContext.Write(string.Join(", ", fmts.Select(f => Clipboard.GetFormatName((uint)f.cfFormat)))); var fmt = fmts.First(); - Assert.IsTrue(Clipboard.IsFormatAvailable(fmt)); - } - - [Test] - public void GetNativeTextTest() - { - using var cb = new Clipboard(); - foreach (TextDataFormat e in Enum.GetValues(typeof(TextDataFormat))) - TestContext.WriteLine($"{e}: {cb.GetText(e)}"); + Assert.IsTrue(ido.IsFormatAvailable((uint)fmt.cfFormat)); } [Test] @@ -77,83 +61,223 @@ namespace Vanara.Windows.Shell.Tests Assert.That(Clipboard.GetFirstFormatAvailable(fmts), Is.GreaterThan(0)); } - [Test, Apartment(ApartmentState.STA)] + [Test] public void GetSetShellItems1() { - //Ole32.CoInitializeEx(default, Ole32.COINIT.COINIT_APARTMENTTHREADED).ThrowIfFailed(); - //Clipboard.DataObject = null; - string[] files = { TestCaseSources.SmallFile, TestCaseSources.ImageFile, TestCaseSources.LogFile }; ShellItemArray items = new(Array.ConvertAll(files, f => new ShellItem(f))); - Clipboard.SetShellItems(items); - var shArray = Clipboard.GetShellItemArray(); + var ido = items.ToDataObject(); + var shArray = ShellItemArray.FromDataObject(ido); Assert.That(shArray.Count, Is.GreaterThan(0)); - Assert.IsTrue(files.SequenceEqual(shArray.Select(s => s.FileSystemPath))); + CollectionAssert.AreEquivalent(files, shArray.Select(s => s.FileSystemPath)); } [Test] public void GetSetShellItems2() { Clipboard.DataObject = null; - string[] files = { TestCaseSources.SmallFile, TestCaseSources.ImageFile, TestCaseSources.LogFile }; ShellItem[] items = Array.ConvertAll(files, f => new ShellItem(f)); Clipboard.SetShellItems(items); var shArray = Clipboard.GetShellItemArray(); Assert.That(shArray.Count, Is.GreaterThan(0)); - Assert.IsTrue(files.SequenceEqual(shArray.Select(s => s.FileSystemPath))); + CollectionAssert.AreEquivalent(files, shArray.Select(s => s.FileSystemPath)); + } + + [Test] + public void GetSetDataTest() + { + SHCreateDataObject(ppv: out var ido).ThrowIfFailed(); + + //using var hPal = Gdi32.CreatePalette(new LOGPALETTE() { palNumEntries = 256, palVersion = 0x0300, palPalEntry = new PALETTEENTRY[256] }); + //ido.SetData(CLIPFORMAT.CF_PALETTE, hPal); + //Assert.That((HPALETTE)ido.GetData(CLIPFORMAT.CF_PALETTE), Is.EqualTo((HPALETTE)hPal)); + + using System.Drawing.Bitmap bmp = new(TestCaseSources.BmpFile); + using Gdi32.SafeHBITMAP hBmp = new(bmp.GetHbitmap()); + ido.SetData(CLIPFORMAT.CF_BITMAP, hBmp); + Assert.AreEqual((HBITMAP)ido.GetData(CLIPFORMAT.CF_BITMAP), (HBITMAP)hBmp); + + //using System.Drawing.Imaging.Metafile enhMeta = new System.Drawing.Imaging.Metafile(TestCaseSources.TempChildDirWhack + "test.wmf"); + //using Gdi32.SafeHENHMETAFILE hEnh = new(enhMeta.GetHenhmetafile()); + //ido.SetData(CLIPFORMAT.CF_ENHMETAFILE, hEnh); + //Assert.That((HENHMETAFILE)ido.GetData(CLIPFORMAT.CF_ENHMETAFILE), Is.EqualTo((HENHMETAFILE)hEnh)); + + ido.SetData(CLIPFORMAT.CF_HDROP, files); + ido.SetData(ShellClipboardFormat.CFSTR_FILENAMEMAPA, files); + ido.SetData(ShellClipboardFormat.CFSTR_FILENAMEMAPW, files); + CollectionAssert.AreEquivalent(files, (string[])ido.GetData(CLIPFORMAT.CF_HDROP)); + CollectionAssert.AreEquivalent(files, (string[])ido.GetData(ShellClipboardFormat.CFSTR_FILENAMEMAPA)); + CollectionAssert.AreEquivalent(files, (string[])ido.GetData(ShellClipboardFormat.CFSTR_FILENAMEMAPW)); + + ido.SetData(CLIPFORMAT.CF_OEMTEXT, ptxt); + Assert.AreEqual(ido.GetData(CLIPFORMAT.CF_OEMTEXT), ptxt); + + ido.SetData(CLIPFORMAT.CF_TEXT, txt); + Assert.AreEqual(ido.GetData(CLIPFORMAT.CF_TEXT), txt); + + ido.SetData(CLIPFORMAT.CF_UNICODETEXT, txt); + Assert.AreEqual(ido.GetData(CLIPFORMAT.CF_UNICODETEXT), txt); + + var r = new RECT(0, 8, 16, 32); + ido.SetData("RECT", r); + Assert.AreEqual(ido.GetData("RECT"), r); + + var lcid = Kernel32.GetUserDefaultLCID(); + ido.SetData(CLIPFORMAT.CF_LOCALE, lcid); + //Assert.AreEqual(ido.GetData(CLIPFORMAT.CF_LOCALE), lcid); + //Assert.That(ido.GetData(CLIPFORMAT.CF_LOCALE), lcid); + + const string csv = "a,b,c,d\n1,2,3,4"; + ido.SetData(ShellClipboardFormat.CF_CSV, csv); + Assert.AreEqual(ido.GetData(ShellClipboardFormat.CF_CSV), csv); + + ido.SetData(ShellClipboardFormat.CF_HTML, html); + Assert.AreEqual(ido.GetData(ShellClipboardFormat.CF_HTML), html); + + var rtf = System.IO.File.ReadAllText(TestCaseSources.TempDirWhack + "Test.rtf"); + ido.SetData(ShellClipboardFormat.CF_RTF, rtf); + Assert.AreEqual(ido.GetData(ShellClipboardFormat.CF_RTF), rtf); + + ido.SetData(ShellClipboardFormat.CF_RTFNOOBJS, rtf); + Assert.AreEqual(ido.GetData(ShellClipboardFormat.CF_RTFNOOBJS), rtf); + + DROPDESCRIPTION dropDesc = new() { type = DROPIMAGETYPE.DROPIMAGE_COPY, szMessage = "Move this" }; + ido.SetData(ShellClipboardFormat.CFSTR_DROPDESCRIPTION, dropDesc); + Assert.AreEqual(((DROPDESCRIPTION)ido.GetData(ShellClipboardFormat.CFSTR_DROPDESCRIPTION)).szMessage, dropDesc.szMessage); + + FILE_ATTRIBUTES_ARRAY faa = new() { cItems = 1, rgdwFileAttributes = new[] { 4U } }; + ido.SetData(ShellClipboardFormat.CFSTR_FILE_ATTRIBUTES_ARRAY, faa); + Assert.AreEqual(((FILE_ATTRIBUTES_ARRAY)ido.GetData(ShellClipboardFormat.CFSTR_FILE_ATTRIBUTES_ARRAY)).cItems, faa.cItems); + + FILEGROUPDESCRIPTOR fgd = new() { cItems = (uint)files.Length, fgd = new FILEDESCRIPTOR[files.Length] }; + for (int i = 0; i < files.Length; i++) + { + if (i == 0) { ido.SetData(ShellClipboardFormat.CFSTR_FILENAMEA, files[i]); ido.SetData(ShellClipboardFormat.CFSTR_FILENAMEW, files[i]); } + fgd.fgd[i] = new FileInfo(files[i]); + ShlwApi.SHCreateStreamOnFileEx(fgd.fgd[i].cFileName, STGM.STGM_READ | STGM.STGM_SHARE_DENY_WRITE, 0, false, null, out var istream).ThrowIfFailed(); + ido.SetData(ShellClipboardFormat.CFSTR_FILECONTENTS, istream, System.Runtime.InteropServices.ComTypes.DVASPECT.DVASPECT_CONTENT, i); + } + ido.SetData(ShellClipboardFormat.CFSTR_FILEDESCRIPTORW, fgd); + Assert.AreEqual(((FILEGROUPDESCRIPTOR)ido.GetData(ShellClipboardFormat.CFSTR_FILEDESCRIPTORW)).cItems, fgd.cItems); + Assert.IsNotNull(ido.GetData(ShellClipboardFormat.CFSTR_FILECONTENTS, index: 1)); + Assert.IsNotNull(ido.GetData(ShellClipboardFormat.CFSTR_FILENAMEA)); + Assert.IsNotNull(ido.GetData(ShellClipboardFormat.CFSTR_FILENAMEW)); + + ido.SetUrl("https://microsoft.com", "Microsoft"); + Assert.That(ido.GetData(ShellClipboardFormat.CFSTR_INETURLA), Does.StartWith("https://microsoft.com")); + Assert.That(ido.GetData(ShellClipboardFormat.CFSTR_INETURLW), Does.StartWith("https://microsoft.com")); + + ido.SetData(ShellClipboardFormat.CFSTR_INDRAGLOOP, true); + Assert.AreEqual((BOOL)ido.GetData(ShellClipboardFormat.CFSTR_INDRAGLOOP), true); + + ido.SetData(ShellClipboardFormat.CFSTR_INVOKECOMMAND_DROPPARAM, ptxt); + Assert.AreEqual(ido.GetData(ShellClipboardFormat.CFSTR_INVOKECOMMAND_DROPPARAM), ptxt); + + ido.SetData(ShellClipboardFormat.CFSTR_LOGICALPERFORMEDDROPEFFECT, Ole32.DROPEFFECT.DROPEFFECT_COPY); + Assert.AreEqual(ido.GetData(ShellClipboardFormat.CFSTR_LOGICALPERFORMEDDROPEFFECT), Ole32.DROPEFFECT.DROPEFFECT_COPY); + + ido.SetData(ShellClipboardFormat.CFSTR_MOUNTEDVOLUME, ptxt); + Assert.AreEqual(ido.GetData(ShellClipboardFormat.CFSTR_MOUNTEDVOLUME), ptxt); + + SafeLPTSTR remName = new("WINSTATION"); + NRESARRAY nres = new() { cItems = 1, nr = new NETRESOURCE[] { new() { lpRemoteName = remName } } }; + ido.SetData(ShellClipboardFormat.CFSTR_NETRESOURCES, nres); + Assert.AreEqual(((NRESARRAY)ido.GetData(ShellClipboardFormat.CFSTR_NETRESOURCES)).cItems, nres.cItems); + + ido.SetData(ShellClipboardFormat.CFSTR_PASTESUCCEEDED, Ole32.DROPEFFECT.DROPEFFECT_COPY); + Assert.AreEqual(ido.GetData(ShellClipboardFormat.CFSTR_PASTESUCCEEDED), Ole32.DROPEFFECT.DROPEFFECT_COPY); + + ido.SetData(ShellClipboardFormat.CFSTR_PERFORMEDDROPEFFECT, Ole32.DROPEFFECT.DROPEFFECT_COPY); + Assert.AreEqual(ido.GetData(ShellClipboardFormat.CFSTR_PERFORMEDDROPEFFECT), Ole32.DROPEFFECT.DROPEFFECT_COPY); + + ido.SetData(ShellClipboardFormat.CFSTR_PREFERREDDROPEFFECT, Ole32.DROPEFFECT.DROPEFFECT_COPY); + Assert.AreEqual(ido.GetData(ShellClipboardFormat.CFSTR_PREFERREDDROPEFFECT), Ole32.DROPEFFECT.DROPEFFECT_COPY); + + ido.SetData(ShellClipboardFormat.CFSTR_PRINTERGROUP, ptxt); + Assert.AreEqual(ido.GetData(ShellClipboardFormat.CFSTR_PRINTERGROUP), ptxt); + + var guid = Guid.NewGuid(); + ido.SetData(ShellClipboardFormat.CFSTR_SHELLDROPHANDLER, guid); + Assert.AreEqual(ido.GetData(ShellClipboardFormat.CFSTR_SHELLDROPHANDLER), guid); + + ido.SetData(ShellClipboardFormat.CFSTR_TARGETCLSID, guid); + Assert.AreEqual(ido.GetData(ShellClipboardFormat.CFSTR_TARGETCLSID), guid); + + ido.SetData(ShellClipboardFormat.CFSTR_UNTRUSTEDDRAGDROP, 16U); + Assert.AreEqual(ido.GetData(ShellClipboardFormat.CFSTR_UNTRUSTEDDRAGDROP), 16U); + + ido.SetData("ByteArray", new byte[] { 1,2,3,4,5,6,7,8 }); + Assert.AreEqual(((byte[])ido.GetData("ByteArray")).Length, 8); + + //using var fs = File.OpenRead(files[0]); + //ido.SetData("Stream", fs); + //Assert.That(ido.GetData("Stream"), Is.TypeOf()); + + //ido.SetData("StringArray", files); + //CollectionAssert.AreEquivalent(files, ido.GetData("StringArray")); + + // CFSTR_SHELLIDLIST + + ShlwApi.SHCreateStreamOnFileEx(files[0], STGM.STGM_READ | STGM.STGM_SHARE_DENY_WRITE, 0, false, null, out var istream2).ThrowIfFailed(); + ido.SetData("IStream", istream2); + Assert.That(ido.GetData("IStream") as IStream, Is.Not.Null); + + // IStorage + + //var fi = new FileInfo(files[0]); + //ido.SetData("File", fi); + //Assert.AreEqual(ido.GetData("File"), files[0]); + + // ISerializable + System.Uri uri = new("https://microsoft.com"); + ido.SetData("Uri", uri); + Assert.AreEqual(ido.GetData("Uri"), uri); + + // SafeAllocated + //SafeCoTaskMemHandle h = SafeCoTaskMemHandle.CreateFromStringList(files); + //ido.SetData("hMem", h); + //Assert.AreEqual(ido.GetData("hMem") as byte[], h.GetBytes()); } [Test] public void SetNativeTextHtmlTest() { - using (var cb = new Clipboard(true)) - cb.SetText(html, TextDataFormat.Html); - using (var cb = new Clipboard()) - { - var outVal = cb.GetText(TextDataFormat.Html); - Assert.That(outVal, Is.EqualTo(html)); - } + SHCreateDataObject(ppv: out var ido).ThrowIfFailed(); + ido.SetData(ShellClipboardFormat.Register(ShellClipboardFormat.CF_HTML), html); + var outVal = ido.GetData(ShellClipboardFormat.Register(ShellClipboardFormat.CF_HTML)); + Assert.That(outVal, Is.EqualTo(html)); } [Test] public void SetNativeTextMultTest() { const string stxt = "112233"; - using (var cb = new Clipboard(true)) - cb.SetText(stxt); - using (var cb = new Clipboard()) - Assert.That(cb.GetText(TextDataFormat.Text), Is.EqualTo(stxt)); + Clipboard.SetText(stxt); + Assert.That(Clipboard.GetText(TextDataFormat.Text), Is.EqualTo(stxt)); - const string txt = @"“0’0©0è0”"; - using (var cb = new Clipboard(true)) - cb.SetText(txt, txt); - using (var cb = new Clipboard()) - { - Assert.That(cb.GetText(TextDataFormat.Text), Is.EqualTo(txt)); - Assert.That(cb.GetText(TextDataFormat.UnicodeText), Is.EqualTo(txt)); - Assert.That(cb.GetText(TextDataFormat.Html), Is.EqualTo(txt)); - TestContext.WriteLine(cb.GetText(TextDataFormat.Html)); - } + Clipboard.SetText(txt, txt); + Assert.That(Clipboard.GetText(TextDataFormat.Text), Is.EqualTo(txt)); + Assert.That(Clipboard.GetText(TextDataFormat.UnicodeText), Is.EqualTo(txt)); + Assert.That(Clipboard.GetText(TextDataFormat.Html), Is.EqualTo(txt)); + TestContext.WriteLine(Clipboard.GetText(TextDataFormat.Html)); } [Test] public void SetNativeTextUnicodeTest() { const string txt = @"“0’0©0è0”"; - using (var cb = new Clipboard(true)) - cb.SetText(txt, TextDataFormat.UnicodeText); - using (var cb = new Clipboard()) - Assert.That(cb.GetText(TextDataFormat.UnicodeText), Is.EqualTo(txt)); + Clipboard.SetText(txt, TextDataFormat.UnicodeText); + Assert.That(Clipboard.GetText(TextDataFormat.UnicodeText), Is.EqualTo(txt)); } //[Test] public void ChangeEventTest() { - var sawChange = new System.Threading.ManualResetEvent(false); + var sawChange = new ManualResetEvent(false); Clipboard.ClipboardUpdate += OnUpdate; - System.Threading.Thread.SpinWait(1000); + Thread.SpinWait(1000); WFClipboard.SetText("Hello"); - //using var cb = new Clipboard(); - //cb.SetText("Hello"); + //using var Clipboard = new Clipboard(); + //Clipboard.SetText("Hello"); Assert.IsTrue(sawChange.WaitOne(5000)); Clipboard.ClipboardUpdate -= OnUpdate; diff --git a/Windows.Shell.Common/NativeClipboard.cs b/Windows.Shell.Common/NativeClipboard.cs index 9fca0bfb..ed1b7eb9 100644 --- a/Windows.Shell.Common/NativeClipboard.cs +++ b/Windows.Shell.Common/NativeClipboard.cs @@ -1,15 +1,8 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; using Vanara.Extensions; using Vanara.InteropServices; using Vanara.PInvoke; @@ -45,38 +38,11 @@ namespace Vanara.Windows.Shell /// disposal. This can be called multiple times in nested calls and will ensure the Clipboard is only opened and closed at the highest scope. ///
/// - public class NativeClipboard : IDisposable + public static class NativeClipboard { private static readonly object objectLock = new(); - private static Dictionary knownIds; private static ListenerWindow listener; - [ThreadStatic] - private static bool open = false; - - private readonly bool dontClose = false; - - /// Initializes a new instance of the class. - /// If set to , is called to clear the Clipboard. - /// - /// A handle to the window to be associated with the open clipboard. If this parameter is HWND.NULL, the open clipboard is - /// associated with the current task. - /// - public NativeClipboard(bool empty = false, HWND hWndNewOwner = default) - { - if (open) - { - dontClose = true; - return; - } - if (hWndNewOwner == default) - hWndNewOwner = GetDesktopWindow(); - Win32Error.ThrowLastErrorIfFalse(OpenClipboard(hWndNewOwner)); - open = true; - if (empty) - Empty(); - } - /// Occurs when whenever the contents of the Clipboard have changed. public static event EventHandler ClipboardUpdate { @@ -139,6 +105,22 @@ namespace Vanara.Windows.Shell /// public static uint SequenceNumber => GetClipboardSequenceNumber(); + /// Enumerates the data formats currently available on the clipboard. + /// An enumeration of the data formats currently available on the clipboard. + /// + /// + /// The EnumFormats function enumerates formats in the order that they were placed on the clipboard. If you are copying + /// information to the clipboard, add clipboard objects in order from the most descriptive clipboard format to the least descriptive + /// clipboard format. If you are pasting information from the clipboard, retrieve the first clipboard format that you can handle. + /// That will be the most descriptive clipboard format that you can handle. + /// + /// + /// The system provides automatic type conversions for certain clipboard formats. In the case of such a format, this function + /// enumerates the specified format, then enumerates the formats to which it can be converted. + /// + /// + public static IEnumerable EnumAvailableFormats() => DataObject.EnumFormats().Select(f => unchecked((uint)f.cfFormat)); + /// Carries out the clipboard shutdown sequence. It also releases any IDataObject instances that were placed on the clipboard. public static void Flush() => OleFlushClipboard().ThrowIfFailed(); @@ -190,9 +172,9 @@ namespace Vanara.Windows.Shell public static string[] GetFileNameMap() { if (IsFormatAvailable(ShellClipboardFormat.CFSTR_FILENAMEMAPW)) - return DataObject.GetData(RegisterFormat(ShellClipboardFormat.CFSTR_FILENAMEMAPW)) as string[]; + return DataObject.GetData(ShellClipboardFormat.CFSTR_FILENAMEMAPW) as string[]; else if (IsFormatAvailable(ShellClipboardFormat.CFSTR_FILENAMEMAPA)) - return DataObject.GetData(RegisterFormat(ShellClipboardFormat.CFSTR_FILENAMEMAPA)) as string[]; + return DataObject.GetData(ShellClipboardFormat.CFSTR_FILENAMEMAPA) as string[]; return new string[0]; } @@ -208,28 +190,7 @@ namespace Vanara.Windows.Shell /// Retrieves from the clipboard the name of the specified registered format. /// The type of format to be retrieved. /// The format name. - public static string GetFormatName(uint formatId) - { - EnsureKnownIds(); - if (knownIds.TryGetValue(formatId, out var value)) - return value; - - // Ask sysetm for the registered name - StringBuilder sb = new(80); - int ret; - while (0 != (ret = GetClipboardFormatName(formatId, sb, sb.Capacity))) - { - if (ret < sb.Capacity - 1) - { - knownIds.Add(formatId, sb.ToString()); - return sb.ToString(); - } - sb.Capacity *= 2; - } - - // Failing all elsewhere, return value as hex string - return string.Format(CultureInfo.InvariantCulture, "0x{0:X4}", formatId); - } + public static string GetFormatName(uint formatId) => ShellClipboardFormat.GetName(formatId); /// Retrieves the handle to the window that currently has the clipboard open. /// @@ -246,6 +207,11 @@ namespace Vanara.Windows.Shell /// The associated with the data object, if set. Otherwise, . public static ShellItemArray GetShellItemArray() => IsFormatAvailable(ShellClipboardFormat.CFSTR_SHELLIDLIST) ? ShellItemArray.FromDataObject(DataObject) : null; + /// Gets the text from the native Clipboard in the specified format. + /// A clipboard format. For a description of the standard clipboard formats, see Standard Clipboard Formats. + /// The string value or if the format is not available. + public static string GetText(TextDataFormat formatId) => GetData(Txt2Id(formatId)) as string; + /// Determines whether the data object pointer previously placed on the clipboard is still on the clipboard. /// /// The IDataObject interface on the data object containing clipboard data of interest, which the caller previously placed on the clipboard. @@ -256,7 +222,7 @@ namespace Vanara.Windows.Shell /// Determines whether the clipboard contains data in the specified format. /// A standard or registered clipboard format. /// If the clipboard format is available, the return value is ; otherwise . - public static bool IsFormatAvailable(uint id) => IsClipboardFormatAvailable(id); + public static bool IsFormatAvailable(uint id) => DataObject.IsFormatAvailable(id); // EnumAvailableFormats().Contains(id); /// Determines whether the clipboard contains data in the specified format. /// A clipboard format string. @@ -272,26 +238,44 @@ namespace Vanara.Windows.Shell /// existing format. This enables more than one application to copy and paste data using the same registered clipboard format. Note /// that the format name comparison is case-insensitive. /// - public static uint RegisterFormat(string format) + public static uint RegisterFormat(string format) => ShellClipboardFormat.Register(format); + + /// Places data on the clipboard in a specified clipboard format. + /// The clipboard format. This parameter can be a registered format or any of the standard clipboard formats. + /// The binary data in the specified format. + /// data + public static void SetBinaryData(uint formatId, byte[] data) => DataObject.SetData(formatId, data); + + /// Places data on the clipboard in a specified clipboard format. + /// The clipboard format. This parameter can be a registered format or any of the standard clipboard formats. + /// The data in the format dictated by . + public static void SetData(uint formatId, T data) where T : struct => DataObject.SetData(formatId, data); + + /// Places data on the clipboard in a specified clipboard format. + /// The clipboard format. This parameter can be a registered format or any of the standard clipboard formats. + /// The data in the format dictated by . + public static void SetData(uint formatId, IEnumerable values) where T : struct { - if (format is null) throw new ArgumentNullException(nameof(format)); + var pMem = SafeMoveableHGlobalHandle.CreateFromList(values); + Win32Error.ThrowLastErrorIfInvalid(pMem); + DataObject.SetData(formatId, pMem); + } - EnsureKnownIds(); - var id = knownIds.FirstOrDefault(p => p.Value == format).Key; - if (id != 0) - return id; - - id = Win32Error.ThrowLastErrorIf(RegisterClipboardFormat(format), v => v == 0); - knownIds.Add(id, format); - return id; + /// Places data on the clipboard in a specified clipboard format. + /// The clipboard format. This parameter can be a registered format or any of the standard clipboard formats. + /// The list of strings. + /// The packing type for the strings. + /// The character set to use for the strings. + public static void SetData(uint formatId, IEnumerable values, StringListPackMethod packing = StringListPackMethod.Concatenated, CharSet charSet = CharSet.Auto) + { + var pMem = SafeMoveableHGlobalHandle.CreateFromStringList(values, packing, charSet); + Win32Error.ThrowLastErrorIfInvalid(pMem); + DataObject.SetData(formatId, pMem); } /// Puts a list of shell items onto the clipboard. /// The sequence of shell items. The PIDL of each shell item must be absolute. - public static void SetShellItems(IEnumerable shellItems) - { - DataObject = (shellItems is ShellItemArray shia ? shia : new ShellItemArray(shellItems)).ToDataObject(); - } + public static void SetShellItems(IEnumerable shellItems) => DataObject = (shellItems is ShellItemArray shia ? shia : new ShellItemArray(shellItems)).ToDataObject(); /// Puts a list of shell items onto the clipboard. /// The parent folder instance. @@ -300,20 +284,33 @@ namespace Vanara.Windows.Shell { if (parent is null) throw new ArgumentNullException(nameof(parent)); if (relativeShellItems is null) throw new ArgumentNullException(nameof(relativeShellItems)); - var pidls = relativeShellItems.Select(i => i.PIDL.DangerousGetHandle()).ToArray(); - SHCreateDataObject(parent.PIDL, (uint)pidls.Length, pidls, default, typeof(IComDataObject).GUID, out var dataObj).ThrowIfFailed(); + SHCreateDataObject(parent.PIDL, relativeShellItems.Select(i => i.PIDL), default, out var dataObj).ThrowIfFailed(); OleSetClipboard(dataObj).ThrowIfFailed(); - - //DataObject = dataObj = shellItems is ShellItemArray shia ? shia.ToDataObject() : new ShellItemArray(shellItems).ToDataObject(); - //if (!setAllFormats) return; - //var files = shellItems.Where(i => i.IsFileSystem).Select(i => i.FileSystemPath).ToArray(); - //if (files.Length == 0) return; - //dataObj.SetData(CLIPFORMAT.CF_HDROP, files); - //dataObj.SetData(RegisterFormat(ShellClipboardFormat.CFSTR_FILENAMEA), files[0]); - //dataObj.SetData(RegisterFormat(ShellClipboardFormat.CFSTR_FILENAMEW), files[0]); - //dataObj.SetData(RegisterFormat(ShellClipboardFormat.CFSTR_FILEDESCRIPTORA)); } + /// Sets multiple text types to the Clipboard. + /// The Unicode Text value. + /// The HTML text value. If , this format will not be set. + /// The Rich Text Format value. If , this format will not be set. + public static void SetText(string text, string htmlText = null, string rtfText = null) + { + if (text is null && htmlText is null && rtfText is null) return; + SetText(text, TextDataFormat.UnicodeText); + if (htmlText != null) SetText(htmlText, TextDataFormat.Html); + if (rtfText != null) SetText(rtfText, TextDataFormat.Rtf); + } + + /// Sets a specific text type to the Clipboard. + /// The text value. + /// The clipboard text format to set. + public static void SetText(string value, TextDataFormat format) => DataObject.SetData(Txt2Id(format), value); + + /// Sets a URL with optional title to the clipboard. + /// The URL. + /// The title. This value can be . + /// url + public static void SetUrl(string url, string title = null) => DataObject.SetUrl(url, title); + /// Obtains data from a source data object. /// The type of the object being retrieved. /// Specifies the particular clipboard format of interest. @@ -325,169 +322,16 @@ namespace Vanara.Windows.Shell /// if data is available and retrieved; otherwise . public static bool TryGetData(uint formatId, out T obj, int index = -1) => DataObject.TryGetData(formatId, out obj, index); - /// - /// Retrieves data from the clipboard in a specified format. The clipboard must have been opened previously and this pointer cannot - /// be used once goes out of scope. - /// - /// A clipboard format. For a description of the standard clipboard formats, see Standard Clipboard Formats. - /// - /// If the function succeeds, the return value is the handle to a clipboard object in the specified format. - /// If the function fails, the return value is IntPtr.Zero. To get extended error information, call GetLastError. - /// - /// - /// Caution Clipboard data is not trusted. Parse the data carefully before using it in your application. - /// An application can enumerate the available formats in advance by using the EnumClipboardFormats function. - /// - /// The clipboard controls the handle that the GetClipboardData function returns, not the application. The application should - /// copy the data immediately. The application must not free the handle nor leave it locked. The application must not use the handle - /// after the method is called, after is disposed, or after any of the - /// Set... methods are called with the same clipboard format. - /// - /// - /// The system performs implicit data format conversions between certain clipboard formats when an application calls the - /// GetClipboardData function. For example, if the CF_OEMTEXT format is on the clipboard, a window can retrieve data in the - /// CF_TEXT format. The format on the clipboard is converted to the requested format on demand. For more information, see Synthesized - /// Clipboard Formats. - /// - /// - public SafeMoveableHGlobalHandle DanagerousGetData(uint formatId) => new(GetClipboardData(formatId), false); - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() + private static uint Txt2Id(TextDataFormat tf) => tf switch { - if (dontClose) return; - CloseClipboard(); - open = false; - } - - /// - /// Empties the clipboard and frees handles to data in the clipboard. The function then assigns ownership of the clipboard to the - /// window that currently has the clipboard open. - /// - public void Empty() => Win32Error.ThrowLastErrorIfFalse(EmptyClipboard()); - - /// Enumerates the data formats currently available on the clipboard. - /// An enumeration of the data formats currently available on the clipboard. - /// - /// - /// The EnumFormats function enumerates formats in the order that they were placed on the clipboard. If you are copying - /// information to the clipboard, add clipboard objects in order from the most descriptive clipboard format to the least descriptive - /// clipboard format. If you are pasting information from the clipboard, retrieve the first clipboard format that you can handle. - /// That will be the most descriptive clipboard format that you can handle. - /// - /// - /// The system provides automatic type conversions for certain clipboard formats. In the case of such a format, this function - /// enumerates the specified format, then enumerates the formats to which it can be converted. - /// - /// - public IEnumerable EnumAvailableFormats() => EnumClipboardFormats(); - - /// Gets the text from the native Clipboard in the specified format. - /// A clipboard format. For a description of the standard clipboard formats, see Standard Clipboard Formats. - /// The string value or if the format is not available. - public string GetText(TextDataFormat formatId) => formatId switch - { - TextDataFormat.Text => DanagerousGetData(CLIPFORMAT.CF_TEXT).CallLocked(Marshal.PtrToStringAnsi), - TextDataFormat.UnicodeText => DanagerousGetData(CLIPFORMAT.CF_UNICODETEXT).CallLocked(Marshal.PtrToStringUni), - TextDataFormat.Rtf => DanagerousGetData(RegisterFormat(ShellClipboardFormat.CF_RTF)).CallLocked(Marshal.PtrToStringAnsi), - TextDataFormat.Html => Utils.GetHtml(GetClipboardData(RegisterFormat(ShellClipboardFormat.CF_HTML))), - TextDataFormat.CommaSeparatedValue => DanagerousGetData(RegisterFormat(ShellClipboardFormat.CF_CSV)).CallLocked(Marshal.PtrToStringAnsi), - _ => throw new ArgumentOutOfRangeException(nameof(formatId)), + TextDataFormat.Text => CLIPFORMAT.CF_TEXT, + TextDataFormat.UnicodeText => CLIPFORMAT.CF_UNICODETEXT, + TextDataFormat.Rtf => ShellClipboardFormat.Register(ShellClipboardFormat.CF_RTF), + TextDataFormat.Html => ShellClipboardFormat.Register(ShellClipboardFormat.CF_HTML), + TextDataFormat.CommaSeparatedValue => ShellClipboardFormat.Register(ShellClipboardFormat.CF_CSV), + _ => throw new ArgumentOutOfRangeException(nameof(tf)), }; - /// Places data on the clipboard in a specified clipboard format. - /// The clipboard format. This parameter can be a registered format or any of the standard clipboard formats. - /// The binary data in the specified format. - /// data - public void SetBinaryData(uint formatId, byte[] data) - { - SafeMoveableHGlobalHandle pMem = new(data); - Win32Error.ThrowLastErrorIfInvalid(pMem); - Win32Error.ThrowLastErrorIfNull(SetClipboardData(formatId, pMem.TakeOwnership())); - } - - /// Places data on the clipboard in a specified clipboard format. - /// The clipboard format. This parameter can be a registered format or any of the standard clipboard formats. - /// The data in the format dictated by . - public void SetData(uint formatId, T data) - { - var pMem = SafeMoveableHGlobalHandle.CreateFromStructure(data); - Win32Error.ThrowLastErrorIfInvalid(pMem); - Win32Error.ThrowLastErrorIfNull(SetClipboardData(formatId, pMem.TakeOwnership())); - } - - /// Places data on the clipboard in a specified clipboard format. - /// The clipboard format. This parameter can be a registered format or any of the standard clipboard formats. - /// The data in the format dictated by . - public void SetData(uint formatId, IEnumerable values) where T : struct - { - var pMem = SafeMoveableHGlobalHandle.CreateFromList(values); - Win32Error.ThrowLastErrorIfInvalid(pMem); - Win32Error.ThrowLastErrorIfNull(SetClipboardData(formatId, pMem.TakeOwnership())); - } - - /// Places data on the clipboard in a specified clipboard format. - /// The clipboard format. This parameter can be a registered format or any of the standard clipboard formats. - /// The list of strings. - /// The packing type for the strings. - /// The character set to use for the strings. - public void SetData(uint formatId, IEnumerable values, StringListPackMethod packing = StringListPackMethod.Concatenated, CharSet charSet = CharSet.Auto) - { - var pMem = SafeMoveableHGlobalHandle.CreateFromStringList(values, packing, charSet); - Win32Error.ThrowLastErrorIfInvalid(pMem); - Win32Error.ThrowLastErrorIfNull(SetClipboardData(formatId, pMem.TakeOwnership())); - } - - /// Sets multiple text types to the Clipboard. - /// The Unicode Text value. - /// The HTML text value. If , this format will not be set. - /// The Rich Text Format value. If , this format will not be set. - public void SetText(string text, string htmlText = null, string rtfText = null) - { - if (text is null && htmlText is null && rtfText is null) return; - SetText(text, TextDataFormat.UnicodeText); - if (htmlText != null) SetText(htmlText, TextDataFormat.Html); - if (rtfText != null) SetText(rtfText, TextDataFormat.Rtf); - } - - /// Sets a specific text type to the Clipboard. - /// The text value. - /// The clipboard text format to set. - public void SetText(string value, TextDataFormat format) - { - (byte[] bytes, uint fmt) = format switch - { - TextDataFormat.Text => (UnicodeToAnsiBytes(value), (uint)CLIPFORMAT.CF_TEXT), - TextDataFormat.UnicodeText => (Encoding.Unicode.GetBytes(value + '\0'), (uint)CLIPFORMAT.CF_UNICODETEXT), - TextDataFormat.Rtf => (Encoding.ASCII.GetBytes(value + '\0'), RegisterFormat(ShellClipboardFormat.CF_RTF)), - TextDataFormat.Html => (FormatHtmlForClipboard(value), RegisterFormat(ShellClipboardFormat.CF_HTML)), - TextDataFormat.CommaSeparatedValue => (Encoding.ASCII.GetBytes(value + '\0'), RegisterFormat(ShellClipboardFormat.CF_CSV)), - _ => throw new ArgumentOutOfRangeException(nameof(format)), - }; - SetBinaryData(fmt, bytes); - } - - /// Sets a URL with optional title to the clipboard. - /// The URL. - /// The title. This value can be . - /// url - public void SetUrl(string url, string title = null) - { - if (url is null) throw new ArgumentNullException(nameof(url)); - SetText(url, $"{System.Net.WebUtility.HtmlEncode(title ?? url)}", null); - var textUrl = System.Net.WebUtility.UrlEncode(url + (title is null ? "" : ('\n' + title))) + '\0'; - SetBinaryData(RegisterFormat(ShellClipboardFormat.CFSTR_INETURLA), Encoding.ASCII.GetBytes(textUrl)); - SetBinaryData(RegisterFormat(ShellClipboardFormat.CFSTR_INETURLW), Encoding.Unicode.GetBytes(textUrl)); - } - - private static void EnsureKnownIds() - { - if (knownIds is not null) - return; - var type = typeof(CLIPFORMAT); - knownIds = type.GetFields(BindingFlags.Static | BindingFlags.Public).Where(f => f.FieldType == type && f.IsInitOnly).ToDictionary(f => (uint)(CLIPFORMAT)f.GetValue(null), f => f.Name); - } - private class ListenerWindow : SystemEventHandler { protected override bool MessageFilter(HWND hwnd, uint msg, IntPtr wParam, IntPtr lParam, out IntPtr lReturn) diff --git a/Windows.Shell/ShellDataObject.cs b/Windows.Shell/ShellDataObject.cs index f9ff8581..efac6aef 100644 --- a/Windows.Shell/ShellDataObject.cs +++ b/Windows.Shell/ShellDataObject.cs @@ -34,10 +34,7 @@ namespace Vanara.Windows.Shell /// /// The format of the specified data. See for predefined formats. /// The data to store. - public ShellDataObject(string format, object data) : base() - { - SetData(format, data); - } + public ShellDataObject(string format, object data) : base() => SetData(format, data); /// Initializes a new instance of the class. /// A list of ShellItem instances. @@ -80,7 +77,7 @@ namespace Vanara.Windows.Shell /// if the data object is within a drag-and-drop loop; otherwise, . public bool InDragLoop { - get => base.GetDataPresent(ShellClipboardFormat.CFSTR_INDRAGLOOP) ? (int)base.GetData(ShellClipboardFormat.CFSTR_INDRAGLOOP, false) != 0 : false; + get => base.GetDataPresent(ShellClipboardFormat.CFSTR_INDRAGLOOP) && (int)base.GetData(ShellClipboardFormat.CFSTR_INDRAGLOOP, false) != 0; set => base.SetData(ShellClipboardFormat.CFSTR_INDRAGLOOP, false, value ? 1 : 0); }