PDF 报告生成器:用 reportlab 和 pdfrw 生成自定义 PDF 报告
如果您的工作涉及生成PDF報(bào)告,發(fā)票等,則您可能已經(jīng)考慮過(guò)使用Python自動(dòng)化。Python有一些很不錯(cuò)的第三方庫(kù)用于處理PDF文件,使您可以從腳本中讀取和寫入PDF。同樣,您也可以將這些庫(kù)作為簡(jiǎn)單GUI工具的基礎(chǔ),從而為您提供一種在桌面上操作自動(dòng)填充或編輯PDF報(bào)告的簡(jiǎn)便方法。
在本教程中,我們將使用兩個(gè)庫(kù)來(lái)創(chuàng)建自定義PDF報(bào)告填充器。數(shù)據(jù)將使用Qt表單收集:只需編輯字段,按“生成”按鈕即可在文件夾中獲取填寫的表單。我們將在這里使用的兩個(gè)庫(kù)是:
reportlab,可讓您使用文本和圖片類原件創(chuàng)建PDF
pdfrw,一個(gè)用于從現(xiàn)有PDF讀取和提取頁(yè)面的庫(kù)
盡管我們可以使用reportlab來(lái)繪制整個(gè)PDF,但是使用外部工具設(shè)計(jì)模板然后在其上疊加動(dòng)態(tài)內(nèi)容會(huì)更容易。我們可以使用pdfrw來(lái)讀取模板PDF,提取頁(yè)面,然后可以使用reportlab在該頁(yè)面上進(jìn)行繪制。這樣一來(lái),我們就可以將自定義信息(來(lái)自我們的應(yīng)用程序)直接覆蓋到現(xiàn)有的PDF模板上,并以新名稱保存。
在此示例中,我們通過(guò)手動(dòng)輸入字段,但是您可以修改應(yīng)用程序以從外部CSV文件讀取PDF數(shù)據(jù)并從中生成多個(gè)PDF。
PDF 模板
為了進(jìn)行測(cè)試,我使用Google Docs創(chuàng)建了一個(gè)自定義的TPS報(bào)告模板,并將頁(yè)面下載為PDF。該頁(yè)面包含許多要填寫的字段。在本教程中,我們將編寫一個(gè)PyQt表單,用戶可以填寫該表單,然后將數(shù)據(jù)寫到正確位置的PDF上。
模板為A4格式。將其與腳本保存在同一文件夾中。
如果您想使用其他模板,請(qǐng)隨時(shí)使用。只需記住,編寫表單時(shí)需要調(diào)整表單字段的位置。
布置表單視圖
Qt包含一個(gè)QFormLayout布局,該布局簡(jiǎn)化了生成簡(jiǎn)單表單布局的過(guò)程。它的工作方式類似于網(wǎng)格,但是您可以將元素的行添加在一起,并將字符串自動(dòng)轉(zhuǎn)換為QLabel對(duì)象。我們的框架應(yīng)用程序,包括與模板表單匹配的完整布局,如下所示。
from?PyQt5.QtWidgets?import?QPushButton,?QLineEdit,?QApplication,?QFormLayout,?QWidget,?QTextEdit,?QSpinBoxclass?Window(QWidget):def?__init__(self):super().__init__()self.name?=?QLineEdit()self.program_type?=?QLineEdit()self.product_code?=?QLineEdit()self.customer?=?QLineEdit()self.vendor?=?QLineEdit()self.n_errors?=?QSpinBox()self.n_errors.setRange(0,?1000)self.comments?=?QTextEdit()self.generate_btn?=?QPushButton("Generate?PDF")layout?=?QFormLayout()layout.addRow("Name",?self.name)layout.addRow("Program?Type",?self.program_type)layout.addRow("Product?Code",?self.product_code)layout.addRow("Customer",?self.customer)layout.addRow("Vendor",?self.vendor)layout.addRow("No.?of?Errors",?self.n_errors)layout.addRow("Comments",?self.comments)layout.addRow(self.generate_btn)self.setLayout(layout)app?=?QApplication([]) w?=?Window() w.show() app.exec()在編寫用于替換/自動(dòng)化紙質(zhì)表格的工具時(shí),嘗試模仿紙質(zhì)表格的布局通常是個(gè)好主意,這樣就很熟悉了。
上面的代碼運(yùn)行后在窗口中提供以下布局。您已經(jīng)可以在字段中輸入內(nèi)容,但是按下按鈕尚無(wú)任何作用 —— 我們尚未編寫代碼來(lái)生成PDF或?qū)⑵溥B接到按鈕。
生成 PDF 文本
為了將基本模板生成PDF,我們將結(jié)合reportlab和PdfReader兩個(gè)庫(kù)。流程如下:
使用PdfReader讀入template.pdf文件,并僅提取第一頁(yè)。
創(chuàng)建一個(gè)reportlab 的 Canvas對(duì)象
使用pdfrw.toreportlab.makerl生成畫布對(duì)象,然后使用canvas.doForm()將其添加到Canvas中。
在畫布上繪制自定義位
將PDF保存到文件
代碼如下所示,不需要Qt,您可以保存到文件并按原樣運(yùn)行。運(yùn)行后,生成的PDF將作為result.pdf保存在同一文件夾中。
from?reportlab.pdfgen.canvas?import?Canvas from?pdfrw?import?PdfReader from?pdfrw.buildxobj?import?pagexobj from?pdfrw.toreportlab?import?makerloutfile?=?"result.pdf"template?=?PdfReader("template.pdf",?decompress=False).pages[0] template_obj?=?pagexobj(template)canvas?=?Canvas(outfile)xobj_name?=?makerl(canvas,?template_obj) canvas.doForm(xobj_name)ystart?=?443#?Prepared?by canvas.drawString(170,?ystart,?"My?name?here")canvas.save()由于生成PDF的過(guò)程正在進(jìn)行IO操作,因此可能會(huì)花費(fèi)一些時(shí)間(例如,如果我們從網(wǎng)絡(luò)驅(qū)動(dòng)器中加載文件)。因此,最好在單獨(dú)的線程中進(jìn)行處理。接下來(lái),我們將定義這個(gè)自定義線程運(yùn)行器。
在單獨(dú)的線程中運(yùn)行生成器
由于每個(gè)生成器都是一個(gè)孤立的工作,因此使用Qt的QRunner框架來(lái)處理該流程是很有意義的,這也使以后為每個(gè)作業(yè)添加可自定義的模板變得很簡(jiǎn)單。我們?cè)谑褂枚嗑€程教程中可以看到相同的方法,在該方法中,我們使用QRunner的子類來(lái)保存我們的自定義運(yùn)行代碼,并在單獨(dú)的QObject子類上實(shí)現(xiàn)特定于運(yùn)行器的信號(hào)。
from?PyQt5.QtWidgets?import?QPushButton,?QLineEdit,?QApplication,?QFormLayout,?QWidget,?QTextEdit,?QMessageBox,?QSpinBox from?PyQt5.QtCore?import?QObject,?QRunnable,?QThreadPool,?pyqtSignal,?pyqtSlotfrom?reportlab.pdfgen.canvas?import?Canvasfrom?pdfrw?import?PdfReader from?pdfrw.buildxobj?import?pagexobj from?pdfrw.toreportlab?import?makerlclass?WorkerSignals(QObject):"""Defines?the?signals?available?from?a?running?worker?thread."""error?=?pyqtSignal(str)file_saved_as?=?pyqtSignal(str)class?Generator(QRunnable):"""Worker?threadInherits?from?QRunnable?to?handle?worker?thread?setup,?signalsand?wrap-up.:param?data:?The?data?to?add?to?the?PDF?for?generating."""def?__init__(self,?data):super().__init__()self.data?=?dataself.signals?=?WorkerSignals()@pyqtSlot()def?run(self):try:outfile?=?"result.pdf"template?=?PdfReader("template.pdf",?decompress=False).pages[0]template_obj?=?pagexobj(template)canvas?=?Canvas(outfile)xobj_name?=?makerl(canvas,?template_obj)canvas.doForm(xobj_name)ystart?=?443#?Prepared?bycanvas.drawString(170,?ystart,?self.data['name'])canvas.save()except?Exception?as?e:self.signals.error.emit(str(e))returnself.signals.file_saved_as.emit(outfile)我們?cè)谶@里定義了兩個(gè)信號(hào):
file_saved_as,它發(fā)出已保存的PDF文件的文件名(成功時(shí))
error,它以調(diào)試字符串的形式發(fā)出錯(cuò)誤信號(hào)
我們需要一個(gè)QThreadPool來(lái)添加運(yùn)行我們的自定義運(yùn)行器。我們可以將它添加到__init__塊的MainWindow中。
class?Window(QWidget):def?__init__(self):super().__init__()self.threadpool?=?QThreadPool()現(xiàn)在我們已經(jīng)定義了生成器QRunner,我們只需要實(shí)現(xiàn)generate方法來(lái)創(chuàng)建運(yùn)行器,將表單字段中的數(shù)據(jù)傳遞給運(yùn)行器,并開(kāi)始運(yùn)行生成器。
def?generate(self):self.generate_btn.setDisabled(True)data?=?{'name':?self.name.text(),'program_type':?self.program_type.text(),'product_code':?self.product_code.text(),'customer':?self.customer.text(),'vendor':?self.vendor.text(),'n_errors':?str(self.n_errors.value()),'comments':?self.comments.toPlainText()}g?=?Generator(data)g.signals.file_saved_as.connect(self.generated)g.signals.error.connect(print)??#?Print?errors?to?console.self.threadpool.start(g)def?generated(self,?outfile):pass28在此代碼中,我們首先禁用了generate_btn,目的是使用戶在生成過(guò)程中無(wú)法多次按下按鈕。然后,我們從控件中構(gòu)造數(shù)據(jù)字典,使用.text()方法從QLineEdit控件中獲取文本,.value()從QSpinBox中獲取值,以及.toPlainText()獲得QTextEdit的純文本表示。因?yàn)槲覀円胖梦谋靖袷?#xff0c;所以我們將數(shù)值轉(zhuǎn)換為字符串。
為了實(shí)際生成PDF,我們創(chuàng)建了剛剛定義的Generator運(yùn)行器的實(shí)例,并傳入了數(shù)據(jù)字典。我們將file_saved_as信號(hào)連接到生成的方法(在底部定義,但尚未執(zhí)行任何操作),并將錯(cuò)誤信號(hào)連接到標(biāo)準(zhǔn)Python打印功能:這會(huì)自動(dòng)將任何錯(cuò)誤打印到控制臺(tái)。
最后,我們使用Generator實(shí)例,并將其傳遞到線程池的.start()方法以使其排隊(duì)運(yùn)行(它應(yīng)立即啟動(dòng))。然后,我們可以將此方法掛接到主窗口__init__中的按鈕上,例如:
self.generate_btn.pressed.connect(self.generate)如果立即運(yùn)行該應(yīng)用程序,則按下按鈕將觸發(fā)PDF的生成,并且結(jié)果將作為result.pdf保存在啟動(dòng)該應(yīng)用程序的同一文件夾中。到目前為止,我們只在頁(yè)面上放置了一個(gè)文本塊,因此讓我們完成生成器的工作,以將所有字段寫在正確的位置。
完成生成器
接下來(lái),我們需要完成模板上的文本放置。這里的技巧是弄清模板的每行間距(取決于字體大小等),然后計(jì)算相對(duì)于第一行的位置。y坐標(biāo)增加了頁(yè)面的高度(所以0,0在左下角),因此在之前的代碼中,我們?yōu)轫斝卸xystart,然后為每行減去28。
ystart?=?443#?Prepared?by canvas.drawString(170,?ystart,?self.data['name'])#?Date:?Todays?date today?=?datetime.today() canvas.drawString(410,?ystart,?today.strftime('%F'))#?Device/Program?Type canvas.drawString(230,?ystart-28,?self.data['program_type'])#?Product?code canvas.drawString(175,?ystart-(2*28),?self.data['product_code'])#?Customer canvas.drawString(315,?ystart-(2*28),?self.data['customer'])#?Vendor canvas.drawString(145,?ystart-(3*28),?self.data['vendor'])ystart?=?250#?Program?Language canvas.drawString(210,?ystart,?"Python")canvas.drawString(430,?ystart,?self.data['n_errors'])包裝
對(duì)于大多數(shù)的表單字段,我們都可以按原樣輸出文本,因?yàn)闆](méi)有換行符。如果輸入的文本太長(zhǎng),則會(huì)溢出 —— 但是如果我們希望可以通過(guò)設(shè)置字符的最大長(zhǎng)度來(lái)限制字段本身,例如
field.setMaxLength(25)對(duì)于注釋字段,事情有些棘手。該字段可以更長(zhǎng),并且需要將行包裝在模板中的多行上。該字段還接受換行符(通過(guò)按Enter鍵),這些換行符會(huì)在寫入PDF時(shí)出現(xiàn)問(wèn)題。
如您在上面的屏幕截圖中所見(jiàn),換行符在文本中顯示為黑色正方形。好的方面是,僅刪除換行符將使換行更加容易:我們可以將每行換行為指定數(shù)量的字符。
由于字符的寬度是可變的,因此這并不是完美的選擇,但這無(wú)關(guān)緊要。如果我們換行以最寬的字符(W)填充,則任何實(shí)際行都將適合。
Python帶有內(nèi)置的textwrap庫(kù),一旦我們刪除了換行符,我們就可以使用該庫(kù)包裝文本。
import?textwrap comments?=?comments.replace('\n',?'?') lines?=?textwrap.wrap(comments,?width=80)但是我們需要考慮第一行較短,這可以通過(guò)以下方法實(shí)現(xiàn):首先將其包裝為較短的長(zhǎng)度,重新加入其余部分,然后重新包裝,例如:
import?textwrap comments?=?comments.replace('\n',?'?') lines?=?textwrap.wrap(comments,?width=65)?#?45 first_line?=?lines[0] remainder?=?'?'.join(lines[1:])lines?=?textwrap.wrap(remainder,?75)?#?55 lines?=?lines[:4]??#?max?lines,?not?including?the?first.換行線(45和55)上的注釋標(biāo)記顯示了將Ws線插入空間所需的換行長(zhǎng)度。這是最短的線,但不現(xiàn)實(shí)。使用的值應(yīng)適用于大多數(shù)普通文本。
為了正確執(zhí)行此操作,我們應(yīng)該計(jì)算文檔字體中每個(gè)文本長(zhǎng)度的實(shí)際大小,并使用該大小告知包裝器。
準(zhǔn)備好行之后,可以遍歷列表并每次減小y位置,將它們打印到 PDF 上。模板文檔中各行之間的間距為28。
comments?=?self.data['comments'].replace('\n',?'?') if?comments:lines?=?textwrap.wrap(comments,?width=65)?#?45first_line?=?lines[0]remainder?=?'?'.join(lines[1:])lines?=?textwrap.wrap(remainder,?75)?#?55lines?=?lines[:4]??#?max?lines,?not?including?the?first.canvas.drawString(155,?223,?first_line)for?n,?l?in?enumerate(lines,?1):canvas.drawString(80,?223?-?(n*28),?l)這給出了一些帶有亂數(shù)假文文本的結(jié)果。
自動(dòng)顯示結(jié)果
創(chuàng)建文件后,運(yùn)行程序會(huì)在信號(hào)中返回創(chuàng)建文件的文件名(當(dāng)前始終相同)。最好自動(dòng)將生成的PDF呈現(xiàn)給用戶,這樣他們就可以檢查運(yùn)行是否正常。在Windows上,我們可以使用os.startfile以該類型的默認(rèn)啟動(dòng)器打開(kāi)文件 —— 在這種情況下,使用默認(rèn)的PDF查看器打開(kāi)PDF。
由于這在其他平臺(tái)上不可用,因此我們捕獲了錯(cuò)誤,而是顯示了QMessageBox
def?generated(self,?outfile):self.generate_btn.setDisabled(False)try:os.startfile(outfile)except?Exception:#?If?startfile?not?available,?show?dialog.QMessageBox.information(self,?"Finished",?"PDF?has?been?generated")完整代碼
PyQt5 的完整代碼如下所示。
from?PyQt5.QtWidgets?import?QPushButton,?QLineEdit,?QApplication,?QFormLayout,?QWidget,?QTextEdit,?QMessageBox,?QSpinBox from?PyQt5.QtCore?import?QObject,?QRunnable,?QThreadPool,?pyqtSignal,?pyqtSlotfrom?reportlab.pdfgen.canvas?import?Canvasimport?osimport?textwrap from?datetime?import?datetimefrom?pdfrw?import?PdfReader from?pdfrw.buildxobj?import?pagexobj from?pdfrw.toreportlab?import?makerlclass?WorkerSignals(QObject):"""Defines?the?signals?available?from?a?running?worker?thread."""error?=?pyqtSignal(str)file_saved_as?=?pyqtSignal(str)class?Generator(QRunnable):"""Worker?threadInherits?from?QRunnable?to?handle?worker?thread?setup,?signalsand?wrap-up.:param?data:?The?data?to?add?to?the?PDF?for?generating."""def?__init__(self,?data):super().__init__()self.data?=?dataself.signals?=?WorkerSignals()@pyqtSlot()def?run(self):try:outfile?=?"result.pdf"template?=?PdfReader("template.pdf",?decompress=False).pages[0]template_obj?=?pagexobj(template)canvas?=?Canvas(outfile)xobj_name?=?makerl(canvas,?template_obj)canvas.doForm(xobj_name)ystart?=?443#?Prepared?bycanvas.drawString(170,?ystart,?self.data['name'])#?Date:?Todays?datetoday?=?datetime.today()canvas.drawString(410,?ystart,?today.strftime('%F'))#?Device/Program?Typecanvas.drawString(230,?ystart-28,?self.data['program_type'])#?Product?codecanvas.drawString(175,?ystart-(2*28),?self.data['product_code'])#?Customercanvas.drawString(315,?ystart-(2*28),?self.data['customer'])#?Vendorcanvas.drawString(145,?ystart-(3*28),?self.data['vendor'])ystart?=?250#?Program?Languagecanvas.drawString(210,?ystart,?"Python")canvas.drawString(430,?ystart,?self.data['n_errors'])comments?=?self.data['comments'].replace('\n',?'?')if?comments:lines?=?textwrap.wrap(comments,?width=65)?#?45first_line?=?lines[0]remainder?=?'?'.join(lines[1:])lines?=?textwrap.wrap(remainder,?75)?#?55lines?=?lines[:4]??#?max?lines,?not?including?the?first.canvas.drawString(155,?223,?first_line)for?n,?l?in?enumerate(lines,?1):canvas.drawString(80,?223?-?(n*28),?l)canvas.save()except?Exception?as?e:self.signals.error.emit(str(e))returnself.signals.file_saved_as.emit(outfile)class?Window(QWidget):def?__init__(self):super().__init__()self.threadpool?=?QThreadPool()self.name?=?QLineEdit()self.program_type?=?QLineEdit()self.product_code?=?QLineEdit()self.customer?=?QLineEdit()self.vendor?=?QLineEdit()self.n_errors?=?QSpinBox()self.n_errors.setRange(0,?1000)self.comments?=?QTextEdit()self.generate_btn?=?QPushButton("Generate?PDF")self.generate_btn.pressed.connect(self.generate)layout?=?QFormLayout()layout.addRow("Name",?self.name)layout.addRow("Program?Type",?self.program_type)layout.addRow("Product?Code",?self.product_code)layout.addRow("Customer",?self.customer)layout.addRow("Vendor",?self.vendor)layout.addRow("No.?of?Errors",?self.n_errors)layout.addRow("Comments",?self.comments)layout.addRow(self.generate_btn)self.setLayout(layout)def?generate(self):self.generate_btn.setDisabled(True)data?=?{'name':?self.name.text(),'program_type':?self.program_type.text(),'product_code':?self.product_code.text(),'customer':?self.customer.text(),'vendor':?self.vendor.text(),'n_errors':?str(self.n_errors.value()),'comments':?self.comments.toPlainText()}g?=?Generator(data)g.signals.file_saved_as.connect(self.generated)g.signals.error.connect(print)??#?Print?errors?to?console.self.threadpool.start(g)def?generated(self,?outfile):self.generate_btn.setDisabled(False)try:os.startfile(outfile)except?Exception:#?If?startfile?not?available,?show?dialog.QMessageBox.information(self,?"Finished",?"PDF?has?been?generated")app?=?QApplication([]) w?=?Window() w.show() app.exec_()從CSV文件生成
在上面的示例中,您需要輸入數(shù)據(jù)以手動(dòng)填寫。如果您沒(méi)有大量的PDF生成,這很好,但是如果您有一個(gè)完整的CSV文件,可以生成報(bào)告的數(shù)據(jù),那么就沒(méi)那么有趣了。在下面的示例中,我們沒(méi)有向用戶顯示表單字段列表,而是要求提供可從中生成PDF的源CSV文件 —— 文件中的每一行都使用文件中的數(shù)據(jù)生成單獨(dú)的PDF文件。
from?PyQt5.QtWidgets?import?QPushButton,?QLineEdit,?QApplication,?QFormLayout,?QWidget,?QTextEdit,?QMessageBox,?QSpinBox,?QFileDialog from?PyQt5.QtCore?import?QObject,?QRunnable,?QThreadPool,?pyqtSignal,?pyqtSlotfrom?reportlab.pdfgen.canvas?import?Canvasimport?os,?csvimport?textwrap from?datetime?import?datetimefrom?pdfrw?import?PdfReader from?pdfrw.buildxobj?import?pagexobj from?pdfrw.toreportlab?import?makerlclass?WorkerSignals(QObject):"""Defines?the?signals?available?from?a?running?worker?thread."""error?=?pyqtSignal(str)finished?=?pyqtSignal()class?Generator(QRunnable):"""Worker?threadInherits?from?QRunnable?to?handle?worker?thread?setup,?signalsand?wrap-up.:param?data:?The?data?to?add?to?the?PDF?for?generating."""def?__init__(self,?data):super().__init__()self.data?=?dataself.signals?=?WorkerSignals()@pyqtSlot()def?run(self):try:filename,?_?=?os.path.splitext(self.data['sourcefile'])folder?=?os.path.dirname(self.data['sourcefile'])template?=?PdfReader("template.pdf",?decompress=False).pages[0]template_obj?=?pagexobj(template)with?open(self.data['sourcefile'],?'r',?newline='')?as?f:reader?=?csv.DictReader(f)for?n,?row?in?enumerate(reader,?1):fn?=?f'{filename}-{n}.pdf'outfile?=?os.path.join(folder,?fn)canvas?=?Canvas(outfile)xobj_name?=?makerl(canvas,?template_obj)canvas.doForm(xobj_name)ystart?=?443#?Prepared?bycanvas.drawString(170,?ystart,?row.get('name',?''))#?Date:?Todays?datetoday?=?datetime.today()canvas.drawString(410,?ystart,?today.strftime('%F'))#?Device/Program?Typecanvas.drawString(230,?ystart-28,?row.get('program_type',?''))#?Product?codecanvas.drawString(175,?ystart-(2*28),?row.get('product_code',?''))#?Customercanvas.drawString(315,?ystart-(2*28),?row.get('customer',?''))#?Vendorcanvas.drawString(145,?ystart-(3*28),?row.get('vendor',?''))ystart?=?250#?Program?Languagecanvas.drawString(210,?ystart,?"Python")canvas.drawString(430,?ystart,?row.get('n_errors',?''))comments?=?row.get('comments',?'').replace('\n',?'?')if?comments:lines?=?textwrap.wrap(comments,?width=65)?#?45first_line?=?lines[0]remainder?=?'?'.join(lines[1:])lines?=?textwrap.wrap(remainder,?75)?#?55lines?=?lines[:4]??#?max?lines,?not?including?the?first.canvas.drawString(155,?223,?first_line)for?n,?l?in?enumerate(lines,?1):canvas.drawString(80,?223?-?(n*28),?l)canvas.save()except?Exception?as?e:self.signals.error.emit(str(e))returnself.signals.finished.emit()class?Window(QWidget):def?__init__(self):super().__init__()self.threadpool?=?QThreadPool()self.sourcefile?=?QLineEdit()self.sourcefile.setDisabled(True)??#?must?use?the?file?finder?to?select?a?valid?file.self.file_select?=?QPushButton("Select?CSV...")self.file_select.pressed.connect(self.choose_csv_file)self.generate_btn?=?QPushButton("Generate?PDF")self.generate_btn.pressed.connect(self.generate)layout?=?QFormLayout()layout.addRow(self.sourcefile,?self.file_select)layout.addRow(self.generate_btn)self.setLayout(layout)def?choose_csv_file(self):filename,?_?=?QFileDialog.getOpenFileName(self,?"Select?a?file",?filter="CSV?files?(*.csv)")if?filename:self.sourcefile.setText(filename)def?generate(self):if?not?self.sourcefile.text():return??#?If?the?field?is?empty,?ignore.self.generate_btn.setDisabled(True)data?=?{'sourcefile':?self.sourcefile.text(),}g?=?Generator(data)g.signals.finished.connect(self.generated)g.signals.error.connect(print)??#?Print?errors?to?console.self.threadpool.start(g)def?generated(self):self.generate_btn.setDisabled(False)QMessageBox.information(self,?"Finished",?"PDFs?have?been?generated")app?=?QApplication([]) w?=?Window() w.show() app.exec()您可以使用template.pdf和此示例CSV文件運(yùn)行此應(yīng)用,以生成一些TPS報(bào)告。
注意事項(xiàng):
現(xiàn)在我們生成了多個(gè)文件,完成后打開(kāi)它們并沒(méi)有多大意義。取而代之的是,我們始終只顯示一次“完成”消息。信號(hào)file_saved_as已重命名為finished,并且由于不再使用文件名str,我們將其刪除。
用于獲取文件名的QLineEdit已禁用,因此無(wú)法直接進(jìn)行編輯:設(shè)置源CSV文件的唯一方法是直接選擇文件,確保已在其中。
我們基于導(dǎo)入文件名和當(dāng)前行號(hào)自動(dòng)生成輸出文件名。文件名取自輸入CSV:CSV文件名為tps.csv,文件名為tps-1.pdf,tps-2.pdf等。文件被寫到源CSV所在的文件夾中。
由于某些行/文件可能會(huì)漏掉必填字段,因此我們?cè)谛凶值渖鲜褂?get()并使用默認(rèn)的空字符串。
可能的改進(jìn)
如果您想改進(jìn)此代碼,可以嘗試以下方法
使模板和輸出文件位置可配置 —— 使用Qt文件對(duì)話框
從文件和模板(JSON)一起加載字段位置,因此您可以將同一表單用于多個(gè)模板
使字段可配置-這非常棘手,但是您可以為特定類型(str,datetime,int等)分配特定的小部件
更多閱讀
5 分鐘快速上手 pytest 測(cè)試框架
5分鐘掌握 Python 隨機(jī)爬山算法
5分鐘快速掌握 Adam 優(yōu)化算法
特別推薦
點(diǎn)擊下方閱讀原文加入社區(qū)會(huì)員
總結(jié)
以上是生活随笔為你收集整理的PDF 报告生成器:用 reportlab 和 pdfrw 生成自定义 PDF 报告的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: hdu 3405 world islan
- 下一篇: html中dd dt的效果,html中d