Как автоматически изменить размер QVBoxLayout в соответствии с его дочерним содержимым внутри QScrollArea?
В последнее время я пытаюсь использовать PyQT5 для создания программы просмотра PDF. Я адаптировал код, представленный в этом сообщении ( графический интерфейс Image Viewer не может правильно сопоставить координаты для события нажатия мыши). Я создал QScrollArea, который содержит QVBoxLayout, чтобы динамически добавлять несколько QLables в область прокрутки. Затем я загружу страницы PDF как QImage (pixmap) в каждый отдельный QLabel. Я успешно загрузил и отобразил страницы PDF в QLabels. Однако я столкнулся с проблемой. QLabel в вертикальном макете с изображениями страниц PDF не может расширяться, чтобы показать всю страницу (в соответствии с размером растрового изображения QImage). Таким образом, результатом использования этого способа будет отображение только небольшой части страницы. Я тоже не могу прокрутить страницу вниз. Я ожидал, что страницы 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_())
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_())