跳转至

tkflu.menubar · FluMenuBar 菜单栏

菜单栏。

FluMenuBar 横向排布若干菜单项,配合 :class:~tkflu.menu.FluMenu 构成完整的菜单体系。子菜单通过弹出窗口实现,鼠标悬停即展开。

FluMenuBar

Bases: Frame, DObject, FluGradient

源代码位于: tkflu\menubar.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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
157
158
159
160
161
162
163
164
165
166
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
class FluMenuBar(Frame, DObject, FluGradient):
    def __init__(self, *args, mode="light", height=40, **kwargs):
        self._init(mode)

        #: 记下配置高度,菜单项据此算出自身高度(见 _item_height)
        self._configured_height = height

        super().__init__(*args, height=height, **kwargs)

        self._draw(None)

        self.bind("<Configure>", self._event_configure, add="+")

    def _item_height(self) -> int:
        """菜单项按钮的高度。

        历史实现沿用 ``FluButton`` 的默认高度 32,再配 ``pady=5``,
        合计需要 42px;而菜单栏默认只有 40px,底部还有一条 1.2px 的分隔线。
        结果按钮被垂直压扁,文字跟着变形。

        这里按菜单栏的实际高度,留出上下各 4px 与分隔线的余量。
        """
        total = getattr(self, "_configured_height", 40) or 40
        try:
            total = int(self.cget("height")) or total
        except Exception:
            pass
        return max(18, int(total) - 10)

    def _item_width(self, label: str, width=None) -> int:
        """菜单项的宽度。

        ``len(label) * 8`` 对中文会严重偏窄("文件" 只算出 16 像素),
        改用 :func:`~tkflu.defs.measure_label_width` 做真实度量。
        """
        if width is not None:
            return width
        from .defs import measure_label_width

        return measure_label_width(self, label)

    def _init(self, mode):

        from easydict import EasyDict

        self.attributes = EasyDict(
            {"back_color": "#f3f3f3", "border_color": "#e5e5e5", "actions": {}}
        )

        self.theme(mode=mode)

    def show(self):
        self.pack(fill="x")

    def add_command(self, custom_widget=None, width=None, label: str = "", **kwargs):
        width = self._item_width(label, width)
        if custom_widget:
            widget = custom_widget(self)
        else:
            from .button import FluButton

            widget = FluButton(self, width=width, height=self._item_height())
        if "style" in kwargs:
            style = kwargs.pop("style")
        else:
            style = "menu"
        if "command" in kwargs:
            command = kwargs.pop("command")
        else:

            def empty():
                pass

            command = empty
        if "id" in kwargs:
            id = kwargs.pop("id")
        else:
            id = widget._w
        if hasattr(widget, "dconfigure"):
            widget.dconfigure(text=label, command=command)
        else:
            if hasattr(widget, "configure"):
                widget.configure(text=label, command=command)
        if hasattr(widget, "theme"):
            widget.theme(style=style)

        widget.pack(side="left", padx=4, pady=4)
        self.dcget("actions")[id] = widget

    from .menu import FluMenu

    def add_cascade(
        self,
        custom_widget=None,
        width=None,
        label: str = "",
        menu: FluMenu = None,
        **kwargs,
    ):
        width = self._item_width(label, width)
        if custom_widget:
            widget = custom_widget(self)
        else:
            from .button import FluButton

            widget = FluButton(self, width=width, height=self._item_height())
        if "style" in kwargs:
            style = kwargs.pop("style")
        else:
            style = "menu"
        if "id" in kwargs:
            id = kwargs.pop("id")
        else:
            id = widget._w

        def command():
            menu.focus_set()
            menu.popup(
                widget.winfo_rootx() - 5, widget.winfo_rooty() + widget.winfo_height()
            )
            # 弹窗尺寸按内容算,不再写死 100px 宽(中文菜单项会被截断)
            menu_width, menu_height = menu.preferred_size()
            menu.window.geometry(f"{menu_width}x{menu_height}")
            menu.window.deiconify()
            menu.window.attributes("-topmost")

        if hasattr(widget, "dconfigure"):
            widget.dconfigure(text=label, command=command)
        else:
            if hasattr(widget, "configure"):
                widget.configure(text=label, command=command)
        if hasattr(widget, "theme"):
            widget.theme(style=style)

        widget.pack(side="left", padx=4, pady=4)
        self.dcget("actions")[id] = widget

    def action(self, id):
        return self.dcget("actions")[id]

    def update_children(self):
        """把菜单项按钮重新换一次肤(历史 API)。

        保留名字是为了兼容老代码;内部走统一过渡,因此不再逐个 ``update()``。
        """
        for widget in self.dcget("actions").values():
            if hasattr(widget, "theme"):
                widget.theme(mode=self.mode)

    def theme(self, mode="light"):
        """菜单栏(连同菜单项按钮)切到 ``mode``。

        旧实现是"自己换 + 遍历按钮 + 每个按钮 ``update()``":那会让每个按钮
        各排各的过渡帧,还会在遍历途中把别人的帧提前执行掉,于是菜单项
        一个一个变色。现在整棵子树交给统一过渡:一条时间轴,同步插值。
        """
        from .theme_transition import applying_themes, run_theme_transition

        if applying_themes():
            # 有人(FluThemeManager)正在统一驱动:只改自己的配色,
            # 菜单项按钮由驱动方负责,别在这里另起一条时间轴。
            self.theme_myself(mode=mode)
            return
        run_theme_transition(self, mode)

    def theme_myself(
        self, mode="light", animation_steps: int = None, animation_step_time: int = None
    ):
        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()
        from .designs.menubar import menubar

        m = menubar(mode)
        self.mode = mode
        if hasattr(self, "tk"):
            if not animation_steps == 0 or not animation_step_time == 0:
                if mode.lower() == "dark":
                    back_colors = self.generate_hex2hex(
                        self.attributes.back_color,
                        m["back_color"],
                        steps=animation_steps,
                    )
                    border_colors = self.generate_hex2hex(
                        self.attributes.border_color,
                        m["border_color"],
                        steps=animation_steps,
                    )
                else:
                    back_colors = self.generate_hex2hex(
                        self.attributes.back_color,
                        m["back_color"],
                        steps=animation_steps,
                    )
                    border_colors = self.generate_hex2hex(
                        self.attributes.border_color,
                        m["border_color"],
                        steps=animation_steps,
                    )
                for i in range(animation_steps):

                    def update(ii=i):  # 使用默认参数立即捕获i的值
                        self.dconfigure(
                            back_color=back_colors[ii], border_color=border_colors[ii]
                        )
                        self._draw()

                    self.after(
                        i * animation_step_time, update
                    )  # 直接传递函数,不需要lambda

                self.after(
                    animation_steps * animation_step_time + 50,
                    lambda: self.update_children(),
                )
            else:
                self.dconfigure(back_color=m["back_color"])
                self.dconfigure(border_color=m["border_color"])
                self._draw()

    def _draw(self, event=None):
        self.config(background=self.attributes.back_color)
        if not hasattr(self, "border"):
            self.border = Frame(
                self, height=1.2, background=self.attributes.border_color
            )
        else:
            self.border.configure(background=self.attributes.border_color)
        self.border.pack(fill="x", expand="yes", side="bottom")

    def _event_configure(self, event=None):
        self._draw(event)

