ttk,如何将标题定位在 Labelframe 内而不是边框​​上?

ttk, How to position heading inside Labelframe instead of on the border?

我正在尝试根据我公司的 CI 创建一个自己的 ttk 主题。我以 Sun Valley theme 作为起点并交换了图形、字体和颜色。

但是我卡在了标签框上。我试图将标签 放置在 框架内,有点像标题。 IE。顶部边缘和标签之间应该有一些边距,并且内容适当 top-padding(child 小部件)。

现在:

+-- Label ------
| ...

期望:

+---------------
| Label
| ...

我尝试设置 padding 选项:

但是标签没有移动一个像素。如何实现?

一般来说,我对 ttk:style layoutttk:style elementttk:style configure 中哪些标识符和选项是合法的感到非常困惑,因为文档模糊不清,散布在整个“网络”中,并且没有任何错误信息。有什么有用的提示吗?

编辑:发帖后发现的内容:

ttk.Labelframe 文字放置在浮雕图形下方、上方或上方相对容易。此示例使用文本属性,但也可以使用 labelwidget。 为了使浮雕可见,Labelframe.Label 的背景颜色必须设置为“”。

import tkinter as tk
from tkinter import font
from tkinter import ttk

message = "Hello World"

master = tk.Tk()
style = ttk.Style(master)
style.theme_use(themename = "default")

actualFont = font.Font(
    family = "Courier New", size = 20, weight = "bold")
style.configure(
    "TLabelframe.Label", background = "", font = actualFont)

frame = ttk.LabelFrame(
    master, labelanchor = "n", text = message)
frame.grid(sticky = tk.NSEW)
frame.rowconfigure(0, weight = 1)
frame.columnconfigure(0, weight = 1)

def change_heading():
    if frame["text"][0] == "\n":
        frame["text"] = f"{message}\n"
    else:
        frame["text"] = f"\n{message}"

button = tk.Button(
    frame, text = "Change", command = change_heading)
button.grid(sticky = "nsew")

master.mainloop()

翻阅 source of the Labelframe widget,我发现:

  • 根据 -labeloutside 配置选项,标签要么放置 vertically-centered 在框架的边框上,要么在其上方齐平。 (默认 NW 锚点)
  • 即通过任何方式在文本顶部添加白色space,标签框将向上延伸与向下延伸相同的量,在框架上方创建一个“死space”。
  • 可能仍然有办法通过增加边框宽度使其“进入”内部,但我无法让它工作。

我现在使用 labeloutside 选项制作“tab-like”标题。

    # ... (define $images array much earlier) ...
    ttk::style element create Labelframe.border image $images(card2) \
        -border 6 -padding 6 -sticky nsew
    ttk::style configure TLabelframe -padding {8 8 8 8} -labeloutside 1 -labelmargins {2 2 2 0}
    ttk::style element create Label.fill image $images(header2) -height 31 -padding {8 0 16 0} -border 1

有了合适的图像,这几乎就是我的目标,只是 header 没有延伸到整个帧宽度。 Tkinter 元素使用类似“9-patch”的图像细分策略,因此您可以使用 element create-border 参数制作可拉伸的框架。

结果大概是这样的:

+-------------+
| Heading     |
+-------------+----------------+
| ...                          |

这可以通过更改布局定义来完成,以便文本元素由 Labelframe 布局保留并且 Layoutframe.Label 不再绘制文本元素。添加一点填充可确保包含的小部件使标签清晰。

示例代码:

import sys
import tkinter as tk
import tkinter.ttk as ttk


class CustomLabelframe(ttk.Labelframe):
    def __init__(self, master, **kwargs):
        """Initialize the widget with the custom style."""
        kwargs["style"] = "Custom.Labelframe"
        super(CustomLabelframe, self).__init__(master, **kwargs)

    @staticmethod
    def register(master):
        style = ttk.Style(master)
        layout = CustomLabelframe.modify_layout(style.layout("TLabelframe"), "Custom")
        style.layout('Custom.Labelframe.Label', [
            ('Custom.Label.fill', {'sticky': 'nswe'})])
        style.layout('Custom.Labelframe', [
            ('Custom.Labelframe.border', {'sticky': 'nswe', 'children': [
                ('Custom.Labelframe.text', {'side': 'top'}),
                ('Custom.Labelframe.padding', {'side': 'top', 'expand': True})
            ]})
        ])
        if (style.configure('TLabelframe')):
            style.configure("Custom.Labelframe", **style.configure("TLabelframe"))
        # Add space to the top to prevent child widgets overwriting the label.
        style.configure("Custom.Labelframe", padding=(0,12,0,0))
        style.map("Custom.Labelframe", **style.map("TLabelframe"))
        master.bind("<<ThemeChanged>>", lambda ev: CustomLabelframe.register(ev.widget))
        
    @staticmethod
    def modify_layout(layout, prefix):
        """Copy a style layout and rename the elements with a prefix."""
        result = []
        for item in layout:
            element,desc = item
            if "children" in desc:
                desc["children"] = HistoryCombobox.modify_layout(desc["children"], prefix)
            result.append((f"{prefix}.{element}",desc))
        return result


class App(ttk.Frame):
    """Test application for the custom widget."""
    def __init__(self, master, **kwargs):
        super(App, self).__init__(master, **kwargs)
        self.master.wm_geometry("640x480")
        
        frame = self.create_themesframe()
        frame.pack(side=tk.TOP, fill=tk.BOTH)
        
        for count in range(3):
            frame = CustomLabelframe(self, text=f"Frame {count}", width=160, height=80)
            frame.pack(side=tk.TOP, expand=True, fill=tk.BOTH)
            button = ttk.Button(frame, text="Test")
            button.pack(side=tk.LEFT)
        self.pack(side=tk.TOP, expand=True, fill=tk.BOTH)

    def create_themesframe(self):
        frame = ttk.Frame(self)
        label = ttk.Label(frame, text="Theme: ")
        themes = ttk.Combobox(frame, values=style.theme_names(), state="readonly")
        themes.current(themes.cget("values").index(style.theme_use()))
        themes.bind("<<ComboboxSelected>>", lambda ev: style.theme_use(ev.widget.get()))
        label.pack(side=tk.LEFT)
        themes.pack(side=tk.LEFT)
        return frame


def main(args=None):
    global root, app, style
    root = tk.Tk()
    style = ttk.Style(root)
    CustomLabelframe.register(root)
    app = App(root)
    try:
        import idlelib.pyshell
        sys.argv = [sys.argv[0], "-n"]
        root.bind("<Control-i>", lambda ev: idlelib.pyshell.main())
    except Exception as e:
        print(e)
    root.mainloop()
    return 0

if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))