vPaste 剪贴板工具:Windows / macOS 开源剪贴板历史管理


头像
派森小新
原创
发布时间: 2026-11-07 09:33:57 | 阅读数 0收藏数 0评论数 0
封面
Python + PyQt 实战:打造你的专属剪贴板管家。 复制过的文字、图片自动记录,可视化卡片展示,支持搜索和一键粘贴
1

创建项目

使用pycharm开发软件新建项目,项目命名为vPaste,根据你本地的环境来选择解释器类型,目前我本使用的conda。

2

桌面窗口

需要安装对应的依赖。

pip install PyQt5

安装完成后创建项目文件,把以下代码写入main.py文件,运行后可以看到窗口。


import sys

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QVBoxLayout,
QWidget,
)


class ClipboardWindow(QMainWindow):
"""剪贴板管理器主窗口"""

def __init__(self):
super().__init__()

# 设置窗口基本属性
self.setWindowTitle("vPaste Pro - 剪贴板管理器")
self.resize(900, 600)
self.setMinimumSize(680, 450)

self.init_ui()

def init_ui(self):
# 创建中央容器
central_widget = QWidget()
central_widget.setObjectName("centralWidget")
self.setCentralWidget(central_widget)

# 创建垂直布局
main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(28, 24, 28, 24)
main_layout.setSpacing(16)

# 页面标题
title_label = QLabel("vPaste Pro")
title_label.setObjectName("titleLabel")

# 页面副标题
subtitle_label = QLabel("简单、直观的剪贴板历史管理工具")
subtitle_label.setObjectName("subtitleLabel")

# 中间提示内容
content_label = QLabel("第 1 步:基础窗口已经创建")
content_label.setObjectName("contentLabel")
content_label.setAlignment(Qt.AlignCenter)

# 添加到布局
main_layout.addWidget(title_label)
main_layout.addWidget(subtitle_label)
main_layout.addStretch()
main_layout.addWidget(content_label)
main_layout.addStretch()

# 设置界面样式
self.setStyleSheet("""
QMainWindow {
background-color: #F3F5F8;
}

QWidget#centralWidget {
background-color: #F3F5F8;
}

QLabel#titleLabel {
color: #172B4D;
font-size: 28px;
font-weight: 700;
}

QLabel#subtitleLabel {
color: #718096;
font-size: 13px;
}

QLabel#contentLabel {
background-color: #08BDE8;
color: #073B4C;
border: 2px solid #172B4D;
border-radius: 16px;
padding: 36px;
font-size: 20px;
font-weight: 600;
}
""")


def main():
app = QApplication(sys.argv)
app.setApplicationName("vPaste Pro")

window = ClipboardWindow()
window.show()

sys.exit(app.exec_())


if __name__ == "__main__":
main()

3

获取系统剪贴板

电脑复制文字,程序自动铺获内容并在窗口中历史列表记录中展示。


import sys
from datetime import datetime

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QLabel,
QVBoxLayout,
QListWidget,
QListWidgetItem,
)


class ClipboardWindow(QMainWindow):
"""vPaste Pro 剪贴板管理器"""

def __init__(self):
super().__init__()

self.setWindowTitle("vPaste Pro - 剪贴板管理器")
self.resize(900, 600)
self.setMinimumSize(680, 450)

# 保存最近一次捕获的文本,避免重复记录
self.last_clipboard_text = ""

# 获取系统剪贴板
self.clipboard = QApplication.clipboard()

self.init_ui()

# 监听剪贴板变化
self.clipboard.dataChanged.connect(
self.on_clipboard_changed
)

def init_ui(self):
"""初始化界面"""

central_widget = QWidget()
central_widget.setObjectName("centralWidget")
self.setCentralWidget(central_widget)

main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(28, 24, 28, 24)
main_layout.setSpacing(16)

# 页面标题
title_label = QLabel("vPaste Pro")
title_label.setObjectName("titleLabel")

subtitle_label = QLabel(
"复制的内容会自动保存到历史记录"
)
subtitle_label.setObjectName("subtitleLabel")

# 历史记录标题
history_title = QLabel("剪贴板历史")
history_title.setObjectName("sectionTitle")

# 历史记录列表
self.history_list = QListWidget()
self.history_list.setObjectName("historyList")

# 初始提示
self.empty_label = QLabel(
"还没有剪贴板记录\n\n请尝试复制一段文字"
)
self.empty_label.setObjectName("emptyLabel")
self.empty_label.setAlignment(Qt.AlignCenter)

main_layout.addWidget(title_label)
main_layout.addWidget(subtitle_label)
main_layout.addSpacing(12)
main_layout.addWidget(history_title)
main_layout.addWidget(self.history_list)
main_layout.addWidget(self.empty_label)

self.empty_label.show()
self.history_list.hide()

self.setStyleSheet("""
QMainWindow,
QWidget#centralWidget {
background-color: #F3F5F8;
}

QLabel#titleLabel {
color: #172B4D;
font-size: 28px;
font-weight: 700;
}

QLabel#subtitleLabel {
color: #718096;
font-size: 13px;
}

QLabel#sectionTitle {
color: #172B4D;
font-size: 17px;
font-weight: 700;
}

QListWidget#historyList {
background-color: #FFFFFF;
border: 1px solid #DCE3EC;
border-radius: 12px;
padding: 8px;
outline: none;
}

QListWidget#historyList::item {
background-color: #E9F9FD;
color: #173B4A;
border-left: 4px solid #08BDE8;
border-radius: 7px;
padding: 15px;
margin: 5px;
}

QListWidget#historyList::item:selected {
background-color: #C8F1FA;
color: #073B4C;
}

QLabel#emptyLabel {
background-color: #FFFFFF;
color: #8995A5;
border: 2px dashed #D5DEE8;
border-radius: 14px;
font-size: 15px;
padding: 30px;
}
""")

