作者: Jim Wang 公众号: 巴博萨船长

剪切板的基本操作

在Python的实际应用中有时候会遇到对剪切板进行操作的问题。剪切板的基本操作需求如下:

  • 获取剪切板中的内容。
  • 向剪切板中注入内容。
  • 清除剪切板的内容。

使用Python对剪切板进行操作,可以使用tkinter和ctypes这两个标准库。或者使用Qt或者wxpython这些第三方模块(库)来实现。

img

使用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_function
import ctypes

CF_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 ctypes
import ctypes.wintypes as w

CF_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


版权声明:
文章首发于 Jim Wang's blog , 转载文章请务必以超链接形式标明文章出处,作者信息及本版权声明。