资讯详情

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

ce576afa0220a035bc58c64e1050a63e.gif

作者 | 天元浪子

出品 | CSDN(ID:CSDNnews)

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也许是你苦苦追求的真爱。

Tkinter该模块提供了一个名称Tk窗体类,十几个基本控件,多种对象,多种常量,可选主题的控件包ttk与各种对话框组件。ttk它被理解为一个增强的控件包,它提供了更多控件。Tkinter模块的组织结构如下图所示。

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

fromtkinterimport*

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

from tkinter import * fromtkinterimportttk,filedialog,messagebox

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

  1. 创建窗体

  2. 将所需的控制器放在窗户上,并告诉他们在预期事件发生时执行预设动作

  3. 启动循环监控事件

无论程序有多简单或复杂,第一步和第三步都是固定的,设计师只需要关注第二步的实现。下面的代码实现了最简单的代码之一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赋予其他含义。

代码操作界面如上图所示。弹出的程序窗口又小又丑,就像新生儿一样,但它确实是一个完整的桌面应用程序。

所谓控件布局,就是设置控件在窗体内的位置、填充、间隔等属性。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后面会详细说明方法。

桌面程序不仅是控制器的列表,而且是对外部刺激的反应,包括用户的操作。如果将窗口和控制器与桌面程序的身体进行比较,那么响应外部刺激就是它的灵魂。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两种方式。

对于上一段代码,熟悉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适用于绑定鼠标和键盘事件。

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()

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

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

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()

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

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

组件是一个较为含糊的说法,大致可以认为是窗体和控件的统称。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()

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

  • widget - 触发事件的控件

  • type - 事件类型

  • x, y - 鼠标在窗体上的坐标(以左上角为原点)

  • x_root, y_root - 鼠标在屏幕上的坐标(以左上角为原点)

  • num - 鼠标事件对应的按键码

  • char - 键盘事件对应的字符代码

  • keysym - 键盘事件对应的字符串

  • keycode - 键盘事件对应的按键码

  • width, height - 受事件影响后的控件宽高

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

在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个按钮。代码运行界面如下图所示。

通过输入框的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=LEFT, pady=5)
        Entry(f2, textvariable=passwd, width=15, show='*', justify=CENTER).pack(side=LEFT, pady=5)


        btn = Button(self, text='确定', bg='#90c0c0', command=lambda : print(account.get(), passwd.get()))
        btn.pack(fill=X, padx=20, pady=10)


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

这段代码还有同时演示了带标签的窗格控件LabelFrame的用法。代码运行界面如下图所示。

单选框通常是成组使用的,每个Radiobutton都关联同一个整型对象,该整型对象的值就是单选框选中选项的索引号。

from tkinter import *


