下面介绍tkinter常用的开发技巧。

tkinter完整教程:https://blog.csdn.net/qq_48979387/article/details/125706562

1 多窗口管理

tkinter常常需要用到多个窗口的界面,这就涉及到一系列常用操作。

1.1 窗口的销毁、隐藏、显示

destroy()方法用来销毁一个窗口,即关闭某个窗口。这个方法也可以用来销毁组件。
窗口或组件被销毁后,它的Python对象仍然保留,但是它在tcl/tk解释器中已经不复存在。

withdraw()方法用来隐藏一个窗口。与最小化不同,被隐藏的窗口就像完全消失了一样,不能通过点击任务栏上的对应图标来恢复这个窗口的显示。
要显示窗口,需要采用deiconify()方法,这个方法与withdraw()功能相反。

1.2 窗口关闭事件处理

当用户关闭窗口时,可能需要询问用户是否真的关闭窗口。此时可以通过protocol方法进行处理。

from tkinter import *
from tkinter.messagebox import *

root = Tk()

def really_quit():
    if askyesno("", "是否退出?"):
        root.destroy() # 销毁窗口
    else:
        pass # 什么都不做

root.protocol("WM_DELETE_WINDOW", really_quit) # 用户按下关闭按钮时,触发really_quit

mainloop()

这样,当用户点击关闭按钮时,会进行询问,确定用户真的要退出后执行destroy()方法将窗口销毁。

1.3 窗口信息的获取

窗口/组件有一系列winfo开头的方法,允许我们获取窗口或组件的一些信息。需要注意的是,调用这些方法获取窗口/组件的尺寸和位置信息时,需要先映射组件。也就是说,如果此时没有mainloop(),必须先确保调用一次update()方法,否则返回的信息不准确。

组件的tcl/tk内部路径

每个tkinter组件对象其实都有一个tcl/tk的内部路径,用于在tcl/tk解释器中访问它们。直接将组件通过str()字符串化或者通过组件的_w属性可以访问这个组件的内部路径。

如果只需要获取这个组件的名称,而不用完整路径,也可以采用组件的winfo_name()方法。

from tkinter import *

root = Tk()
frame = Frame(root)
label = Label(frame)
print(str(root), "\n", root.winfo_name(), "\n\n",
      str(frame), "\n", frame.winfo_name(), "\n\n",
      str(label), "\n", label.winfo_name(), sep="")
'''
输出:
.
tk

.!frame
!frame

.!frame.!label
!label
'''

可以看到,tkinter根窗口的路径名为一个点:“.”,名称是"tk"。

事实上,组件的名称可以在实例化这个组件时,通过name参数设定,但是这一般不需要。这个name参数一般都是tkinter自动生成的。可以看到组件自动生成的名称前面有一个感叹号,这是tcl/tk允许的命名方式。

使用nametowidget方法可以通过组件的tcl/tk路径获得组件的Python对象。

from tkinter import *

root = Tk()
frame = Frame(root)
print(type(root.nametowidget(".!frame")))

'''
输出:
<class 'tkinter.Frame'>
'''

位置和宽高

winfo_x(), winfo_y()可以获取某个组件相对于父容器左上角的位置。这个父容器可能是窗口或Frame等。如果想要获取组件相对于整个电脑屏幕的位置,可以使用winfo_rootx()和winfo_rooty()方法。

from tkinter import *

root = Tk()
root.update() # 在没有启动mainloop前,必须刷新后才能正确显示尺寸和坐标信息

print("相对于屏幕", root.winfo_rootx(), root.winfo_rooty(),
      "容器", root.winfo_x(), root.winfo_y(),
      "宽高", root.winfo_width(), root.winfo_height())

mainloop()

'''
输出:
相对于屏幕 112 135 容器 104 104 宽高 200 200
'''

对于窗口而言,winfo_rootx/y和winfo_x/y返回的结果往往也是不一样的,因为winfo_rootx/y返回的窗口坐标不止包括窗口的内部工作区(容器)还包括窗口的标题栏、边框等装饰内容。

屏幕尺寸

winfo_screenwidth()和winfo_screenheight()用于返回电脑屏幕的宽和高。

from tkinter import *

root = Tk()
width, height = 300, 200
x = (root.winfo_screenwidth() - width) // 2 # 将窗口居中时的x坐标位置
y = (root.winfo_screenheight() - height) // 2
root.geometry("{}x{}+{}+{}".format(width, height, x, y)) # 设置窗口尺寸和位置

