Python Tkinter实战:从零构建桌面计算器应用 📅 发布时间:2026/9/2 3:18:23 👁 浏览次数: 你是不是被“tk”这个词搞得有点懵搜了一圈发现它既能出现在 Python 教程里又能变成一个“星球”的名字甚至还有专门的讨论网址。作为一个技术写作者我可以直接告诉你一个判断在这个上下文里tk 最可靠、也最值得你花时间掌握的指向就是 Python 内置的标准 GUI 库 Tkinter。至于“艾比仙 tk 2410”你可以把它理解成某个学习社群或项目包对 Tkinter 知识体系的一次版本化整理类似“2410 期”但真正的核心资产依然是 Tkinter 本身。这篇文章的目标很明确不扯那些模棱两可的“tk 星球”概念而是带你用 Tkinter 从零到一写出一个能跑、能打包、能交付的桌面应用。你会搞清楚一个最典型的问题为什么明明照着文档写了代码窗口却动不动卡死为什么用 grid 布局控件就是不按你想象的位置排这些问题才是真正阻挡你用好 tkinter 的门槛。我会从 Tkinter 最容易被误解的几个核心概念讲起然后带你用完整的“计算器”项目跑通一个真实 GUI 应用的开发流程再聊生产环境里必然会遇到的窗口布局、事件处理、数据同步、打包分发问题。读完这篇文章你不仅能用 Python 写出界面还能理解这背后的一套工程化思路。1. 这篇文章真正要解决的问题不少 Python 开发者对 Tkinter 有一个刻板印象要么觉得它“太简单”只配用来写写小工具和课堂作业要么觉得它“太老旧”界面不够现代不如 PyQt、wxPython 有前途。这两种判断其实都偏离了一个关键事实Tkinter 是 Python 标准库中唯一内置的 GUI 框架它真正降低的是桌面应用开发的门槛和分发成本。如果你在做数据分析经常需要把一个清洗脚本包装成让业务同事也能操作的界面Tkinter 是可行且最稳妥的方案。你不需要额外安装任何第三方依赖写完代码直接就能跑Python 环境在哪里它就在哪里。反过来如果你要在生产环境做一个面向大量真实用户的高交互桌面客户端界面需要非常强的自绘能力、皮肤定制能力那 Tkinter 确实不是最佳选择这个结论也得承认。所以这篇文章第一个要解决的问题是帮你建立对 Tkinter 的合理预期。它不是万能的但绝对比很多人想象的强大。第二个要解决的问题是真正的实操痛点布局管理和事件驱动。我见过太多初学者把代码写在同一个文件里用 pack 布局把控件一个个叠上去结果界面稍微拉伸一下就乱套或者把一个耗时任务直接丢在按钮回调里结果窗口直接无响应系统提示“程序未响应”。这些问题看起来很基础但如果没有透彻理解 Tkinter 的底层机制排查起来会非常痛苦。第三个要解决的问题是桌面应用开发的完整性。写完界面不等于做完应用。你还需要考虑如何用面向对象的方式组织代码让逻辑和界面分离如何在长时间的耗时任务中保持界面不冻结如何把 Python 脚本打包成 exe 或其他可执行文件给用户使用如何在窗口大小变化时让布局依然保持合理。这些问题Tkinter 官方文档并不会手把手教你但从材料来看凡是 Tkinter 讨论得比较深的地方不管是论坛还是“tk 讨论网址”里的内容都在围绕这些主题展开。本文就把这条路径完整走一遍。另外关于“tk 星球”“tk 网”“pthon tk 计算器”这些热词我的处理方式是回归技术本身。与其去猜一个不确定的社区名称指向什么不如把 Tkinter 这套技能本身讲透。假如你是在某个学习社群里看到“tk 星球”的入口课程内容大概率也逃不开布局、事件、控件、打包这些知识点你把这篇文章掌握好依然不会落后。2. 基础概念与核心原理要理解 Tkinter先理解它的底层。Tkinter 是 Python 对 Tk 图形库的封装Tk 是一个跨平台、基于 C 语言实现的 GUI 工具库。换句话说Tkinter 是你的 Python 代码和操作系统窗口系统之间的翻译官。你把控件放在窗口里Tkinter 负责把这些控件请求交给 Tk 处理Tk 再调用底层窗口系统把东西画出来。这个架构决定了几个非常关键的特性第一Tkinter 是单线程的。所有界面更新、事件处理、控件绘制都要在主线程也就是所谓的“事件循环”里完成。如果你在回调函数里做耗时计算主线程被卡住了界面就不会刷新表现就是“无响应”。这一点是无数新手踩坑的根源后面我会专门演示解决方案。第二Tkinter 不是线程安全的。你可以用 Python 的 threading 模块开子线程去做耗时任务但绝对不能在子线程里直接操作 Tkinter 控件比如调用 Label 的 set() 方法或用 insert() 往文本框里塞内容。正确做法是通过队列或特殊方法把数据传回主线程处理。这个约束看起来很啰嗦但遵守了它你的程序就会稳定很多。第三布局管理是 Tkinter 最核心的难点之一。Tkinter 提供了三种布局管理器pack、grid、place。我用一个简单的对比来说明它们之间的区别布局方式工作原理适合场景注意事项pack按添加顺序把控件依次排入容器简单工具、垂直或水平排列的界面控件多了之后嵌套层次容易失控grid把容器分成行和列的网格控件按坐标放入表单类界面、计算器、设置面板需要维护好行列关系注意合并单元格place按绝对坐标或相对坐标定位控件特殊绘图、精确控制位置窗口大小变化时控件不会自动适应不推荐默认使用初学者最常见的错误是在一个容器里混合使用 pack 和 grid结果 Tkinter 直接抛异常或者控件布局完全乱掉。正确的做法是同一个父容器只能使用一种布局管理器不同容器之间可以自由切换比如外层用 pack内层 Frame 用 grid这是完全合法的。第四个关键概念是事件循环。你写一个 Tkinter 程序最后一定有一行root.mainloop()它才是程序的真正主人。mainloop 启动之后会无限循环等待并分发按键、鼠标移动、定时器等事件。你写的所有按钮回调本质上都是给这个循环注册的“处理函数”。理解 event driven 模式之后你会突然明白为什么 Tkinter 的控件方法大多叫bind、command、after因为它们都在围绕事件做文章。第五个概念是控件的组成结构。Tkinter 控件并不只是一个“画好的组件”它内部由多个部件组成包括背景、边框、内容区、高亮区等。你可以通过bd、highlightthickness等选项控制这些区域的视觉表现。这也是为什么有时候你设置了bg颜色控件边框却还是灰色——因为灰色部分是边框本身的颜色不是背景。这些概念听起来不算复杂但很多人写 Tkinter 代码时根本不考虑它们背后的逻辑只是照着文档“抄”。结果就是代码能跑但一旦改动稍微复杂比如加一个多线程任务、改一下窗口缩放策略程序就变得不可控。理解了上面这五点你的 Tkinter 水平就已经超过很大一部分“复制粘贴党”了。3. 环境准备与前置条件在开始写代码之前先把环境确认清楚。“tk 计算器”这类项目需要的东西很少但版本问题依旧值得认真对待因为版本差异直接影响控件行为和打包表现。Tkinter 随 Python 标准库一起提供所以你不需要像安装 PyQt 那样额外下载庞大 SDK。但请先在你的终端里确认两件事一是 Python 版本二是 Tkinter 是否可用。在命令行输入python --version常见输出Python 3.12.1然后验证 Tkinter 是否能正常导入python -c import tkinter; print(tkinter.TkVersion)如果你能看到类似8.6这样的输出说明 Tkinter 可用。如果报错大概率是当前 Python 环境没有安装 Tk 的动态库比如 Linux 下需要额外安装python3-tk包sudo apt-get install python3-tkmacOS 上如果你使用的是官网安装的 Python一般自带 Tkinter但如果用的是 Homebrew 安装的 Python也可能是缺失的需要额外确认。Windows 下的官方 Python 安装包默认包含 Tkinter通常不需要额外操作。这是完整的开发环境建议清单操作系统Windows 10/11、macOS、主流 Linux 发行版均可 Python 版本建议 3.8 及以上本文示例在 3.12 下验证通过 Tk 版本8.6 及以上Tkinter 导入时打印出的版本号 开发工具VS Code、PyCharm、或任意你顺手的编辑器 打包工具PyInstaller后续章节会使用请留意一个细节Tkinter 在 Python 2 时代写作Tkinter在 Python 3 时代写作tkinter全部小写。这两个不能混淆新手从网上复制旧代码时经常会踩这个坑。另外关于 IDE 的选择我没有必须推荐的偏好但有一点值得注意在 VS Code 或 PyCharm 中运行 Tkinter 程序时如果使用了虚拟环境请确保虚拟环境里装好了 Python 包Tkinter 虽然是标准库但打包时依然会涉及环境问题建议先设置好工作区。4. 核心流程拆解一个完整的 Tkinter 项目按流程可以拆解成五个步骤创建根窗口、构建与布局控件、绑定事件、进入事件循环、打包分发。下面我一步步拆开讲每一步都告诉你“为什么需要这么做”。4.1 创建根窗口Tk()创建的是应用程序的主窗口也是一切控件的祖先。忘记调用mainloop()是新手最常见的错误之一那样窗口会一闪而过程序直接退出。正确做法是import tkinter as tk root tk.Tk() root.title(我的应用) root.geometry(400x300) root.mainloop()这里我强烈建议你使用import tkinter as tk这种导入方式而不是from tkinter import *。原因很简单命名空间干净。如果你使用通配符导入很容易不小心覆盖 Tkinter 内部的标识符而且写tk.Button的方式也更有助你理解每个控件来自哪里。4.2 构建与布局控件创建控件很简单把父容器作为第一个参数传入即可。但控件的摆放却需要讲究。我以计算器项目为例推荐使用 grid 布局显示屏占据第一行整行数字按钮按 4 行 3 列排布运算符按钮单独占一列。核心代码大概是这样的# 文件路径calculator/calculator_ui.py import tkinter as tk class Calculator: def __init__(self, root): self.root root self.root.title(计算器) self.root.resizable(False, False) self.display tk.Entry(root, font(Arial, 20), justifyright, bd10) self.display.grid(row0, column0, columnspan4, padx5, pady5, stickynsew)这里有一个关键点columnspan4表示显示屏控件占据 4 列宽度的格子。如果不写这个控件就会默认只占第一列右边空出三列界面会非常难看。4.3 绑定事件Tkinter 里有两种常见的事件绑定方式一种是按钮控件的command选项点击时触发另一种是bind()方法可以绑定键盘、鼠标移动等更多类型的事件。两者区别在于command更高级、更语义化适合按钮点击bind更底层、更灵活适合捕获按键、鼠标悬停等。在计算器项目中数字按钮和运算符按钮适合用commandbutton tk.Button(root, text7, font(Arial, 14), commandlambda: self.on_digit(7)) button.grid(row1, column0, stickynsew)而键盘输入则适合用bindroot.bind(Key, self.on_key_press)事件绑定时函数或方法只能接收一个event参数这是 Tkinter 的规则。如果你需要给回调函数传额外参数务必要用lambda表达式包装否则会出现程序一启动就执行函数、而点击时什么都不发生的奇怪现象。4.4 进入事件循环mainloop()会把控制权交给 Tkinter 的事件调度器。从这一刻起你的代码顺序执行阶段结束程序进入持续监听事件的状态。你所有需要在窗口关闭时执行的清理逻辑可以通过协议处理来实现例如def on_close(): print(窗口关闭) root.destroy() root.protocol(WM_DELETE_WINDOW, on_close)这里的含义是当用户点击窗口右上角的关闭按钮时不再执行默认的销毁动作而是先执行on_close()再销毁窗口。这在需要保存配置、关闭数据库连接或结束子线程的场景里非常有用。4.5 打包分发写好的 Tkinter 程序要交付给没有 Python 环境的用户最常用的方式是 PyInstaller。打包命令通常是pyinstaller -F -w calculator.py其中-F打包成单个可执行文件-w不显示命令行窗口只显示 GUI 界面-n可以自定义生成的文件名。Tkinter 程序打包时最容易遇到的坑是文件缺失和图标丢失。建议先使用单文件模式跑通流程再根据项目需要调整资源文件路径。5. 完整示例与代码实现理论讲完了现在我们来写一个可以直接运行、具备完整加减乘除功能的计算器。这个项目小巧但包含了 Tkinter 应用开发的典型边界问题界面布局、事件处理、逻辑与界面分离、键盘输入支持。5.1 项目结构先规划一个清晰的目录calculator/ ├── calculator.py ├── requirements.txt └── README.mdrequirements.txt在标准 Tkinter 项目里通常只有一行注释因为不依赖任何第三方库# 本项目的 Tkinter 来自 Python 标准库无需额外安装依赖5.2 逻辑层为了让代码清晰我先把计算逻辑独立出来。这样可以单独测试逻辑不依赖界面。# 文件路径calculator/calculator.py逻辑层也可单独拆成 core.py class CalculatorCore: 负责计算逻辑不包含任何 Tkinter 代码 def __init__(self): self.current 0 self.previous None self.operator None self.waiting_second_operand False def input_digit(self, digit: str): if self.waiting_second_operand: self.current digit self.waiting_second_operand False else: if self.current 0: self.current digit else: self.current digit def input_dot(self): if self.waiting_second_operand: self.current 0. self.waiting_second_operand False return if . not in self.current: self.current . def set_operator(self, operator: str): if self.operator is not None and not self.waiting_second_operand: self.calculate() self.previous float(self.current) self.operator operator self.waiting_second_operand True def calculate(self): if self.operator is None or self.previous is None: return second float(self.current) if self.operator : result self.previous second elif self.operator -: result self.previous - second elif self.operator *: result self.previous * second elif self.operator /: if second 0: self.current 错误 self.reset() return result self.previous / second else: result second self.current self._format_number(result) self.previous None self.operator None staticmethod def _format_number(value: float) - str: if value.is_integer(): return str(int(value)) return str(value) def clear(self): self.current 0 self.previous None self.operator None self.waiting_second_operand False def reset(self): self.previous None self.operator None self.waiting_second_operand False这段代码把计算器的核心状态管理完全从界面层抽离出来。好处是显而易见的你可以为CalculatorCore写单元测试如果以后想把计算器移植到命令行或 Web 界面逻辑层不需要改动。5.3 界面层界面层负责创建控件、布局和分发输入事件。# 文件路径calculator/gui.py import tkinter as tk from typing import Callable from .core import CalculatorCore class CalculatorApp: def __init__(self, root: tk.Tk): self.root root self.core CalculatorCore() self.root.title(计算器) self.root.resizable(False, False) self.display tk.Entry( root, font(Consolas, 24), justifyright, bd12, relieftk.GROOVE, statereadonly, ) self.display.grid( row0, column0, columnspan4, padx8, pady(12, 8), stickynsew ) buttons [ 7, 8, 9, /, 4, 5, 6, *, 1, 2, 3, -, 0, ., , , ] self._create_buttons(buttons) self._bind_keyboard() self._update_display() def _create_buttons(self, buttons: list[str]) - None: for index, text in enumerate(buttons): row index // 4 1 column index % 4 command self._make_handler(text) btn tk.Button( self.root, texttext, font(Consolas, 18), padx20, pady20, commandcommand, ) btn.grid(rowrow, columncolumn, padx2, pady2, stickynsew) def _make_handler(self, text: str) - Callable: if text.isdigit(): return lambda: self._on_digit(text) if text .: return self._on_dot if text : return self._on_equals if text C: return self._on_clear return lambda: self._on_operator(text) def _bind_keyboard(self) - None: self.root.bind(Key, self._on_key) self.root.focus_set() def _on_digit(self, digit: str) - None: self.core.input_digit(digit) self._update_display() def _on_dot(self) - None: self.core.input_dot() self._update_display() def _on_operator(self, operator: str) - None: self.core.set_operator(operator) self._update_display() def _on_equals(self) - None: self.core.calculate() self._update_display() def _on_clear(self) - None: self.core.clear() self._update_display() def _on_key(self, event: tk.Event) - None: key event.char if key.isdigit(): self._on_digit(key) elif key .: self._on_dot() elif key in -*/: self._on_operator(key) elif key \r: self._on_equals() elif key c or key C: self._on_clear() def _update_display(self) - None: self.display.configure(statenormal) self.display.delete(0, tk.END) self.display.insert(0, self.core.current) self.display.configure(statereadonly) def main() - None: root tk.Tk() app CalculatorApp(root) root.mainloop() if __name__ __main__: main()注意我在界面层用了statereadonly的 Entry 控件这样可以防止用户直接在输入框里写字确保所有输入都经过事件处理逻辑。这是 Tkinter 项目里一个典型的防呆设计。5.4 运行项目在项目根目录执行python -m calculator.gui或者直接python gui.py如果项目结构采用了包形式推荐使用python -m方式它能正确处理模块导入路径。6. 运行结果与效果验证如果代码正确你会看到一个标题为“计算器”的窗口上面是显示屏下方是 4 行 4 列的按钮。窗口无法拉伸布局稳定。验证功能时请按顺序测试点击7 3数字区域依次显示 7、7、3点击显示屏显示10点击9 / 0再点击显示屏显示“错误”且程序不崩溃按键盘数字键5、*、6、Enter显示屏显示30按C键显示屏回到0。如果某个步骤不符合预期不要急着改代码先按下面的排查思路定位。先说一个最容易遇到的问题窗口启动后按钮没有响应。可能原因有两个一是command指向的函数写错了比如在装饰器或 lambda 里带括号会导致函数在创建按钮时就被执行而不是点击时执行二是mainloop()没有调用程序在窗口显示后立刻退出。另一个常见问题是显示屏里的数字出现类似0.30000000000000004的长小数。这是浮点数运算的正常表现不是 Tkinter 的问题。我在逻辑层里已经通过_format_number方法做了基础格式化如果你做更复杂的计算建议使用decimal.Decimal代替浮点数。再有一个问题打包成 exe 之后运行没有任何反应或者提示缺少 MSVCP140.dll。这是运行环境的 VC 运行库缺失属于常见的 Windows 部署问题需要在目标机器上安装对应版本的 Visual C Redistributable而不是盲目修改打包参数。问题现象可能原因排查方式解决方案窗口一闪而过未调用 mainloop()检查代码末尾是否有 mainloop()补充 root.mainloop()点击按钮无反应command 绑定错误检查是否误写了括号使用 lambda 或无括号的函数引用控件布局错乱同一容器混用 pack 和 grid查看代码中的布局调用统一使用一种布局管理器小数位太长浮点数计算精度问题打印计算值使用 Decimal 或格式化字符串打包后无窗口PyInstaller 参数错误查看打包日志确认使用 -w 参数且代码无命令行输入运行提示缺少 DLL目标机器缺运行库查看系统事件日志安装 VC 运行库7. 常见问题与排查思路关于 Tkinter 的常见问题我在上面表格里列了一部分但还有几个典型场景值得单独展开。这些都是真正写项目时躲不开的坑也是“tk 讨论网址”里出现频率最高的话题。7.1 多线程与界面冻结最经典的场景是你点击“开始处理”按钮程序开始读取一个大文件处理后更新表格或标签。但因为处理耗时较长界面直接白屏标题栏变成“未响应”。这是因为处理逻辑在主线程运行阻塞了 Tkinter 的事件循环。解决思路是把耗时任务放到子线程然后用队列把结果发回主线程。一个标准的做法是结合threading和root.after。需要特别提醒的是不要在其他线程里直接调用 Tkinter 控件的方法如果你这样做了界面不一定会立即崩但会产生随机性的更新错乱极难排查。# 文件路径demo_thread.py import threading import queue import time import tkinter as tk def long_task(result_queue: queue.Queue) - None: time.sleep(3) result_queue.put(任务完成) def poll_queue(root: tk.Tk, q: queue.Queue) - None: try: message q.get_nowait() label.config(textmessage) except queue.Empty: root.after(100, lambda: poll_queue(root, q)) root tk.Tk() label tk.Label(root, text准备中) label.pack() q queue.Queue() threading.Thread(targetlong_task, args(q,), daemonTrue).start() root.after(100, lambda: poll_queue(root, q)) root.mainloop()更现代的做法是使用after作为周期性轮询机制为核心因为它天然是主线程安全的。7.2 布局失控新手最崩溃的错误不是代码报错而是界面长得完全不是自己预想的样子。最常见的原因就是混用了布局管理器。请记住同一个容器只能用 pack、grid、place 中的一种。如果你需要复杂布局可以用Frame做嵌套外层用 pack 放上下两栏内层用 grid 放表格按钮。这是完全合法的。7.3 命令行窗口残留使用pythonw.exe运行 Tkinter 程序可以避免命令行窗口弹出但如果你用 PyInstaller 打包这个需求直接通过-w参数解决。7.4 Entry 输入框获取内容为空很多人写entry.get()拿到空字符串排查后发现是 Entry 控件还没有被创建或者获取时机不对。Tkinter 不会自动把控件内容同步到某个变量里你一定要使用StringVar或直接调用get()方法。推荐用后者代码更简单直接。8. 最佳实践与工程建议写多了 Tkinter 你会发现它的 API 复杂度并不高真正的难点在于如何把代码组织好。从工程角度看有五个点是我强烈建议的。第一强制区分逻辑层和界面层。刚才的计算器代码已经展示了这种做法CalculatorCore完全不 import tkinter所有计算逻辑独立存在。这样设计之后你可以为逻辑层写单元测试以后如果想换 PyQt 甚至命令行界面逻辑层一行都不用动。GUI 代码最大的坏味道就是逻辑和界面混在一起导致没法测、没法维护。第二使用root.after而不是time.sleep。任何时候你需要在 Tkinter 主线程里做延迟或周期性任务都优先使用after。它不会阻塞事件循环而且是线程安全的。用time.sleep处理界面动画或轮询状态几乎必然会造成卡顿。第三控件命名和回调命名要可读。比如btn_1、btn_add、entry_username这种命名方式比b1、b2好看得多。回调函数最好体现动作比如on_add_clicked、on_username_changed。别小看命名问题它直接影响几天之后你自己还能不能快速进入状态。第四弹窗和错误处理尽可能用 messagebox。Tkinter 自带messagebox.showerror、showwarning、askyesno等方法用起来非常方便。但要注意messagebox 同样依赖主线程如果程序已经卡死它也不会正常工作。第五生产环境的日志与异常捕获。在非命令行窗口模式下一旦程序抛出未捕获异常用户会看到一堆晦涩的报错弹窗或者程序直接消失。建议在入口处做一个全局异常捕获把异常写入日志文件# 文件路径exception_hook.py import logging import tkinter as tk from tkinter import messagebox logging.basicConfig(filenameapp.log, levellogging.ERROR) def global_exception_hook(exc_type, exc_value, exc_traceback): logging.critical(未捕获异常, exc_info(exc_type, exc_value, exc_traceback)) try: messagebox.showerror(程序错误, f发生未预期错误{exc_value}) except tk.TclError: pass def install_hook() - None: import sys sys.excepthook global_exception_hook在程序入口处调用install_hook()至少能保证用户遇到错误时你能拿到日志文件里的调用栈而不是只有一句“程序打不开”。第六打包之前先做减法。不要在 Python 环境里安装大量用不到的包之后再开始打包 PyInstaller。因为 PyInstaller 的依赖分析器会尝试把额外的包也打进去导致最终 exe 体积膨胀。建议在干净的虚拟环境里只安装项目依赖再执行打包命令。9. 总结与后续学习方向从“tk 是什么”这个疑惑开始到今天你已经掌握了一个完整的 Tkinter 计算器项目理解了 Tkinter 的底层架构、布局管理器、事件循环写了一个逻辑与界面分离的计算器踩过了多线程、布局、打包这几个最常见的坑。这些能力比单纯记住某个控件的方法要值钱得多。下一步可以继续深入的方向是清晰的如果你的目标是更专业的桌面应用开发可以研究 Tkinter 的 Canvas 自绘能力或者转向 PyQt/PySide 这类功能更强大的框架如果想让应用看起来更现代化可以学习 ttk 主题控件它能用较少的改动显著改善默认外观如果想让项目具备商业交付能力需要掌握 PyInstaller 的资源文件管理和多环境打包策略如果还想深入事件驱动编程Tkinter 的bind机制和自定义事件绝对是很好的入口。还有一个务实的建议以本文计算器为模板加一个历史记录列表、加一个科学计算函数、加一个按钮长按的重复输入效果。每一个需求都会逼你回头重新审视布局设计和状态管理这种“改需求”的过程比看十遍文档都管用。桌面 GUI 开发在现代软件生态里不是最热门的方向但 Tkinter 依然是 Python 生态里最稳定、最没有经济成本的选择之一。它解决的不是“炫酷”的问题而是“可靠交付”的问题。理解这一点你才算真正理解了 Tkinter 的位置。最后提醒一句本文所有示例代码都建议在虚拟环境里跑不要把项目依赖装在全局环境里这不是 Tkinter 特有的要求而是所有 Python 项目都该遵守的基本纪律。