PyQt5 從零開始制作一個 PDF 閱讀器
大家好,歡迎來到 Crossin的編程教室 !
今天,我們分享一個開發(fā)實例:用 Python 的 PyQt5 庫制作帶 UI 界面的 PDF 閱讀器。
這篇文章介紹了如何創(chuàng)建主界面,以及添加、刪除圖書封面,并實現(xiàn)閱讀功能,可以對 PDF 文檔進(jìn)行翻頁、縮放等基本操作。
效果圖

UI 設(shè)計
首先使用 Qt Designer 設(shè)計出圖形界面:
新建一個 MainWindow 主界面,然后設(shè)置一個 toolbar,并在 toolbar 中添加三個 action,并為每個 action 設(shè)置好相應(yīng)圖標(biāo)。
也可以直接 compile 我制作好的 PyReader.ui 文件,或者導(dǎo)入 Ui_PyReader.py 文件。

依賴要求
Python3
PyQt5
PyMuPDF
主要任務(wù)
我們使用 PyMuPDF 來解析 PDF ,來獲取 PDF 文本信息。
安裝
我們只要在 cmd 中輸入:
pip?install?PyMuPDF即可安裝 PyMuPDF。
導(dǎo)入
#?導(dǎo)入?PyMuPDF?
import?fitz
我們需要了解以下幾個基本操作:
fitz.open() 函數(shù)用來讀取 PDF 文件內(nèi)容,doc.loadPage() 函數(shù)用來獲取具體某一頁的信息。特別的 ,我們使用loadPage(0) 來獲取封面信息。
#?讀取?PDF
doc?=?fitz.open(fname)
#?獲取第?n?頁內(nèi)容
page?=?doc.loadPage(n)
這一部分的主要內(nèi)容就是把封面渲染到主界面中,并完成添加與刪除封面的任務(wù)。
顯示表格
我們采用 QtWidgets.QTableWidget 表格控件來顯示封面。
首先讓我們設(shè)置表格樣式與功能:
其中,我們設(shè)置了單元格的縱橫比為 4 : 3,以及其他的一些靜態(tài)屬性,并將 self.table 與右鍵菜單綁定,支持點擊單元格調(diào)用 self.generateMenu 函數(shù)。
def?_setTableStyle(self):
????#?開啟水平與垂直滾軸
????self.table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
????self.table.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
????#?設(shè)置?5?行?8?列?的表格
????self.table.setColumnCount(8)
????self.table.setRowCount(5)
????#?設(shè)置標(biāo)準(zhǔn)寬度
????self.width?=?self.screen.width()?//?8
????#?設(shè)置單元格的寬度
????for?i?in?range(8):
????????self.table.setColumnWidth(i,?self.width)
????#?設(shè)置單元格的高度
????#?設(shè)置縱橫比為?4?:?3
????for?i?in?range(5):
????????self.table.setRowHeight(i,?self.width?*?4?//?3)
????#?隱藏標(biāo)題欄
????self.table.verticalHeader().setVisible(False)
????self.table.horizontalHeader().setVisible(False)
????#?禁止編輯
????self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
????#?不顯示網(wǎng)格線
????self.table.setShowGrid(False)
????#?將單元格綁定右鍵菜單
????#?點擊單元格,調(diào)用?self.generateMenu?函數(shù)
????self.table.setContextMenuPolicy(Qt.CustomContextMenu)
????self.table.customContextMenuRequested.connect(self.generateMenu)
添加封面
首先讓我們來看如何生成 TableWidget 可顯示的 圖像類文件。
我們通過 doc.loadPage(0) 獲取頁面對象,并傳遞給 render_pdf_page() 函數(shù),設(shè)置縮放比為 1 : 1。首先構(gòu)建 QImage 對象,在通過 convertFromImage 函數(shù)將 QImage 對象轉(zhuǎn)化為可顯示對象。
#?顯示?PDF?封面
#?page_data?為?page?對象
def?render_pdf_page(page_data,?for_cover=False):
????#?圖像縮放比例
????zoom_matrix?=?fitz.Matrix(4,?4)
????if?for_cover:
????????zoom_matrix?=?fitz.Matrix(1,?1)
????#?獲取封面對應(yīng)的?Pixmap?對象
????#?alpha?設(shè)置背景為白色
????pagePixmap?=?page_data.getPixmap(
????????matrix?=?zoom_matrix,?
????????alpha=False)?
????#?獲取?image?格式
????imageFormat?=?QtGui.QImage.Format_RGB888?
????#?生成?QImage?對象
????pageQImage?=?QtGui.QImage(
????????pagePixmap.samples,
????????pagePixmap.width,?
????????pagePixmap.height,?
????????pagePixmap.stride,
????????imageFormat)
????#?生成?pixmap?對象
????pixmap?=?QtGui.QPixmap()
????pixmap.convertFromImage(pageQImage)
????return?pixmap
接著,我們就要向單元格中添加封面圖片:
我們使用工具欄中的 + 號來添加 PDF 封面。
self.addbar.triggered.connect(self.open),當(dāng)點擊 + 時,就會調(diào)用 self.open 函數(shù)。
我們通過 getOpenFileName() 函數(shù)來獲取文件地址,self 后面的三個參數(shù)分別是窗口名稱,文件默認(rèn)路徑以及支持的文件類型。這個函數(shù)返回文件的地址。
filter_book() 函數(shù)用來確保不會重復(fù)顯示同一本書的封面。
def?getfile(self):
????#?打開單個文件
????fname,?_?=?QFileDialog.getOpenFileName(self,?'Open?files',?'./',?'(*.pdf)')
????return?fname
def?open(self):
????#?打開文件
????fname?=?self.getfile()
????if?self.filter_book(fname):
????????self.setIcon(fname)
#?獲取無重復(fù)圖書的地址
def?filter_book(self,?fname):
????if?not?fname:
????????return?False
????if?fname?not?in?self.booklist:
????????self.booklist.append(fname)
????????return?True
????return?False?????????????????????
然后,我們就要將 PDF 封面渲染到主界面上:
label.setScaledContents(True) 使得圖片可以充滿 label。self.table.setCellWidget(self.x, self.y, label) 用來設(shè)置標(biāo)簽的行與列。最后確保每八個元素?fù)Q行,換行后將列數(shù)清零。
def?setIcon(self,?fname):
????#?打開?PDF
????doc?=?fitz.open(fname)
????#?加載封面
????page?=?doc.loadPage(0)
????#?生成封面圖像
????cover?=?render_pdf_page(page,?True)
????label?=?QLabel(self)
????#?設(shè)置圖片自動填充?label
????label.setScaledContents(True)
????#?設(shè)置封面圖片
????label.setPixmap(QPixmap(cover))
????#?設(shè)置單元格元素為?label
????self.table.setCellWidget(self.x,?self.y,?label)
????#?刪除?label?對象,防止后期無法即時刷新界面
????#?因為?label?的生存周期未結(jié)束
????del?label
????#?設(shè)置當(dāng)前行數(shù)與列數(shù)
????self.crow,?self.ccol?=?self.x,?self.y
????#?每?8?個元素?fù)Q行
????if?(not?self.y?%?7)?and?(self.y):
????????self.x?+=?1
????????self.y?=?0
????else:
????????self.y?+=?1
右鍵菜單
上面我們已經(jīng)提到,如何將單元格與右鍵菜單綁定。

