跳转至

tkflu.frame · FluFrame 面板

面板/容器组件。

FluFrame 是一个带圆角背景与边框的容器。它内部由"画布 + 子 Frame"组成:

  • 画布负责画圆角矩形背景;
  • 子 Frame 承载你的内容。

因此 FluFrame 把 pack / grid / place 都代理给了它内部的画布, 你可以像用普通容器一样使用它。

FluFrame

Bases: Frame, DObject, FluGradient, TracedAfter

源代码位于: tkflu\frame.py
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)

destroy()

销毁面板,连同它的画布。

这是一个容易被忽略的泄漏:FluFrame 自己挂在内部画布上, 而那张画布是父容器的直接子控件(self.canvas.master 才是 真正的父容器)。Tk 只会自动销毁"子控件",所以 FluFrame.destroy() 过去只干掉了内嵌的 Frame,画布和画在它上面的圆角背景留在原地—— 每建一次再销毁一次就多留一块面板。

源代码位于: tkflu\frame.py
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
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

FluFrameCanvas

Bases: DCanvas

面板的画布。

create_round_rectangle / create_roundrect 直接继承 :class:tkdeft.windows.canvas.DCanvas——它内部会调用 :meth:~tkdeft.windows.canvas.DCanvas.draw_roundrect, 当前引擎是栅格引擎时走进程内位图快速路径,否则回退到 :meth:~tkdeft.windows.canvas.DCanvas.draw_roundrect_svg (图元来自 :class:FluFrameDraw)。

源代码位于: tkflu\frame.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
class FluFrameCanvas(DCanvas):
    """面板的画布。

    ``create_round_rectangle`` / ``create_roundrect`` 直接继承
    :class:`tkdeft.windows.canvas.DCanvas`——它内部会调用
    :meth:`~tkdeft.windows.canvas.DCanvas.draw_roundrect`,
    当前引擎是栅格引擎时走进程内位图快速路径,否则回退到
    :meth:`~tkdeft.windows.canvas.DCanvas.draw_roundrect_svg`
    (图元来自 :class:`FluFrameDraw`)。
    """

    draw = FluFrameDraw
    frame = None

    #: 这张画布**不持有主题状态**——配色属于它内嵌的 :class:`FluFrame`。
    #: ``FluThemeManager`` 会走整棵控件树,那个 ``FluFrame`` 自己就会被收到;
    #: 若再让画布转发一次,同一套配色会被算两遍(旧实现就是这样,还附带
    #: 每个子控件一次 ``update()``)。
    _theme_proxy = True

    def theme(self, mode="light"):
        """把面板(连同它的子控件)切到 ``mode``。

        旧实现是"自己写 + 遍历子控件 + 每个子控件 ``update()``",那是
        "一个一个变色"的根源。现在整棵子树交给统一过渡:一条时间轴,
        所有组件同步插值(见 :mod:`tkflu.theme_transition`)。
        """
        from .theme_transition import applying_themes, run_theme_transition

        if applying_themes():
            # 有人(FluThemeManager)正在统一驱动:只改自己的配色,
            # 子控件由驱动方负责,别在这里另起一条时间轴。
            self.frame.theme(mode)
            return
        run_theme_transition(self, mode)

theme(mode='light')

把面板(连同它的子控件)切到 mode。

旧实现是"自己写 + 遍历子控件 + 每个子控件 update()",那是 "一个一个变色"的根源。现在整棵子树交给统一过渡:一条时间轴, 所有组件同步插值(见 :mod:tkflu.theme_transition)。

源代码位于: tkflu\frame.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def theme(self, mode="light"):
    """把面板(连同它的子控件)切到 ``mode``。

    旧实现是"自己写 + 遍历子控件 + 每个子控件 ``update()``",那是
    "一个一个变色"的根源。现在整棵子树交给统一过渡:一条时间轴,
    所有组件同步插值(见 :mod:`tkflu.theme_transition`)。
    """
    from .theme_transition import applying_themes, run_theme_transition

    if applying_themes():
        # 有人(FluThemeManager)正在统一驱动:只改自己的配色,
        # 子控件由驱动方负责,别在这里另起一条时间轴。
        self.frame.theme(mode)
        return
    run_theme_transition(self, mode)