mainloop()

以上代码将创建一个居中显示的窗口。

子组件和父组件

winfo_children()返回一个组件的子组件的tkinter对象列表(不是子组件的tcl/tk路径)。只包括直接从属于这个组件的子组件,而不包括子组件的子组件(我的附庸的附庸不是我的附庸)。

这常用于清空某个Frame的所有子组件。

for widget in window_or_frame.winfo_children():
    widget.destroy() # 销毁组件

winfo_parent()方法返回某个组件的父组件的tcl/tk路径。

1.4 焦点管理

在计算机中,用户只能同时在一个窗口上进行操作。比如当你同时开启两个界面时,你在一个界面输入一段内容,并不会影响到另一个打开的界面。此时就称这个窗口获取了“焦点”。如果想要转变窗口的焦点,需要用鼠标点击另一个窗口,然后就能在另一个窗口上进行输入了。转变窗口的焦点这个操作叫做窗口的“激活”。

在同一个窗口上,也可能会有多个不同的组件,在某一个组件上进行操作并不会影响到其他的组件。那么这个捕获了用户的操作的组件被称作获得了“焦点”的组件。

tkinter可以进行焦点的设置,常用的方法包括focus_force()和focus_set()。这两个方法继承Misc类,这意味着所有组件和Tk/Toplevel窗口均支持这两个方法。

focus_force()用于强制使某个窗口/组件获取焦点,也就是直接激活这个窗口或者组件。

focus_set()用于设置哪个窗口或组件获取默认焦点。如果焦点在别的应用程序上,这个方法并不会直接激活该窗口或组件。但是当用户手动激活这个窗口后,使用focus_set()的组件会优先获得焦点。

from tkinter import *

root = Tk()
root.geometry("500x200")

win = Toplevel()

e1 = Entry(root)
e1.pack()
e2 = Entry(root)
e2.pack()

root.focus_force()
e2.focus_set()

mainloop()

在上面这段代码中,创建了两个窗口。如果不加root.focus_force()这一句,那么应该是win窗口获取焦点。但是调用了focus_force()方法后,root窗口获取了焦点。同时,通过focus_set()将应用程序的焦点转移到第二个Entry组件上。

用户可以通过键盘上的tab在窗口上转移焦点。

所有tkinter组件都有一个参数takefocus,表示这个组件是否能获取焦点。

1.5 设置抓取

创建多窗口时,往往会遇到这样的问题:如何限制用户只在某个特定的窗口上操作?例如需要实现一个“改变图像大小”的功能,在询问图像如何改变大小的对话框打开时,不希望用户在图像上随意操作,怎么办?tkinter“抓取”可以实现这个功能。

from tkinter import *

root = Tk()
root.title("Tk窗口")
root.geometry("300x100")
Button(root, text="Tk").pack()

win = Toplevel(root)
win.title("Toplevel窗口")
win.geometry("300x100")
Button(win, text="Toplevel").pack()

win.grab_set() # 设置抓取

mainloop()

在这里插入图片描述

设置抓取后,被抓取的窗口将置于其他tk窗口的顶部,用户被限制只能在这个窗口上进行操作。

相应地,grab_release()方法可以释放一个被抓取的窗口。不过这个方法不常调用,因为当窗口销毁的时候,窗口抓取会自动解除。

抓取的效果只能作用于一个窗口或一个组件上。当作用于一个组件上时,用户将被限制只能在这个组件和其子组件上操作。

1.6 wait_window——对话框实现关键

在开发应用时常常需要制作对话框。例如:询问用户是否要关闭窗口,要求用户输入登录信息;等等。这些输入信息,在获取之后往往需要立刻被处理。

tkinter中组件的创建都是“非阻塞式的”。也就是说,当你创建完一个询问的Toplevel,并且要求用户输入时,代码不会在获取输入的地方一直等着用户输入完成,按下“确认”键后,再继续往后执行。如果想要获取输入,正确的做法是设置一个回调函数,在用户点击“确认”后调用这个函数获取用户的输入内容。代码如下所示。

from tkinter import *

