文章目錄
- 自定義坐標列表控件
- 顯示和隱藏
- 加載按鈕
- 坐標設置控件的顯示和隱藏
- 源代碼
Python繪圖系統:
- 基礎:將matplotlib嵌入到tkinter 📈簡單的繪圖系統 📈數據導入📈三維繪圖系統
- 自定義控件:坐標設置控件📉坐標列表控件
自定義坐標列表控件
繪制多組數據是很自然的一個需求,但目前來說只有一套坐標軸,除非是疊加繪制,否則是沒法繪制多組數據的。
為了解決這個問題,最簡單的方案當然是瘋狂地添加坐標軸,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 = Trueself.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]:returnself.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 askopenfilenameimport matplotlib as mpl
mpl.use('TkAgg')
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
from matplotlib.figure import Figureimport numpy as npclass AxisFrame(ttk.Frame):# widths 是每個控件的寬度def __init__(self, master, label, mode, widths, **options):super().__init__(master, **options)self.pack()self.label = labelself.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.MODESself.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.datadef loadData(self, data):if type(data) != type(None):self.data = datareturn self.datadef getArray(self):val = self.get()self.data = eval(f"np.linspace({val})")return self.dataclass 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 = Trueself.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]:returnself.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.collapseddef 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]:returnself.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()