#nullable enable using System.Collections.Generic; using System.Linq; 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; /// Static class with methods to interact with the Clipboard. /// /// // This model let's you place multiple formats at once on the clipboard /// IDataObject ido = NativeClipboard.CreateEmptyDataObject(); /// ido.SetData(CLIPFORMAT.CF_UNICODETEXT, txt); /// ido.SetData(Shell32.ShellClipboardFormat.CF_HTML, htmlFragment); /// ido.SetData("MyRectFormat", new RECT(1, 2, 3, 4)); /// NativeClipboard.SetDataObject(ido); public static class NativeClipboard { private const int stdRetryCnt = 5; private const int stdRetryDelay = 100; private static readonly object objectLock = new(); private static ListenerWindow? listener; [ThreadStatic] private static bool oleInit = false; /// Occurs when whenever the contents of the Clipboard have changed. public static event EventHandler ClipboardUpdate { add { lock (objectLock) { listener ??= new ListenerWindow(); InternalClipboardUpdate += value; } } remove { lock (objectLock) { InternalClipboardUpdate -= value; if (InternalClipboardUpdate is null || InternalClipboardUpdate.GetInvocationList().Length == 0) listener = null; } } } private static event EventHandler? InternalClipboardUpdate; /// Gets the instance from the Windows Clipboard. /// A instance. public static IComDataObject CurrentDataObject { get { Init(); int n = stdRetryCnt; HRESULT hr = HRESULT.S_OK; for (int i = 1; i <= n; i++) { hr = OleGetClipboard(out var idata); if (hr.Succeeded) return idata; if (i < n) System.Threading.Thread.Sleep(stdRetryDelay); } throw hr.GetException()!; } } /// 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).ToArray(); } } /// Retrieves the clipboard sequence number for the current window station. /// /// The clipboard sequence number. If you do not have WINSTA_ACCESSCLIPBOARD access to the window station, the function /// returns zero. /// /// /// The system keeps a serial number for the clipboard for each window station. This number is incremented whenever the contents of /// the clipboard change or the clipboard is emptied. You can track this value to determine whether the clipboard contents have /// changed and optimize creating DataObjects. If clipboard rendering is delayed, the sequence number is not incremented until the /// changes are rendered. /// public static uint SequenceNumber => GetClipboardSequenceNumber(); /// Clears the clipboard of any data or formatting. public static void Clear() => SetDataObject(null); /// 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 IComDataObject CreateDataObjectFromShellItems(params ShellItem[] shellItems) => shellItems.Length == 0 ? CreateEmptyDataObject() : new ShellItemArray(shellItems).ToDataObject()!; /// Puts a list of shell items onto the clipboard. /// The parent folder instance. /// The sequence of shell items relative to . public static IComDataObject CreateDataObjectFromShellItems(ShellFolder parent, params ShellItem[] relativeShellItems) { if (parent is null) throw new ArgumentNullException(nameof(parent)); if (relativeShellItems.Length == 0) return CreateEmptyDataObject(); SHCreateDataObject(parent.PIDL, relativeShellItems.Select(i => i.PIDL), default, out var dataObj).ThrowIfFailed(); return dataObj; } /// Creates an empty, writable data object. /// The data object. public static IComDataObject CreateEmptyDataObject() { SHCreateDataObject(ppv: out var writableDataObj).ThrowIfFailed(); return writableDataObj; } /// Carries out the clipboard shutdown sequence. It also releases any IDataObject instances that were placed on the clipboard. public static void Flush() { Init(); TryMultThenThrowIfFailed(OleFlushClipboard); } /// 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 HWND GetClipboardOwner() => 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 uint[] idList) => GetPriorityClipboardFormat(idList, idList.Length); /// 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) => ShellClipboardFormat.GetName(formatId); /// 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 HWND GetOpenClipboardWindow() => User32.GetOpenClipboardWindow(); /// 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. /// /// on success; otherwise, . public static bool IsCurrentDataObject(IComDataObject dataObject) => OleIsCurrentClipboard(dataObject) == HRESULT.S_OK; /// 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); /// Determines whether the clipboard contains data in the specified format. /// A clipboard format string. /// If the clipboard format is available, the return value is ; otherwise . public static bool IsFormatAvailable(string id) => IsClipboardFormatAvailable(RegisterFormat(id)); // EnumAvailableFormats().Contains(id); /// 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 RegisterFormat(string format) => ShellClipboardFormat.Register(format); /// /// Places a specific data object onto the clipboard. This makes the data object accessible to the OleGetClipboard function. /// /// /// The IDataObject interface on the data object from which the data to be placed on the clipboard can be obtained. /// public static void SetDataObject(IComDataObject? dataObj) { Init(); TryMultThenThrowIfFailed(OleSetClipboard, dataObj); if (dataObj is not null) Marshal.ReleaseComObject(dataObj); Flush(); } private static void Init() { if (!oleInit) { oleInit = CoInitialize().Succeeded; } } private static bool TryMultThenThrowIfFailed(Func func, int n = stdRetryCnt) { HRESULT hr = HRESULT.S_OK; for (int i = 1; i <= n; i++) { hr = func(); if (hr.Succeeded) return hr == HRESULT.S_OK; if (i < n) System.Threading.Thread.Sleep(stdRetryDelay); } throw hr.GetException()!; } private static bool TryMultThenThrowIfFailed(Func func, IComDataObject? o, int n = stdRetryCnt) { HRESULT hr = HRESULT.S_OK; for (int i = 1; i <= n; i++) { hr = func(o); if (hr.Succeeded) return hr == HRESULT.S_OK; if (i < n) System.Threading.Thread.Sleep(stdRetryDelay); } throw hr.GetException()!; } private class ListenerWindow : SystemEventHandler { protected override bool MessageFilter(HWND hwnd, uint msg, IntPtr wParam, IntPtr lParam, out IntPtr lReturn) { lReturn = default; switch (msg) { case (uint)WindowMessage.WM_DESTROY: RemoveClipboardFormatListener(MessageWindowHandle); break; case (uint)ClipboardNotificationMessage.WM_CLIPBOARDUPDATE: InternalClipboardUpdate?.Invoke(this, EventArgs.Empty); break; } return false; } protected override void OnMessageWindowHandleCreated() { base.OnMessageWindowHandleCreated(); AddClipboardFormatListener(MessageWindowHandle); } } }