知识储备得差不多了,是时候完成我们的文档转换器了。
荀子有云:“君子生非异也,善假于物也”。不要说文档转换,就算是任意一个文档格式,也能写几本大书。所以,我们不可能去阅读各种文档格式的规范,手动实现文档转换。
因此,我们需要借助外部的文档转换工具。当然,不可能是一个图形化工具,不然我们的转换器也就没有存在的意义了。我们需要的是一个命令行工具,也就是可以在虚拟终端(控制台)下执行的工具。
我们选择的工具是pandoc
。这是一个万能文档转换器,几乎支持所有主流文档格式的相互转换。如果要转PDF
格式,pandoc
需要依赖外部的工具,这里我们选择wkhtmltopdf
。pandoc
会将文档转成网页,然后再调用这个工具将网页转成PDF
。
调用外部工具,涉及到了多进程的概念。Python
会将外部工具运行在新进程里,然后根这个新进程交互,完成我们的文档转换。
import os
import os.path
import platform
from subprocess import Popen, PIPE
from threading import Thread
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
label: QLabel
wnd: QMainWindow
currentFilename: str = None
dlg: QProgressDialog = None
def loadFile(filename):
if filename is None:
return
if platform.system() == "Windows":
filename = filename[1:]
global currentFilename
currentFilename = filename
label.setText("当前文件: " + filename)
wnd.statusBar().showMessage("文件载入成功: " + filename)
class Converter(QThread):
done = pyqtSignal(bool)
def __init__(self, fmt):
super().__init__()
self.fmt = fmt
def run(self):
fmt = self.fmt
fromFilename = currentFilename
toFilename = os.path.splitext(fromFilename)[0] + "." + fmt
args = ["pandoc", fromFilename]
if fmt == "pdf":
args.extend(["--pdf-engine", "wkhtmltopdf"])
args.extend(["-o", toFilename])
try:
from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW
startupinfo = STARTUPINFO()
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
except:
startupinfo = None
process = Popen(args=args, stdin=PIPE, stdout=PIPE, stderr=PIPE, startupinfo=startupinfo)
err = process.communicate()[1].decode()
code = process.returncode
wnd.statusBar().showMessage(("转换成功: " + toFilename) if code == 0 else "转换失败: " + err)
self.done.emit(True)
def convertFile(fmt):
if currentFilename is None:
return
global dlg
dlg = QProgressDialog(wnd)
dlg.setWindowTitle("转换中...")
dlg.setMaximum(0)
dlg.setCancelButton(None)
dlg.setWindowFlags(Qt.Window | Qt.WindowTitleHint | Qt.CustomizeWindowHint)
dlg.setWindowModality(Qt.WindowModal)
dlg.show()
wnd.statusBar().showMessage("转换中...")
global converter
converter = Converter(fmt)
converter.done.connect(dlg.close)
converter.start()
app = QApplication([])
wnd = QMainWindow()
wnd.resize(400, 400 * 0.618)
wnd.setAcceptDrops(True)
wnd.dragEnterEvent = lambda e: e.accept()
wnd.dropEvent = lambda e: print(e.mimeData().urls())
wnd.dropEvent = lambda e: loadFile(next(iter([x.path() for x in e.mimeData().urls()]), None))
wnd.statusBar().showMessage("就绪.")
payload = QWidget()
label = QLabel("请拖入文件.转换后会自动覆盖同名文件,请谨慎使用")
buttons = QGridLayout()
mapper = QSignalMapper()
for idx, (k, v) in enumerate(
dict(docx="Word", pdf="PDF", pptx="PowerPoint", html="HTML", mobi="Mobi", epub="EPUB").items()):
button = QPushButton(v)
button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
mapper.setMapping(button, k)
button.clicked.connect(mapper.map)
buttons.addWidget(button, idx / 3 + 1, idx % 3 + 1)
mapper.mapped["QString"].connect(convertFile)
layout = QVBoxLayout()
layout.addWidget(label)
layout.addLayout(buttons)
payload.setLayout(layout)
wnd.setCentralWidget(payload)
wnd.setWindowTitle("麦多文档转换器")
wnd.show()
font = app.font()
font.setPointSize(font.pointSize() / 0.618)
app.setFont(font)
app.exec()
这段代码涉及到的新知识点主要有:
Python
回收掉,我们使用了global
关键字,将对话框定义为全局变量。至于为什么Python会将我们好好运行着的对话框回收掉,我想是因为Python
系统和Qt
系统相互独立,一个对象在Python
系统中好像没用了,但是在Qt
系统中还在起作用。打开文件
对话框定位文件非常麻烦。所以,我们选用拖拽选择文件。Qt
支持文件拖拽。在一个控件上调用QWidget.setAcceptDrops(True)
,即可启用文件拖拽。一次文件拖拽会产生两个事件:拖和放。我们需要处理放事件,但是拖事件不处理,就不会产生放事件。我们需要在QWidget.dragEnterEvent
中,调用e.accept()
表示接受拖事件,这样才会产生放事件。e.mimeData().urls()
可能会有多个文件的URL
(QUrl对象)。调用QUrl.path()
方法,可以将文件的URL
转换成文件的磁盘路径。next(iter(...), None)
可以安全地取出列表中的第一个元素,取不到则返回None
。Qt
还支持网格布局。QGridLayout.addWidget(控件, 第几行, 第几列)
用来放置网格中的控件。QGridLayout
会根据放置的控件自动调整布局。Windows
和macOS
的文件路径格式不一致,我们需要根据当前的操作系统类型处理文件路径。platform.system()
方法可以获取当前的操作系统。subprocess.Popen
方法可以创建新进程,执行指定的程序。我们需要通过这个方法来调用pandoc
。为了跟这个进程通信,我们需要将这个进程的输入和输出都通过管道进入我们的程序(比如stdout=PIPE
)。process.communicate()
方法会执行进程,返回管道列表。第二个管道是标准错误流管道,如果转换出错,我们可以通过这个管道获取错误信息。通过管道获取到的是字节数组,我们需要调用decode()
方法解码成字符串。process.returncode
是进程的结束代码。一般情况,0表示正常,其他表示出错。pandoc
调用格式类似pandoc FROM.doc -o TO.pdf
。-o
后面表示转换后文件的名称。pandoc
会根据这个名称的后缀确定转换格式。如果要转PDF
,需要使用选项--pdf-engine
指定PDF引擎。Windows
下,调用控制台程序会拖出一个控制台窗口。为了避免这种情况,Popen
函数需要传入Windows
操作系统专用的参数startupinfo
。由于这个参数对应的类在其他操作系统中不存在,直接import
会导致程序无法在其他操作系统下运行。所以,我们将这些逻辑放在一个try...except
块中,忽略掉产生的异常,避免程序奔溃。师父领进门,修行在个人。教程虽短,涉及到的东西可不少,需要多多练习才能逐渐掌握。由于笔者水平太次,找工作屡屡碰壁,需要时间来反省反省。所以,这个系列教程就暂时完结了。希望读者们可以拥抱谷歌大法,创造美好明天!