我正在尝试使用 tkinter 创建一个材料设计风格的 GUI,像这样(取自 LOOT mod Organizer thingy):
目前,我有一个框架作为灰色背景,另一个白色框架将保存我的内容,我的目标是向这个白色框架添加阴影,因为没有一个标准浮雕选项可以接近。这可能吗?
到目前为止,我已经尝试将白色框架放置在带有阴影图像的画布内,但我无法获得接近我想要的图像位置或尺寸。
有趣的是,
OptionMenu
小部件的下拉菜单确实有一个阴影:
虽然我不确定这是由于 tkinter 还是因为 Windows
我终于找到了阴影的解决方案(实际上效率不是很高,但图形上很吸引人)。
import tkinter as tk
class Shadow(tk.Tk):
'''
Add shadow to a widget
This class adds a squared shadow to a widget. The size, the position, and
the color of the shadow can be customized at wills. Different shadow
behaviors can also be specified when hovering or clicking on the widget,
with binding autonomously performed when initializing the shadow. If the
widget has a 'command' function, it will be preserved when updating the
shadow appearance.
Note that enough space around the widget is required for the shadow to
correctly appear. Moreover, other widgets nearer than shadow's size will be
covered by the shadow.
'''
def __init__(self, widget, color='#212121', size=5, offset_x=0, offset_y=0,
onhover={}, onclick={}):
'''
Bind shadow to a widget.
Parameters
----------
widget : tkinter widget
Widgets to which shadow should be binded.
color : str, optional
Shadow color in hex notation. The default is '#212121'.
size : int or float, optional
Size of the shadow. If int type, it is the size of the shadow out
from the widget bounding box. If float type, it is a multiplier of
the widget bounding box (e.g. if size=2. then shadow is double in
size with respect to widget). The default is 5.
offset_x : int, optional
Offset by which shadow will be moved in the horizontal axis. If
positive, shadow moves toward right direction. The default is 0.
offset_y : int, optional
Offset by which shadow will be moved in the vertical axis. If
positive, shadow moves toward down direction. The default is 0.
onhover : dict, optional
Specify the behavior of the shadow when widget is hovered. Keys may
be: 'size', 'color', 'offset_x', 'offset_y'. If a key-value pair is
not provided, normal behavior is maintained for that key. The
default is {}.
onclick : dict, optional
Specify the behavior of the shadow when widget is clicked. Keys may
be: 'size', 'color', 'offset_x', 'offset_y'. If a key-value pair is
not provided, normal behavior is maintained for that key. The
default is {}.
Returns
-------
None.
'''
# Save parameters
self.widget = widget
self.normal_size = size
self.normal_color = color
self.normal_x = int(offset_x)
self.normal_y = int(offset_y)
self.onhover_size = onhover.get('size', size)
self.onhover_color = onhover.get('color', color)
self.onhover_x = onhover.get('offset_x', offset_x)
self.onhover_y = onhover.get('offset_y', offset_y)
self.onclick_size = onclick.get('size', size)
self.onclick_color = onclick.get('color', color)
self.onclick_x = onclick.get('offset_x', offset_x)
self.onclick_y = onclick.get('offset_y', offset_y)
# Get master and master's background
self.master = widget.master
self.to_rgb = tuple([el//257 for el in self.master.winfo_rgb(self.master.cget('bg'))])
# Start with normal view
self.__lines = []
self.__normal()
# Bind events to widget
self.widget.bind("<Enter>", self.__onhover, add='+')
self.widget.bind("<Leave>", self.__normal, add='+')
self.widget.bind("<ButtonPress-1>", self.__onclick, add='+')
self.widget.bind("<ButtonRelease-1>", self.__normal, add='+')
def __normal(self, event=None):
''' Update shadow to normal state '''
self.shadow_size = self.normal_size
self.shadow_color = self.normal_color
self.shadow_x = self.normal_x
self.shadow_y = self.normal_y
self.display()
def __onhover(self, event=None):
''' Update shadow to hovering state '''
self.shadow_size = self.onhover_size
self.shadow_color = self.onhover_color
self.shadow_x = self.onhover_x
self.shadow_y = self.onhover_y
self.display()
def __onclick(self, event=None):
''' Update shadow to clicked state '''
self.shadow_size = self.onclick_size
self.shadow_color = self.onclick_color
self.shadow_x = self.onclick_x
self.shadow_y = self.onclick_y
self.display()
def __destroy_lines(self):
''' Destroy previous shadow lines '''
for ll in self.__lines:
ll.destroy()
self.__lines = []
def display(self):
''' Destroy shadow according to selected configuration '''
def _rgb2hex(rgb):
"""
Translates an rgb tuple of int to hex color
"""
return "#%02x%02x%02x" % rgb
def _hex2rgb(h):
"""
Translates an hex color to rgb tuple of int
"""
h = h.strip('#')
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
# Destroy old lines
self.__destroy_lines()
# Get widget position and size
self.master.update_idletasks()
x0, y0, w, h = self.widget.winfo_x(), self.widget.winfo_y(), self.widget.winfo_width(), self.widget.winfo_height()
x1 = x0 + w - 1
y1 = y0 + h - 1
# Get shadow size from borders
if type(self.shadow_size) is int:
wh_shadow_size = self.shadow_size
else:
wh_shadow_size = min([int(dim * (self.shadow_size - 1)) for dim in (w,h)])
uldr_shadow_size = wh_shadow_size - self.shadow_y, wh_shadow_size - self.shadow_x, \
wh_shadow_size + self.shadow_y, wh_shadow_size + self.shadow_x
uldr_shadow_size = {k:v for k,v in zip('uldr', uldr_shadow_size)}
self.uldr_shadow_size = uldr_shadow_size
# Prepare shadow color
shadow_color = self.shadow_color
if not shadow_color.startswith('#'):
shadow_color = _rgb2hex(tuple([min(max(self.to_rgb) + 30, 255)] * 3))
self.from_rgb = _hex2rgb(shadow_color)
# Draw shadow lines
max_size = max(uldr_shadow_size.values())
diff_size = {k: max_size-ss for k,ss in uldr_shadow_size.items()}
rs = np.linspace(self.from_rgb[0], self.to_rgb[0], max_size, dtype=int)
gs = np.linspace(self.from_rgb[2], self.to_rgb[2], max_size, dtype=int)
bs = np.linspace(self.from_rgb[1], self.to_rgb[1], max_size, dtype=int)
rgbs = [_rgb2hex((r,g,b)) for r,g,b in zip(rs,gs,bs)]
for direction, size in uldr_shadow_size.items():
for ii, rgb in enumerate(rgbs):
ff = tk.Frame(self.master, bg=rgb)
self.__lines.append(ff)
if direction=='u' or direction=='d':
diff_1 = diff_size['l']
diff_2 = diff_size['r']
yy = y0-ii+1+diff_size[direction] if direction == 'u' else y1+ii-diff_size[direction]
if diff_1 <= ii < diff_size[direction]:
ff1 = tk.Frame(self.master, bg=rgb)
self.__lines.append(ff1)
ff1.configure(width=ii+1-diff_1, height=1)
ff1.place(x=x0-ii+1+diff_size['l'], y=yy)
if diff_2 <= ii < diff_size[direction]:
ff2 = tk.Frame(self.master, bg=rgb)
self.__lines.append(ff2)
ff2.configure(width=ii+1-diff_2, height=1)
ff2.place(x=x1, y=yy)
if ii >= diff_size[direction]:
ff.configure(width=x1-x0+ii*2-diff_size['l']-diff_size['r'], height=1)
ff.place(x=x0-ii+1+diff_size['l'], y=yy)
elif direction=='l' or direction=='r':
diff_1 = diff_size['u']
diff_2 = diff_size['d']
xx = x0-ii+1+diff_size[direction] if direction == 'l' else x1+ii-diff_size[direction]
if diff_1 <= ii < diff_size[direction]:
ff1 = tk.Frame(self.master, bg=rgb)
self.__lines.append(ff1)
ff1.configure(width=1, height=ii+1-diff_1)
ff1.place(x=xx, y=y0-ii+1+diff_size['u'])
if diff_2 <= ii < diff_size[direction]:
ff2 = tk.Frame(self.master, bg=rgb)
self.__lines.append(ff2)
ff2.configure(width=1, height=ii+1-diff_2)
ff2.place(x=xx, y=y1)
if ii >= diff_size[direction]:
ff.configure(width=1, height=y1-y0+ii*2-diff_size['u']-diff_size['d'])
ff.place(x=xx, y=y0-ii+1+diff_size['u'])
要使用它,只需使用应被遮蔽的小部件调用
Shadow
类即可:
import tkinter as tk
import numpy as np
# COPY HERE SHADOW CLASS
def test_click():
button1.configure(text='You have clicked me!')
if __name__ == '__main__':
root = tk.Tk()
# Create dummy buttons
button1 = tk.Button(root, text="Click me!", width=20, height=2, command=test_click)
button1.grid(row=0, column=0, padx=50, pady=20)
button2 = tk.Button(root, text="Hover me!", width=20, height=2)
button2.bind('<Enter>', lambda e: button2.configure(text='You have hovered me!'))
button2.grid(row=1, column=0, padx=50, pady=20)
# Add shadow
Shadow(button1, color='#ff0000', size=1.3, offset_x=-5, onclick={'color':'#00ff00'})
Shadow(button2, size=10, offset_x=10, offset_y=10, onhover={'size':5, 'offset_x':5, 'offset_y':5})
root.mainloop()
结果(按顺序:原始,当单击按钮1时,当悬停按钮2时):
我在打包小部件时利用选项
padx
和 pady
创建了一个解决方案。效果并不完全如您所说,但您可能想尝试其他选项组合。
import tkinter as tk
class MDLabel(tk.Frame):
def __init__(self, parent=None, **options):
tk.Frame.__init__(self, parent, bg=options["sc"]) # sc = shadow color
self.label = tk.Label(self, text=options["text"], padx=15, pady=10)
self.label.pack(expand=1, fill="both", padx=(0, options["si"]), pady=(0, options["si"])) # shadow intensity
root = tk.Tk()
root.geometry("600x300+900+200")
main_frame = tk.Frame(root, bg="white")
body_frame = tk.Frame(main_frame)
for i in range(3):
md_label = MDLabel(body_frame, sc="grey", si=1, text="Label " + str(i))
md_label.pack(expand=1, fill="both", pady=5)
body_frame.pack(expand=1, fill="both", pady=5, padx=5)
main_frame.pack(expand=True, fill="both")
root.mainloop()
这就是结果(在 Mac OS X、Sierra 上):
我发现这是一个旧线程,但仍然相关。这是我的 DropShadow 类,并提供了一个示例。它适用于“pack”和“grid”,但目前不适用于“place”。
from tkinter import *
import tkinter as tk
import sys
import os
from PIL import ImageTk, Image
class DropShadow():
def shadow(widget, offset, shadowcolor):
# if no custom shadow color is defined, then use default shadow color
if shadowcolor == None:
shadowcolor = "#414a4c"
# get the parent of the widget that will throw the shadow
rt = widget.nametowidget(widget.winfo_parent())
# get the background color of the parent widget
rtbgcolor = rt.cget('bg')
# initialize boolean variables for check on which layout manager is being used
tpack = False
tplace = False
tgrid = False
try:
# try to obtain the 'place' information from the "shadow widget"
tryplace = widget.place_info()
tplace = True
except:
pass
try:
# try to obtain the 'grid' information from the "shadow widget"
trygrid = widget.grid_info()
tgrid = True
except:
pass
try:
# try to obtain the 'pack' information from the "shadow widget"
trypack = widget.pack_info()
tpack = True
except:
pass
# update the "shadow widget" before getting height, width and positional values
widget.update()
# positional values of the "shadow widget"
xx = widget.winfo_x()
yy = widget.winfo_y()
# height and width of the "shadow widget"
w = widget.winfo_width()
h = widget.winfo_height()
# check if offset is set to be greater than the height and/or width of the
# "shadow widget" and adjust accordingly
if offset > w or offset > h:
if w < h:
offset = int(w/2)
else:
offset = int(h/2)
# hide the "shadow widget" so the shadow frames can be drawn in its exact place
if tpack:
widget.pack_forget()
elif tgrid:
widget.grid_forget()
else:
widget.place_forget()
# main shadow frame drawn in place of the shadow widget
shadBox = tk.Frame(rt, bg=shadowcolor, width=w+offset, height=h+offset)
if tpack:
shadBox.pack(trypack)
elif tgrid:
shadBox.grid(trygrid)
else:
shadBox.place(tryplace)
# frames the same color as the widget background are drawn across the top and down the left side
# at the 'offset' width to give the shadow a floating effect
topBar = tk.Frame(shadBox, bg=rtbgcolor, width=w+offset, height=offset)
topBar.place(x=0, y=0)
leftBar = tk.Frame(shadBox, bg=rtbgcolor, width=offset, height=h+offset)
leftBar.place(x=0, y=0)
# put the shadow widget back where it came from and bring it to the top of the shadow frames
widget.place(x=xx, y=yy)
widget.lift()
# Works perfect using pack and grid, but not with place YET
# togle packed between True/False in order to see the difference between "pack" and "grid"
# layout managers
packed = True
root = Tk()
root.title("Shadow Test")
root.configure(bg="black")
fr = tk.Frame(root, bg="gray")
fr.pack(side="left", padx=8, pady=8, anchor="w")
if getattr(sys, 'frozen', False):
application_path = os.path.dirname(sys.executable)
elif __file__:
application_path = os.path.dirname(__file__)
lab = tk.Label(fr, text="I am a label.\nCheck me out!\nI'm dropping a shadow!", bg="blue", fg="white")
if packed:
lab.pack(side="left", anchor="n", padx=6, pady=6)
else:
lab.grid(row = 0, column = 0, sticky = W, pady = 10, padx = 10)
#lab.place(x=2, y=2)
DropShadow.shadow(lab, 8, None) # parameters are (widget to drop shadow, shadow offset number (int), custom shadow color (use 'None' for default shadow color))
try:
# you can use this if you have an image named test.jpg in the same folder
# as this example
image1 = Image.open(application_path + "/test.jpg")
image1 = image1.resize((188, 88), Image.LANCZOS)
test = ImageTk.PhotoImage(image1)
but = tk.Button(fr, image=test, bd=0)
but.image = test
if packed:
but.pack(side="left", anchor="n", padx=8, pady=8)
else:
but.grid(row = 1, column = 0, sticky = W, pady = 10, padx = 10)
DropShadow.shadow(but, 8, None) # parameters are (widget to drop shadow, shadow offset number (int), custom shadow color (use 'None' for default shadow color))
except:
pass
but2 = tk.Button(fr, text = "I am a button with a \ncustom blue shadow.", bd=0)
if packed:
but2.pack(side="left", anchor="n", padx=8, pady=8)
else:
but2.grid(row = 0, column = 1, sticky = W, pady = 10, padx = 10)
#but2.place(x=10, y=10)
DropShadow.shadow(but2, 10, "blue") # parameters are (widget to drop shadow, shadow offset number (int), custom shadow color (use 'None' for default shadow color))
frm = tk.Frame(fr, bg="black")
if packed:
frm.pack(side="left", padx=8, pady=8, anchor="n")
else:
frm.grid(row = 1, column = 1, sticky = W, pady = 10, padx = 10)
lab2 = tk.Label(frm, text = " I am a label with a frame for a border. ", bg="green", fg="white", bd=0)
lab2.pack(side="left", anchor="n", padx=8, pady=8)
DropShadow.shadow(frm, 12, None) # parameters are (widget to drop shadow, shadow offset number (int), custom shadow color (use 'None' for default shadow color))
if __name__ == "__main__":
root.mainloop()