#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);
}
}
}