def on_clipboard_changed(self):
"""剪贴板发生变化时调用"""

# 获取剪贴板文本
text = self.clipboard.text()

# 忽略空内容
if not text or not text.strip():
return

# 忽略重复内容
if text == self.last_clipboard_text:
return

self.last_clipboard_text = text

# 添加到历史记录
self.add_history_item(text)

def add_history_item(self, text):
"""添加一条剪贴板历史记录"""

# 获取当前时间
current_time = datetime.now().strftime(
"%H:%M:%S"
)

# 生成记录显示内容
display_text = (
f"复制时间:{current_time}\n\n{text}"
)

item = QListWidgetItem(display_text)

# 设置列表项高度
item.setSizeHint(
item.sizeHint()
)

# 最新记录放在最上面
self.history_list.insertItem(0, item)

# 切换空状态
self.empty_label.hide()
self.history_list.show()


def main():
app = QApplication(sys.argv)
app.setApplicationName("vPaste Pro")

window = ClipboardWindow()
window.show()

sys.exit(app.exec_())


if __name__ == "__main__":
main()

4

记录卡片

前几步以及实现了基础的窗口和文件监听,接下来修改内容的样式,样式为卡片。


import sys
from datetime import datetime

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QLabel,
QVBoxLayout,
QHBoxLayout,
QListWidget,
QListWidgetItem,
QFrame,
QSizePolicy,
)


class ClipboardCard(QFrame):
"""剪贴板历史记录卡片"""

def __init__(self, content, copy_time):
super().__init__()

self.content = content
self.copy_time = copy_time

self.init_ui()

def init_ui(self):
self.setObjectName("clipboardCard")
self.setSizePolicy(
QSizePolicy.Expanding,
QSizePolicy.Minimum
)

# 卡片整体布局
card_layout = QVBoxLayout(self)
card_layout.setContentsMargins(0, 0, 0, 0)
card_layout.setSpacing(0)

# 创建卡片顶部区域
header = QFrame()
header.setObjectName("cardHeader")

header_layout = QHBoxLayout(header)
header_layout.setContentsMargins(16, 10, 16, 10)

# 内容类型
type_label = QLabel("文本")
type_label.setObjectName("typeLabel")

# 复制时间
time_label = QLabel(self.copy_time)
time_label.setObjectName("timeLabel")

header_layout.addWidget(type_label)
header_layout.addStretch()
header_layout.addWidget(time_label)

# 创建卡片内容区域
content_widget = QFrame()
content_widget.setObjectName("cardContent")

content_layout = QVBoxLayout(content_widget)
content_layout.setContentsMargins(16, 14, 16, 14)

content_label = QLabel(self.content)
content_label.setObjectName("contentLabel")
content_label.setWordWrap(True)
content_label.setTextInteractionFlags(
Qt.TextSelectableByMouse
)

content_layout.addWidget(content_label)

# 添加到卡片
card_layout.addWidget(header)
card_layout.addWidget(content_widget)

# 卡片样式
self.setStyleSheet("""
QFrame#clipboardCard {
background-color: transparent;
border: 1px solid #DCE3EC;
border-radius: 12px;
}

QFrame#cardHeader {
background-color: #08BDE8;
border-top-left-radius: 11px;
border-top-right-radius: 11px;
}

QLabel#typeLabel {
color: #073B4C;
font-size: 14px;
font-weight: 700;
}

QLabel#timeLabel {
color: #073B4C;
font-size: 12px;
}

QFrame#cardContent {
background-color: #FFFFFF;
border-bottom-left-radius: 11px;
border-bottom-right-radius: 11px;
}

QLabel#contentLabel {
color: #263244;
font-size: 13px;
line-height: 1.5;
}
""")


class ClipboardWindow(QMainWindow):
"""vPaste Pro 主窗口"""

def __init__(self):
super().__init__()

self.setWindowTitle("vPaste Pro - 剪贴板管理器")
self.resize(900, 650)
self.setMinimumSize(680, 450)

self.last_clipboard_text = ""
self.clipboard = QApplication.clipboard()

self.init_ui()

# 监听剪贴板变化
self.clipboard.dataChanged.connect(
self.on_clipboard_changed
)

def init_ui(self):
central_widget = QWidget()
central_widget.setObjectName("centralWidget")
self.setCentralWidget(central_widget)

main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(28, 24, 28, 24)
main_layout.setSpacing(16)

# 页面标题
title_label = QLabel("vPaste Pro")
title_label.setObjectName("titleLabel")

subtitle_label = QLabel(
"复制的内容会自动保存到历史记录"
)
subtitle_label.setObjectName("subtitleLabel")

history_title = QLabel("剪贴板历史")
history_title.setObjectName("sectionTitle")

# 历史记录列表
self.history_list = QListWidget()
self.history_list.setObjectName("historyList")
self.history_list.setSpacing(12)
self.history_list.setFrameShape(QFrame.NoFrame)
self.history_list.setVerticalScrollMode(
QListWidget.ScrollPerPixel
)

# 空记录提示
self.empty_label = QLabel(
"还没有剪贴板记录\n\n请尝试复制一段文字"
)
self.empty_label.setObjectName("emptyLabel")
self.empty_label.setAlignment(Qt.AlignCenter)

main_layout.addWidget(title_label)
main_layout.addWidget(subtitle_label)
main_layout.addSpacing(12)
main_layout.addWidget(history_title)
main_layout.addWidget(self.history_list)
main_layout.addWidget(self.empty_label)

self.history_list.hide()

