Tkinter:实用至上主义的经典之作

Posted 天元浪子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tkinter:实用至上主义的经典之作相关的知识,希望对你有一定的参考价值。

文章目录


1. 概述

1.1 Tkinter是什么?

Tkinter是Python自带的GUI库,Python的IDEL就是Tkinter的应用实例。Tkinter可以看作是Tk和inter的合体。词根inter之意不言自明,而Tk则是工具控制语言Tcl(Tool Command Language)的一个图形工具箱的扩展,它提供各种标准的GUI接口。

和其他GUI库相比,Tkinter有一个与生俱来的优势:无需安装就可以直接使用。当然,也有很多人——曾经我也是其中之一,认为这恰是Tkinter的唯一优点。不过,后来我改变了看法。相较于wx或Qt多如牛毛的控件和组件,Tk只用十几个控件就可以满足几乎所有的应用需求,用最低的学习成本、最简单的方式解决问题,这不正是实用至上主义的典范吗?

从实用主义的角度看,Qt的博大精深就是尾大不掉,Wx的精致严谨就是循规蹈矩。如果你正在寻找一款用于桌面程序设计的GUI库,并且只打算花一个小时学会使用它,那么就请选择Tkinter吧。这款以学习曲线平缓和易于嵌入为特定目标而设计的GUI库,也许正是你苦苦追寻的真爱。

1.2 Tkinter的组织架构

Tkinter模块提供了一个名为Tk的窗体类、十几个基本控件,多个类型对象,若干常量,以及一个可选主题的控件包ttk和各种对话框组件。可以把ttk理解为增强的控件包,它提供了更多、更美观的控件。Tkinter模块的组织架构如下图所示。

对于简单的应用需求,只需要像下面这样导入模块就可以了。

from tkinter import *

由于Tkinter模块在其__init__.py脚本中将可选主题的控件包ttk和各种对话框组件从__all__里面排除了,上面的模块导入方式只导入了Tk类、基本控件、类型对象和常量。如果应用程序需要打开文件、保存文件等对话操作,或者需要更多更个性化的控件,就需要像下面这样导入模块了。

from tkinter import *
from tkinter import ttk, filedialog, messagebox

2. 快速体验

2.1 GUI设计的一般流程

用Tkinter写一个桌面应用程序,只需要三步:

  1. 创建一个窗体
  2. 把需要的控件放到窗体上,并告诉它们当有预期的事件发生时就执行预设的动作
  3. 启动循环监听事件

无论这个程序有多么简单或多么复杂,第1步和第3步是固定不变的,设计者只需要专注于第2步的实现。下面这段代码实现了一个最简单的Hello World桌面程序。

from tkinter import *

root = Tk() # 1. 创建一个窗体
Label(root, text='Hello World').pack() # 2. 添加Label控件
root.mainloop() # 3. 启动循环监听事件

不同于wx用frame表示窗体,我习惯用root作为窗体的名字。当然,你也可以用window或其他你喜欢的名字,但不要用frame,因为Tkinter为frame赋予了其他的含义。

代码运行界面如上图所示。弹出来的程序窗口既小且丑,就像一个新生的婴儿,但这的确是一个完整的桌面应用程序。

2.2 控件布局

所谓控件布局,就是设置控件在窗体内的位置以及填充、间隔等属性。在Hello world程序中,我使用了pack方法来设置控件Label的布局,并把它们写成了链式调用的形式。如果将控件的创建和布局分写成两行的话,代码的可读性会更好一点。

pack方法是Tinkter最常用的布局手段,功能强大,参数众多,这里只介绍pack的几个主要参数。下表中用到了Tkinter定义的常量,比如,TOP就是tkinter.TOP,等价于字符串’top’,YES就是tkinter.YES,等价于字符串’yes’。

