第十七节_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基本窗口控件深度补充_窗口绘图类

相关推荐
c骑着乌龟追兔子6 小时前
Day 42 复习日
python
Robot侠6 小时前
视觉语言导航从入门到精通(二)
开发语言·人工智能·python·llm·vln
无限大.6 小时前
为什么玩游戏需要独立显卡?——GPU与CPU的分工协作
python·玩游戏
deephub6 小时前
llama.cpp Server 引入路由模式:多模型热切换与进程隔离机制详解
人工智能·python·深度学习·llama
简单点好不好6 小时前
2025--简单点--python之状态模式
开发语言·python·状态模式
棒棒的皮皮6 小时前
【OpenCV】Python图像处理之仿射变换
图像处理·python·opencv·计算机视觉
weixin_446260856 小时前
FastF1: 轻松获取和分析F1数据的Python包
开发语言·python
我送炭你添花6 小时前
Pelco KBD300A 模拟器:06.用 PyQt5 实现 1:1 像素级完美复刻 Pelco KBD300A 键盘
python·qt·自动化·运维开发
山土成旧客6 小时前
【Python学习打卡-Day22】启航Kaggle:从路径管理到独立项目研究的全方位指南
开发语言·python·学习