跳转至

版本 0.4.0:把没做完的组件做完,把做完的过一遍

这一版有两半:

  1. 补齐——FluCheckBox / FluRadioBox / FluLiteNav 是新写的, FluListBox 从"占位实现"补成了真列表;
  2. 体检——把已有组件逐个体检了一遍,修掉 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 来。