diff --git a/UnitTests/Windows.Shell/ClipboardTests.cs b/UnitTests/Windows.Shell/ClipboardTests.cs new file mode 100644 index 00000000..d5fde5e5 --- /dev/null +++ b/UnitTests/Windows.Shell/ClipboardTests.cs @@ -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 = "
“We’ve been here”
"; + + [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())}"); + 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 = @"“We’ve been here”"; + using var cb = new Clipboard(); + cb.SetText(txt, html); + } + } +} \ No newline at end of file diff --git a/UnitTests/Windows.Shell/Windows.Shell.csproj b/UnitTests/Windows.Shell/Windows.Shell.csproj index c57d9504..b46c7dd0 100644 --- a/UnitTests/Windows.Shell/Windows.Shell.csproj +++ b/UnitTests/Windows.Shell/Windows.Shell.csproj @@ -20,7 +20,7 @@ DEBUG;TRACE prompt 4 - AnyCPU + x86 pdbonly @@ -49,6 +49,7 @@ + @@ -88,6 +89,10 @@ {30fd6779-6549-449e-880a-695815eb89b0} Vanara.PInvoke.ShlwApi + + {a6771907-addc-49fc-8444-a97aa65e77e2} + Vanara.PInvoke.User32 + {43685be2-a65e-4b01-be16-479526940f23} Vanara.Windows.Shell diff --git a/Windows.Shell/NativeClipboard.cs b/Windows.Shell/NativeClipboard.cs index 0be6b660..c90890b0 100644 --- a/Windows.Shell/NativeClipboard.cs +++ b/Windows.Shell/NativeClipboard.cs @@ -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 + /// + /// 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 { - 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 GetFileDropList() => new List(Clipboard.GetFileDropList().Cast()); - 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 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 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; + + /// 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) { - 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) + /// 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 { - 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(); + } + } + + /// 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; } } } - - 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, - } } \ No newline at end of file