PyQt5小技巧整理2:剪贴板的介绍和遇到的坑

完成一个项目可以学到很多东西,最近利用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,如果你尝试在类以外实例化它:


image.png

是的,肯定会报错;这个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()
  1. 若剪贴板内容为文本,则返回文本;若剪贴板中内容不是文本,则会返回一个空字符串;
  2. 若剪贴板内容为图像,则返回图像;若剪贴板中内容不是图像或者是格式不支持的图像,则会返回一个空图像;这个函数我没用,因为我实际应用中需要判断剪贴板中是否是图片,通过读取剪贴板的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,在线画流程图非常方便)。

image.png

代码实现也非常简单。

# 将剪贴板中的图片保存成本地图片,利用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打印,下面是这段代码的流程图。


image.png

运行一下main.py,点击一下中间的按钮,可以看到打印信息。

Write time to cb...   # 点击按钮触发,向剪贴板写入当前时间
Clipboard data changed.  # 写入时间后检测到剪贴板内容变化
Not a img in clipboard!  # 这时剪贴板中是文本不是图片,什么也不做

我们再用Ditto或者Paste查看一下剪贴板内容。


image.png

第一条是当前的时间,并且是文本内容,和打印的信息相符合,说明通过点击按钮向剪贴板写入文本信息没有问题。接着,我们重新运行一下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!  # 这时剪贴板中不是图片了,什么都不干

看打印的信息,好像流程没什么问题,这时我们想一下,正常情况下运行完上述代码之后剪贴板会是什么情况?对,第一条应该是当前时间,第二条是刚才截图,第三条是上一次运行时写入的时间。但是打开剪贴板一看,好像并不是那么回事...

image.png

很明显,截图触发的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上传到七牛云
...       
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容

  • 一、窗口控件 如果是主窗口,用QmainWindow类; 如果是对话框,就用Qdialog类; 如果不确定,或者可...
    2e07917c964c阅读 11,387评论 0 15
  • 徐小胖是一个没有什么朋友的人。从小两个哥哥之间就老打架,大哥实诚力气大,二哥人鬼机灵,这两人打来打去,各有输赢,也...
    秦巴佬阅读 416评论 0 5
  • 1)小学时期 之前写过一篇《我的小学老师》,班主任是教语文的,在考试之前就把试题反复讲解了,我很顺利的步入初中。 ...
    _矛盾_阅读 628评论 11 11