def ask_yes_no(command):
    top = Toplevel(root)
    
    Label(top, text="确认要这样?").pack(padx=100, pady=20)

    def yes():
        top.destroy() # 销毁窗口
        command(True) # 将用户的选择传递给command回调函数并调用

    def no():
        top.destroy() # 销毁窗口
        command(False) # 将用户的选择传递给command回调函数并调用

    Button(top, text="是", command=yes).pack(pady=20)
    Button(top, text="否", command=no).pack(pady=20)

def ask_question():
    def command(choice):
        if choice:
            print("用户同意了")
        else:
            print("用户拒绝了")

    ask_yes_no(command)

root = Tk()
Button(root, text="询问", command=ask_question).pack(padx=100, pady=100)

mainloop()

在这里插入图片描述

但是,这种写法很麻烦。假设有这样一个情境:你需要多次调用询问对话框来征得用户的同意,但是每次调用后续执行的功能都不相同,难道需要每次都创建一个不同的回调函数?

有读者可能联想到tkinter.messagebox。在通过messagebox弹出一个询问对话框时,并不会要求你传递一个回调函数给它,而是直接在调用完这个函数后,直接返回用户的选择。

from tkinter import *
from tkinter.messagebox import *

def ask_question():
    if askyesno("", "是否要这样?"):
        print("用户同意了")
    else:
        print("用户拒绝了")
    
root = Tk()
Button(root, text="询问", command=ask_question).pack(padx=100, pady=100)

mainloop()

为了实现像messagebox那样的对话框,有一个关键的函数就是wait_*系列的函数,包括wait_window, wait_variable, wait_visibility。下面只介绍最常用的wait_window方法。这个方法需要提供一个窗口对象作为参数,表示当这个窗口未被销毁的时候,持续阻塞代码。

from tkinter import *

def ask_yes_no():
    top = Toplevel(root)
    
    Label(top, text="确认要这样?").pack(padx=100, pady=20)
    result = BooleanVar() # 布尔值变量

    def yes():
        top.destroy()
        result.set(True)

    def no():
        top.destroy()
        result.set(False)

    Button(top, text="是", command=yes).pack(pady=20)
    Button(top, text="否", command=no).pack(pady=20)

    top.wait_window(top) # 在top窗口未被销毁时,一直阻塞
    
    return result.get() # 返回结果

def ask_question():
    if ask_yes_no():
        print("用户同意了")
    else:
        print("用户拒绝了")

root = Tk()
Button(root, text="询问", command=ask_question).pack(padx=100, pady=100)

mainloop()

当用户在决定到底选择“是”还是“否”的时候。程序会一直停留在wait_window的地方,等用户做出抉择后,窗口销毁,wait_window的阻塞被解除,代码继续运行,并通过BooleanVar返回用户的选择值。使用wait_window后,实现询问功能就简便多了。

1.7 临时窗口

在打开一款应用时,有时候会见到那种“简化”的窗口:只有右上角的“X”用于关闭窗口,而没有“最大化”和“最小化”的按钮。tkinter也可以实现这样的窗口,即“临时窗口”。

调用一个窗口的transient方法可以将一个窗口设置为临时窗口。这个方法接收一个参数,一般是一个其他的窗口对象,表示这个临时窗口将“依附”在哪个窗口上。被“依附”的窗口如果被隐藏(通过withdraw()方法)或者被销毁,临时窗口也会一并被隐藏或者被销毁。临时窗口将始终置于被“依附”的窗口之上。

from tkinter import *

root = Tk()

top = Toplevel(root)
top.transient(root) # 设置临时窗口

mainloop()

在这里插入图片描述

1.8 实例:整数询问框

下面展示一个实例,实现了一个整数询问框。可参考代码注释。

from tkinter import *
from tkinter.messagebox import *

