You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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操作必须在主线程执行):

  1. 在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)
  1. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 15:27:50