167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466 | class FluFrame(Frame, DObject, FluGradient, TracedAfter):
def __init__(
self,
master=None,
*args,
width=300,
height=150,
mode="light",
style="standard",
**kwargs,
):
self.canvas = FluFrameCanvas(
master, *args, width=width, height=height, **kwargs
)
self.canvas.frame = self
# 旧实现在这里 mkstemp() 了一个 .svg 临时文件,既泄漏 fd 又从不清理。
# 现在直接复用画布绘制对象自带的 scratch 文件(控件销毁时统一回收)。
self.temppath = self.canvas.svgdraw.scratch_path(".svg")
super().__init__(master=self.canvas)
self._init(mode, style)
self.enter = False
self.button1 = False
self._draw(None)
self.canvas.bind("<Configure>", self._event_configure, add="+")
def _init(self, mode, style):
from easydict import EasyDict
self.attributes = EasyDict(
{
"back_color": None,
# "back_opacity": None,
"border_color": None,
"border_color_opacity": None,
"border_width": None,
"radius": None,
}
)
self.theme(mode=mode, style=style)
def theme(self, mode=None, style=None):
if mode:
self.mode = mode
if style:
self.style = style
if self.mode.lower() == "dark":
if self.style.lower() == "popupmenu":
self._dark_popupmenu()
else:
self._dark()
else:
if self.style.lower() == "popupmenu":
self._light_popupmenu()
else:
self._light()
def _theme(
self, mode, style, animation_steps: int = None, animation_step_time: int = None
):
n = frame(mode, style)
if animation_steps is None:
from .designs.animation import get_animation_steps
animation_steps = get_animation_steps()
if animation_step_time is None:
from .designs.animation import get_animation_step_time
animation_step_time = get_animation_step_time()
# 只在"确实有一个旧颜色可以过渡"时才做动画。
#
# 旧实现写的是 hasattr(self.attributes, "back_color") and
# hasattr(n, "back_color")——`n` 是普通的 dict,`hasattr(dict, ...)`
# 永远是 False,于是这段过渡动画**从来没有跑过**(静默的死代码)。
# 顺带一提:首次构造时 self.attributes.back_color 还是 None,
# 直接拿去插值会抛 AttributeError,所以这里也要挡住。
previous = self.attributes.back_color
if (
previous
and n.get("back_color")
and (animation_steps or animation_step_time)
):
steps = max(1, int(animation_steps))
back_colors = self.generate_hex2hex(previous, n["back_color"], steps=steps)
for i in range(steps):
def update(ii=i): # 使用默认参数立即捕获i的值
self._draw(tempcolor=back_colors[ii])
self.after_traced(i * animation_step_time, update)
self.after_traced(
steps * animation_step_time + 10, lambda: self._draw()
)
self.dconfigure(
back_color=n["back_color"],
border_color=n["border_color"],
border_color_opacity=n["border_color_opacity"],
border_width=n["border_width"],
radius=n["radius"],
)
self._draw()
self.update_idletasks()
self.canvas.update_idletasks()
def _light(self):
self._theme("light", "standard")
def _light_popupmenu(self):
self._theme("light", "popupmenu")
def _dark(self):
self._theme("dark", "standard")
def _dark_popupmenu(self):
self._theme("dark", "popupmenu")
# ------------------------------------------------------------------
# 几何管理代理
# ------------------------------------------------------------------
# FluFrame 自己挂在内部画布上,真正参与父容器布局的是那张画布。
# 所以下面这些方法都要转给画布——早先有几处转错了:pack_propagate
# 把 flag 丢了(变成了"读取"而不是"设置")、grid_location 丢参数、
# grid_anchor 传了个 Ellipsis 对象、place_info 返回的是 grid_info。
def _parent_background(self):
"""父容器的背景色(拿不到时给一个安全的兜底)。
``FluFrame`` 会把画布背景同步成父容器的背景,这样圆角外面
不会露出突兀的一块灰。但 **ttk 控件没有 ``-background`` 选项**:
``ttk.Frame`` / ``ttk.Notebook`` / ``ttk.LabelFrame`` 里放一个
``FluFrame`` 时,旧写法 ``self.canvas.master.cget("background")``
会直接抛 ``TclError: unknown option "-background"``,控件根本建不出来。
"""
parent = self.canvas.master
for option in ("background", "bg"):
try:
return parent.cget(option)
except Exception:
continue
# ttk:向当前主题要它自己的底色
try:
from tkinter import ttk
style = ttk.Style(parent)
return style.lookup(parent.winfo_class(), "background") or "#ffffff"
except Exception:
return "#ffffff"
def pack_info(self):
return self.canvas.pack_info()
def pack_forget(self):
return self.canvas.pack_forget()
def pack_slaves(self):
return self.canvas.pack_slaves()
def pack_propagate(self, flag=None):
return self.canvas.pack_propagate(flag)
def pack_configure(self, **kwargs):
return self.canvas.pack_configure(**kwargs)
pack = pack_configure
def grid_info(self):
return self.canvas.grid_info()
def grid_forget(self):
return self.canvas.grid_forget()
def grid_size(self):
return self.canvas.grid_size()
def grid_remove(self):
return self.canvas.grid_remove()
def grid_anchor(self, anchor=None):
if anchor is None:
return self.canvas.grid_anchor()
return self.canvas.grid_anchor(anchor)
def grid_slaves(self, row=None, column=None):
kwargs = {}
if row is not None:
kwargs["row"] = row
if column is not None:
kwargs["column"] = column
return self.canvas.grid_slaves(**kwargs)
def grid_propagate(self, flag=None):
return self.canvas.grid_propagate(flag)
def grid_location(self, x, y):
return self.canvas.grid_location(x, y)
def grid_bbox(self, **kwargs):
return self.canvas.grid_bbox(**kwargs)
def grid_configure(self, **kwargs):
return self.canvas.grid_configure(**kwargs)
grid = grid_configure
def grid_rowconfigure(self, **kwargs):
return self.canvas.grid_rowconfigure(**kwargs)
def grid_columnconfigure(self, **kwargs):
return self.canvas.grid_columnconfigure(**kwargs)
def place_info(self):
return self.canvas.place_info()
def place_forget(self):
return self.canvas.place_forget()
def place_slaves(self):
return self.canvas.place_slaves()
def place_configure(self, **kwargs):
return self.canvas.place_configure(**kwargs)
place = place_configure
def destroy(self):
"""销毁面板,**连同它的画布**。
这是一个容易被忽略的泄漏:``FluFrame`` 自己挂在内部画布上,
而那张画布是**父容器的直接子控件**(``self.canvas.master`` 才是
真正的父容器)。Tk 只会自动销毁"子控件",所以 ``FluFrame.destroy()``
过去只干掉了内嵌的 Frame,画布和画在它上面的圆角背景**留在原地**——
每建一次再销毁一次就多留一块面板。
"""
canvas = getattr(self, "canvas", None)
try:
self.cancel_traced()
super().destroy()
finally:
if canvas is not None:
try:
canvas.destroy()
except Exception:
pass
def _draw(self, event=None, tempcolor: dict = None):
self.canvas.delete("all")
self.canvas.config(background=self._parent_background())
if not tempcolor:
_back_color = self.attributes.back_color
else:
_back_color = tempcolor
# _back_opacity = self.attributes.back_opacity
_border_color = self.attributes.border_color
_border_color_opacity = self.attributes.border_color_opacity
_border_width = self.attributes.border_width
_radius = self.attributes.radius
self.element1 = self.canvas.create_round_rectangle(
0,
0,
self.canvas.winfo_width(),
self.canvas.winfo_height(),
_radius,
temppath=self.temppath,
fill=_back_color, # fill_opacity=_back_opacity,
outline=_border_color,
outline_opacity=_border_color_opacity,
width=_border_width,
)
self.config(background=_back_color)
self.element2 = self.canvas.create_window(
self.canvas.winfo_width() / 2,
self.canvas.winfo_height() / 2,
window=self,
width=self.canvas.winfo_width() - _border_width * 2 - _radius,
height=self.canvas.winfo_height() - _border_width * 2 - _radius,
)
# 只做 idle 级刷新(布局 + 重绘),刻意不使用 update():
# update() 会处理**全部**待处理事件,其中包括 <Configure>,
# 于是可能在 _draw 内部再次触发 _draw,形成重入。
# 另外这里也刻意不再挂 after(100, ...) 延迟回调——它对最终外观没有
# 额外贡献(背景色已在上面同步设置,且任何尺寸变化都会经 <Configure>
# 重新走一遍 _draw),却会带来两个真实问题:
# 1. 连续 resize 时回调不断堆积,每个都强制刷新一次布局 → 卡顿;
# 2. 窗口关闭后残留回调仍会触发,控制台刷
# "invalid command name ...<lambda>"。
self.update_idletasks()
def _event_configure(self, event=None):
self._draw(event)
|