class PromptForInteger(Toplevel):
    def __init__(self, master, title, message):
        super().__init__(master)

        '''Step1: 设置标题、抓取、临时窗口'''
        self.title(title)
        self.grab_set()
        self.transient(master)

        '''Step2: 设置关闭事件'''
        self.bind("<Return>", self.confirm) # 按下回车键
        self.protocol("WM_DELETE_WINDOW", self.confirm) # 修改用户关闭窗口时执行的事件
        
        '''Step3: 显示对话框的组件'''
        Label(self, text=message).pack(padx=50, pady=10)

        self.result = None
        self.var = IntVar()
        entry = Entry(self, textvariable=self.var)
        entry.pack(padx=50, pady=10)
        entry.focus_force() # 强行使输入框获取焦点

        Button(self, text="确认", command=self.confirm).pack(pady=10)

        '''Step4: 让对话框居中显示'''
        self.update() # 刷新,使winfo信息获取准确
        x = (self.winfo_screenwidth() - self.winfo_width()) // 2
        y = (self.winfo_screenheight() - self.winfo_height()) // 2
        self.geometry("+{}+{}".format(x, y)) # 将窗口居中展示

        '''Step5: 等待,直到对话框被销毁(通过confirm里面的destroy语句)'''
        self.wait_window(self)

    def confirm(self, event=None):
        try:
            self.result = self.var.get()
        except TclError: # 输入的不是合法整数
            showerror("错误", "必须输入整数!", parent=self)
            # 将parent设为self,可以阻止用户在messagebox未关闭时在self窗口上继续操作
        else:
            self.destroy()

root = Tk()

result = PromptForInteger(root, "询问", "请输入一个任意整数:").result
print("你输入的是:", result)

mainloop()

在这里插入图片描述

2 常用图像操作

2.1 将图片嵌入代码

在制作一款应用时,不可避免地会使用到一些外部图片。例如实现一款画图应用,就会用到“画笔”“橡皮”“颜料桶”等图标。怎么存储这些外部图片,并导入到程序中?最常见的方式是在main.py的项目文件夹中建立一个专门的文件夹,例如"assets",用于专门存放图片文件。但是,对于一些简单的图标,也可以采用图片嵌入代码的形式。下面就介绍这种嵌入方式。

tkinter.PhotoImage支持直接将图片数据传递给data参数。当然可以直接将图片数据以python变量的形式存储到代码中。不过为了调试和存储的方便,可以将外部图片的数据转换成base64编码存储到python代码中,借助base64模块进行读取。

首先,准备好相关的图片素材。将图片转换为base64编码的方式有很多,比如借助下面的代码:

import base64

def image_to_base64(file_path):
    with open(file_path, "rb") as image_file:
        encoded_string = base64.b64encode(image_file.read())
    return encoded_string.decode("utf-8")

print(image_to_base64(...))

或者借助网上的转base64工具,例如这一款工具:https://www.sojson.com/image2base64.html

import base64
from tkinter import *
from functools import lru_cache

all_images = {
    "paint":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAB2AAAAdgFOeyYIAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAKlJREFUOI3N0M+KgWEUB+CHhbFzExbT1OQSBiXNDdiysXGRsp+tpJRIpFjYULPwZ+H7Sny8n2ycOqvzPr/3dHi+mlhiivqzuIMDjlFPXsFHzF7BezTS4PYd3HxvXMIuAbceoQJqqOADRQzT4h+sL34b4TsK2YZwHourdeOQHL4eYfhNwHGXQziLamAeDPi8M1vhLxQAfberz50PG6xM9HiDMQbooof/NAEnz75MmjmtIncAAAAASUVORK5CYII=",
    "eraser":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAB2AAAAdgFOeyYIAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAOBJREFUOI2d0TtKBEEUheHP6dHegQiDyUQ+IjPdj7mRIOqAOzB1EjUSIxFk3IlgKogOYuACxDboaigKe6rwwqGoW/znPqpSFmPs4RtfhQxYwRQ/aMJ5EfJF8H0AU81Q5+CHHrjTHYb/hTsdDRK40i7qo2RG7FfRpdbOPMYh1rGTMWhieBa1NsEAl5kRbmEZj388ngaTqx74DSM4W1DhJJhcJ/l3bHTtv2TanCQmc2zHi/jMGDQ41v7QOTbTTd4UGDQ4SMEu1grGmGOrz4D2758KO4n1vBSZ1NjF6qJKSbz+AvZEfoXja4pqAAAAAElFTkSuQmCC",
    "fill":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAB2AAAAdgFOeyYIAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAOJJREFUOI2d0T0vBFEUh/FfYr0VOpV+Cp3KJ/AZ6FckEs1qRETtoyi28FKIbPYLbC0ahSjsRoOIGqNwhpth7Mye5DT35nnu/54z5afmsIMrvOEGLQzUrEU8oheyTbxjt64AVkqSdkj2m0g2kOMCs9jGR90kGYYhyHEZkq2Q/JskwyiBiz7DdCTIq5JUwUWfYNXXPH5JlsbAVf0t6U4AF92B5wnAEY5wDXcN4QcspzM4bPhyVt5AC+cN4XXcxwLAzBhJCi/gKc5P0yRVknLsteTu5a/vHOA2XjlOY0bNo49X7H0CypKOKyIAEgQAAAAASUVORK5CYII="
}