class MyApp(Tk):
    """继承Tk,创建自己的桌面应用程序类"""
    
    def __init__(self):
        """构造函数"""
        
        super().__init__()
        
        self.title('单选框:Radiobutton')
        self.iconbitmap('res/Tk.ico')
        self.init_ui()
    
    def init_ui(self):
        """初始化界面"""
        
        f0 = Frame(self)
        f0.pack(padx=5, pady=5)
         
        f1 = Frame(f0) 
        f1.pack(side=LEFT, padx=5, pady=5)


        g1 = LabelFrame(f1, text="你最擅长哪一个?", padx=5, pady=5)
        g1.pack(padx=5, pady=5)


        self.rb_v1 = IntVar()
        self.rb_v1.set(0)
        rb_11 = Radiobutton(g1, variable=self.rb_v1, text='Tkinter', value=0, command=self.on_radio_1)
        rb_12 = Radiobutton(g1, variable=self.rb_v1, text='wxPython', value=1, command=self.on_radio_1)
        rb_13 = Radiobutton(g1, variable=self.rb_v1, text='PyQt5', value=2, command=self.on_radio_1)
        rb_11.pack(ancho='w', padx=5, pady=5)
        rb_12.pack(ancho='w', padx=5, pady=5)
        rb_13.pack(ancho='w', padx=5, pady=5)


        f2 = Frame(f0) 
        f2.pack(side=LEFT, padx=5, pady=5)


        g2 = LabelFrame(f2, text="你最擅长哪一个?", padx=5, pady=5)
        g2.pack(padx=5, pady=5)


        self.rb_v2 = IntVar()
        self.rb_v2.set(0)
        rb_21 = Radiobutton(g2, variable=self.rb_v2, text='Tkinter', value=0, indicatoron=False, command=self.on_radio_2)
        rb_22 = Radiobutton(g2, variable=self.rb_v2, text='wxPython', value=1, indicatoron=False, command=self.on_radio_2)
        rb_23 = Radiobutton(g2, variable=self.rb_v2, text='PyQt5', value=2, indicatoron=False, command=self.on_radio_2)
        rb_21.pack(fill=X, padx=5, pady=5)
        rb_22.pack(fill=X, padx=5, pady=5)
        rb_23.pack(fill=X, padx=5, pady=5)


        self.info = StringVar()
        self.info.set('')
        label = Label(self, textvariable=self.info, bg='#ffffff')
        label.pack(expand='yes', fill='x', padx=5, pady=10)
    
    def on_radio_1(self):
        """响应第1组单选框事件的函数"""
        
        selected = ['Tkinter', 'wxPython', 'PyQt5'][self.rb_v1.get()]
        self.info.set('你选择了第1组的%s'%selected)


    def on_radio_2(self):
        """响应第2组单选框事件的函数"""
        
        selected = ['Tkinter', 'wxPython', 'PyQt5'][self.rb_v2.get()]
        self.info.set('你选择了第2组的%s'%selected)


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

这段代码演示了两种不同风格的单选框控件。代码运行界面如下图所示。

复选框的每一项都需要关联一个整型对象,每当有选项被点击时,逐一检查每一个整型对象的值,就可以获得当前选中的选项。

from tkinter import *


class MyApp(Tk):
    """继承Tk,创建自己的桌面应用程序类"""
    
    def __init__(self):
        """构造函数"""
        
        super().__init__()
        
        self.title('复选框:Checkbox')
        self.iconbitmap('res/Tk.ico')
        self.init_ui()
    
    def init_ui(self):
        """初始化界面"""
        
        group = LabelFrame(self, text="你擅长哪个?", padx=20, pady=5)
        group.pack(padx=30, pady=5)


        self.cb_v1 = IntVar()
        self.cb_v2 = IntVar()
        self.cb_v3 = IntVar()
        self.cb_v1.set(0)
        self.cb_v2.set(0)
        self.cb_v3.set(0)


        cb_1 = Checkbutton(group, variable=self.cb_v1, text='Tkinter', onvalue=1, offvalue=0, command=self.on_cb).pack(ancho='w', padx=5, pady=5)
        cb_2 = Checkbutton(group, variable=self.cb_v2, text='wxPython', onvalue=1, offvalue=0, command=self.on_cb).pack(ancho='w', padx=5, pady=5)
        cb_3 = Checkbutton(group, variable=self.cb_v3, text='PyQt5', onvalue=1, offvalue=0, command=self.on_cb).pack(ancho='w', padx=5, pady=5)


        self.info = StringVar()
        self.info.set('')
        label = Label(self, textvariable=self.info, bg='#ffffff')
        label.pack(expand='yes', fill='x', padx=5, pady=5)
    
    def on_cb(self):
        """响应复选框事件的函数"""
        
        selected = list()
        if self.cb_v1.get():
           selected.append('Tkinter') 
        if self.cb_v2.get():
           selected.append('wxPython') 
        if self.cb_v3.get():
           selected.append('PyQt5') 
        
        self.info.set(', '.join(selected))


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

运行界面如下图所示。

计数器Spinbox既可以向Entry那样接受键盘输入,也可以点击上下的箭头实现数值的增加,适用于小幅度连续调整的场合。

from tkinter import *


def on_spin():
    """响应可调输入框事件的函数"""
    
    info.set(str(spin_v.get()))


