如何向 tkinter 框架添加阴影?

问题描述 投票:0回答:3

我正在尝试使用 tkinter 创建一个材料设计风格的 GUI,像这样(取自 LOOT mod Organizer thingy):

目前,我有一个框架作为灰色背景,另一个白色框架将保存我的内容,我的目标是向这个白色框架添加阴影,因为没有一个标准浮雕选项可以接近。这可能吗?

到目前为止,我已经尝试将白色框架放置在带有阴影图像的画布内,但我无法获得接近我想要的图像位置或尺寸。

有趣的是,

OptionMenu
小部件的下拉菜单确实有一个阴影:

虽然我不确定这是由于 tkinter 还是因为 Windows

python python-3.x tkinter
3个回答
5
投票

我终于找到了阴影的解决方案(实际上效率不是很高,但图形上很吸引人)。

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时):


1
投票

我在打包小部件时利用选项

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 上):


0
投票

我发现这是一个旧线程,但仍然相关。这是我的 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()
© www.soinside.com 2019 - 2024. All rights reserved.