@lru_cache(len(all_images))
def get_image(name):
    '''获取相应名称的tkinter图片对象'''
    decoded_data = base64.b64decode(all_images[name]) # 应用base64解码
    return PhotoImage(data=decoded_data)

root = Tk()

Button(root, image=get_image("paint")).pack(padx=100)
Button(root, image=get_image("eraser")).pack()
Button(root, image=get_image("fill")).pack()

mainloop()

成功实现嵌入图片的显示:
在这里插入图片描述

除了解码操作之外,代码中还使用了functools.lru_cache。这个函数是用于实现“保存函数的返回值”的,相当好用。
在第一次调用由lru_cache()装饰的某个函数时,它会将这个函数的返回值记录到一个“缓存列表”中。这样,当这个函数被第二次以相同参数调用时,不会再次运行一遍函数里面的代码,而是直接从“缓存列表”中取出上一次调用的值并返回。这样以来,不仅提高了反复加载同一张图片的速度,更解决了一个重要的问题——因为图片对象一直被缓存列表保存着,所以就避免了tkinter.PhotoImage对象被Python垃圾回收机制清除,就不会造成tkinter图片的显示异常。

lru_cache接收一个参数,表示缓存列表中最多保存多少个值。因此,此处设为len(all_images),表示所有图片的数量。

2.2 设置窗口图标

设置窗口图标有两种方式,即iconbitmap方法和iconphoto方法。iconbitmap使用相对简单,只需要将图标的文件路径传递给它即可(必须是*.ico图片文件才能正常显示)。

但是,更推荐使用iconphoto方法,因为它支持更多的功能。

Wm.iconphoto(default=False, *args)

iconphoto方法的第一个参数表示是否将你所设的图片设置为“默认图标”。如果True,那么创建这个窗口的子窗口时,会自动将子窗口的图标也一并设置为这个“默认图标”。

第二个参数可以有多个,都是尺寸各不相同的tkinter.PhotoImage对象,会根据系统需求选择合适大小的图标。一般这个参数只指定一个。(Windows系统中常用32x32作为图标)

import base64
from tkinter import *

all_images = {
    "paint":"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAB2AAAAdgFOeyYIAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAKlJREFUOI3N0M+KgWEUB+CHhbFzExbT1OQSBiXNDdiysXGRsp+tpJRIpFjYULPwZ+H7Sny8n2ycOqvzPr/3dHi+mlhiivqzuIMDjlFPXsFHzF7BezTS4PYd3HxvXMIuAbceoQJqqOADRQzT4h+sL34b4TsK2YZwHourdeOQHL4eYfhNwHGXQziLamAeDPi8M1vhLxQAfberz50PG6xM9HiDMQbooof/NAEnz75MmjmtIncAAAAASUVORK5CYII=",
}

def get_image(name):
    '''获取相应名称的tkinter图片对象'''
    decoded_data = base64.b64decode(all_images[name]) # 应用base64解码
    return PhotoImage(data=decoded_data)

root = Tk()
root.iconphoto(True, get_image("paint"))

mainloop()

在这里插入图片描述

2.3 用自定义图片作为光标

tkinter中也可以用自定义的图片作为光标。在Windows中,支持的光标格式是*.cur(Windows静态光标)和*.ani(Windows动态光标)。设置方法非常简单,只要将对应的cursor参数设为"@filename"即可。

from tkinter import *

root = Tk()
root.config(cursor="@cursor.cur")

mainloop()

运行这段代码时,需要先确保同一文件夹下有一个cursor.cur文件。

对于​​Linux/macOS 系统,采用*.xbm作为光标。因为*.xbm格式限制,需要提供更多参数。

"@cursor.xbm mask.xbm black white 5 5"

这些参数分别表示:光标对象、光标掩码、光标前景色、背景色、光标热点位置。

3 其他常用操作

3.1 定时事件

注册定时事件

