Toplevel控件搭建父子窗口
最近,用Python给单位里用的“智慧食堂”系统编制了一个餐卡充值文件生成器,自动匹配餐卡号并快速生成导入数据用的Excel表格,截图如下:
使用tkinter Toplevel控件弹出子窗口,用作设置备注的子窗口。在编程过程中,边学边写探索到不少新知识,简单介绍如下:
最简明的父子窗口框架
创建一个主窗口、一个子窗口,各放一个按钮,代码如下:
import win32api, tkinter as tk def _toplevel(): top = tk.Toplevel(root) top.title("Toplevel Window") W,H=400,300 top.geometry(f'{W}x{H}+{(X-W)//2}+{(Y-H)//2}') btn_Close = tk.Button(top, text="Close", command=top.destroy) btn_Close.pack() if __name__=='__main__': # 创建主窗口 root = tk.Tk() root.title("Main Window") # 获取windows系统桌面分辨率 X,Y=win32api.GetSystemMetrics(0),win32api.GetSystemMetrics(1) W,H=600,480 root.geometry(f'{W}x{H}+{(X-W)//2}+{(Y-H)//2}') # 创建一个打开Toplevel窗口的按钮 btn_Open = tk.Button(root, text="Open Toplevel", command=_toplevel) btn_Open.pack() # 运行Tkinter事件循环 root.mainloop()
上述代码的缺点是主窗口上的Open按钮可以反复点击打开多个子窗口,要想办法按需要来屏蔽它的点击功能。
改进一:屏蔽和开放按钮
以下代码可以调整按钮的使用状态:tk.DISABLED、tk.NORMAL
button.config(state=tk.DISABLED)
button.config(state=tk.NORMAL)
打开子窗口时,Open按钮的状态改为tk.DISABLED,此时已无法点击了。
import win32api, tkinter as tk class TopWindow: def __init__(self, parent): top = self.top = tk.Toplevel(parent) top.title("Toplevel Window") W,H=400,300 top.geometry(f'{W}x{H}+{(X-W)//2}+{(Y-H)//2}') btn_Close = tk.Button(top, text="Close", command=self.on_close) btn_Close.pack() def on_close(self): btn_Open.config(state=tk.NORMAL) self.top.destroy() def on_open(): TopWindow(root) btn_Open.config(state=tk.DISABLED) if __name__=='__main__': root = tk.Tk() root.title("Main Window") X,Y=win32api.GetSystemMetrics(0),win32api.GetSystemMetrics(1) W,H=600,480 root.geometry(f'{W}x{H}+{(X-W)//2}+{(Y-H)//2}') btn_Open = tk.Button(root, text="Open Toplevel", command=on_open) btn_Open.pack() root.mainloop()
改进二:子窗口始终在主窗口之上
top.transient(root) # 设置Toplevel窗口始终在主窗口root的上方
import win32api, tkinter as tk class TopWindow: def __init__(self, parent): top = self.top = tk.Toplevel(parent) top.title("Toplevel Window") W,H=400,300 top.geometry(f'{W}x{H}+{(X-W)//2}+{(Y-H)//2}') top.transient(root) # 设置Toplevel窗口始终在主窗口上方 btn_Close = tk.Button(top, text="Close", command=self.on_close) btn_Close.pack() def on_close(self): btn_Open.config(state=tk.NORMAL) self.top.destroy() def on_open(): TopWindow(root) btn_Open.config(state=tk.DISABLED) if __name__=='__main__': root = tk.Tk() root.title("Main Window") X,Y=win32api.GetSystemMetrics(0),win32api.GetSystemMetrics(1) W,H=600,480 root.geometry(f'{W}x{H}+{(X-W)//2}+{(Y-H)//2}') btn_Open = tk.Button(root, text="Open Toplevel", command=on_open) btn_Open.pack() root.mainloop()
另外一种方法也能设置子窗口永远在前:
top.wm_attributes('-topmost', True) # 设置Toplevel窗口在所有窗口的上方
两种方法的区别在于后者是全局的设置,它使得子窗口在操作系统中所有窗口的上面,包括其它应用程序的窗口。
如下图,请比较一下与上一张截图的效果有啥区别:
改进三:增加子窗口的关闭协议
如下图,直接点击子窗口右上关闭按钮,只触发窗口默认的top.destroy事件。这样关闭子窗口后,主窗口的按钮状态并不能恢复;以下代码使得子窗口的"WM_DELETE_WINDOW"关闭协议绑定了自定义的关闭事件self.onclose:
top.protocol("WM_DELETE_WINDOW", self.on_close)
完整代码如下:
import win32api, tkinter as tk class TopWindow: def __init__(self, parent): top = self.top = tk.Toplevel(parent) top.title("Toplevel Window") W,H=400,300 top.geometry(f'{W}x{H}+{(X-W)//2}+{(Y-H)//2}') top.transient(root) top.protocol("WM_DELETE_WINDOW", self.on_close) btn_Close = tk.Button(top, text="Close", command=self.on_close) btn_Close.pack() def on_close(self): btn_Open.config(state=tk.NORMAL) self.top.destroy() def on_open(): TopWindow(root) btn_Open.config(state=tk.DISABLED) if __name__=='__main__': root = tk.Tk() root.title("Main Window") X,Y=win32api.GetSystemMetrics(0),win32api.GetSystemMetrics(1) W,H=600,480 root.geometry(f'{W}x{H}+{(X-W)//2}+{(Y-H)//2}') btn_Open = tk.Button(root, text="Open Toplevel", command=on_open) btn_Open.pack() root.mainloop()
改进四:使子窗口长获焦点
top.grab_set() # 确保Toplevel窗口长获焦点
使用这个方法,前面提到的按钮状态的切换以及子窗口绑定关闭协议的代码都不需要了,非常简洁。top.grab_set() 配合 top.transient(root) 共同使用(如下标注红色部分),效果最佳:
import win32api, tkinter as tk class TopWindow: def __init__(self, parent): top = self.top = tk.Toplevel(parent) top.title("Toplevel Window") W,H=400,300 top.geometry(f'{W}x{H}+{(X-W)//2}+{(Y-H)//2}') top.grab_set() top.transient(root) btn_Close = tk.Button(top, text="Close", command=top.destroy) btn_Close.pack() def on_open(): TopWindow(root) if __name__=='__main__': root = tk.Tk() root.title("Main Window") X,Y=win32api.GetSystemMetrics(0),win32api.GetSystemMetrics(1) W,H=600,480 root.geometry(f'{W}x{H}+{(X-W)//2}+{(Y-H)//2}') btn_Open = tk.Button(root, text="Open Toplevel", command=on_open) btn_Open.pack() root.mainloop()
源代码复制框如下:
import win32api, tkinter as tk class TopWindow: def __init__(self, parent): top = self.top = tk.Toplevel(parent) top.title("Toplevel Window") W,H=400,300 top.geometry(f'{W}x{H}+{(X-W)//2}+{(Y-H)//2}') top.grab_set() top.transient(root) btn_Close = tk.Button(top, text="Close", command=top.destroy) btn_Close.pack() def on_open(): TopWindow(root) if __name__=='__main__': root = tk.Tk() root.title("Main Window") X,Y=win32api.GetSystemMetrics(0),win32api.GetSystemMetrics(1) W,H=600,480 root.geometry(f'{W}x{H}+{(X-W)//2}+{(Y-H)//2}') btn_Open = tk.Button(root, text="Open Toplevel", command=on_open) btn_Open.pack() root.mainloop()
总结
通过对toplevel控件的编程操练,掌握了tkinter子窗口的调用方法,了解了topleve的多种特殊方法、响应事件以及绑定协议。