self.setStyleSheet("""
QMainWindow,
QWidget#centralWidget {
background-color: #F3F5F8;
}

QLabel#titleLabel {
color: #172B4D;
font-size: 28px;
font-weight: 700;
}

QLabel#subtitleLabel {
color: #718096;
font-size: 13px;
}

QLabel#sectionTitle {
color: #172B4D;
font-size: 17px;
font-weight: 700;
}

QListWidget#historyList {
background-color: transparent;
border: none;
outline: none;
}

QListWidget#historyList::item {
background-color: transparent;
border: none;
}

QLabel#emptyLabel {
background-color: #FFFFFF;
color: #8995A5;
border: 2px dashed #D5DEE8;
border-radius: 14px;
font-size: 15px;
padding: 30px;
}

QScrollBar:vertical {
background: transparent;
width: 8px;
margin: 0;
}

QScrollBar::handle:vertical {
background: #B9C5D3;
border-radius: 4px;
min-height: 30px;
}

QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
height: 0;
}
""")

def on_clipboard_changed(self):
"""监听剪贴板变化"""

text = self.clipboard.text()

if not text or not text.strip():
return

if text == self.last_clipboard_text:
return

self.last_clipboard_text = text

self.add_history_item(text)

def add_history_item(self, text):
"""创建并添加历史记录卡片"""

current_time = datetime.now().strftime(
"%H:%M:%S"
)

# 限制卡片预览内容长度
display_content = text.strip()

if len(display_content) > 500:
display_content = (
display_content[:500] + "..."
)

# 创建列表项
item = QListWidgetItem()

# 创建自定义卡片
card = ClipboardCard(
display_content,
current_time
)

# 先添加列表项,再设置卡片
self.history_list.insertItem(0, item)
self.history_list.setItemWidget(item, card)

# 根据卡片实际高度设置列表项高度
item.setSizeHint(card.sizeHint())

# 隐藏空状态
self.empty_label.hide()
self.history_list.show()


def main():
app = QApplication(sys.argv)
app.setApplicationName("vPaste Pro")

window = ClipboardWindow()
window.show()

sys.exit(app.exec_())


if __name__ == "__main__":
main()

5

图片剪贴板

剪贴板支持图片粘贴,并生成图片历史卡片记录。


import sys
from datetime import datetime

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QLabel,
QVBoxLayout,
QHBoxLayout,
QListWidget,
QListWidgetItem,
QFrame,
QSizePolicy,
)


class ClipboardCard(QFrame):
"""剪贴板历史记录卡片"""

def __init__(self, content, copy_time, content_type="text"):
super().__init__()

self.content = content
self.copy_time = copy_time
self.content_type = content_type

self.init_ui()

def init_ui(self):
self.setObjectName("clipboardCard")
self.setSizePolicy(
QSizePolicy.Expanding,
QSizePolicy.Minimum
)

card_layout = QVBoxLayout(self)
card_layout.setContentsMargins(0, 0, 0, 0)
card_layout.setSpacing(0)

# 根据内容类型设置主题色
if self.content_type == "image":
theme_color = "#FF9F0A"
title_color = "#432400"
type_text = "图片"
else:
theme_color = "#08BDE8"
title_color = "#073B4C"
type_text = "文本"

# 顶部区域
header = QFrame()
header.setObjectName("cardHeader")
header.setStyleSheet(
f"background-color: {theme_color};"
)

header_layout = QHBoxLayout(header)
header_layout.setContentsMargins(16, 10, 16, 10)

type_label = QLabel(type_text)
type_label.setStyleSheet(
f"color: {title_color}; font-size: 14px; "
"font-weight: 700;"
)

time_label = QLabel(self.copy_time)
time_label.setStyleSheet(
f"color: {title_color}; font-size: 12px;"
)

header_layout.addWidget(type_label)
header_layout.addStretch()
header_layout.addWidget(time_label)

# 内容区域
content_widget = QFrame()
content_widget.setObjectName("cardContent")

content_layout = QVBoxLayout(content_widget)
content_layout.setContentsMargins(16, 14, 16, 14)

if self.content_type == "image":
# 图片缩略图
image_label = QLabel()
image_label.setAlignment(Qt.AlignCenter)
image_label.setObjectName("imageLabel")

pixmap = self.content

thumbnail = pixmap.scaled(
240,
160,
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)

image_label.setPixmap(thumbnail)
image_label.setMinimumHeight(160)

content_layout.addWidget(image_label)

description_label = QLabel("剪贴板图片")
description_label.setAlignment(Qt.AlignCenter)
description_label.setObjectName(
"descriptionLabel"
)

content_layout.addWidget(description_label)

else:
# 文本内容
content_label = QLabel(self.content)
content_label.setObjectName("contentLabel")
content_label.setWordWrap(True)
content_label.setTextInteractionFlags(
Qt.TextSelectableByMouse
)

content_layout.addWidget(content_label)

card_layout.addWidget(header)
card_layout.addWidget(content_widget)

self.setStyleSheet("""
QFrame#clipboardCard {
background-color: transparent;
border: 1px solid #DCE3EC;
border-radius: 12px;
}

QFrame#cardHeader {
border-top-left-radius: 11px;
border-top-right-radius: 11px;
}

QFrame#cardContent {
background-color: #FFFFFF;
border-bottom-left-radius: 11px;
border-bottom-right-radius: 11px;
}

QLabel#contentLabel {
color: #263244;
font-size: 13px;
}

QLabel#imageLabel {
background-color: #FFF6E8;
border-radius: 8px;
}

QLabel#descriptionLabel {
color: #8A94A3;
font-size: 12px;
}
""")


class ClipboardWindow(QMainWindow):

def __init__(self):
super().__init__()

self.setWindowTitle("vPaste Pro - 剪贴板管理器")
self.resize(900, 650)
self.setMinimumSize(680, 450)

self.last_clipboard_text = ""
self.last_clipboard_image = None

