完成一个项目可以学到很多东西,最近利用PyQt5写的一个七牛云管理器也让自己长进了一些。今天这篇博文讲一下使用PyQt5中的剪贴板时遇到的坑。不得不补充一句,PyQt5是跨平台的,剪贴板功能也是跨平台的,写好了在Win/Mac/Linux上都能用。
Qclipboard在哪
首先,PyQt5中剪贴板模块的位置为:
PyQt5 -> QtWidget -> QApplication -> QClipboard
项目中可以这样来实例化:
from PyQt5 import QtWidgets
class MainUi(...):
def __init__(self);
self.cb = QtWidgets.QApplication.clipboard()
...
需要注意的是,必须在一个Gui类里面才能访问QClipboard,如果你尝试在类以外实例化它:
是的,肯定会报错;这个Gui的类可以是PyQt的QMainWindow、QWidgets等。
QClipboard的API
查看PyQt5的API,就像我在QyQt5入门系列博文中提到的,要养成翻阅QT官方文档库的习惯,目前PyQt5的官方文档只有英文的,而且是C++语言的应用,但是其实熟悉之后并不影响。
QT官方文档地址:https://doc.qt.io/
当然,现在Qt for Python的官方文档库也在慢慢完善,不过是Qt的另外一个分支叫PySide2,然而用法基本一致。
Qt for Python官方文档地址:https://doc.qt.io/qtforpython/
善用搜索找到Qclipboard所在的位置:http://doc.qt.io/qt-5/qclipboard.html
我们可以看到,PyQt5对系统剪贴板提供了不少的接口,这里介绍几个可能比较常用的。假如你已经如上面介绍的一样创建了一个MainUi的类,无论这个类继承的是QMainwindow还是QWidget还是其他Gui窗口类型,都是没有问题的。
from PyQt5 import QtWidgets
class MainUi(...):
def __init__(self);
self.cb = QtWidgets.QApplication.clipboard()
...
常用的公共方法和信号如下。
self.cb.clear()
清除剪贴板的内容
self.cb.text()
self.cb.image()
- 若剪贴板内容为文本,则返回文本;若剪贴板中内容不是文本,则会返回一个空字符串;
- 若剪贴板内容为图像,则返回图像;若剪贴板中内容不是图像或者是格式不支持的图像,则会返回一个空图像;这个函数我没用,因为我实际应用中需要判断剪贴板中是否是图片,通过读取剪贴板的mimeData来识别,这个后面会讲到。
mdata = self.cb.mimeData()
if mdata.hasImage():
...
elif mdata.hasText():
...
获取剪贴板的mimeData,其实返回的是PyQt5定义的QMimeData类型,点击查看QMimeData官方文档说明。其实mimeData自身也有很多操作,这里提到的hasImage和hasText是判断剪贴板中是否有图片或者是否有文本,然后根据判断结果进行其他操作。
self.cb.setImage(...)
self.cb.setText(...)
向剪贴板中写入图像或者文本。
self.cb.dataChanged.connect(...)
这是PyQt5的QClipboard的一个很有用的信号,当剪贴板中的数据发生改变时,会发出这个信号,通过connect链接到其他方法上,这样可以监控剪贴板来做一些事情。本篇博文说的坑,也就是在这个地方遇到的。
使用QClipboard.dataChanged()遇到的坑
我的七牛云助手小项目中,QClipboard.dataChange()是为了实现监控剪贴板是否拷贝了新的图片、如果有就询问是否上传、并自动拷贝上传后的文件链接的功能。其实我觉得想法还不错,不过直到项目上传完了自己日常使用的时候才发现一个问题:上传文件完成后总不能自动拷贝文件链接。允许我用流程图来表示一下项目中的这个功能(这里安利一下processon,在线画流程图非常方便)。
代码实现也非常简单。
# 将剪贴板中的图片保存成本地图片,利用PIL,这里不详写
def save_tmp_bmp(...):
...
# 将本地图片上传到七牛云,具体过程不详写
def qiniu_upload(...):
upload... # 上传文件
link = ... # 获取文件在七牛云上的链接
ui.cb.setText(link) # 向剪贴板中写入链接
class MainUi(...):
...
setupFunction(...):
# 实例化QClipboard,开启监控
self.cb = QtWidgets.QApplication.clipboard()
self.cb.dataChanged.connect(self.monitor_clipboard)
...
def monitor_clipboard(...):
if self.cb.mimeData.hasImage(): # 当剪贴板中有图片
save_tmp_image('tmp.png') # 将剪贴板中的图片保存成本地图片tmp.png
qiniu_upload('tmp.img') # 讲tmp.png上传到七牛云
...
ui = MainUi() # 创建主窗口,实例名称为ui
ui.setupFunction() #执行setupFunction方法
...
看起来合情合理对不对?然而事实上,不管怎么调试,复制图片没问题、剪贴板监控和触发没问题、上传图片没问题,就是最后复制不了链接。通过添加print来看,setText确实运行了,但是剪贴板中最后一条记录任然是之前复制的图片而不是链接。(关于便捷查看剪贴板中的内容,这里安利2个软件:Windows上用Ditto,Mac上用Paste。这2个软件都是各自平台上非常优秀的剪贴板增强软件,可以很大程度上增加日常的办事效率。)
问题分析和解决
经过一番艰苦卓绝的debug,最后发现问题症结所在:
在QClipboard.dataChanged.connect()所链接到的方法中,不能含有QClipboard.setText()
其实也很好理解,这可能是Qt为了防止出现因为剪贴板内容改变引发的死循环触发:
检测到剪贴板内改变 -> setText() -> 检测到剪贴板内改变 -> setText() -> 检测到剪贴板内改变 -> setText()...
不管这个链接的方法多么复杂,在多少个子方法/函数之间跳转,都不可以。这里可以通过一个比较简单的例子来说明一下。且看下面的代码(点击下载完整工程文件)
import sys
from PyQt5 import QtWidgets
from src.mainwindow import Ui_MainWindow as MW
import time
class MainUI(MW):
# 初始化方法,实例化clipboard()
def __init__(self):
self.cb = QtWidgets.QApplication.clipboard()
# 功能设置方法
def setupFunction(self):
# 按钮按下时,执行self.set_cb()
self.pushButton.clicked.connect(self.set_cb)
# 当剪贴板内容发生改变时,执行self.cb_changed()
self.cb.dataChanged.connect(self.cb_changed)
# 当剪贴板内容改变时执行
# 如果当前剪贴板内容为图片,则转到执行self.set_cb()
# 如果当前姐铁板内容为其他,则打印信息提示
def cb_changed(self):
print('Entering set_cb...')
mdata = self.cb.mimeData()
if mdata.hasImage():
print('Img in clipboard.')
self.set_cb()
else:
print('Not a img in clipboard!')
# 向剪贴板中写入当前日期
def set_cb(self):
localtime = time.asctime(time.localtime(time.time()))
self.cb.setText(localtime)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
# 主窗口
MainWindow = QtWidgets.QMainWindow()
ui = MainUI()
ui.setupUi(MainWindow)
ui.setupFunction()
MainWindow.show()
sys.exit(app.exec_())
这是个非常简化的应用,注释已经对主要语句做了说明,我也在关键地方增加了print打印,下面是这段代码的流程图。
运行一下main.py,点击一下中间的按钮,可以看到打印信息。
Write time to cb... # 点击按钮触发,向剪贴板写入当前时间
Clipboard data changed. # 写入时间后检测到剪贴板内容变化
Not a img in clipboard! # 这时剪贴板中是文本不是图片,什么也不做
我们再用Ditto或者Paste查看一下剪贴板内容。
第一条是当前的时间,并且是文本内容,和打印的信息相符合,说明通过点击按钮向剪贴板写入文本信息没有问题。接着,我们重新运行一下main.py,截一张图(键盘的PrtSc或者QQ截图都可以),再看一下打印信息。
Clipboard data changed. # 截图之后自然会检测到剪贴板内容变化
Img in clipboard. # 这时剪贴板中是图片
Write time to cb... # 根据代码规则,检测到是图片则触发write_time_to_cb,向剪贴板写入当前时间
Clipboard data changed. # 写入时间后剪贴板再次检测到数据变化
Not a img in clipboard! # 这时剪贴板中不是图片了,什么都不干
看打印的信息,好像流程没什么问题,这时我们想一下,正常情况下运行完上述代码之后剪贴板会是什么情况?对,第一条应该是当前时间,第二条是刚才截图,第三条是上一次运行时写入的时间。但是打开剪贴板一看,好像并不是那么回事...
很明显,截图触发的write_time_to_cb并没有成功向剪贴板中写入时间。这就是我说的问题症结所在:这一次,setText()在write_time_to_db()中,而write_time_to_db()在dataChanged的链接中,这时候,为了防止死循环触发(当然我的代码中通过判断是否是图片避免了无限触发),最后一次setText并没有生效。
解决办法
解决办法简单的来说就是:放弃使用QClipboard.dataChanged信号,改用PyQt5的定时器QTimer定期扫描剪贴板内容,发生变化则进入下一步。简单代码如下,对具体实现感兴趣的请自行下载我的项目查看。
class MainUi(...):
setupFunction(...):
self.timer_clipboard = QtCore.QTimer() # 声明定时器
self.timer_clipboard.timeout.connect(self.monitor_clipboard) # 定时器触发monitor_clipboard方法
self.timer_clipboard.start(3000) # 定时器触发间隔为3秒
def monitor_clipboard(...):
mdata = self.cb.mimeData() # 获取剪贴板内容
if mdata != mdata_tmp: # 如果当前剪贴板内容mdata和上次剪贴板内容mdata_tmp不一样
mdata_tmp = mdata # 讲当前简体版内容赋值到mdata_tmp中
if mdata.hasImage(): # 当剪贴板中有图片
save_tmp_image('tmp.png') # 将剪贴板中的图片保存成本地图片tmp.png
qiniu_upload('tmp.img') # 讲tmp.png上传到七牛云
...