You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PyQt5中QWidget、QScrollArea与QBrush结合的绘图不显示问题

问题:启用窗口滚动后,图像上的绘制内容无法显示

需求:

  • 显示尺寸大于窗口的图像
  • 窗口可滚动以浏览完整图像
  • 在图像上进行绘制

问题:启用滚动功能后,绘制的矩形无法在图像上显示,最小复现代码如下:

class DocumentWindow(QWidget):
    def __init__(self, main_obj):
        super().__init__()

        self.WIDTH = 500
        self.HEIGHT = 500
        self.resize(self.WIDTH, self.HEIGHT)

        file_path = 'invoice_example.jpeg'
        self.pages = {0: Image.open(file_path)}
        image = QImage(file_path)
        self.doc_original_width = image.width()
        self.doc_original_height = image.height()
        self.doc_new_width = image.width()
        self.doc_new_height = image.height()
        palette = QPalette()
        palette.setBrush(QPalette.Window, QBrush(image))
        self.setPalette(palette)
        label = QLabel(None, self)
        label.setGeometry(0, 0, self.WIDTH, self.HEIGHT)

        conWin = QWidget()
        conLayout = QVBoxLayout(self)
        conLayout.setContentsMargins(0, 0, 0, 0)
        conWin.setLayout(conLayout)
        scroll = QScrollArea()
        scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
        scroll.setWidgetResizable(True)
        scroll.setWidget(conWin)
        scrollLayout = QVBoxLayout(self)

        # 启用这两行后,绘制内容不可见
        scrollLayout.addWidget(scroll)
        scrollLayout.setContentsMargins(0, 0, 0, 0)

        self.begin = QtCore.QPoint()
        self.end = QtCore.QPoint()
        self.show()
    
    def paintEvent(self, event):
        qp = QtGui.QPainter(self)
        br = QtGui.QBrush(QtGui.QColor(100, 10, 10, 40))  
        qp.setBrush(br)   
        qp.drawRect(QtCore.QRect(self.begin, self.end))
        rect = QtCore.QRect(self.begin, self.end)
        self.coordinates = rect.getCoords()

    def mousePressEvent(self, event):
        self.begin = event.pos()
        self.end = event.pos()
        self.update()

    def mouseMoveEvent(self, event):
        self.end = event.pos()
        self.update()

    def mouseReleaseEvent(self, event):
        x1, y1, x2, y2 = self.coordinates
        if x1 == x2 or y1 == y2:
            return
        self.width_factor = self.doc_original_width / self.doc_new_width
        self.height_factor = self.doc_original_height / self.doc_new_height
        self.normalized_coordinates = [ 
            int(x1*self.width_factor), int(y1*self.height_factor),
            int(x2*self.width_factor), int(y2*self.height_factor) ]
        self.update()

解决方案

问题根源

你把绘制逻辑写在主窗口DocumentWindow的paintEvent里,但启用滚动后,滚动区域的子控件(conWin)会覆盖主窗口的绘制层,导致绘制的矩形被遮挡。同时,鼠标事件的坐标没有考虑滚动偏移,即使绘制可见,坐标也会和图像位置不匹配。

修复方案

创建一个自定义的图像画布控件,把图像显示、绘制逻辑、鼠标事件都放在这个控件里,再将该控件放入滚动区域中。这样滚动和绘制就能正确关联,且不会被遮挡。

修改后的完整代码

from PyQt5 import QtCore, QtGui, QtWidgets
from PIL import Image

class ImageCanvas(QtWidgets.QWidget):
    def __init__(self, image_path, parent=None):
        super().__init__(parent)
        self.image = QtGui.QImage(image_path)
        self.doc_original_width = self.image.width()
        self.doc_original_height = self.image.height()
        # 画布尺寸和图像一致
        self.setFixedSize(self.doc_original_width, self.doc_original_height)
        
        self.begin = QtCore.QPoint()
        self.end = QtCore.QPoint()
        self.normalized_coordinates = []

    def paintEvent(self, event):
        qp = QtGui.QPainter(self)
        # 先绘制图像
        qp.drawImage(QtCore.QPoint(0,0), self.image)
        # 再绘制矩形
        if not self.begin.isNull() and not self.end.isNull():
            br = QtGui.QBrush(QtGui.QColor(100, 10, 10, 40))
            qp.setBrush(br)
            qp.drawRect(QtCore.QRect(self.begin, self.end))

    def mousePressEvent(self, event):
        self.begin = event.pos()
        self.end = event.pos()
        self.update()

    def mouseMoveEvent(self, event):
        self.end = event.pos()
        self.update()

    def mouseReleaseEvent(self, event):
        x1, y1, x2, y2 = QtCore.QRect(self.begin, self.end).getCoords()
        if x1 == x2 or y1 == y2:
            self.begin = QtCore.QPoint()
            self.end = QtCore.QPoint()
            self.update()
            return
        # 这里因为画布尺寸和原图一致,所以比例系数为1,若后续有缩放可调整
        self.normalized_coordinates = [int(x1), int(y1), int(x2), int(y2)]
        self.update()

class DocumentWindow(QtWidgets.QWidget):
    def __init__(self, main_obj=None):
        super().__init__()
        self.WIDTH = 500
        self.HEIGHT = 500
        self.resize(self.WIDTH, self.HEIGHT)

        file_path = 'invoice_example.jpeg'
        # 创建图像画布
        self.canvas = ImageCanvas(file_path)
        
        # 设置滚动区域
        scroll = QtWidgets.QScrollArea()
        scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
        scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
        scroll.setWidgetResizable(False)  # 保持画布原始尺寸,否则会被拉伸
        scroll.setWidget(self.canvas)

        # 主布局
        main_layout = QtWidgets.QVBoxLayout(self)
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.addWidget(scroll)

        self.show()

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = DocumentWindow()
    sys.exit(app.exec_())

关键改动说明

  1. 分离画布与主窗口:ImageCanvas专门负责图像显示和交互,所有绘制、鼠标事件都在这里处理,避免被滚动区域控件遮挡。
  2. 滚动区域配置:设置setWidgetResizable(False),确保画布保持图像原始尺寸,滚动区域仅提供滚动功能,不会拉伸图像。
  3. 绘制顺序:先绘制图像,再绘制矩形,保证矩形在图像上方显示。
  4. 坐标逻辑修正:鼠标事件的坐标直接对应画布(图像)的坐标,无需额外转换(若后续需要缩放图像,可在ImageCanvas中添加缩放逻辑,再调整坐标转换)。

内容的提问来源于stack exchange,提问作者Julio S.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 10:30:42