Python绘图系统:
基础:|||
自定义控件:
自定义坐标列表控件
绘制多组数据是很自然的一个需求,但目前来说只有一套坐标轴,除非是叠加绘制,否则是没法绘制多组数据的。
为了解决这个问题,最简单的方案当然是疯狂地添加坐标轴,x,y,z用完之后再来x1,y1,z1,然后x2,y2,z2,子子孙孙无穷匮也。简单地说,就是设置一个按钮,点击按钮之后,就会出现一组新的xyz输入框。
为了实现这个功能,最好把多个坐标输入控件AxisFrame列在一起,封装成一个新的控件。由于此前已经做了AxisFrame的自定义,所以这一步不存在难度,初步的代码如下
class AxisList(ttk.Frame): def __init__(self, master, mode, widths, **options): super().__init__(master, **options) self.pack() self.afs = {} self.initWidgets(mode, widths) def initWidgets(self, mode, widths): for flag in 'xyz': self.afs[flag] = AxisFrame(self, flag, mode, widths) self.afs[flag].pack(side=tk.TOP, fill=tk.X) def setData(self, flag, data=None, **options): return self.afs[flag].setData(data, **options) def setOneMode(self, flag, mode): self.afs[flag].setMode(mode)
然后将DrawSystem中需要更改的地方修改一下就可以了,首先是setFrmCtrl中做下修改,把self.AL作为AxisList对象的名字。其余类似self.afs[flag].XXX(YYY)之类的写法,均换成self.AL.XXX(flag, YYY)就可以了。
# setFrmCtrl函数 self.AL = AxisList(frmCtrl, 1, [5,10,20]) self.AL.pack(side=tk.TOP, fill=tk.X)
这一步在界面上尚看不出任何端倪。
显示和隐藏
如果想绘制多组数据,那么就需要很多AxisList,但这玩意一多了之后特别占地方,很难看。解决这个问题,一个比较好的方案是设置一个隐藏按钮,将这个AxisList做成类似Expander的形式。更改initWidgets函数如下
# 这些代码都在类中,注意缩进 def initWidgets(self, title, mode, widths): self.btn = ttk.Button(self, text=title, width=sum(widths)+5, command=self.Click) self.btn.pack(side=tk.TOP, fill=tk.X, expand=tk.YES) self._c = ttk.Frame(self) self.collapsed = True self.Click() for flag in 'txyz': self.afs[flag] = AxisFrame(self._c, flag, mode, widths) self.afs[flag].pack(side=tk.TOP, fill=tk.X) ```python 其中Click函数是控制显隐的关键函数,内容如下。 ```python def Click(self): if self.collapsed: self._c.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES) else: self._c.pack_forget() self.collapsed = not self.collapsed
效果为
加载按钮
现在这个测试坐标列表上面还有两个按键,分别是绘图和加载。绘图倒是没什么可说的,但加载按钮却很可疑。当我们有多套坐标的时候,一个全局的加载按钮是让人疑惑的。
为此,可以将加载按钮挪到AxisList控件中。由于功能的相似性,这里的确相当于是直接挪过去的,代码如下
def initFeature(self): frm = ttk.Frame(self._c) frm.pack(side=tk.TOP, fill=tk.X) ttk.Button(frm, text="加载",width=5, command=self.btnLoadData).pack(side=tk.LEFT) def btnLoadData(self): name = askopenfilename() data = np.genfromtxt(name) for i, flag in enumerate('xyz'): if i >= data.shape[1]: return self.setOneMode(flag, "外部导入") self.data[flag] = self.setData(flag, data[:,i])
坐标设置控件的显示和隐藏
最后,考虑到实际绘图时,有时要用到两个坐标轴,有时需要三个,所以最好能对单个的坐标轴进行显示和隐藏,实现方法与Expander的逻辑如出一辙,在initFeature方法中添加如下内容
self.vis = {L : True for L in 'txyz'} for flag in 'txyz': btn = ttk.Button(frm, text=flag, width=3) btn.pack(side=tk.LEFT) btn.bind("<Button-1>", self.btnAxisCollapse)
其中self.vis是描述某个坐标控件是否显示的布尔标记,btn就是对应的按钮,而self.btnAxisCollapse则是具体的显示隐藏函数,内容如下
def btnAxisCollapse(self, evt): flag = evt.widget['text'] self.vis[flag] = not self.vis[flag] for flag in 'txyz': self.afs[flag].pack_forget() for flag in 'txyz': if self.vis[flag]: self.afs[flag].pack(side=tk.TOP, fill=tk.X)
这里的实现过程稍显繁琐,主要是考虑到要保证txyz的显示顺序,结果如下
源代码
import tkinter as tk import tkinter.ttk as ttk from tkinter.filedialog import askopenfilename import matplotlib as mpl mpl.use('TkAgg') import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import ( FigureCanvasTkAgg, NavigationToolbar2Tk) from matplotlib.figure import Figure import numpy as np class AxisFrame(ttk.Frame): # widths 是每个控件的宽度 def __init__(self, master, label, mode, widths, **options): super().__init__(master, **options) self.pack() self.label = label self.initVar(mode) self.initWidgets(widths) def initVar(self, mode): self.MODES = ("外部导入", "序列化", "源代码", "无数据") self.mode = tk.StringVar() self.setMode(mode) def initWidgets(self, widths): tk.Label(self, text=self.label, width=widths[0]).pack(side=tk.LEFT) self.slct = ttk.Combobox(self, width=widths[1], textvariable=self.mode) self.slct['value'] = self.MODES self.slct.pack(side=tk.LEFT) self.entry = tk.Entry(self, width=widths[2]) self.entry.pack(padx=5, side=tk.LEFT, fill=tk.X) def setText(self, text): self.entry.delete(0, "end") self.entry.insert(0, text) def get(self): return self.entry.get() def setMode(self, mode): if type(mode) != str: mode = self.MODES[mode] self.mode.set(mode) def setData(self, data=None, **txyz): if self.mode.get() == "序列化": return self.getArray() elif self.mode.get() == "外部导入": return self.loadData(data) else: return self.readPython(**txyz) def readPython(self, t=None, x=None, y=None, z=None): self.data = eval(self.get()) return self.data def loadData(self, data): if type(data) != type(None): self.data = data return self.data def getArray(self): val = self.get() self.data = eval(f"np.linspace({val})") return self.data class AxisList(ttk.Frame): def __init__(self, master, title, mode, widths, **options): super().__init__(master, **options) self.pack() self.afs = {} self.data = {} self.initWidgets(title, widths) self.initAxis(mode, widths) def initWidgets(self, title, widths): self.btn = ttk.Button(self, text=title, width=sum(widths)+5, command=self.Click) self.btn.pack(side=tk.TOP, fill=tk.X, expand=tk.YES) self._c = ttk.Frame(self) self.collapsed = True self.Click() self.initFeature() def initAxis(self, mode, widths): for flag in 'txyz': self.afs[flag] = AxisFrame(self._c, flag, mode, widths) self.afs[flag].pack(side=tk.TOP, fill=tk.X) def initFeature(self): frm = ttk.Frame(self._c) frm.pack(pady=2, side=tk.TOP, fill=tk.X) ttk.Button(frm, text="加载",width=5, command=self.btnLoadData).pack(side=tk.LEFT) self.vis = {L : True for L in 'txyz'} for flag in 'txyz': btn = ttk.Button(frm, text=flag, width=3) btn.pack(side=tk.LEFT) btn.bind("<Button-1>", self.btnAxisCollapse) def btnAxisCollapse(self, evt): flag = evt.widget['text'] self.vis[flag] = not self.vis[flag] for flag in 'txyz': self.afs[flag].pack_forget() for flag in 'txyz': if self.vis[flag]: self.afs[flag].pack(side=tk.TOP, fill=tk.X) def btnLoadData(self): name = askopenfilename() data = np.genfromtxt(name) for i, flag in enumerate('xyz'): if i >= data.shape[1]: return self.setOneMode(flag, "外部导入") self.data[flag] = self.setData(flag, data[:,i]) def Click(self): if self.collapsed: self._c.pack(side=tk.TOP, fill=tk.BOTH, expand=tk.YES) else: self._c.pack_forget() self.collapsed = not self.collapsed def setData(self, flag, data=None, **options): return self.afs[flag].setData(data, **options) def setOneMode(self, flag, mode): self.afs[flag].setMode(mode) class DarwSystem(): def __init__(self): self.root = tk.Tk() self.root.title("数据展示工具") self.data = {} frmCtrl = ttk.Frame(self.root,width=320) frmCtrl.pack(side=tk.RIGHT, fill=tk.Y) self.setFrmCtrl(frmCtrl) frmFig = ttk.Frame(self.root) frmFig.pack(side=tk.LEFT,fill=tk.BOTH,expand=tk.YES) self.setFrmFig(frmFig) self.root.mainloop() def setFrmCtrl(self, frmCtrl): frm = ttk.Frame(frmCtrl, width=320) frm.pack(side=tk.TOP, fill=tk.X) self.setCtrlButtons(frm) self.AL = AxisList(frmCtrl, "测试坐标列表", 1, [5,10,20]) self.AL.pack(side=tk.TOP, fill=tk.X) def setCtrlButtons(self, frm): ttk.Button(frm, text="绘图",width=5, command=self.btnDrawImg).pack(side=tk.LEFT) ttk.Button(frm, text="加载",width=5, command=self.btnLoadData).pack(side=tk.LEFT) def btnLoadData(self): name = askopenfilename() data = np.genfromtxt(name) for i, flag in enumerate('xyz'): if i >= data.shape[1]: return self.AL.setOneMode(flag, "外部导入") self.data[flag] = self.AL.setData(flag, data[:,i]) def readDatas(self): dct = {} for flag in 'xyz': self.data[flag] = self.AL.setData(flag, **dct) dct[flag] = self.data[flag] def btnDrawImg(self): self.readDatas() self.fig.clf() if 'z' in self.data: self.drawPlot3D() else: self.drawPlot() self.fig.subplots_adjust(left=0.1, right=0.95, top=0.95, bottom=0.08) self.canvas.draw() def drawPlot3D(self): ax = self.fig.add_subplot(projection='3d') ax.plot(self.data['x'], self.data['y'], self.data['z']) def drawPlot(self): ax = self.fig.add_subplot() ax.plot(self.data['x'], self.data['y']) def setFrmFig(self, frmFig): self.fig = Figure() self.canvas = FigureCanvasTkAgg(self.fig,frmFig) self.canvas.get_tk_widget().pack( side=tk.TOP,fill=tk.BOTH,expand=tk.YES) self.toolbar = NavigationToolbar2Tk(self.canvas,frmFig, pack_toolbar=False) self.toolbar.update() self.toolbar.pack(side=tk.RIGHT) if __name__ == "__main__": test = DarwSystem()