Thonny 这个 IDE 我用得算久了,从最早带零基础的人入门 Python,到后来拿它当 ESP32-S3 跑 MicroPython 的主力编辑器,它一直属于"装完就能干活"的那一类工具。但"装完就能干活"和"装完就完全符合我的交付要求"是两回事。上个月给一间实训室做统一环境交付,Thonny 装完第一件事就是处理界面里那个内置的旗帜图标——一张和功能完全无关的装饰性贴图,出现在"关于"对话框里。实训机的要求是界面元素尽量干净,学生打开软件看到的每一像素都应该和写代码有关。所以我花了点时间把 Thonny 的资源目录翻了一遍,把去掉这个图标的几种做法都试了一轮,顺手也把批量部署和 ESP32 教学环境的配置一起理顺了。这篇就把整个过程拆开讲清楚,包括为什么有些改法看着省事却一定会翻车、升级之后怎么不让改动被冲掉、以及同一套思路怎么顺带用在 MicroPython 环境的统一定制上。
1. 先把"去掉图标"这件事拆成能执行的动作
很多人遇到这类需求,第一反应是打开安装目录找到一个 png 删掉,重启软件,发现没变化或者直接报错,然后就卡住了。问题不在于手速,而在于一开始没有判断这张图到底是"文件资源"还是"代码里内嵌的数据",也没有想清楚这次改动是要一次性的还是要能反复执行的。这两点判断错了,后面每一步都会走偏。
1.1 要动的到底是哪一层东西
Thonny 的界面是用 Python 加 tkinter 写的,这一点很关键。tkinter 加载图片的典型方式是tkinter.PhotoImage(file="某个路径"),也就是说,界面上看到的绝大多数图标、贴图,本质上是磁盘上一个真实的图片文件,运行时被读进内存。这意味着两件事:第一,改文件是可行的;第二,如果文件被删掉而代码还在尝试加载它,Tcl 层会直接抛TclError: couldn't open "...",软件可能在启动或者点开对话框的瞬间崩给你看。
另一个可能性是图片根本没有独立文件,而是被转成 base64 字符串写死在.py里,再用tkinter.PhotoImage(data=...)加载。这种情况删文件是没用的,必须动源码。判断方法很简单:先在资源目录里找有没有对应的图片文件,再去源码里搜这个文件名的引用。如果两处都找不到,才需要往 base64 方向查。
所以整个动作链其实是三步:定位资源 → 判断类型 → 选择改法。跳过第一步直接改,等于闭着眼睛修车。
1.2 三种场景对应的改法完全不同
同一个"去掉图标"的需求,放在不同场景下,成本差好几倍。我在实训室、个人笔记本、以及给朋友做的内部打包版这三种环境里各走了一遍,感受非常明显:
| 场景 | 影响面 | 推荐改法 | 后续维护成本 |
|---|---|---|---|
| 个人单机自用 | 1 台机器 | 直接替换资源文件 | 升级后手动重做一次,可接受 |
| 实训室/机房批量 | 几十台机器 | 替换文件 + 脚本化 + 配置一起下发 | 低,脚本可重复执行 |
| 做成内部发行版 | 长期多处使用 | fork 源码改完重新打包 | 前期高,后期最低 |
单机自用其实是最简单的,改完就完事。真正的坑在批量场景:你今天改完十台,明天有人手抖点了升级,图标全回来了,而且因为文件被覆盖,你甚至不容易发现。发行版场景则要考虑源码怎么改得干净、不破坏原有的资源加载逻辑。
我的建议是,只要涉及两台以上机器,就一定要把改动写成脚本,而不是靠手动操作记录。这个习惯在后面会反复救你。
2. 定位资源:从"关于"对话框反查到具体文件
这一步是整个流程里最像侦探工作的部分,也是最值得花时间的部分。找对了文件,后面十分钟就结束了;找错了,你可能改一个下午都在改一个根本没被加载的副本。
2.1 先确认你的 Thonny 装在哪
不同安装方式,目录结构差别很大,先确认清楚再动手:
- pip / pipx 安装:包在 Python 环境的
site-packages/thonny下,跟着虚拟环境走。 - 官方 Windows 安装包:通常落在
%LOCALAPPDATA%\Programs\Thonny,包体在Lib\site-packages\thonny;有些版本或安装选项会装到Program Files下,那里默认只读。 - Linux 发行版仓库装:一般在
/usr/lib/python3/dist-packages/thonny或/usr/lib/python3.*/site-packages/thonny。 - macOS 应用包:在
.app里,需要右键"显示包内容"才能进去。
最省事的确认方式不是去猜路径,而是让 Python 自己告诉你:
python -c "import thonny, os; print(os.path.dirname(thonny.__file__))"如果你平时用的是 Thonny 自带的解释器而不是系统 Python,记得在 Thonny 的 Shell 里跑这一句,才能拿到它自己实际加载的那个包路径。这一步经常被忽略,结果是在系统 Python 的目录里改了半天,而 Thonny 用的是另一份拷贝。
2.2 一段脚本把所有图片资源列出来
拿到包目录之后,不要手动一层层点。直接扫一遍所有图片文件,按大小排序,通常一眼就能锁定目标:
import os import thonny root = os.path.dirname(thonny.__file__) for dirpath, dirnames, filenames in os.walk(root): for fn in filenames: if fn.lower().endswith((".png", ".gif", ".jpg", ".jpeg", ".ico", ".bmp", ".ppm", ".xbm")): p = os.path.join(dirpath, fn) print(f"{os.path.getsize(p):>8} {os.path.relpath(p, root)}")输出里那些几百字节到几 KB 的小图,基本都是按钮图标;"关于"对话框里那张尺寸明显大一圈的旗帜贴图,通常会是列表里比较扎眼的一个。资源一般集中在thonny/res这个目录下,但也有些版本会把图标分散到插件目录里,所以用遍历而不是用ls是更稳的做法。
拿到候选之后,最直接的验证方法是把文件临时改个名,然后打开 Thonny 点开"关于"对话框,看是报错、留白,还是完全没变化。这个对照实验能一次性告诉你三件事:文件是不是真的被用、代码有没有容错、以及布局会不会塌。
2.3 在源码里搜引用,确认是文件还是内嵌
Windows 上用 PowerShell:
Get-ChildItem -Path $env:THONNY_ROOT -Recurse -Include *.py | Select-String -Pattern "PhotoImage|\.png|\.gif|base64"Linux / macOS 上更快:
grep -rn --include="*.py" -E "PhotoImage|\.png|\.gif|base64" "$(python -c 'import thonny,os;print(os.path.dirname(thonny.__file__))')"重点看两类结果:一类是file=<资源路径>,说明是外部文件,替换文件即可;另一类是data=<长字符串>,说明是内嵌数据,必须改代码。还有一种中间情况是资源路径由变量拼接而成,比如os.path.join(get_workbench().get_localized_... ),这种就得顺着变量往上追一层。
提示:搜索时不要只搜旗帜这个词,搜加载图片的 API 名字命中率更高,"关于"对话框的具体实现模块名各版本有差异,按 API 搜比按语义搜靠谱。
3. 替换、删除还是打补丁:三种做法的真实成本
这是最容易想当然的地方。我一开始也以为"删掉就完了",实测下来删文件是最容易翻车的方案,没有之一。
3.1 为什么直接删文件最容易出事
tkinter 的PhotoImage在文件不存在时不会安静地返回一张空图,它会把 Tcl 层的错误直接抛到 Python,也就是TclError。如果这个加载过程发生在窗口初始化阶段,结果就是对话框打不开甚至整个界面起不来;如果发生在回调里,就是点一下崩一下。更麻烦的是,这类异常往往只在打开特定对话框时才触发,日常敲代码完全正常,等到你在实训室里演示的时候才炸,现场非常尴尬。
正确做法是用一张同样尺寸的透明 PNG 覆盖原文件,而不是删掉它。这样代码照样能加载成功,只是画出来什么都看不见,布局也不会因为缺图而塌掉。这个思路在资源定制里是通用套路:能替换就不要删除。
3.2 什么时候必须动源码
有三种情况替换文件解决不了:
第一,图片是 base64 内嵌的,磁盘上没有对应文件。这种情况必须找到构造PhotoImage的那一行,把 data 换成一张透明图的 base64。
第二,代码里对图片尺寸有硬编码依赖,比如布局按图片宽度算坐标,你换了一张不同尺寸的图,对话框排版就歪了。这种情况下替换时要严格保证宽高一致,或者干脆改源码里那段布局逻辑。
第三,图片来源被简化到只剩一个明显的留白,视觉上比原来更难看。这时候与其纠结替换尺寸,不如把那段创建图片并pack的代码整段去掉,反而更干净。
3.3 三种方案对比
| 方案 | 操作难度 | 抗升级 | 风险 | 适用场景 |
|---|---|---|---|---|
| 直接替换资源文件 | 低 | 差,升级即失效 | 低(有备份时) | 单机自用 |
| 替换文件 + 补丁脚本 | 中 | 中,升级后重跑脚本 | 低 | 机房批量 |
| 改源码重新打包 | 高 | 好 | 中,需回归测试 | 内部发行版 |
补丁脚本方案是我最推荐的折中:成本可控,而且"升级后重跑一次"这个动作本身就是一种保障,你能明确知道当前机器的状态是不是你想要的。
4. 实操:十几分钟把图标换成透明占位
下面这套流程我在 Windows 和 Linux 上都跑过,Windows 上唯一多出来的一步是权限处理。
4.1 先做备份,别省这一步
cd "$(python -c 'import thonny,os;print(os.path.dirname(thonny.__file__))')" mkdir -p _backup_res cp -r res _backup_res/ 2>/dev/null || true或者更保险的做法,直接把整个thonny包目录复制一份到旁边,起名thonny.orig。这样即使你把某个.py改坏了,也能整目录换回来。备份这件事在资源定制里尤其重要,因为资源文件没有任何版本控制帮你兜底。
4.2 生成一张同尺寸的透明 PNG
不必依赖 Pillow,纯标准库就能写出来,机房环境里少装一个库就少一个坑:
import struct import zlib def write_transparent_png(path, width, height): raw = b"".join(b"\x00" + b"\x00\x00\x00\x00" * width for _ in range(height)) def chunk(tag, data): head = struct.pack(">I", len(data)) + tag + data return head + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF) ihdr = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0) png = (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", zlib.compress(raw, 9)) + chunk(b"IEND", b"")) with open(path, "wb") as f: f.write(png) write_transparent_png("blank.png", 48, 32)宽高必须按你实际找到的那张原图来填。拿不准尺寸的话,用前面那段遍历脚本配合PIL.Image.open(p).size看一眼,或者干脆照着原图宽高填。尺寸不对虽然也能跑,但布局可能出现偏移。
4.3 覆盖并验证
cp blank.png "$(python -c 'import thonny,os;print(os.path.join(os.path.dirname(thonny.__file__),"res"))')/<原文件名>"覆盖之后重新启动 Thonny,第一件事是打开"关于"对话框看了一眼,确认没有报错;第二件事是正常敲几行代码、运行一次,确认主界面没受影响;第三件事是打开文件、切主题,把常用交互过一遍。只验证图标本身是不够的,因为资源目录里往往还躺着其他图标,误伤的情况要靠完整走一遍流程才能发现。
4.4 Windows 上的只读目录问题
如果 Thonny 装在Program Files或者受管目录下,覆盖会直接报权限错误。三个选择:以管理员身份运行一次命令行做覆盖;把 Thonny 重装到用户目录;或者用pip install --user装一份到自己的环境里。我个人倾向于第二种,一次装好,后续所有改动都不用提权,长期看省事得多。
5. 改完之后怎么让它活下来:升级、分发与批量部署
改一次不难,难的是三个月后这批机器还是你交付时的样子。
5.1 升级会冲掉什么
pip install --upgrade thonny或者安装包覆盖安装,都会把整个thonny包目录重新写一遍,你替换的资源文件、改过的.py,全部回到原样。这一点必须提前知道,否则会出现"明明改过,怎么又回来了"的困惑,而且因为改动无声无息地消失,很容易被误判成改法无效。
如果你改的是.py文件,Python 会依据文件修改时间重新编译__pycache__里的缓存,所以正常编辑后直接重启就能生效,不需要手动删缓存。但如果 Thonny 是以某种打包形式安装的(比如被压成 zip 或者冻结进可执行文件),那么源码改动根本不会被读取,这种情况下只能走重新打包的路线。
5.2 写一个可重复执行的补丁脚本
思路很简单:脚本自己找到当前生效的 thonny 包目录,把目标资源覆盖成透明图,同时打印出改动的文件路径和大小。这样每次升级完跑一遍就行:
import os import shutil import thonny TARGET = "res" # 目标资源目录 ICON = "目标文件名.png" BLANK = os.path.join(os.path.dirname(__file__), "blank.png") root = os.path.dirname(thonny.__file__) for dirpath, dirnames, filenames in os.walk(root): if ICON in filenames: dst = os.path.join(dirpath, ICON) shutil.copyfile(BLANK, dst) print("patched:", dst)把它和blank.png放在同一个目录,双击即可运行。加上"打印改动了哪些文件"这一句是有意为之的——批量部署时,你需要一份可以贴在交付记录里的输出。
5.3 配置文件一起下发,效果更统一
界面定制只解决了外观,使用体验的一致性还得靠配置。Thonny 的配置存在Thonny.ini里,Windows 在%APPDATA%\Thonny\,Linux 和 macOS 在~/.config/Thonny/。你在 Tools → Options 里调好的字体、缩进、自动保存、解释器路径等,全在这个文件里。
机房交付的常见做法是:在一台机器上把所有设置调好,把Thonny.ini拷出来,批量分发到每台机器的对应目录,覆盖前先退出 Thonny。这比让几十号人自己点一遍设置靠谱得多,也避免了"为什么他的缩进是 4 格我的是 8 格"这类无意义的问答。
注意:
Thonny.ini里可能记录了上次连接的串口、最近打开的文件路径等机器相关的内容。批量分发前建议把这几项清一下,否则新机器上会残留别人的路径。
6. 顺带聊聊 Thonny 的另一条主线:ESP32-S3 与 ST7789 的配置
之所以要在同一篇里说这块,是因为我这次做界面定制,起因就是实训室的嵌入式教学环境。Thonny 不只是个 Python 编辑器,它在 MicroPython 教学里几乎是标配,而一旦涉及几十台带屏幕的开发板,环境统一和界面统一的诉求是一起出现的。
6.1 解释器与串口这两个设置决定了大部分"连不上"
Tools → Options → Interpreter 里选 "MicroPython (ESP32)",端口尽量手动指定,不要依赖自动检测。自动检测在多设备同时插着的机器上经常认错口,表现就是"连上了但 REPL 没反应"。Windows 上看设备管理器确认是哪个 COM 口,Linux 上通常是/dev/ttyUSB0或/dev/ttyACM0,用之前记得把自己加进对应的用户组并重新登录,否则会出现权限被拒。
如果板子上还没烧 MicroPython 固件,可以用 Thonny 自带的安装器:Interpreters 页面里点安装或更新固件,芯片选 ESP32-S3,选对端口,按提示走完。手动方式也行,esptool两个命令,芯片参数写esp32s3,写入偏移是0x0。烧录时如果一直提示连接失败,按住板子上的 BOOT 键再上电,进入下载模式通常就能过。
REPL 里两个快捷键要记住:Ctrl+C中断当前运行的程序,Ctrl+D软重启。写屏幕驱动的时候这两个键会被反复用到,尤其是代码跑飞导致 REPL 没响应的时候。
6.2 点亮 ST7789 的接线与最小代码
ST7789 是很常见的 SPI 小屏,接线的关键是把 SPI 的时钟和数据线接对,其余都是控制线。下面是一组可用的示例接法,具体 GPIO 按你的板子调整:
| 屏幕引脚 | ESP32-S3 示例 GPIO | 说明 |
|---|---|---|
| VCC | 3V3 | 供电,别接 5V |
| GND | GND | 共地,必须接 |
| SCL / CLK | GPIO12 | SPI 时钟 |
| SDA / MOSI | GPIO11 | SPI 数据 |
| RES | GPIO9 | 复位 |
| DC | GPIO8 | 数据/命令选择 |
| CS | GPIO10 | 片选,固定接低也能省一个引脚 |
| BLK | GPIO7 或 3V3 | 背光控制 |
最小验证代码大致是这样:
from machine import Pin, SPI import st7789 spi = SPI(2, baudrate=40_000_000, polarity=0, phase=0, sck=Pin(12), mosi=Pin(11)) tft = st7789.ST7789( spi, 240, 320, reset=Pin(9, Pin.OUT), dc=Pin(8, Pin.OUT), cs=Pin(10, Pin.OUT), backlight=Pin(7, Pin.OUT), rotation=0, ) tft.init() tft.fill(st7789.BLACK) tft.fill_rect(20, 20, 200, 100, st7789.RED)跑不通的时候,排查顺序我一般是这样的:先看背光亮不亮(不亮是供电或背光引脚的问题),再看屏幕是全白还是全黑(全白多半是复位或初始化时序),最后再怀疑 SPI 参数。花屏和颜色错乱九成是频率太高或者颜色顺序不对,把 baudrate 从 40MHz 降到 20MHz 甚至 10MHz 试一次,再把颜色顺序在 RGB 和 BGR 之间切一下,基本能解决。这个降频排查法比反复检查代码有效得多。
6.3 把驱动文件放进板子,别每次往 REPL 里贴
驱动代码动辄几百行,直接贴在 REPL 里既占内存又难维护,改一个参数就得重新贴一遍。正确做法是在 Thonny 里用"另存为 MicroPython 设备",把驱动文件存成板子上的模块文件,之后import st7789就能直接用。板子文件系统空间有限,上传前把驱动里的注释和用不到的示例代码删掉,往往能省下可观的体积。
上传完之后如果导入报错,先确认文件名和模块名一致,再确认板子文件系统里没有同名目录。这两个原因占了我遇到过的导入失败问题的绝大多数。
6.4 界面定制和教学环境的真实关系
讲到这里,前面那条"去掉图标"的线索就和这块合上了。教学机房的机器是给学生用的,界面里任何装饰性的、与写代码无关的元素,都会变成课堂上的提问来源。把界面收干净,把解释器路径、串口、驱动文件、示例代码全部预置好,学生开机就能跑通第一个程序,这才是交付的意义。界面定制看起来是个很小的事,但它是整个环境统一工作的一部分,做法和思路跟配置串口、预置驱动是完全一样的:先定位,再判断,再写脚本固化下来。
7. 排查清单:改完图标之后最常见的几个问题
最后把这一路踩过的坑整理成一份对照表,遇到问题可以直接对着查。
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 重启后图标还在 | 改的是另一份安装,或者根本没重启 | 用thonny.__file__确认实际加载路径 |
| 点开"关于"直接报 TclError | 原文件被删了,代码仍在加载 | 换回同尺寸透明图,不要删文件 |
| 布局明显歪了 | 替换图尺寸与原图不一致 | 严格按原图宽高重新生成 |
| 覆盖时报权限错误 | 目录只读或需要管理员 | 提权操作,或改用用户目录安装 |
| 升级后所有改动消失 | 包目录被整体覆盖 | 重跑补丁脚本,或改用打包发行 |
改了.py但行为没变 | 安装形态不支持读源码 | 确认是否为打包/冻结安装 |
| 改完 Thonny 但 ESP32 连不上 | 与图标改动无关,是解释器和端口配置 | 手动指定端口,检查驱动和权限 |
我个人在实际操作中的体会是,这类"改一个资源"的需求,真正花时间的从来不是改的动作,而是确认改的是哪个文件、以及让改动在三个月后还能存活。所以哪怕只是给一台机器去掉一个图标,我也会顺手把补丁脚本和备份一起留下。下次升级完,跑一遍脚本,三十秒,状态就回到了你交付时的样子——这个习惯,在机房环境里比任何技巧都值钱。