PySide6 中 QWidget 动态绘制与视频录制教程

雨宇小哥_7876

雨宇小哥_7876

2025-10-24

446人浏览

原创

PySide6 中 QWidget 动态绘制与视频录制教程

本教程详细介绍了如何在 pyside6 应用中实现 qwidget 的动态内容绘制,并同时将这些动态帧捕获并保存为视频文件。文章将指导读者正确使用 qpainter 进行界面绘制,并通过 qwidget 的 `grab()` 方法结合 `imageio` 库高效地将实时画面转换为视频帧,避免常见的绘制上下文错误,确保流畅的显示与录制。

引言:PySide6 动态绘制与视频生成的需求

在许多图形界面应用中,我们可能需要在一个 QWidget 上实时显示动态内容(例如动画、数据可视化),并同时将这些动态变化的过程录制成视频或 GIF。直接在 paintEvent 中尝试将内容绘制到 QImage 上,再将 QImage 渲染回 QWidget,往往会导致 QPainter 上下文冲突或 QWidget::render 调用错误。本文将提供一个健壮的解决方案,利用 QPainter 进行高效的界面绘制,并通过 QWidget.grab() 结合 imageio 库实现无缝的视频帧捕获与生成。

核心概念与技术栈

要实现上述目标,我们需要掌握以下核心技术和库:

  1. PySide6 (Qt for Python): 用于构建图形用户界面。
    • QWidget: 基础的用户界面组件。
    • QPainter: 用于在绘制设备(如 QWidget、QPixmap、QImage)上进行低级绘制。
    • QTimer: 用于定时触发更新,实现动画效果。
    • QPixmap, QImage: 用于处理图像数据。
  2. imageio: 一个强大的 Python 库,用于读取和写入各种图像和视频文件格式。
    • 需要安装 imageio 和 imageio[ffmpeg](用于支持 FFmpeg 编解码器,以便生成常见的视频格式如 AVI, MP4)。
  3. NumPy: 用于高效地处理图像数据,将 QImage 转换为 imageio 可接受的 NumPy 数组格式。

环境准备

在开始之前,请确保您的 Python 环境中已安装必要的库:

pip install PySide6 imageio numpy
pip install imageio[ffmpeg] # 确保视频编码功能可用

实现动态绘制与帧捕获

我们将创建一个自定义的 PlotWidget 类,它继承自 QWidget。这个组件将负责:

  1. 定时更新: 使用 QTimer 定期触发绘制和帧捕获。
  2. 界面绘制: 在 paintEvent 中使用 QPainter 绘制动态内容。
  3. 帧捕获与视频生成: 在定时器触发的方法中,捕获当前 QWidget 的内容,并将其追加到 imageio 视频写入器中。

1. PlotWidget 初始化

在 PlotWidget 的构造函数中,我们将设置定时器、初始化视频写入器,并定义窗口大小。

import imageio, numpy as np
from PySide6.QtWidgets import QApplication, QWidget
from PySide6.QtCore import QPoint, QRect, QTimer, Qt
from PySide6.QtGui import QPainter, QPointList, QImage

WIDTH = 720
HEIGHT = 720

class PlotWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("PySide6 动态绘制与视频录制")
        self.setFixedSize(WIDTH, HEIGHT) # 固定窗口大小

        self._timer = QTimer(self)
        self._timer.setInterval(100) # 每100毫秒触发一次,即10帧/秒
        self._timer.timeout.connect(self.frame)

        self._points = QPointList() # 示例数据,用于绘制

        self._totalFrames = 100 # 录制100帧后停止
        # 初始化 imageio 视频写入器,指定输出文件名和帧率
        self._vid_writer = imageio.get_writer('output_video.avi', fps=10) 

        self._timer.start() # 启动定时器

2. paintEvent 实现

paintEvent 负责在 QWidget 上进行绘制。关键在于,QPainter 应该直接作用于 self (即 QWidget 实例),而不是一个临时的 QImage。

macOS UI 自动化技能包 —— 用 Python 控制你的 Mac
macOS UI 自动化技能包 —— 用 Python 控制你的 Mac

让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限

下载
    def paintEvent(self, event):
        with QPainter(self) as painter: # QPainter 直接作用于当前 QWidget
            rect = QRect(QPoint(0, 0), self.size())
            painter.fillRect(rect, Qt.white) # 填充背景
            painter.setPen(Qt.red) # 设置画笔颜色
            painter.drawPoints(self._points) # 绘制示例点

3. frame 方法:动画逻辑与帧捕获

