Search code examples
c#winapistreampinvokeclipboard

Read system clipboard as stream instead of string


If I have a massive amount of text on the system clipboard (e.g. 150MB text file), I'd like to be able to read the system clipboard from a stream as Unicode text so as to avoid an OutOfMemoryException. Is this at all possible by tweaking the pinvoke example below?

For these very large clipboards, Clipboard.GetText(TextDataFormat.UnicodeText) will return an empty string without throwing an exception.

Alternatively, if I use pinvoke like the example from here, I will get an OutOfMemoryException http://komalmangal.blogspot.ca/2016/04/how-to-get-clipboard-data-and-its-size.html

    [DllImport("user32.dll")]
    static extern IntPtr GetClipboardData(uint uFormat);
    [DllImport("user32.dll")]
    static extern bool IsClipboardFormatAvailable(uint format);
    [DllImport("user32.dll", SetLastError = true)]
    static extern bool OpenClipboard(IntPtr hWndNewOwner);
    [DllImport("user32.dll", SetLastError = true)]
    static extern bool CloseClipboard();
    [DllImport("kernel32.dll")]
    static extern IntPtr GlobalLock(IntPtr hMem);
    [DllImport("kernel32.dll")]
    static extern bool GlobalUnlock(IntPtr hMem);

    const uint CF_UNICODETEXT = 13;
    public static string GetText()
    {
        if (!IsClipboardFormatAvailable(CF_UNICODETEXT))
            return null;
        if (!OpenClipboard(IntPtr.Zero))
            return null;

        string data = null;
        var hGlobal = GetClipboardData(CF_UNICODETEXT);
        if (hGlobal != IntPtr.Zero)
        {
            var lpwcstr = GlobalLock(hGlobal);
            if (lpwcstr != IntPtr.Zero)
            {
                data = Marshal.PtrToStringUni(lpwcstr);
                GlobalUnlock(lpwcstr);
            }
        }
        CloseClipboard();

        return data;
    }

Solution

  • This will write out the system clipboard to a text file without first converting it to a string, allowing very large clipboards to be written out without encountering an OutOfMemoryException. It requires that the Visual Studio project be built with the /unsafe flag.

    [DllImport("user32.dll")]
    private static extern IntPtr GetClipboardData(uint uFormat);
    [DllImport("user32.dll")]
    private static extern bool IsClipboardFormatAvailable(uint format);
    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool OpenClipboard(IntPtr hWndNewOwner);
    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool CloseClipboard();
    [DllImport("kernel32.dll")]
    private static extern IntPtr GlobalLock(IntPtr hMem);
    [DllImport("kernel32.dll")]
    private static extern bool GlobalUnlock(IntPtr hMem);
    [DllImport("kernel32.dll")]
    private static extern UIntPtr GlobalSize(IntPtr hMem);
    private const uint CF_UNICODETEXT = 13;
    
    //Write the clipboard to a text file without having to first convert it to a string.
    //This avoids OutOfMemoryException for large clipboards and is faster than other methods
    public static bool WriteClipboardTextToFile(string filename)
    {
        try
        {
            if (!IsClipboardFormatAvailable(CF_UNICODETEXT) || !OpenClipboard(IntPtr.Zero))
                return false;
        }
        catch
        {
            return false;
        }
    
        try
        {
            var hGlobal = GetClipboardData(CF_UNICODETEXT);
            if (hGlobal == IntPtr.Zero)
                return false;
    
            var lpwcstr = GlobalLock(hGlobal);
            if (lpwcstr == IntPtr.Zero)
                return false;
    
            try
            {
                long length = (long)GlobalSize(lpwcstr);
                Stream stream;
                unsafe
                {
                    stream = new UnmanagedMemoryStream((byte*)lpwcstr, length);
                }
    
                const int bufSize = 4096;
                var buffer = new char[bufSize];
                using (var sw = new StreamWriter(new FileStream(filename, FileMode.Create, FileAccess.Write), Encoding.UTF8))
                {
                    //Clipboard text is in Encoding.Unicode == UTF-16LE
                    using (var sr = new StreamReader(stream, Encoding.Unicode))
                    {
                        int charCount;
                        while (!sr.EndOfStream && (charCount = sr.ReadBlock(buffer, 0, bufSize)) > 0)
                        {
                            if (sr.EndOfStream && buffer[charCount - 1] == '\0')
                                sw.Write(buffer, 0, charCount - 1); //don't write out null terminator
                            else
                                sw.Write(buffer, 0, charCount);
                        }
                    }
                }
            }
            finally
            {
                GlobalUnlock(hGlobal);
            }
        }
        catch
        {
            return false;
        }
        finally
        {
            try
            {
                CloseClipboard();
            }
            catch
            {
                //ignore
            }
        }
        return true;
    }