参数说明
side布局方向,可选项:TOP、BOTTOM、 LEFT、RIGHT,缺省默认TOP
anchor对齐方式,可选项:E、W、N 、S、NE、NW、SE、SW、CENTER,缺省默认CNETER
expand是否占用剩余可用空间作为控件的可用空间,可选项:NO、YES,缺省默认NO
fill控件在指定方向上扩展至填满自己的可用空间,可选项:X、Y、BOTH、NONE,缺省默认NONE
padx水平方向上控件与可用空间的留空距离,以像素表示,缺省默认0
pady垂直方向上控件与可用空间的留空距离,以像素表示,缺省默认0

下面的代码创建了标签和按钮两个控件,使用pack方法使其上下排列,同时还演示了窗口标题、窗口图标和窗口大小的设置方式。代码中用到了.ico格式的图标文件,想要运行这段代码的话,请先替换成本地文件。

from tkinter import *

root = Tk()
root.title('最简单的桌面应用程序') # 设置窗口标题
root.geometry('480x200') # 设置窗口大小
root.iconbitmap('res/Tk.ico') # 设置窗口图标

label = Label(root, text='Hello World', font=("Arial Bold", 50))
label.pack(side='top', expand='yes', fill='both') # 使用全部可用空间,水平和垂直两个方向填充
btn = Button(root, text='关闭窗口', bg='#C0C0C0') # 按钮背景深灰色
btn.pack(side='top', fill='x', padx=5, pady=5) # 水平方向填充,水平垂直两个方向留白5个像素

root.mainloop()

代码运行界面如下图所示,看上去比第一个Hello World程序要顺眼得多。在这个界面上,虽然按钮的名字叫做“关闭窗口”,但是目前还不能对点击操作做出任何反应。

控件布局除了pack方法外,还有place方法和grid方法,后面会有详细的说明。

2.3 事件驱动

一个桌面程序不单是控件的罗列,更重要的是对外部的刺激——包括用户的操作做出反应。如果把窗体和控件比作是桌面程序的躯体,那么响应外部刺激就是它的灵魂。Tkinter的灵魂是事件驱动机制:当某事件发生时,程序就会自动执行预先设定的动作。

事件驱动机制有三个要素:事件、事件函数和事件绑定。比如,当一个按钮被点击时,就会触发按钮点击事件,该事件如果绑定了事件函数,事件函数就会被调用。下面的代码演示了如何将按钮点击事件和对应的事件函数绑定在一起。

from tkinter import *

def click_button():
    """点击按钮的事件函数"""
    
    root.destroy() # 调用root的析构函数

root = Tk()
root.title('最简单的桌面应用程序')
root.geometry('640x320')
root.iconbitmap('res/Tk.ico')

label = Label(root, text='Hello World', font=("Arial Bold", 50))
label.pack(side='top', expand='yes', fill='both')
btn = Button(root, text='关闭窗口', bg='#C0C0C0', command=click_button) # 用command参数绑定事件函数
btn.pack(side='top', fill='x', padx=5, pady=5)

root.mainloop()

现在点击按钮就可关闭窗口了。你看,事件驱动机制是多么的简单和美妙!当然,绑定事件和事件函数的方法不止有本例用到的command,后面还会谈到bind和bind_class两种方式。

2.4 面向对象使用Tkinter

对于上一段代码,熟悉OOP的读者会注意到事件函数click_button中使用了root这个全局变量。从语法和编程规范的角度看,这样做没有任何问题。不过,当桌面程序面对稍微复杂的业务逻辑时,势必要大量使用全局变量,这给程序的安全带来了隐患,同时也不便于程序的维护。下面的代码以面向对象的方式设计了一个按钮点击计数器。

from tkinter import *

class MyApp(Tk):
    """继承Tk,创建自己的桌面应用程序类"""
    
    def __init__(self):
        """构造函数"""
        
        super().__init__()
        
        self.title('按钮点击计数器')
        self.geometry('320x160')
        self.iconbitmap('res/Tk.ico')
        
        self.counter = IntVar() # 创建一个整型变量对象
        self.counter.set(0) # 置其初值为0
        
        label = Label(self, textvariable=self.counter, font=("Arial Bold", 50)) # 将Label和整型变量对象关联
        label.pack(side='left', expand='yes', fill='both', padx=5, pady=5)
        
        btn = Button(self, text='点我试试看', bg='#90F0F0')
        btn.pack(side='right', anchor='center', fill='y', padx=5, pady=5)
        
        btn.bind(sequence='<Button-1>', func=self.on_button) # 绑定事件和事件函数
    
    def on_button(self, evt):
        """点击按钮事件的响应函数, evt是事件对象"""
        
        self.counter.set(self.counter.get()+1)