frame 方法由 QTimer 定时调用。它负责更新绘制数据、触发 paintEvent、捕获当前 QWidget 的画面,并将其追加到视频文件中。

    def frame(self):
        # 示例:更新绘制数据,这里只是简单地清空并添加一个点
        self._points.clear()
        self._points.append(QPoint(np.random.randint(0, WIDTH), np.random.randint(0, HEIGHT)))

        # 如果还有帧需要录制
        if self._totalFrames > 0:
            self.update() # 触发 paintEvent,更新界面显示

            # 捕获 QWidget 的当前内容
            pixmap = self.grab() 
            # 将 QPixmap 转换为 QImage,并确保格式为 RGB888,便于 NumPy 处理
            qimg = pixmap.toImage().convertToFormat(QImage.Format_RGB888)

            # 将 QImage 的像素数据转换为 NumPy 数组
            # 注意:这里直接访问 QImage 的底层数据,效率高
            # strides 参数是关键,确保 NumPy 正确解析内存布局
            array = np.ndarray((qimg.height(), qimg.width(), 3), 
                               buffer=qimg.constBits(), 
                               strides=[qimg.bytesPerLine(), 3, 1], 
                               dtype=np.uint8)

            # 如果视频写入器未关闭,则追加帧
            if not self._vid_writer.closed:
                self._vid_writer.append_data(array)
        else:
            # 录制完成后,停止定时器并关闭视频写入器
            self._timer.stop()
            if not self._vid_writer.closed:
                self._vid_writer.close()
            print("视频录制完成!")

        self._totalFrames -= 1 # 减少剩余帧数

4. 资源清理 (closeEvent)

为了确保视频文件正确关闭,即使程序异常退出,也应在 QWidget 关闭时执行清理操作。

    def closeEvent(self, event):
        if not self._vid_writer.closed:
            self._vid_writer.close() # 关闭视频写入器
        self._timer.stop() # 停止定时器
        event.accept() # 接受关闭事件

完整示例代码

将以上部分整合,形成一个可运行的完整示例:

import imageio, numpy as np
from PySide6.QtWidgets import QApplication, QWidget
from PySide6.QtCore import QPoint, QRect, QTimer, Qt
from PySide6.QtGui import QPainter, QPointList, QImage

WIDTH = 720
HEIGHT = 720

class PlotWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("PySide6 动态绘制与视频录制")
        self.setFixedSize(WIDTH, HEIGHT)

        self._timer = QTimer(self)
        self._timer.setInterval(100) # 100ms = 10 FPS
        self._timer.timeout.connect(self.frame)

        self._points = QPointList()

        self._totalFrames = 100 # 录制100帧
        self._vid_writer = imageio.get_writer('output_video.avi', fps=10) # 输出视频文件

        self._timer.start() # 启动定时器

    def closeEvent(self, event):
        """
        在窗口关闭时,确保视频写入器和定时器被正确关闭。
        """
        if not self._vid_writer.closed:
            self._vid_writer.close()
            print("视频写入器已关闭。")
        self._timer.stop()
        event.accept()

    def frame(self):
        """
        定时器触发的方法,用于更新数据、重绘界面并捕获帧。
        """
        # 示例:更新绘制数据,这里只是简单地添加一个随机点
        self._points.clear()
        self._points.append(QPoint(np.random.randint(0, WIDTH), np.random.randint(0, HEIGHT)))

        if self._totalFrames > 0:
            self.update() # 触发 paintEvent 重新绘制界面

            # 捕获 QWidget 的当前显示内容
            pixmap = self.grab()
            # 转换为 QImage,并指定 RGB888 格式,便于后续 NumPy 处理
            qimg = pixmap.toImage().convertToFormat(QImage.Format_RGB888)

            # 将 QImage 的像素数据转换为 NumPy 数组
            # 注意 strides 参数确保正确解析 QImage 的内存布局
            array = np.ndarray((qimg.height(), qimg.width(), 3), 
                               buffer=qimg.constBits(), 
                               strides=[qimg.bytesPerLine(), 3, 1], 
                               dtype=np.uint8)

            # 将 NumPy 数组作为一帧追加到视频文件
            if not self._vid_writer.closed:
                self._vid_writer.append_data(array)
        else:
            # 录制帧数达到上限,停止定时器并关闭视频写入器
            self._timer.stop()
            if not self._vid_writer.closed:
                self._vid_writer.close()
            print(f"视频录制完成,已生成 {self._totalFrames} 帧,文件:output_video.avi")
            # 录制完成后可以考虑关闭应用程序
            QApplication.instance().quit()

        self._totalFrames -= 1

    def paintEvent(self, event):
        """
        QPainter 绘制事件,用于在 QWidget 上绘制内容。
        """
        with QPainter(self) as painter: # QPainter 直接作用于当前 QWidget
            rect = QRect(QPoint(0, 0), self.size())
            painter.fillRect(rect, Qt.white) # 填充白色背景

            painter.setPen(Qt.red) # 设置画笔颜色为红色
            painter.setBrush(Qt.NoBrush) # 不填充
            painter.drawPoints(self._points) # 绘制随机点

if __name__ == '__main__':
    app = QApplication([])
    plot_widget = PlotWidget()
    plot_widget.show()
    app.exec()

