图形界面程序的结构、事件与交互
程序分成界面和逻辑两部分,两者只通过函数调用联系。换一个界面,逻辑一行都不用改。
把与人打交道的代码和把事情算对的代码写在不同位置,两者只通过接口互相调用。
算出结果、真正改动文件的代码,本章指 preview_rename 和 do_rename 两个函数,共 29 行。
负责显示、输入、确认和提示的代码,把使用者的动作转成对逻辑函数的调用。
do_rename 里直接写 status_label.config(…)
窗口版能跑;命令行下没有 status_label,命令行版坏了
显示、弹窗、状态行全部写在外壳里
两个壳都能跑
函数是起了名字的一段代码,写上名字加括号就是调用。壳把材料作为参数交进去,拿回返回值,再自己决定怎么显示。
起了名字的一段代码。别处写上这个名字加一对括号,就是调用它。
写在函数名后括号里的材料,调用时填什么,函数就用什么。
函数用 return 交回的结果。交回之后由谁显示、怎么显示,函数一概不管。
from pathlib import Path # ============ 逻辑区 · 换壳时一行都不要动 ============ def preview_rename(folder, prefix): # 预演:算出每个文件将要改成什么名字,返回一份清单,不改动任何文件 plan = [] number = 0 for old in sorted(p for p in Path(folder).iterdir() if p.is_file()): if old.suffix.lower() not in ['.jpg', '.png']: continue number = number + 1 new = old.with_name(prefix + '_' + str(number).zfill(3) + old.suffix.lower()) if new.exists() and new != old: new = old.with_name(new.stem + '-2' + new.suffix) plan.append((old, new)) return plan def do_rename(plan, on_progress=None): # 执行:按清单真正改名,返回成功条数;每改完一个调用一次 on_progress done = 0 for old, new in plan: old.rename(new) done = done + 1 if on_progress is not None: on_progress(done, len(plan)) return done # ============ 命令行外壳 · 窗口版会把这一段整个换掉 ============ def print_progress(done, total): print(' 已处理', done, '/', total) if __name__ == '__main__': # 外壳的起点:只在直接运行本文件时执行 plan = preview_rename(folder, PREFIX) # ……打印清单,input 等使用者输入 y done = do_rename(plan, print_progress)
函数内部直接写 '测试文件夹'、'课堂照片'
窗口上的输入框传不进去,换一批文件只能改代码
def preview_rename(folder, prefix)
调用时填什么就用什么
函数内部逐行打印清单
窗口版没法把结果放进文本框
return plan
命令行外壳打印,窗口外壳放进文本框
接口是两部分代码之间的约定:函数名、参数、返回值的形状。接口不变,壳和逻辑可以各改各的。
两部分代码之间约定好的函数名、参数(个数与顺序)和返回值的形状。
AI 顺手给函数改名,或者加一个参数
窗口版能跑,命令行版随之失效
"只能调用它们,不要给它们改名或加参数"
接口不变,命令行版照常可用
窗口程序先搭好界面,然后停下来等待。使用者点了哪个按钮,才执行登记给它的那段代码。
功能不变,使用者从你自己变成别人。多做一个窗口,要处理这三件事:
| 命令行版要求使用者 | 窗口版的做法 | 不要这样做 |
|---|---|---|
| 在终端里打出文件夹路径,还要知道先切到哪个目录 | 弹出系统对话框,用鼠标点选 | 让对方自己把路径抄进去 |
| 打开代码,改最上面设置区里的文件夹、前缀 | 窗口上放输入框 | 让对方去改代码里的设置区 |
| 看懂一屏英文报错 | 一句中文提示:出了什么事、现在是什么状态 | 把英文报错原样弹给对方 |
提供窗口、按钮、输入框等现成部件。tkinter 随 Python 一同安装,本课程一律用它。
界面搭好后停下等待,使用者的操作(事件)发生时,才执行登记给它的代码。
command=on_run 登记按钮被点时执行哪个函数;root.mainloop() 让程序停在这里等待。
# ① 点「选择文件夹」时执行:弹出系统对话框,选完立即预演 def on_choose_folder(): picked = filedialog.askdirectory(title='选择要处理的文件夹') if not picked: # 使用者点了取消 return current_plan = preview_rename(Path(picked), prefix_entry.get().strip()) # ……把清单逐行放进文本框 # ② 点「确认执行」时执行:先二次确认,再调用逻辑区的 do_rename def on_run(): agreed = messagebox.askyesno('确认执行', '将要改动 ' + str(len(current_plan)) + ' 个文件的名称。\n\n这一步没有撤销,确定继续吗?') if not agreed: return try: done = do_rename(current_plan, update_progress) except Exception as error: messagebox.showerror('执行中断', '出错的文件已停下,后面的文件保持原样。\n\n' + str(error)) return messagebox.showinfo('完成', '共改名 ' + str(done) + ' 个文件。') # ③ 登记:这个按钮被点的时候,去执行 on_run 这一段 tk.Button(top, text='确认执行', command=on_run).pack(side='left') # ④ 搭完界面之后停在这里等,下面不会再有代码被执行 root.mainloop()
command=on_run()
界面刚搭好就执行了一遍,之后点按钮没反应
command=on_run
按钮被点时才执行
AI 换用需要 pip 安装的界面库
机房网络下可能装不上;装上了文件体积也明显变大
import tkinter as tk
装好 Python 的机器直接能跑
把一个函数交给另一个函数、由对方回头调用,这叫回调。逻辑每改完一个文件调用一次,显示在哪里由壳决定。
作为参数交给另一个函数、由对方在合适的时候回头调用的函数。交的时候不加括号,交进去的是函数本身。
一段代码长时间不歇手,窗口得不到重画的机会,看上去像死了,程序仍在运行。
do_rename 里 print 每一条进度
窗口版要么改函数,要么看不到进度
do_rename(plan, on_progress=None)
命令行交打印,窗口交更新状态行,不交也能跑
循环中途不更新界面
窗口整个变白、点不动,像是崩了
状态行显示「已处理 N / 共 M」,并让窗口重画
看得到数字在变
路径由系统对话框选取,点取消时拿回空值,要单独处理。不可撤销的操作执行前再问一次,写明动什么、有多少、能不能撤销。
操作系统提供的选择框。filedialog.askdirectory() 一行弹出,选完返回路径,点取消返回空值。
会造成不可撤销后果的操作,执行前再问一次。messagebox.askyesno() 返回"是"或"否"。
拿到返回值直接往下用
点了取消,程序报错退出
if not picked: return
点取消什么都不做
"确定要执行吗?"
使用者点得越熟越不看,确认框形同虚设
"将要改动 24 个文件的名称。这一步没有撤销,确定继续吗?"
使用者看到具体数字,会停下来核对
报错原文是给开发者定位用的。给使用者的提示要说清出了什么事、现在是什么状态、接下来怎么办;逻辑照常抛出错误,由外壳接住并改写。
面向使用者的出错说明,回答出了什么事、现在是什么状态、接下来该怎么办。
逻辑遇到错误不处理、直接抛出;外壳用 try … except 接住,再决定怎么告诉使用者。
弹窗里是一屏 Traceback
使用者看不懂,也不知道哪些文件已经改了
"出错的文件已停下,后面的文件保持原样。"英文原文附在末尾
使用者知道现在是什么状态
do_rename 出错时直接调用 messagebox
逻辑绑死在窗口上,命令行版没法用
try: do_rename(…) except …
换个壳,逻辑照样能用
给跑通的代码加界面,先划出不可改动区,起止位置写函数名。换壳单六行按颜色对应写进提示词。
提示词里写明一行都不许改的代码范围,用起止位置(函数名)标出,剩下的部分才允许 AI 自由发挥。
AI 说原来的写法可以更简洁,改掉几行
功能大概率仍对,但已无法确定与原来等价
逐行比对不可改动区,被改动的行贴回原文
逻辑区与命令行版逐行相同
| 提示词中的规定 | 缺少时的表现 | 代码中的对应 |
|---|---|---|
| 从 def preview_rename 到 do_rename 结束,一行都不要动 | AI 觉得原写法可以更简洁,改掉几行 | 两个函数与命令行版逐行相同 |
| 不要给它们改名或加参数 | 窗口版能跑,命令行版随之失效 | preview_rename(folder, prefix) |
| 点了取消时什么都不做 | 点取消后报错退出 | if not picked: return |
| 确认框写明将要改动多少个文件 | 只弹一句"确定吗" | askyesno(…len(current_plan)…) |
| 每处理一个就更新一次状态行 | 执行时窗口停止响应,像是崩了 | update_progress |
| 只用标准库 | 引入需要 pip 安装的界面库 | import tkinter as tk |
先有能跑通的命令行版,再加窗口;加完之后两个壳都要能跑。
运行生成脚本建立测试文件夹,把命令行版跑通。用注释划一条线,线上是逻辑,线下是外壳;圈不出来就先分开。没跑通就加壳,出了问题两头都不好查
产出:分好区的命令行脚本不可改动区的起止位置写函数名,不写"逻辑部分";窗口要包含的部件逐条列出
产出:一份完整的提示词拿到新代码先逐行比对不可改动区,可以用编辑器的文件比较功能,行数与内容都要对得上。被改动的行把原文贴回去,重新运行确认仍然可用
产出:对照检查的结论运行窗口版,正常执行一次、中途点取消一次、制造一次错误(预演后到文件夹里手工删掉或改名其中一个文件,再点执行),各截一张图
产出:三张截图确认它没有被改坏。两个壳都能跑,是本章的核心验收项
产出:命令行版仍可用的截图把两份文件发给同组同学,请他只用窗口完成一次操作。不要口头指导,他卡在哪一步、哪句提示看不懂,如实记下来
产出:一行互测记录窗口出问题时,先按现象找原因;改完之后两个壳都再跑一次。
| 现象 | 原因 | 处理 |
|---|---|---|
ModuleNotFoundError: No module named 'tkinter' | 安装 Python 时没勾选 tcl/tk 组件 | 重新运行安装包选 Modify,勾上 tcl/tk and IDLE 一项 |
| 运行后窗口随即关闭 | 缺少最后一行 root.mainloop() | 在文件末尾补上这一行 |
| 界面刚出来按钮就自己执行了一次,之后点它没反应 | 登记时写成了 command=on_run() | 去掉括号,写成 command=on_run |
| 点按钮没有任何反应 | 函数名拼错,或者按钮登记的是另一个名字 | 核对 command 后面的名字与函数定义处是否完全一致 |
| 执行时窗口整个变白、点不动 | 循环中途没有留给窗口重画的机会 | 每处理一个就更新一次状态行,并让窗口重画一次 |
| 点了取消,程序却报错退出 | 对话框返回空值时没有处理 | 调用之后先判断结果是否为空,为空就直接返回 |
| 窗口里的中文变成方块 | 源文件保存时用了非 UTF-8 编码 | 用编辑器另存为 UTF-8,重新运行 |
| 加完窗口,命令行版跑不通了 | 逻辑函数被改动,或者界面代码混进了逻辑区 | 把逻辑区恢复成原样,界面相关的语句一律移到外壳里 |
| 没选文件夹就点「确认执行」,没有任何反应 | 外壳没有处理"还没有预演"的情况 | 用"改界面"提示词,改成弹窗提示一句中文 |
| 文本框显示不下,看不到后面的内容;窗口太小时按钮被挤掉 | 没有滚动条,窗口没有最小尺寸 | 用"改界面"提示词,加竖向滚动条、设最小尺寸 |
提交四项内容,按七条标准验收;选做一项。
按教师提供的作业模板填写,文档开头注明所选题目;提交渠道见课堂通知,当堂提交。
零代码应用开发 · 第 12 章 给工具一个窗口