2

最近,我正在尝试使用 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_())
4

1 回答 1

1

不要使用 QScrollArea + QLabel,因为它会使任务复杂化,最好使用 QGraphicsView、QGraphicsScene 和项目。根据我之前的回答,并实现了如下逻辑,我还创建了clicked信号,它携带了被按下的页面的信息和页面上点击的位置:

from PyQt5 import QtCore, QtGui, QtWidgets

import fitz


class PageItem(QtWidgets.QGraphicsPixmapItem):
    def __init__(self, page, pixmap):
        super().__init__(pixmap)
        self._page = page

    @property
    def page(self):
        return self._page


class PdfViewer(QtWidgets.QGraphicsView):
    clicked = QtCore.pyqtSignal(int, QtCore.QPoint)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setBackgroundRole(QtGui.QPalette.Dark)
        self.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignTop)
        self.setScene(QtWidgets.QGraphicsScene(self))
        self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
        self._filename = ""
        self._page_count = 0

    def load_pdf(self, filename):
        self.scene().clear()
        self._filename = filename
        try:
            doc = fitz.open(filename)
        except RuntimeError:
            return False
        self._page_count = doc.pageCount
        spaces = 10
        tl = spaces
        width = 0
        for i, page in enumerate(doc):
            pix = page.getPixmap()
            fmt = (
                QtGui.QImage.Format_RGBA8888
                if pix.alpha
                else QtGui.QImage.Format_RGB888
            )
            qtimg = QtGui.QImage(pix.samples, pix.width, pix.height, pix.stride, fmt)
            it = PageItem(i, QtGui.QPixmap(qtimg))
            self.scene().addItem(it)
            it.setPos(QtCore.QPointF(0, tl))
            tl += qtimg.height() + spaces
            width = max(width, qtimg.width())
        self.setSceneRect(QtCore.QRectF(0, 0, width, tl))
        return True

    @property
    def page_count(self):
        return self._page_count

    def zoomIn(self):
        self.scale(1.25, 1.25)

    def zoomOut(self):
        self.scale(0.8, 0.8)

    def resetZoom(self):
        self.resetTransform()

    def fitToWindow(self):
        self.fitInView(self.sceneRect(), QtCore.Qt.KeepAspectRatio)

    def mousePressEvent(self, event):
        vp = event.pos()
        sp = self.mapToScene(vp)

        for it in self.items(vp):
            if isinstance(it, PageItem):
                self.clicked.emit(it.page, it.mapFromScene(sp).toPoint())
        super().mousePressEvent(event)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.view = PdfViewer()
        self.setCentralWidget(self.view)

        self.createActions()
        self.createMenus()

        self.resize(640, 480)

        self.view.clicked.connect(self.on_clicked)

    @QtCore.pyqtSlot(int, QtCore.QPoint)
    def on_clicked(self, page, pos):
        print(page, pos)

    def open(self):
        fileName, _ = QtWidgets.QFileDialog.getOpenFileName(
            self, "Open File", QtCore.QDir.currentPath()
        )
        if fileName:
            is_loaded = self.view.load_pdf(fileName)
            self.printAct.setEnabled(is_loaded)
            self.fitToWindowAct.setEnabled(is_loaded)
            self.updateActions()

    def print_(self):
        dialog = QtPrintSupport.QPrintDialog(self.printer, self)
        if dialog.exec_():
            pass

    def fitToWindow(self):
        if self.fitToWindowAct.isChecked():
            self.view.fitToWindow()
        else:
            self.view.resetZoom()
        self.updateActions()

    def about(self):
        QtWidgets.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 = QtWidgets.QAction(
            "&Open...", self, shortcut="Ctrl+O", triggered=self.open
        )
        self.printAct = QtWidgets.QAction(
            "&Print...", self, shortcut="Ctrl+P", enabled=False, triggered=self.print_
        )
        self.exitAct = QtWidgets.QAction(
            "E&xit", self, shortcut="Ctrl+Q", triggered=self.close
        )
        self.zoomInAct = QtWidgets.QAction(
            "Zoom &In (25%)",
            self,
            shortcut="Ctrl++",
            enabled=False,
            triggered=self.view.zoomIn,
        )
        self.zoomOutAct = QtWidgets.QAction(
            "Zoom &Out (25%)",
            self,
            shortcut="Ctrl+-",
            enabled=False,
            triggered=self.view.zoomOut,
        )
        self.normalSizeAct = QtWidgets.QAction(
            "&Normal Size",
            self,
            shortcut="Ctrl+S",
            enabled=False,
            triggered=self.view.resetZoom,
        )
        self.fitToWindowAct = QtWidgets.QAction(
            "&Fit to Window",
            self,
            enabled=False,
            checkable=True,
            shortcut="Ctrl+F",
            triggered=self.fitToWindow,
        )
        self.aboutAct = QtWidgets.QAction("&About", self, triggered=self.about)
        self.aboutQtAct = QtWidgets.QAction(
            "About &Qt", self, triggered=QtWidgets.qApp.aboutQt
        )

    def createMenus(self):
        self.fileMenu = QtWidgets.QMenu("&File", self)
        self.fileMenu.addAction(self.openAct)
        self.fileMenu.addAction(self.printAct)
        self.fileMenu.addSeparator()
        self.fileMenu.addAction(self.exitAct)

        self.viewMenu = QtWidgets.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 = QtWidgets.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())


if __name__ == "__main__":
    import sys

    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

在此处输入图像描述

于 2020-02-25T09:50:57.480 回答