Search code examples
pythonwindowsnumpyctypesclipboard

Copy a Numpy array to clipboard on Windows without win32 dependency?


The main answer from How to paste a Numpy array to Excel works to copy a Numpy array in the clipboard, ready to be pasted in Excel:

import numpy as np
import win32clipboard as clipboard
def toClipboardForExcel(array):
    array_string = "\r\n".join("\t".join(line.astype(str)).replace("\n","") for line in array)
    clipboard.OpenClipboard()
    clipboard.EmptyClipboard()
    clipboard.SetClipboardText(array_string)
    clipboard.CloseClipboard()
Y = np.arange(64).reshape((8, 8))
toClipboardForExcel(Y)

Is it possible to do this without the extra win32 package dependency? (I don't use it currently in my project and I'd like to avoid adding it only for clipboard, NB: I don't use pandas either)

I already tried with os.system(`echo string | clip`) but it doesn't work for multiline content (containing \n).

Or maybe is OpenClipboard, SetClipboardText, etc. accessible via ctypes (which I already use)?

NB: this is not a duplicate of Python script to copy text to clipboard because the latter is general, and with extra dependencies, and here in my question, we would like to avoid new dependencies.


Solution

  • Here is a ctypes-only example to set any Python str-type text on the clipboard and read it back. Note that text should not contain nulls as the CF_UNICODETEXT type expects to be null-terminated text.

    import ctypes as ct
    import ctypes.wintypes as w
    
    CF_UNICODETEXT = 13
    NO_ERROR = 0
    SIZE_T = ct.c_size_t
    GMEM_MOVEABLE = 0x0002
    
    # Error handlers to raise exceptions on failure.
    
    def boolcheck(result, func, args):
        if not result:
            raise ct.WinError(ct.get_last_error())
    
    def nullcheck(result, func, args):
        if result is None:
            raise ct.WinError(ct.get_last_error())
        return result
    
    def zeroerrorcheck(result, func, args):
        if not result:
            err = ct.get_last_error()
            if err != NO_ERROR:
                raise ct.WinError(err)
        return result
    
    # Capture GetLastError() code after each call.
    # Fully specify argtypes and restype for ctypes type-checking.
    
    kernel32 = ct.WinDLL('kernel32', use_last_error=True)
    GlobalLock = kernel32.GlobalLock
    GlobalLock.argtypes = w.HGLOBAL,
    GlobalLock.restype = w.LPVOID
    GlobalLock.errcheck = nullcheck
    GlobalAlloc = kernel32.GlobalAlloc
    GlobalAlloc.argtypes = w.UINT, SIZE_T
    GlobalAlloc.restype = w.HGLOBAL
    GlobalAlloc.errcheck = nullcheck
    GlobalUnlock = kernel32.GlobalUnlock
    GlobalUnlock.argtypes = w.HGLOBAL,
    GlobalUnlock.restype = w.BOOL
    GlobalUnlock.errcheck = zeroerrorcheck
    
    user32 = ct.WinDLL('user32', use_last_error=True)
    OpenClipboard = user32.OpenClipboard
    OpenClipboard.argtypes = w.HWND,
    OpenClipboard.restype = w.BOOL
    OpenClipboard.errcheck = boolcheck
    GetClipboardData = user32.GetClipboardData
    GetClipboardData.argtypes = w.UINT,
    GetClipboardData.restype = w.HANDLE
    GetClipboardData.errcheck = nullcheck
    SetClipboardData = user32.SetClipboardData
    SetClipboardData.argtypes = w.UINT, w.HANDLE
    SetClipboardData.restype = w.HANDLE
    SetClipboardData.errcheck = nullcheck
    CloseClipboard = user32.CloseClipboard
    CloseClipboard.argtypes = ()
    CloseClipboard.restype = w.BOOL
    CloseClipboard.errcheck = boolcheck
    EmptyClipboard = user32.EmptyClipboard
    EmptyClipboard.argtypes = ()
    EmptyClipboard.restype = w.BOOL
    EmptyClipboard.errcheck = boolcheck
    GetForegroundWindow = user32.GetForegroundWindow
    GetForegroundWindow.argtypes = ()
    GetForegroundWindow.restype = w.HWND
    
    def get_clipboard_text():
        OpenClipboard(GetForegroundWindow())
        hmem = GetClipboardData(CF_UNICODETEXT)
        pmem = GlobalLock(hmem)
        text = ct.wstring_at(pmem)
        GlobalUnlock(hmem)
        CloseClipboard()
        return text
    
    def set_clipboard_text(text):
        ztext = text + '\x00'  # null terminator required
        OpenClipboard(None)
        EmptyClipboard()
        hmem = GlobalAlloc(GMEM_MOVEABLE, len(ztext) * ct.sizeof(w.WCHAR))
        pmem = GlobalLock(hmem)
        btext = ztext.encode('utf-16le')
        ct.memmove(pmem, btext, len(btext))
        GlobalUnlock(hmem)
        SetClipboardData(CF_UNICODETEXT, hmem)
        CloseClipboard()
        
    set_clipboard_text('马克')
    print(get_clipboard_text())
    

    Per OpenClipboard documentation remarks:

    If an application calls OpenClipboard with hwnd set to NULL, EmptyClipboard sets the clipboard owner to NULL; this causes SetClipboardData to fail.

    My experience is that using NULL (None in Python) with OpenClipboard does not cause SetClipboardData to fail, but doesn't prevent other apps using the clipboard while open (potential race condition?). When a valid Window handle is used, other apps will fail to open the clipboard until CloseClipboard is called, so some retrying and error checking will be needed. Probably best to use a valid owner. I've used GetForegroundWindow above.