零代码应用开发·第 12 章·3 学时

给工具一个窗口

同一套逻辑,换一个壳。逻辑那二十九行,一个字都不改。

本章精髓 界面和逻辑是两件可以分开的事。同一套逻辑既能套一个命令行的壳,也能套一个窗口的壳。换壳不换芯,是程序能被不懂终端的人用起来的关键一步。

学完本章后,你应该能够:

  1. 指出一份脚本里哪几行属于逻辑、哪几行属于界面,并说明分界线画在哪里;
  2. 在提示词里划出不可改动区,保住已经跑通的那部分逻辑;
  3. 给一个命令行脚本加上窗口,包含文件选择、结果显示与状态提示;
  4. 为危险操作加上有效的二次确认,并把报错改写成使用者看得懂的提示;
  5. 说明窗口在长任务中停止响应的原因,并给出最简单的缓解办法。
配套演示文件 第12章演示_三个壳.html 第12章演示_窗口沙盘.html 第12章演示_生成测试文件夹.py 第12章演示_命令行版.py 第12章演示_窗口版.py

同一个工具,两个壳

成品 · 共用的二十九行 · 划出不可改动区

先看两个壳分别长什么样,再看它们共用的是什么。

1.1本章成品:同一个工具的两个壳

手上有一个批量重命名的脚本,已经在终端里跑通了。本章给它加一个窗口:点按钮选文件夹,在窗口里看到将要改成什么,点确认才执行,底下一行显示进度。

原来的样子。在终端里输入命令,看打印,输入 y 确认。

命令提示符
…\第12章> python 第12章演示_命令行版.py 预演结果,共 24 个文件将被改名: DSC01.JPG → 课堂照片_001.jpg (中间省略二十余行) 确认执行请输入 y,直接回车取消:

加上窗口之后。同样这几件事,改成点按钮完成。

批量重命名
文件名前缀: 课堂照片 选择文件夹 确认执行
当前文件夹:C:\Users\liwei\Desktop\第12章\测试文件夹
DSC01.JPG → 课堂照片_001.jpg IMG_2001.jpg → 课堂照片_002-2.jpg IMG_2002.jpg → 课堂照片_003.jpg (下略)
预演完成,共 24 个文件待改名。核对无误后点「确认执行」。

窗口里显示的内容,与终端里打印的完全一致

1.2两个壳共用的是什么

两份文件放在一起对照,会发现中间那二十九行完全相同:算出每个文件将要改成什么名字的那一段,以及按清单真正改名的那一段。换壳的过程中,这二十九行一个字都没有动。

命令行外壳
print / input
打印清单,输入 y 确认
逻辑区
preview_rename
do_rename
共 29 行 · 一行未改
窗口外壳
按钮 / 弹窗
点按钮,弹窗确认

壳负责与人打交道,芯负责把事情算对

打开 第12章演示_三个壳.html,同一段逻辑外面换三种壳,看哪一部分在变、哪一部分一动不动。

1.3为什么值得多做一个壳

命令行版本已经能用了,功能一样也不少。多做一个窗口,改变的不是功能,而是使用者:从"你自己"变成"别人"。这一变化带来三件必须处理的事。

第一件
不用打路径
别人不会在终端里输入文件夹路径,也不知道要先切到哪个目录。改成弹一个系统对话框,用鼠标点。
让对方自己把路径抄进去
第二件
不用记参数
前缀、文件夹这些设置原来写在代码最上面,要改就得打开代码。改成窗口上的输入框。
让对方去改代码里的设置区
第三件
出错要能看懂
终端里那一屏英文报错,对别人没有任何意义。改成一句中文提示,说清出了什么事、现在是什么状态。
把英文报错原样弹给对方
讨论命令行版本与窗口版本的功能完全相同,为什么不干脆只做窗口版?在什么情况下命令行反而更好用?想一想需要在服务器上定时自动运行、或者一次要处理三十个文件夹的场景。

1.4本章新技巧:划出不可改动区

把一份已经跑通的脚本整个交给大模型,再要求它加界面,得到的往往是一份被重写过的新代码:变量改了名、循环换了写法、几个判断被合并。这些改动多数没有错,但它们把已经验证过的东西变回了未验证的东西,而你手上没有测试可以再验一遍。