本次教程中,右鍵菜單只有兩項,分別為開始閱讀,以及刪除圖書。
def?generateMenu(self,?pos):
????row_num?=?col_num?=?-1
????#?獲取選中的單元格的行數(shù)以及列數(shù)
????for?i?in?self.table.selectionModel().selection().indexes():
????????row_num?=?i.row()
????????col_num?=?i.column()
????#?若選取的單元格中有元素,則支持右鍵菜單
????if?(row_num?or?(row_num?==?self.crow?and?col_num?<=?self.ccol):
????????menu?=?QMenu()
????????#?添加選項
????????item1?=?menu.addAction('開始閱讀')
????????item2?=?menu.addAction('刪除圖書')
????????#?獲取選項
????????action?=?menu.exec_(self.table.mapToGlobal(pos))
????????if?action?==?item1:
????????????pass
????????#?點擊選項二,調(diào)用?self.delete_book?刪除圖書
????????elif?action?==?item2:
????????????self.delete_book(row_num,?col_num)
接下來,讓我們看如何刪除圖書:
首先維護(hù)一個 self.booklist ,里面儲存無重復(fù) PDF 文件地址。首先獲取圖書在 booklist 中的索引,在 booklist 中刪除該元素。接著清空選中單元格之后(包含選中單元格)的所有單元格的內(nèi)容。最后將 booklist 中 index 之后的圖書地址重新顯示到 table 上。簡單地說,就是刪除選中單元格,并將之后單元格向前挪一位。
#?刪除圖書
def?delete_book(self,?row,?col):
????#?獲取圖書在列表中的位置
????index?=?row?*?8?+?col
????self.x?=?row
????self.y?=?col
????if?index?>=?0:
????????self.booklist.pop(index)
????i,?j?=?row,?col
????while?1:
????????#?移除?i?行?j?列單元格的元素
????????self.table.removeCellWidget(i,?j)
????????#?一直刪到最后一個有元素的單元格
????????if?i?==?self.crow?and?j?==?self.ccol:
????????????break
????????if?(not?j?%?7)?and?j:
????????????i?+=?1
????????????j?=?0
????????else:
????????????j?+=?1
????#?如果?booklist?為空,設(shè)置當(dāng)前單元格為?-1
????if?not?self.booklist:
????????self.crow?=?-1
????????self.ccol?=?-1
????#?刪除圖書后,重新按順序顯示封面圖片
????for?fname?in?self.booklist[index:]:
????????self.setIcon(fname)
閱讀功能
現(xiàn)在我們已經(jīng)完成了 PDF 閱讀器的初始界面。接下來要新增閱讀功能,實現(xiàn)基本的翻頁以及縮放等操作。
下圖為效果圖:

下面我們來看具體實現(xiàn):
選項卡
QTabWidget 可以允許我們在一個窗口顯示多個頁面。對于書庫的這個選項卡,頁面顯示為 self.table ,即初始界面。
self.table(QTableWidget) -> self.tabwidget(QTabWidge)
#?初始化選項卡
self.tabwidget?=?QTabWidget()
#?添加書庫選項卡
self.tabwidget.addTab(self.table,?'書庫')
self.setCentralWidget(self.tabwidget)
#?設(shè)置選項卡可以關(guān)閉
self.tabwidget.setTabsClosable(True)
#?點擊選項卡叉號時,執(zhí)行?removeTabab?操作
self.tabwidget.tabCloseRequested[int].connect(self.remove_tab)
新建選項卡:每次開始閱讀時,新建一個選項卡,名稱為文件名。
def?read_book(self,?fname):
????#?self.close()
????#?內(nèi)存有可能泄露
????self.doc?=?fitz.open(fname)
????#?metadata?=?doc.metadata
????title?=?fname.split('/'?or?'\\')[-1].replace('.pdf',?'')
????vbox?=?self.book_area(self.doc.loadPage(0))
????self.book_add_tab(title,?vbox)
其中,我們要求主選項卡,即書庫選項卡是不可以關(guān)閉的。
def?remove_tab(self,?index):
????if?index:
????????#?當(dāng)前頁數(shù)
????????self.current_page?=?0
????????self.tabwidget.removeTab(index)
????????#?正在閱讀的書
????????self.read_list.pop(index)
閱讀界面的選項卡對應(yīng)的頁面區(qū)域為 QScrollArea ,QScrollArea 支持滾輪操作。也就是說,如果我們縮放 PDF 頁面大小超過 QScrollArea 的大小,那么就會自動出現(xiàn)滾輪,以便我們?yōu)g覽頁面。其中,MyArea 類是對 QScrollArea 的重載,綁定了快捷鍵以支持翻頁以及縮放等操作。
Pixmap -> label -> area(MyArea) -> vbox(QVBoxLayout) -> tab(QWidget) -> self.tabwidget(QTabWidge)
def?book_add_tab(self,?title,?vbox):
????tab?=?QWidget()
????tab.setLayout(vbox)
????#?tab?為頁面,title?為標(biāo)簽名稱
????self.tabwidget.addTab(tab,?title)
def?book_area(self,?page):
????label?=?self.page_pixmap(page)
????#?area?=?QScrollArea()
????area?=?MyArea(self)
????area.init(self)
????area.setWidget(label)
????vbox?=?QVBoxLayout()
????vbox.addWidget(area)
????return?vbox
下面我們來看看, MyArea 這個類該如何定義:
MyArea(QScrollArea)
MyArea 繼承了 QScrollArea 類,所以支持自適應(yīng)滾輪操作。這里,我們定義了 init 方法,用來接受 Reader 主類 的 self 參數(shù), 即通過 self.widget 調(diào)用 Reader 類的實例方法。
在 init_action 函數(shù)中,我們新建了四個 QShortCut 實例,分別支持快捷鍵實現(xiàn)縮小、放大、下一頁、上一頁的操作。
class?MyArea(QScrollArea):
????def?init(self,?widget):
????????self.widget?=?widget
????????self.init_action()
????def?init_action(self):
????????zoom_minus?=?QShortcut(QKeySequence("Ctrl+-"),?self)
????????zoom_minus.activated.connect(self.minus)
????????zoom_plus?=?QShortcut(QKeySequence("Ctrl+="),?self)
????????zoom_plus.activated.connect(self.plus)
????????switch_left?=?QShortcut(QKeySequence(Qt.Key_Left),?self)
????????switch_left.activated.connect(self.left)
????????switch_right?=?QShortcut(QKeySequence(Qt.Key_Right),?self)
????????switch_right.activated.connect(self.right)
????def?plus(self):
????????self.widget.zoom_book(plus=True)
????def?minus(self):
????????self.widget.zoom_book(plus=False)
????def?right(self):
????????self.widget.switch_page(right=True)
????def?left(self):
????????self.widget.switch_page(right=False)
下面,我們來介紹縮放與翻頁功能的具體實現(xiàn):
縮放功能
self.size 用來存儲頁面大小,self.page 正是根據(jù) self.size 來實現(xiàn)縮放功能。
def?zoom_book(self,?plus=True):
????a,?b?=?self.size
????if?plus:
????????a?+=?0.4
????????b?+=?0.4
????????self.size?=?(a,?b)
????????self.set_page()
????elif?not?plus?and?a?>?0:
????????if?a?>=?1:
????????????a?-=?0.4
????????????b?-=?0.4
????????self.size?=?(a,?b)
????????self.set_page()
Pixmap -> label -> area(MyArea) -> vbox(QVBoxLayout) -> tab(QWidget) -> self.tabwidget(QTabWidge)
tab 獲取 tab 對象,layout 獲取 vbox 對象,widget 獲取 area 對象,直接更改 area 上 label 控件。
def?set_page(self):
????#?加載頁面
????page?=?self.doc.loadPage(self.current_page)
????#?獲取當(dāng)前?Widget
????tab?=?self.tabwidget.currentWidget()
????#?獲取當(dāng)前的?Layout
????layout?=?tab.layout()
????#?獲取?Layout?上的控件
????widget?=?layout.itemAt(0).widget()
????#?獲取已經(jīng)繪制好的?label?對象
????label?=?self.page_pixmap(page)
????#?將?widget?的內(nèi)容更改為現(xiàn)在的?label?對象
????widget.setWidget(label)
最后我們來介紹如何實現(xiàn)翻頁功能
翻頁功能
這次,我們實現(xiàn)的 PDF 閱讀器只能同時閱讀一本書,所以翻頁功能只需由 self.current_page 控制就行。
self.doc.pageCount 為總頁數(shù),當(dāng)前頁數(shù)不能為負(fù)數(shù)或者大于總頁數(shù)。更改完 self.current_page 之后,就可以執(zhí)行 self.set_page 操作,直接更改 area 上的 label 控件。
def?set_current_page(self,?right):
????if?right?and?self.current_page?1:
????????self.current_page?+=?1
????elif?not?right?and?self.current_page?>?0:
????????self.current_page?-=?1
def?switch_page(self,?right=True):
????self.set_current_page(right)
????self.set_page()
到這里,我們就已經(jīng)實現(xiàn)了一個具有完整功能的 PDF 閱讀器。感興趣的同學(xué)也可以下載代碼后參考實現(xiàn)一個,并在此基礎(chǔ)做一些修改和擴(kuò)展。
公眾號對話頁回復(fù)關(guān)鍵字 PDF 可獲取源碼。
覺得這個案例對你有幫助的話,歡迎點贊/收藏/轉(zhuǎn)發(fā)。
_往期文章推薦_
