第十七节_PySide6基本窗口控件深度补充_窗口绘图类(QPicture类) 下篇

文章目录


前言

本节主要讲解 QPainter 类在 QPixmap、QBitmap、QPicture 上的 基础操作常规应用


一、QPainter 在 QPixmap 上绘图

在 QLabel 上显示 QPixmap 并在其上进行绘图有两种主要方法:离屏绘制直接在 QLabel 的 paintEvent 中绘制

1.方法一:离屏绘制

先在一个临时的 QPixmap 上绘制,然后将结果设置到 QLabel,该方式简单易懂,还易操作。

python 复制代码
		# QPainter 在 QPixmap 上绘图
        self.pixmap = QPixmap("Resources/image/jd.png")  # 加载图像文件
        self.current_pixmap = self.pixmap.copy()
        self.update_label_image()

        # 方法一:离屏绘制  先在一个临时的 QPixmap 上绘制,然后将结果设置到 QLabel
        # 对齐方式
        self.ui.lab_Show.setAlignment(Qt.AlignmentFlag.AlignCenter)  # 图片在Label中居中显示

        # 槽函数
        self.ui.btn_Rect.clicked.connect(self.draw_rectangle)
        self.ui.btn_Circle.clicked.connect(self.draw_circle)
        self.ui.btn_Text.clicked.connect(self.draw_text)
        self.ui.btn_Line.clicked.connect(self.draw_line)
        self.ui.btn_Reset.clicked.connect(self.reset_image)
        self.ui.btn_Save.clicked.connect(self.save_image)

    def draw_rectangle(self):
        """绘制矩形"""
        painter = QPainter(self.current_pixmap)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        # 设置画笔和画刷
        painter.setPen(QPen(QColor(0, 0, 255), 3))
        painter.setBrush(QBrush(QColor(0, 0, 255).lighter(150)))

        # 随机位置绘制矩形
        x = random.randint(50, self.current_pixmap.width() - 150)
        y = random.randint(50, self.current_pixmap.height() - 150)
        width = random.randint(50, 150)
        height = random.randint(50, 150)

        painter.drawRect(x, y, width, height)
        painter.end()

        self.update_label_image()
        self.statusBar().showMessage(f"绘制矩形: ({x}, {y}, {width}, {height})", 3000)

    def draw_circle(self):
        """绘制圆形"""
        painter = QPainter(self.current_pixmap)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        painter.setPen(QPen(QColor(255, 255, 0), 3))
        painter.setBrush(Qt.BrushStyle.NoBrush)  # 设置"无填充"画刷
        #painter.setBrush(QBrush(QColor(255, 255, 0).lighter(200)))

        x = random.randint(50, self.current_pixmap.width() - 150)
        y = random.randint(50, self.current_pixmap.height() - 150)
        diameter = random.randint(100, 150)

        painter.drawEllipse(x, y, diameter, diameter)
        painter.end()

        self.update_label_image()
        self.statusBar().showMessage(f"绘制圆形: ({x}, {y}, {diameter})", 3000)

    def draw_text(self):
        """绘制文字"""
        painter = QPainter(self.current_pixmap)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        painter.setPen(QPen(QColor(255, 0, 0)))

        # 设置字体
        font = QFont()
        font.setPointSize(random.randint(24, 36))
        font.setBold(random.choice([True, False]))
        painter.setFont(font)

        texts = ["Hello!", "PySide6", "绘图演示", "QLabel", "QPixmap", "QPainter"]
        text = random.choice(texts)

        x = random.randint(50, self.current_pixmap.width() - 150)
        y = random.randint(50, self.current_pixmap.height() - 50)

        painter.drawText(x, y, text)
        painter.end()

        self.update_label_image()
        self.statusBar().showMessage(f"绘制文字: '{text}' 位置: ({x}, {y})", 3000)

    def draw_line(self):
        """绘制线条"""
        painter = QPainter(self.current_pixmap)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        painter.setPen(QPen(QColor(255, 0, 255), random.randint(3, 6)))

        x1 = random.randint(50, self.current_pixmap.width() - 50)
        y1 = random.randint(50, self.current_pixmap.height() - 50)
        x2 = random.randint(50, self.current_pixmap.width() - 50)
        y2 = random.randint(50, self.current_pixmap.height() - 50)

        painter.drawLine(x1, y1, x2, y2)
        painter.end()

        self.update_label_image()
        self.statusBar().showMessage(f"绘制线条: ({x1}, {y1}) -> ({x2}, {y2})", 3000)

    def reset_image(self):
        """重置图像"""
        self.current_pixmap = self.pixmap.copy()
        self.update_label_image()
        self.statusBar().showMessage("图像已重置", 3000)

    def save_image(self):
        """保存图像"""
        file_path, _ = QFileDialog.getSaveFileName(self, "保存图像", "", "PNG图像 (*.png);;JPEG图像 (*.jpg)")

        if file_path:
            if self.current_pixmap.save(file_path):
                self.statusBar().showMessage(f"图像已保存到: {file_path}", 3000)
            else:
                self.statusBar().showMessage("保存失败", 3000)

    def update_label_image(self):
        """更新QLabel显示的图像"""
        # 保持宽高比缩放
        scaled_pixmap = self.current_pixmap.scaled(self.ui.lab_Show.geometry().width(),
                                             self.ui.lab_Show.geometry().height(),
                                             Qt.AspectRatioMode.KeepAspectRatio,  # 保持宽高比
                                             Qt.TransformationMode.SmoothTransformation)  # 平滑变换
        self.ui.lab_Show.setPixmap(scaled_pixmap)

