简介这份资源面向需要批量处理文档格式转换的办公人员与Python初学者提供一套基于PySide6与docx2pdf库实现的Word转PDF桌面工具。它解决的是日常办公中手动另存为PDF效率低、跨平台兼容性差的问题用户无需掌握复杂编程知识通过图形界面即可完成文件选择与转换。压缩包内共1个文件为单个py脚本包体仅约1KB轻量易部署下载后可直接运行或按需修改。目前已有167人学习下载说明该方案在办公自动化场景中具有一定参考价值。读者可获得完整的GUI实现代码理解如何用PySide6搭建窗口、绑定按钮事件并调用docx2pdf完成格式转换同时掌握转换结果保存与界面反馈的基本思路。代码结构简洁适合作为桌面工具开发入门案例也可在此基础上扩展批量处理、进度提示等功能提升日常文档处理效率。1. 用 PySide6 把 Word 转 PDF一个桌面工具的真实拆解手里攒了一堆.docx合同、报告、说明书要批量转成 PDF 归档第一反应往往是找在线转换站。但商业资料一旦上传到第三方服务器保密性就彻底失控了。我见过太多团队在这件事上翻车——不是转换失败而是文件已经出了内网。所以当有人问我「Python 能不能做个本地 Word 转 PDF 的小工具」时我的答案一直是能而且用 PySide6 做界面是最省心的路线之一。这份资源就是围绕这个场景展开的一个基于 PySide6 的桌面程序把 Word 文档在本地转成 PDF不依赖任何在线服务。它适合两类人——需要批量处理文档的办公自动化从业者以及想学 PySide6 桌面开发、拿一个完整可跑项目练手的 Python 开发者。下面我按「它怎么跑起来、界面怎么搭、转换怎么落地、哪里会炸」的顺序把这份源码拆开讲。2. 环境与依赖先把 Word 转 PDF 的底层链路跑通2.1 为什么转换核心不选 python-docx很多人第一反应是用python-docx读 Word再想办法写 PDF。这条路走不通——python-docx只能读写.docx的 XML 结构它没有渲染引擎处理不了分页、字体嵌入、表格跨页这些排版问题。你拿它读出来的内容转成 PDF 后格式基本是废的。真正靠谱的本地转换链路有两条方案原理优点缺点docx2pdf调用本机已安装的 Word 的 COM 接口格式还原度最高必须装 Word仅 Windows/macOSLibreOffice 无头模式调用soffice --headless --convert-to pdf跨平台不依赖 Office复杂排版偶有偏差需装 LibreOffice这份资源我建议默认走 LibreOffice 路线因为它是真正跨平台的Linux 服务器上也能跑。如果你的机器上已经有 Office可以加一个 docx2pdf 作为备选后端。常见做法是做一个后端抽象运行时探测哪个可用就用哪个。2.2 依赖安装与版本约束PySide6 的版本和 Python 版本绑定比较紧装之前先确认解释器版本。我一般用 Python 3.10 或 3.11太新的 3.13 在部分平台上 PySide6 的 wheel 还没跟上。# 建议在虚拟环境里操作避免污染全局 python -m venv venv # Windows 激活 venv\Scripts\activate # macOS / Linux 激活 source venv/bin/activate # 安装界面框架锁定大版本避免 API 漂移 pip install PySide66.6,6.8 # 转换后端二选一或都装 pip install docx2pdf # 依赖本机 Word # LibreOffice 不走 pip需要单独安装系统包参数说明PySide66.6,6.8这个约束不是随便写的。6.6 之前QFileDialog的部分静态方法签名有差异6.8 之后信号槽的一些重载行为有调整锁在中间区间能少踩坑。LibreOffice 在 Windows 上装完后soffice.exe的路径通常是C:\Program Files\LibreOffice\program\soffice.exe这个路径后面要传给转换函数。提示如果你只打算在 Windows 上用且机器有 Word可以跳过 LibreOffice直接用 docx2pdf省去路径配置的麻烦。2.3 验证转换链路是否通在写界面之前先用命令行验证后端能不能转这一步能帮你把「环境问题」和「代码问题」分开。# test_convert.py —— 脱离界面单独验证转换后端 import subprocess import sys from pathlib import Path def convert_with_libreoffice(src: Path, out_dir: Path) - bool: 调用 LibreOffice 无头模式转换返回是否成功 soffice rC:\Program Files\LibreOffice\program\soffice.exe # 按实际路径改 cmd [ soffice, --headless, # 不弹窗口 --convert-to, pdf, # 目标格式 --outdir, str(out_dir), # 输出目录 str(src), ] result subprocess.run(cmd, capture_outputTrue, textTrue, timeout120) # LibreOffice 成功时 returncode 为 0但有时会返回非 0 却仍生成文件 expected out_dir / (src.stem .pdf) return expected.exists() if __name__ __main__: src Path(sys.argv[1]) out Path(./output) out.mkdir(exist_okTrue) ok convert_with_libreoffice(src, out) print(转换成功 if ok else 转换失败检查 LibreOffice 路径)逻辑说明这段代码把转换逻辑从界面里剥离出来subprocess.run的timeout120是防止某个文档卡死导致进程挂起。注意判断成功不能只看returncodeLibreOffice 在某些版本下即使转换成功也可能返回非零码所以最终以「目标 PDF 文件是否存在」为准。这是我在实际项目里踩过的坑后面避坑章节还会展开。3. PySide6 界面搭建从文件选择到任务队列3.1 主窗口布局与控件选型PySide6 的布局系统比 tkinter 清晰得多主窗口我用QVBoxLayout纵向堆叠三个区域文件选择区、任务列表区、操作按钮区。文件选择用QListWidget而不是QLineEdit因为要支持多选和拖拽。# main_window.py —— 主窗口骨架 from PySide6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QListWidget, QListWidgetItem, QLabel, QFileDialog ) from PySide6.QtCore import Qt class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(Word 转 PDF 工具) self.resize(640, 480) central QWidget() self.setCentralWidget(central) layout QVBoxLayout(central) # 文件列表开启多选和拖拽 self.file_list QListWidget() self.file_list.setSelectionMode(QListWidget.ExtendedSelection) self.file_list.setAcceptDrops(True) layout.addWidget(QLabel(待转换文件)) layout.addWidget(self.file_list) # 按钮行 btn_row QHBoxLayout() self.btn_add QPushButton(添加文件) self.btn_clear QPushButton(清空列表) self.btn_convert QPushButton(开始转换) btn_row.addWidget(self.btn_add) btn_row.addWidget(self.btn_clear) btn_row.addWidget(self.btn_convert) layout.addLayout(btn_row) # 信号槽绑定 self.btn_add.clicked.connect(self.on_add_files) self.btn_clear.clicked.connect(self.file_list.clear) self.btn_convert.clicked.connect(self.on_convert) def on_add_files(self): files, _ QFileDialog.getOpenFileNames( self, 选择 Word 文档, , Word 文档 (*.docx *.doc) ) for f in files: self.file_list.addItem(QListWidgetItem(f)) def on_convert(self): # 转换逻辑下一节展开 pass参数说明setSelectionMode(QListWidget.ExtendedSelection)允许 Ctrl/Shift 多选这是批量场景的基本要求。getOpenFileNames末尾的s很关键——不带 s 的getOpenFileName只能选一个文件很多人第一次写会搞混。过滤器字符串Word 文档 (*.docx *.doc)里的空格和分号格式要严格写错了对话框里就看不到文件。3.2 用 QThread 避免界面卡死这是 PySide6 桌面开发最容易翻车的地方。转换一个几十页的文档可能要好几秒如果你直接在按钮的槽函数里跑转换主线程被阻塞界面会变成「无响应」白屏用户以为程序崩了。正确做法是把转换任务丢到QThread里通过信号把进度回传主线程更新界面。# worker.py —— 后台转换线程 from PySide6.QtCore import QThread, Signal from pathlib import Path import subprocess class ConvertWorker(QThread): # 定义信号进度文本、单个完成、全部完成 progress Signal(str) one_done Signal(str, bool) all_done Signal(int, int) def __init__(self, files, out_dir, soffice_path): super().__init__() self.files files self.out_dir Path(out_dir) self.soffice_path soffice_path def run(self): success 0 for f in self.files: src Path(f) self.progress.emit(f正在转换{src.name}) ok self._convert(src) if ok: success 1 self.one_done.emit(str(src), ok) self.all_done.emit(success, len(self.files)) def _convert(self, src: Path) - bool: cmd [ self.soffice_path, --headless, --convert-to, pdf, --outdir, str(self.out_dir), str(src), ] try: subprocess.run(cmd, capture_outputTrue, timeout120) except subprocess.TimeoutExpired: return False return (self.out_dir / (src.stem .pdf)).exists()逻辑说明Signal是跨线程通信的唯一安全方式绝对不要在子线程里直接操作QListWidget这类界面控件否则会随机崩溃。run()方法里循环处理每个文件每转完一个就emit一次主线程收到后更新列表项的状态图标或文字。all_done信号带上成功数和总数方便最后弹一个汇总提示。3.3 把 Worker 接回主窗口# 在 MainWindow.on_convert 里启动线程 def on_convert(self): files [self.file_list.item(i).text() for i in range(self.file_list.count())] if not files: return out_dir ./output Path(out_dir).mkdir(exist_okTrue) self.worker ConvertWorker( files, out_dir, rC:\Program Files\LibreOffice\program\soffice.exe ) self.worker.progress.connect(lambda msg: self.statusBar().showMessage(msg)) self.worker.one_done.connect(self.on_one_done) self.worker.all_done.connect(self.on_all_done) self.worker.start() # 注意是 start 不是 run self.btn_convert.setEnabled(False) # 转换期间禁用按钮 def on_one_done(self, path, ok): # 根据结果更新对应列表项的文字颜色 for i in range(self.file_list.count()): item self.file_list.item(i) if item.text() path: item.setForeground(Qt.green if ok else Qt.red) def on_all_done(self, success, total): self.btn_convert.setEnabled(True) self.statusBar().showMessage(f完成{success}/{total})参数说明启动线程必须用start()它会新建线程并调用run()如果你手滑写成run()那就是在主线程里同步执行等于白做。setEnabled(False)在转换期间禁用按钮防止用户重复点击导致多个线程同时跑。Qt.green和Qt.red是预定义颜色常量比手写 RGB 省事。4. 转换后端与批量处理参数、路径与并发控制4.1 LibreOffice 无头模式的关键参数soffice命令看着简单但参数顺序和取值有讲究写错了要么不转换要么转换到一半退出。# 完整参数示例 soffice \ --headless \ --norestore \ --convert-to pdf:writer_pdf_Export \ --outdir /path/to/output \ /path/to/input.docx逐个说--headless不弹界面--norestore禁止崩溃恢复对话框批量场景下这个必须加否则某个文档出错会弹窗卡住整个流程--convert-to pdf:writer_pdf_Export里的writer_pdf_Export是导出过滤器名指定它能保证用 Writer 的 PDF 导出引擎比裸写pdf更稳定--outdir后面跟输出目录注意它不会自动创建目录目录不存在会静默失败。注意--outdir的目录必须提前用Path.mkdir(exist_okTrue)建好这是新手最常忽略的一步。4.2 批量转换的并发策略一次转几百个文件串行跑太慢但无脑开多线程又会把 CPU 打满、LibreOffice 进程互相抢资源。我的经验是并发数控制在 CPU 核心数的一半左右且每个转换任务用独立的soffice进程。# 用线程池控制并发避免一次性起太多进程 from concurrent.futures import ThreadPoolExecutor from pathlib import Path import subprocess def batch_convert(files, out_dir, soffice_path, max_workers4): out_dir Path(out_dir) out_dir.mkdir(exist_okTrue) def _one(src): src Path(src) cmd [soffice_path, --headless, --norestore, --convert-to, pdf, --outdir, str(out_dir), str(src)] try: subprocess.run(cmd, capture_outputTrue, timeout180) except subprocess.TimeoutExpired: return (src.name, False) return (src.name, (out_dir / (src.stem .pdf)).exists()) results [] with ThreadPoolExecutor(max_workersmax_workers) as pool: for name, ok in pool.map(_one, files): results.append((name, ok)) return results参数说明max_workers4是保守值8 核机器可以调到 416 核可以到 6 到 8但别超过 8LibreOffice 每个实例内存占用不低。timeout180比单文件场景放宽了因为并发时单个任务的实际耗时会被拉长。pool.map会按输入顺序返回结果方便和原文件列表对应。4.3 输出路径与重名处理批量转换最烦的是重名覆盖。两个不同目录下的报告.docx转到同一个输出目录后一个会把前一个覆盖掉。解决办法是在输出文件名里带上源文件的父目录名或哈希。import hashlib from pathlib import Path def safe_output_name(src: Path, out_dir: Path) - Path: 生成不冲突的输出路径 base src.stem target out_dir / f{base}.pdf if not target.exists(): return target # 冲突时追加源路径的短哈希 suffix hashlib.md5(str(src.parent).encode()).hexdigest()[:6] return out_dir / f{base}_{suffix}.pdf逻辑说明先尝试用原始文件名如果已存在就用源文件父目录路径的 MD5 前 6 位做后缀。这样同一个文件名来自不同目录时不会互相覆盖而来自同一目录的重复文件本来就应该被视为同一个。哈希取 6 位足够避免日常冲突又不会让文件名太长。5. 避坑与排查那些让转换失败的细节5.1 现象转换成功但输出目录是空的原因--outdir指定的目录不存在LibreOffice 不会自动创建命令返回 0 但什么都没生成。解决在调用转换前强制Path(out_dir).mkdir(parentsTrue, exist_okTrue)并且转换后用「目标文件是否存在」来判断成功而不是看返回码。5.2 现象界面在转换时白屏无响应原因转换逻辑写在了主线程的槽函数里subprocess.run是阻塞调用主线程被占住Qt 的事件循环停摆。解决所有耗时操作放进QThread子类用Signal回传进度主线程只负责更新界面。记住worker.start()不是worker.run()。5.3 现象中文文件名转换后乱码或找不到文件原因subprocess在 Windows 上默认用系统编码传参中文路径可能被错误编码。解决给subprocess.run显式传encodingutf-8或者用subprocess.run(cmd, ...)时确保cmd列表里的路径是str而非bytes。更稳妥的做法是转换前把文件复制到纯英文临时目录转完再改回中文名。5.4 现象LibreOffice 进程残留越转越慢原因某个文档转换超时后soffice进程没被正确回收残留在后台占内存。解决在subprocess.run外层加try/except subprocess.TimeoutExpired超时后主动kill进程批量任务结束后可以调一次taskkill /f /im soffice.exeWindows或pkill sofficeLinux做兜底清理。5.5 现象docx2pdf 报「无法找到 Word」原因docx2pdf 依赖本机安装的 Microsoft Word 的 COM 组件没装 Word 或装的是 WPS 都会失败。解决要么装正版 Word要么切换到 LibreOffice 后端。判断逻辑可以写成「先试 docx2pdf抛异常就 fallback 到 LibreOffice」这样兼容性最好。6. 进阶技巧让工具真正好用起来6.1 拖拽入列表的完整实现前面主窗口只开了setAcceptDrops(True)但没实现拖拽事件实际拖文件进去是没反应的。要重写dragEnterEvent和dropEvent。from PySide6.QtCore import QUrl class FileListWidget(QListWidget): def __init__(self): super().__init__() self.setAcceptDrops(True) def dragEnterEvent(self, event): # 只接受文件拖拽 if event.mimeData().hasUrls(): event.acceptProposedAction() def dropEvent(self, event): for url in event.mimeData().urls(): path url.toLocalFile() if path.lower().endswith((.docx, .doc)): self.addItem(QListWidgetItem(path))逻辑说明hasUrls()判断拖进来的是不是文件而不是纯文本toLocalFile()把QUrl转成本地路径。过滤后缀是为了防止用户把 PDF、图片也拖进来。这个自定义控件替换掉主窗口里的QListWidget即可。6.2 用 QSettings 记住上次的输出目录每次打开都要重新选输出目录很烦。用QSettings把上次的目录存下来下次自动填上。from PySide6.QtCore import QSettings settings QSettings(MyTools, Word2Pdf) last_dir settings.value(output_dir, ./output) # 用户选了新目录后保存 settings.setValue(output_dir, chosen_dir)参数说明QSettings在 Windows 上写注册表在 macOS/Linux 上写配置文件跨平台不用你操心。第一个参数是组织名第二个是应用名随便取但要保持稳定改了就读不到旧值了。6.3 转换结果的校验方法转完之后怎么确认 PDF 是好的光看文件存在不够有些 PDF 是 0 字节或者损坏的。我一般做两层校验文件大小大于 1KB且能被 PDF 库打开。def validate_pdf(path: Path) - bool: if not path.exists() or path.stat().st_size 1024: return False try: with open(path, rb) as f: header f.read(5) return header b%PDF- # PDF 文件头校验 except OSError: return False逻辑说明%PDF-是 PDF 规范规定的文件头读前 5 字节就能判断是不是合法 PDF。大小阈值 1KB 是经验值正常文档转出来的 PDF 不会小于这个数。两层都过才算成功这样能过滤掉 LibreOffice 偶尔生成的空壳文件。6.4 打包成单文件 exe工具做完了想发给同事用总不能让人家装 Python。用 PyInstaller 打包但 PySide6 的打包有几个坑。pyinstaller --noconfirm --windowed --onefile \ --name Word2Pdf \ --hidden-import PySide6.QtCore \ main.py参数说明--windowed去掉控制台黑窗--onefile打成单个 exe启动稍慢但分发方便--hidden-import PySide6.QtCore是因为 PyInstaller 有时分析不出 PySide6 的动态导入手动补上。打包后记得在干净机器上测一遍因为 LibreOffice 是外部依赖exe 里不含它目标机器仍需装 LibreOffice。从那以后我每次做这类本地转换工具都会先把「后端链路」在命令行单独跑通再动界面代码——因为界面卡死和转换失败是两类完全不同的问题混在一起排查就是给自己找罪受。希望这个拆解能帮你少走点弯路源码包里的结构和上面讲的模块基本对应拿到手先跑test_convert.py验证环境再启动主程序。本文还有配套的精品资源点击获取