self.clipboard = QApplication.clipboard()

self.init_ui()

self.clipboard.dataChanged.connect(
self.on_clipboard_changed
)

def init_ui(self):
central_widget = QWidget()
central_widget.setObjectName("centralWidget")
self.setCentralWidget(central_widget)

main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(28, 24, 28, 24)
main_layout.setSpacing(16)

title_label = QLabel("vPaste Pro")
title_label.setObjectName("titleLabel")

subtitle_label = QLabel(
"自动捕获文本和图片剪贴板内容"
)
subtitle_label.setObjectName("subtitleLabel")

history_title = QLabel("剪贴板历史")
history_title.setObjectName("sectionTitle")

self.history_list = QListWidget()
self.history_list.setObjectName("historyList")
self.history_list.setSpacing(12)
self.history_list.setFrameShape(QFrame.NoFrame)
self.history_list.setVerticalScrollMode(
QListWidget.ScrollPerPixel
)

self.empty_label = QLabel(
"还没有剪贴板记录\n\n请尝试复制文字或图片"
)
self.empty_label.setObjectName("emptyLabel")
self.empty_label.setAlignment(Qt.AlignCenter)

main_layout.addWidget(title_label)
main_layout.addWidget(subtitle_label)
main_layout.addSpacing(12)
main_layout.addWidget(history_title)
main_layout.addWidget(self.history_list)
main_layout.addWidget(self.empty_label)

self.history_list.hide()

self.setStyleSheet("""
QMainWindow,
QWidget#centralWidget {
background-color: #F3F5F8;
}

QLabel#titleLabel {
color: #172B4D;
font-size: 28px;
font-weight: 700;
}

QLabel#subtitleLabel {
color: #718096;
font-size: 13px;
}

QLabel#sectionTitle {
color: #172B4D;
font-size: 17px;
font-weight: 700;
}

QListWidget#historyList {
background-color: transparent;
border: none;
outline: none;
}

QListWidget#historyList::item {
background-color: transparent;
border: none;
}

QLabel#emptyLabel {
background-color: #FFFFFF;
color: #8995A5;
border: 2px dashed #D5DEE8;
border-radius: 14px;
font-size: 15px;
padding: 30px;
}

QScrollBar:vertical {
background: transparent;
width: 8px;
}

QScrollBar::handle:vertical {
background: #B9C5D3;
border-radius: 4px;
min-height: 30px;
}

QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
height: 0;
}
""")

def on_clipboard_changed(self):
"""剪贴板变化监听"""

# 优先判断图片
image = self.clipboard.image()

if not image.isNull():
pixmap = QPixmap.fromImage(image)

# 根据图片尺寸生成唯一标识
image_key = (
pixmap.width(),
pixmap.height(),
pixmap.cacheKey()
)

if image_key == self.last_clipboard_image:
return

self.last_clipboard_image = image_key
self.last_clipboard_text = ""

self.add_history_item(
pixmap,
content_type="image"
)
return

# 处理文本
text = self.clipboard.text()

if not text or not text.strip():
return

if text == self.last_clipboard_text:
return

self.last_clipboard_text = text
self.last_clipboard_image = None

self.add_history_item(
text,
content_type="text"
)

def add_history_item(self, content, content_type):
"""添加历史记录"""

current_time = datetime.now().strftime(
"%H:%M:%S"
)

if content_type == "text":
display_content = content.strip()

if len(display_content) > 500:
display_content = (
display_content[:500] + "..."
)
else:
display_content = content

item = QListWidgetItem()

card = ClipboardCard(
display_content,
current_time,
content_type
)

self.history_list.insertItem(0, item)
self.history_list.setItemWidget(item, card)

item.setSizeHint(card.sizeHint())

self.empty_label.hide()
self.history_list.show()


def main():
app = QApplication(sys.argv)
app.setApplicationName("vPaste Pro")

window = ClipboardWindow()
window.show()

sys.exit(app.exec_())


if __name__ == "__main__":
main()

6

文件剪贴板

复制文件同样会进入到这个剪贴板内。


import sys
import os
import re
import subprocess
from datetime import datetime

from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QListWidget,
QListWidgetItem,
QFrame,
QPushButton,
QMessageBox,
)
from PyQt5.QtCore import Qt, QSize, QUrl
from PyQt5.QtGui import QPixmap, QImage, QDesktopServices


# =========================
# 剪贴板卡片
# =========================

class ClipboardCard(QFrame):

def __init__(
self,
content_type,
content,
create_time,
callback=None,
parent=None
):
super().__init__(parent)

self.content_type = content_type
self.content = content
self.callback = callback

self.setObjectName("clipboardCard")

self.init_ui(create_time)

def init_ui(self, create_time):

main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(14, 12, 14, 12)
main_layout.setSpacing(10)

# 顶部信息
header = QHBoxLayout()

type_label = QLabel(self.get_type_name())
type_label.setStyleSheet(
"font-size: 14px; font-weight: bold; color: white;"
)

time_label = QLabel(create_time)
time_label.setStyleSheet(
"font-size: 11px; color: #D1D5DB;"
)

header.addWidget(type_label)
header.addStretch()
header.addWidget(time_label)

main_layout.addLayout(header)

# 不同类型的内容展示
if self.content_type == "text":
self.create_text_content(main_layout)

elif self.content_type == "image":
self.create_image_content(main_layout)

elif self.content_type == "file":
self.create_file_content(main_layout)

def get_type_name(self):

names = {
"text": "文本记录",
"image": "图片记录",
"file": "文件记录",
}

return names.get(self.content_type, "剪贴板记录")

def create_text_content(self, layout):

text_label = QLabel(self.content)
text_label.setWordWrap(True)
text_label.setTextInteractionFlags(
Qt.TextSelectableByMouse
)