注意事项与最佳实践

  1. QPainter 上下文: 始终确保 QPainter 在其绘制设备上是唯一的活动实例。在 paintEvent 中,QPainter(self) 是正确的用法,因为它直接在当前 QWidget 上绘制。避免在同一个 paintEvent 周期内尝试在不同设备(如 QImage 和 QWidget)之间切换 QPainter。
  2. 帧捕获时机: self.grab() 应该在 self.update() 之后调用,以确保捕获到的是最新的绘制内容。
  3. 图像格式转换: QPixmap 转换为 QImage 时,选择合适的格式(如 QImage.Format_RGB888 或 Format_ARGB32)可以简化后续与 NumPy 的集成。imageio 通常期望 RGB 格式的 NumPy 数组。
  4. NumPy 数组转换效率: 使用 qimg.constBits() 直接访问 QImage 的底层数据缓冲区,并结合 np.ndarray 的 buffer 和 strides 参数,是最高效的转换方式,避免了数据复制。
  5. 资源管理: 务必在应用关闭或视频录制完成后,调用 _vid_writer.close() 来释放文件句柄并确保视频文件完整。closeEvent 是一个理想的清理位置。
  6. 帧率控制: QTimer.setInterval() 和 imageio.get_writer(fps=...) 的帧率应保持一致,以确保视频播放速度与预期相符。
  7. 性能考虑: 对于高分辨率或高帧率的视频录制,图像转换和写入操作可能会消耗较多 CPU 资源。可以考虑在单独的线程中执行视频写入操作,以避免阻塞 UI 线程。

总结

通过本教程,我们学习了如何在 PySide6 中优雅地实现 QWidget 的动态绘制,并同时将这些动态画面录制成视频。关键在于理解 QPainter 的绘制上下文,利用 QWidget.grab() 进行界面捕获,并通过 imageio 库将捕获的图像帧高效地转换为视频。这种方法避免了常见的绘制错误,并提供了一个清晰、专业的解决方案,适用于需要实时动画显示和视频输出的 PySide6 应用。

相关文章

PHP速学视频免费教程(入门到精通)
PHP速学视频免费教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

python 数据可视化 重绘

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
C++ Qt图形开发
C++ Qt图形开发

本专题专注于 C++ Qt框架在图形界面开发中的应用,系统讲解窗口设计、信号与槽机制、界面布局、事件处理、数据库连接与跨平台打包等核心技能,通过多个桌面应用项目实战,帮助学员快速掌握 Qt 框架并独立完成跨平台GUI软件的开发。

2025.08.15

5702

7

C++桌面程序开发
C++桌面程序开发

本专题聚焦 C++ 在桌面端图形界面应用开发中的实战应用,系统讲解 Qt 框架的信号与槽机制、界面布局管理、事件响应、数据交互、样式美化与多窗口应用设计等核心知识。通过实战案例(如记事本应用、数据管理工具、系统监控面板),帮助学习者掌握 从UI设计到完整桌面软件开发的C++工程化能力。

2025.10.23

1514

13

C++ 图形界面开发基础(Qt方向)
C++ 图形界面开发基础(Qt方向)

本专题系统讲解 使用 C++ 与 Qt 进行图形界面(GUI)开发的核心技能,内容涵盖 Qt 项目结构、窗口组件、信号与槽机制、事件处理、布局管理、资源管理,以及跨平台编译与打包流程。通过多个小型桌面应用实战案例,帮助学习者掌握从界面设计到功能实现的完整 GUI 开发能力。

2025.12.05

1441

15

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

2023.07.18

6607

5

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

2023.08.10

2748

6

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

2023.08.10

4638

6

DBeaver怎么导入数据
DBeaver怎么导入数据

DBeaver数据导入专题,整理Excel、CSV、SQL、TXT等常见文件的数据导入方法,涵盖目标数据库选择、数据表创建、字段映射、数据类型设置及导入参数配置等操作,帮助用户将本地文件中的数据写入数据库,并了解批量导入、重复数据处理和导入失败等常见问题的解决方法。

2026.11.05

0

12

DBeaver安全防护教程大全
DBeaver安全防护教程大全

DBeaver安全防护专题,围绕数据库连接安全、账号密码保护和访问权限管理展开,整理SSL/TLS加密连接、SSH隧道、数据库凭据保存及证书验证等配置方法,同时介绍敏感信息保护、远程连接风险和高权限账号使用等注意事项,帮助用户提高数据库日常管理的安全性。

2026.11.05

0

12

DBeaver连接创建与配置教程
DBeaver连接创建与配置教程

本专题聚焦DBeaver数据库连接配置与管理,覆盖从新建连接、选择数据库类型到填写连接参数和验证连接的完整流程。针对MySQL、PostgreSQL、SQL Server等常见数据库,整理相应的连接设置方法,并提供驱动缺失、认证失败和连接异常等问题的排查方向。

2026.11.04

0

16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程