304 lines
11 KiB
Python
304 lines
11 KiB
Python
#!/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) |