所以提问时要先划出边界:哪一段一行都不许动,写清楚起止位置。剩下的部分才允许它自由发挥。边界划得越具体,它照做的可能性越高。写明"从 def preview_rename 到 do_rename 结束",比写"逻辑部分不要动"有效得多。

已经跑通的代码是一份资产。给它加东西是投入,重写它则是把资产折价重来一次。

1.5可直接复制的提示词

发给 AI主提示词 · 给已跑通的脚本加窗口
下面这段 Python 脚本我已经跑通了,逻辑正确,现在要给它加一个窗口界面。 【不要改动】从 def preview_rename 到 do_rename 结束的这两个函数,一行都不要动, 原样保留在新代码里。你只能调用它们,不能修改它们,也不要给它们改名或加参数。 【要新增】一个 tkinter 窗口,包含: ① 一个填写文件名前缀的输入框,默认值填「课堂照片」; ② 一个「选择文件夹」按钮,点了弹出系统的文件夹选择框,选完立即调用 preview_rename; ③ 一个多行文本框,把预演结果一行一条显示出来; ④ 一个「确认执行」按钮,先弹出确认框写明将要改动多少个文件,点「是」才调用 do_rename; ⑤ 底部一行状态提示,执行过程中显示「已处理 N / 共 M」。 【其他要求】 使用者在文件夹选择框里点了取消时,什么都不做,不要报错; 出错时用弹窗给出中文提示,写清出了什么事、现在是什么状态,不要把英文报错直接显示给使用者; 执行过程中每处理一个就更新一次状态行,让窗口不至于长时间没有反应; 只用标准库,不要引入任何需要 pip 安装的界面库。 每一段代码前加一行中文注释。脚本如下: [粘贴脚本]
发给 AI改界面 · 逻辑仍然不许动
窗口已经能用了,要调整界面,逻辑区仍然一行都不要动。 要改的地方: ① 文本框显示不下时看不到后面的内容,请加上竖向滚动条; ② 窗口太小时按钮会被挤掉,请给窗口设一个最小尺寸; ③ 没有选择文件夹就点「确认执行」时,现在没有任何反应,请改成弹窗提示一句中文。 【不要改动】preview_rename 与 do_rename 两个函数,以及已经写好的确认框文案。 只给出需要新增或修改的那几行,不要重写整个脚本。
发给 AI改写错误提示 · 把报错翻译给使用者
窗口版现在出错时,弹窗里显示的是 Python 的英文报错原文。请改写成使用者看得懂的中文。 对下面三种情况分别给出提示文案,每条都要说清三件事:出了什么事、现在是什么状态、接下来该怎么办。 ① 选中的文件夹里一个图片都没有; ② 某个文件正被其他程序打开,改名被系统拒绝; ③ 目标名称已经被占用。 【不要改动】逻辑区的两个函数。错误仍然由它们抛出,只在外壳里接住并改写文案。 英文原文可以附在提示的末尾,但要放在中文说明后面。 只给出需要新增或修改的那几行。

主提示词里的几条要求,分别对应后面要讲的知识点:

从 def preview_rename 到 do_rename 结束,一行都不要动
知识点 04:写明起止位置,比写"逻辑部分不要动"有效得多
只能调用它们,不能改名或加参数
知识点 03:改了名字或参数就等于改了接口,命令行版随之失效
点了取消时什么都不做,不要报错
对话框返回空值是正常情况,必须单独处理
先弹出确认框写明将要改动多少个文件
知识点 08:确认框要给出具体数量,只写"确定吗"起不到作用
每处理一个就更新一次状态行
知识点 07:否则窗口在执行期间会停止响应,看上去像是崩了
只用标准库,不要引入需要 pip 安装的界面库
知识点 05:tkinter 随 Python 一同安装,机房网络下不必冒安装失败的风险

1.6大模型的常见偏差

未经要求就重写了逻辑函数。 这是本章最需要防的一种。它会觉得原来的写法可以更简洁,于是改掉几行。改完之后功能大概率仍然正确,但你已经无法确定它和原来完全等价。"一行都不要动"这句必须写,并且要写明起止位置。

