【问题标题】:How to auto resize QVBoxLayout according to its child contents inside a QScrollArea?如何根据 QScrollArea 内的子内容自动调整 QVBoxLayout 的大小?
【发布时间】:2020-06-08 22:28:40
【问题描述】:

最近,我正在尝试使用 PyQT5 制作 PDF 查看器。我修改了这篇文章 (Image Viewer GUI fails to properly map coordinates for mouse press event) 中提供的代码。我创建了一个包含 QVBoxLayout 的 QScrollArea,以便将多个 QLables 动态添加到滚动区域中。然后我会将 PDF 页面作为 QImage (pixmap) 加载到每个单独的 QLabel 中。我已成功加载并显示 QLabels 中的 PDF 页面。但是,我遇到了一个问题。 PDF 页面图像的垂直布局中的 QLabel 无法展开以显示整个页面(根据 QImage pixmap 的大小)。所以使用这种方式的结果只会显示页面的一小部分。我也无法向下滚动所有页面。 我希望 PDF 页面可以加载到 QLabels 中,并根据内容很好地展开。然后,Qlabels 可以在布局中垂直分组。布局可以根据 QLable 自动扩展和调整大小。最后,我可以向下滚动 scrollArea 以阅读所有 PDF 页面。就像其他 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_())

【问题讨论】:

    标签: python pdf pyqt pyqt5 pymupdf


    【解决方案1】:

    不要使用 QScrollArea + QLabel,因为它会使任务复杂化,最好使用 QGraphicsView、QGraphicsScene 和项目。在my previous answer的基础上,实现了如下逻辑,我还创建了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_())
    

    【讨论】:

    • 非常感谢!这对我来说很有效!我坚持使用 Qlabel 和 QscrollArea 一段时间...感谢您指出使用 QGraphicsView!
    • 我还有一个问题。我已更改您的代码以发出 pageItme 而不仅仅是页码。因此,我可以直接联系 pageItem 以更新图像。因此,在 on_clicked 函数中,我使用 page.setPixmap( some Image )。但是,我无法更新现场的项目。我哪里做错了?非常感谢!
    • @ps2pspgood 如果没有 MRE,我无法理解这个问题,所以如果您需要帮助,请创建一个新问题。
    猜你喜欢
    • 2020-10-20
    • 1970-01-01
    • 1970-01-01
    • 2021-05-16
    • 1970-01-01
    • 1970-01-01
    • 2020-01-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多