本文摘要Python Windows桌面PDF小工具制作很早之前就想过做一个PDF的简单操作工具,因为现在基本的图片转PDF功能大多数软件都是要开会员,WPS就是说你的。最初的想法是做个小程序,后来想了想小程序实现这个功能也简单,但是需要用户上传PDF到服务器转换后下载,对于服务器硬盘和带宽都有要求,还是先搞个本地的用一下。1.首先确定大概需求我的主要需求是图片/pdf互转,以及PDF快速添加密码,软件最...
Python Windows桌面PDF小工具制作
很早之前就想过做一个PDF的简单操作工具,因为现在基本的图片转PDF功能大多数软件都是要开会员,WPS就是说你的。最初的想法是做个小程序,后来想了想小程序实现这个功能也简单,但是需要用户上传PDF到服务器转换后下载,对于服务器硬盘和带宽都有要求,还是先搞个本地的用一下。
1.首先确定大概需求
我的主要需求是图片/pdf互转,以及PDF快速添加密码,软件最好可以绑定右键菜单,有需要的时候直接右键打开.
2.使用带任务设定的提示词生成Python代码
你是资深交付程序员,帮我写可直接落地交付的Windows本地脚本
要求:
1 语言优先Python,代码完整可运行,不用我补依赖
2 自带极简可视化界面,一键点击即可执行
3 自动异常捕获、中文报错提示、运行完成弹窗提醒
4 依赖清单清晰,最后附上PyInstaller打包exe详细命令
功能需求:一个可以在本地进行图片中PDF转换/编辑的Windows小工具,简单的UI界面可以进行拖动/点击选择文件,选择后点击按钮进行格式转换/合并拆分/添加修改密码等操作。添加右键菜单功能,当选中可操作的文件是右键菜单增加转换按钮,点击转换按钮后打开软件界面并自动选中文件与AI经过几次界面美化沟通后获得Python代码
import os
import sys
import tkinter as tk
from tkinter import filedialog, messagebox, ttk, simpledialog
import winreg
from PyPDF2 import PdfReader, PdfWriter, PdfMerger
from PIL import Image
import fitz
# 尝试导入tkinterdnd2库用于文件拖拽
try:
from tkinterdnd2 import DND_FILES, TkinterDnD
has_dnd = True
except ImportError:
has_dnd = False
# 高DPI适配
try:
import ctypes
ctypes.windll.shcore.SetProcessDpiAwareness(1)
except:
pass
class PDFTool:
def __init__(self, root):
self.root = root
self.root.title("PDF 全能处理工具 V1.1.1")
self.root.geometry("880x620") # 稍微加高窗口,保证按钮可见
self.root.resizable(False, False)
self.root.configure(bg="#f5f5f5")
self.file_list = []
self.check_vars = {}
main_pane = tk.Frame(root, bg="#f5f5f5")
main_pane.pack(fill=tk.BOTH, expand=True, padx=15, pady=15)
# ========== 左侧 ==========
left_frame = tk.Frame(main_pane, width=380, bg="white", bd=0, relief=tk.FLAT)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=False, padx=8)
left_frame.pack_propagate(False)
# 拖拽区域 —— 拖拽时蓝色边框高亮
drop_frame = tk.LabelFrame(
left_frame, text="📁 拖拽文件到此处", bg="white",
font=("微软雅黑", 10, "bold"), fg="#333"
)
drop_frame.pack(fill=tk.X, padx=15, pady=10)
self.drop_label = tk.Label(
drop_frame, text="📂 拖拽文件到这里\n或点击下方按钮选择",
bg="#e3f2ff", fg="#0277bd", font=("微软雅黑", 10),
bd=2, relief=tk.FLAT
)
self.drop_label.pack(fill=tk.BOTH, padx=12, pady=12, ipady=20)
# 拖拽高亮效果
self.drop_label.bind("<Enter>", self.on_drag_enter)
self.drop_label.bind("<Leave>", self.on_drag_leave)
self.drop_label.bind("<Button-1>", lambda _: self.select_files())
# 注册拖拽目标
self.drop_label.configure(bg="#e3f2ff", relief=tk.FLAT)
# 启用文件拖拽功能
if has_dnd:
self.drop_label.drop_target_register(DND_FILES)
self.drop_label.dnd_bind('<<Drop>>', self.on_drop_files)
else:
self.root.bind("<ButtonRelease-1>", self.on_mouse_release)
tk.Button(
drop_frame, text="选择文件", command=self.select_files,
bg="#0277bd", fg="white", relief=tk.FLAT, height=2
).pack(fill=tk.X, padx=12, pady=8)
# 文件列表(多选框)
list_frame = tk.LabelFrame(
left_frame, text="📋 文件列表(多选)", bg="white",
font=('微软雅黑', 10, 'bold'), fg="#333"
)
list_frame.pack(fill=tk.BOTH, expand=True, padx=15, pady=(0,8))
# 多选列表容器(带滚动条)
self.list_canvas = tk.Canvas(list_frame, bg="white", highlightthickness=0)
self.list_scroll = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.list_canvas.yview)
self.list_container = tk.Frame(self.list_canvas, bg="white")
self.list_container.bind("<Configure>", lambda e: self.list_canvas.configure(scrollregion=self.list_canvas.bbox("all")))
self.list_canvas.create_window((0, 0), window=self.list_container, anchor="nw")
self.list_canvas.configure(yscrollcommand=self.list_scroll.set)
# 列表按钮(移到列表框内部,保证可见)
btn_frame = tk.Frame(list_frame, bg="white")
btn_frame.pack(fill=tk.X, padx=10, pady=5)
# 调整布局,确保按钮能够正确显示
self.list_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.list_scroll.pack(side=tk.RIGHT, fill=tk.Y)
tk.Button(btn_frame, text="全选", command=self.select_all, bg="#e3f2ff", fg="#0277bd", relief=tk.FLAT).pack(side=tk.LEFT, padx=5)
tk.Button(btn_frame, text="清空", command=self.clear_list, bg="#ffebee", fg="#c62828", relief=tk.FLAT).pack(side=tk.RIGHT, padx=5)
# ========== 右侧 ==========
right_frame = tk.Frame(main_pane, bg="white", bd=0, relief=tk.FLAT)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=8)
func_frame = tk.LabelFrame(right_frame, text="⚙️ 功能选择", bg="white", font=("微软雅黑",10,"bold"))
func_frame.pack(fill=tk.X, padx=18, pady=14)
self.operation = tk.StringVar(value="图片转PDF")
functions = [
"图片转PDF", "PDF转图片",
"PDF合并", "PDF拆分",
"PDF添加密码", "PDF移除密码"
]
for i, text in enumerate(functions):
ttk.Radiobutton(func_frame, text=text, variable=self.operation, value=text
).grid(row=i//2, column=i%2, sticky="w", padx=20, pady=8)
out_frame = tk.LabelFrame(right_frame, text="📂 输出目录", bg="white", font=("微软雅黑",10,"bold"))
out_frame.pack(fill=tk.X, padx=18, pady=12)
self.out_var = tk.StringVar()
tk.Entry(out_frame, textvariable=self.out_var, font=("微软雅黑",9)).pack(fill=tk.X, padx=10, pady=8)
tk.Button(out_frame, text="选择目录", command=self.select_output_dir, bg="#2e7d32", fg="white", relief=tk.FLAT, height=2).pack(fill=tk.X, padx=10, pady=6)
# 执行按钮
tk.Button(
right_frame, text="▶ 开始执行", command=self.run_task,
bg="#d32f2f", fg="white", font=("微软雅黑",13,"bold"), height=2, relief=tk.FLAT
).pack(fill=tk.X, padx=20, pady=20)
# 右键菜单
menu_f = tk.Frame(right_frame, bg="white")
menu_f.pack(fill=tk.X, padx=20, pady=5)
tk.Button(menu_f, text="注册右键菜单", command=self.register_right_menu, width=18).pack(side=tk.LEFT, padx=5)
tk.Button(menu_f, text="删除右键菜单", command=self.unregister_right_menu, width=18).pack(side=tk.LEFT, padx=5)
# ===================== 拖拽边框高亮 =====================
def on_drag_enter(self, event):
self.drop_label.config(relief=tk.SOLID, bd=3, bg="#c8e6ff")
def on_drag_leave(self, event):
self.drop_label.config(relief=tk.FLAT, bd=2, bg="#e3f2ff")
# ===================== 稳定拖拽实现 =====================
def on_mouse_release(self, event):
# 检查是否在drop_label上释放鼠标
print("on_mouse_release:", event.x_root, event.y_root)
if self.drop_label.winfo_containing(event.x_root, event.y_root):
# 尝试获取拖拽的文件路径
print("尝试进行拖拽操作")
try:
# 尝试使用tkinter的内置方法获取拖拽的文件
if hasattr(event, 'data'):
files = event.data
if isinstance(files, str):
files = [files]
for f in files:
path = f.strip('{}')
if os.path.exists(path) and path not in self.file_list:
self.file_list.append(path)
self.refresh_check_list()
print("[拖拽成功tkinter] 文件:", files)
else:
# 尝试使用win32clipboard获取拖拽的文件
import win32clipboard
win32clipboard.OpenClipboard()
if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_HDROP):
files = win32clipboard.GetClipboardData(win32clipboard.CF_HDROP)
win32clipboard.CloseClipboard()
for f in files:
if os.path.exists(f) and f not in self.file_list:
self.file_list.append(f)
self.refresh_check_list()
print("[拖拽成功win32clipboard] 文件:", files)
else:
win32clipboard.CloseClipboard()
print("[拖拽异常] 剪贴板中没有文件路径")
except Exception as e:
print("[拖拽异常]", e)
def on_drop_files(self, event):
try:
self.on_drag_leave(None)
# 处理文件拖拽
if hasattr(event, 'data'):
files = event.data
print("files",files)
if isinstance(files, str):
file_paths = files.split()
#files = [files]
for f in file_paths:
path = f.strip('{}')
if os.path.exists(path) and path not in self.file_list:
self.file_list.append(path)
self.refresh_check_list()
print("[拖拽成功tkinterdnd2] 文件:", files)
except Exception as e:
print("[拖拽异常]", e)
# ===================== 多选列表 =====================
def refresh_check_list(self):
for w in self.list_container.winfo_children():
w.destroy()
self.check_vars.clear()
for path in self.file_list:
var = tk.BooleanVar()
self.check_vars[path] = var
cb = ttk.Checkbutton(
self.list_container, text=os.path.basename(path), variable=var
)
cb.pack(anchor="w", padx=8, pady=5, fill=tk.X)
def get_selected_files(self):
return [p for p, v in self.check_vars.items() if v.get()]
def select_all(self):
for v in self.check_vars.values():
v.set(True)
def select_files(self):
files = filedialog.askopenfilenames(filetypes=[
("支持文件", "*.png;*.jpg;*.jpeg;*.pdf"),
("图片", "*.png;*.jpg;*.jpeg"),
("PDF", "*.pdf")
])
for f in files:
if f not in self.file_list:
self.file_list.append(f)
self.refresh_check_list()
def clear_list(self):
self.file_list.clear()
self.check_vars.clear()
self.refresh_check_list()
def select_output_dir(self):
d = filedialog.askdirectory()
if d:
self.out_var.set(d)
# ===================== 格式校验 =====================
def check_file(self, path, op):
ext = os.path.splitext(path)[1].lower()
img = [".png", ".jpg", ".jpeg"]
if op == "图片转PDF" and ext not in img:
return False, f"{os.path.basename(path)} 仅支持图片"
if op != "图片转PDF" and ext != ".pdf":
return False, f"{os.path.basename(path)} 仅支持PDF"
return True, ""
# ===================== 执行 =====================
def run_task(self):
try:
selected = self.get_selected_files()
out_dir = self.out_var.get().strip()
op = self.operation.get()
if not selected:
messagebox.showwarning("提示", "请选择文件")
return
if not out_dir:
messagebox.showwarning("提示", "请选择输出目录")
return
for f in selected:
ok, msg = self.check_file(f, op)
if not ok:
messagebox.showerror("格式错误", msg)
return
# 批量处理
for filepath in selected:
name = os.path.splitext(os.path.basename(filepath))[0]
if op == "图片转PDF":
Image.open(filepath).convert("RGB").save(os.path.join(out_dir, f"{name}.pdf"))
elif op == "PDF转图片":
doc = fitz.open(filepath)
for page in doc:
page.get_pixmap().save(os.path.join(out_dir, f"{name}_第{page.number+1}页.png"))
elif op == "PDF合并":
if len(selected) < 2:
messagebox.showwarning("提示", "至少选择2个PDF")
return
out_path = os.path.join(out_dir, "合并结果.pdf")
m = PdfMerger()
for f in selected:
m.append(f)
m.write(out_path)
m.close()
messagebox.showinfo("完成", "合并成功!")
return
elif op == "PDF拆分":
r = PdfReader(filepath)
for i in range(len(r.pages)):
w = PdfWriter()
w.add_page(r.pages[i])
with open(os.path.join(out_dir, f"{name}_第{i+1}页.pdf"), "wb") as fp:
w.write(fp)
elif op == "PDF添加密码":
pwd = simpledialog.askstring("设置密码", "输入密码:")
if not pwd:
return
r = PdfReader(filepath)
w = PdfWriter()
for p in r.pages:
w.add_page(p)
w.encrypt(pwd)
with open(os.path.join(out_dir, f"{name}_加密.pdf"), "wb") as fp:
w.write(fp)
elif op == "PDF移除密码":
old = simpledialog.askstring("权限验证", "输入原密码:")
if not old:
return
r = PdfReader(filepath, password=old)
w = PdfWriter()
for p in r.pages:
w.add_page(p)
with open(os.path.join(out_dir, f"{name}_已解密.pdf"), "wb") as fp:
w.write(fp)
messagebox.showinfo("完成", f"{op} 全部处理成功!")
except Exception as e:
messagebox.showerror("失败", str(e))
# ===================== 右键菜单 =====================
def register_right_menu(self):
try:
exe = os.path.abspath(sys.argv[0])
with winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, r"*\shell\PDF工具") as k:
winreg.SetValue(k, "", winreg.REG_SZ, "使用PDF工具处理")
with winreg.CreateKey(k, "command") as c:
winreg.SetValue(c, "", winreg.REG_SZ, f'"{exe}" "%1"')
messagebox.showinfo("成功", "注册完成")
except:
messagebox.showerror("失败", "请管理员运行")
def unregister_right_menu(self):
try:
winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, r"*\shell\PDF工具\command")
winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, r"*\shell\PDF工具")
messagebox.showinfo("成功", "已移除")
except:
messagebox.showinfo("提示", "未找到")
if __name__ == "__main__":
if has_dnd:
root = TkinterDnD.Tk()
else:
root = tk.Tk()
app = PDFTool(root)
root.mainloop()功能基本可以满足需求,后续可以进一步增加功能,右键菜单功能也可以进一步完善。
依赖库:
- PyPDF2
- Pillow
- fitz (PyMuPDF)
- tkinterdnd2
pyinstaller打包为exe命令
pyinstaller --onefile --windowed --noconsole --clean --upx-dir "D:\upx-5.1.1-win64" --name "PDF全能工具" pdf_tools.py --additional-hooks-dir=.参数 说明 备注 --onefile打包成单个exe文件 - --windowed无控制台窗口 GUI应用 --noconsole不显示控制台 - --clean清理临时文件 - --upx-dirUPX压缩工具路径 可选,用于压缩exe --name输出文件名 "PDF全能工具" --additional-hooks-dir额外钩子目录 防止DND功能缺失 3.遇到的问题和反思
本来这个小工具可以在半个小时左右的时间直接可以完成进行使用的,遇到了拖动文件功能的依赖tkinterdnd2在打包后无法加载的问题。其实只需要在根目录添加 hook-tkinterdnd2.py 打包时加入
--additional-hooks-dir即可。AI却更换了几种方式进行重写拖拽功能,甚至中途还自作主张删除了拖拽文件功能。中间至少有一个多小时的时间是浪费在这上面了。后续遇到类似的问题还是要自己先找结果,或者直接询问AI解决方法,而不是直接让AI改。后者AI可能就一直卡在一个位置了。
觉得内容不错?我要