引入需要安装的界面库。 它常会用比 tkinter 更现代的界面库,做出来确实好看。代价是要先 pip 安装,机房网络下可能装不上;即便装上,文件体积也会明显变大。

把逻辑和界面重新搅在一起。 常见的形式是:它在逻辑函数里直接加一句弹窗,或者直接把结果写进文本框。这样改完窗口版能跑,命令行版却坏了,而且下次再换壳还要重来。验收时要专门检查这一点。

确认框写成一句"确定吗"。 它默认给的确认文案往往不含具体信息。要求里要写明"写清将要改动多少个文件",否则这个确认框等于没有。

备用方案 现场生成不顺时,直接打开 第12章演示_窗口版.py。它的逻辑区与 第12章演示_命令行版.py 逐字符相同,可以直接用于对照检查。装不上 tcl/tk 的机器,用 第12章演示_窗口沙盘.html 代替:括号笔误、界面卡死、确认框的两种写法,都能在网页里当堂演一遍。所有演示文件只用标准库,不依赖网络。

函数、接口与界面逻辑分离 本章重点

函数 · 参数与返回值 · 接口 · 分离

本章的核心内容集中在这一部分和下一部分。

本章要学的术语 · 八个
01函数function
02参数与返回值parameter / return
03接口interface
04界面逻辑分离UI / logic
05图形界面 GUItkinter / GUI
06事件驱动event-driven
07回调callback
08二次确认confirmation

这八个术语会原样出现在界面库文档、交互规范与缺陷记录里。前四个在本部分讲解,后四个在第三部分讲解。

2.1四个知识点,一个一个看

01函数:给一段代码起个名字
起名字preview_rename
命令行壳按名字调用
窗口壳按名字调用

同一个名字,两处调用,代码只有一份

现象:代码里出现了 def preview_rename(folder, prefix): 这样的一行,下面缩进的几行是它的内容。

概念:函数(function)就是给一段代码起个名字。别处写上这个名字加一对括号,就是调用它。分离之所以能成立,靠的就是这一点:逻辑写成函数,壳只负责按名字调用,不必知道里面是怎么算的。

类比:菜单上写"宫保鸡丁"三个字,点菜的人不必知道后厨怎么做。

02参数与返回值:交进去什么,交回来什么
文件夹、前缀 preview_rename一份改名清单
材料写死在里面 谁也传不进去换一批文件只能改代码

括号里填的是材料,return 交回来的是成果

现象:函数名后面的括号里写着 folder, prefix,函数最后一行是 return plan

概念:括号里的名字叫参数(parameter),是这段代码干活需要的材料,调用时填什么它就用什么。return 交回去的东西叫返回值交回去之后由谁显示、怎么显示,这个函数一概不管。正是这一点让窗口壳能把同一份清单放进文本框,而命令行壳把它打印出来。

类比:把布料和尺寸交给裁缝,拿回一件衣服。衣服穿去哪里,裁缝不问。

03接口:两边约定好的那几个名字
约定的内容
改了会怎样
函数叫什么名字
另一个壳按旧名字找不到它
要几个参数、什么顺序
另一个壳传进去的东西对不上
交回来的是什么形状
另一个壳拿到手不会处理

这三项就是接口,改任意一项,两边都要一起改

现象:提示词里那句"不要给它们改名或加参数"。

概念:接口(interface)是两部分代码之间约定好的那几件事:名字、参数、返回值的形状。接口不变,两边就可以各自改各自的。所以"不许动逻辑"这句要求,实质上要保的不是那二十九行的写法,而是这三项约定。改名和加参数看起来只是小改,其实是在改约定。

类比:插头和插座。形状对得上,电器和电网各自换代都不影响使用。

04界面逻辑分离:壳与芯各管一头
界面(壳)
与人打交道:显示、输入、确认、提示
逻辑(芯)
把事情算对:算出改成什么、真正去改
两者之间
只靠约定好的函数名互相调用

换壳时只动上面一层,下面一层不动

现象:两份文件的中间二十九行完全相同,不同的只有上下两头。

概念:界面逻辑分离指把"与人打交道"的部分和"把事情算对"的部分写在不同的位置,两者只通过接口互相调用。分离之后,界面可以换、可以重做,逻辑不必跟着改;反过来,逻辑出错也不会牵连到界面。判断分离得好不好的办法很简单:换一个壳,逻辑要一行都不用改。