text_label.setStyleSheet(
"font-size: 13px; color: #263238;"
)

layout.addWidget(text_label)

def create_image_content(self, layout):

image_label = QLabel()
image_label.setAlignment(Qt.AlignCenter)

pixmap = QPixmap.fromImage(self.content)

pixmap = pixmap.scaled(
280,
180,
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)

image_label.setPixmap(pixmap)

layout.addWidget(image_label)

def create_file_content(self, layout):

file_paths = self.content

file_count = len(file_paths)
folder_count = sum(
1 for path in file_paths if os.path.isdir(path)
)
normal_file_count = file_count - folder_count

summary = QLabel(
f"共 {file_count} 项 | "
f"文件 {normal_file_count} 个 | "
f"文件夹 {folder_count} 个"
)

summary.setStyleSheet(
"font-size: 12px; color: #CBD5E1;"
)

layout.addWidget(summary)

# 展示文件列表
for path in file_paths[:5]:

file_layout = QHBoxLayout()

icon_label = QLabel(
"📁" if os.path.isdir(path) else "📄"
)

icon_label.setStyleSheet("font-size: 20px;")

name_label = QLabel(
os.path.basename(path.rstrip(os.sep)) or path
)

name_label.setStyleSheet(
"font-size: 13px; color: white; font-weight: bold;"
)

file_layout.addWidget(icon_label)
file_layout.addWidget(name_label)
file_layout.addStretch()

layout.addLayout(file_layout)

path_label = QLabel(path)
path_label.setWordWrap(True)
path_label.setTextInteractionFlags(
Qt.TextSelectableByMouse
)

path_label.setStyleSheet(
"font-size: 10px; color: #AAB7C4;"
)

layout.addWidget(path_label)

if file_count > 5:

more_label = QLabel(
f"还有 {file_count - 5} 个文件未显示"
)

more_label.setStyleSheet(
"font-size: 11px; color: #CBD5E1;"
)

layout.addWidget(more_label)

# 文件专属操作按钮
button_layout = QHBoxLayout()
button_layout.addStretch()

open_button = QPushButton("打开")
reveal_button = QPushButton("在 Finder 中显示")

open_button.setObjectName("fileOpenButton")
reveal_button.setObjectName("fileRevealButton")

open_button.clicked.connect(
lambda: self.open_file(file_paths[0])
)

reveal_button.clicked.connect(
lambda: self.reveal_file(file_paths[0])
)

button_layout.addWidget(open_button)
button_layout.addWidget(reveal_button)

layout.addLayout(button_layout)

def open_file(self, path):

if not os.path.exists(path):
QMessageBox.warning(
self,
"提示",
"文件不存在,可能已经被移动或删除。"
)
return

QDesktopServices.openUrl(
QUrl.fromLocalFile(path)
)

def reveal_file(self, path):

if not os.path.exists(path):
QMessageBox.warning(
self,
"提示",
"文件不存在,可能已经被移动或删除。"
)
return

try:

if sys.platform == "darwin":
subprocess.Popen(["open", "-R", path])

elif sys.platform.startswith("win"):
subprocess.Popen(
["explorer", "/select,", os.path.normpath(path)]
)

else:
target = path if os.path.isdir(path) else os.path.dirname(path)
QDesktopServices.openUrl(
QUrl.fromLocalFile(target)
)

except Exception as error:

QMessageBox.warning(
self,
"操作失败",
str(error)
)


# =========================
# 主窗口
# =========================

class ClipboardWindow(QMainWindow):

def __init__(self):

super().__init__()

self.setWindowTitle("vPaste Pro")
self.resize(520, 680)

# 剪贴板去重标识
self.last_clipboard_key = None

self.init_ui()

# 监听系统剪贴板
self.clipboard = QApplication.clipboard()
self.clipboard.dataChanged.connect(
self.on_clipboard_changed
)

def init_ui(self):

central_widget = QWidget()
self.setCentralWidget(central_widget)

main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(20, 20, 20, 20)
main_layout.setSpacing(15)

# 页面标题
title = QLabel("vPaste Pro")

title.setStyleSheet("""
font-size: 24px;
font-weight: bold;
color: #172126;
""")

subtitle = QLabel("简单、高效的剪贴板管理工具")

subtitle.setStyleSheet("""
font-size: 12px;
color: #84929B;
""")

main_layout.addWidget(title)
main_layout.addWidget(subtitle)

# 提示信息
self.status_label = QLabel(
"复制文本、图片或 Finder 文件,即可自动记录"
)

self.status_label.setWordWrap(True)

self.status_label.setStyleSheet("""
background: #DFF7FC;
color: #087E9C;
padding: 12px;
border-radius: 8px;
font-size: 12px;
""")

main_layout.addWidget(self.status_label)

# 剪贴板记录列表
self.clipboard_list = QListWidget()

self.clipboard_list.setSpacing(12)
self.clipboard_list.setFrameShape(QFrame.NoFrame)

self.clipboard_list.setStyleSheet("""
QListWidget {
background: transparent;
border: none;
}

QListWidget::item {
border: none;
}

QScrollBar:vertical {
width: 6px;
background: transparent;
}

QScrollBar::handle:vertical {
background: #C5D1D7;
border-radius: 3px;
}
""")

main_layout.addWidget(self.clipboard_list)

self.setStyleSheet("""
QMainWindow {
background: #F4F7F8;
}

QFrame#clipboardCard {
border-radius: 12px;
background: white;
}

QPushButton {
border: none;
border-radius: 6px;
padding: 7px 14px;
font-size: 12px;
font-weight: bold;
}

QPushButton#fileOpenButton {
background: #08BDE8;
color: #10232B;
}

QPushButton#fileRevealButton {
background: #FF9F0A;
color: #30200A;
}

QPushButton:hover {
opacity: 0.8;
}
""")