root = Tk()
root.title('可调输入框:Spinbox')


spin_v = IntVar()
spin_v.set(5)
entry = Spinbox(root, textvariable=spin_v, from_=0, to=9, bg='#ffffff', command=on_spin).pack(fill=X, padx=5, pady=5)


info = StringVar()
info.set(str(spin_v.get()))
label = Label(root, textvariable=info, bg='#ffffff')
label.pack(expand=YES, fill=X, padx=5, pady=5)


root.mainloop()

在这段代码中,Spinbox只绑定了鼠标事件没有绑定键盘事件,因此信息显式区不能显示键盘输入信息,只响应鼠标操作。代码运行界面如下图所示。

和其他控件相比,滑块控件Scale在应用上有一点点怪异:如果用command参数绑定事件函数,则要求该函数接收一个事件对象作为参数。类似的情况还出现在控件命名上,比如,Radiobutton的第2个单词首字母小写,LabelFrame的第2个单词首字母却是大写。特例破坏了一致性的美感,这也是Tkinter为人诟病的一个突出问题。

from tkinter import *


class MyApp(Tk):
    """继承Tk,创建自己的桌面应用程序类"""
    
    def __init__(self):
        """构造函数"""
        
        super().__init__()
        
        self.title('滑块:Scale')
        self.geometry('240x100')
        self.iconbitmap('res/Tk.ico')
        self.init_ui()
    
    def init_ui(self):
        """初始化界面"""
        
        self.scale_v = DoubleVar()
        self.scale_v.set(50)
        scale = Scale(self, variable=self.scale_v, from_=0, to=100, orient=HORIZONTAL, command=self.on_scale)
        scale.pack(fill=X, padx=5, pady=5)


        self.info = StringVar()
        self.info.set(str(self.scale_v.get()))
        label = Label(self, textvariable=self.info, bg='#ffffff')
        label.pack(expand=YES, fill=X, padx=5, pady=5)
    
    def on_scale(self, evt):
        """响应滑块事件的函数"""
        
        self.info.set(str(self.scale_v.get()))


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

代码运行界面如下图所示。

下面的代码给出了一个完整的菜单例子。

class MyApp(Tk):
    """继承Tk,创建自己的桌面应用程序类"""
    
    def __init__(self):
        """构造函数"""
        
        super().__init__()
        
        self.title('菜单按钮:Menubutton')
        self.geometry('300x100')
        self.iconbitmap('res/Tk.ico')
        self.init_ui()
    
    def init_ui(self):
        """初始化界面"""
        
        frame_menu = Frame(self)
        frame_menu.pack(anchor=NW) # 菜单位于窗口左上角(North_West)


        mb_file = Menubutton(frame_menu, text='文件', relief=RAISED)
        mb_file.pack(side='left')
        file_menu = Menu(mb_file, tearoff=False)
        file_menu.add_command(label='打开', command=lambda :print('打开文件'))
        file_menu.add_command(label='保存', command=lambda :print('保存文件'))
        file_menu.add_separator()
        file_menu.add_command(label='退出', command=self.destroy)
        mb_file.config(menu=file_menu)


        mb_help = Menubutton(frame_menu, text='帮助', relief=RAISED)
        mb_help.pack(side='left')
        help_menu = Menu(mb_help, tearoff=False)
        help_menu.add_command(label='关于...', command=lambda :print('帮助文档'))
        mb_help.config(menu=help_menu)


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

为了让代码看起来更清晰,这里用lambda函数代替了菜单按钮的事件函数。其实,lambda函数就是匿名函数,它以lambda关键字开始,以半角冒号分隔函数参数和函数体。代码运行界面如下图所示。

Tkinter的消息对话框子模块messagebox提供了多种对话框,以适应不同的应用需求,下面的代码演示了其中常用的七个对话框。前文已经提到过,子模块messagebox必须要显式导入才能使用。

from tkinter import *
from tkinter import messagebox as mb


root = Tk()
root.title('消息对话框')


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


f = Frame(root)
f.pack(padx=5, pady=10)


