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
选项:
- 布局内
- 在 TLabelframe 本身上
- 于 TLabelframe.Label
但是标签没有移动一个像素。如何实现?
一般来说,我对 ttk:style layout
、ttk:style element
和 ttk:style configure
中哪些标识符和选项是合法的感到非常困惑,因为文档模糊不清,散布在整个“网络”中,并且没有任何错误信息。有什么有用的提示吗?
编辑:发帖后发现的内容:
- Labelframe 标签完全是一个单独的小部件,class
TLabelframe.Label
。
- 可以覆盖其布局并在顶部添加一个分隔符,将文本向下移动。
- 但是,标签小部件 v-centered 在框架线上。如果它的高度增加,它会“向上”推动和向下推动一样多。我找不到改变对齐方式的方法 w.r.t。到实际帧。
- 可能可以将
Labelframe
完全替换为具有所需布局的自定义 Frame
subclass。但这意味着要在很多地方更改“客户端”代码。 :-/
将 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:]))
我正在尝试根据我公司的 CI 创建一个自己的 ttk 主题。我以 Sun Valley theme 作为起点并交换了图形、字体和颜色。
但是我卡在了标签框上。我试图将标签 放置在 框架内,有点像标题。 IE。顶部边缘和标签之间应该有一些边距,并且内容适当 top-padding(child 小部件)。
现在:
+-- Label ------
| ...
期望:
+---------------
| Label
| ...
我尝试设置 padding
选项:
- 布局内
- 在 TLabelframe 本身上
- 于 TLabelframe.Label
但是标签没有移动一个像素。如何实现?
一般来说,我对 ttk:style layout
、ttk:style element
和 ttk:style configure
中哪些标识符和选项是合法的感到非常困惑,因为文档模糊不清,散布在整个“网络”中,并且没有任何错误信息。有什么有用的提示吗?
编辑:发帖后发现的内容:
- Labelframe 标签完全是一个单独的小部件,class
TLabelframe.Label
。 - 可以覆盖其布局并在顶部添加一个分隔符,将文本向下移动。
- 但是,标签小部件 v-centered 在框架线上。如果它的高度增加,它会“向上”推动和向下推动一样多。我找不到改变对齐方式的方法 w.r.t。到实际帧。
- 可能可以将
Labelframe
完全替换为具有所需布局的自定义Frame
subclass。但这意味着要在很多地方更改“客户端”代码。 :-/
将 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:]))