# =========================
# 剪贴板监听
# =========================

def on_clipboard_changed(self):

mime_data = self.clipboard.mimeData()

if mime_data is None:
return

# 第一步:优先识别 Finder 文件
if mime_data.hasUrls():

file_paths = []

for url in mime_data.urls():

if not url.isLocalFile():
continue

path = url.toLocalFile()

if os.path.exists(path):
file_paths.append(path)

if file_paths:

# 去重
file_key = ("file", tuple(file_paths))

if file_key == self.last_clipboard_key:
return

self.last_clipboard_key = file_key

self.add_record(
"file",
file_paths
)

return

# 第二步:识别图片
image = self.clipboard.image()

if not image.isNull():

image_key = (
"image",
image.width(),
image.height(),
image.cacheKey()
)

if image_key == self.last_clipboard_key:
return

self.last_clipboard_key = image_key

self.add_record(
"image",
image.copy()
)

return

# 第三步:识别文本
if mime_data.hasText():

text = mime_data.text().strip()

if not text:
return

text_key = ("text", text)

if text_key == self.last_clipboard_key:
return

self.last_clipboard_key = text_key

self.add_record(
"text",
text
)

# =========================
# 添加剪贴板记录
# =========================

def add_record(self, content_type, content):

create_time = datetime.now().strftime(
"%H:%M:%S"
)

card = ClipboardCard(
content_type,
content,
create_time
)

item = QListWidgetItem()

item.setSizeHint(
QSize(0, card.sizeHint().height() + 8)
)

# 最新记录显示在最上方
self.clipboard_list.insertItem(0, item)

self.clipboard_list.setItemWidget(
item,
card
)

# 控制记录数量
while self.clipboard_list.count() > 100:

self.clipboard_list.takeItem(
self.clipboard_list.count() - 1
)

type_names = {
"text": "文本",
"image": "图片",
"file": "文件",
}

self.status_label.setText(
f"已记录一条{type_names[content_type]}内容"
)


# =========================
# 程序入口
# =========================

if __name__ == "__main__":

app = QApplication(sys.argv)

window = ClipboardWindow()
window.show()

sys.exit(app.exec_())

7

顶部筛选

此版本新增了筛选功能,可按复制内容的类型进行分类。

import sys
import os
import re
import hashlib
import subprocess
from datetime import datetime

from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QListWidget,
QListWidgetItem,
QFrame,
QPushButton,
QMessageBox,
)

from PyQt5.QtCore import Qt, QSize, QUrl
from PyQt5.QtGui import QPixmap, QImage, QDesktopServices


class ClipboardCard(QFrame):

def __init__(self, record, copy_callback, parent=None):
super().__init__(parent)

self.record = record
self.copy_callback = copy_callback

self.setObjectName("clipboardCard")

self.init_ui()

def init_ui(self):

layout = QVBoxLayout(self)
layout.setContentsMargins(14, 12, 14, 12)
layout.setSpacing(10)

content_type = self.record["type"]

type_names = {
"text": "文本记录",
"link": "链接记录",
"image": "图片记录",
"file": "文件记录",
}

header = QHBoxLayout()

title = QLabel(type_names[content_type])
title.setStyleSheet("""
font-size: 14px;
font-weight: bold;
color: white;
""")

time_label = QLabel(self.record["time"])
time_label.setStyleSheet("""
font-size: 11px;
color: #CBD5E1;
""")

header.addWidget(title)
header.addStretch()
header.addWidget(time_label)

layout.addLayout(header)

if content_type in ("text", "link"):
self.create_text_content(layout)

elif content_type == "image":
self.create_image_content(layout)

elif content_type == "file":
self.create_file_content(layout)

# =========================
# 文本和链接
# =========================

def create_text_content(self, layout):

content_label = QLabel(self.record["content"])
content_label.setWordWrap(True)

content_label.setTextInteractionFlags(
Qt.TextSelectableByMouse
)

content_label.setStyleSheet("""
font-size: 13px;
color: #263238;
""")

layout.addWidget(content_label)

button_text = (
"复制链接"
if self.record["type"] == "link"
else "复制内容"
)

button = QPushButton(button_text)
button.setObjectName("copyButton")

button.clicked.connect(
lambda: self.copy_callback(self.record)
)

self.add_action_button(layout, button)

# =========================
# 图片
# =========================

def create_image_content(self, layout):

image = self.record["content"]

image_label = QLabel()
image_label.setAlignment(Qt.AlignCenter)

pixmap = QPixmap.fromImage(image)

if pixmap.isNull():
image_label.setText("图片无法显示")
layout.addWidget(image_label)
return

pixmap = pixmap.scaled(
300,
180,
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)

image_label.setPixmap(pixmap)

layout.addWidget(image_label)

button = QPushButton("复制图片")
button.setObjectName("copyButton")

button.clicked.connect(
lambda: self.copy_callback(self.record)
)

self.add_action_button(layout, button)

# =========================
# 文件
# =========================

def create_file_content(self, layout):

file_paths = self.record["content"]

folder_count = sum(
1 for path in file_paths
if os.path.isdir(path)
)

file_count = len(file_paths) - folder_count

summary = QLabel(
f"共 {len(file_paths)} 项 | "
f"文件 {file_count} 个 | "
f"文件夹 {folder_count} 个"
)

summary.setStyleSheet("""
font-size: 12px;
color: #CBD5E1;
""")

layout.addWidget(summary)

for path in file_paths[:5]:

name = (
os.path.basename(path.rstrip(os.sep))
or path
)

icon = "📁" if os.path.isdir(path) else "📄"

name_label = QLabel(f"{icon} {name}")
name_label.setWordWrap(True)

name_label.setStyleSheet("""
font-size: 13px;
color: white;
font-weight: bold;
""")