if __name__ == '__main__':
    app = MyApp()
    app.mainloop()

这段代码用到了整型对象IntVar,这是Tkinter独有的概念。当类型对象被改变时,与其关联的控件文本内容会自动更新。借助于类型对象和控件之间的关联,用户可以方便地在其他线程中更新UI。

代码运行界面如上图所示。每点击一次按钮,计数器自动加1并显示在Lable控件上。请注意,这个例子并没有使用command绑定按钮事件,而是使用了bind方法将鼠标左键点击事件和事件函数on_button绑定在一起。这个用法要求事件函数on_button接受一个事件对象evt作为参数,该参数提供了和事件相关的详细信息。不难理解,command适用于绑定控件自身的事件,bind适用于绑定鼠标和键盘事件。


3. 事件和事件对象

3.1 鼠标事件

Tkinter支持的鼠标事件如下所列。

  • <Button-1> - 左键单击
  • <Button-2> - 中键单击
  • <Button-3> - 右键单击
  • <Button-1> - 左键单击
  • <B1-Motion> - 左键拖动
  • <B2-Motion> - 中键拖动
  • <B3-Motion> - 右键拖动
  • <ButtonRelease-1> - 左键释放
  • <ButtonRelease-2> - 中键释放
  • <ButtonRelease-3> - 右键释放
  • <Double-Button-1> - 左键双击
  • <Double-Button-2> - 中键双击
  • <Double-Button-3> - 右键双击
  • <Motion> - 移动
  • <MouseWheel> - 滚轮
  • <Enter> - 进入控件
  • <Leave> - 离开控件

下面的代码演示了如何绑定鼠标事件,以及如何使用鼠标事件对象。

from tkinter import *

class MyApp(Tk):
    """继承Tk,创建自己的桌面应用程序类"""
    
    def __init__(self):
        """构造函数"""
        
        super().__init__()
        
        self.title('鼠标事件演示程序')
        self.geometry('480x200')
        self.iconbitmap('res/Tk.ico')
        
        self.info = StringVar()
        self.info.set('')

        label = Label(self, textvariable=self.info, font=("Arial Bold", 18))
        label.pack(side='top', expand='yes', fill='both')
        
        btn = Button(self, text='确定', bg='#C0C0C0')
        btn.pack(side='top', fill='x', padx=5, pady=5)

        label.bind('<Enter>', self.on_mouse)
        label.bind('<Leave>', self.on_mouse)
        label.bind('<Motion>', self.on_mouse)
        label.bind('<MouseWheel>', self.on_mouse)
        btn.bind('<Button-1>', self.on_mouse)
        btn.bind('<Button-2>', self.on_mouse)
        btn.bind('<Button-3>', self.on_mouse)
        btn.bind('<B1-Motion>', self.on_mouse)
        btn.bind('<Double-Button-1>', self.on_mouse)
        btn.bind('<Double-Button-3>', self.on_mouse)
    
    def on_mouse(self, evt):
        """响应所有鼠标事件的函数"""
        
        if isinstance(evt.num, int):
            self.info.set('事件类型:%s\\n键码:%d\\n鼠标位置:(%d, %d)\\n时间:%d'%(evt.type, evt.num, evt.x, evt.y, evt.time))
        else:
            self.info.set('事件类型:%s\\n鼠标位置:(%d, %d)\\n时间:%d'%(evt.type, evt.x, evt.y, evt.time))

if __name__ == '__main__':
    app = MyApp()
    app.mainloop()

这段代码在标签控件和按钮控件上绑定了多种鼠标事件,并把这些事件绑定到了同一个事件函数上,事件函数被调用时会传入事件对象作为参数。借助于事件对象可以获得事件类型、鼠标位置、触发时间等详细信息。

