clipboard/clipboard_monitor.py
2025-07-30 22:11:45 +08:00

304 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
剪贴板监控程序
该程序在后台运行实时监控Windows系统剪贴板的变化
当检测到剪贴板内容变化时,记录内容并输出提示。
支持文本和图片媒体的记录。
"""
import time
import threading
import sys
import os
import datetime
import uuid
import io
import pyperclip
from pystray import Icon, Menu, MenuItem
from PIL import Image, ImageDraw, ImageGrab
import tkinter as tk
from tkinter import messagebox
import logging
import win32clipboard
from io import BytesIO
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("clipboard_monitor.log", encoding='utf-8'),
logging.StreamHandler()
]
)
class ClipboardMonitor:
def __init__(self):
self.previous_clipboard = ""
self.previous_image_hash = None
self.monitoring = True
self.paused = False
self.log_file = "clipboard_history.txt"
self.media_folder = "clipboard_media"
# 确保媒体文件夹存在
if not os.path.exists(self.media_folder):
os.makedirs(self.media_folder)
self.setup_gui()
self.create_tray_icon()
def setup_gui(self):
"""设置GUI窗口但默认不显示"""
self.root = tk.Tk()
self.root.title("剪贴板监控")
self.root.geometry("400x300")
self.root.protocol("WM_DELETE_WINDOW", self.hide_window)
# 创建文本区域显示剪贴板历史
self.text_area = tk.Text(self.root, wrap=tk.WORD)
self.text_area.pack(expand=True, fill=tk.BOTH, padx=10, pady=10)
# 创建按钮框架
button_frame = tk.Frame(self.root)
button_frame.pack(fill=tk.X, padx=10, pady=5)
# 添加按钮
tk.Button(button_frame, text="暂停监控", command=self.toggle_pause).pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="清空历史", command=self.clear_history).pack(side=tk.LEFT, padx=5)
tk.Button(button_frame, text="退出程序", command=self.exit_app).pack(side=tk.RIGHT, padx=5)
# 默认隐藏窗口
self.root.withdraw()
def create_tray_icon(self):
"""创建系统托盘图标"""
# 创建图标
image = self.create_icon_image()
# 创建菜单
menu = Menu(
MenuItem('显示主窗口', self.show_window),
MenuItem('暂停/继续监控', self.toggle_pause),
MenuItem('清空历史记录', self.clear_history),
MenuItem('退出', self.exit_app)
)
# 创建托盘图标
self.icon = Icon("clipboard_monitor", image, "剪贴板监控", menu)
# 在单独的线程中运行图标
threading.Thread(target=self.icon.run, daemon=True).start()
def create_icon_image(self):
"""创建托盘图标图像"""
# 创建一个32x32的图像白色背景
image = Image.new('RGB', (32, 32), color=(255, 255, 255))
d = ImageDraw.Draw(image)
# 绘制剪贴板图标
d.rectangle([(8, 8), (24, 24)], outline=(0, 0, 0), width=2)
d.rectangle([(12, 4), (20, 8)], fill=(0, 0, 0))
return image
def show_window(self):
"""显示主窗口"""
self.root.deiconify()
self.root.lift()
self.update_text_area()
def hide_window(self):
"""隐藏主窗口"""
self.root.withdraw()
def toggle_pause(self):
"""切换暂停/继续监控状态"""
self.paused = not self.paused
status = "暂停" if self.paused else "继续"
logging.info(f"监控已{status}")
self.show_notification(f"剪贴板监控已{status}")
def clear_history(self):
"""清空历史记录"""
try:
with open(self.log_file, 'w', encoding='utf-8') as f:
f.write("")
self.update_text_area()
logging.info("历史记录已清空")
self.show_notification("历史记录已清空")
except Exception as e:
logging.error(f"清空历史记录失败: {e}")
def exit_app(self):
"""退出应用程序"""
self.monitoring = False
if hasattr(self, 'icon'):
self.icon.stop()
self.root.quit()
# 不使用sys.exit()避免引发SystemExit异常
# 让程序自然退出
def show_notification(self, message):
"""显示通知"""
if hasattr(self, 'icon'):
self.icon.notify(message)
def update_text_area(self):
"""更新文本区域内容"""
self.text_area.delete(1.0, tk.END)
try:
if os.path.exists(self.log_file):
with open(self.log_file, 'r', encoding='utf-8') as f:
content = f.read()
self.text_area.insert(tk.END, content)
except Exception as e:
logging.error(f"读取历史记录失败: {e}")
def save_clipboard_content(self, content, media_path=None):
"""保存剪贴板内容到文件"""
try:
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(self.log_file, 'a', encoding='utf-8') as f:
f.write(f"\n[{timestamp}]\n")
# 如果有媒体文件,添加引用
if media_path:
f.write(f"[图片文件: {media_path}]\n")
else:
f.write(f"{content}\n")
f.write(f"{'='*50}\n")
# 如果窗口可见,更新文本区域
if self.root.winfo_viewable():
self.update_text_area()
except Exception as e:
logging.error(f"保存剪贴板内容失败: {e}")
def get_clipboard_image(self):
"""尝试从剪贴板获取图片"""
try:
# 尝试使用PIL的ImageGrab获取剪贴板图片
image = ImageGrab.grabclipboard()
if isinstance(image, Image.Image):
return image
return None
except Exception as e:
logging.error(f"获取剪贴板图片失败: {e}")
return None
def save_image_to_file(self, image):
"""保存图片到文件"""
try:
# 生成唯一文件名
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{timestamp}_{uuid.uuid4().hex[:8]}.png"
filepath = os.path.join(self.media_folder, filename)
# 保存图片
image.save(filepath)
logging.info(f"图片已保存到: {filepath}")
return filepath
except Exception as e:
logging.error(f"保存图片失败: {e}")
return None
def get_image_hash(self, image):
"""获取图片的简单哈希值用于比较"""
if not image:
return None
try:
# 缩小图片以加快哈希计算
small_image = image.resize((32, 32), Image.LANCZOS)
# 转换为灰度
gray_image = small_image.convert("L")
# 获取像素数据
pixels = list(gray_image.getdata())
# 简单哈希:将像素值二值化并转为字符串
binary_pixels = ['1' if p > 128 else '0' for p in pixels]
return ''.join(binary_pixels)
except Exception as e:
logging.error(f"计算图片哈希失败: {e}")
return None
def monitor_clipboard(self):
"""监控剪贴板变化"""
try:
# 获取初始剪贴板内容
self.previous_clipboard = pyperclip.paste()
# 获取初始图片(如果有)
initial_image = self.get_clipboard_image()
if initial_image:
self.previous_image_hash = self.get_image_hash(initial_image)
except Exception as e:
self.previous_clipboard = ""
self.previous_image_hash = None
logging.error(f"获取初始剪贴板内容失败: {e}")
while self.monitoring:
try:
if not self.paused:
# 检查是否有图片
current_image = self.get_clipboard_image()
if current_image:
current_image_hash = self.get_image_hash(current_image)
# 如果图片变化了
if current_image_hash != self.previous_image_hash:
logging.info("检测到剪贴板图片变化")
self.show_notification("检测到新的剪贴板图片")
# 保存图片到文件
image_path = self.save_image_to_file(current_image)
if image_path:
# 保存引用到日志
self.save_clipboard_content("", image_path)
# 更新上一次的图片哈希
self.previous_image_hash = current_image_hash
else:
# 获取当前文本内容
current_clipboard = pyperclip.paste()
# 如果文本内容变化了
if current_clipboard != self.previous_clipboard:
logging.info("检测到剪贴板文本变化")
self.show_notification("检测到新的剪贴板内容")
# 保存新内容
self.save_clipboard_content(current_clipboard)
# 更新上一次的内容
self.previous_clipboard = current_clipboard
except Exception as e:
logging.error(f"监控剪贴板时出错: {e}")
# 休眠一段时间再检查
time.sleep(0.5)
def run(self):
"""运行监控程序"""
# 启动监控线程
monitor_thread = threading.Thread(target=self.monitor_clipboard, daemon=True)
monitor_thread.start()
# 显示启动通知
self.show_notification("剪贴板监控已启动")
logging.info("剪贴板监控程序已启动")
# 运行主循环
self.root.mainloop()
if __name__ == "__main__":
try:
monitor = ClipboardMonitor()
monitor.run()
except Exception as e:
logging.critical(f"程序启动失败: {e}")
messagebox.showerror("错误", f"程序启动失败: {e}")
sys.exit(1)