最近,我正在尝试使用 PyQT5 制作 PDF 查看器。我修改了这篇文章中提供的代码(图像查看器 GUI 无法正确映射鼠标按下事件的坐标)。我创建了一个包含 QVBoxLayout 的 QScrollArea,以便将多个 QLables 动态添加到滚动区域中。然后我会将 PDF 页面作为 QImage (pixmap) 加载到每个单独的 QLabel 中。我已成功加载并显示 QLabels 中的 PDF 页面。但是,我遇到了一个问题。PDF 页面图像的垂直布局中的 QLabel 无法展开以显示整个页面(根据 QImage pixmap 的大小)。所以使用这种方式的结果只会显示页面的一小部分。我也无法向下滚动所有页面。我希望 PDF 页面可以加载到 QLabels 中,并根据内容很好地展开。然后,Qlabels 可以在布局中垂直分组。布局可以根据 QLable 自动扩展和调整大小。最后,我可以向下滚动 scrollArea 以阅读所有 PDF 页面。
另外,如何在每个 QLabel 中捕获鼠标位置?最终,我想让用户单击页面上的特定位置以在该位置添加文本。在我从 QLabel 和特定页码中获得坐标后,我会将信息传递给 PyMuPDF 以将文本写入 textBox 并导出 PDF 文件。
到目前为止,这是我的代码:
import fitz
import cv2
import numpy as np
from PyQt5.QtCore import QDir, Qt, QPoint
from PyQt5.QtGui import QImage, QPainter, QPalette, QPixmap, QColor, QFont
from PyQt5.QtWidgets import (QAction, QApplication, QFileDialog, QLabel,
QMainWindow, QMenu, QMessageBox, QScrollArea, QSizePolicy)
from PyQt5.QtPrintSupport import QPrintDialog, QPrinter
"""
class MyLabel(QLabel):
def __init__(self):
super(MyLabel, self).__init__()
def paintEvent(self, event):
super(MyLabel, self).paintEvent(event)
if txt_cache:
for c in txt_cache:
print(c)
pos, txt = c
painter = QPainter(self)
painter.setPen(QColor(255, 0, 0))
painter.drawText(pos, txt)
"""
class ImageViewer(QMainWindow):
def __init__(self):
super(ImageViewer, self).__init__()
self.original_pdf_img_cv = []
self.qImg_pdf = []
self.qLabels = []
self.pageCount = 0
self.printer = QPrinter()
self.scaleFactor = 0.0
self.imageLabel = QLabel()
self.imageLabel.setBackgroundRole(QPalette.Base)
self.imageLabel.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
self.imageLabel.setScaledContents(True)
self.content_widget = QtWidgets.QWidget()
self.content_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.scrollArea = QScrollArea(widgetResizable=True)
self.scrollArea.setBackgroundRole(QPalette.Dark)
self.scroll_layout = QtWidgets.QVBoxLayout(self.content_widget)
self.scrollArea.setWidget(self.content_widget)
self.setCentralWidget(self.scrollArea)
self.createActions()
self.createMenus()
self.setWindowTitle("PDF Viewer")
self.resize(500, 400)
def open(self):
fileName, _ = QFileDialog.getOpenFileName(self, "Open File", QDir.currentPath())
if fileName:
doc = fitz.open(fileName)
self.pageCount = doc.pageCount
print(self.pageCount)
for page in doc:
pix = page.getPixmap()
im = self.pix2np(pix)
self.original_pdf_img_cv.append(im)
self.qImg_pdf.append(self.convert_cv(im))
pp_num = 1
for qimg in self.qImg_pdf:
label = QLabel()
label.setBackgroundRole(QPalette.Base)
label.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
label.setScaledContents(True)
#self.scrollArea.setWidget(label)
label.setPixmap(QPixmap.fromImage(qimg))
self.scroll_layout.addWidget(label)
label.setObjectName(str(pp_num))
print(label.objectName())
self.qLabels.append(label)
pp_num += 1
"""
image = QImage(fileName)
if image.isNull():
QMessageBox.information(self, "Image Viewer", "Cannot load %s." % fileName)
return
"""
#self.imageLabel.setPixmap(QPixmap.fromImage(image))
self.scaleFactor = 1.0
self.printAct.setEnabled(True)
self.fitToWindowAct.setEnabled(True)
self.updateActions()
if not self.fitToWindowAct.isChecked():
for qlabel in self.qLabels:
qlabel.adjustSize()
#self.imageLabel.adjustSize()
def print_(self):
dialog = QPrintDialog(self.printer, self)
if dialog.exec_():
painter = QPainter(self.printer)
rect = painter.viewport()
size = self.imageLabel.pixmap().size()
size.scale(rect.size(), Qt.KeepAspectRatio)
painter.setViewport(rect.x(), rect.y(), size.width(), size.height())
painter.setWindow(self.imageLabel.pixmap().rect())
painter.drawPixmap(0, 0, self.imageLabel.pixmap())
def zoomIn(self):
self.scaleImage(1.25)
def zoomOut(self):
self.scaleImage(0.8)
def normalSize(self):
for qlabel in self.qLabels:
qlabel.adjustSize()
#self.imageLabel.adjustSize()
self.scaleFactor = 1.0
def fitToWindow(self):
fitToWindow = self.fitToWindowAct.isChecked()
self.scrollArea.setWidgetResizable(fitToWindow)
if not fitToWindow:
self.normalSize()
self.updateActions()
def about(self):
QMessageBox.about(self, "About Image Viewer",
"<p>The <b>Image Viewer</b> example shows how to combine "
"QLabel and QScrollArea to display an image. QLabel is "
"typically used for displaying text, but it can also display "
"an image. QScrollArea provides a scrolling view around "
"another widget. If the child widget exceeds the size of the "
"frame, QScrollArea automatically provides scroll bars.</p>"
"<p>The example demonstrates how QLabel's ability to scale "
"its contents (QLabel.scaledContents), and QScrollArea's "
"ability to automatically resize its contents "
"(QScrollArea.widgetResizable), can be used to implement "
"zooming and scaling features.</p>"
"<p>In addition the example shows how to use QPainter to "
"print an image.</p>")
def createActions(self):
self.openAct = QAction("&Open...", self, shortcut="Ctrl+O",
triggered=self.open)
self.printAct = QAction("&Print...", self, shortcut="Ctrl+P",
enabled=False, triggered=self.print_)
self.exitAct = QAction("E&xit", self, shortcut="Ctrl+Q",
triggered=self.close)
self.zoomInAct = QAction("Zoom &In (25%)", self, shortcut="Ctrl++",
enabled=False, triggered=self.zoomIn)
self.zoomOutAct = QAction("Zoom &Out (25%)", self, shortcut="Ctrl+-",
enabled=False, triggered=self.zoomOut)
self.normalSizeAct = QAction("&Normal Size", self, shortcut="Ctrl+S",
enabled=False, triggered=self.normalSize)
self.fitToWindowAct = QAction("&Fit to Window", self, enabled=False,
checkable=True, shortcut="Ctrl+F", triggered=self.fitToWindow)
self.aboutAct = QAction("&About", self, triggered=self.about)
self.aboutQtAct = QAction("About &Qt", self,
triggered=QApplication.instance().aboutQt)
def createMenus(self):
self.fileMenu = QMenu("&File", self)
self.fileMenu.addAction(self.openAct)
self.fileMenu.addAction(self.printAct)
self.fileMenu.addSeparator()
self.fileMenu.addAction(self.exitAct)
self.viewMenu = QMenu("&View", self)
self.viewMenu.addAction(self.zoomInAct)
self.viewMenu.addAction(self.zoomOutAct)
self.viewMenu.addAction(self.normalSizeAct)
self.viewMenu.addSeparator()
self.viewMenu.addAction(self.fitToWindowAct)
self.helpMenu = QMenu("&Help", self)
self.helpMenu.addAction(self.aboutAct)
self.helpMenu.addAction(self.aboutQtAct)
self.menuBar().addMenu(self.fileMenu)
self.menuBar().addMenu(self.viewMenu)
self.menuBar().addMenu(self.helpMenu)
def updateActions(self):
self.zoomInAct.setEnabled(not self.fitToWindowAct.isChecked())
self.zoomOutAct.setEnabled(not self.fitToWindowAct.isChecked())
self.normalSizeAct.setEnabled(not self.fitToWindowAct.isChecked())
def scaleImage(self, factor):
self.scaleFactor *= factor
for qlabel in self.qLabels:
qlabel.resize(self.scaleFactor * qlabel.pixmap().size())
#self.imageLabel.resize(self.scaleFactor * self.imageLabel.pixmap().size())
self.adjustScrollBar(self.scrollArea.horizontalScrollBar(), factor)
self.adjustScrollBar(self.scrollArea.verticalScrollBar(), factor)
self.zoomInAct.setEnabled(self.scaleFactor < 3.0)
self.zoomOutAct.setEnabled(self.scaleFactor > 0.333)
def adjustScrollBar(self, scrollBar, factor):
scrollBar.setValue(int(factor * scrollBar.value()
+ ((factor - 1) * scrollBar.pageStep()/2)))
def mousePressEvent(self, event):
self.originQPoint = self.imageLabel.mapFromGlobal(self.mapToGlobal(event.pos()))
self.currentQRubberBand = QtWidgets.QRubberBand(QtWidgets.QRubberBand.Rectangle, self.imageLabel)
self.currentQRubberBand.setGeometry(QtCore.QRect(self.originQPoint, QtCore.QSize()))
self.currentQRubberBand.show()
def mouseMoveEvent(self, event):
p = self.imageLabel.mapFromGlobal(self.mapToGlobal(event.pos()))
QtWidgets.QToolTip.showText(event.pos(), "X: {} Y: {}".format(p.x(), p.y()), self)
if self.currentQRubberBand.isVisible() and self.imageLabel.pixmap() is not None:
self.currentQRubberBand.setGeometry(
QtCore.QRect(self.originQPoint, p).normalized() & self.imageLabel.rect())
def mouseReleaseEvent(self, event):
self.currentQRubberBand.hide()
currentQRect = self.currentQRubberBand.geometry()
self.currentQRubberBand.deleteLater()
if self.imageLabel.pixmap() is not None:
tr = QtGui.QTransform()
if self.fitToWindowAct.isChecked():
tr.scale(self.imageLabel.pixmap().width() / self.scrollArea.width(),
self.imageLabel.pixmap().height() / self.scrollArea.height())
else:
tr.scale(1 / self.scaleFactor, 1 / self.scaleFactor)
r = tr.mapRect(currentQRect)
txt_cache.append((QPoint(r.x(), r.y()), 'Test!!!!!!'))
self.imageLabel.update()
cropQPixmap = self.imageLabel.pixmap().copy(r)
cropQPixmap.save('output.png')
def pix2np(self, pix):
im = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.h, pix.w, pix.n)
im = np.ascontiguousarray(im[..., [2, 1, 0]]) # rgb to bgr
return im
def convert_cv(self, cvImg):
height, width, channel = cvImg.shape
bytesPerLine = 3 * width
qImg = QImage(cvImg.data, width, height, bytesPerLine, QImage.Format_RGB888)
return qImg
if __name__ == '__main__':
import sys
from PyQt5 import QtGui, QtCore, QtWidgets
app = QApplication(sys.argv)
imageViewer = ImageViewer()
imageViewer.show()
sys.exit(app.exec_())