Button(f, text='提示信息', command=lambda :info.set(mb.showinfo(title='提示信息', message='对手认负,比赛结束。'))).pack(side=LEFT, padx=5)
Button(f, text='警告信息', command=lambda :info.set(mb.showwarning(title='警告信息', message='不能连续提和!'))).pack(side=LEFT, padx=5)
Button(f, text='错误信息', command=lambda :info.set(mb.showerror(title='错误信息', message='着法错误!'))).pack(side=LEFT, padx=5)
Button(f, text='Yes/No', command=lambda :info.set(mb.askyesno(title='操作提示', message='对手提和,接受吗?'))).pack(side=LEFT, padx=5)
Button(f, text='Ok/Cancel', command=lambda :info.set(mb.askokcancel(title='操作提示', message='再来一局?'))).pack(side=LEFT, padx=5)
Button(f, text='Retry/Cancel', command=lambda :info.set(mb.askretrycancel(title='操作提示', message='消息发送失败!'))).pack(side=LEFT, padx=5)
Button(f, text='Yes/No/Cancel', command=lambda :info.set(mb.askyesnocancel(title='操作提示', message='是否保存对局记录?'))).pack(side=LEFT, padx=5)


label = Label(root, textvariable=info, bg='#ffffff')
label.pack(expand='yes', fill='x', padx=5, pady=20)


root.mainloop()

代码运行界面如下图所示。

点击按钮后弹出的各个对话框如下图所示。

Tkinter的文件对话框子模块filedialog提供了多种对话框,以适应不同的应用需求,下面的代码演示了其中常用的文件选择、目录选择和文件保存等三个对话框。同样的,子模块filedialog也必须要显式导入才能使用。

from tkinter import *
from tkinter import filedialog as fd


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


        f = Frame(self)
        f.pack(padx=20, pady=10)


        Button(f, text='选择文件', command=lambda :info.set(fd.askopenfilename(title='选择文件'))).pack(side=LEFT, padx=10)
        Button(f, text='选择目录', command=lambda :info.set(fd.askdirectory(title='选择目录'))).pack(side=LEFT, padx=10)
        Button(f, text='保存文件', command=lambda :info.set(fd.asksaveasfilename(title='保存文件', defaultextension='.png'))).pack(side=LEFT, padx=10)


        label = Label(self, textvariable=info, bg='#ffffff')
        label.pack(expand='yes', fill='x', padx=5, pady=20)


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

代码运行界面如下图所示。

下图是选择打开文件的对话窗口,选择路径和保存文件与之类似。

Tk的研发始于1989 年,第一个版本于1991年问世,彼时还是一个重实力轻颜值的年代。相比于后来的wx和Qt,Tk的控件更注重实用,卖相自然不会太好。好在Tkinter与时俱进,后期推出了可选主题的控件包ttk,算是对其控件颜值的补救吧。

可选主题的控件包ttk包含了18个控件,其中Button、Checkbutton、Entry、Frame、Label, LabelFrame、Menubutton、PanedWindow、Radiobutton、Scale、Scrollbar和Spinbox等12个和已有的控件重合,只是用法上有些差异,6个新增的控件是Combobox、Notebook、Progressbar、Separator、Sizegrip和Treeview。

之所以称其为可选主题的控件包,是因为ttk提供了Style类,可统一定制所有ttk控件的风格。在Python的IDLE中可以方便地查看ttk包含的可用主题。

>>> from tkinter import ttk
>>> style = ttk.Style()
>>> style.theme_names()
('winnative', 'clam', 'alt', 'default', 'classic', 'vista', 'xpnative')

让我们先来看看这些主题和原来的控件有什么不同。

from tkinter import *
from tkinter import ttk


