Added Vanara.Windows.Shell.NativeClipboard to expose native clipboard functions that are not routed through COM and DataObject.

pull/161/head
dahall 2020-07-26 13:54:57 -06:00
parent 3d0422bc96
commit 4ef43cf843
3 changed files with 660 additions and 98 deletions

View File

@ -0,0 +1,81 @@
using NUnit.Framework;
using System;
using System.Linq;
using System.Windows.Forms;
using Vanara.PInvoke;
using static Vanara.PInvoke.Shell32;
using Clipboard = Vanara.Windows.Shell.NativeClipboard;
using WFClipboard = System.Windows.Forms.Clipboard;
namespace Vanara.Windows.Shell.Tests
{
[TestFixture, SingleThreaded]
public class ClipboardTests
{
private const string html = "<pre style=\"font-family:Consolas;font-size:13px;color:#dadada;\"><span style=\"color:#dcdcaa;\">“Weve been here”</span></pre>";
[Test]
public void DumpWFClipboardTest()
{
TestContext.WriteLine($"ContainsAudio: {WFClipboard.ContainsAudio()}");
TestContext.WriteLine($"ContainsData: {WFClipboard.ContainsData(DataFormats.StringFormat)}");
TestContext.WriteLine($"ContainsFileDropList: {WFClipboard.ContainsFileDropList()}");
TestContext.WriteLine($"ContainsImage: {WFClipboard.ContainsImage()}");
TestContext.WriteLine($"ContainsText: {WFClipboard.ContainsText()}");
TestContext.WriteLine($"GetAudioStream: {WFClipboard.GetAudioStream()}");
TestContext.WriteLine($"GetData: {WFClipboard.GetData(DataFormats.StringFormat)}");
TestContext.WriteLine($"GetDataObject: {WFClipboard.GetDataObject()}");
TestContext.WriteLine($"GetFileDropList: {string.Join("\n", WFClipboard.GetFileDropList().Cast<string>())}");
TestContext.WriteLine($"GetImage: {WFClipboard.GetImage()}");
TestContext.WriteLine($"GetText: {WFClipboard.GetText()}");
}
[Test]
public void EnumFormatsTest()
{
using var cb = new Clipboard();
var fmts = cb.EnumAvailableFormats();
Assert.That(fmts, Is.Not.Empty);
TestContext.Write(string.Join(", ", fmts.Select(f => f.Name)));
var fmt = fmts.First();
Assert.IsTrue(Clipboard.IsFormatAvailable(fmt.Id));
Assert.IsTrue(Clipboard.IsFormatAvailable(fmt.Name));
}
[Test]
public void GetNativeTextTest()
{
using var cb = new Clipboard();
foreach (TextDataFormat e in Enum.GetValues(typeof(TextDataFormat)))
TestContext.WriteLine($"{e}: {cb.GetText(e)}");
}
[Test]
public void GetPriorityFormatTest()
{
var fmts = Clipboard.CurrentlySupportedFormats.Select(f => f.Id).ToArray();
Assert.That(Clipboard.GetFirstFormatAvailable(fmts), Is.GreaterThan(0));
}
[Test]
public void SetNativeTextHtmlTest()
{
using (var cb = new Clipboard())
cb.SetText(html, TextDataFormat.Html);
using (var cb = new Clipboard())
{
var outVal = cb.GetText(TextDataFormat.Html);
Assert.That(outVal, Is.EqualTo(html));
}
}
[Test]
public void SetNativeTextMultTest()
{
const string txt = @"“Weve been here”";
using var cb = new Clipboard();
cb.SetText(txt, html);
}
}
}

View File