layout.addWidget(name_label)

path_label = QLabel(path)
path_label.setWordWrap(True)

path_label.setTextInteractionFlags(
Qt.TextSelectableByMouse
)

path_label.setStyleSheet("""
font-size: 10px;
color: #AAB7C4;
""")

layout.addWidget(path_label)

if len(file_paths) > 5:

more_label = QLabel(
f"还有 {len(file_paths) - 5} 个项目未显示"
)

more_label.setStyleSheet("""
font-size: 11px;
color: #CBD5E1;
""")

layout.addWidget(more_label)

# 只有文件卡片显示文件操作按钮
button_layout = QHBoxLayout()
button_layout.addStretch()

open_button = QPushButton("打开")
open_button.setObjectName("fileOpenButton")

reveal_button = QPushButton("在 Finder 中显示")
reveal_button.setObjectName("fileRevealButton")

open_button.clicked.connect(
lambda: self.open_file(file_paths[0])
)

reveal_button.clicked.connect(
lambda: self.reveal_file(file_paths[0])
)

button_layout.addWidget(open_button)
button_layout.addWidget(reveal_button)

layout.addLayout(button_layout)

def add_action_button(self, layout, button):

button_layout = QHBoxLayout()
button_layout.addStretch()
button_layout.addWidget(button)

layout.addLayout(button_layout)

# =========================
# 文件操作
# =========================

def open_file(self, path):

if not os.path.exists(path):
QMessageBox.warning(
self,
"提示",
"文件不存在或已经被删除。"
)
return

QDesktopServices.openUrl(
QUrl.fromLocalFile(path)
)

def reveal_file(self, path):

if not os.path.exists(path):
QMessageBox.warning(
self,
"提示",
"文件不存在或已经被删除。"
)
return

try:

if sys.platform == "darwin":

subprocess.Popen(["open", "-R", path])

elif sys.platform.startswith("win"):

subprocess.Popen([
"explorer",
"/select,",
os.path.normpath(path)
])

else:

target = (
path
if os.path.isdir(path)
else os.path.dirname(path)
)

QDesktopServices.openUrl(
QUrl.fromLocalFile(target)
)

except Exception as error:

QMessageBox.warning(
self,
"操作失败",
str(error)
)


class ClipboardWindow(QMainWindow):

def __init__(self):

super().__init__()

self.setWindowTitle("vPaste Pro")
self.resize(560, 720)

self.records = []

self.current_category = "all"

self.last_clipboard_key = None

self.category_buttons = {}

self.init_ui()

self.clipboard = QApplication.clipboard()

self.clipboard.dataChanged.connect(
self.on_clipboard_changed
)

# =========================
# 初始化界面
# =========================

def init_ui(self):

central_widget = QWidget()
self.setCentralWidget(central_widget)

main_layout = QVBoxLayout(central_widget)

main_layout.setContentsMargins(
20, 20, 20, 20
)

main_layout.setSpacing(14)

title = QLabel("vPaste Pro")

title.setStyleSheet("""
font-size: 25px;
font-weight: bold;
color: #172126;
""")

subtitle = QLabel(
"简单、高效的剪贴板管理工具"
)

subtitle.setStyleSheet("""
font-size: 12px;
color: #84929B;
""")

main_layout.addWidget(title)
main_layout.addWidget(subtitle)

self.status_label = QLabel(
"复制文本、链接、图片或文件,自动保存记录"
)

self.status_label.setWordWrap(True)

self.status_label.setStyleSheet("""
background: #DFF7FC;
color: #087E9C;
padding: 12px;
border-radius: 8px;
font-size: 12px;
""")

main_layout.addWidget(self.status_label)

# 分类栏
category_layout = QHBoxLayout()
category_layout.setSpacing(8)

categories = [
("all", "全部"),
("text", "文本"),
("link", "链接"),
("image", "图片"),
("file", "文件"),
]

for category, name in categories:

button = QPushButton(name)

button.setCheckable(True)
button.setObjectName("categoryButton")

button.clicked.connect(
lambda checked, value=category:
self.switch_category(value)
)

self.category_buttons[category] = button

category_layout.addWidget(button)

main_layout.addLayout(category_layout)

# 记录列表
self.clipboard_list = QListWidget()

self.clipboard_list.setSpacing(12)
self.clipboard_list.setFrameShape(QFrame.NoFrame)

self.clipboard_list.setStyleSheet("""
QListWidget {
background: transparent;
border: none;
}

QListWidget::item {
border: none;
}

QScrollBar:vertical {
width: 6px;
background: transparent;
}

QScrollBar::handle:vertical {
background: #C5D1D7;
border-radius: 3px;
}
""")

main_layout.addWidget(self.clipboard_list)

self.setStyleSheet("""
QMainWindow {
background: #F4F7F8;
}

QPushButton {
border: none;
border-radius: 6px;
padding: 8px 12px;
font-size: 11px;
font-weight: bold;
}

QPushButton#categoryButton {
background: #E5EAED;
color: #52616B;
}

QPushButton#categoryButton:checked {
background: #172126;
color: white;
}

QPushButton#copyButton {
background: #08BDE8;
color: #10232B;
}

QPushButton#fileOpenButton {
background: #08BDE8;
color: #10232B;
}

QPushButton#fileRevealButton {
background: #FF9F0A;
color: #30200A;
}
""")

self.update_category_buttons()

# =========================
# 分类统计
# =========================

def update_category_buttons(self):

counts = {
"all": len(self.records),
"text": 0,
"link": 0,
"image": 0,
"file": 0,
}

for record in self.records:
counts[record["type"]] += 1

names = {
"all": "全部",
"text": "文本",
"link": "链接",
"image": "图片",
"file": "文件",
}