有时候,可能需要实现:等待3秒后执行某个功能。或者是:每过3秒钟执行某个功能。但是在tkinter中,绝对不能使用time.sleep来实现这些功能,因为这个方法会阻碍窗口的刷新,使得用户无法在上面操作,而且系统会认为这个窗口无响应,效果非常差。

after方法可以有效解决这个问题。所有窗口和组件都支持这个方法。

Misc.after(ms, func=None, *args)

这个方法需要的参数:ms,表示等待多少毫秒后执行func函数;func,表示被执行的回调函数;如果指定*args,那么这些参数会在调用时被传递给func。

after方法是非阻塞的,它向tkinter注册一个定时事件,不会影响窗口的正常刷新。

from tkinter import *

root = Tk()

def callback():
    print("3秒钟已到!")

root.after(3000, callback)

mainloop()

如果在after注册的事件中继续调用after,就相当于实现了“每过一段时间执行一次”的功能。

from tkinter import *

root = Tk()

def callback():
    print("又过了3秒...")
    root.after(3000, callback)

root.after(3000, callback)

mainloop()

因为after方法是非阻塞的,所以它不会在等待的时间未到时一直让程序卡在那里,也就不会引发递归深度错误。

注意:如果调用after时,只提供毫秒数作为参数,而将func设为默认的None的话,它将变成一个阻塞的函数,作用和time.sleep差不多。
这个需要引起警惕。假如想要实现2秒后关闭窗口的功能,如下所示:

root.after(2000, print("过了两秒钟..."))

这行代码看起来好像挺对的,但是因为它忽略了lambda,所以其实并没有真正地注册定时事件。print会被立刻执行,然后整个窗口卡死2秒钟。
上面的这个错误案例还是比较容易被察觉到的。但是,如果那个函数不是print,而是涉及到某些tkinter方法时,就很有可能出现一些奇怪的情况。

from tkinter import *

root = Tk()
root.overrideredirect(True) # 去除窗口的标题栏
root.withdraw() # 隐藏窗口

def callback():
    root.deiconify()
root.after(2000, callback())

mainloop()

例如将某个隐藏的无标题栏窗口通过deiconify()恢复显示,你会发现这个事件看起来真的注册成功了!那个隐藏了的无标题栏窗口确实是在2秒后恢复了,即使事实上并没有成功完成after的注册。但是,当时那个程序确实处于卡死的状态。
所以,不要忘记:传递的参数是函数,而不是运行后的函数返回值。正确的写法如下所示:

root.after(2000, lambda: print("过了两秒钟..."))
root.after(2000, callback)

取消定时事件

after方法会返回一个值表示注册的定时事件的id,通过这个id,我们可以用after_cancel方法取消这个注册的定时事件。

from tkinter import *

root = Tk()

def callback():
    global after_id
    after_id = root.after(1000, callback)

    print("又过了1秒...")

def stop():
    root.after_cancel(after_id) # 取消after注册的定时事件

Button(root, text="启动1秒播报", command=callback).pack(padx=100)
Button(root, text="取消1秒播报", command=stop).pack()

mainloop()

3.2 禁用与繁忙

有时候,可能需要暂时禁止用户在某一个地方操作。例如实现“下载”按钮,这一操作往往耗时,你肯定不会希望用户在已经执行下载功能的时候还在反复地点击那个按钮。

禁用

将按钮禁用往往是一个直接的方案。

from tkinter import *

root = Tk()

def download():
    b.config(state="disabled") # 禁用组件
    
    print("正在下载...")
    var = BooleanVar()
    root.after(2000, var.set, True) # 模拟下载过程
    root.wait_variable(var)
    print("下载完成!")
    
    b.config(state="normal") # 取消禁用,恢复正常

b = Button(root, text="下载", command=download)
b.pack(padx=100, pady=100)

mainloop()

对于ttk风格的组件,也可以采用下面的禁用和取消禁用方式:

b.state(["disabled"]) # 禁用
b.state(["!disabled"]) # 取消禁用

如果是禁用一个窗口,可以用attributes方法。

root.attributes("-disable", True)
root.attributes("-disable", False)

繁忙

可以使用busy_hold(**kw)方法将组件设为繁忙状态。鼠标移动到设为繁忙状态的组件上时,默认显示为watch样式,而且不能对繁忙组件进行操作。
busy_hold有一个参数cursor,表示繁忙光标样式,默认为watch。

相反地,busy_forget()用于去除组件的繁忙状态。

