using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Windows.Forms; using Vanara.Extensions; using Vanara.InteropServices; using Vanara.PInvoke; using static Vanara.PInvoke.Ole32; using static Vanara.PInvoke.Shell32; using static Vanara.PInvoke.User32; using IComDataObject = System.Runtime.InteropServices.ComTypes.IDataObject; namespace Vanara.Windows.Shell { /// /// Initializes and closes a session using the Clipboard calling and then on /// 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 { private static readonly ListenerWindow listener = new ListenerWindow(); private static int HdrLen = 0; [ThreadStatic] private static bool open = false; private 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 (!OpenClipboard(hWndNewOwner)) Win32Error.ThrowLastError(); open = true; if (empty) Empty(); } /// Occurs when whenever the contents of the Clipboard have changed. public static event EventHandler ClipboardUpdate; /// Retrieves the currently supported clipboard formats. /// A sequence of the currently supported formats. public static IEnumerable CurrentlySupportedFormats { get { GetUpdatedClipboardFormats(null, 0, out var cnt); var fmts = new uint[cnt]; Win32Error.ThrowLastErrorIfFalse(GetUpdatedClipboardFormats(fmts, (uint)fmts.Length, out cnt)); return fmts.Take((int)cnt).Select(u => DataFormats.GetFormat(unchecked((int)u))).ToArray(); } } /// Retrieves the window handle of the current owner of the clipboard. /// /// If the function succeeds, the return value is the handle to the window that owns the clipboard. /// If the clipboard is not owned, the return value is IntPtr.Zero. /// /// /// The clipboard can still contain data even if the clipboard is not currently owned. /// In general, the clipboard owner is the window that last placed data in clipboard. /// public static IntPtr GetClipboardOwner() => (IntPtr)User32.GetClipboardOwner(); /// Retrieves the first available clipboard format in the specified list. /// The clipboard formats, in priority order. /// /// If the function succeeds, the return value is the first clipboard format in the list for which data is available. If the /// clipboard is empty, the return value is 0. If the clipboard contains data, but not in any of the specified formats, the return /// value is –1. /// public static int GetFirstFormatAvailable(params int[] idList) => GetPriorityClipboardFormat(Array.ConvertAll(idList, i => (uint)i), idList.Length); /// Retrieves the handle to the window that currently has the clipboard open. /// /// If the function succeeds, the return value is the handle to the window that has the clipboard open. If no window has the /// clipboard open, the return value is IntPtr.Zero. /// /// /// If an application or DLL specifies a NULL window handle when calling the OpenClipboard function, the clipboard is opened /// but is not associated with a window. In such a case, GetOpenClipboardWindow returns IntPtr.Zero. /// public static IntPtr GetOpenClipboardWindow() => (IntPtr)User32.GetOpenClipboardWindow(); /// Determines whether the clipboard contains data in the specified format. /// The name of a standard or registered clipboard format. /// If the clipboard format is available, the return value is ; otherwise . public static bool IsFormatAvailable(string format) => IsClipboardFormatAvailable((uint)DataFormats.GetFormat(format).Id); /// 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(int id) => IsClipboardFormatAvailable((uint)id); /// /// 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 IntPtr DanagerousGetData(int formatId) => GetClipboardData((uint)formatId); /// /// 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 IntPtr DanagerousGetData(string format) => GetClipboardData((uint)DataFormats.GetFormat(format).Id); /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. public void Dispose() { 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().Select(i => DataFormats.GetFormat((int)i)); /// Gets the text from the native Clipboard in the specified format. /// The format. /// The string value or if the format is not available. public string GetText(TextDataFormat format) { return format switch { TextDataFormat.Text => StringHelper.GetString(GetClipboardData(CLIPFORMAT.CF_TEXT), CharSet.Ansi), TextDataFormat.UnicodeText => StringHelper.GetString(GetClipboardData(CLIPFORMAT.CF_UNICODETEXT), CharSet.Unicode), TextDataFormat.Rtf => StringHelper.GetString(DanagerousGetData(DataFormats.Rtf), CharSet.Ansi), TextDataFormat.Html => GetHtml(DanagerousGetData(DataFormats.Html)), TextDataFormat.CommaSeparatedValue => StringHelper.GetString(DanagerousGetData(DataFormats.CommaSeparatedValue), CharSet.Ansi), _ => null, }; } /// 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(string format, byte[] data) => SetBinaryData(DataFormats.GetFormat(format).Id, 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 binary data in the specified format. /// data public void SetBinaryData(int formatId, byte[] data) { using var pMem = new SafeMoveableHGlobalHandle(data); Win32Error.ThrowLastErrorIfInvalid(pMem); Win32Error.ThrowLastErrorIfNull(SetClipboardData((uint)formatId, pMem.DangerousGetHandle())); } /// 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(int formatId, T data) { using var pMem = SafeMoveableHGlobalHandle.CreateFromStructure(data); Win32Error.ThrowLastErrorIfInvalid(pMem); Win32Error.ThrowLastErrorIfNull(SetClipboardData((uint)formatId, pMem.DangerousGetHandle())); } /// 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(int formatId, IEnumerable values) where T : struct { using var pMem = SafeMoveableHGlobalHandle.CreateFromList(values); Win32Error.ThrowLastErrorIfInvalid(pMem); Win32Error.ThrowLastErrorIfNull(SetClipboardData((uint)formatId, pMem.DangerousGetHandle())); } /// 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(int formatId, IEnumerable values, StringListPackMethod packing = StringListPackMethod.Concatenated, CharSet charSet = CharSet.Auto) { using var pMem = SafeMoveableHGlobalHandle.CreateFromStringList(values, packing, charSet); Win32Error.ThrowLastErrorIfInvalid(pMem); Win32Error.ThrowLastErrorIfNull(SetClipboardData((uint)formatId, pMem.DangerousGetHandle())); } /// 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); SetBinaryData(DataFormats.Locale, BitConverter.GetBytes(CultureInfo.CurrentCulture.LCID)); 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, int fmt) = format switch { TextDataFormat.Text => (Encoding.ASCII.GetBytes(value + '\0'), (int)CLIPFORMAT.CF_TEXT), TextDataFormat.UnicodeText => (Encoding.Unicode.GetBytes(value + '\0'), (int)CLIPFORMAT.CF_UNICODETEXT), TextDataFormat.Rtf => (Encoding.ASCII.GetBytes(value + '\0'), DataFormats.GetFormat(DataFormats.Rtf).Id), TextDataFormat.Html => (MakeClipHtml(value), DataFormats.GetFormat(DataFormats.Html).Id), TextDataFormat.CommaSeparatedValue => (Encoding.ASCII.GetBytes(value + '\0'), DataFormats.GetFormat(DataFormats.CommaSeparatedValue).Id), _ => default, }; 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, $"{title ?? url}", null); var textUrl = (title is null ? "" : (title + '\n')) + url + '\0'; SetBinaryData(ShellClipboardFormat.CFSTR_INETURLA, Encoding.ASCII.GetBytes(textUrl)); SetBinaryData(ShellClipboardFormat.CFSTR_INETURLW, Encoding.Unicode.GetBytes(textUrl)); } internal static StringCollection ToSC(IEnumerable e) { var sc = new StringCollection(); if (e != null) sc.AddRange(e.ToArray()); return sc; } private static T GetComData(DataObject dobj, string fmt, Func convert, T defValue = default) { T ret = defValue; if (dobj is IComDataObject cdo) { var fc = new FORMATETC { cfFormat = (short)DataFormats.GetFormat(fmt).Id, dwAspect = DVASPECT.DVASPECT_CONTENT, lindex = -1, tymed = TYMED.TYMED_HGLOBAL }; try { cdo.GetData(ref fc, out var medium); if (medium.unionmember != default) ret = convert(medium.unionmember); ReleaseStgMedium(medium); } catch { } } return ret; } internal static string GetHtml(IntPtr ptr) { const string HdrRegEx = @"Version:\d\.\d\s+StartHTML:(\d+)\s+EndHTML:(\d+)\s+StartFragment:(\d+)\s+EndFragment:(\d+)\s+(?:StartSelection:(\d+)\s+EndSelection:(\d+)\s+)?"; if (ptr == IntPtr.Zero) return null; // Find length of data by looking for a '\0' byte. var byteCount = 0; unsafe { for (byte* bp = (byte*)ptr.ToPointer(); byteCount < 4 * 1024 * 1024 && *bp != 0; byteCount++, bp++) ; } var bytes = ptr.ToArray(byteCount); // Get UTF8 encoded string var utf8String = Encoding.UTF8.GetString(bytes); // Find markers var match = Regex.Match(utf8String, HdrRegEx); if (!match.Success) throw new InvalidOperationException("HTML format header cannot be processed."); var startHtml = int.Parse(match.Groups[1].Value.TrimStart('0')); var endHtml = int.Parse(match.Groups[2].Value.TrimStart('0')); var startFrag = int.Parse(match.Groups[3].Value.TrimStart('0')); var endFrag = int.Parse(match.Groups[4].Value.TrimStart('0')); var startSel = int.Parse(match.Groups[5].Value.TrimStart('0')); var endSel = int.Parse(match.Groups[6].Value.TrimStart('0')); return Encoding.UTF8.GetString(bytes, startFrag, endFrag - startFrag); } private static byte[] MakeClipHtml(string value) { const string Header = "Version:0.9\r\nStartHTML:{0:0000000000}\r\nEndHTML:{1:0000000000}\r\nStartFragment:{2:0000000000}\r\nEndFragment:{3:0000000000}\r\nStartSelection:{4:0000000000}\r\nEndSelection:{5:0000000000}\r\n"; const string htmlDocType = ""; const string htmlBodyStart = "Snippet"; const string htmlBodyEnd = ""; const string fragmentStart = ""; const string fragmentEnd = ""; var sb = new StringBuilder(); if (value.IndexOf("", StringComparison.OrdinalIgnoreCase) < 0) sb.Append(htmlBodyStart); var fragStartIdx = value.IndexOf(fragmentStart, StringComparison.OrdinalIgnoreCase); if (fragStartIdx < 0) sb.Append(fragmentStart); else { sb.Append(value.Substring(0, fragStartIdx + fragmentStart.Length)); value = value.Remove(0, fragStartIdx + fragmentStart.Length); } fragStartIdx = Encoding.UTF8.GetByteCount(sb.ToString()); var fragEndIdx = value.IndexOf(fragmentEnd, StringComparison.OrdinalIgnoreCase); if (fragEndIdx < 0) { sb.Append(value); fragEndIdx = Encoding.UTF8.GetByteCount(sb.ToString()); sb.Append(fragmentEnd); } else { var preFrag = value.Substring(0, fragEndIdx); value = value.Remove(0, fragEndIdx); sb.Append(preFrag); fragEndIdx = Encoding.UTF8.GetByteCount(sb.ToString()); sb.Append(value); } if (value.IndexOf("", StringComparison.OrdinalIgnoreCase) < 0) sb.Append(htmlBodyEnd); if (HdrLen == 0) HdrLen = string.Format(Header, 0, 0, 0, 0, 0, 0).Length; var startHtml = HdrLen; var endHtml = HdrLen + Encoding.UTF8.GetByteCount(sb.ToString()); var startFrag = HdrLen + fragStartIdx; var endFrag = HdrLen + fragEndIdx; var startSel = startFrag; var endSel = endFrag; sb.Insert(0, string.Format(Header, startHtml, endHtml, startFrag, endFrag, startSel, endSel)); sb.Append('\0'); return Encoding.UTF8.GetBytes(sb.ToString()); } private static void RunAsSTAThread(ThreadStart threadStart) { var thread = new Thread(threadStart); thread.SetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); } internal sealed class MoveableHGlobalMethods : MemoryMethodsBase { /// Gets a static instance of these methods. public static readonly IMemoryMethods Instance = new MoveableHGlobalMethods(); /// Gets a handle to a memory allocation of the specified size. /// The size, in bytes, of memory to allocate. /// A memory handle. public override IntPtr AllocMem(int size) => Win32Error.ThrowLastErrorIfNull((IntPtr)Kernel32.GlobalAlloc(Kernel32.GMEM.GMEM_MOVEABLE | Kernel32.GMEM.GMEM_ZEROINIT, size)); /// Frees the memory associated with a handle. /// A memory handle. public override void FreeMem(IntPtr hMem) => Kernel32.GlobalFree(hMem); /// Locks the memory of a specified handle and gets a pointer to it. /// A memory handle. /// A pointer to the locked memory. public override IntPtr LockMem(IntPtr hMem) => Kernel32.GlobalLock(hMem); /// Gets the reallocation method. /// A memory handle. /// The size, in bytes, of memory to allocate. /// A memory handle. public override IntPtr ReAllocMem(IntPtr hMem, int size) => Win32Error.ThrowLastErrorIfNull((IntPtr)Kernel32.GlobalReAlloc(hMem, size, Kernel32.GMEM.GMEM_MOVEABLE | Kernel32.GMEM.GMEM_ZEROINIT)); /// Unlocks the memory of a specified handle. /// A memory handle. public override void UnlockMem(IntPtr hMem) => Kernel32.GlobalUnlock(hMem); } private class ListenerWindow : NativeWindow, IDisposable { public ListenerWindow() { var cp = new CreateParams { Style = 0, ExStyle = 0, ClassStyle = 0, Parent = IntPtr.Zero, Caption = GetType().Name }; CreateHandle(cp); AddClipboardFormatListener(Handle); } void IDisposable.Dispose() => base.DestroyHandle(); protected override void WndProc(ref Message m) { switch (m.Msg) { case (int)WindowMessage.WM_DESTROY: RemoveClipboardFormatListener(Handle); break; case (int)ClipboardNotificationMessage.WM_CLIPBOARDUPDATE: ClipboardUpdate?.Invoke(this, EventArgs.Empty); break; } base.WndProc(ref m); } } private class SafeMoveableHGlobalHandle : SafeHandle { private static readonly IMemoryMethods mm = MoveableHGlobalMethods.Instance; private SizeT sz; /// Initializes a new instance of the class. /// The handle. /// The size of memory allocated to the handle, in bytes. /// if set to true if this class is responsible for freeing the memory on disposal. public SafeMoveableHGlobalHandle(IntPtr handle, SizeT size, bool ownsHandle = true) : base(IntPtr.Zero, ownsHandle) { SetHandle(handle); sz = size; } /// Initializes a new instance of the class. /// The size of memory to allocate, in bytes. /// size - The value of this argument must be non-negative public SafeMoveableHGlobalHandle(SizeT size) : base(IntPtr.Zero, true) { if (size == 0) return; RuntimeHelpers.PrepareConstrainedRegions(); SetHandle(mm.AllocMem(sz = size)); } /// /// Allocates from unmanaged memory to represent an array of pointers and marshals the unmanaged pointers (IntPtr) to the native /// array equivalent. /// /// Array of unmanaged pointers /// SafeMoveableHGlobalHandle object to an native (unmanaged) array of pointers public SafeMoveableHGlobalHandle(byte[] bytes) : this(bytes?.Length ?? 0) { if (sz == 0) return; CallLocked(p => Marshal.Copy(bytes, 0, p, sz)); } /// Represents a NULL memory pointer. public static SafeMoveableHGlobalHandle Null { get; } = new SafeMoveableHGlobalHandle(IntPtr.Zero, 0, false); public override bool IsInvalid => handle == IntPtr.Zero; /// Gets or sets the size in bytes of the allocated memory block. /// The size in bytes of the allocated memory block. public SizeT Size { get => sz; set { if (value == 0) { ReleaseHandle(); } else { RuntimeHelpers.PrepareConstrainedRegions(); handle = IsInvalid ? mm.AllocMem(value) : mm.ReAllocMem(handle, value); sz = value; } } } /// /// Allocates from unmanaged memory to represent a structure with a variable length array at the end and marshal these structure /// elements. It is the callers responsibility to marshal what precedes the trailing array into the unmanaged memory. ONLY /// structures with attribute StructLayout of LayoutKind.Sequential are supported. /// /// Type of the trailing array of structures /// Collection of structure objects /// Number of bytes preceding the trailing array of structures /// object to an native (unmanaged) structure with a trail array of structures public static SafeMoveableHGlobalHandle CreateFromList(IEnumerable values, int prefixBytes = 0) => new SafeMoveableHGlobalHandle(InteropExtensions.MarshalToPtr(values, mm.AllocMem, out int s, prefixBytes, mm.LockMem, mm.UnlockMem), s); /// Allocates from unmanaged memory sufficient memory to hold an array of strings. /// The list of strings. /// The packing type for the strings. /// The character set to use for the strings. /// Number of bytes preceding the trailing strings. /// /// object to an native (unmanaged) array of strings stored using the model and the character set defined by . /// public static SafeMoveableHGlobalHandle CreateFromStringList(IEnumerable values, StringListPackMethod packing = StringListPackMethod.Concatenated, CharSet charSet = CharSet.Auto, int prefixBytes = 0) => new SafeMoveableHGlobalHandle(InteropExtensions.MarshalToPtr(values, packing, mm.AllocMem, out int s, charSet, prefixBytes, mm.LockMem, mm.UnlockMem), s); /// Allocates from unmanaged memory sufficient memory to hold an object of type T. /// Native type /// The value. /// object to an native (unmanaged) memory block the size of T. public static SafeMoveableHGlobalHandle CreateFromStructure(in T value = default) => new SafeMoveableHGlobalHandle(InteropExtensions.MarshalToPtr(value, mm.AllocMem, out int s, 0, mm.LockMem, mm.UnlockMem), s); /// Converts an to a where it owns the reference. /// The . /// The result of the conversion. public static implicit operator SafeMoveableHGlobalHandle(IntPtr ptr) => new SafeMoveableHGlobalHandle(ptr, 0, true); protected void CallLocked(Action action) { try { action.Invoke(mm.LockMem(handle)); } finally { mm.UnlockMem(handle); } } protected override bool ReleaseHandle() { mm.FreeMem(handle); sz = 0; handle = IntPtr.Zero; return true; } } } }