当鼠标进入或离开标签控件、在标签控件上移动鼠标或滚动滚轮、在按钮控件上点击鼠标按键,相应的事件类型和信息就会显示在标签上。代码运行界面如上图所示。

3.2 键盘事件

Tkinter支持的鼠标事件如下所列。

  • <Return> - 回车
  • <Cancel> - Break键
  • <BackSpace> - BackSpace键
  • <Tab> - Tab键
  • <Shift_L> - Shift键
  • <Alt_R> - Alt键
  • <Control_L> - Control键
  • <Pause> - Pause键
  • <Caps_Lock> - Caps_Lock键
  • <Escape> - Escapel键
  • <Prior> - PageUp键
  • <Next> - PageDown键
  • <End> - End键
  • <Home> - Home键
  • <Left> - 左箭头
  • <Up> - 上箭头
  • <Right> - 右箭头
  • <Down> - 下箭头
  • <Print> - Print Screen键
  • <Insert> - Insert键
  • <Delete> - Delete键
  • <F1> - F1键
  • <Num_Lock> - Num_Lock键
  • <Scroll_Lock> - Scroll_Lock键
  • <Key> - 任意键

下面的代码演示了如何绑定键盘事件,以及如何使用键盘事件对象。

from tkinter import *

class MyApp(Tk):
    """继承Tk,创建自己的桌面应用程序类"""
    
    def __init__(self):
        """构造函数"""
        
        super().__init__()
        
        self.title('键盘事件演示程序')
        self.geometry('480x200')
        self.iconbitmap('res/Tk.ico')
        
        self.info = StringVar()
        self.info.set('')

        self.info = StringVar()
        self.info.set('')

        self.lab = Label(self, textvariable=self.info, font=("Arial Bold", 18))
        self.lab.pack(side='top', expand='yes', fill='both')
        self.lab.focus_set()
        self.lab.bind('<Key>', self.on_key)
        
        self.btn = Button(self, text='切换焦点', bg='#C0C0C0', command=self.set_label_focus)
        self.btn.pack(side='top', fill='x', padx=5, pady=5)
    
    def on_key(self, evt):
        """响应所有键盘事件的函数"""
        
        self.info.set('evt.char = %s\\nevt.keycode = %s\\nevt.keysym = %s'%(evt.char, evt.keycode, evt.keysym))
    
    def set_label_focus(self):
        """在Label和Button之间切换焦点"""
            
        self.info.set('')
        
        if isinstance(self.lab.focus_get(), Label):
            self.btn.focus_set()
        else:
            self.lab.focus_set()

if __name__ == '__main__':
    app = MyApp()
    app.mainloop()

这段代码在标签控件上绑定了任意键被按下事件,在按钮控件上绑定了切换焦点的事件函数。代码运行界面如下所示。

这里需要特别说明一下,绑定键盘事件的控件必须在获得焦点后绑定才能生效。本例点击按钮可在按钮和标签之间切换焦点,请仔细体会标签在或获得和失去焦点后对于键盘事件的不同反应。

3.3 组件事件

组件是一个较为含糊的说法,大致可以认为是窗体和控件的统称。Tkinter支持的组件事件较多,这里只介绍最为常用的几个。

  • <Configure> - 改变大小或位置
  • <FocusIn> - 获得焦点时触发
  • <FocusOut> - 失去焦点时触发
  • <Destroy> - 销毁时触发

下面的例子演示了窗体绑定销毁事件的用法。通常,这样做是为了在用户关闭窗口前做些保护性的清理性的工作。

from tkinter import *

def befor_quit(evt):
    """关闭之前清理现场"""
    
    print('关闭之前,可以做点什么')

root = Tk()
Label(root, text='Hello World').pack()

root.bind('<Destroy>', befor_quit)

root.mainloop()

3.4 事件对象

