QPdfView 中的可滚动覆盖

问题描述 投票:0回答:1

我正在 QPdfView 中显示 pdf 文档,我需要在几页中的某些区域上绘制覆盖图。

我已经成功绘制了覆盖层,但我需要一些帮助来调整滚动时其位置的调整方式。

这是代码:

from PySide6.QtCore import QRectF
from PySide6.QtGui import QPainter, QColor
from PySide6.QtPdfWidgets import QPdfView
from PySide6.QtCore import Qt
from PySide6.QtPdf import QPdfDocument

class CustomPdfView(QPdfView):  
    def __init__(self, parent=None):
        super().__init__(parent)
        
        # Placeholder areas
        self.highlights: dict[int, list[QRectF]] = {
            i : [QRectF(150.0, 150.0, 300.0, 300.0)] for i in range(200)
            }

    def paintEvent(self, event):
        super().paintEvent(event)

        if self.document().status() == QPdfDocument.Status.Ready:

            current_page_number = self.pageNavigator().currentPage()

            if current_page_number in self.highlights.keys():
                painter = QPainter(self.viewport())
                painter.setRenderHint(QPainter.RenderHint.Antialiasing)
                painter.setBrush(QColor(255, 255, 0, 100))
                painter.setPen(Qt.PenStyle.NoPen)

                total_pages = self.document().pageCount()
                current_page_size = self.document().pagePointSize(current_page_number)
                viewport_size = self.viewport().size()
                
                scroll_bar = self.verticalScrollBar()
                max_scroll = scroll_bar.maximum()
                
                scroll_per_page = max_scroll / total_pages
                scroll_calculated = scroll_per_page * current_page_number
                
                for rect in self.highlights[current_page_number]:

                    h_diff = scroll_bar.value() - scroll_calculated
                    
                    scaled_rect = QRectF(
                        (rect.x() / current_page_size.width()) * viewport_size.width(),
                        rect.y() - h_diff,
                        (rect.width() / current_page_size.width()) * viewport_size.width(),
                        rect.height()
                    )
                    painter.drawRect(scaled_rect)

                painter.end()

在最好的情况下,我想知道当前页面有多少像素已在视口中渲染,因此 h_diff 变为 page_height - 可见像素

qt pyqt pyside6
1个回答
0
投票

您的方法有多个问题,最重要的是:

  • 它只占导航器的
    currentPage
    ,但在
    MultiPage
    模式下可以一次看到多个页面,“当前”仅是与视图上半部分的假想线相交的页面;
  • 它不考虑基于自定义缩放或
    FitToWidth
    /
    FitInView
    模式可能的缩放;

QtPdf 相对较新且简单,不幸的是缺乏许多有用的方面(例如了解每个页面的视觉边界矩形),因此需要自定义实现。

通过检查官方来源我们可以看到它在内部根据当前设置创建了一个私有的“文档布局”,考虑了视口大小、实际比例、边距和页面间距。它从根本上来说是一个基本映射,每个页面都有一个基于视觉位置的特定 QRect 和比例。

通过访问该布局,我们可以根据每个页面的实际坐标正确映射每个“突出显示”。

首先,我们需要创建一个计算所有文档布局的函数,并确保每当以下任何方面发生更改时它始终会更新:

  • 文档(加载新的 pdf 时);
  • 文档状态;
  • 视图边距 (
    setDocumentMargins()
    );
  • 自定义缩放系数;
  • 缩放模式;
  • 页间距;
  • 当前页面,
    SinglePage
    模式;
  • 视图大小已调整(通过覆盖
    resizeEvent()
    );

布局使用

{page: (pageRect, scale)}
形式的映射,并通过循环所有可见页面来计算:对于每个页面,它获取原始大小,对其进行缩放(考虑缩放模式、屏幕 DPI 和可能的自定义缩放系数,或者可能考虑到水平边距),然后为上面的 dict 映射创建值,默认 QRect 位于
0, 0
并具有新大小,并且比例基于新大小与新大小之间的比率 原来的。在此过程中,还考虑了最小宽度,考虑边距和实际页面宽度。
之后,再次循环页面以更新每个 QRect 水平(基于视口大小和最小宽度居中)和垂直(基于每个页面高度和页面间距)位置。

然后我们显然需要在

paintEvent()
中绘制“高光”,它循环遍历所有布局的页面,检查页面当前是否在视口中,如果它包含高光,则根据比例映射高光矩形该页面的:x 和 y 被转换为页面原点,然后与大小一起乘以页面比例。

