如何在单个PyQt5窗口中实现两种动画(旋转扇形圆与轨迹)
解决PyQt5中Matplotlib双动画(旋转扇形+轨迹)的合并问题
问题描述
需要在单个PyQt5窗口的Matplotlib极坐标图中同时实现两个动画:
- 旋转的红色扇形动画
- 从JSON文件读取的轨迹绘制动画
现有代码单独运行两个动画正常,但合并时出现错误,核心是不理解FuncAnimation的使用和参数传递规则。
问题分析
原有代码的核心问题:
- 使用两个独立的
FuncAnimation实例,其中update1函数调用ax.clear()会清除整个坐标轴,包括轨迹线,导致两个动画互相干扰。 fargs传递了不必要的参数,增加了逻辑混乱。- 极坐标下错误使用了笛卡尔坐标系的
set_xlim/set_ylim,应该用极坐标专属的半径范围设置。 - 扇形每次重新创建而非更新已有对象,效率低且容易冲突。
修正后的完整代码
import numpy as np import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure from PyQt5.QtWidgets import QWidget, QGridLayout, QGroupBox # 假设make_points_between是你已实现的轨迹插值函数 def make_points_between(trajectory): # 示例实现:将轨迹点插值为更密集的点 x = np.array([p[0] for p in trajectory]) y = np.array([p[1] for p in trajectory]) t = np.linspace(0, 1, len(x)) t_new = np.linspace(0, 1, len(x)*10) x_new = np.interp(t_new, t, x) y_new = np.interp(t_new, t, y) return np.vstack((x_new, y_new)) class MyFigurePolar(FigureCanvas): def __init__(self, width=5, height=4, dpi=100, trajectory_data=None): self.fig = Figure(figsize=(width, height), dpi=dpi) self.axes = self.fig.add_subplot(111, projection='polar') super().__init__(self.fig) # 初始化极坐标参数 self.r = 7 # 外圆半径 self.period = np.pi / 64 # 扇形角度范围 self.trajectory_points = make_points_between(trajectory_data).T # 初始化动画元素 self._init_static_elements() self._init_animation_elements() self._start_animations() def _init_static_elements(self): # 绘制固定外圆 theta = np.linspace(0, 2 * np.pi, 100) self.axes.plot(theta, np.ones(100) * self.r, linestyle='-', color='black') # 设置极坐标属性 self.axes.set_theta_zero_location("N") self.axes.set_theta_direction(-1) self.axes.set_yticklabels([5000, 10000, 15000, 20000, 25000]) self.axes.set_rmax(self.r * 1.1) # 设置最大半径 def _init_animation_elements(self): # 初始化轨迹线 self.trajectory_ln, = self.axes.plot([], [], color='blue') # 初始化扇形(用fill_between创建,保存对象以便更新) self.sector = self.axes.fill_between([], [], [], alpha=0.5, facecolor='red') def _update_trajectory(self, num): # 更新轨迹数据:转换为极坐标 x = self.trajectory_points[:num, 0] y = self.trajectory_points[:num, 1] theta = np.arctan2(y, x) # 注意极坐标theta是从x轴正方向逆时针转,这里要对应 r = np.sqrt(x**2 + y**2) self.trajectory_ln.set_data(theta, r) return self.trajectory_ln, def _update_sector(self, frame): # 更新扇形数据,不清除坐标轴 start = frame * np.pi / 180 end = start - self.period # 更新扇形的顶点数据 theta_vals = np.linspace(end, start, 20) r_vals = np.zeros_like(theta_vals) r_vals[:] = self.r # 获取fill_between的路径集合并更新 path = self.sector.get_paths()[0] vertices = np.vstack((np.append(theta_vals, [end, start]), np.append(r_vals, [0, 0]))).T path.vertices = vertices return self.sector, def _start_animations(self): # 轨迹动画:帧数量等于轨迹点数量,速度根据总时长调整 self.trajectory_anim = FuncAnimation( self.fig, self._update_trajectory, frames=len(self.trajectory_points), interval=10000 / len(self.trajectory_points), blit=True ) # 扇形旋转动画:360帧,循环播放 self.sector_anim = FuncAnimation( self.fig, self._update_sector, frames=360, interval=15, blit=True, repeat=True ) class MainWindow(QWidget): def __init__(self, json_data): super().__init__() self.init_ui(json_data) def init_ui(self, json_data): self.groupBox = QGroupBox("双动画演示") self.gridlayout = QGridLayout(self.groupBox) # 创建带动画的极坐标画布 self.figure = MyFigurePolar(trajectory_data=json_data['trajectory']) self.gridlayout.addWidget(self.figure, 0, 1) main_layout = QGridLayout() main_layout.addWidget(self.groupBox, 0, 0) self.setLayout(main_layout) self.setWindowTitle("PyQt5 + Matplotlib 双动画") # 示例JSON数据(替换为你的实际数据) sample_json_data = { 'trajectory': [ (1, 1), (2, 3), (3, 2), (4, 4), (3, 5), (2, 4), (1, 3), (0, 0) ] } if __name__ == "__main__": import sys from PyQt5.QtWidgets import QApplication app = QApplication(sys.argv) win = MainWindow(sample_json_data) win.show() sys.exit(app.exec_())
关键修改说明
- 避免坐标轴清除:不再使用
ax.clear(),直接更新已有动画元素的数据源,确保两个动画互不干扰。 - 动画元素复用:提前创建轨迹线和扇形对象,每次更新仅修改它们的顶点/数据,而非重新绘制。
- 统一极坐标处理:正确设置极坐标的零位置、方向和半径范围,替换错误的笛卡尔坐标系范围设置。
- 分离动画逻辑:将轨迹和扇形的更新逻辑拆分为独立函数,各自维护自己的动画元素,通过两个
FuncAnimation分别驱动,但确保它们操作同一坐标轴的不同元素。 - 参数传递优化:移除不必要的
fargs参数,直接通过类实例属性访问数据,简化逻辑。
内容的提问来源于stack exchange,提问作者Алина Люлюшкина
相关产品推荐
相关产品推荐