class MyApp(Tk):
    """继承Tk,创建自己的桌面应用程序类"""
    
    def __init__(self):
        """构造函数"""
        
        super().__init__()
        
        self.title('主题控件')
        self.iconbitmap('res/Tk.ico')
        self.init_ui()
    
    def init_ui(self):
        """初始化界面"""
        
        self.style = ttk.Style()
        
        self.theme = StringVar() 
        self.theme.set(self.style.theme_use())
        
        ttk.Button(self, text='切换主题按钮', command=self.on_style).pack(padx=30, pady=20)
        ttk.Entry(self, textvariable=self.theme, justify=CENTER, width=20).pack(padx=30, pady=0)
        ttk.Combobox(self, value=('Tkinter', 'wxPython', 'PyQt5')).pack(padx=30, pady=20)
    
    def on_style(self):
        """更换主题"""
        
        items = self.style.theme_names()
        new_theme = items[(items.index(self.theme.get())+1)%len(items)]
        self.theme.set(new_theme)
        self.style.theme_use(new_theme)


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

代码运行界面如下图所示。点击按钮可在ttk可用的主题之间循环切换,截图为Windows原生主题。

本文开始的快速体验环节,已经介绍过用窗体的geometry方法设置窗口大小,其实,它也被用来设置窗口位置。

from tkinter import *


class MyApp(Tk):
    """继承Tk,创建自己的桌面应用程序类"""
    
    def __init__(self):
        """构造函数"""
        
        super().__init__()
        
        self.title('窗口居中')
        self.iconbitmap('res/Tk.ico')
        self.init_ui()
        self.center()
    
    def init_ui(self):
        """初始化界面"""
        
        Label(self, text='Hello World', font=("Arial Bold", 50)).pack(expand=YES, fill='both')
    
    def center(self):
        """窗口居中"""
        
        self.update() # 更新显示以获取最新的窗口尺寸
        scr_w = self.winfo_screenwidth() # 获取屏幕宽度
        scr_h = self.winfo_screenheight() # 获取屏幕宽度
        w = self.winfo_width() # 窗口宽度
        h = self.winfo_height() # 窗口高度
        x = (scr_w-w)//2 # 窗口左上角x坐标
        y = (scr_h-h)//2 # 窗口左上角y坐标


        self.geometry('%dx%d+%d+%d'%(w,h,x,y)) # 设置窗口大小和位置


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

运行这段代码,显示的仍然是最初的Hello World,但是不管设置了多大的字号,窗口总是位于屏幕的中央。

Tinkter的很多控件都可以作为图像显示的容器,或者用图片来提升颜值,只是Tinkter的图像处理能力有点弱,比如,BitmapImage类只能处理灰度图像,PhotoImage只能打开.gif格式和部分.png格式的图像。幸好pillow模块提供了可用于Tinkter的PhotoImage对象,使得Tinkter也可以非常方便地使用图像了。下面的例子使用标签控件Label作为图像容器,点击前翻后翻按钮可在多张照片之间循环切换。

from tkinter import *
from tkinter import ttk
from PIL import Image, ImageTk


class MyApp(Tk):
    """继承Tk,创建自己的桌面应用程序类"""
    
    def __init__(self):
        """构造函数"""
        
        super().__init__()
        
        self.title('相册')
        self.iconbitmap('res/Tk.ico')
        self.init_ui()
        self.center()
    
    def init_ui(self):
        """初始化界面"""
        
        self.curr = 0
        self.photos = ('res/DSC03363.jpg', 'res/DSC03394.jpg', 'res/DSC03402.jpg')
        self.img = ImageTk.PhotoImage(Image.open(self.photos[self.curr]))


        self.album = Label(self, image=self.img)
        self.album.pack(expand=YES, fill='both', padx=5, pady=5)


        f = Frame(self)
        f.pack(padx=10, pady=20)


        style = ttk.Style()
        style.theme_use('vista')


        ttk.Button(f, text='<', width=10, command=self.on_prev).pack(side=LEFT, padx=10)
        ttk.Button(f, text='>', width=10, command=self.on_next).pack(side=LEFT, padx=10)
    
    def center(self):
        """窗口居中"""
        
        self.update() # 更新显示以获取最新的窗口尺寸
        scr_w = self.winfo_screenwidth() # 获取屏幕宽度
        scr_h = self.winfo_screenheight() # 获取屏幕宽度
        w = self.winfo_width() # 窗口宽度
        h = self.winfo_height() # 窗口高度
        x = (scr_w-w)//2 # 窗口左上角x坐标
        y = (scr_h-h)//2 # 窗口左上角y坐标


        self.geometry('%dx%d+%d+%d'%(w,h,x,y)) # 设置窗口大小和位置
    
    def on_prev(self):
        """前一张照片"""
        
        self.curr = (self.curr-1)%3
        img = ImageTk.PhotoImage(Image.open(self.photos[self.curr]))
        self.album.configure(image=img)
        self.album.image = img


    def on_next(self):
        """后一张照片"""
        
        self.curr = (self.curr+1)%3
        img = ImageTk.PhotoImage(Image.open(self.photos[self.curr]))
        self.album.configure(image=img)
        self.album.image = img


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

