版本 0.4.0:把没做完的组件做完,把做完的过一遍
这一版有两半:
- 补齐——
FluCheckBox/FluRadioBox/FluLiteNav是新写的,FluListBox从"占位实现"补成了真列表; - 体检——把已有组件逐个体检了一遍,修掉 20 多条真缺陷 (其中有几条会让整个换肤崩掉、让图片显示不出来、让窗口销毁后残留)。
除此之外还加了 121 个 pytest 用例,ruff 在两个仓库里都清零。
新增
FluCheckBox(多选框)
20×20 的圆角方框 + 标签。勾选状态与 Windows 一致,允许三种取值:
import tkflu
box = tkflu.FluCheckBox(root, text="启用动画", checked=True)
three = tkflu.FluCheckBox(root, text="不确定态", three_state=True, checked=None)
box.dconfigure(command=lambda: print(box.dcget("checked")))
True 画勾、False 空框、None 画横杠(不确定态)。
构造时传 three_state=True 就会在 False → True → None → False 之间循环。
Tab 聚焦、空格 切换,聚焦时方框外会画一圈焦点环。
FluRadioBox(单选框)
圆环 + 实心圆点,同组互斥。两种分组写法:
import tkinter as tk
# 1) 共享变量(与 ttk.Radiobutton 一致)
plan = tk.StringVar(master=root, value="b")
tkflu.FluRadioBox(root, text="甲", variable=plan, value="a")
tkflu.FluRadioBox(root, text="乙", variable=plan, value="b")
# 2) 组名:窗口内同名的一组自动互斥,窗口销毁时自动回收
tkflu.FluRadioBox(root, text="甲", group="plan", value="a")
tkflu.FluRadioBox(root, text="乙", group="plan", value="b")
比较取值用的是 str(变量) == str(value)——这是 Tk 一直以来的语义,
也顺手避开了"IntVar 配字符串 value,看起来选了却没高亮"那个坑。
换选另一项时只有新选中的那个会回调,旧项不会跟着响一次。
FluListBox(真列表)
0.3.0 之前它只是个"长得像按钮的圆角矩形"。现在:
- 条目数据:
insert/append/delete_item/clear/get/index; - 虚拟滚动:只画可见的那几行,条目多也不卡;
- 三种选择模式:
single/multiple/extended(Ctrl切换、Shift连选); - 键盘:
↑↓/PageUp/PageDown/Home/End/Enter; - 滚轮:Windows 的
<MouseWheel>与 X11 的Button-4/5都接上了; - 自绘滚动条:浮在右侧,可以拖;内容不多时不出现;
- 过长的条目会截断成
abc…(Tk 画布没有裁剪概念,不截断就会糊到控件外面)。
选中的那一行左侧有一条强调色指示条——这是 Windows NavigationView 的记号。
box = tkflu.FluListBox(
root, width=300, height=180,
items=[f"第 {i} 行" for i in range(1, 101)],
selectmode="extended",
command=lambda index, item: print("激活", index, item),
on_select=lambda indices, items: print("选中", indices),
)
条目删除叫 delete_item
控件本身是 Canvas,delete 是"删除画布元素"(重绘第一步就是
self.delete("all"))。两者同名会递归调用自己。
老写法 FluListBox(parent, text="标题", width=120) 保持原样:
列表为空时 text 就是空状态提示,高度仍是 32px。
FluLiteNav(轻量导航栏)
一排可点条目 + 一个选中项,对应 NavigationView 里最常用的那部分。
竖排时指示条在左,横排时在下方。
nav = tkflu.FluLiteNav(
root,
items=[
("🏠", "首页"),
("🔍", "搜索"),
{"label": "设置", "icon": "⚙", "key": "settings"},
],
orient="horizontal", style="card", selected="settings",
)
nav.dconfigure(command=lambda index, item: print("切到", item["key"]))
条目可以简写("标签" / ("图标", "标签"))也可以写成字典
(label / icon / key / enabled / command)。
禁用的条目在键盘导航里会被跳过。
修复
会让整趟换肤崩掉的那种
FluButton.theme()查表后无条件调用结果:style写错一个字母就TypeError: 'NoneType' object is not callable,而且那个错误值会被存进self.style—— 这个按钮从此再也切不了主题。更糟的是FluThemeManager.mode()会崩在这里,后面的兄弟组件全都轮不到换肤。FluListBox.theme()用参数而不是self.style判断分支(0.3.0 已修,这里补上回归)。
看不见但很要命的那种
FluImage的图片被内嵌 Frame 盖住:FluFrame的内嵌 Frame 是真实的 控件窗口,而嵌入窗口永远画在画布元素之上,跟创建顺序无关。所以 "先画背景、再叠图片"根本没生效——整张图被盖住,只露出边框内侧一圈。 现在有图片时把那个窗口元素隐藏掉。FluSlider(orient="vertical")构造即崩:非横向时轨道/把手根本没创建, 紧接着tag_bind抛AttributeError。纵向方向现在真的实现了。FluScrollBar.set()是坏的:Tk 的yscrollcommand传进来的是字符串, 而代码直接拿它做乘法;就算转成数字,coords(item, x1,y1,x2,y2)对图片 元素也不合法(只接受 0 或 2 个坐标)。顺带一提,横向滚动条在静止时 整个是空的(未展开分支只画竖直方向)。FluFrame.destroy()把面板留在屏幕上:画布是父容器的直接子控件, 只销毁内嵌 Frame 等于没销毁。建一次销毁一次就多留一块。_after的清理是"整个解释器"级的:销毁一个子窗口会把主窗口排的after一起取消,还会在别的控件里留下已删命令的名字,于是下次关窗口报TclError: can't delete Tcl command,_default_root残留,之后建窗口直接 "application has been destroyed"。现在改成按归属回收。- 图标
PhotoImage的进程级缓存钉住解释器:反复开关窗口 50 次内存涨 ~96 MB (清掉缓存只涨 ~12 MB),而且id(tk)会被复用,还能拿到属于已死解释器的图片。 FluSlider(min=max)抛ZeroDivisionError(一段死代码里的除法先执行)。FluPopupWindow.popup():只设了动画帧间隔(帧数为 0)时, 淡入的除法会炸——表现为菜单打不开、提示气泡一悬停就报错。
看得见的那种
FluButton的过渡动画最后一帧就是"最终状态",而且那一次权威重绘被注释掉了:steps=1时按钮会停在旧主题上,直到下一次无关事件才刷新。 顺带把写死的border_width=1 / radius=6换成目标状态的值—— 浅色强调色的禁用态线宽本来是 0,写死 1 会留一圈多余的白边。FluToggleButton打开时的描边是从背景色插值的(复制粘贴漏改), 勾选过程中边框会从浅灰一路闪成强调蓝。- 滑块
value超出[min, max]时进度条会画到控件外面几百像素。 - 拖动滑块时把手没有按下态:配色分支写成
if event:,而拖动最后那次_draw()不带事件 —— 于是拖动永远是静止外观。 - 鼠标划过禁用的滚动条直接抛
TypeError(禁用配色表里没有track_color,None被当成 SVG 的fill)。 - 第二个窗口里的文字全变成很大的宋体:字体对象是按 Tk 解释器注册的, 不传 master 就挂在第一个解释器上,第二个解释器认不出这个名字。
FluLabel(..., font=...)传进去的字体被整个丢掉,attributes.font一直是None。FluMenu(tl)的第一个位置参数是height,于是 toplevel 被当成了高度, 弹出窗口永远挂在默认根窗口上(属主关了它还留着)。- 点掉子菜单里的一项,父菜单不会跟着收起。
FluFrame放进ttk.Frame/ttk.Notebook直接报TclError: unknown option "-background"(ttk 没有这个选项)。FluEntry的<Button-1>绑定漏了add="+",把基类的处理覆盖掉了, 画布上的button1永远是False。- 菜单宽度估算写的
Font(master=...)参数名是错的(应为root=),TclError又被except吞掉,于是永远走"宽字符按 2 计 × 9"的估算—— 菜单比实际需要宽 1.6~2 倍。现在用真实字体度量。 - 画廊里的"切换可用状态"按钮没有反应:
dconfigure()只写属性不重绘, 要鼠标划过才刷新。
顺手清掉的
FluFrame的 5 个几何代理方法是坏的:pack_propagate把flag丢了 (变成"读取"而不是"设置")、grid_location丢参数、grid_anchor传了个Ellipsis对象、place_info返回的是grid_info。FluFrame的过渡动画是死代码:hasattr(某个 dict, "back_color")恒为False,那段插值从来没跑过。FluSlider/FluEntry/FluText/FluScrollBar的theme()在mode为空时会抛AttributeError(有的是self.mode还不存在, 有的是把None直接传给设计函数)。FluSlider现在有set()/get(),支持方向键与Home/End, 并且接受command=(以前FluSlider(command=cb)会把-command透给Canvas然后抛TclError)。FluText不再把像素值写进Text的width/height(那两个选项的单位是字符与行数,实测会算出 1334×1232 的请求尺寸)。- 动画帧不再堆积:每轮动画开始前先撤销上一轮没跑完的帧。
tkdeft侧:DCanvas._prune_photo_refs永远清不掉失效引用 (删除的 item 让type()返回None而不是抛异常),于是每次重绘都往_photo_refs里多存一张图;现在能正确回收。
测试
新增 tests/(pytest,121 个用例):
| 文件 | 覆盖 |
|---|---|
test_new_widgets.py |
四个新组件的 API 与交互(多选/三态/分组/选择模式/键盘/滚动截断/命中测试) |
test_regressions.py |
上面每一条修复各一到两个断言——改回去就会红 |
test_engines.py |
五个渲染引擎 ×(全部组件构建 + 换肤 + 画廊自检 + 画廊结构) |
cd tkfluent
python -m pytest tests/ # 121 passed
python -m ruff check tkflu tests # All checks passed!
两个测试环境的坑(都写进了 conftest)
- 几何断言必须在窗口已映射时做:
withdraw()状态下子控件的winfo_width()一律是 1,命中测试、可见行数、裁剪全都量不出来。 - 别在整套测试里反复建新的 Tcl 解释器:一个
FluWindow一个解释器, 开上百个之后 Tk 会开始报Can't find a usable tk.tcl ... couldn't read file .../ttk/scale.tcl——文件其实好好地在那儿,是资源被耗干了。 所以测试共用一个解释器,每个用例开一个新的FluToplevel。
升级提示
FluListBox从占位变成真列表,行为有变化(单击=选中,双击/回车=激活)。 只传text=的老写法外观不变。细节见 常见问题。FluButton.theme()现在对非法mode/style退回默认值而不是崩掉。- 组件内部不再有"每次重绘都排一个
after且从不取消"的写法; 自己扩展组件时,请照着tkflu._after.TracedAfter来。