PyQt5 從零開始製作 PDF 閱讀器(一)

此前,我已經寫了三篇關於 Ui 界面的文章,分別是:

、 和 。這次,我們使用 Python 實現 PDF 閱讀器。

這篇文章,主要介紹如何實現主界面,以及添加、刪除圖書封面,後續會不斷完善程序功能。

效果圖

UI 設計

首先使用 Qt Designer 設計出圖形界面:

新建一個 MainWindow 主界面,然後設置一個 toolbar,並在 toolbar 中添加三個 action,併為每個 action 設置好相應圖標。

也可以直接 compile 我製作好的 PyReader.ui 文件,或者導入 Ui_PyReader.py 文件。

依賴要求

Python3PyQt5PyMuPDF

主要任務

我們使用 PyMuPDF 來解析 PDF ,來獲取 PDF 文本信息。

安裝

我們只要在 cmd 中輸入:pip install PyMuPDF,即可安裝 PyMuPDF。

導入

# 導入 PyMuPDF
import fitz

在本節中,我們只需瞭解以下幾個基本操作:

fitz.open() 函數用來讀取 PDF 文件內容,doc.loadPage() 函數用來獲取具體某一頁的信息。特別的 ,我們使用loadPage(0) 來獲取封面信息。

# 讀取 PDF
doc = fitz.open(fname)
# 獲取第 n 頁內容
page = doc.loadPage(n)

本節主要的內容就是把封面渲染到主界面中,並完成添加與刪除封面的任務。

顯示錶格

我們採用 QtWidgets.QTableWidget 表格控件來顯示封面。

首先讓我們設置表格樣式與功能:

其中,我們設置了單元格的縱橫比為 4 : 3,以及其他的一些靜態屬性,並將 self.table 與右鍵菜單綁定,支持點擊單元格調用 self.generateMenu 函數。

def _setTableStyle(self):
# 開啟水平與垂直滾軸
self.table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.table.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
# 設置 5 行 8 列 的表格
self.table.setColumnCount(8)
self.table.setRowCount(5)
# 設置標準寬度
self.width = self.screen.width() // 8
# 設置單元格的寬度
for i in range(8):
self.table.setColumnWidth(i, self.width)
# 設置單元格的高度
# 設置縱橫比為 4 : 3
for i in range(5):
self.table.setRowHeight(i, self.width * 4 // 3)
# 隱藏標題欄
self.table.verticalHeader().setVisible(False)
self.table.horizontalHeader().setVisible(False)
# 禁止編輯
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
# 不顯示網格線
self.table.setShowGrid(False)
# 將單元格綁定右鍵菜單
# 點擊單元格,調用 self.generateMenu 函數
self.table.setContextMenuPolicy(Qt.CustomContextMenu)
self.table.customContextMenuRequested.connect(self.generateMenu)

添加封面

首先讓我們來看如何生成 TableWidget 可顯示的 圖像類文件。

我們通過 doc.loadPage(0) 獲取頁面對象,並傳遞給 render_pdf_page() 函數,設置縮放比為 1 : 1。首先構建 QImage 對象,在通過 convertFromImage 函數將 QImage 對象轉化為可顯示對象。

# 顯示 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)
# 獲取封面對應的 Pixmap 對象
# alpha 設置背景為白色
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),當點擊 + 時,就會調用 self.open 函數。

我們通過 getOpenFileName() 函數來獲取文件地址,self 後面的三個參數分別是窗口名稱,文件默認路徑以及支持的文件類型。這個函數返回文件的地址。

filter_book() 函數用來確保不會重複顯示同一本書的封面。

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)
# 獲取無重複圖書的地址
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) 用來設置標籤的行與列。最後確保每八個元素換行,換行後將列數清零。

def setIcon(self, fname):
# 打開 PDF
doc = fitz.open(fname)
# 加載封面
page = doc.loadPage(0)
# 生成封面圖像
cover = render_pdf_page(page, True)
label = QLabel(self)
# 設置圖片自動填充 label
label.setScaledContents(True)
# 設置封面圖片
label.setPixmap(QPixmap(cover))
# 設置單元格元素為 label
self.table.setCellWidget(self.x, self.y, label)
# 刪除 label 對象,防止後期無法即時刷新界面


# 因為 label 的生存週期未結束
del label
# 設置當前行數與列數
self.crow, self.ccol = self.x, self.y
# 每 8 個元素換行
if (not self.y % 7) and (self.y):
self.x += 1
self.y = 0
else:
self.y += 1

右鍵菜單

上面我們已經提到,如何將單元格與右鍵菜單綁定。

本次教程中,右鍵菜單隻有兩項,分別為開始閱讀(暫未實現),以及刪除圖書。

def generateMenu(self, pos):
row_num = col_num = -1
# 獲取選中的單元格的行數以及列數
for i in self.table.selectionModel().selection().indexes():
row_num = i.row()
col_num = i.column()
# 若選取的單元格中有元素,則支持右鍵菜單
if (row_num < self.crow) 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
# 點擊選項二,調用 self.delete_book 刪除圖書
elif action == item2:
self.delete_book(row_num, col_num)

接下來,讓我們看如何刪除圖書:

首先維護一個 self.booklist ,裡面儲存無重複 PDF 文件地址。首先獲取圖書在 booklist 中的索引,在 booklist 中刪除該元素。接著清空選中單元格之後(包含選中單元格)的所有單元格的內容。最後將 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 為空,設置當前單元格為 -1
if not self.booklist:
self.crow = -1
self.ccol = -1
# 刪除圖書後,重新按順序顯示封面圖片
for fname in self.booklist[index:]:
self.setIcon(fname)

點擊下面鏈接,獲取源碼。