Python中同时使用PyQt与pynput时应用启动卡顿问题求助
问题与解决方案
问题描述
开发一款PyQt应用,需求是检测用户在电脑任意位置选中文本并按下Ctrl+C后,从剪贴板获取对应文本。但同时使用PyQt与pynput时,应用启动后立即卡顿,推测是线程相关问题。可复现代码如下:
app.py
from copy_view import Copy_view from PyQt5.QtWidgets import QApplication from pynput.mouse import Listener as MouseListener from pynput.keyboard import Listener as KeyboardListener from pynput.keyboard import Key from pynput.mouse import Button import sys if __name__ == "__main__": app = QApplication(sys.argv) global copy_view copy_view = Copy_view() def on_click(x, y, button, pressed): if pressed and button == Button.left: copy_view.drag.click(x, y) else: result = copy_view.drag.release(x, y) if result: text = copy_view.get_text() copy_view.add_text(text) listener = MouseListener(on_click=on_click) listener.start() def on_press(key): if key == Key.esc: listener.stop() exitListener = KeyboardListener(on_release=on_press) exitListener.start() listener.join() sys.exit(app.exec_())
copy_view.py
from PyQt5.QtWidgets import ( QWidget, QListView, QVBoxLayout, ) from PyQt5.QtGui import ( QStandardItemModel, QStandardItem, ) import win32clipboard as wClip from drag import Drag, do_copy class Copy_view(QWidget): def __init__(self): super().__init__() self.drag = Drag() self.initUI() def initUI(self): self.texts = [] self.texts_model = QStandardItemModel() self.texts_view = QListView(self) # layout self.setGeometry(500, 500, 300, 500) self.vbox = QVBoxLayout() self.vbox.addWidget(self.texts_view) self.setLayout(self.vbox) self.show() def initUI_setup(self): self.texts_view.setModel(self.texts_model) self.texts_view.setWordWrap(True) def get_text(self): do_copy() wClip.OpenClipboard() text = wClip.GetClipboardData() # type: ignore wClip.CloseClipboard() return text def add_text(self, text): self.texts.append(text) self.texts_model.appendRow(QStandardItem(text))
卡顿原因
代码中listener.join()会阻塞主线程,而Qt的app.exec_()需要在主线程启动事件循环来处理界面交互。这行代码让主线程卡在等待pynput监听器结束,永远无法进入Qt的事件循环,直接导致界面卡顿无响应。
快速修复卡顿
修改app.py的最后部分,移除listener.join():
# 移除这行代码:listener.join() sys.exit(app.exec_())
pynput的监听器本身是后台守护线程,不需要调用join()等待,启动后会在后台运行,不会阻塞主线程。
更优实现方案
1. 用Qt原生剪贴板替代win32clipboard
Qt自带QClipboard类,跨平台且与Qt框架兼容更好,无需依赖win32api。修改copy_view.py的get_text方法:
from PyQt5.QtGui import QClipboard from PyQt5.QtWidgets import QApplication # 替换原get_text方法 def get_text(self): clipboard = QApplication.clipboard() return clipboard.text()
2. 监听剪贴板变化,彻底移除pynput
不需要监听鼠标/键盘,直接监听剪贴板内容变化,更高效且无线程问题。修改Copy_view类:
from PyQt5.QtGui import QClipboard from PyQt5.QtWidgets import QApplication class Copy_view(QWidget): def __init__(self): super().__init__() self.initUI() # 监听剪贴板变化 clipboard = QApplication.clipboard() clipboard.dataChanged.connect(self.on_clipboard_update) def on_clipboard_update(self): clipboard_text = QApplication.clipboard().text() # 避免重复添加空文本或重复内容 if clipboard_text and clipboard_text not in self.texts: self.add_text(clipboard_text) # 其他方法保持不变...
这样用户只要复制文本(无论通过Ctrl+C还是右键菜单),都会自动触发文本获取与添加,完全不需要pynput,从根源避免线程冲突。
3. 移除全局变量,用Qt信号槽传递数据
如果必须保留pynput监听,不要用全局变量,改用Qt的信号槽机制将后台线程的数据传递到主线程处理UI更新(Qt要求所有UI操作必须在主线程执行):
- 在
Copy_view类中添加信号:
from PyQt5.QtCore import pyqtSignal class Copy_view(QWidget): # 定义传递文本的信号 new_text_signal = pyqtSignal(str) def __init__(self): super().__init__() self.drag = Drag() self.initUI() # 绑定信号到添加文本的槽函数 self.new_text_signal.connect(self.add_text)
- 修改
app.py中的on_click函数,通过信号传递文本:
def on_click(x, y, button, pressed): if pressed and button == Button.left: copy_view.drag.click(x, y) else: result = copy_view.drag.release(x, y) if result: text = copy_view.get_text() # 发射信号,让主线程处理UI更新 copy_view.new_text_signal.emit(text)
4. 管理监听器生命周期
在应用关闭时停止pynput监听器,避免残留后台线程。在Copy_view类中重写closeEvent:
def closeEvent(self, event): # 假设listener和exitListener是全局变量,或者可以通过其他方式获取 listener.stop() exitListener.stop() event.accept()
内容的提问来源于stack exchange,提问作者Natsu Kin
相关产品推荐
相关产品推荐