theme(mode='light')

菜单栏(连同菜单项按钮)切到 mode。

旧实现是"自己换 + 遍历按钮 + 每个按钮 update()":那会让每个按钮 各排各的过渡帧,还会在遍历途中把别人的帧提前执行掉,于是菜单项 一个一个变色。现在整棵子树交给统一过渡:一条时间轴,同步插值。

源代码位于: tkflu\menubar.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def theme(self, mode="light"):
    """菜单栏(连同菜单项按钮)切到 ``mode``。

    旧实现是"自己换 + 遍历按钮 + 每个按钮 ``update()``":那会让每个按钮
    各排各的过渡帧,还会在遍历途中把别人的帧提前执行掉,于是菜单项
    一个一个变色。现在整棵子树交给统一过渡:一条时间轴,同步插值。
    """
    from .theme_transition import applying_themes, run_theme_transition

    if applying_themes():
        # 有人(FluThemeManager)正在统一驱动:只改自己的配色,
        # 菜单项按钮由驱动方负责,别在这里另起一条时间轴。
        self.theme_myself(mode=mode)
        return
    run_theme_transition(self, mode)

update_children()

把菜单项按钮重新换一次肤(历史 API)。

保留名字是为了兼容老代码;内部走统一过渡,因此不再逐个 update()。

源代码位于: tkflu\menubar.py
153
154
155
156
157
158
159
160
def update_children(self):
    """把菜单项按钮重新换一次肤(历史 API)。

    保留名字是为了兼容老代码;内部走统一过渡,因此不再逐个 ``update()``。
    """
    for widget in self.dcget("actions").values():
        if hasattr(widget, "theme"):
            widget.theme(mode=self.mode)