① 大体逻辑是 copy() 函数复制一张 QPixmap 图像,然后在这张图进行 QPainter 各图形绘制操作,然后将绘制完的图显示在 QLabel 上;

② 绘制矩形时,矩形内部有填充,但是绘制的圆形图形内部没有填充,通过 painter.setBrush(Qt.BrushStyle.NoBrush) 进行"无填充"画刷的设置;

2.方法二:自定义 QLabel 子类(paintEvent 中绘制)

通过继承 QLabel 并重写 paintEvent 方法,在绘制时叠加图形的方式,较难理解,但能进行丰富的交互功能,并且能对多层 / 多对象进行管理。

主函数程序:

python 复制代码
		# 方法二:自定义 QLabel 子类(paintEvent 中绘制)  通过继承 QLabel 并重写 paintEvent 方法,在绘制时叠加图形
		
        # 加载或创建基础图像
        pixmap2 = QPixmap("Resources/image/110.jpg")  # 加载图像文件
        self.ui.add_Show.set_base_pixmap(pixmap2)

        # 槽函数
        self.ui.spin_R.valueChanged.connect(self.update_color_preview)
        self.ui.spin_G.valueChanged.connect(self.update_color_preview)
        self.ui.spin_B.valueChanged.connect(self.update_color_preview)
        self.ui.add_Rect.clicked.connect(self.add_rectangle)
        self.ui.add_Circle.clicked.connect(self.add_circle)
        self.ui.add_Text.clicked.connect(self.add_text)
        self.ui.add_clear.clicked.connect(self.clear_overlay)
        self.ui.add_save.clicked.connect(self.save_result)

        self.ui.spin_R.setValue(255)

	#方法二函数
    def get_current_color(self):
        """获取当前设置的颜色"""
        m_R = self.ui.spin_R.value()
        m_G = self.ui.spin_G.value()
        m_B = self.ui.spin_B.value()

        return QColor(m_R, m_G, m_B)

    def update_color_preview(self):
        """更新颜色预览"""
        color = self.get_current_color()
        self.ui.add_Color.setStyleSheet(f"background-color: {color.name()}; border: 1px solid black;")
        self.ui.add_Color.setText(f"RGB({color.red()},{color.green()},{color.blue()})")

    def add_rectangle(self):
        """添加矩形"""
        color = self.get_current_color()
        x = random.randint(50, 500)
        y = random.randint(50, 300)
        w = random.randint(50, 150)
        h = random.randint(50, 150)

        self.ui.add_Show.add_shape("rectangle", color, x, y, w, h)
        print(f"添加矩形: ({x}, {y}, {w}, {h}) 颜色: {color.name()}")

    def add_circle(self):
        """添加圆形"""
        color = self.get_current_color()
        x = random.randint(50, 500)
        y = random.randint(50, 300)
        diameter = random.randint(50, 150)

        self.ui.add_Show.add_shape("circle", color, x, y, diameter)
        print(f"添加圆形: ({x}, {y}, {diameter}) 颜色: {color.name()}")

    def add_text(self):
        """添加文字"""
        color = self.get_current_color()
        x = random.randint(50, 500)
        y = random.randint(50, 300)
        texts = ["Hello", "World", "PySide6", "绘图", "QLabel", "叠加"]
        text = random.choice(texts)
        font_size = random.randint(12, 24)

        self.ui.add_Show.add_shape("text", color, x, y, text, font_size)
        print(f"添加文字: '{text}' 位置: ({x}, {y}) 大小: {font_size}")

    def clear_overlay(self):
        """清除叠加层"""
        self.ui.add_Show.clear_overlay()
        print("已清除所有叠加图形")

    def save_result(self):
        """保存最终结果(基础图像+叠加图形)"""

        # 创建临时QPixmap来绘制最终结果
        final_pixmap = QPixmap(self.ui.add_Show.base_pixmap.size())
        final_pixmap.fill(Qt.GlobalColor.transparent)

        # 绘制基础图像
        painter = QPainter(final_pixmap)
        painter.drawPixmap(0, 0, self.ui.add_Show.base_pixmap)

        # 绘制所有叠加图形
        for shape_type, args in self.ui.add_Show.overlay_shapes:
            self.draw_shape_on_pixmap(painter, shape_type, *args)

        painter.end()

        # 保存文件
        file_path, _ = QFileDialog.getSaveFileName(self, "保存结果图像", "", "PNG图像 (*.png);;JPEG图像 (*.jpg)")

        if file_path:
            if final_pixmap.save(file_path):
                print(f"结果已保存到: {file_path}")
            else:
                print("保存失败")

    def draw_shape_on_pixmap(self, painter, shape_type, *args):
        """在QPixmap上绘制图形"""
        if shape_type == "rectangle":
            color, x, y, w, h = args
            painter.setPen(QPen(color, 3))
            painter.setBrush(QBrush(color.lighter(150)))
            painter.drawRect(x, y, w, h)

        elif shape_type == "circle":
            color, x, y, diameter = args
            painter.setPen(QPen(color, 3))
            painter.setBrush(QBrush(color.lighter(200)))
            painter.drawEllipse(x, y, diameter, diameter)

        elif shape_type == "text":
            color, x, y, text, font_size = args
            painter.setPen(QPen(color))
            font = QFont()
            font.setPointSize(font_size)
            painter.setFont(font)
            painter.drawText(x, y, text)