代码运行界面如下图所示。点击前翻后翻按钮可在多张照片之间循环切换。

几乎所有的GUI课程都会用计算器作为例子,Tkinter怎能缺席呢?这个例子除了演示如何使用grid方法布局外,还演示了在一个控件类的所有实例上绑定事件和事件函数,即bind_class的用法。

from tkinter import *


class MyApp(Tk):
    """继承Tk,创建自己的桌面应用程序类"""
    
    def __init__(self):
        """构造函数"""
        
        super().__init__()
        
        self.title('计算器')
        self.iconbitmap('res/Tk.ico')
        self.init_ui()
        self.center()
    
    def init_ui(self):
        """初始化界面"""
        
        self.screen = StringVar()
        self.screen.set('')
        Label(self, textvariable=self.screen, anchor=E, bg='#000030', fg='#30ff30', font=("Arial Bold", 16)).pack(fill=X, padx=10, pady=10)


        keys = [
            ['(', ')', 'Back', 'Clear'],
            ['7',  '8',  '9',  '/'], 
            ['4',  '5',  '6',  '*'], 
            ['1',  '2',  '3',  '-'], 
            ['0',  '.',  '=',  '+']
        ]


        f = Frame(self)
        f.pack(padx=10, pady=10)


        for i in range(5):
            for j in range(4):
                if i == 0 or j == 3:
                    Button(f, text=keys[i][j], width=8, bg='#f0e0d0', fg='red').grid(row=i, column=j, padx=3, pady=3)
                elif i == 4 and j == 2:
                    Button(f, text=keys[i][j], width=8, bg='#f0e0a0').grid(row=i, column=j, padx=3, pady=3)
                else:
                    Button(f, text=keys[i][j], width=8, bg='#d9e4f1').grid(row=i, column=j, padx=3, pady=3)


        self.bind_class("Button", "<ButtonRelease-1>", self.on_button)
    
    def center(self):
        """窗口居中"""
        
        self.update() # 更新显示以获取最新的窗口尺寸
        scr_w = self.winfo_screenwidth() # 获取屏幕宽度
        scr_h = self.winfo_screenheight() # 获取屏幕宽度
        
        w = self.winfo_width() # 窗口宽度
        h = self.winfo_height() # 窗口高度
        x = (scr_w-w)//2 # 窗口左上角x坐标
        y = (scr_h-h)//2 # 窗口左上角y坐标


        self.geometry('%dx%d+%d+%d'%(w,h,x,y)) # 设置窗口大小和位置
    
    def on_button(self, evt):
        """响应按键"""
        
        if self.screen.get() == 'Error':
            self.screen.set('')
        
        ch = evt.widget.cget('text')
        if ch == 'Clear':
            self.screen.set('')
        elif ch == 'Back':
            self.screen.set(self.screen.get()[:-1])
        elif ch == '=':
            try:
                result = str(eval(self.screen.get()))
            except:
                result = 'Error'
            self.screen.set(result)
        else:
            self.screen.set(self.screen.get() + ch)


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

代码运行界面如下图所示。