类比:同一台发动机可以装进不同的车壳。换壳的时候,发动机不必重造。

本章方法 先让命令行版本完全跑通,再加窗口。壳套在没跑通的芯上,出了问题两头都不好查。

2.2先看命令行版:逻辑与外壳已经分开了

加窗口之前,这份脚本已经写成了两段。上半段是逻辑,下半段是命令行外壳,中间用一行注释隔开。

第12章演示_命令行版.py全文 · 可直接运行
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__':
    FOLDER = '测试文件夹'
    PREFIX = '课堂照片'

    folder = Path(__file__).with_name(FOLDER)
    plan = preview_rename(folder, PREFIX)

    print('预演结果,共', len(plan), '个文件将被改名:')
    for old, new in plan:
        print('  ', old.name, '→', new.name)

    answer = input('确认执行请输入 y,直接回车取消:')
    if answer.strip().lower() == 'y':
        done = do_rename(plan, print_progress)
        print('完成,共改名', done, '个文件。')
    else:
        print('已取消,没有改动任何文件。')

    input('按回车键结束')

按本章方法核对:输入是文件夹与前缀两项,输出是屏幕上的预演清单与进度,重复执行的部分在两个函数内部各有一处。整份文件里真正与"命令行"这个壳有关的,只有最下面那十几行。

def preview_rename(folder, prefix):
给这一段算法起名,需要两样材料:文件夹与前缀。括号里的名字是给里面用的代号,调用时填什么它就用什么。 材料写死在函数里面时,窗口上的输入框就传不进去,只能改代码
return plan
把算好的清单交回给调用它的人。交回去之后由谁显示、怎么显示,这个函数一概不管。 在函数里直接 print 时,窗口版就没法把结果放进文本框
on_progress=None
留一个报告进度的接口,默认不报告。命令行壳传进来的是打印,窗口壳传进来的是更新状态行。同一个函数,两种显示方式。 进度直接写成 print 时,窗口版要么改函数,要么看不到进度
old.rename(new)
整份文件里唯一改动磁盘的一行。出错时不作处理,直接抛出,交给外壳去决定怎么告诉使用者。 在函数里弹窗提示时,这段逻辑就绑死在窗口上,命令行版没法用
if __name__ == '__main__':
外壳的起点。这一行以下的内容,只在直接运行本文件时才执行。 没有这一行时,别的文件一旦引用这两个函数,命令行那一套就会跟着跑起来
do_rename(plan, print_progress)
把"怎么显示进度"作为材料交进去。注意 print_progress 后面没有括号,交进去的是这个函数本身,不是它的结果

窗口这一层:事件、回调与二次确认 本章重点

tkinter · 点了才发生 · 回调 · 有效确认

窗口程序不从上往下跑完就结束,它搭好界面之后停下来等。

3.1另外四个知识点

05图形界面 GUI:Python 自带的那一个
批量重命名
课堂照片选择文件夹
DSC01.JPG → 课堂照片_001.jpg
共 24 个文件待改名

窗口、按钮、输入框、状态栏,都由界面库画出来

现象:代码第二行 import tkinter as tk,运行之后屏幕上出现一个窗口。

概念:窗口、按钮、输入框这些成品部件由图形界面库(GUI library)提供。tkinter 随 Python 一同安装,不需要另行下载,画出来的窗口也谈不上好看,但胜在到处都能跑。其他界面库功能更强,代价是要额外安装,并且体积可观。本课程一律用 tkinter,理由只有一条:不必安装。

类比:家里现成的工具,样子普通,但拿来就能用,不必出门去借。

06事件驱动:点了才发生
命令行 从上往下跑一遍跑完就退出
窗口 搭好界面,停下来等点了按钮才跑那一段

窗口程序大部分时间在等,不在跑

现象:代码最后一行是 root.mainloop(),运行到这里就停住了,窗口却一直开着。

概念:命令行程序从第一行跑到最后一行就结束。窗口程序不同:它先把界面搭好,然后停在原地等待,使用者点了哪个按钮,才去执行对应的那一段代码,这种方式叫事件驱动(event-driven)。command=on_run 这样的写法就是登记:这个按钮被点的时候,去执行这个名字对应的那段代码。

