作者: Jim Wang 公众号: 巴博萨船长
剪切板的基本操作 在Python的实际应用中有时候会遇到对剪切板进行操作的问题。剪切板的基本操作需求如下:
获取剪切板中的内容。
向剪切板中注入内容。
清除剪切板的内容。
使用Python对剪切板进行操作,可以使用tkinter和ctypes这两个标准库。或者使用Qt或者wxpython这些第三方模块(库)来实现。
使用ctypes标准库操作剪切板 使用tkinter标准库或者第三方的Qt或者wxpython,这些实现方式常常是用在在图形界面化 的项目中的,比如结合按钮事件等。使用ctypes就可以直接在非图形化界面 项目中实现对剪切板的操作。或者使用pyperclip第三方模块也可实现跨平台的普通项目中对剪切板的操作,基本实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from __future__ import print_functionimport ctypesCF_TEXT = 1 kernel32 = ctypes.windll.kernel32 user32 = ctypes.windll.user32 def get (): rts = "" user32.OpenClipboard(0 ) if user32.IsClipboardFormatAvailable(CF_TEXT): data = user32.GetClipboardData(CF_TEXT) data_locked = kernel32.GlobalLock(data) text = ctypes.c_char_p(data_locked) print (text.value) rts = text.value kernel32.GlobalUnlock(data_locked) else : print ('no text in clipboard' ) user32.CloseClipboard() return rts def set (text ): GMEM_DDESHARE = 0x2000 user32.OpenClipboard(0 ) user32.EmptyClipboard() hCd = ctypes.windll.kernel32.GlobalAlloc(GMEM_DDESHARE, len (bytes (text))+1 ) pchData = ctypes.windll.kernel32.GlobalLock(hCd) ctypes.cdll.msvcrt.strcpy(ctypes.c_char_p(pchData), bytes (text)) kernel32.GlobalUnlock(hCd) user32.SetClipboardData(CF_TEXT, hCd) user32.CloseClipboard()
上述代码中,比较难以理解的应该是GlobalLock和GlobalUnlock,如果有C++的开发背景应该很容易理解这两个函数。简单解释:
GlobalLock()函数 说明:锁定内存中指定的内存块,并返回一个地址值,令其指向内存块的起始处。除非用 GlobalUnlock 函数将内存块解锁,否则地址会一直保持有效。Windows 为每个内存对象都维持着一个锁定计数。对这个函数的每次调用都应有一个对应的 GlobalUnlock 调用 返回值 Long,如成功,返回内存块的地址;如出错,或者这是一个已被丢弃的“可丢弃”内存块,则返回零。通常我们在编程的时候,给应用程序分配的内存都是可以移动的或者是可以丢弃的,这样能使有限的内存资源充分利用,所以,在某一个时候我们分配 的那块内存的地址是不确定的,因为他是可以移动的,所以得先锁定那块内存块,这儿应用程序需要调用API函数GlobalLock函数来锁定句柄。如下:lpMem=GlobalLock(hMem)(数据类型应该是指针类型); 这样应用程序才能存取这块内存。
剪切板操作的兼容性问题 上述代码我在公司的项目中也有使用。但是自从公司决定将软件中内嵌的Python2.7升级到Python3.7之后,类似上述的代码就不能在继续运行了。经过调试之后,发现了问题所在,请看下面代码:
1 2 3 4 5 6 7 8 9 10 def get_clipboard_text (): user32.OpenClipboard(0 ) if user32.IsClipboardFormatAvailable(CF_TEXT): data = user32.GetClipboardData(CF_TEXT) data_locked = kernel32.GlobalLock(data) text = ctypes.c_char_p(data_locked) value = text.value kernel32.GlobalUnlock(data_locked) return value user32.CloseClipboard()
在上述代码中,在调试代码时,代码可以运行至带有注释的这一行的前一行。当试图尝试使用便利text时,程序就会出错,从而引发崩溃。可以肯定的是text变量并不为None,在一开始一直以为是锁定内存带来的问题。但是最后发现整个代码都有问题的。
在不断尝试之后,项目背景是,剪切板的中不会存在特殊字符或者宽字符。本着最少代码修改量就能解决问题的原则,以及“简单胜过复杂”的设计哲学。我们用下列代码解决了这部分代码对Python3的兼容。
“Simple is better than complicated”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 kernel32.GlobalLock.argtypes = [ctypes.c_void_p] kernel32.GlobalLock.restype = ctypes.c_void_p kernel32.GlobalUnlock.argtypes = [ctypes.c_void_p] user32.GetClipboardData.restype = ctypes.c_void_p def get_clipboard_text (): user32.OpenClipboard(0 ) try : if user32.IsClipboardFormatAvailable(CF_TEXT): data = user32.GetClipboardData(CF_TEXT) data_locked = kernel32.GlobalLock(data) text = ctypes.c_char_p(data_locked) value = text.value kernel32.GlobalUnlock(data_locked) return value finally : user32.CloseClipboard()
在使用函数的时候,只要提前申明了函数参数的数据类型,和函数返回值的数据类型。就解决了兼容性问题。虽然解决了问题,但是总觉得有种知其然不知所以然的感觉。为什么Python2中不需要声明,而Python3中却需要。经过不断地查找,找到了下面的解决方案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import ctypesimport ctypes.wintypes as wCF_UNICODETEXT = 13 u32 = ctypes.WinDLL('user32' ) k32 = ctypes.WinDLL('kernel32' ) OpenClipboard = u32.OpenClipboard OpenClipboard.argtypes = w.HWND, OpenClipboard.restype = w.BOOL GetClipboardData = u32.GetClipboardData GetClipboardData.argtypes = w.UINT, GetClipboardData.restype = w.HANDLE GlobalLock = k32.GlobalLock GlobalLock.argtypes = w.HGLOBAL, GlobalLock.restype = w.LPVOID GlobalUnlock = k32.GlobalUnlock GlobalUnlock.argtypes = w.HGLOBAL, GlobalUnlock.restype = w.BOOL CloseClipboard = u32.CloseClipboard CloseClipboard.argtypes = None CloseClipboard.restype = w.BOOL def get_clipboard_text (): text = "" if OpenClipboard(None ): h_clip_mem = GetClipboardData(CF_UNICODETEXT) text = ctypes.wstring_at(GlobalLock(h_clip_mem)) GlobalUnlock(h_clip_mem) CloseClipboard() return text print (get_clipboard_text())
乍一看,很复杂,其实其实现过程和第一个解决方案相似,而且上述的代码够同时在Python2.x和Python3.x中完美运行,也同时支持ascii与unicode字符,算是一个完美的解决方案。
这个解决方案的提供者Mark Tolonen也解释道,由于Python3是64位的,如果我们在Python3.x的环境下使用原来在Python2.x中的代码。默认情况下,我们传递是句柄就是c_int(32bit)的,存储长度不够。超出会存在负值,导致代码不兼容的主要问题就在这儿。
kernel内核和windows的句柄都是32位的。如果在64位环境中使用这些句柄就会存在负值,为了避免这种情况发生,就应该将这些句柄扩展至64位。而且一些句柄实际上是内存的地址,例如HMODULE和HGLOBAL,以及GlobalLock的返回结果也应该得到扩展。总的来讲,在处理句柄和指针的时候总是声明参数argtypes和返回值restype,就可以避免硬编码实现细节和假设。
学无止境,总有高手,在这些高手的解释中也学习到了很多,了解一些问题出现的根本原因,和这些高手在面对这些问题时所思考的内容。然后学以致用,来提高自己。
参考目录:
Stackflow: https://stackoverflow.com/questions/46132401/read-text-from-clipboard-in-windows-using-ctypes
CSDN: https://blog.csdn.net/longxin5/article/details/83394388