无论是鼠标事件、键盘事件还是组件事件,都要求与其绑定的事件函数接受一个事件对象作为参数。一个事件对象一般包含下列信息。

  • widget - 触发事件的控件
  • type - 事件类型
  • x, y - 鼠标在窗体上的坐标(以左上角为原点)
  • x_root, y_root - 鼠标在屏幕上的坐标(以左上角为原点)
  • num - 鼠标事件对应的按键码
  • char - 键盘事件对应的字符代码
  • keysym - 键盘事件对应的字符串
  • keycode - 键盘事件对应的按键码
  • width, height - 受事件影响后的控件宽高

在鼠标事件和键盘事件的例子中已经演示了事件对象的用法,这里不再赘述。


4. 常用控件

4.1 窗格Frame

在wx等GUI库中,Frame的含义是窗体,不过Tkinter的Frame控件更像一个控件的容器,这里我把它称为窗格,以免产生歧义。配合pack方法,Frame堪称是Tkinter的布局利器。

from tkinter import *

class MyApp(Tk):
    """继承Tk,创建自己的桌面应用程序类"""
    
    def __init__(self):
        """构造函数"""
        
        super().__init__()
        
        self.title('窗格:Frame')
        self.iconbitmap('res/Tk.ico')
        self.init_ui()
    
    def init_ui(self):
        """初始化界面"""
        
        frame1 = Frame(self, bg='#90c0c0') 
        frame1.pack(padx=5, pady=5)

        # Label是frame1的第1个子控件,从左向右布局
        Label(frame1, bg='#f0f0f0', width=25).pack(side=LEFT, fill=BOTH, padx=5, pady=5)

        # frame2是frame1的第2个子控件,从左向右布局
        frame2 = Frame(frame1, bg='#f0f0f0')
        frame2.pack(side=LEFT, padx=5, pady=5)

        # 3个Button是frame2的子控件,自上而下布局
        Button(frame2, text='按钮1', width=10).pack(padx=5, pady=5)
        Button(frame2, text='按钮2', width=10).pack(padx=5, pady=5)
        Button(frame2, text='按钮3', width=10).pack(padx=5, pady=5)

if __name__ == '__main__':
    app = MyApp()
    app.mainloop()

这段代码最外层的frame1是为了控制窗体内上下左右的留白大小。lable和frame2同属于frame1的子元素,分列左右。frame2里面自上而下放置了3个按钮。代码运行界面如下图所示。

4.2 输入框Entry

通过输入框的textvariable参数关联一个字符串类型对象,当输入框内容改变时会自动同步到关联的字符串类型对象——这是输入框控件Entry的一个使用技巧。输入框的另一个常用参数是justify,用来指定输入内容的对齐方式。另外,输入框控件输入密码时,show参数可以指定一个字符以替换实际输入的内容。

from tkinter import *

class MyApp(Tk):
    """继承Tk,创建自己的桌面应用程序类"""
    
    def __init__(self):
        """构造函数"""
        
        super().__init__()
        
        self.title('输入框:Entry')
        self.iconbitmap('res/Tk.ico')
        self.init_ui()
    
    def init_ui(self):
        """初始化界面"""
        
        account, passwd = StringVar(), StringVar()
        account.set('')
        passwd.set('')

        group = LabelFrame(self, text="登录", padx=5, pady=5)
        group.pack(padx=20, pady=10)

        f1 = Frame(group)
        f1.pack(padx=5, pady=5)
        Label(f1, text='账号:').pack(side=LEFT, pady=5)
        Entry(f1, textvariable=account, width=15, justify=CENTER).pack(side=LEFT, pady=5)

        f2 = Frame(group)
        f2.pack(padx=5, pady=5) 
        Label(f2, text='密码:').pack(side以上是关于Tkinter:实用至上主义的经典之作的主要内容,如果未能解决你的问题,请参考以下文章

花一个小时,学会用这个实用至上主义的 GUI 库!

如何利用Tkinter中Canvas绘制曲线图,请教高手

《简约至上》阅读整理

《简约至上》阅读整理

软件开发中的两种人:实用主义和发烧友

css BaaN:百度作为网络实用程序 - 百度的实用主义方法论。