类比:按门铃才响。门铃平时并不工作,它只是接着,等人来按。

07回调:把"怎么显示"交给壳去决定
命令行壳交进去 print_progress终端打印一行
窗口壳交进去 update_progress状态行变数字
什么都不交不报告进度,照样能跑

同一个 do_rename,三种进度显示方式

现象:do_rename(plan, print_progress) 这一行,第二个材料填的是一个函数的名字。

概念:把一个函数当作材料交给另一个函数,让对方在合适的时候回头来调用它,交进去的这个函数叫回调(callback)。逻辑区只负责在每改完一个文件时喊一声,喊出来的话显示在哪里,由壳自己决定。注意 print_progress 后面没有括号,交进去的是这个函数本身,不是它的结果。

类比:寄快递时留一个电话号码,让对方到了再打给你。号码是留给他的,什么时候打由他定。

08二次确认:动手之前再问一句
确认执行
将要改动 24 个文件的名称。
这一步没有撤销,确定继续吗?

数量写进提示里,比只写"确定吗"有用得多

现象:点「确认执行」之后没有立刻开始,而是先弹出一个问句。

概念:会造成不可撤销后果的操作,执行前应当再问一次,这一步叫二次确认(confirmation)。有效的确认要给出具体信息:动的是什么、有多少、能不能撤销。只写"确定吗"的提示框,使用者点得越熟越不看,形同虚设。

类比:删除文件时系统问的那一句。问得具体,人才会真的停下来看一眼。

3.2窗口外壳怎么调用同一套逻辑

窗口版把上面那二十九行原样抄了过来,只重写外壳。外壳做三件事:搭界面、登记按钮、在被点击时调用逻辑。

第12章演示_窗口版.py节选 · 外壳的四个关键位置
# ① 点「选择文件夹」时执行:弹出系统对话框,选完立即预演
def on_choose_folder():
    picked = filedialog.askdirectory(title='选择要处理的文件夹')
    if not picked:                      # 使用者点了取消
        return
    current_plan = preview_rename(Path(picked), prefix_entry.get().strip())
    result_box.delete('1.0', tk.END)
    for old, new in current_plan:
        result_box.insert(tk.END, old.name + ' → ' + new.name + '\n')

# ② 点「确认执行」时执行:先二次确认,再调用逻辑区的 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()

3.3登记的时候不要加括号

这是本章最常见的一处笔误。 command=on_run 交进去的是这段代码本身,等按钮被点了才执行。写成 command=on_run() 的话,界面刚搭好它就立刻执行了一遍,而按钮反倒变成点了没反应。两个症状同时出现,就是这一处。

打开 第12章演示_窗口沙盘.html,把"登记时多加一对括号"这个开关打开,看窗口刚出来时发生了什么,再点一下按钮。这个网页里的其他几个开关,对应的都是本章要防的错误。

把两份文件并排一看,会发现窗口版里没有任何一行在算文件该改成什么名字。它只是把使用者的动作翻译成对那两个函数的调用,再把结果翻译成屏幕上的文字。这就是"壳"这个词的意思。

讨论如果现在要再做一个网页版的壳,逻辑区需要改动吗?把"逻辑区一行不改"当成硬性要求,会给写逻辑的人带来哪些额外的约束?例如,逻辑函数里能不能直接弹一个对话框问使用者?

3.4给普通人用的三个细节

功能做完之后,还有三处只有换了使用者才会暴露的问题。

文件对话框:让不会打路径的人也能选
选择要处理的文件夹
📁 Desktop
📁 第12章
📁 测试文件夹
📁 Documents

路径由系统对话框给出,不必由人输入

什么样:点「选择文件夹」按钮,弹出的是系统自带的那个熟悉的选择框。

为什么要管:文件对话框是操作系统提供的成品部件,程序只要一行代码就能弹出它,选完拿回一个路径。好处有两个:使用者不必知道路径怎么写,程序也不会拿到一个拼错的路径。使用者点了取消时,拿回的是空值,这种情况必须单独处理,否则下一步就会报错。

界面无响应:窗口卡住不动的原因
一口气跑完 没空重画窗口白屏、点不动
每处理一个 更新一次状态行看得到进度