需要注意:busy_hold等busy系列的方法仅在Python 3.13后支持。对于旧版的Python,应该采用如下的方式调用:

root.tk.call("tk", "busy", "hold", widget) # 将widget设为繁忙
root.tk.call("tk", "busy", "forget", widget) # 取消widget的繁忙状态

3.3 剪贴板管理

tkinter提供了一些默认的剪贴板操作,用于实现复制、粘贴的功能。

复制

Misc.clipboard_append(string)

clipboard_append方法用于将一个字符串“添加”复制到剪贴板。这个“添加”指的是在上一次复制内容的末尾补充一个字符串,不同于常规的的复制。

为了实现常规的复制操作,可以先调用一次clipboard_clear()方法,将剪贴板内容清除,再调用clipboard_append()方法。

from tkinter import *

def copy_it():
    root.clipboard_clear() # 清空剪贴板
    root.clipboard_append(entry.get()) # 将entry的内容添加到剪贴板
    
root = Tk()

entry = Entry(root)
entry.pack()

Button(root, text="复制", command=copy_it).pack()

mainloop()

粘贴

clipboard_get()方法用于获取剪贴板的内容,可用于实现“粘贴”。

from tkinter import *

def paste():
    l.config(text=root.clipboard_get())
    
root = Tk()

l = Label(root, text="先复制一段内容,然后点击下面的按钮")
l.pack()

Button(root, text="粘贴", command=paste).pack()

mainloop()

4 实用扩展组件

4.1 悬浮提示框(balloon/tooltip)

制作应用时常常需要用到下面这样的悬浮提示框。这通常称作balloon或者tooltip。
在这里插入图片描述
tkinter没有直接提供Balloon这一组件,但是可以通过Toplevel来模拟。

from tkinter import *
 
class Balloon(Toplevel):
    def __init__(self, master):
        super().__init__(master)

        self.withdraw() # 默认隐藏窗口
        self.overrideredirect(True) # 隐藏标题栏等窗口装饰

        self.label = Label(self, background="#ffffd0",
                           wraplength=200, # 达到一定宽度后自动换行
                           padx=2, pady=2,
                           relief="solid", borderwidth=1)
        self.label.pack()

    def show(self, widget, text):
        '''在widget处显示text'''
        self.label.config(text=text) # 设置文字
        self.geometry("+{}+{}".format(
            widget.winfo_rootx() + widget.winfo_width(),
            widget.winfo_rooty() + widget.winfo_height()
            )) # 将Balloon的位置设置在组件的右下角
        self.deiconify() # 显示窗口

    def hide(self, event=None):
        self.withdraw() # 隐藏窗口

def bind_balloon(widget, text):
    '''为某个组件设置悬浮提示'''
    widget.bind("<Enter>", lambda event: balloon.show(widget, text)) # 鼠标进入该组件
    widget.bind("<Leave>", balloon.hide) # 鼠标离开
    
root = Tk()
root.focus_force()

balloon = Balloon(root) # 创建悬浮提示

w = Label(root, text="Beautiful is better than ugly.")
w.pack(padx=20)
bind_balloon(w, "优美胜于丑陋")

w = Button(root, text="Explicit is better than implicit.")
w.pack(pady=10)
bind_balloon(w, "明了胜于晦涩")

w = Entry(root, width=30)
w.insert(0, "Simple is better than complex.")
w.pack()
bind_balloon(w, "简洁胜于复杂")
 
mainloop()

在这里插入图片描述

这段代码实现了悬浮提示的效果。关键点在于利用窗口的overrideredirect方法隐藏标题栏,通过withdraw和deiconify隐藏和显示悬浮提示,通过geometry设置悬浮提示的位置。

如果想要让悬浮提示不是立马显示,而是用户将光标在上面悬停一段时间后显示,只需要借助3.1节介绍的after即可。

def bind_balloon(widget, text):
    '''为某个组件设置悬浮提示'''
    after_id = None

    def enter(event):
        nonlocal after_id
        after_id = widget.after(600, lambda: balloon.show(widget, text)) # 0.6s后显示
        
    def leave(event):
        widget.after_cancel(after_id)
        balloon.hide()
        
    widget.bind("<Enter>", enter) # 鼠标进入该组件
    widget.bind("<Leave>", leave) # 鼠标离开

4.2 待定

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