[Python] 纯文本查看 复制代码
import sys, json, os
import pyperclip
from PyQt5 import QtWidgets, QtCore, QtGui
from pynput import keyboard
DATA_FILE = "clipboard_data.json"
# =========================
# 数据结构
# =========================
class ClipboardItem:
def __init__(self, text, count=0, pinned=False, timestamp=None):
self.text = text
self.count = count
self.pinned = pinned
self.timestamp = timestamp or QtCore.QDateTime.currentDateTime().toString()
# =========================
# 主程序
# =========================
class ClipboardManager(QtWidgets.QWidget):
def __init__(self):
super().__init__()
# ===== 无边框 + 置顶 + 透明 =====
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
self.resize(420, 320)
# ===== 状态 =====
self.dragPos = None
self.hidden_to_edge = False
self.edge_side = None
# =========================
# UI 主容器
# =========================
self.main = QtWidgets.QFrame(self)
self.layout = QtWidgets.QVBoxLayout(self.main)
self.layout.setContentsMargins(6, 6, 6, 6)
self.layout.setSpacing(6)
# =========================
# 顶部栏
# =========================
top = QtWidgets.QHBoxLayout()
self.search = QtWidgets.QLineEdit()
self.search.setPlaceholderText("搜索")
self.sort_box = QtWidgets.QComboBox()
self.sort_box.addItems(["次数", "时间"])
# 🎨 皮肤
self.skin_box = QtWidgets.QComboBox()
self.skin_box.addItems(["light", "dark", "blue"])
self.skin_box.currentTextChanged.connect(self.apply_skin)
close_btn = QtWidgets.QPushButton("✕")
close_btn.setFixedSize(24, 24)
close_btn.clicked.connect(self.close)
top.addWidget(self.search)
top.addWidget(self.sort_box)
top.addWidget(self.skin_box)
top.addWidget(close_btn)
self.layout.addLayout(top)
# =========================
# 表格
# =========================
self.table = QtWidgets.QTableWidget(0, 2)
self.table.setHorizontalHeaderLabels(["内容", "次"])
self.table.setFrameShape(QtWidgets.QFrame.NoFrame)
self.table.verticalHeader().setVisible(False)
self.table.setColumnWidth(0, 300)
self.table.setColumnWidth(1, 50)
self.layout.addWidget(self.table)
# 外层布局
wrapper = QtWidgets.QVBoxLayout(self)
wrapper.addWidget(self.main)
# =========================
# 数据
# =========================
self.data = []
self.current_text = ""
self.last_clipboard = ""
self.load_data()
# =========================
# 定时监听剪贴板
# =========================
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.check_clipboard)
self.timer.start(600)
# =========================
# UI事件
# =========================
self.search.textChanged.connect(self.refresh_table)
self.sort_box.currentIndexChanged.connect(self.refresh_table)
self.table.cellClicked.connect(self.select_item)
self.table.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.table.customContextMenuRequested.connect(self.menu)
# =========================
# Alt + Q 全局快捷键
# =========================
self.hotkey = keyboard.GlobalHotKeys({
'<alt>+q': self.toggle_window
})
self.hotkey.start()
# =========================
# 贴边检测
# =========================
self.edge_timer = QtCore.QTimer()
self.edge_timer.timeout.connect(self.edge_check)
self.edge_timer.start(300)
# 默认皮肤
self.apply_skin("light")
# =========================================================
# 🎨 换肤系统
# =========================================================
def apply_skin(self, skin):
if skin == "light":
bg = "rgba(255,255,255,190)"
text = "#222"
elif skin == "dark":
bg = "rgba(35,35,35,210)"
text = "#eee"
else:
bg = "rgba(180,210,255,180)"
text = "#111"
self.main.setStyleSheet(f"""
QFrame {{
background-color: {bg};
border-radius: 10px;
}}
""")
self.table.setStyleSheet(f"""
QTableWidget {{
background: transparent;
border: none;
color: {text};
gridline-color: rgba(0,0,0,40);
}}
QHeaderView::section {{
background: transparent;
border: none;
color: {text};
}}
QTableWidget::item {{
border: none;
padding: 4px;
}}
QTableWidget::item:hover {{
background: rgba(0,0,0,20);
}}
""")
# =========================================================
# 🧲 Alt+Q 显示/隐藏
# =========================================================
def toggle_window(self):
if self.isVisible():
self.hide()
else:
self.show()
self.raise_()
# =========================================================
# 🧲 贴边隐藏
# =========================================================
def edge_check(self):
if not self.isVisible():
return
geo = self.geometry()
screen = QtWidgets.QApplication.primaryScreen().geometry()
# 左边
if geo.x() <= 0:
self.edge_side = "left"
self.move(-self.width() + 5, geo.y())
self.hidden_to_edge = True
# 右边
elif geo.right() >= screen.width() - 1:
self.edge_side = "right"
self.move(screen.width() - 5, geo.y())
self.hidden_to_edge = True
# 鼠标唤出
pos = QtGui.QCursor.pos()
if self.hidden_to_edge:
if self.edge_side == "left" and pos.x() <= 20:
self.move(0, self.y())
self.hidden_to_edge = False
if self.edge_side == "right" and pos.x() >= screen.width() - 20:
self.move(screen.width() - self.width(), self.y())
self.hidden_to_edge = False
# =========================================================
# 🖱️ ⭐关键:拖动窗口(已修复)
# =========================================================
def mousePressEvent(self, event):
self.dragPos = event.globalPos()
def mouseMoveEvent(self, event):
if self.dragPos:
self.move(self.pos() + event.globalPos() - self.dragPos)
self.dragPos = event.globalPos()
# =========================================================
# 📋 剪贴板
# =========================================================
def check_clipboard(self):
text = pyperclip.paste()
if not text:
return
# 统计
if text != self.last_clipboard and text == self.current_text:
for i in self.data:
if i.text == text:
i.count += 1
break
self.last_clipboard = text
# 新增
if not any(i.text == text for i in self.data):
self.data.append(ClipboardItem(text))
self.save_data()
self.refresh_table()
# =========================================================
# 点击复制
# =========================================================
def select_item(self, row, col):
data = self.get_data()
self.current_text = data[row].text
pyperclip.copy(self.current_text)
# =========================================================
# 排序逻辑(置顶永远优先)
# =========================================================
def get_data(self):
keyword = self.search.text().lower()
data = [i for i in self.data if keyword in i.text.lower()]
pinned = [x for x in data if x.pinned]
normal = [x for x in data if not x.pinned]
if self.sort_box.currentIndex() == 0:
normal.sort(key=lambda x: -x.count)
else:
normal.sort(key=lambda x: x.timestamp, reverse=True)
return pinned + normal
# =========================================================
# 刷新表格
# =========================================================
def refresh_table(self):
data = self.get_data()
self.table.setRowCount(len(data))
for i, item in enumerate(data):
self.table.setItem(i, 0, QtWidgets.QTableWidgetItem(item.text))
self.table.setItem(i, 1, QtWidgets.QTableWidgetItem(str(item.count)))
# =========================================================
# 右键菜单
# =========================================================
def menu(self, pos):
row = self.table.currentRow()
if row < 0:
return
data = self.get_data()
item = data[row]
menu = QtWidgets.QMenu()
pin = menu.addAction("置顶/取消")
delete = menu.addAction("删除")
action = menu.exec_(self.table.viewport().mapToGlobal(pos))
if action == pin:
item.pinned = not item.pinned
elif action == delete:
self.data.remove(item)
self.save_data()
self.refresh_table()
# =========================================================
# 数据保存
# =========================================================
def save_data(self):
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump([i.__dict__ for i in self.data], f, ensure_ascii=False)
def load_data(self):
if not os.path.exists(DATA_FILE):
return
with open(DATA_FILE, "r", encoding="utf-8") as f:
self.data = [ClipboardItem(**i) for i in json.load(f)]
# =========================
# 启动
# =========================
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = ClipboardManager()
w.show()
sys.exit(app.exec_())