程序没有死,只是没有空闲去重画窗口

什么样:点了执行之后窗口整个变白,鼠标点哪里都没反应,过几秒才恢复。

为什么要管:窗口要靠程序抽空重画。一段代码如果一口气跑很久不歇手,窗口就得不到重画的机会,看上去像是死了,这叫界面无响应。最简单的缓解办法是每处理完一个就更新一次状态、让窗口重画一下,这正是回调的用处。任务真正很长时才需要另开线程,本章不涉及。

错误提示:写给使用者看,不是写给你自己看
给开发者 Traceback … FileNotFoundError定位用
给使用者 出错的文件已停下,后面的保持原样知道现在什么状态

同一个错误,两种说法,读者不同

什么样:同一个错误,在终端里是一屏英文,在窗口里是一句中文。

为什么要管:报错原文是写给开发者的定位信息;面向用户的错误提示要回答另外三个问题:出了什么事、现在是什么状态、接下来该怎么办。本章的做法是让逻辑函数照常抛出错误,由外壳接住并改写。逻辑不管怎么显示错误,这样它换个壳仍然能用。

完整演示与一条行业惯例

跑通命令行 · 加窗口 · 对照检查

从一个能跑的命令行脚本开始,到一个可以交给别人用的窗口。

4.1演示步骤

1
先确认命令行版本完全跑通
运行生成脚本建立测试文件夹,再跑一次命令行版:预演清单正确、输入 y 之后改名成功。这一步不通过就不要加窗口
产出:一个确认可用的逻辑区
2
把整份脚本贴进主提示词,划出不可改动区
起止位置写成函数名,不要写"逻辑部分"这种模糊说法
产出:一份带窗口的新脚本
3
打开新代码,对照检查那两个函数是否一字未改
把两份文件的逻辑区并排看一遍,或者用编辑器的文件比较功能。共二十九行,行数与内容都要对得上。发现被改动的,把原文贴回去,不要接受"改得更好"这个理由
产出:一次对照结果
4
运行窗口版,点「选择文件夹」
选中测试文件夹,看预演结果是否出现在文本框里,条数与命令行版是否一致。顺便试一次点取消,确认程序没有报错
产出:窗口里的预演结果
5
点「确认执行」,看确认框与状态行
确认框里应当写明将要改动多少个文件。点「是」之后,底部状态行要能看到数字在变
产出:一次完整的执行过程
6
制造一次错误,看弹窗提示
预演之后先不执行,到文件夹里手工删掉或改名其中一个文件,再点执行。应当弹出中文提示,而不是让终端刷出一屏英文
产出:一次错误提示截图
7
回头再跑一次命令行版,确认它仍然能用
加窗口不应当让原来的版本失效。两个壳都能跑,才说明逻辑真的没有被绑在界面上
产出:两个壳同时可用的验证

4.2前沿三分钟:核心与界面分开的那条线

每章固定栏目 · 一条与本章内容相关的行业动态

本章这种结构在实际项目中相当常见。版本控制工具既有命令行也有多种图形客户端,数据库既有命令行客户端也有图形管理工具,它们共用的都是同一套核心。这类做法的正式说法是核心与前端分离,也有人称之为无头(headless):核心本身不带界面,界面另做一层,甚至可以做好几层。

这样安排的好处在换代时才看得出来。界面的样式与交互习惯几年一变,核心的算法却往往能用很多年。分开之后,界面重做几轮,核心不必跟着重写;反过来,核心升级也不必等界面配合。近年常见的形式还包括把核心做成一个可供调用的服务,界面、脚本、别的程序都能接上去。

判断分离得彻不彻底,业内有个朴素的检验办法,和本章的做法完全一致:再写一个壳,看核心要不要改。一行都不用改,才算分开了。

讨论把逻辑和界面分开,写代码的总量其实变多了,短期看是多花时间。什么情况下这笔投入划得来,什么情况下不如直接写在一起?如果一个脚本只有你自己用、而且只用一次呢?

上机实验

三选一 · 六步 · 两个壳都要能跑

上机环节由此开始,当堂完成并提交。

5.1三个题目,任选其一

题目 A
给改名工具加窗口

把批量改名脚本加上界面:选文件夹、看预演、确认执行。

