(备注:本文基于Python3.7)
本想写tkinter的入门教程,但已经有非常多的技术大佬对Tkinter的API有着非常全面的介绍,我临时决定改变策略,决定分析Tkinter框架的源码,让我们从创建一个最简单的TkInter应用开始,只需如下的3行代码!
import tkinter
root = tkinter.Tk()
root.mainloop()
1、导入tkinter模块
2、创建Tk对象
3、调用Tk对象的mainloop()方法
让我们运行起来……下图为Windows系统下的效果,标题栏有个羽毛的图标、还有一个英文标题:tk
这个窗口是如何创建出来的呢?Tkinter框架执行了哪些代码呢?让我们从源码中一探究竟!
先了解一下tkinter框架的所有代码,tkinter本身为一个包模块,tkinter包体中的代码定义在__init__.py模块文件中,当执行import tkinter的时候,__init__.py中没有缩进的代码会立刻执行(python标准)
分析tkinter包的__init__.py中没有缩进的代码前,再先介绍一下tkinter图形界面框架中的所有模块(如上方截图)
算上__init__.py文件,一共14个模块文件 + 1个模块包test(这个模块包用于单元测试,值得学习),接下来介绍下14个模块文件以及test模块包
1、__main__.py
用于测试tkinter
2、colorchooser.py
实现了颜色选择对话框
3、commondialog.py
定义所有对话框的基类
4、constants.py
定义了很多有用的常量
5、dialog.py
定义了一个对话框类
6、dnd.py
一个测试模块
7、filedialog.py
定义了与文件系统相关的对话框
8、font.py
定义了关于字体的相关类
9、messagebox.py
定义了很多对话框
10、scrolledtest.py
一个全新的控件ScrolledText,自带滑动条
11、simpledialog.py
定义了一些简单的对话框
12、tix.py
实现的新控件,不过已经在python3.6后废弃,可以不用关心它的代码了
13、ttk.py
定义了一些全新样式的控件,非常好用
14、__init__.py
tkinter包模块的代码体文件,tkinter框架的主要代码都在里面,各种控件,比如Label、Button都在里面
15、test包
用于执行单元测试的模块包
接下来分析__init__.py包模块文件,当我们使用import tkinter的时候,即会创建tkinter模块对象,此时__init__.py文件中的顶层代码(没有缩进)全部会执行…………我会按照__init__.py模块中的执行顺序做一个介绍
__init__.py模块,大约4000+行,按照书写的从上到下的执行顺序,描述这些顶层代码都是干什么的,首先是总览
导入模块:enum、sys、_tkinter、tkinter.constants、re
全局变量:14个
函数:多少个来着,再多了
类:控件类都在里面、还有其他类
一、注释内容
"""Wrapper functions for Tcl/Tk.
Tkinter provides classes which allow the display, positioning and
control of widgets. Toplevel widgets are Tk and Toplevel. Other
widgets are Frame, Label, Entry, Text, Canvas, Button, Radiobutton,
Checkbutton, Scale, Listbox, Scrollbar, OptionMenu, Spinbox
LabelFrame and PanedWindow.
Properties of the widgets are specified with keyword arguments.
Keyword arguments have the same name as the corresponding resource
under Tk.
Widgets are positioned with one of the geometry managers Place, Pack
or Grid. These managers can be called with methods place, pack, grid
available in every Widget.
Actions are bound to events by resources (e.g. keyword argument
command) or with the method bind.
Example (Hello, World):
import tkinter
from tkinter.constants import *
tk = tkinter.Tk()
frame = tkinter.Frame(tk, relief=RIDGE, borderwidth=2)
frame.pack(fill=BOTH,expand=1)
label = tkinter.Label(frame, text="Hello, World")
label.pack(fill=X, expand=1)
button = tkinter.Button(frame,text="Exit",command=tk.destroy)
button.pack(side=BOTTOM)
tk.mainloop()
"""
作者不仅阐述了tkinter框架的底层依赖为Tcl/Tk程序,也写了一个Tkinter的Hello World,所以好好看看源码中作者的注释,受益匪浅!!
二、导入模块
import enum
import sys
import _tkinter # If this fails your Python may not be configured for Tk
TclError = _tkinter.TclError
from tkinter.constants import *
import re
import enum 导入enum模块 【说明:python规定每个模块也是对象,会隐式的使用同名的全局变量enum指向一个模块对象】
import sys 导入sys模块,sys模块表示Python虚拟机相关的操作
import _tkinter 导入_tkinter模块,表示依赖的内部模块
创建全局变量TclError,它指向的是_tkinter模块对象的一个属性TclError,TclError是一个class,从面向对象的角度看,全局变量TclError指向的是一个class对象(TclError类对象)
from tkinter.constants imort * 从tkinter包下的constansts模块下导入所有可导出的属性,这个位于tkinter包中的constants模块,定义了好多全局变量,这里主要就是为了导入这些常量,比如我们可以直接使用tkinter.TOP,后续的文章会专门介绍constants的设计(如果模块没有覆盖__all__内置属性,则默认导出所有属性,如何覆盖了__all__内置属性,则*仅会导出__all__序列中的元素)
import re 导入re模块,表示正则表达式模块
三、创建8个全局变量
wantobjects = 1
TkVersion = float(_tkinter.TK_VERSION)
TclVersion = float(_tkinter.TCL_VERSION)
READABLE = _tkinter.READABLE
WRITABLE = _tkinter.WRITABLE
EXCEPTION = _tkinter.EXCEPTION
_magic_re = re.compile(r'([\\{}])')
_space_re = re.compile(r'([\s])', re.ASCII)
wantobjects 没看出来有啥用
TkVersion 此全局变量保存着Tk的版本号
TclVersion 此全局变量保存着Tcl的版本号
READABLE、WRITABLE、EXCEPTION 这三个全局变量应该是调试用的吧
四、连续创建3个函数
_join 返回值为一个字符串,主要是拼接字符串
_stringify 返回值也是一个字符串,里面利用正则,替换字符串了
_flatten 返回值是一个元组,看样子是合并序列用的(以后专门学习一下)
五、执行一行语句
try: _flatten = _tkinter._flatten
except AttributeError: pass
尝试访问_tkinter模块的一个属性_flatten,然后赋值给新建的全局变量_flatten,不过对于可能出现的AttributeError,直接捕获,然后什么也不做……,只是pass
六、创建一个函数
_cnfmerge 用于合并属性的?
七、再次尝试定义一个全局变量
try: _cnfmerge = _tkinter._cnfmerge
except AttributeError: pass
八、再创建一个函数(_开头的函数表示内部函数)
_splitdict 看名称是分离字典用的
九、创建两个关于事件的类
EventType
class EventType(str, enum.Enum):
#省略很多代码#
EventType继承了两个类,一个str、一个Enum,Python支持多继承,看来此处的EventType对字符串和枚举,进行了扩展,里面定了很多的类变量、还重写了特殊方法__str__,这个类值得学习
Event
class Event:
#省略很多代码#
Event类比较单纯,就重写了一个__repr__方法
十、再次创建两个内部使用的全局变量
_support_default_root = 1 _default_root = None
十一、创建3个函数
NoDefaultRoot
_tkerror
_exit
十二、创建1个内部使用的全局变量
_varnum = 0
十三、创建关于双向绑定的数据类,共计5个
Variable
class Variable:
#省略很多代码#
Variable是所有双向绑定数据类的父类,它写了很多通用的方法 StringVar
class StringVar(Variable):
#省略很多代码#
StringVar是用于持有一个字符串对象的双向绑定类 IntVar
IntVar用于持有一个整型
DoubleVar
DoubleVar用于持有一个double
BooleanVar
BooleanVar用于持有一个boolean值
十四、创建用于将主线程进入循环事件的函数,这个函数会一直循环下去
mainloop
十五、再次创建两个全局变量,指向两个类,一个指向int,一个指向float
getint = int getdouble = float
十六、转化true和false的函数
getboolean
十七、创建控件基类
Misc 控件的基类之一,它的代码量非常大
十八、创建用于可调用对象的包装类
CallWrapper
十九、创建用于控件所在Window的类2个
XView
YView
二十、创建关于Window的类
Wm 窗口的基类
二十一、创建我们经常使用的,当作窗口的类
Tk
class Tk(Misc, Wm):
#省略很多代码#
Tk类继承了Misc和Wm
二十二、创建一个TCL函数
Tcl
二十三、创建布局管理器类3个
Pack:表示线性布局
Place:表示位置布局
Grid:表示网格(表格)布局
二十四、创建控件相关类
BaseWidget
Widget
Toplevel
Button
Canvas
Checkbutton
Entry
Frame
Label
Listbox
Menu
Menubutton
Message
Radiobutton
Scale
Scrollbar
Text
_setit
OptionMenu
二十五、创建关于图片的类
Image
PhotoImage
BitmapImage
二十六、又是控件类
Spinbox
LabelFrame
PanedWindow
二十七、创建用于测试当前模块的函数
_test
二十八、执行语句,__init__.py作为脚本执行时,会执行测试
if __name__ == '__main__':
_test()
以上是整个tkinter包中__init__.py代码执行过程,我们常见的一些功能全部在这里看到了,接下来继续我们的主流程,3行代码创建了一个窗口,剩下的两行做了什么?
import tkinter
root = tkinter.Tk()
root.mainloop()
class Tk(Misc, Wm):
#省略很多代码#
Tk类定义在tkinter包中的__init__.py模块中,它继承于两个父类,Misc和Wm,每个Tk对象表示顶级窗口,看你下它的继承树,会很清晰
class Misc:
#省略很多代码#
Misc类位于tkinter包中的__init__.py模块中,父类则是Object,官方给的注释为:
Methods defined on both toplevel and interior widgets
class Wm:
#省略很多源码#
Wm类也位于tkinter包中的__init__.py模块中,父类为Object,官方的注释为:
Provides functions for the communication with the window manager.
当tkinter包导入时,这3个类被创建,同时它们中的类属性将会执行,我罗列一下(注意:不列出函数的定义)
Tk类有个类变量_w
_w = '.'
Misc类的类变量好多呀
_last_child_ids = None
_tclCommands = None
waitvar = wait_variable # XXX b/w compat
focus = focus_set # XXX b/w compat?
lift = tkraise
_nametowidget = nametowidget
register = _register
_subst_format = ('%#', '%b', '%f', '%h', '%k',
'%s', '%t', '%w', '%x', '%y',
'%A', '%E', '%K', '%N', '%W', '%T', '%X', '%Y', '%D')
_subst_format_str = " ".join(_subst_format)
config = configure
_noarg_ = ['_noarg_']
propagate = pack_propagate
slaves = pack_slaves
anchor = grid_anchor
bbox = grid_bbox
columnconfigure = grid_columnconfigure
rowconfigure = grid_rowconfigure
size = grid_size
Wm的类变量也不少,全部指向的是方法对象,相当于方法的别称,怪不得呢
aspect = wm_aspect
attributes=wm_attributes
client = wm_client
colormapwindows = wm_colormapwindows
command = wm_command
deiconify = wm_deiconify
focusmodel = wm_focusmodel
forget = wm_forget
frame = wm_frame
geometry = wm_geometry
grid = wm_grid
group = wm_group
iconbitmap = wm_iconbitmap
iconify = wm_iconify
iconmask = wm_iconmask
iconname = wm_iconname
iconphoto = wm_iconphoto
iconposition = wm_iconposition
iconwindow = wm_iconwindow
manage = wm_manage
maxsize = wm_maxsize
minsize = wm_minsize
overrideredirect = wm_overrideredirect
positionfrom = wm_positionfrom
protocol = wm_protocol
resizable = wm_resizable
sizefrom = wm_sizefrom
state = wm_state
title = wm_title
transient = wm_transient
withdraw = wm_withdraw
def __init__(self, screenName=None, baseName=None, className='Tk',
useTk=1, sync=0, use=None):
self.master = None
self.children = {}
self._tkloaded = 0
self.tk = None
if baseName is None:
import os
baseName = os.path.basename(sys.argv[0])
baseName, ext = os.path.splitext(baseName)
if ext not in ('.py', '.pyc'):
baseName = baseName + ext
interactive = 0
self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
if useTk:
self._loadtk()
if not sys.flags.ignore_environment:
# Issue #16248: Honor the -E flag to avoid code injection.
self.readprofile(baseName, className)
Tk类定义在tkinter包下的__init__.py模块中, 当我们执行tkinter.Tk()创建好Tk对象后,它的特殊方法__init__()会被自动调用,分析一下它的方法体
1、Tk对象持有的实例变量master初始化为None
2、创建一个空的字典对象,由实例变量children持有
3、创建一个_tkloaded实例变量,初始化为0
4、持有的tk,初始化为None
5、检查默认值参数baseName,当baseName为None时,先导入os模块,取出当前模块文件的全路径名称(sys.argv[0]),再使用os.path.basename取出文件名,接着检查将文件名进一步分离为文件名与扩展名,检查扩展名是否是py或者pyc,如果不是,basename直接使用basename与ext拼接起来
6、通过_tkinter的create()函数,创建tk对象,此为c语言实现,具体代码看不到
7、如果使用了tk,则会调用一个_loadtk()方法,局部变量useTk默认值为1
8、一个环境检查,忽略环境则会执行readprofile()方法
如果你仔细找,发现Tk中并没有定义mainloop()方法,Tk类下只有下面3个方法,且只有一个loadtk()允许你调用,那么mainloop()在哪里?根据继承树的查找属性的逻辑,从下到上,从左到右的顺序,找啊找!
终于在父类Misc类中找到了mainloop()方法
def mainloop(self, n=0):
"""Call the mainloop of Tk."""
self.tk.mainloop(n)
方法体分析
1、直接调用了持有的self.tk的mainloop()方法,那么self.tk指向的是哪个对象呢?
2、在Misc类中找了一遍没有找到,原来它是在子类Tk中初始化的,根据从下到上的继承树查找逻辑,我们找到了self.tk的初始化代码(此处为模版方法模式),位于Tk类中
self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
而_tkinter而是一个内部模块,具体实现我还没有找到,create()方法也是未知实现,应该快要到tkinter的底层tcl了,c语言实现的部分
def create(*args, **kwargs): # real signature unknown """ wantTk if false, then Tk_Init() doesn't get called sync if true, then pass -sync to wish use if not None, then pass -use to wish """ pass
而位于_tkinter.py的mainloop()方法,也看不到了
def mainloop(self, *args, **kwargs): # real signature unknown pass
1、学习tkinter框架的设计,主要是为了学习Python,学习大佬们怎么更好的写Python代码
2、总之本篇文章虽然没有写的很完整,自己还是收获满满的……