@ -20,7 +20,7 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>AnyCPU</PlatformTarget>
<PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
@ -49,6 +49,7 @@
<ItemGroup>
<Compile Include="RecycleBinTests.cs" />
<Compile Include="BindContextTests.cs" />
<Compile Include="ClipboardTests.cs" />
<Compile Include="WallpaperTests.cs" />
<Compile Include="ShellLinkTests.cs" />
<Compile Include="ShellSearchTests.cs" />
@ -88,6 +89,10 @@
<Project>{30fd6779-6549-449e-880a-695815eb89b0}</Project>
<Name>Vanara.PInvoke.ShlwApi</Name>
</ProjectReference>
<ProjectReference Include="..\..\PInvoke\User32\Vanara.PInvoke.User32.csproj">
<Project>{a6771907-addc-49fc-8444-a97aa65e77e2}</Project>
<Name>Vanara.PInvoke.User32</Name>
</ProjectReference>
<ProjectReference Include="..\..\Windows.Shell\Vanara.Windows.Shell.csproj">
<Project>{43685be2-a65e-4b01-be16-479526940f23}</Project>
<Name>Vanara.Windows.Shell</Name>

View File

@ -1,121 +1,597 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Drawing;
using System.IO;
using System.Reflection;
using System.Security.Permissions;
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.User32;
using static Vanara.PInvoke.Shell32;
using System.Linq;
using static Vanara.PInvoke.User32;
using IComDataObject = System.Runtime.InteropServices.ComTypes.IDataObject;
using System.Runtime.InteropServices.ComTypes;
namespace Vanara.Windows.Shell
{
internal static class ClipboardEx
/// <summary>
/// Initializes and closes a session using the Clipboard calling <see cref="OpenClipboard"/> and then <see cref="CloseClipboard"/> 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.
/// </summary>
/// <seealso cref="System.IDisposable"/>
public class NativeClipboard : IDisposable
{
public static void Clear() => Clipboard.Clear();
public static bool ContainsAudio() => Clipboard.ContainsAudio();
public static bool ContainsData(string format) => Clipboard.ContainsData(format);
public static bool ContainsFileDropList() => Clipboard.ContainsFileDropList();
public static bool ContainsImage() => Clipboard.ContainsImage();
public static bool ContainsText() => Clipboard.ContainsText();
public static bool ContainsText(TextDataFormat format) => Clipboard.ContainsText(format);
public static Stream GetAudioStream() => Clipboard.GetAudioStream();
public static object GetData(string format) => Clipboard.GetData(format);
public static System.Windows.Forms.IDataObject GetDataObject() => Clipboard.GetDataObject();
public static IList<string> GetFileDropList() => new List<string>(Clipboard.GetFileDropList().Cast<string>());
public static Image GetImage() => Clipboard.GetImage();
public static string GetText() => Clipboard.GetText();
public static string GetText(TextDataFormat format) => Clipboard.GetText(format);
public static void SetAudio(byte[] audioBytes) => Clipboard.SetAudio(audioBytes);
public static void SetAudio(Stream audioStream) => Clipboard.SetAudio(audioStream);
public static void SetData(string format, object data) => Clipboard.SetData(format, data);
public static void SetDataObject(object data) => Clipboard.SetDataObject(data);
public static void SetDataObject(object data, bool copy) => Clipboard.SetDataObject(data, copy);
[UIPermission(SecurityAction.Demand, Clipboard = UIPermissionClipboard.OwnClipboard)]
public static void SetDataObject(object data, bool copy, int retryTimes, int retryDelay) => Clipboard.SetDataObject(data, copy, retryTimes, retryDelay);
public static void SetFileDropList(IEnumerable<string> filePaths) => Clipboard.SetFileDropList(ToSC(filePaths));
public static void SetImage(Image image) => Clipboard.SetImage(image);
public static void SetText(string text) => Clipboard.SetText(text);
public static void SetText(string text, TextDataFormat format) => Clipboard.SetText(text, format);
private static readonly ListenerWindow listener = new ListenerWindow();
private static int HdrLen = 0;
internal static StringCollection ToSC(IEnumerable<string> e) { var sc = new StringCollection(); if (e != null) sc.AddRange(e.ToArray()); return sc; }
[ThreadStatic]
private static bool open = false;
internal static string Id(this ShellDataFormat fmt)
private bool dontClose = false;
/// <summary>Initializes a new instance of the <see cref="NativeClipboard"/> class.</summary>
/// <param name="empty">If set to <see langword="true"/>, <see cref="EmptyClipboard"/> is called to clear the Clipboard.</param>
/// <param name="hWndNewOwner">
/// A handle to the window to be associated with the open clipboard. If this parameter is <c>HWND.NULL</c>, the open clipboard is
/// associated with the current task.
/// </param>
public NativeClipboard(bool empty = false, HWND hWndNewOwner = default)
{
var ansi = System.Text.Encoding.Default.IsSingleByte;
string cfval = null;
switch (fmt)
if (open)
{
case ShellDataFormat.FileDescriptor:
cfval = ansi ? ShellClipboardFormat.CFSTR_FILEDESCRIPTORA : ShellClipboardFormat.CFSTR_FILEDESCRIPTORW;
break;
case ShellDataFormat.FileName:
cfval = ansi ? ShellClipboardFormat.CFSTR_FILENAMEA : ShellClipboardFormat.CFSTR_FILENAMEW;
break;
case ShellDataFormat.FileNameMap:
cfval = ansi ? ShellClipboardFormat.CFSTR_FILENAMEMAPA : ShellClipboardFormat.CFSTR_FILENAMEMAPW;
break;
case ShellDataFormat.InetUrl:
cfval = ansi ? ShellClipboardFormat.CFSTR_INETURLA: ShellClipboardFormat.CFSTR_INETURLW;
break;
case ShellDataFormat.AutoPlayLists:
cfval = ShellClipboardFormat.CFSTR_AUTOPLAY_SHELLIDLISTS;
break;
case ShellDataFormat.FileAttributes:
cfval = ShellClipboardFormat.CFSTR_FILE_ATTRIBUTES_ARRAY;
break;
case ShellDataFormat.InvokeCommandDropParam:
cfval = ShellClipboardFormat.CFSTR_INVOKECOMMAND_DROPPARAM;
break;
default:
cfval = GetSCFField("CFSTR_" + fmt.ToString().ToUpper());
break;
dontClose = true;
return;
}
return cfval ?? throw new ArgumentOutOfRangeException(nameof(fmt));
if (!OpenClipboard(hWndNewOwner))
Win32Error.ThrowLastError();
open = true;
if (empty)
Empty();
}
string GetSCFField(string fName)
/// <summary>Occurs when whenever the contents of the Clipboard have changed.</summary>
public static event EventHandler ClipboardUpdate;
/// <summary>Retrieves the currently supported clipboard formats.</summary>
/// <value>A sequence of the currently supported formats.</value>
public static IEnumerable<DataFormats.Format> CurrentlySupportedFormats
{
get
{
var fi = typeof(ShellClipboardFormat).GetField(fName, BindingFlags.Public | BindingFlags.Static);
if (fi != null && fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(string))
return (string)fi.GetRawConstantValue();
return null;
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();
}
}
/// <summary>Retrieves the window handle of the current owner of the clipboard.</summary>
/// <returns>
/// <para>If the function succeeds, the return value is the handle to the window that owns the clipboard.</para>
/// <para>If the clipboard is not owned, the return value is <c>IntPtr.Zero</c>.</para>
/// </returns>
/// <remarks>
/// <para>The clipboard can still contain data even if the clipboard is not currently owned.</para>
/// <para>In general, the clipboard owner is the window that last placed data in clipboard.</para>
/// </remarks>
public static IntPtr GetClipboardOwner() => (IntPtr)User32.GetClipboardOwner();
/// <summary>Retrieves the first available clipboard format in the specified list.</summary>
/// <param name="idList">The clipboard formats, in priority order.</param>
/// <returns>
/// 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.
/// </returns>
public static int GetFirstFormatAvailable(params int[] idList) => GetPriorityClipboardFormat(Array.ConvertAll(idList, i => (uint)i), idList.Length);
/// <summary>Retrieves the handle to the window that currently has the clipboard open.</summary>
/// <returns>
/// 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 <c>IntPtr.Zero</c>.
/// </returns>
/// <remarks>
/// If an application or DLL specifies a <c>NULL</c> window handle when calling the OpenClipboard function, the clipboard is opened
/// but is not associated with a window. In such a case, <c>GetOpenClipboardWindow</c> returns <c>IntPtr.Zero</c>.
/// </remarks>
public static IntPtr GetOpenClipboardWindow() => (IntPtr)User32.GetOpenClipboardWindow();
/// <summary>Determines whether the clipboard contains data in the specified format.</summary>
/// <param name="format">The name of a standard or registered clipboard format.</param>
/// <returns>If the clipboard format is available, the return value is <see langword="true"/>; otherwise <see langword="false"/>.</returns>
public static bool IsFormatAvailable(string format) => IsClipboardFormatAvailable((uint)DataFormats.GetFormat(format).Id);
/// <summary>Determines whether the clipboard contains data in the specified format.</summary>
/// <param name="id">A standard or registered clipboard format.</param>
/// <returns>If the clipboard format is available, the return value is <see langword="true"/>; otherwise <see langword="false"/>.</returns>
public static bool IsFormatAvailable(int id) => IsClipboardFormatAvailable((uint)id);
/// <summary>
/// Retrieves data from the clipboard in a specified format. The clipboard must have been opened previously and this pointer cannot
/// be used once <see cref="NativeClipboard"/> goes out of scope.
/// </summary>
/// <param name="formatId">A clipboard format. For a description of the standard clipboard formats, see Standard Clipboard Formats.</param>
/// <returns>
/// <para>If the function succeeds, the return value is the handle to a clipboard object in the specified format.</para>
/// <para>If the function fails, the return value is <c>IntPtr.Zero</c>. To get extended error information, call GetLastError.</para>
/// </returns>
/// <remarks>
/// <para><c>Caution</c> Clipboard data is not trusted. Parse the data carefully before using it in your application.</para>
/// <para>An application can enumerate the available formats in advance by using the EnumClipboardFormats function.</para>
/// <para>
/// The clipboard controls the handle that the <c>GetClipboardData</c> 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 <see cref="Empty"/> method is called, after <see cref="NativeClipboard"/> is disposed, or after any of the
/// <c>Set...</c> methods are called with the same clipboard format.
/// </para>
/// <para>
/// The system performs implicit data format conversions between certain clipboard formats when an application calls the
/// <c>GetClipboardData</c> 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.
/// </para>
/// </remarks>
public IntPtr DanagerousGetData(int formatId) => GetClipboardData((uint)formatId);
/// <summary>
/// Retrieves data from the clipboard in a specified format. The clipboard must have been opened previously and this pointer cannot
/// be used once <see cref="NativeClipboard"/> goes out of scope.
/// </summary>
/// <param name="format">A clipboard format. For a description of the standard clipboard formats, see Standard Clipboard Formats.</param>
/// <returns>
/// <para>If the function succeeds, the return value is the handle to a clipboard object in the specified format.</para>
/// <para>If the function fails, the return value is <c>IntPtr.Zero</c>. To get extended error information, call GetLastError.</para>
/// </returns>
/// <remarks>
/// <para><c>Caution</c> Clipboard data is not trusted. Parse the data carefully before using it in your application.</para>
/// <para>An application can enumerate the available formats in advance by using the EnumClipboardFormats function.</para>
/// <para>
/// The clipboard controls the handle that the <c>GetClipboardData</c> 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 <see cref="Empty"/> method is called, after <see cref="NativeClipboard"/> is disposed, or after any of the
/// <c>Set...</c> methods are called with the same clipboard format.
/// </para>
/// <para>
/// The system performs implicit data format conversions between certain clipboard formats when an application calls the
/// <c>GetClipboardData</c> 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.
/// </para>
/// </remarks>
public IntPtr DanagerousGetData(string format) => GetClipboardData((uint)DataFormats.GetFormat(format).Id);
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
if (dontClose) return;
CloseClipboard();
open = false;
}
/// <summary>
/// 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.
/// </summary>
public void Empty() => Win32Error.ThrowLastErrorIfFalse(EmptyClipboard());
/// <summary>Enumerates the data formats currently available on the clipboard.</summary>
/// <returns>An enumeration of the data formats currently available on the clipboard.</returns>
/// <remarks>
/// <para>
/// The <c>EnumFormats</c> 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
public IEnumerable<DataFormats.Format> EnumAvailableFormats() => EnumClipboardFormats().Select(i => DataFormats.GetFormat((int)i));
/// <summary>Gets the text from the native Clipboard in the specified format.</summary>
/// <param name="format">The format.</param>
/// <returns>The string value or <see langword="null"/> if the format is not available.</returns>
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,
};
}
/// <summary>Places data on the clipboard in a specified clipboard format.</summary>
/// <param name="format">The clipboard format. This parameter can be a registered format or any of the standard clipboard formats.</param>
/// <param name="data">The binary data in the specified format.</param>
/// <exception cref="System.ArgumentNullException">data</exception>
public void SetBinaryData(string format, byte[] data) => SetBinaryData(DataFormats.GetFormat(format).Id, data);
/// <summary>Places data on the clipboard in a specified clipboard format.</summary>
/// <param name="formatId">The clipboard format. This parameter can be a registered format or any of the standard clipboard formats.</param>
/// <param name="data">The binary data in the specified format.</param>
/// <exception cref="System.ArgumentNullException">data</exception>
public void SetBinaryData(int formatId, byte[] data)
{
using var pMem = new SafeMoveableHGlobalHandle(data);
Win32Error.ThrowLastErrorIfInvalid(pMem);
Win32Error.ThrowLastErrorIfNull(SetClipboardData((uint)formatId, pMem.DangerousGetHandle()));
}
/// <summary>Places data on the clipboard in a specified clipboard format.</summary>
/// <param name="formatId">The clipboard format. This parameter can be a registered format or any of the standard clipboard formats.</param>
/// <param name="data">The data in the format dictated by <paramref name="formatId"/>.</param>
public void SetData<T>(int formatId, T data)
{
using var pMem = SafeMoveableHGlobalHandle.CreateFromStructure(data);
Win32Error.ThrowLastErrorIfInvalid(pMem);
Win32Error.ThrowLastErrorIfNull(SetClipboardData((uint)formatId, pMem.DangerousGetHandle()));
}
/// <summary>Places data on the clipboard in a specified clipboard format.</summary>
/// <param name="formatId">The clipboard format. This parameter can be a registered format or any of the standard clipboard formats.</param>
/// <param name="values">The data in the format dictated by <paramref name="formatId"/>.</param>
public void SetData<T>(int formatId, IEnumerable<T> values) where T : struct
{
using var pMem = SafeMoveableHGlobalHandle.CreateFromList(values);
Win32Error.ThrowLastErrorIfInvalid(pMem);
Win32Error.ThrowLastErrorIfNull(SetClipboardData((uint)formatId, pMem.DangerousGetHandle()));
}
/// <summary>Places data on the clipboard in a specified clipboard format.</summary>
/// <param name="formatId">The clipboard format. This parameter can be a registered format or any of the standard clipboard formats.</param>
/// <param name="values">The list of strings.</param>
/// <param name="packing">The packing type for the strings.</param>
/// <param name="charSet">The character set to use for the strings.</param>
public void SetData(int formatId, IEnumerable<string> 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()));
}
/// <summary>Sets multiple text types to the Clipboard.</summary>
/// <param name="text">The Unicode Text value.</param>
/// <param name="htmlText">The HTML text value. If <see langword="null"/>, this format will not be set.</param>
/// <param name="rtfText">The Rich Text Format value. If <see langword="null"/>, this format will not be set.</param>
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);
}
/// <summary>Sets a specific text type to the Clipboard.</summary>
/// <param name="value">The text value.</param>
/// <param name="format">The clipboard text format to set.</param>
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);
}
/// <summary>Sets a URL with optional title to the clipboard.</summary>
/// <param name="url">The URL.</param>
/// <param name="title">The title. This value can be <see langword="null"/>.</param>
/// <exception cref="ArgumentNullException">url</exception>
public void SetUrl(string url, string title = null)
{
if (url is null) throw new ArgumentNullException(nameof(url));
SetText(url, $"<a href=\"{url}\">{title ?? url}</a>", 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<string> e)
{
var sc = new StringCollection();
if (e != null)
sc.AddRange(e.ToArray());
return sc;
}
private static T GetComData<T>(DataObject dobj, string fmt, Func<IntPtr, T> 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<byte>(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 = "<!DOCTYPE html>";
const string htmlBodyStart = "<HTML><HEAD><meta charset=\"UTF-8\"><TITLE>Snippet</TITLE></HEAD><BODY>";
const string htmlBodyEnd = "</BODY></HTML>";
const string fragmentStart = "<!--StartFragment-->";
const string fragmentEnd = "<!--EndFragment-->";
var sb = new StringBuilder();
if (value.IndexOf("<!DOCTYPE", StringComparison.OrdinalIgnoreCase) < 0)
sb.Append(htmlDocType);
if (value.IndexOf("<HTML>", 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("</HTML>", 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
{
/// <summary>Gets a static instance of these methods.</summary>
public static readonly IMemoryMethods Instance = new MoveableHGlobalMethods();
/// <summary>Gets a handle to a memory allocation of the specified size.</summary>
/// <param name="size">The size, in bytes, of memory to allocate.</param>
/// <returns>A memory handle.</returns>
public override IntPtr AllocMem(int size) => Win32Error.ThrowLastErrorIfNull((IntPtr)Kernel32.GlobalAlloc(Kernel32.GMEM.GMEM_MOVEABLE | Kernel32.GMEM.GMEM_ZEROINIT, size));
/// <summary>Frees the memory associated with a handle.</summary>
/// <param name="hMem">A memory handle.</param>
public override void FreeMem(IntPtr hMem) => Kernel32.GlobalFree(hMem);
/// <summary>Locks the memory of a specified handle and gets a pointer to it.</summary>
/// <param name="hMem">A memory handle.</param>
/// <returns>A pointer to the locked memory.</returns>
public override IntPtr LockMem(IntPtr hMem) => Kernel32.GlobalLock(hMem);
/// <summary>Gets the reallocation method.</summary>
/// <param name="hMem">A memory handle.</param>
/// <param name="size">The size, in bytes, of memory to allocate.</param>
/// <returns>A memory handle.</returns>
public override IntPtr ReAllocMem(IntPtr hMem, int size) => Win32Error.ThrowLastErrorIfNull((IntPtr)Kernel32.GlobalReAlloc(hMem, size, Kernel32.GMEM.GMEM_MOVEABLE | Kernel32.GMEM.GMEM_ZEROINIT));
/// <summary>Unlocks the memory of a specified handle.</summary>
/// <param name="hMem">A memory handle.</param>
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;
/// <summary>Initializes a new instance of the <see cref="SafeMoveableHGlobalHandle"/> class.</summary>
/// <param name="handle">The handle.</param>
/// <param name="size">The size of memory allocated to the handle, in bytes.</param>
/// <param name="ownsHandle">if set to <c>true</c> if this class is responsible for freeing the memory on disposal.</param>
public SafeMoveableHGlobalHandle(IntPtr handle, SizeT size, bool ownsHandle = true) : base(IntPtr.Zero, ownsHandle)
{
SetHandle(handle);
sz = size;
}
/// <summary>Initializes a new instance of the <see cref="SafeMoveableHGlobalHandle"/> class.</summary>
/// <param name="size">The size of memory to allocate, in bytes.</param>
/// <exception cref="System.ArgumentOutOfRangeException">size - The value of this argument must be non-negative</exception>
public SafeMoveableHGlobalHandle(SizeT size) : base(IntPtr.Zero, true)
{
if (size == 0) return;
RuntimeHelpers.PrepareConstrainedRegions();
SetHandle(mm.AllocMem(sz = size));
}
/// <summary>
/// Allocates from unmanaged memory to represent an array of pointers and marshals the unmanaged pointers (IntPtr) to the native
/// array equivalent.
/// </summary>
/// <param name="bytes">Array of unmanaged pointers</param>
/// <returns>SafeMoveableHGlobalHandle object to an native (unmanaged) array of pointers</returns>
public SafeMoveableHGlobalHandle(byte[] bytes) : this(bytes?.Length ?? 0)
{
if (sz == 0) return;
CallLocked(p => Marshal.Copy(bytes, 0, p, sz));
}
/// <summary>Represents a NULL memory pointer.</summary>
public static SafeMoveableHGlobalHandle Null { get; } = new SafeMoveableHGlobalHandle(IntPtr.Zero, 0, false);
public override bool IsInvalid => handle == IntPtr.Zero;
/// <summary>Gets or sets the size in bytes of the allocated memory block.</summary>
/// <value>The size in bytes of the allocated memory block.</value>
public SizeT Size
{
get => sz;
set
{
if (value == 0)
{
ReleaseHandle();
}
else
{
RuntimeHelpers.PrepareConstrainedRegions();
handle = IsInvalid ? mm.AllocMem(value) : mm.ReAllocMem(handle, value);
sz = value;
}
}
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">Type of the trailing array of structures</typeparam>
/// <param name="values">Collection of structure objects</param>
/// <param name="prefixBytes">Number of bytes preceding the trailing array of structures</param>
/// <returns><see cref="SafeMoveableHGlobalHandle"/> object to an native (unmanaged) structure with a trail array of structures</returns>
public static SafeMoveableHGlobalHandle CreateFromList<T>(IEnumerable<T> values, int prefixBytes = 0) =>
new SafeMoveableHGlobalHandle(InteropExtensions.MarshalToPtr(values, mm.AllocMem, out int s, prefixBytes, mm.LockMem, mm.UnlockMem), s);
/// <summary>Allocates from unmanaged memory sufficient memory to hold an array of strings.</summary>
/// <param name="values">The list of strings.</param>
/// <param name="packing">The packing type for the strings.</param>
/// <param name="charSet">The character set to use for the strings.</param>
/// <param name="prefixBytes">Number of bytes preceding the trailing strings.</param>
/// <returns>
/// <see cref="SafeMoveableHGlobalHandle"/> object to an native (unmanaged) array of strings stored using the <paramref
/// name="packing"/> model and the character set defined by <paramref name="charSet"/>.
/// </returns>
public static SafeMoveableHGlobalHandle CreateFromStringList(IEnumerable<string> 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);
/// <summary>Allocates from unmanaged memory sufficient memory to hold an object of type T.</summary>
/// <typeparam name="T">Native type</typeparam>
/// <param name="value">The value.</param>
/// <returns><see cref="SafeMoveableHGlobalHandle"/> object to an native (unmanaged) memory block the size of T.</returns>
public static SafeMoveableHGlobalHandle CreateFromStructure<T>(in T value = default) =>
new SafeMoveableHGlobalHandle(InteropExtensions.MarshalToPtr(value, mm.AllocMem, out int s, 0, mm.LockMem, mm.UnlockMem), s);
/// <summary>Converts an <see cref="IntPtr"/> to a <see cref="SafeMoveableHGlobalHandle"/> where it owns the reference.</summary>
/// <param name="ptr">The <see cref="IntPtr"/>.</param>
/// <returns>The result of the conversion.</returns>
public static implicit operator SafeMoveableHGlobalHandle(IntPtr ptr) => new SafeMoveableHGlobalHandle(ptr, 0, true);
protected void CallLocked(Action<IntPtr> 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;
}
}
}
internal enum ShellDataFormat
{
ShellIdList,
ShellIdListOffset,
NetResources,
FileDescriptor,
FileContents,
FileName,
PrinterGroup,
FileNameMap,
ShellUrl,
InetUrl,
PreferredDropEffect,
PerformedDropEffect,
PasteSucceeded,
InDragLoop,
MountedVolume,
PersistedDataObject,
TargetClsid,
LogicalPerformedDropEffect,
AutoPlayLists,
UntrustedDragDrop,
FileAttributes,
InvokeCommandDropParam,
ShellDropHandler,
DropDescription,
ZoneIdentifier,
}
}