以下使用基于上面链接的来源的

updateDocumentLayout
函数:

class CustomPdfView(QPdfView):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.documentLayout = OrderedDict()
        self.documentChanged.connect(self.updateDocumentLayout)
        self.documentMarginsChanged.connect(self.updateDocumentLayout)
        self.zoomFactorChanged.connect(self.updateDocumentLayout)
        self.zoomModeChanged.connect(self.updateDocumentLayout)
        self.pageSpacingChanged.connect(self.updateDocumentLayout)
        self.pageNavigator().currentPageChanged.connect(
            self.currentPageChanged)
        
        # Placeholder areas
        self.highlights = {
            i : [QRectF(150.0, 150.0, 300.0, 300.0)] for i in range(200)
            }

    def currentPageChanged(self, currentPage):
        if self.pageMode() == self.PageMode.SinglePage:
            self.updateDocumentLayout()
            self.viewport().update()

    def documentStatusChanged(self, status):
        if status == QPdfDocument.Status.Ready:
            self.updateDocumentLayout()

    def updateDocumentLayout(self):
        self.documentLayout.clear()
        if self.document() is None:
            return
        elif self.document().status() != QPdfDocument.Status.Ready:
            # the document may have been just changed, so we need to 
            # connect its statusChanged signal, but only once
            try:
                self.document().statusChanged.connect(
                    self.documentStatusChanged, 
                    Qt.ConnectionType.UniqueConnection
                )
            except TypeError:
                pass
            return

        screenRes = QApplication.primaryScreen().logicalDotsPerInch() / 72
        viewSize = self.viewport().size()
        viewWidth = viewSize.width()
        spacing = self.pageSpacing()
        totalWidth = 0

        margins = self.documentMargins()
        left = margins.left()
        right = margins.right()
        top = margins.top()

        if self.pageMode() == self.PageMode.SinglePage:
            startPage = self.pageNavigator.currentPage()
            endPage = startPage + 1
        else:
            startPage = 0
            endPage = self.document().pageCount()

        zoomMode = self.zoomMode()
        zoomFactor = self.zoomFactor()
        sizeFunc = self.document().pagePointSize
        for page in range(startPage, endPage):
            origSize = sizeFunc(page)
            pageScale = zoomFactor
            if zoomMode == self.ZoomMode.Custom:
                pageSize = QSizeF(origSize * screenRes * zoomFactor).toSize()
            elif zoomMode == self.ZoomMode.FitToWidth:
                pageSize = QSizeF(origSize * screenRes).toSize()
                pageScale = (viewWidth - left - right) / pageSize.width()
                pageSize *= factor
            else:
                vsize = QSize(viewSize + QSize(-left - right, -spacing))
                pageSize = QSizeF(origSize * screenRes).toSize()
                scaledSize = pageSize.scaled(vsize, Qt.AspectRatioMode.KeepAspectRatio)
                pageScale = scaledSize.width() / pageSize.width()
                pageSize = scaledSize

            if pageSize.width() > totalWidth:
                totalWidth = pageSize.width()

            self.documentLayout[page] = QRect(QPoint(), pageSize), pageScale

        totalWidth += left + right

        pageY = top

        for page in range(startPage, endPage):
            pageRect = self.documentLayout[page][0]
            pageX = (max(totalWidth, viewWidth) - pageRect.width()) // 2
            pageRect.moveTopLeft(QPoint(pageX, pageY))
            pageY += pageRect.height() + spacing


    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.updateDocumentLayout()

    def paintEvent(self, event):
        super().paintEvent(event)
        if not self.documentLayout:
            return

        viewRect = QRect(
            self.horizontalScrollBar().value(), 
            self.verticalScrollBar().value(), 
            self.viewport().width(), 
            self.viewport().height()
        )
        bottom = viewRect.bottom()

        qp = QPainter(self.viewport())
        qp.translate(-viewRect.x(), -viewRect.y())

        for page, (pageRect, scale) in self.documentLayout.items():
            if not pageRect.intersects(viewRect):
                if pageRect.y() > bottom:
                    # no need to go further
                    break
                continue
            qp.drawRect(pageRect)
            if not page in self.highlights:
                continue

            for rect in self.highlights[page]:
                scaled = QRectF(
                    QPointF(pageRect.topLeft()) + rect.topLeft() * scale, 
                    rect.size() * scale
                )
                qp.drawRect(scaled)
© www.soinside.com 2019 - 2024. All rights reserved.