<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          PDF 報告生成器:用 reportlab 和 pdfrw 生成自定義 PDF 報告

          共 36359字,需瀏覽 73分鐘

           ·

          2021-04-29 21:18

          如果您的工作涉及生成PDF報告,發(fā)票等,則您可能已經考慮過使用Python自動化。Python有一些很不錯的第三方庫用于處理PDF文件,使您可以從腳本中讀取和寫入PDF。同樣,您也可以將這些庫作為簡單GUI工具的基礎,從而為您提供一種在桌面上操作自動填充或編輯PDF報告的簡便方法。
          在本教程中,我們將使用兩個庫來創(chuàng)建自定義PDF報告填充器。數(shù)據(jù)將使用Qt表單收集:只需編輯字段,按“生成”按鈕即可在文件夾中獲取填寫的表單。我們將在這里使用的兩個庫是:
          • reportlab,可讓您使用文本和圖片類原件創(chuàng)建PDF
          • pdfrw,一個用于從現(xiàn)有PDF讀取和提取頁面的庫
          盡管我們可以使用reportlab來繪制整個PDF,但是使用外部工具設計模板然后在其上疊加動態(tài)內容會更容易。我們可以使用pdfrw來讀取模板PDF,提取頁面,然后可以使用reportlab在該頁面上進行繪制。這樣一來,我們就可以將自定義信息(來自我們的應用程序)直接覆蓋到現(xiàn)有的PDF模板上,并以新名稱保存。
          在此示例中,我們通過手動輸入字段,但是您可以修改應用程序以從外部CSV文件讀取PDF數(shù)據(jù)并從中生成多個PDF。
          PDF 模板
          為了進行測試,我使用Google Docs創(chuàng)建了一個自定義的TPS報告模板,并將頁面下載為PDF。該頁面包含許多要填寫的字段。在本教程中,我們將編寫一個PyQt表單,用戶可以填寫該表單,然后將數(shù)據(jù)寫到正確位置的PDF上。

          模板為A4格式。將其與腳本保存在同一文件夾中。
          如果您想使用其他模板,請隨時使用。只需記住,編寫表單時需要調整表單字段的位置。
          布置表單視圖
          Qt包含一個QFormLayout布局,該布局簡化了生成簡單表單布局的過程。它的工作方式類似于網(wǎng)格,但是您可以將元素的行添加在一起,并將字符串自動轉換為QLabel對象。我們的框架應用程序,包括與模板表單匹配的完整布局,如下所示。
          from PyQt5.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QSpinBox

          class 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(01000)
                  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()
          在編寫用于替換/自動化紙質表格的工具時,嘗試模仿紙質表格的布局通常是個好主意,這樣就很熟悉了。
          上面的代碼運行后在窗口中提供以下布局。您已經可以在字段中輸入內容,但是按下按鈕尚無任何作用 —— 我們尚未編寫代碼來生成PDF或將其連接到按鈕。


          生成 PDF 文本
          為了將基本模板生成PDF,我們將結合reportlabPdfReader兩個庫。流程如下:
          • 使用PdfReader讀入template.pdf文件,并僅提取第一頁。
          • 創(chuàng)建一個reportlabCanvas對象
          • 使用pdfrw.toreportlab.makerl生成畫布對象,然后使用canvas.doForm()將其添加到Canvas中。
          • 在畫布上繪制自定義位
          • 將PDF保存到文件
          代碼如下所示,不需要Qt,您可以保存到文件并按原樣運行。運行后,生成的PDF將作為result.pdf保存在同一文件夾中。
          from reportlab.pdfgen.canvas import Canvas
          from pdfrw import PdfReader
          from pdfrw.buildxobj import pagexobj
          from pdfrw.toreportlab import makerl

          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 by
          canvas.drawString(170, ystart, "My name here")

          canvas.save()
          由于生成PDF的過程正在進行IO操作,因此可能會花費一些時間(例如,如果我們從網(wǎng)絡驅動器中加載文件)。因此,最好在單獨的線程中進行處理。接下來,我們將定義這個自定義線程運行器。
          在單獨的線程中運行生成器
          由于每個生成器都是一個孤立的工作,因此使用Qt的QRunner框架來處理該流程是很有意義的,這也使以后為每個作業(yè)添加可自定義的模板變得很簡單。我們在使用多線程教程中可以看到相同的方法,在該方法中,我們使用QRunner的子類來保存我們的自定義運行代碼,并在單獨的QObject子類上實現(xiàn)特定于運行器的信號。
          from PyQt5.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox
          from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot

          from reportlab.pdfgen.canvas import Canvas

          from pdfrw import PdfReader
          from pdfrw.buildxobj import pagexobj
          from pdfrw.toreportlab import makerl


          class WorkerSignals(QObject):
              """
              Defines the signals available from a running worker thread.
              """

              error = pyqtSignal(str)
              file_saved_as = pyqtSignal(str)


          class Generator(QRunnable):
              """
              Worker thread

              Inherits from QRunnable to handle worker thread setup, signals
              and wrap-up.

              :param data: The data to add to the PDF for generating.
              """


              def __init__(self, data):
                  super().__init__()
                  self.data = data
                  self.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 by
                      canvas.drawString(170, ystart, self.data['name'])

                      canvas.save()

                  except Exception as e:
                      self.signals.error.emit(str(e))
                      return

                  self.signals.file_saved_as.emit(outfile)
          我們在這里定義了兩個信號:
          • file_saved_as,它發(fā)出已保存的PDF文件的文件名(成功時)
          • error,它以調試字符串的形式發(fā)出錯誤信號
          我們需要一個QThreadPool來添加運行我們的自定義運行器。我們可以將它添加到__init__塊的MainWindow中。
          class Window(QWidget):

              def __init__(self):
                  super().__init__()

                  self.threadpool = QThreadPool()
          現(xiàn)在我們已經定義了生成器QRunner,我們只需要實現(xiàn)generate方法來創(chuàng)建運行器,將表單字段中的數(shù)據(jù)傳遞給運行器,并開始運行生成器。
          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,目的是使用戶在生成過程中無法多次按下按鈕。然后,我們從控件中構造數(shù)據(jù)字典,使用.text()方法從QLineEdit控件中獲取文本,.value()QSpinBox中獲取值,以及.toPlainText()獲得QTextEdit的純文本表示。因為我們要放置文本格式,所以我們將數(shù)值轉換為字符串。
          為了實際生成PDF,我們創(chuàng)建了剛剛定義的Generator運行器的實例,并傳入了數(shù)據(jù)字典。我們將file_saved_as信號連接到生成的方法(在底部定義,但尚未執(zhí)行任何操作),并將錯誤信號連接到標準Python打印功能:這會自動將任何錯誤打印到控制臺。
          最后,我們使用Generator實例,并將其傳遞到線程池的.start()方法以使其排隊運行(它應立即啟動)。然后,我們可以將此方法掛接到主窗口__init__中的按鈕上,例如:
          self.generate_btn.pressed.connect(self.generate)
          如果立即運行該應用程序,則按下按鈕將觸發(fā)PDF的生成,并且結果將作為result.pdf保存在啟動該應用程序的同一文件夾中。到目前為止,我們只在頁面上放置了一個文本塊,因此讓我們完成生成器的工作,以將所有字段寫在正確的位置。
          完成生成器
          接下來,我們需要完成模板上的文本放置。這里的技巧是弄清模板的每行間距(取決于字體大小等),然后計算相對于第一行的位置。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'])
          包裝
          對于大多數(shù)的表單字段,我們都可以按原樣輸出文本,因為沒有換行符。如果輸入的文本太長,則會溢出 —— 但是如果我們希望可以通過設置字符的最大長度來限制字段本身,例如
          field.setMaxLength(25)
          對于注釋字段,事情有些棘手。該字段可以更長,并且需要將行包裝在模板中的多行上。該字段還接受換行符(通過按Enter鍵),這些換行符會在寫入PDF時出現(xiàn)問題。

          如您在上面的屏幕截圖中所見,換行符在文本中顯示為黑色正方形。好的方面是,僅刪除換行符將使換行更加容易:我們可以將每行換行為指定數(shù)量的字符。
          由于字符的寬度是可變的,因此這并不是完美的選擇,但這無關緊要。如果我們換行以最寬的字符(W)填充,則任何實際行都將適合。
          Python帶有內置的textwrap庫,一旦我們刪除了換行符,我們就可以使用該庫包裝文本。
          import textwrap
          comments = comments.replace('\n'' ')
          lines = textwrap.wrap(comments, width=80)
          但是我們需要考慮第一行較短,這可以通過以下方法實現(xiàn):首先將其包裝為較短的長度,重新加入其余部分,然后重新包裝,例如:
          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)上的注釋標記顯示了將Ws線插入空間所需的換行長度。這是最短的線,但不現(xiàn)實。使用的值應適用于大多數(shù)普通文本。
          為了正確執(zhí)行此操作,我們應該計算文檔字體中每個文本長度的實際大小,并使用該大小告知包裝器。
          準備好行之后,可以遍歷列表并每次減小y位置,將它們打印到 PDF 上。模板文檔中各行之間的間距為28。
          comments = self.data['comments'].replace('\n'' ')
          if comments:
              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.

              canvas.drawString(155, 223, first_line)
              for n, l in enumerate(lines, 1):
                  canvas.drawString(80, 223 - (n*28), l)

          這給出了一些帶有亂數(shù)假文文本的結果。


          自動顯示結果
          創(chuàng)建文件后,運行程序會在信號中返回創(chuàng)建文件的文件名(當前始終相同)。最好自動將生成的PDF呈現(xiàn)給用戶,這樣他們就可以檢查運行是否正常。在Windows上,我們可以使用os.startfile以該類型的默認啟動器打開文件 —— 在這種情況下,使用默認的PDF查看器打開PDF。
          由于這在其他平臺上不可用,因此我們捕獲了錯誤,而是顯示了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, pyqtSlot

          from reportlab.pdfgen.canvas import Canvas

          import os

          import textwrap
          from datetime import datetime

          from pdfrw import PdfReader
          from pdfrw.buildxobj import pagexobj
          from pdfrw.toreportlab import makerl


          class WorkerSignals(QObject):
              """
              Defines the signals available from a running worker thread.
              """

              error = pyqtSignal(str)
              file_saved_as = pyqtSignal(str)


          class Generator(QRunnable):
              """
              Worker thread

              Inherits from QRunnable to handle worker thread setup, signals
              and wrap-up.

              :param data: The data to add to the PDF for generating.
              """


              def __init__(self, data):
                  super().__init__()
                  self.data = data
                  self.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 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'])

                      comments = self.data['comments'].replace('\n'' ')
                      if comments:
                          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.

                          canvas.drawString(155223, first_line)
                          for n, l in enumerate(lines, 1):
                              canvas.drawString(80223 - (n*28), l)

                      canvas.save()

                  except Exception as e:
                      self.signals.error.emit(str(e))
                      return

                  self.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(01000)
                  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ù)以手動填寫。如果您沒有大量的PDF生成,這很好,但是如果您有一個完整的CSV文件,可以生成報告的數(shù)據(jù),那么就沒那么有趣了。在下面的示例中,我們沒有向用戶顯示表單字段列表,而是要求提供可從中生成PDF的源CSV文件 —— 文件中的每一行都使用文件中的數(shù)據(jù)生成單獨的PDF文件。
          from PyQt5.QtWidgets import QPushButton, QLineEdit, QApplication, QFormLayout, QWidget, QTextEdit, QMessageBox, QSpinBox, QFileDialog
          from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot

          from reportlab.pdfgen.canvas import Canvas

          import os, csv

          import textwrap
          from datetime import datetime

          from pdfrw import PdfReader
          from pdfrw.buildxobj import pagexobj
          from pdfrw.toreportlab import makerl


          class WorkerSignals(QObject):
              """
              Defines the signals available from a running worker thread.
              """

              error = pyqtSignal(str)
              finished = pyqtSignal()


          class Generator(QRunnable):
              """
              Worker thread

              Inherits from QRunnable to handle worker thread setup, signals
              and wrap-up.

              :param data: The data to add to the PDF for generating.
              """


              def __init__(self, data):
                  super().__init__()
                  self.data = data
                  self.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 by
                              canvas.drawString(170, ystart, row.get('name'''))

                              # Date: Todays date
                              today = datetime.today()
                              canvas.drawString(410, ystart, today.strftime('%F'))

                              # Device/Program Type
                              canvas.drawString(230, ystart-28, row.get('program_type'''))

                              # Product code
                              canvas.drawString(175, ystart-(2*28), row.get('product_code'''))

                              # Customer
                              canvas.drawString(315, ystart-(2*28), row.get('customer'''))

                              # Vendor
                              canvas.drawString(145, ystart-(3*28), row.get('vendor'''))

                              ystart = 250

                              # Program Language
                              canvas.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# 45
                                  first_line = lines[0]
                                  remainder = ' '.join(lines[1:])

                                  lines = textwrap.wrap(remainder, 75# 55
                                  lines = lines[:4]  # max lines, not including the first.

                                  canvas.drawString(155223, first_line)
                                  for n, l in enumerate(lines, 1):
                                      canvas.drawString(80223 - (n*28), l)

                              canvas.save()

                  except Exception as e:

                      self.signals.error.emit(str(e))
                      return

                  self.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文件運行此應用,以生成一些TPS報告。
          注意事項:
          • 現(xiàn)在我們生成了多個文件,完成后打開它們并沒有多大意義。取而代之的是,我們始終只顯示一次“完成”消息。信號file_saved_as已重命名為finished,并且由于不再使用文件名str,我們將其刪除。
          • 用于獲取文件名的QLineEdit已禁用,因此無法直接進行編輯:設置源CSV文件的唯一方法是直接選擇文件,確保已在其中。
          • 我們基于導入文件名和當前行號自動生成輸出文件名。文件名取自輸入CSV:CSV文件名為tps.csv,文件名為tps-1.pdftps-2.pdf等。文件被寫到源CSV所在的文件夾中。
          • 由于某些行/文件可能會漏掉必填字段,因此我們在行字典上使用.get()并使用默認的空字符串。
          可能的改進
          如果您想改進此代碼,可以嘗試以下方法
          • 使模板和輸出文件位置可配置 —— 使用Qt文件對話框
          • 從文件和模板(JSON)一起加載字段位置,因此您可以將同一表單用于多個模板
          • 使字段可配置-這非常棘手,但是您可以為特定類型(strdatetimeint等)分配特定的小部件

          更多閱讀



          5 分鐘快速上手 pytest 測試框架


          5分鐘掌握 Python 隨機爬山算法


          5分鐘快速掌握 Adam 優(yōu)化算法

          特別推薦




          點擊下方閱讀原文加入社區(qū)會員

          瀏覽 139
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久做爱视频 | 午夜精品久久久久久久99蜜桃乐播 | 亚洲成人在线播放 | 豆花视频操逼 | 91精品国产综合久久久果冻传媒 |