以百分之一秒的频率刷新显示,对于任何一款GUI库来说,都是不容小觑的负担。不过,由于Tkinter采用了独树一帜的类型对象关联控件机制,在计时线程中高速刷新标签显示内容却是从容不迫、游刃有余。

import time
import threading
from tkinter import *


def on_btn():
    """点击按钮"""
    
    global t0
    
    if btn_name.get() == '开始':
        lcd.set('0.00')
        t0 = time.time()
        btn_name.set('停止')
    else:
        btn_name.set('开始')


def watch():
    """秒表计时线程函数"""
    
    while True:
        if btn_name.get() == '停止':
            lcd.set('%.2f'%(time.time()-t0))
        else:
            time.sleep(0.01)


root = Tk()
root.title('秒表')


btn_name = StringVar() # 按钮名
btn_name.set('开始')


t0 = 0 # 计时开始的时间戳
lcd = StringVar() # 液晶显示值
lcd.set('0:00')


f = Frame(root)
f.pack(padx=20, pady=10)


Label(f, textvariable=lcd, width=10, bg='#000030', fg='#30ff30', font=("Arial Bold", 24)).pack(pady=10)
Button(f, textvariable=btn_name, bg='#f0e0d0', command=on_btn).pack(fill=X, pady=10)


threading.Thread(target=watch).start()


root.mainloop()

点击开始按钮,秒表自动清零并启动计时,计时精度高达百分之一秒。代码运行界面如下图所示。

Canvas组件为Tkinter的图形绘制提供了基础。Canvas是一个高度灵活的组件,可以用来展示图片,也可以用来绘制图形和图表,创建图形编辑器,并实现各种自定义的小部件,比如弧形、线条、椭圆形、多边形和矩形等。

from tkinter import *
import tkinter.colorchooser as tc


class MyApp(Tk):
    """继承Tk,创建自己的桌面应用程序类"""
    
    def __init__(self):
        """构造函数"""
        
        super().__init__()
        
        self.title('画板')
        self.iconbitmap('res/Tk.ico')
        self.init_ui()
    
    def init_ui(self):
        """初始化界面"""
        
        self.color = '#90f010' # 当前颜色
        self.pen = 3 # 当前画笔
        self.pos = None # 鼠标当前位置
        
        self.rbv = IntVar() # 当前画笔
        self.rbv.set(self.pen)
        
        self.cav = Canvas(self, bg='#ffffff', width=480, height=320)
        self.cav.pack(side=LEFT, padx=5, pady=5)
        
        self.cav.bind('<Button-1>', self.on_down)
        self.cav.bind('<ButtonRelease-1>', self.on_up)
        self.cav.bind('<B1-Motion>', self.on_motion)
        
        frame = Frame(self)
        frame.pack(side=LEFT, anchor=N, padx=5, pady=20)
        
        Radiobutton(frame, variable=self.rbv, text='1pix', value=1, command=self.on_radio).pack(ancho='w', padx=5, pady=5)
        Radiobutton(frame, variable=self.rbv, text='3pix', value=3, command=self.on_radio).pack(ancho='w', padx=5, pady=5)
        Radiobutton(frame, variable=self.rbv, text='5pix', value=5, command=self.on_radio).pack(ancho='w', padx=5, pady=5)
        Radiobutton(frame, variable=self.rbv, text='7pix', value=7, command=self.on_radio).pack(ancho='w', padx=5, pady=5)
        Radiobutton(frame, variable=self.rbv, text='9pix', value=9, command=self.on_radio).pack(ancho='w', padx=5, pady=5)
        
        self.btn = Button(frame, text='', width=6, bg=self.color, command=self.on_btn)
        self.btn.pack(padx=5, pady=10)
    
    def on_radio(self):
        """选择画笔"""
        
        self.pen = self.rbv.get()
    
    def on_btn(self):
        """选择颜色"""
        
        color = tc.askcolor()[1]
        if color:
            self.color = color
            self.btn.configure(bg=self.color)
    
    def on_down(self, evt):
        """左键按下"""
        
        self.pos = evt.x, evt.y
    
    def on_up(self, evt):
        """左键弹起"""
        
        self.pos = None
    
    def on_motion(self, evt):
        """鼠标移动"""
        
        if not self.pos is None:
            line = (*self.pos, evt.x, evt.y)
            self.pos = evt.x, evt.y
            self.cav.create_line(line, fill=self.color, width=self.pen)


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