通用
题目 B
一键出结果

选一个文件、点一个按钮,直接在窗口里看到结论,不必看终端。

偏文科
题目 C
给汇总工具加窗口

把一个数据处理脚本加上界面:选文件夹、显示统计结果、导出。

偏理工

三个题目的共同要求是:先有一个能跑通的命令行版本,再加窗口;加完之后两个版本都要能跑。手上没有现成脚本的,可以直接用演示件里的命令行版作为起点。允许更换题材。

5.2上机六步

1
确认命令行版本跑通,并把逻辑单独圈出来
在代码里用注释划一条线,线以上是逻辑,线以下是外壳。圈不出来的,说明还没有分开,先分开再往下走
产出:分好区的命令行脚本
2
写主提示词,把不可改动区的起止位置写清楚
写函数名,不要写"逻辑部分"。窗口要包含哪些部件,逐条列出来
产出:一份完整的提示词
3
拿到新代码后,先做对照检查
逐行比对不可改动区。被改动的行原样贴回去,然后重新运行确认仍然可用
产出:对照检查的结论
4
运行窗口版,把三条交互路径各走一遍
正常执行一次、中途点取消一次、制造一次错误。三条路径都要有截图
产出:三张截图
5
回头再跑一次命令行版,确认它没有被改坏
两个壳都能跑,这一条是本章的核心验收项
产出:命令行版仍可用的截图
6
组内互测:把两份文件发给同组同学,请他只用窗口完成一次操作
不要口头指导。他卡在哪一步、哪句提示看不懂,如实记下来
产出:一行互测记录

5.3共同验收标准

逻辑区一行未改 与命令行版逐行相同 把两份文件的逻辑区并排比对,行数与内容都要一致
两个壳都能跑 加了窗口之后命令行版仍然可用 两个版本各运行一次,结果相同
路径由对话框选取 使用者不必输入任何路径 从头到尾只用鼠标完成一次操作
危险操作有二次确认 确认框里写明了数量 看确认框的文案里有没有具体数字
出错时给的是中文提示 不是英文报错原文,也不是无声无息 手工制造一次错误,看弹窗说了什么
执行期间状态行有变化 窗口不至于长时间毫无反应 执行过程中观察底部状态行的数字
只用标准库 换一台装好 Python 的机器可以直接跑 检查文件开头的 import,不应出现需要安装的库名

5.4常见错误与处理

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,重新运行
加完窗口,命令行版跑不通了
逻辑函数被改动,或者界面代码混进了逻辑区
把逻辑区恢复成原样,界面相关的语句一律移到外壳里

5.5提交内容

两份脚本
命令行版与窗口版,逻辑区相同
两个 .py 文件
窗口截图三张
预演结果、确认框、错误提示各一张
三张截图贴进作业文档
分区说明
写明第几行到第几行是逻辑、第几行起是界面
一句话加行号
互测记录
同组同学只用窗口操作时卡在哪一步
一行文字

提交方式:按教师提供的作业模板填写,文档开头注明所选题目。提交渠道见课堂通知,当堂提交。

5.6再进一步(选做)

给窗口加一个「撤销上一次」按钮:执行时把新旧名称记在一份文件里,点撤销就按记录改回去。

做的时候会碰到一个需要判断的问题:撤销这个动作本身,算逻辑还是算界面,需要你自己判断。按本章的分法,它应当写成逻辑区的第三个函数,由按钮调用;但这样一来,命令行版也应该能撤销,外壳那一段也要跟着补。功能加在哪一层,决定了它将来能被谁复用。

本章小结

本章五条结论

  1. 界面与逻辑是可以分开的两件事。逻辑写成函数,壳只负责按名字调用。检验办法是换一个壳,看逻辑要不要改。
  2. 接口是名字、参数、返回值这三项约定。改名和加参数看起来是小改,其实是在改约定。
  3. 窗口程序不从上往下跑完就结束,它搭好界面之后停下来等。点了哪个按钮,才执行对应的那一段。
  4. 使用者从自己变成别人时,要补三样东西:路径改成对话框选、危险操作加二次确认、报错改写成中文提示。
  5. 给已跑通的代码加东西时,先划出不可改动区,写明起止位置。已经跑通的代码是一份资产。
本章脉络