试过把常用的参数写成脚本,但还是麻烦——文件路径一变就要改脚本内容,批量处理的时候看不到进度,也不知道哪个文件失败了。
最后还是决定写个界面,花了两个晚上做了一个PyQt5桌面应用,把文件队列、引擎状态、实时日志、结果统计全部做进去了,还加了拖拽添加和后台线程。

⚠️ 使用这个工具前的第一件事:安装LibreOffice
开始说界面之前,有一件必须先说清楚的事:
这个工具依赖LibreOffice,必须先安装才能正常使用。
没有LibreOffice,Word文档(DOCX)、Excel表格(XLSX)、PPT演示文稿(PPT)这些格式都无法转换,工具只能处理图片和纯文本。
下载地址:
https://www.libreoffice.org/download/download/
Windows、macOS、Linux都有对应版本,安装时一路下一步即可,不需要特殊配置。
安装完成后打开工具,界面右上角的"⚡引擎状态面板"会显示LibreOffice的状态为"✅可用"。
如果显示"❌不可用",在"⚙️LibreOffice 路径配置"里手动填写安装路径,然后回车确认即可。
为什么必须用QThread:UI卡死的教训
第一次写的时候,转换逻辑直接写在主线程里,点击"开始转换"之后界面直接卡住,进度条一动不动。
原因是Python的GIL(全局解释器锁),转换操作是CPU密集型的,主线程被占用,界面就无法更新。
解决方案是创建一个后台线程处理转换:
classConverterWorker(QThread): log_signal = pyqtSignal(str) progress_signal = pyqtSignal(int) result_signal = pyqtSignal(Result) finished_signal = pyqtSignal()defrun(self):for i, file_path in enumerate(self.file_list): result = self.converter.convert(file_path, output_dir=self.output_dir)self.result_signal.emit(result) progress = int((i + 1) / total * 100)self.progress_signal.emit(progress)主线程和后台线程之间用信号(pyqtSignal)通信,转换完成或进度更新时自动通知主线程刷新界面。
界面布局:三栏设计
整个界面分成三个主要区域:
- 左侧文件队列:支持拖拽添加、删除选中、清空,表格实时显示每个文件的状态
- 中间引擎状态:三个引擎的可用性检测结果,LibreOffice路径配置
- 右侧结果统计:进度条、成功率、失败数、耗时统计,QMessageBox只在全部完成时弹一次
LibreOffice路径配置这一栏是我后来加的,原因是用户经常遇到"工具检测不到LibreOffice"的问题。加了这个之后,填错路径有验证,填对了有确认,比之前的报错提示友好很多。
拖拽添加:eventFilter的拦截
文件队列支持从资源管理器拖文件进去,这个功能用eventFilter实现:
def eventFilter(self, obj, event):if obj is self.table_files.viewport():if event.type() == event.Drop: mime = event.mimeData()if mime.hasUrls():files = []for url in mime.urls():f = url.toLocalFile()if os.path.isfile(f):files.append(f) elif os.path.isdir(f):for root, dirs, fnames in os.walk(f):for fname in fnames:files.append(os.path.join(root, fname))if files: self._add_to_table(files)return Truereturn super().eventFilter(obj, event)这里有个细节:DragEnter事件也要接受,否则DragMove事件不会触发,最终Drop事件也不会发生。
停止与中断:QMutex的必要性
停止按钮的实现比想象中复杂,不只是设个标志位就完了。
ConverterWorker在循环里每次处理文件前检查_is_running标志,但线程在判断和赋值之间可能存在竞争条件。QMutex在这里保证了标志位读写的原子性,确保停止信号能够可靠地被线程感知:
defstop(self):self.mutex.lock()self._is_running = Falseself.mutex.unlock()删除和清空:倒序删除防止索引错乱
删除选中行的时候,一开始按顺序删,结果删完之后表格显示错乱了——因为删除第一行后,后面的索引全变了。
正确做法是倒序删除:
rows_to_remove = sorted(list(set(rowfor rng in selected_rangesforrowinrange(rng.topRow(), rng.bottomRow() + 1))), reverse=True)forrowin rows_to_remove: self.table_files.removeRow(row)LibreOffice路径配置:手动填写的验证逻辑
这个功能是踩过坑之后加的。
用户最常遇到的问题是:LibreOffice明明装好了,但工具显示"未安装"。原因是工具检测的默认路径和用户实际的安装路径不一致。
后来加了手动填写路径功能,用户填入路径后回车,工具会验证路径有效性:
defrefresh_engine_status(self): raw = self.lo_path_edit.text().strip() ok = self.converter.set_libreoffice_path(raw) if raw \elseself.converter.set_libreoffice_path(None)# 验证结果显示在标签上路径填对了工具会提示"✅可用",填错了会报错"无效的路径"。这样用户能第一时间知道配置对不对,而不是等到开始转换了才发现问题。
写完之后的感受
从写库到写界面,其实思路是一样的:先解决能不能用的问题,再解决好不好用的问题。
界面写完之后最大的感受是:很多细节只有在别人用的时候才会暴露。LibreOffice检测不到、停止按钮没有反馈、输出目录没选时文件不知道存在哪……这些问题自己测试的时候不会发现,发给别人用才会一个一个冒出来。
⚠️ 再次提醒:使用前必须安装LibreOffice
下载地址: https://www.libreoffice.org/download/download/
安装完成之后打开工具,引擎状态面板里LibreOffice显示"✅可用"就可以开始用了。如果提示找不到,手动填写安装路径回车确认即可。
📌 今日话题
工具做完了,你最看重哪个功能?
A.文件拖拽添加
B.实时进度条
C.引擎状态检测
D.批量处理能力
或者你最希望加什么功能?在评论区说说,我来看看能不能做👇
喜欢小居的文章,点赞、关注、转发,点个“在看”不迷路~
有问题找小居,小居看到马上回!
LibreOffice下载地址:https://www.libreoffice.org/download/download/
工具获取:公众号回复「PDF转换器」即可
夜雨聆风