for category, button in self.category_buttons.items():

button.setText(
f"{names[category]} {counts[category]}"
)

button.setChecked(
self.current_category == category
)

# =========================
# 分类切换
# =========================

def switch_category(self, category):

self.current_category = category

self.update_category_buttons()
self.render_records()

# =========================
# 渲染记录
# =========================

def render_records(self):

self.clipboard_list.clear()

for record in self.records:

if (
self.current_category != "all"
and record["type"] != self.current_category
):
continue

self.add_card_to_list(record)

def add_card_to_list(self, record):

card = ClipboardCard(
record,
self.copy_record
)

background_colors = {
"text": "#08BDE8",
"link": "#3978E8",
"image": "#FF9F0A",
"file": "#24313A",
}

card.setStyleSheet(f"""
QFrame#clipboardCard {{
background: {background_colors[record["type"]]};
border-radius: 12px;
}}
""")

item = QListWidgetItem()

item.setSizeHint(
QSize(0, card.sizeHint().height() + 8)
)

self.clipboard_list.addItem(item)

self.clipboard_list.setItemWidget(
item,
card
)

# =========================
# 重新复制
# =========================

def copy_record(self, record):

content_type = record["type"]
content = record["content"]

if content_type in ("text", "link"):

self.clipboard.setText(content)

elif content_type == "image":

self.clipboard.setImage(content)

else:
return

self.status_label.setText(
"已重新复制" + {
"text": "文本内容",
"link": "链接",
"image": "图片",
}[content_type]
)

# =========================
# 读取图片
# =========================

def get_clipboard_image(self, mime_data):

try:

if mime_data.hasImage():

image_data = mime_data.imageData()

if isinstance(image_data, QImage):

if not image_data.isNull():
return image_data.copy()

elif isinstance(image_data, QPixmap):

if not image_data.isNull():
return image_data.toImage()

elif image_data is not None:

try:
image_bytes = bytes(image_data)
image = QImage.fromData(image_bytes)

if not image.isNull():
return image.copy()

except (TypeError, ValueError):
pass

except Exception as error:

print("MIME 图片读取异常:", repr(error))

# 备用方式:直接读取系统剪贴板图片
try:

image = self.clipboard.image()

if not image.isNull():
return image.copy()

except Exception as error:

print("系统剪贴板图片读取异常:", repr(error))

return None

# =========================
# 图片去重标识
# =========================

def get_image_key(self, image):

image = image.convertToFormat(
QImage.Format_RGBA8888
)

byte_count = image.byteCount()

bits = image.bits()
bits.setsize(byte_count)

image_bytes = bits.asstring(byte_count)

image_hash = hashlib.sha1(
image_bytes
).hexdigest()

return (
"image",
image.width(),
image.height(),
image_hash
)

# =========================
# 识别本地文件
# =========================

def get_local_file_paths(self, mime_data):

if not mime_data.hasUrls():
return []

file_paths = []

for url in mime_data.urls():

if not url.isLocalFile():
continue

path = os.path.abspath(
url.toLocalFile()
)

if os.path.exists(path):
file_paths.append(path)

# 去重,同时保留原顺序
return list(dict.fromkeys(file_paths))

# =========================
# 剪贴板识别
# =========================

def on_clipboard_changed(self):

mime_data = self.clipboard.mimeData()

if mime_data is None:
return

print("\n========== 剪贴板变化 ==========")
print("MIME 类型:", mime_data.formats())
print("hasUrls:", mime_data.hasUrls())
print("hasImage:", mime_data.hasImage())
print("hasText:", mime_data.hasText())

# ---------------------------------
# 1. 优先识别本地文件
# ---------------------------------

file_paths = self.get_local_file_paths(
mime_data
)

if file_paths:

file_key = (
"file",
tuple(file_paths)
)

if file_key == self.last_clipboard_key:
return

self.last_clipboard_key = file_key

self.add_record(
"file",
file_paths
)

print("识别结果:本地文件")
return

# ---------------------------------
# 2. 识别图片
# ---------------------------------

image = self.get_clipboard_image(
mime_data
)

if image is not None and not image.isNull():

image_key = self.get_image_key(image)

if image_key == self.last_clipboard_key:
return

self.last_clipboard_key = image_key

self.add_record(
"image",
image.copy()
)

print(
"识别结果:图片",
image.width(),
image.height()
)

return

# ---------------------------------
# 3. 识别文本和链接
# ---------------------------------

if mime_data.hasText():

text = mime_data.text().strip()

if not text:
return

content_type = (
"link"
if self.is_url(text)
else "text"
)

text_key = (
content_type,
text
)

if text_key == self.last_clipboard_key:
return

self.last_clipboard_key = text_key

self.add_record(
content_type,
text
)

print("识别结果:", content_type)

# =========================
# 链接识别
# =========================

def is_url(self, text):

pattern = re.compile(
r"^(https?://|www\.)\S+$",
re.IGNORECASE
)

return bool(pattern.match(text))

# =========================
# 添加记录
# =========================

def add_record(self, content_type, content):

record = {
"id": datetime.now().timestamp(),
"type": content_type,
"content": content,
"time": datetime.now().strftime("%H:%M:%S"),
}

self.records.insert(0, record)

# 最多保留 100 条
self.records = self.records[:100]

self.update_category_buttons()
self.render_records()

names = {
"text": "文本",
"link": "链接",
"image": "图片",
"file": "文件",
}

self.status_label.setText(
f"已记录一条{names[content_type]}内容"
)


if __name__ == "__main__":

ap
阅读记录0
点赞0
收藏0
禁止 本文未经作者允许授权,禁止转载
猜你喜欢
评论/提问(已发布 0 条)
头像
评论 评论
收藏 收藏
分享 分享
pdf下载 下载
pdf下载 举报