如果你跟我一样,写Python脚本的时候经常得给工具加个界面,但又不想为了一个小工具去啃一套重型的GUI框架,那Tkinter基本上就是绕不开的选择。作为Python标准库自带的GUI工具集,Tkinter不需要额外安装扩展包,写出来的界面跨平台运行,Windows、macOS、Linux都能跑,非常适合做桌面小工具、内部自动化脚本的图形外壳,以及入门图形界面开发的练手项目。
这篇博文会从环境准备讲起,把Tkinter最核心的组件、布局方式、事件处理机制全部过一遍,最后用一个完整的实用小工具案例把前面的知识点串起来。不管你是刚接触Python的初学者,还是已经用Python写了一阵脚本但没碰过GUI开发的朋友,都可以照着文章一步步做出来自己的第一个窗口程序。我会在每一步里标注我在实际开发中踩过的坑和总结的细节,尽量让这篇文章不只是文档的翻译,而更像一个过来人带着你走一遍。
1. 项目概述:为什么做GUI先选Tkinter
1.1 Tkinter的定位与适用场景
Tkinter是Tk图形库的Python绑定,Tk本身是一套跨平台的GUI工具包,诞生于上世纪90年代,经过几十年迭代,稳定性和兼容性都相当可靠。Python官方文档里,Tkinter被称作“the standard Python interface to the Tk GUI toolkit”,也就是标准库自带的GUI接口。这意味着只要你安装了官方版本的Python,Tkinter就已经躺在你的环境里了,不需要像PyQt、wxPython那样额外pip install,也不存在编译依赖的问题。
那Tkinter到底适合做什么?从我自己的经验来看,它的定位非常明确:适合轻量级工具型软件、日常脚本的图形化外壳、学习GUI编程原理的教材级框架。比如你写了一个批量重命名文件的脚本,每次都要在终端里敲参数,用起来很不友好,这时用Tkinter包一层界面,加上文件选择对话框、进度条、结果提示,整个工具的可用性瞬间不一样。再比如给团队内部做的数据清洗小工具、给运维用的日志过滤面板,这些不需要多炫酷的视觉效果,重点是快速交付、稳定运行,Tkinter在这些场景下非常顺手。
它不擅长什么?如果你要做的是动画效果丰富、自定义皮肤、复杂布局的大型商业软件,或者需要GPU加速的图形界面,那Tkinter确实不是合适的选择。这个时候建议看PySide6/PyQt6或者Kivy。但在入门阶段,用Tkinter把事情做出来,远比在框架选型上反复纠结更有价值。
1.2 准备开发环境与第一个窗口
先说环境。Windows下从python.org下载安装包,安装的时候记得勾选“Add Python to PATH”。macOS用户建议直接去python.org下载官方安装包,不要用系统自带的Python,因为系统Python版本太旧,而且很多组件被裁剪过。Linux用户如果是Ubuntu/Debian,系统自带的Python通常不带Tkinter,需要单独装:
sudo apt install python3-tk装完之后怎么确认Tkinter可用?打开Python解释器,输入以下命令:
import tkinter print(tkinter.TkVersion)能输出类似8.6的版本号,说明环境没有问题。如果你用的是Anaconda,一般已经自带Tkinter,但也有个别精简版需要手动补齐。
接下来是我们的第一个窗口程序,完整代码如下:
import tkinter as tk root = tk.Tk() root.title("我的第一个GUI") root.geometry("400x300") label = tk.Label(root, text="Hello, Tkinter!") label.pack(pady=20) root.mainloop()逐行解释一下。tk.Tk()是创建主窗口,这是整个程序的根容器,所有组件都挂在它下面。title设置窗口顶部标题栏文字。geometry传进去一个"宽x高"的字符串,控制窗口初始大小。tk.Label创建一个文本标签,text参数指定显示内容。pack()是Tkinter的布局方法之一,后面的章节会详细展开。mainloop()是事件循环,它会启动Tkinter的消息循环机制,持续监听鼠标、键盘、窗口重绘等事件,直到用户关闭窗口。很多新手第一次写Tkinter程序,发现窗口一闪而过,就是因为漏了mainloop(),代码执行完毕进程直接退出了。
运行这段代码,你会看到一个标题为“我的第一个GUI”的窗口,里面显示一行文字。别小看这一步,整个Tkinter开发的核心逻辑已经铺开了:创建根窗口、往里面放组件、用布局方法管理组件位置、启动事件循环。后面的所有内容,都是在往这套框架里填充更多的组件和交互逻辑。
2. 核心细节解析:组件与布局管理的正确姿势
2.1 常用组件详解
Tkinter的组件体系非常丰富,但真正用得高频的就那么几个。我先把最常用的整理出来,每个都会说明用途和关键参数。
Label标签,用来显示静态文本或图片。最常用参数是text、font、fg(前景色)、bg(背景色)。很多新手以为Label只能显示固定内容,其实配合变量类(后面会讲)它也能动态更新。
Button按钮,用户点击后触发的核心交互组件。关键参数是text、command(点击回调函数)。这里有个值得注意的点:command传的是函数名,不是函数调用。很多新手写成了command=my_func(),结果程序一启动函数就执行了,点击按钮反而没反应。
Entry单行文本框,用于接收用户在界面上输入的文本。常用参数有textvariable(绑定变量)、show(密码框用show="*")、width。获取输入内容用entry.get(),清空用entry.delete(0, tk.END)。
Text多行文本框,适合展示日志、多行输入。get("1.0", tk.END)获取全部内容,其中"1.0"表示从第1行第0个字符开始。插入内容用insert(tk.END, "text")。
Listbox列表框,用来展示一个列表供用户选择。配合listbox.insert(tk.END, item)添加列表项,curselection()获取选中项的索引。双击事件可以通过bind("<Double-Button-1>", callback)绑定。
Combobox下拉选择框,这个组件在tkinter.ttk子模块里。ttk是Tkinter的主题版本,控件外观更现代。用法是先创建ttk.Combobox(root, values=[...]),然后用get()获取选中值。开发中我基本都建议用ttk版本的组件,外观比经典模式好看很多。
Checkbutton复选框和Radiobutton单选框,它们的核心区别在于多选和单选。单选框必须保证同一组内只能选一个,设置variable为同一个变量,并给每个选项设置不同的value。
Scale滑块,通过拖动调整数值。参数有from_和to(范围)、orient(水平或垂直)、command(值变化回调)。注意from是Python关键字,所以Tkinter里用from_代替。
Frame容器,用来分组和管理其他组件的布局。相当于一个矩形区域,其他组件可以放到Frame里面,然后整体移动Frame。这在复杂界面设计中极其重要。
2.2 三种布局管理方式pack、grid与place
Tkinter提供三种布局管理器:pack、grid和place。这可能是新手最容易踩坑的地方,我详细讲清楚。
pack是顺序布局,组件按照添加顺序从上到下(或从左右到右)依次放置。常用参数有side(tk.TOP、tk.BOTTOM、tk.LEFT、tk.RIGHT)、fill(tk.X、tk.Y、tk.BOTH,表示填充方向)、expand(是否扩展占用额外空间)、padx和pady(外边距)。pack的优点是写起来快,适合组件排列简单的界面,比如一列按钮、一个文本加一个按钮。缺点是不容易做精细的对齐控制,交互性强、组件多的界面用pack会很痛苦。
grid是网格布局,可以想象成把一个容器分成行和列,然后通过row和column参数指定每个组件放在哪一格。它是实际开发中最推荐的布局方式,对表单类界面尤其友好。常用参数有row、column、rowspan(跨行)、columnspan(跨列)、sticky(对齐方式,取值n/s/e/w,类似CSS的锚定)、padx/pady。
place是绝对定位,通过x、y坐标精确指定组件位置,支持relx、rely(相对位置0.0到1.0)、relwidth、relheight(相对大小)。它适合一些不规则摆放的界面,或者动态控制组件位置的场景。但一般不建议大范围使用,因为窗口大小变化时,绝对定位的组件不会自动调整位置,比较难维护。
什么场景用哪种布局?我的经验是:简单的纵向排列表单、按钮组用pack;表单类、表格类、数据录入界面用grid;需要精确控制位置的自定义控件用place;复杂界面采用“Frame+不同布局管理器组合”的方式,最外层用grid,内部某些分组用pack或grid嵌套。实际上在同一个容器中,pack和grid是不能混用的,因为两种布局管理器各自维护一套几何计算,混用会导致程序直接抛出TclError: cannot use geometry manager pack inside .的错误。
2.3 布局实操中的几个关键细节
布局这块虽然概念不难,但真正写起来有很多细节会影响界面效果。第一个是sticky参数的使用。在grid布局中,如果不设置sticky,组件会默认占据网格空间的正中央,不会填充格子。想让按钮撑满整个格子,需要设置sticky="nsew"(即北南东西四个方向都贴边)。这只是按钮横向拉伸的常用做法。
第二个是让某个列或行自适应拉伸。比如窗口变宽时,希望某个文本框也跟着变宽,光靠sticky="nsew"还不够,还需要设置列的权重:root.grid_columnconfigure(0, weight=1)。weight表示该列在额外空间分配中的占比,不设置的列不会随着窗口拉伸。这个细节不掌握,很多新手会发现自己的界面窗口一拉大,组件纹丝不动,怀疑是不是代码写错了,其实只是权重没配置。
第三个是内边距和外边距。Tkinter中组件自身内部的留白和组件之间的间距,靠padx/pady控制。但注意,grid布局中padx作用于网格单元的外侧,而ipadx作用于组件内部。如果组件之间有内容挤在一起的情况,优先检查是不是间距设置不够。
第四个是ttk组件的样式和普通组件的差异。ttk组件的bg、fg参数在很多情况下不生效,需要配置style。比如想设置ttk.Button的背景色,直接传bg="red"是无效的,必须用ttk.Style()来配置主题。这是新手从经典组件切换到ttk时最常遇到的问题。
3. 实操过程:事件机制与界面交互
3.1 command与bind两种事件绑定方式
Tkinter有两种绑定事件的模式:command参数和bind方法。
command是按钮、菜单等组件特有的简化绑定方式,绑定的是“组件被触发”这个动作。写法最简洁,也是绝大多数场景下的首选:
def on_click(): print("按钮被点击了") btn = tk.Button(root, text="点击", command=on_click)注意,command=on_click,没有括号。如果写成command=on_click(),函数会在创建按钮时立即执行,结果就是界面还没显示,函数已经跑完了,按钮点击反而没有任何反应。这个坑几乎所有Tkinter新手都会踩到。
bind是更通用的事件绑定机制。command只能绑定组件预定义的动作,但bind可以绑定任意事件,包括鼠标点击、键盘按键、鼠标进入/离开等。语法是:
def on_enter(event): label.config(text="鼠标进来了") btn.bind("<Enter>", on_enter)bind的回调函数接收一个event参数,这个event对象包含事件的详细信息,比如event.x和event.y是鼠标相对坐标,event.keysym是按键名称(如"Return"、"a"),event.widget是触发事件的组件。通过bind可以制作悬停变色、快捷键操作等交互效果。
实际开发中我的选择标准是:如果只是响应按钮点击,用command;如果需要键盘事件、鼠标事件、双击事件等更丰富的交互,用bind。两者可以同时存在,互不冲突。
3.2 变量类StringVar与动态界面
Tkinter中有一个非常重要但经常被忽视的点:直接给Label的text参数传一个Python字符串变量,当这个变量的值改变时,Label并不会自动更新。比如:
count = 0 label = tk.Label(root, text="计数: " + str(count)) count = 1 # 界面上不会变为什么?因为text接收的是字符串的值,不是变量的引用。想让组件自动跟踪变量变化,必须用Tkinter自己的变量类:StringVar、IntVar、DoubleVar、BooleanVar。
count_var = tk.IntVar(value=0) label = tk.Label(root, textvariable=count_var) def add(): count_var.set(count_var.get() + 1) btn = tk.Button(root, text="加一", command=add)这里textvariable接收的是变量对象本身,当用set()修改值时,所有关联该变量的组件会自动刷新显示。这个机制在动态界面里非常常用,比如显示计数器、显示当前选中项、显示进度文本。
变量类的好处不止于此。它还能简化取值逻辑,get()方法获取当前值,set()方法写入新值,类型转换都是内部处理好的。在写表单的时候,多个Entry绑定同一个字典里的变量,提交时统一get(),代码会清爽很多。
3.3 计数器小案例的完整实现
这里用一个计数器案例把前面的知识点串起来。功能很简单:界面上显示一个数字,三个按钮分别对应加一、减一、重置。但它涵盖了Label动态更新、Button事件、变量类、布局排列这几个核心概念。
import tkinter as tk root = tk.Tk() root.title("计数器") root.resizable(False, False) count_var = tk.IntVar(value=0) def add(): count_var.set(count_var.get() + 1) def sub(): count_var.set(count_var.get() - 1) def reset(): count_var.set(0) label = tk.Label(root, textvariable=count_var, font=("Arial", 32), width=10) label.grid(row=0, column=0, columnspan=3, pady=20) btn_add = tk.Button(root, text="加一", command=add, width=8) btn_add.grid(row=1, column=0, padx=10, pady=10) btn_sub = tk.Button(root, text="减一", command=sub, width=8) btn_sub.grid(row=1, column=1, padx=10, pady=10) btn_reset = tk.Button(root, text="重置", command=reset, width=8) btn_reset.grid(row=1, column=2, padx=10, pady=10) root.mainloop()注意几个细节。root.resizable(False, False)禁止窗口大小调节,防止布局错乱。font=("Arial", 32)设置字体,Tkinter的font参数接收一个元组,第一个元素是字体名,第二个是字号。columnspan=3让Label跨三列居中。三个按钮各占一列,通过grid排列在同一行。
运行这个小程序,你会发现按钮点击后数字实时变化,界面没有闪烁也没有卡顿。这就是Tkinter事件驱动模型的工作方式:用户每次点击,产生一个事件,事件循环捕获后调用绑定的回调函数,回调修改变量,变量触发组件重绘。
4. 进阶实战:一个完整的实用小工具
4.1 案例需求与功能拆解
计数器只是练习,真正有用的工具得有实际业务场景。我选一个很常见的例子:待办事项管理工具。需求如下:用户在一个输入框里输入待办事项,点击“添加”按钮,事项出现在列表框中;点击“删除选中”按钮,删除当前选中的事项;双击列表框中的条目,也可以删除;界面上显示当前待办总数。
这个案例能覆盖前面讲的几乎所有知识点:Entry输入、Button事件、Listbox列表、Label动态更新、变量类、bind绑定、布局管理,还引入一个messagebox对话框组件。
先做功能拆解:
- 界面结构:上方是输入区和添加按钮,中间是列表框,下方是删除按钮和状态标签。
- 数据存储:用Python列表存事项,界面上的Listbox只是展示层。
- 交互逻辑:添加时读取Entry内容,插入列表和Listbox,清空输入框;删除时读取选中索引,从两个地方同时移除;状态标签用
StringVar绑定,每次增删后刷新。 - 边界处理:输入为空时弹窗提示;没有选中项时也弹窗提示。
4.2 完整代码实现与界面讲解
import tkinter as tk from tkinter import messagebox class TodoApp: def __init__(self, root): self.root = root self.root.title("待办事项管理器") self.root.geometry("480x480") self.todos = [] # 变量 self.todo_var = tk.StringVar() self.status_var = tk.StringVar(value="共 0 条待办") # 输入区 input_frame = tk.Frame(root) input_frame.pack(fill=tk.X, padx=10, pady=10) self.entry = tk.Entry(input_frame, textvariable=self.todo_var, font=("Microsoft YaHei", 12)) self.entry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=4) btn_add = tk.Button(input_frame, text="添加", command=self.add_todo, width=8) btn_add.pack(side=tk.RIGHT, padx=(10, 0)) # 列表区 list_frame = tk.Frame(root) list_frame.pack(fill=tk.BOTH, expand=True, padx=10) scrollbar = tk.Scrollbar(list_frame) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.listbox = tk.Listbox(list_frame, font=("Microsoft YaHei", 12), yscrollcommand=scrollbar.set) self.listbox.pack(fill=tk.BOTH, expand=True) self.listbox.bind("<Double-Button-1>", self.delete_todo) scrollbar.config(command=self.listbox.yview) # 底部操作区 bottom_frame = tk.Frame(root) bottom_frame.pack(fill=tk.X, padx=10, pady=10) btn_del = tk.Button(bottom_frame, text="删除选中", command=self.delete_todo) btn_del.pack(side=tk.LEFT) self.status_label = tk.Label(bottom_frame, textvariable=self.status_var) self.status_label.pack(side=tk.RIGHT) # 回车快捷键绑定 self.entry.bind("<Return>", lambda event: self.add_todo()) def add_todo(self): content = self.todo_var.get().strip() if not content: messagebox.showwarning("提示", "请输入待办内容") return self.todos.append(content) self.listbox.insert(tk.END, content) self.todo_var.set("") self.update_status() def delete_todo(self, event=None): selected = self.listbox.curselection() if not selected: messagebox.showwarning("提示", "请先选中一条待办") return index = selected[0] self.listbox.delete(index) self.todos.pop(index) self.update_status() def update_status(self): self.status_var.set(f"共 {len(self.todos)} 条待办") if __name__ == "__main__": root = tk.Tk() app = TodoApp(root) root.mainloop()把代码分块看。TodoApp类把窗口和组件都封装在初始化方法里,这样做的最大好处是不同按钮的回调函数可以直接通过self访问界面组件,不用定义一堆全局变量。这也是中型Tkinter项目比较推荐的写法:用类组织代码,避免函数之间的变量传递乱成一团。
输入区用了包布局,Entry填充横向空间并展开,按钮固定在右侧。ListView加载滚动条,方便待办多了以后滚动查看。yscrollcommand和scrollbar.config(command=self.listbox.yview)是双向绑定的标准写法,少了任何一个滚动条都不好使。
双击删除的绑定写法是self.listbox.bind("<Double-Button-1>", self.delete_todo),这里delete_todo定义为def delete_todo(self, event=None),event=None是关键。bind绑定和Button按钮的command绑定都调用了同一个方法,但前者会传入event参数,后者不会,用默认参数None就能同时兼容两种调用方式。这也是一个非常实用的小技巧。
messagebox.showwarning是Tkinter的弹窗对话框函数,它在tkinter.messagebox子模块里,需要单独导入。它的返回值在用户选择是/否时会用到,比如messagebox.askyesno返回True/False,可以用来做删除确认。
4.3 消息框与对话框扩展
messagebox常用的几个函数整理一下:
showinfo(title, message):信息提示框showwarning(title, message):警告提示框showerror(title, message):错误提示框askyesno(title, message):返回True/Falseaskokcancel(title, message):返回True/Falseaskstring(title, prompt):弹出输入框,返回字符串或None
在像删除待办这种不可逆操作上,加一个确认提示是个好习惯。改造一下delete_todo方法:
def delete_todo(self, event=None): selected = self.listbox.curselection() if not selected: messagebox.showwarning("提示", "请先选中一条待办") return if not messagebox.askyesno("确认", "确定删除选中的待办吗?"): return index = selected[0] self.listbox.delete(index) self.todos.pop(index) self.update_status()askyesno返回的是用户选择了“是”还是“否”,选择“否”时直接return,不做删除操作。这个判断逻辑可以防止误删,实际使用体验也更好。
除了messagebox,Tkinter还提供filedialog文件对话框,用于打开和保存文件,对工具类软件特别实用。filedialog.askopenfilename()返回用户选择的文件路径,filedialog.asksaveasfilename()返回保存路径。我之前写一个批量处理图片的小工具,就是用filedialog.askopenfilenames()让用户一次选择多张图片,配合PIL做压缩处理,再加个进度条,整个过程Tkinter代码量不到200行就完成了。
5. 常见问题与排查技巧实录
5.1 问题速查表
把我在开发中遇到的典型问题和Tkinter社区里高频出现的故障整理成一个速查表,方便遇到问题直接对号入座。
| 症状 | 常见原因 | 解决方案 |
|---|---|---|
| 窗口一闪而过 | 缺少mainloop() | 在代码末尾补上root.mainloop() |
| 点击按钮没反应 | command误写成command=func() | 改成command=func,不带括号 |
| 界面文字更新不了 | 直接把普通变量赋给了text | 改用StringVar+textvariable绑定 |
| 窗口变为无响应/卡死 | 回调函数中执行了耗时阻塞操作 | 用after分片执行,或使用线程+队列 |
| pack和grid混用报错 | 同一容器中使用了两种布局管理器 | 统一种类,或用Frame分别管理 |
| 组件不显示 | 忘了调用布局方法,或布局方法调用顺序不对 | 每个组件必须调用pack/grid/place |
| 控件大小不合适 | 未设置sticky或权重 | 在grid中设置sticky="nsew"和columnconfigure |
| 删除Listbox选中项失败 | 用get()获取的是文本,不是索引 | 用curselection()获取索引再删除 |
| 字体显示很小/图标模糊 | Windows上DPI缩放未适配 | 添加ctypes.windll.shcore.SetProcessDpiAwareness(1) |
| 关闭窗口后进程不退出 | 有非守护线程仍在运行 | 设置线程daemon=True或重写WM_DELETE_WINDOW协议 |
5.2 几个容易被忽视的深坑详解
第一个坑是lambda闭包变量捕获问题。在循环里给多个按钮绑定command时,很多新手会写成这样:
buttons = [] for i in range(3): btn = tk.Button(root, text=f"按钮{i}", command=lambda: print(i)) buttons.append(btn)运行后发现,不管点哪个按钮,输出的都是2。原因在于lambda表达式捕获的是变量i的引用而不是值,循环结束时i已经变成了2,所有lambda都指向同一个i。解决办法是给lambda设置默认参数:command=lambda i=i: print(i),将循环变量i的当前值绑定到默认参数上,确保每个lambda持有独立的值。
第二个坑是回调函数中的耗时操作导致界面卡死。比如点击“开始”按钮后,回调里执行了一个5秒钟的文件复制操作,这5秒内窗口无法响应任何事件,甚至会出现“无响应”的警告。这是因为Tkinter是单线程模型,回调函数必须执行完毕,事件循环才能处理下一个事件。解决方案有两种:一是对需要分步执行的任务,用root.after(interval, callback)分片执行,把一个大任务拆成多个小片断在执行间隙处理界面事件;二是用threading.Thread在后台线程中执行耗时操作,但注意,后台线程不能直接操作Tkinter组件,需要借助queue.Queue把结果传递回主线程,主线程再通过root.after周期性检查队列并更新界面。
第三个坑是mainloop被嵌套调用导致程序异常。有些新手想关闭一个窗口再打开一个新窗口,就在回调里再次调用new_root.mainloop(),结果会发现程序行为变得很奇怪。正确的做法是用Toplevel创建子窗口,主窗口的mainloop已经在运行了,不需要重复调用。如果确实要关闭主窗口并退出,用root.destroy()。
第四个坑是打包成exe后界面异常或资源丢失。Tkinter程序用PyInstaller打包的时候,如果使用了图片资源,需要用--add-data把图片一起打包进去,并且代码里通过sys._MEIPASS获取临时路径。此外,某些主题下的ttk控件打包后外观会退化,可以在打包命令里带上--collect-data tkinter。
5.3 性能与体验优化心得
Tkinter虽然不追求高性能,但有些习惯还是能对体验有实质性改善。
控件尽量用ttk版本。tkinter.ttk里的Button、Label、Entry、Combobox等控件外观更符合现代审美,在不同系统上自动适应当地主题。经典控件样式老旧,放大缩小后容易模糊,仅在没有ttk替代品时才用(比如Text和Canvas就没有ttk版本)。
能用StringVar的地方别反复config。比如动态更新Label文本,可以用label.config(text=value),也可以用textvariable绑定。前者写起来直接,但每次更新都涉及组件的配置计算;后者如果变量值没变化,不会触发刷新。大量循环更新时,后者的性能优势更明显。
批量操作Listbox时先关闭刷新再更新。如果你要在循环里往Listbox插入几百上千条数据,不要每次都insert,可以先调整组件的state为tk.DISABLED暂时停止重绘,全部插入完成后再恢复state为tk.NORMAL,界面渲染效率会提升不少。
还有一点容易被忽视:窗口的初始大小和布局在Windows、macOS、Linux上显示效果会有差异,因为不同平台的默认字体和DPI缩放不同。我在开发时会刻意不把geometry写死,而是用root.minsize()设置最小尺寸,让布局管理器根据内容自动调整窗口大小。这样可以最大限度保证跨平台兼容性。
6. 写在最后的经验总结
回到整个流程上看,Tkinter真正的价值在于让你用最小的学习成本,把脑子里想的GUI变成能跑的软件。它不完美,界面朴素、动画能力弱,小程序多了之后代码组织全靠自制力,但这些都不妨碍它成为快速交付的第一选择。我自己日常写自动化脚本,凡是需要人来操作的步骤,基本都会用Tkinter包一个壳子,从数据导入到结果展示一条龙,工作效率提升非常明显。
最后再分享一个心态方面的建议:刚开始学Tkinter,不用追求一步到位写出复杂酷炫的界面,从今天这个计数器开始,照着待办事项的例子敲一遍,改一改功能,比如加个搜索过滤、加个编辑功能、加个数据保存在文件里,这些改造过程才是真正提升能力的地方。等你把一个工具做得能用、好用,你对Tkinter的理解自然就到位了,再去看PyQt、PySide这类框架,很多概念都是相通的,切换成本比你想象中低得多。