文章目录
- 时间轴
- 单帧跳转
- 动图绘制函数
- 接口优化
- 📈一 三维绘图系统 📈二 多图绘制系统📈三 坐 标 轴 定 制
- 📈四 定制绘图风格 📈五 数据生成导入
- 源码地址 Python打造动态绘图系统
时间轴
三维并不是人类理解的极限,毕竟我们生活在思维时空中。所以接下来要做的,就是四维图形——加一个时间轴。
实际上,这个项目创建之初就已经考虑到动图绘制的问题,毕竟默认的绘图坐标是txyz。但是,如果想实现动图绘制,就必须要先设置一个额外的线程,用于动态更新图像,否则绘图循环压迫主进程,会导致界面变得奇卡无比。
接下来,就开始实现绘制动图的需求,第一步,绘制UI。先在setFrmCtrl中添加
# 动画控制
frm = ttk.Frame(frmCtrl, width=320)
frm.pack(side=tk.TOP, fill=tk.X)
self.setAnimateFrame(frm)
然后实现具体的self.setAnimateFrame
def setAnimateFrame(self, frm):
pDct = dict(side=tk.LEFT, fill=tk.X, padx=2)
self.aniDelay = tk.StringVar()
self.aniDelay.set(100)
ttk.Label(frm, text="延时/毫秒").pack(**pDct)
ttk.Entry(frm, width=5, textvariable=self.aniDelay).pack(**pDct)
self.aniFrameNum = tk.StringVar()
self.aniFrameNum.set(100)
ttk.Label(frm, text="帧数").pack(**pDct)
ttk.Entry(frm, width=5, textvariable=self.aniFrameNum).pack(**pDct)
self.tIndex = 0 # 当前帧数
ttk.Button(frm, width=3, text= "⇦",
command=self.btnPreFrame).pack(**pDct)
ttk.Button(frm, width=3, text="▶",
command=self.btnAniStart).pack(**pDct)
ttk.Button(frm, width=3, text="⇨",
command=self.btnNextFrame).pack(**pDct)
def btnAniStart(self): pass
def btnPreFrame(self): pass
def btnNextFrame(self): pass
延时表示当自动绘制动图时,两帧间隔时间;帧数表示总共绘制的时间个数。三个按钮,分别用于向前一帧、向后一帧以及动态播放。
单帧跳转
坐标t的工作原理和xyz并不相同,毕竟每次调用时间参数的时候,都只需要调用某一点的时间。所以,现有的设置坐标数据的方法就不适用了,需要更改readDatas函数
# 读取坐标轴al的数据
def readDatas(self, al):
dct = {}
data = {}
for flag in self.drawTypeDim.getDim():
data[flag] = al.setData(flag, **dct)
if flag=='t':
dct['t'] = data['t'][self.tIndex]
else:
dct[flag] = data[flag]
return data
然后再更新一下绘图函数:其实只是取消t作为绘图坐标轴的地位
def btnDrawImg(self):
self.fig.clf()
self.axDct = {}
for al in self.als:
ax = self.setDrawAxis(al)
data = self.readDatas(al)
draw = self.drawDct[al.getDrawType()]
style = al.getStyle()
keys = al.getDrawDim().replace('t',"")
draw(ax, data, keys, style)
self.fig.subplots_adjust(left=0.1, right=0.95, top=0.95, bottom=0.08)
self.canvas.draw()
最后,实现一下btnNextFrame
def btnNextFrame(self):
num = int(self.aniFrameNum.get())
self.tIndex = (self.tIndex + 1) % num
self.btnDrawImg()
效果如下,至此,我们离自动化动态绘图,只剩下一个多线程了。
动图绘制函数
所谓动图绘制,其实只需要一个循环,并且每次绘图之前,要有一定的延时。启用多线程和延时,需要导入下面两个模块
from threading import Thread
import time
接下来,就是self.btnAniStart
函数和具体的动图绘制函数了
def btnAniStart(self):
Thread(target=self.btnDrawCycle, daemon=True).start()
def btnDrawCycle(self):
dt = self.aniDelay.get()/1000.0
for i in range(self.aniFrameNum.get()):
self.tIndex = i
self.btnDrawImg()
time.sleep(dt)
点击播放按钮后效果如下
但是从图像播放的角度来说,这个设计并不十分合理,因为点击播放的三角号之后,应该要给出一个停止播放的按钮,相应地DrawCycle函数也应该新增一个无尽循环模式。
为了实现播放控制,为播放按钮绑定一个可变字符串
self.btnStartText = tk.StringVar()
self.btnStartText.set("▶")
ttk.Button(frm, width=3, textvariable=self.btnStartText,
command=self.btnAniStart).pack(**pDct)
然后修改btnAniStart,大致改成下面的形式,但这里只是添加了暂停的逻辑,但并没有真正实现暂停的功能
def btnAniStart(self):
if self.btnStartText.get() == "▶":
Thread(target=self.drawCycle, daemon=True).start()
self.btnStartText.set("⏸")
else:
self.btnStartText.set("▶")
而真正实现暂停功能,就需要修改drawCycle函数了
def drawCycle(self):
dt = self.aniDelay.get()/1000.0
num = self.aniFrameNum.get()
while self.btnStartText.get() == "⏸":
self.tIndex = (self.tIndex + 1) % num
self.btnDrawImg()
time.sleep(dt)
接口优化
开发过程中会经常遇到新增的需求,这些需求的实现,会对原来的设计形成干扰,而且这些需求累加在一起,会导致代码变得十分畸形。比如一开始DrawType类只包含绘图类型和维度,所以在AxisList中做对象的时候,变量名取为drawTypeDim。但后来DrawType又加入了其他功能,使得drawTypeDim这个变量变得很蠢,所以接下来要重新命名。
主要变更如下
-
AxisList类
- drawTypeDim 变为 drawType
-
DrawSystem类
- drawTypeDim 变为 drawType
-
AxisList类中定义的函数,但DrawSystem中有调用
- getDrawType -> getType
- getDrawDim -> getDim
另外,由于t轴和xyz在绘图时的作用相去甚远,所以将getDim函数拆分成xyz和t两个函数。在AxisList中新增两个函数
def getXYZ(self):
return self.getDim().replace("t", "")
def hasTimeAxis(self):
return "t" in self.getDim()
更改DrawSystem中的调用,btnDrawImg
中al.getDim().replace('t',"")
改为al.getXYZ()
;readDatas
改写如下
def readDatas(self, al):
dct = {}
data = {}
if al.hasTimeAxis():
data['t'] = al.setData('t')
dct['t'] = data['t'][self.tIndex]
for flag in al.getXYZ():
data[flag] = al.setData(flag, **dct)
dct[flag] = data[flag]
return data