这段代码实现了一个简易的画板,提供画笔粗细和颜色选择,拖拽鼠标在画板上移动即可绘制线条。代码运行界面如下图所示。

在Tkinter中使用Matplotlib绘图库的关键在于,Matplotlib的后端子模块可以生成Tkinter的canvas控件,同时Matplotlib也可以在其上绘图。

import numpy as np
import matplotlib


matplotlib.use('TkAgg')
matplotlib.rcParams['font.sans-serif'] = ['FangSong']
matplotlib.rcParams['axes.unicode_minus'] = False


from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from tkinter import *


class MyApp(Tk):
    """继承Tk,创建自己的桌面应用程序类"""
    
    def __init__(self):
        """构造函数"""
        
        Tk.__init__(self)
        
        self.title('集成Matplotlib')
        self.iconbitmap('res/Tk.ico')
        self.init_ui()
        self.center()
    
    def init_ui(self):
        """初始化界面"""
        
        self.fig = Figure(dpi=150)
        self.cv = FigureCanvasTkAgg(self.fig, self)
        self.cv.get_tk_widget().pack(fill=BOTH, expand=1, padx=5, pady=5)
        
        f = Frame(self)
        f.pack(pady=10)
        Button(f, text='散点图', width=12, bg='#f0e0d0', command=self.on_scatter).pack(side=LEFT, padx=20)
        Button(f, text='等值线图', width=12, bg='#f0e0d0', command=self.on_contour).pack(side=LEFT, padx=20)
    
    def center(self):
        """窗口居中"""
        
        self.update() # 更新显示以获取最新的窗口尺寸
        scr_w = self.winfo_screenwidth() # 获取屏幕宽度
        scr_h = self.winfo_screenheight() # 获取屏幕宽度
        
        w = self.winfo_width() # 窗口宽度
        h = self.winfo_height() # 窗口高度
        x = (scr_w-w)//2 # 窗口左上角x坐标
        y = (scr_h-h)//2 # 窗口左上角y坐标


        self.geometry('%dx%d+%d+%d'%(w,h,x,y)) # 设置窗口大小和位置
    
    def on_scatter(self):
        """散点图"""
        
        x = np.random.randn(50) # 随机生成50个符合标准正态分布的点(x坐标)
        y = np.random.randn(50) # 随机生成50个符合标准正态分布的点(y坐标)
        color = 10 * np.random.rand(50) # 随即数,用于映射颜色
        area = np.square(30*np.random.rand(50)) # 随机数表示点的面积
        
        self.fig.clear()
        ax = self.fig.add_subplot(111)
        ax.scatter(x, y, c=color, s=area, cmap='hsv', marker='o', edgecolor='r', alpha=0.5)
        self.cv.draw()
    
    def on_contour(self):
        """等值线图"""
        
        y, x = np.mgrid[-3:3:60j, -4:4:80j]
        z = (1-y**5+x**5)*np.exp(-x**2-y**2)
        
        self.fig.clear()
        ax = self.fig.add_subplot(111)
        ax.set_title('有填充的等值线图')
        c = ax.contourf(x, y, z, levels=8, cmap='jet')
        self.fig.colorbar(c, ax=ax)
        self.cv.draw()


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

代码运行界面如下图所示。

原文链接:https://xufive.blog.csdn.net/article/details/124514094

 
     

☞开发者收到“加料”的假 Offer,害上家被盗近 6.25 亿美元!
☞小米自动驾驶测试车曝光;马斯克疑回应生9个孩子:帮助应对人口不足危机;亚马逊发布AI编程助手|极客头条
☞justjavac:从辍学到成为Deno核心代码贡献者,我的十年编程生涯

标签: 2x100k14继电器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台