DrawableLabel(QLabel) 可绘制的 QLabel 子类程序:

python 复制代码
class DrawableLabel(QLabel):
    """可绘制的QLabel子类"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.overlay_shapes = []  # 存储叠加的图形
        self.base_pixmap = None

    def set_base_pixmap(self, pixmap):
        """设置基础图像"""
        self.base_pixmap = pixmap
        # 保持宽高比缩放
        scaled_pixmap = pixmap.scaled(self.geometry().width(),
                                      self.geometry().height(),
                                      Qt.AspectRatioMode.KeepAspectRatio,  # 保持宽高比
                                      Qt.TransformationMode.SmoothTransformation)  # 平滑变换
        self.setPixmap(scaled_pixmap)

    def add_shape(self, shape_type, *args):
        """添加一个图形到叠加层"""
        self.overlay_shapes.append((shape_type, args))
        self.update()  # 触发重绘

    def clear_overlay(self):
        """清除所有叠加图形"""
        self.overlay_shapes.clear()
        self.update()

    def paintEvent(self, event: QPaintEvent):
        """重写绘制事件"""
        # 先调用父类的绘制,显示基础图像
        super().paintEvent(event)

        # 创建QPainter在QLabel上绘制
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        # 绘制所有叠加图形
        for shape_type, args in self.overlay_shapes:
            self.draw_shape(painter, shape_type, *args)

        painter.end()

    def draw_shape(self, painter, shape_type, *args):
        """根据类型绘制图形"""
        if shape_type == "rectangle":
            color, x, y, w, h = args
            painter.setPen(QPen(color, 3))
            #painter.setBrush(QBrush(color.lighter(150)))
            painter.setBrush(Qt.BrushStyle.NoBrush)  # 设置"无填充"画刷
            painter.drawRect(x, y, w, h)

        elif shape_type == "circle":
            color, x, y, diameter = args
            painter.setPen(QPen(color, 3))
            painter.setBrush(QBrush(color.lighter(200)))
            painter.drawEllipse(x, y, diameter, diameter)

        elif shape_type == "text":
            color, x, y, text, font_size = args
            painter.setPen(QPen(color))
            font = QFont()
            font.setPointSize(font_size)
            painter.setFont(font)
            painter.drawText(x, y, text)

        elif shape_type == "line":
            color, x1, y1, x2, y2, width = args
            painter.setPen(QPen(color, width))
            painter.drawLine(x1, y1, x2, y2)

然后进行组件提升:


最终效果:

①创建一个继承自QLabel的类;

②重写paintEvent方法,在其中先调用基类的paintEvent,然后使用QPainter在QLabel上绘制图形(注意:绘制图形时,需要考虑到QPixmap的缩放和位置,因为QLabel可能会根据其sizePolicy和缩放设置来调整显示的图像);

③基类的paintEvent会绘制QLabel的内容,包括文本和图像。如果我们已经设置了Pixmap,那么基类会绘制这个Pixmap。然后我们再在上面绘制矩形;

④从上述例程中可以看到使用的常值去进行图形的位置绘制,绘制出的各种图形可能会落在图形的外侧,这里读者可以根据上面缩放设置来优化该代码,让所绘制的图形都落在图像中,实践是最快速的学习方式。

注意点:

DrawableLabel 类中所定义的 self.overlay_shapes = [] # 存储叠加的图形,通过改列表可以对所画的图像进行撤销、重画、删除部分图形的操作,能更好的管控所绘画的对象。

3.两种方法对比

特性 方法一:离屏绘制 方法二:自定义QLabel子类
实现复杂度 简单,直接操作QPixmap 中等,需要继承并重写paintEvent
性能 较高,一次绘制完成 每次重绘都需要重新绘制叠加层
内存使用 需要保持两份图像(原始和当前) 只需原始图像,叠加层是动态绘制的
交互性 绘制后图像固定,修改需重绘 叠加层可独立管理,方便单独清除或修改
适用场景 需要保存绘制结果的场景 需要频繁修改叠加层或实时预览的场景
图像保存 直接保存当前QPixmap 需要重新合成图像再保存

基本原则:如果绘制操作本身的计算成本高于复制图像的成本,用方法一;如果绘制操作简单但需要频繁更新和交互,用方法二。

二、QPainter 创建遮罩和光标

python 复制代码
		# 对齐方式
        self.ui.lab_Show.setAlignment(Qt.AlignmentFlag.AlignCenter)  # 图片在Label中居中显示
        #创建自定义光标(结合遮罩)
        self.pixmap01 = QPixmap("Resources/image/3.jpeg")
        self.update_label_image()

        # 创建光标图像(彩色)
        pixmap = QPixmap(64, 64)
        pixmap.fill(Qt.GlobalColor.transparent)  # 透明背景

        painter = QPainter(pixmap)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        # 绘制一个简单的十字光标
        painter.setPen(QPen(QColor(255, 255, 0), 5))  # 红色十字
        painter.drawLine(32, 0, 32, 64)  # 垂直线
        painter.drawLine(0, 32, 64, 32)  # 水平线

        # 绘制中心点
        painter.setBrush(QBrush(QColor(255, 255, 0)))
        painter.drawEllipse(28, 28, 8, 8)
        painter.end()

        # 从彩色图像创建遮罩位图(会自动抖动)
        mask = QBitmap.fromPixmap(pixmap)

        # 创建自定义光标(需要图像和遮罩)
        cursor = QCursor(mask, mask, 16, 16)  # 热点在中心(16,16)

        #彩色光标显示
        #cursor = QCursor(pixmap, 16, 16)  # 热点在中心(16,16)

        # 设置光标
        self.ui.lab_Show.setCursor(cursor)

    def update_label_image(self):
        """更新QLabel显示的图像"""
        # 保持宽高比缩放
        scaled_pixmap = self.pixmap01.scaled(self.ui.lab_Show.geometry().width(),
                                                   self.ui.lab_Show.geometry().height(),
                                                   Qt.AspectRatioMode.KeepAspectRatio,  # 保持宽高比
                                                   Qt.TransformationMode.SmoothTransformation)  # 平滑变换
        self.ui.lab_Show.setPixmap(scaled_pixmap)
        

上面例程展示了通过 QPixmap 图形如何制作光标和遮罩,并通过 mask = QBitmap.fromPixmap(pixmap) 操作从彩色图像创建遮罩位图,应用起来简单高效。

三、QPicture 绘图命令的记录器和回放器

1.工作原理:记录与回放

QPicture 的核心工作流程分为两个阶段:

① 记录阶段:创建一个 QPicture,用 QPainter 在上面"录制"所有绘图命令;

②回放阶段:在任意绘图设备(如窗口、打印机、另一张图片)上,用 QPainter "播放"录制的命令。

2.实践应用

python 复制代码
		#QPicture 保存和加载绘图文件
        self.picture = QPicture()
        self.current_file = None

		self.ui.lab_Show2.setAlignment(Qt.AlignmentFlag.AlignCenter)  # 图片在Label中居中显示

        #槽函数
        self.ui.btn_create.clicked.connect(self.create_sample_picture)
        self.ui.btn_clear.clicked.connect(self.picture_clear)
        self.ui.btn_save.clicked.connect(self.save_picture)
        self.ui.btn_load.clicked.connect(self.load_picture)

        self.ui.lab_Show2.setStyleSheet("border: 2px solid gray; background-color: white;")

    def create_sample_picture(self):
        """创建一个示例图形"""
        self.picture = QPicture()
        painter = QPainter(self.picture)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        # 绘制一个笑脸
        # 脸
        painter.setBrush(QColor(255, 255, 200))
        painter.setPen(QPen(Qt.GlobalColor.black, 2))
        painter.drawEllipse(50, 50, 200, 200)

        # 眼睛
        painter.setBrush(Qt.GlobalColor.white)
        painter.drawEllipse(100, 100, 30, 30)
        painter.drawEllipse(170, 100, 30, 30)
        painter.setBrush(Qt.GlobalColor.black)
        painter.drawEllipse(110, 110, 10, 10)
        painter.drawEllipse(180, 110, 10, 10)

        # 嘴巴
        painter.setBrush(Qt.GlobalColor.red)
        painter.drawChord(80, 120, 140, 80, 30 * 16, 120 * 16)

        # 添加文字
        painter.setPen(Qt.GlobalColor.blue)
        painter.drawText(60, 280, "这是一个保存到 QPicture 的笑脸")

        painter.end()

        self.current_file = None
        self.statusBar().showMessage("状态: 已创建新图形", 3000)
        self.update_preview()

    def picture_clear(self):
        self.ui.lab_Show2.clear()

    def save_picture(self):
        """将 QPicture 保存到文件"""
        if self.picture.isNull():
            QMessageBox.warning(self, "错误", "没有可保存的图形")
            return

        file_path, _ = QFileDialog.getSaveFileName(self, "保存 QPicture", "", "Picture Files (*.pic);;All Files (*)")

        if file_path:
            # 确保文件扩展名
            if not file_path.endswith('.pic'):
                file_path += '.pic'

            if self.picture.save(file_path):
                self.current_file = file_path
                self.statusBar().showMessage(f"状态: 已保存到 {os.path.basename(file_path)}", 3000)

                # 显示文件大小
                file_size = os.path.getsize(file_path)
            else:
                QMessageBox.warning(self, "错误", "保存失败")

    def load_picture(self):
        """从文件加载 QPicture"""
        file_path, _ = QFileDialog.getOpenFileName(self, "加载 QPicture", "", "Picture Files (*.pic);;All Files (*)")

        if file_path and os.path.exists(file_path):
            loaded_picture = QPicture()

            if loaded_picture.load(file_path):
                self.picture = loaded_picture
                self.current_file = file_path

                # 显示文件信息
                file_size = os.path.getsize(file_path)

                self.statusBar().showMessage(f"状态: 已加载 ({file_size} 字节) - {os.path.basename(file_path)}", 3000)
                self.update_preview()
            else:
                QMessageBox.warning(self, "错误", "加载失败,可能不是有效的 QPicture 文件")

    def update_preview(self):
        """更新预览区域的显示"""
        if self.picture.isNull():
            self.ui.lab_Show2.setText("无图形可显示")
            return

        # 创建一个临时的 QPixmap 用于预览
        pixmap = QPixmap(400, 300)
        pixmap.fill(Qt.GlobalColor.white)

        painter = QPainter(pixmap)

        # 获取 QPicture 的边界矩形,以便居中显示
        picture_rect = self.picture.boundingRect()

        if not picture_rect.isEmpty():
            # 计算缩放比例以适应预览区域
            scale_x = 380 / picture_rect.width()
            scale_y = 280 / picture_rect.height()
            scale = min(scale_x, scale_y, 1.0)  # 不超过1倍,防止放大

            # 计算居中位置
            offset_x = (300 - picture_rect.width() * scale) / 2
            offset_y = (160 - picture_rect.height() * scale) / 2

            painter.translate(offset_x, offset_y)
            painter.scale(scale, scale)

        # 绘制 QPicture
        painter.drawPicture(0, 0, self.picture)
        painter.end()

        self.ui.lab_Show2.setPixmap(pixmap)

① 先进行 self.picture = QPicture() 、 painter = QPainter(self.picture) 记录器的创建,当笑脸图形绘制完成后,painter 的绘制流程和命令都已被记录并存储在 QPicture 类中,此时可以进行保存或者显示;

② update_preview 函数中 pixmap = QPixmap(400, 300) 创建一个临时的 QPixmap 用于预览,然后 painter.drawPicture(0, 0, self.picture) 进行 保存在 QPicture 类中数据的绘制,并绘制在 pixmap 上,显示于 QLabel 控件上;

③ 通过 self.picture.save(file_path) 函数进行保存 QPicture 类中的绘制数据,后缀为 pic ;

④ 通过 loaded_picture.load(file_path) 函数可以对 QPicture 文件进行加载并进行显示。

总结

设备选择策略
QWidget:实时交互界面、动画、自定义控件;
QImage:图像处理、滤镜、像素分析、文件I/O;
QPixmap:界面元素、图标、缓存图像、图像合成;
QBitmap:遮罩、光标、简单形状、内存敏感场景;
QPicture:矢量图形、重复绘制、模板化绘图。

本节示例展示了 QPainter 在不同设备上的强大能力。理解每种设备的特性和适用场景,可以让你在 PySide6 开发中选择最合适的绘图方式。

本节源码路径为:PySide6基本窗口控件深度补充_窗口绘图类

相关推荐
_深海凉_26 分钟前
LeetCode热题100-颜色分类
python·算法·leetcode
AC赳赳老秦1 小时前
OpenClaw email技能:批量发送邮件、自动回复,高效处理工作邮件
运维·人工智能·python·django·自动化·deepseek·openclaw
zhaoshuzhaoshu1 小时前
Python 语法之数据结构详细解析
python
AI问答工程师1 小时前
Meta Muse Spark 的"思维压缩"到底是什么?我用 Python 复现了核心思路(附代码)
人工智能·python
zfan5202 小时前
python对Excel数据处理(1)
python·excel·pandas
小饕2 小时前
我从零搭建 RAG 学到的 10 件事
python
老歌老听老掉牙3 小时前
PyQt5+Qt Designer实战:可视化设计智能参数配置界面,告别手动布局时代!
python·qt
格鸰爱童话3 小时前
向AI学习项目技能(六)
java·人工智能·spring boot·python·学习
悟空爬虫-彪哥3 小时前
VRChat开发环境配置,零基础教程
python
数据知道3 小时前
《 Claude Code源码分析与实践》专栏目录
python·ai·github·claude code·claw code