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

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
获取系统剪贴板




12
电脑复制文字,程序自动铺获内容并在窗口中历史列表记录中展示。
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
记录卡片




12
前几步以及实现了基础的窗口和文件监听,接下来修改内容的样式,样式为卡片。
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
qq空间
微博
复制链接
分享 更多相关项目
猜你喜欢
评论/提问(已发布 0 条)
0