part11-2 Python图形界面编程(Tkinter常用组件、对话框(Dialog)、菜单、Canvas绘图)

 

五、 Tkinter 常用组件

Tkinter 各组件的详细用法还需要掌握,也就是掌握各个“积木块”的的详细功能。

 

1、 使用 ttk 组件

在前面直接使用的 tkinter 模块下的 GUI 组件看上去并不美观。为此 Tkinter 引了一个 ttk 组件作为补充,并使用功能更强大的 Combobox 取代原来的 Listbox,且新增了 LabeledScale(带标签的Scale)、Notebook(多文档窗口)、Progressbar(进度条)、Treeview(树)等组件。

ttk 是一个放在 tkinter 包下的模块,使用方式与使用普通的 Tkinter 组件基本相同,只要导入 ttk 模块即可。ttk 组件的使用示例如下代码所示:

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()
        # self.init_ttk()

    def initWidgets(self):
        cb = Listbox(self.master, font=24)
        # 为 Listbox 设置列表项
        for s in ('Python', 'Linux', 'HTML'):
            cb.insert(END, s)
        cb.pack(side=LEFT, fill=X, expand=YES)
        f = Frame(self.master)
        f.pack(side=RIGHT, fill=BOTH, expand=YES)
        lab = Label(self.master, text='我的标签', font=24)
        lab.pack(side=TOP, fill=BOTH, expand=YES)
        bn = Button(self.master, text='我的按钮')
        bn.pack()

    def init_ttk(self):
        # ttk 使用 Combobo 取代 Listbox
        cb = ttk.Combobox(self.master, font=24)
        # 为 Combobox 设置列表项
        cb['value'] = ('Python', 'Linux', 'HTML')
        cb.pack(side=LEFT, fill=X, expand=YES)
        f = ttk.Frame(self.master)
        f.pack(side=RIGHT, fill=BOTH, expand=YES)
        lab = ttk.Label(self.master, text='我的标签', font=24)
        lab.pack(side=TOP, fill=BOTH, expand=YES)
        bn = ttk.Button(self.master, text='我的按钮')
        bn.pack()

root = Tk()
root.title("简单事件处理")
App(root)
root.mainloop()

  上面代码中, init_ttk() 方法是使用 ttk 组件的代码。现在在__init__函数中注释了 self.init_ttk(),直接运行这段代码可以看到图十三中1图的界面。将 self.init_ttk() 取消注释,并且对 self.initWidgets() 添加注释后,再运行代码,看到的是图十三中2图的界面。

图十三   Tkinter组件与ttk组件的运行界面

对比两个界面上的 button 可以看出,ttk 下的Button 接近 Windows 7 本地平台网格,显得更好看些,这就是 ttk 组件优势。

 

2、 Variable 类

Tkinter 支持将多个 GUI 组件与变量进行双向绑定,执行这种双向绑定后编程非常方便。

(1)、如果在程序中改变变量的值,GUI 组件的显示内容或值会随之改变。

(2)、当 GUI 组件的内容发生改变时(如用户输入),变量的值也会随之改变。

要让 Tkinter 组件与变量进行双向绑定,只要为这些组件指定 variable(绑定组件的 value)、textvariable(绑定组件显示的文本)等属性即可。双向绑定还有一个限制,就是 Tkinter 不允许将组件和普通变量进行绑定,只能和 tkinter 包下的 Variable 类的子类进行绑定。该类包含的子类如下:

(1)、StringVar():用于包装 str 值的变量。

(2)、IntVar():用于包装整型值的变量。

(3)、DoubleVar():用于包装浮点值的变量。

(4)、BooleanVar():用于包装 bool 值的变量。

使用 Variable 的 set() 方法可以设置变量值,get() 方法可以得到变量值。下面代码实现将 Entry 组件与 StringVar 进行双向绑定,在程序中可通过 StringVar 改变 Entry 输入框显示的内容,也可通过该 StringVar 获取 Entry 输入框中的内容。

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        self.st = StringVar()
        # 创建 Label 组件,将其 textvariable 绑定到 self.st 变量
        ttk.Entry(self.master, textvariable=self.st,
                  width=24, font=('StSong', 20, 'bold'),
                  foreground='red').pack(fill=BOTH, expand=YES)
        # 创建 Frame 作为容器
        f = Frame(self.master)
        f.pack()
        # 创建两个按钮,并放入 Frame 中
        ttk.Button(f, text='改变', command=self.change).pack(side=LEFT)
        ttk.Button(f, text='获取', command=self.get).pack(side=LEFT)

    def change(self):
        books = ('Python 入门', 'Python 进阶', 'Python 核心')
        import random
        # 改变 self.st 变量值,与之绑定的 Entry 的内容随之改变
        self.st.set(books[random.randint(0, 2)])

    def get(self):
        from tkinter import messagebox
        # 获取 self.st 变量的值,实际上就是获取与之绑定的 Entry 中的内容
        # 并使用消息框显示 self.st 变量的值
        messagebox.showinfo(title='输入内容', message=self.st.get())

root = Tk()
root.title('variable 测试')
App(root)
root.mainloop()

  上面代码中的 self.st.set(books[random.randint(0, 2)]) 行代码调用 StringVar 改变变量的值,这样与该变量绑定的 Entry 中显示的内容会随之改变;另一行 messagebox.showinfo(title='输入内容', message=self.st.get()) 代码获取 StringVar 变量的值,实际上是获取与该变量绑定的 Entry 中的内容。

运行这段代码,单击界面上的“改变”按钮,可看到输入框中的内容会随之改变;单击“获取”按钮就会弹出一个消息框,并显示在 Entry 输入框中输入的内容。运行结果如图十四所示。

图十四  使用variable类设置和获取输入框值

 

3、 使用 compound 选项

还可以为按钮或 Label 等组件同时指定 text 和 image 选项。text 选项用于指定该组件上的文本,image 选项用于显示该组件上的图片。同时指定这两个选项时,image 会覆盖 text,可通过 compound 选项来控制 text 不被 image 覆盖掉。

compound 选项的属性值有:

(1)、None:图片覆盖文字。

(2)、LEFT常量(值为'left'字符串):图片在左,文本在右。

(3)、RIGHT常量(值为'right'字符串):图片在右,文本在左。

(4)、TOP常量(值为'top'字符串):图片在上,文本在下。

(5)、BOTTOM常量(值为'bottom'字符串):图片在底,文本在上。

(6)、CENTER常量(值为'center'字符串):文本在图片上方。

下面代码使用多个单选按钮来控制 Label 的 compound 选项,代码如下:

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 创建一个位图
        bm = PhotoImage(file='baidu.png')
        # 创建一个 Entry ,同时指定 text 和 image
        self.label = ttk.Label(self.master, text='Python学\n习路线', \
                               image=bm, font=('StSong', 20, 'bold'), foreground='#000')
        self.label.bm = bm
        # 设置 Label 默认的 compound 为 None
        self.label['compound'] = None
        self.label.pack()
        # 创建 Frame 容器,用于装多个 Radiobutton
        f = ttk.Frame(self.master)
        f.pack(fill=BOTH, expand=YES)
        compounds = ('None', 'LEFT', 'RIGHT', 'TOP', 'BOTTOM', 'CENTER')
        # 定义一个 StringVar 变量,用作绑定 Radiobutton 的变量
        self.var = StringVar()
        self.var.set('None')
        # 使用循环创建多个 Radiobutton(单选按钮) 组件
        for val in compounds:
            rb = Radiobutton(f, text=val, padx=20, variable=self.var,
                             command=self.change_compound,
                             value=val).pack(side=LEFT, anchor=CENTER)

    # 实现 change_compound 方法,用于动态改变 Label 的 compound 选项
    def change_compound(self):
        self.label['compound'] = self.var.get().lower()

root = Tk()
root.title('compound测试')
App(root)
root.mainloop()

  上面代码中,这行 elf.label['compound'] = None 代码设置 Label 默认的 compound 选项为 None,此时该 Label 默认图片覆盖文字;另一行 self.label['compound'] = self.var.get().lower() 代码会根据单选按钮的值(单选按钮与 self.var 绑定)来确定 Label 的 compound 选项。

运行代码,首先看到的是 Label 只有显示图片,文字不显示,这是由 compound 选项为 None 设置的效果,可点击下边的单选按钮,就可看到文字和图片位置的改变。运行效果如图十五所示。

图十五  将 compound 设为 TOP 让图片在上

 

4、 Entry 和 Text 组件

这两个都是输入框组件,其中 Entry 是单行输入框组件,Text 是多行输入框组件,Text 还可以为不同的部分添加不同的样式,甚至响应事件。

这两个都可通过 get() 方法来获取文本框中的内容;也可通过 insert() 方法来改变文本框中的内容。

要删除这两个组件中的内容,需要使用 delete(self, first, last=None) 方法,该方法删除从 first 到 last 索引之间的内容。对于这两个组件使用的索引方法是不同的。Entry 是单行文本,索引比较简单,例如指定(3,8)表示指定的是第4个到第8个字符。但是Text 是多行文本框组件,它的索引要同时指定行号和列号,它的行号从1开始,列号从0开始。例如 1.0 表示的是第1行、第1列的字符,如果要指定第2行第3个字符到第3行第7个字符,索引应指定为 (2.2, 3.6)。

另外,Entry支持双向绑定(前面代码中已经使用过),可将 Entry 与变量绑定在一起,这样在程序中就可通过该变量来改变、获取Entry 组件中的内容。

关于这两个组件的用法示例如下:

from tkinter import *
from tkinter import ttk
from tkinter import messagebox

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 第1步:创建 Entry 组件
        self.entry = ttk.Entry(self.master, width=44, font=('StSong', 14),
                               foreground='orange')
        self.entry.pack(fill=BOTH, expand=YES)
        # 第2步:创建 Text 组件
        self.text = Text(self.master, width=44, height=4, font=('StSong', 14),
                         foreground='blue')
        self.text.pack(fill=BOTH, expand=YES)
        # 第3步:创建 Frame 容器,用于放5个按钮组件
        f = Frame(self.master)
        f.pack()
        # 第4步:创建5个按钮,将其放入 Frame 中
        ttk.Button(f, text='开始输入', command=self.insert_start).pack(side=LEFT)
        ttk.Button(f, text='编辑处插入', command=self.insert_edit).pack(side=LEFT)
        ttk.Button(f, text='结尾输入', command=self.insert_end).pack(side=LEFT)
        ttk.Button(f, text='获取Entry', command=self.get_entry).pack(side=LEFT)
        ttk.Button(f, text='获取 Text', command=self.get_text).pack(side=LEFT)

    def insert_start(self):
        # 在 Entry 和 Text 的开始处插入内容
        self.entry.insert(0, 'Linux')
        self.text.insert(0.0, 'Linux')

    def insert_edit(self):
        # 在 Entry 和 Text 的编辑处插入内容
        self.entry.insert(INSERT, 'Python')
        self.text.insert(INSERT, 'Python')

    def insert_end(self):
        # 在 Entry 和 Text 的结尾处插入内容
        self.entry.insert(END, 'Html')
        self.text.insert(END, 'Html')

    def get_entry(self):
        messagebox.showinfo(title='输入内容', message=self.entry.get())

    def get_text(self):
        messagebox.showinfo(title='输入内容', message=self.text.get(0.0, END))

root = Tk()
root.title("Entry测试")
App(root)
root.mainloop()

  上面代码中,首先创建了一个 Entry 组件和一个 Text 组件。在接下来的事件响应函数中,调用了这两个组件的 insert() 方法分别在开始位置、指定位置、编辑处、结尾处插入内容,通过该 insert() 方法的第一个参数可以指定要插入内容的位置。

要获取 Entry 组件和 Text 组件的内容,通过调用 get() 方法来实现。在调用 Text 组件的 insert()、get() 方法时,要注意坐标参数的写法。

运行这段代码,向 Entry、Text 中插入一些内容,单击界面上的 “获取 Text” 按钮,可以看到如图十六所示的界面。

图十六  获取文本框中的内容

Text是一个功能强大的“富文本”编辑组件,使用 Text 不仅可能插入文本,还可以插入图片。

(1)、image_create(self,index,cnf={},**kw) 方法可以插入图片;

(2)、还可以设置文本内容的格式,使用的方法是:insert(self, index, chars, *args),最后一个参数传入多个 tag 进行控制,实现图文并茂效果。

此外, Text 组件还可能使用滚动条,在内容较多时实现滚动显示。要实现滚动显示需要进行双向关联。可分成两步操作:

(1)、将 Scrollbar 的 command 设为目标组件的 xview 或 yview,其中 xview 控制水平滚动,yview 控制垂直滚动。

(2)、将目标组件的 xscrollcommand 或 yscrollcommand 属性设为 Scrollbar 的set 方法。

下面代码使用 Text 来实现一个图文并茂的界面。

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 创建第一个 Text 组件
        text1 = Text(self.master, height=27, width=32)
        # 创建图片
        book = PhotoImage(file='images/pyhistory.png')
        text1.bm = book
        text1.insert(END, '\n')
        # 在结尾处插入图片
        text1.image_create(END, image=book)
        text1.pack(side=LEFT, fill=BOTH, expand=YES)
        # 创建第二个 Text 组件
        text2 = Text(self.master, height=33, width=50)
        text2.pack(side=LEFT, fill=BOTH, expand=YES)
        self.text = text2
        # 创建 Scrollbar 组件,设置该组件与 Text2 的垂直滚动关联
        scroll = Scrollbar(self.master, command=text2.yview)
        scroll.pack(side=RIGHT, fill=Y)
        # 设置 Text 的垂直滚动影响 scroll 滚动条
        text2.configure(yscrollcommand=scroll.set)
        # 配置名为 title 的样式
        text2.tag_configure('title', font=('楷体', 20, 'bold'),
                            foreground='red',justify=CENTER, spacing3=20)
        text2.tag_configure('detail', foreground='darkgray',
                            font=('微软雅黑', 11, 'bold'),
                            spacing2=20, # 设置行间距
                            spacing3=15) # 设置段间距
        text2.insert(END, '\n')
        # 插入文本内容,设置使用 title 样式
        text2.insert(END, 'Python语言介绍\n', 'title')
        # 创建图片
        star = PhotoImage(file='images/g016.gif')
        text2.bm = star
        details = ('Python 是一门古老的编程语言,发展到今天,已经应用到多个行业上。\n',
                   'Python 有两个主要的版本,分别是 2.X 和 3.x 版本,现在官方推荐都转到 3.x 版本上。\n',
                   'Python 主要应用领域有Web开发、大数据分析、人工智能。' + \
                   '以及科学计算、金融等领域\n')
        # 采用循环插入多条介绍信息
        for de in details:
            text2.image_create(END, image=star)
            text2.insert(END, de, 'detail')
        url = ['https://www.baidu.com', 'https://www.python.org']
        name = ['百度主页', 'Python官网']
        m = 0
        for each in name:
            # 为每个链接创建单独的配置
            text2.tag_configure(m, foreground='blue', underline=True,
                                font=('微软雅黑', 13, 'bold'))
            text2.tag_bind(m, '<Enter>', self.show_arrow_cursor)
            text2.tag_bind(m, '<Leave>', self.show_common_cursor)
            # 使用 handlerAdaptor 包装,将当前链接参数传入事件处理方法中
            text2.tag_bind(m, '<Button-1>', self.handlerAdaptor(self.click, x=url[m]))
            text2.insert(END, each + '\n', m)
            m += 1

    def show_arrow_cursor(self, event):
        # 光标移上去时变成箭头
        self.text.config(cursor='arrow')
    def show_common_cursor(self, event):
        # 光标移出去恢复原样
        self.text.configure(cursor='xterm')
    def click(self, event, x):
        import webbrowser
        # 使用默认浏览器打开链接
        webbrowser.open(x)
    def handlerAdaptor(self, fun, **kwds):
        return lambda event, fun=fun, kwds=kwds: fun(event, **kwds)

root = Tk()
root.title("Text测试")
App(root)
root.mainloop()

  

上面代码中,第29-34行使用 Text 的tag_configure(也写作 tag_config)方法创建了 title 和 detail 两个 tag,每个 tag 可用于控制一段文本的格式、事件等。接下来使用 title tag 插入一个标题内容,标题内容的格式将受到 title tag 的控制;接着再使用循环插入了三条受 detail tag 控制的描述信息,每次在插入描述信息之前都先插入一张图片。

第54-59行代码在循环内创建了 tag,并调用 Text 组件的 tag_bind() 方法为 tag 绑定事件处理方法。这里要让每链接打开不同页面,因此要为每条链接内容分别创建不同的 tag,从而实现每个链接打开对应的页面。

运行上面的代码,看到图文界面如图十七所示。

图十七  使用Text实现图文并茂的界面

 

5、 Radiobutton 和 Checkbutton 组件

Radiobuton 组件是单选按钮,可为该组件绑定一个方法或函数,当单选按钮被选择时,该方法或函数就会被触发。如果要将多个Radiobutton 编为一组,需要将多个 Radiobutton 绑定到同一个变量。当这组 Radiobutton 的其中一个单选按钮被选中时,该变量就会随之改变;反过来,该变量发生改变时,这组 Radiobutton 也会自动选中该变量值所对应的单选按钮。关于 Radiobutton 组件的用法示例如下:

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 创建一个 Label 组件
        ttk.Label(self.master, text='选择喜欢的图书:').\
            pack(fill=BOTH, expand=YES)
        self.intVar = IntVar()
        # 定义元组
        books = ('Python 基础入门', 'Python 进阶', 'Python 核心编程', 'Python 应用编程')
        i = 1
        # 采用循环创建多个 Radiobutton
        for book in books:
            ttk.Radiobutton(self.master, text=book,
                            variable=self.intVar, # 将 Radiobutton 绑定到 self.intVar 变量
                            command=self.change, # 将选中事件绑定到 self.change 方法
                            value=i).pack(anchor=W)
            i += 1
        # 设置 Radiobutton 绑定的变量值为 2
        # 则默认选中 value 为 2 的 Radiobutton
        self.intVar.set(2)

    def change(self):
        from tkinter import messagebox
        # 通过 Radiobutton 绑定变量获取选中的单选钮
        messagebox.showinfo(title='选中的图书', message=self.intVar.get())

root = Tk()
root.title("Radiobutton 测试")
App(root)
root.mainloop()

  上面代码中使用循环创建多个 Radiobutton 组件,第20行代码指定将这些 Radiobutton 绑定到 self.initVar 变量,这样这些Radiobutton 位于同一组内;第21行代码为这组 Radiobutton 的选中事件绑定了 self.change 方法,每次选择不同的单选按钮时,就会触发对象的 change() 方法。

运行上面的代码,从输出界面可看到默认选中第二个单选钮,这是由第26行代码进行设置的。当选中其它单选钮时,就会弹出提示框显示用户的选择项。运行结果如图十八所示。

图十八  选中不同的单选钮

单选钮不仅可以显示文本,还可以显示图片,使用到的是 image 选项。如果要让图片和文字同时显示,也可通过 compound 选项进行控制,如果不指定 compound,该选项默认为 None,这意味着只显示图片。下面代码示例了带图片的单选钮。

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 创建一个 Label 组件
        ttk.Label(self.master, text='选中你喜欢的兵种:').\
            pack(fill=BOTH, expand=YES)
        self.intVar = IntVar()
        # 定义元组
        races_dict = {'z.png': '虫族', 'p.png': '神族', 't.png': '人族'}
        # 采用循环创建多个 Radiobutton
        for i, rc in enumerate(races_dict.keys(), 1):
            bm = PhotoImage(file='images/' + rc)
            r = ttk.Radiobutton(self.master,
                                image=bm,
                                text=races_dict[rc],
                                compound=RIGHT, # 图片在文字右边
                                variable=self.intVar, # 将 Radiobutton 绑定到 self.intVar 变量
                                command=self.change, # 将选中事件绑定到 self.change 方法
                                value=i)
            r.bm = bm
            r.pack(anchor=W)
        # 设置默认选中 value 为 2 的单选钮
        self.intVar.set(2)
    def change(self): pass

root = Tk()
root.title('Radiobutton 测试')
# 改变窗口图标
root.iconbitmap('images/logo.ico')
App(root)
root.mainloop()

  上面代码中的20~22行为 Radiobutton 同时指定了 image 和 text 选项,并指定 compound 选项为 RIGHT,这时该单选钮的图片显示在文字的右边。此外,第35行代码设置了窗口自定义的图标。现在运行这段代码,可以看到如图十九所示的界面。

图十九  带图标的单选钮

Checkbutton 与 Radiobutton 很相似,但 Checkbutton 可选择多项,与每组 Radiobutton 只能选择一项不同。Checkbutton 同样可显示文字和图片,内样可以绑定变量,同样可以为选中事件绑定函数和处理方法。由于 Checkbutton 可以同时选中多项,因此需要为每个 Checkbutton 都绑定一个变量。

Checkbutton 就像开头一样,支持两个值:开头打开和关闭的值。因此,在创建 Checkbutton 时可同时设置 onvalue 和 offvalue 选项为打开和关闭分别指定值。如果不指定 onvalue 和 offvalue,则 onvalue 默认为1,offvalue 默认为 0。下面代码使用两组 Checkbutton 示例 Checkbutton 的用法。

from tkinter import *
from tkinter import ttk
from tkinter import messagebox

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 创建一个 Label 组件
        ttk.Label(self.master, text='选择你喜欢的人物:').\
            pack(fill=BOTH, expand=YES)
        self.chars = []
        # 定义元组
        characters = ('赵子龙', '关羽', '张飞', '曹操')
        # 采用循环创建多个 Checkbutton
        for each in characters:
            intVar = IntVar()
            self.chars.append(intVar)
            cb = ttk.Checkbutton(self.master,
                                 text=each,
                                 variable=intVar, # 将Checkbutton绑定到intVar变量
                                 command=self.change)   # 将选中事件绑定到 self.change 方法
            cb.pack(anchor=W)
        # 创建一个 Label 组件
        ttk.Label(self.master, text='选择你喜欢的图书:').\
            pack(fill=BOTH, expand=YES)
        # ------------- 下面是第组 Checkbutton ---------------
        self.books = []
        # 定义两个元组
        books = ('Python 基础入门', 'Linux 服务器搭建篇', 'Java 编程思想', 'JavaScript 精通')
        vals = ('python', 'linux', 'java', 'javascript')
        i = 0
        # 采用循环创建多个 Checkbutton
        for book in books:
            strVar = StringVar()
            self.books.append(strVar)
            cb = ttk.Checkbutton(self.master,
                                 text=book,
                                 variable=strVar, # 将 Checkbutton 绑定到 strVar 变量
                                 onvalue=vals[i],
                                 offvalue='无',
                                 command=self.books_change) # 将选中事件绑定到 books_change 方法
            cb.pack(anchor=W)
            i += 1
    def change(self):
        # 将 self.chars 列表转换成元素为 str 的列表
        new_li = [str(e.get()) for e in self.chars]
        # 将 new_li 列表连接成字符串
        st = ', '.join(new_li)
        messagebox.showinfo(title=None, message=st)

    def books_change(self):
        # 将 self.books 列表转换成元素为 str 的列表
        new_li = [e.get() for e in self.books]
        # 将 new_li 列表连接成字符串
        st = ', '.join(new_li)
        messagebox.showinfo(title=None, message=st)

root = Tk()
root.title("Checkbutton 测试")
# 改变窗口图标
root.iconbitmap('images/logo.ico')
App(root)
root.mainloop()

  上面代码中第21~24行创建的第一组 Checkbutton 没有指定 onvalue 和 offvalue,因此 onvalue 和 offvalue 默认分别是1、0,所以将这组 Checkbutton 绑定到 IntVar 类型的变量;第39~44行代码为第二组 Checkbutton 的 onvalue 和 offvalue 都指定为字符串,因此将这组 Checkbutton 绑定到 StringVar 类型的变量。运行这段代码,选中部分选项后可以看到如图二十所示的效果。

图二十  Checkbutton组件

 

6、 Listbox 和 Combobox 组件

Listbox 是一个列表框,通过列表框可选择一个列表项。ttk 下的 Combobox 是 Listbox 的改进版,不仅支持单行文本输入,还提供下拉列表框供选择,因此称为复合框。

要创建 Listbox 需要经过两步:

(1)、创建 Listbox 对象,并且添加各种执行选项。该对象除了支持 GUI 的大部分通用选项外,还支持 selectmode 选项,用于设置 Listbox 的选择模式。

(2)、调用 Listbox 的 insert(self, index, *elements) 方法来添加选项。该方法的最后一个参数是可变长度的参数,所以一次可以添加一个选项,也可传入多个参数来添加多个选项。index 参数是指定插入位置,支持 END(结尾处)、ANCHOR(当前位置)和 ACTIVE(选中处)等特殊索引。

其中 Listbox 的 selectmode 支持的选择模式有如下几种:

(1)、'browse':单选模式,支持按住鼠标键拖动来改变选择。

(2)、'multiple':多选模式。

(3)、'single':单选模式,必须通过鼠标键单击来改变选择。

(4)、'extended':扩展的多选模式,必须通过 Ctrl 或 Shift 键辅助实现多选。

Listbox 的基本用法示例如下:

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        topF = Frame(self.master)
        topF.pack(fill=Y, expand=YES)
        # 创建 Listbox 组件
        self.lb = Listbox(topF)
        self.lb.pack(side=LEFT, fill=Y, expand=YES)
        for item in ['Python', 'Linux', 'HTML', 'Java']:
            self.lb.insert(END, item)
        # 也可直接插入多个选项
        self.lb.insert(ANCHOR, 'Python', 'Linux', 'HTML', 'Java')
        # 创建 Scrollbar 组件,设置该组件与 self.lb 的垂直滚动关联
        scroll = Scrollbar(topF, command=self.lb.yview)
        scroll.pack(side=RIGHT, fill=Y)
        # 设置 self.lb 的垂直滚动影响 scroll 滚动条
        self.lb.configure(yscrollcommand=scroll.set)
        f = Frame(self.master)
        f.pack()
        Label(f, text='选择模式:').pack(side=LEFT)
        modes = ('multiple', 'browse', 'single','extended')
        self.strVar = StringVar()
        for m in modes:
            rb = ttk.Radiobutton(f, text=m, value=m,
                variable=self.strVar, command=self.choose_mode)
            rb.pack(side=LEFT)
        self.strVar.set('browse')

    def choose_mode(self):
        print(self.strVar.get())
        self.lb['selectmode'] = self.strVar.get()

root = Tk()
root.title('Listbox 测试')
# 改变窗口图标
root.iconbitmap('images/logo.ico')
App(root)
root.mainloop()

  上面代码中第15、16行使用循环来插入多个选项;第18行直接插入多个选项。第37行代码根据用户选择来改变 Listbox 的 selectmode 选项,这样可看到 Listbox 不同选项的差异。运行这段代码,可以看到图二十一所示的界面。

图二十一   Listbox 的运行效果

Listbox 除了 insert() 方法外,还支持操作列表项的方法:

(1)、selection_set(self,first,last=None):选中从first到last(包含)的所有列表项。不指定last,则直接选中first列表项。

(2)、selection_clear(self, first, last=None):取消选中从first到last的所有列表项。不指定last则只取消选中first列表项。

(3)、delete(self,first,last=None):删除从first到last的所有列表项。不指定last则只删除first列表项。

Listbox 使用 listvariable 选项与变量进行绑定,用于控制 Listbox 包含哪些项。如果使用 listvariable 选项与变量进行了双向绑定,则无须调用 insert()、delete()方法添加、删除列表项,只要通过绑定变量即可改变 Listbox 中的列表项。关于操作Listbox 中选项的方法如下所示:

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        topF = Frame(self.master)
        topF.pack(fill=Y, expand=YES)
        # 定义 StringVar 变量
        self.v = StringVar()
        # 创建 Listbox 组件, 与 v 变量绑定
        self.lb = Listbox(topF, listvariable=self.v)
        self.lb.pack(side=LEFT, fill=Y, expand=YES)
        for item in range(20):
            self.lb.insert(END, str(item))
        # 创建 Scrollbar 组件,设置组件与 self.lb 的垂直滚动关联
        scroll = Scrollbar(topF, command=self.lb.yview)
        scroll.pack(side=RIGHT, fill=Y)
        # 设置 self.lb 的垂直滚动影响 scroll 滚动条
        self.lb.configure(yscrollcommand=scroll.set)
        f = Frame(self.master)
        f.pack()
        Button(f, text='选中10项', command=self.select).pack(side=LEFT)
        Button(f, text='清除选中3项', command=self.clear_select).pack(side=LEFT)
        Button(f, text='删除3项', command=self.delete).pack(side=LEFT)
        Button(f, text='绑定变量', command=self.var_select).pack(side=LEFT)

    def select(self):
        # 选中指定项
        self.lb.selection_set(0, 9)
    def clear_select(self):
        # 取消选中指定项
        self.lb.selection_clear(1, 3)
    def delete(self):
        # 删除指定项
        self.lb.delete(5, 8)
    def var_select(self):
        # 修改与 Listbox 绑定的变量
        self.v.set(('12', '15'))

root = Tk()
root.title('Listbox测试')
# 改变窗口图标
root.iconbitmap('images/logo.ico')
App(root)
root.mainloop()

  上面代码中第33行控制选中列表项中第一个到第十选项;第36行控制取消选中列表项中的3项;第39行控制删除列表项的4项;第42行代码通过绑定变量来改变 Listbox 中的列表项。运行这段代码,删除其中5~8的4项后的运行效果如图二十二所示。

图二十二  操作列表项

Listbox 的 curselection() 方法可获取当前选中的项,该方法返回一个包含当前 Listbox 的所有选中的项。

Listbox 不能使用 command 选项来绑定事件处理函数或方法,但是可以通过 bind() 方法来实现。示例代码如下:

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        topF = Frame(self.master)
        topF.pack(fill=Y, expand=YES)
        # 定义 StringVar 变量
        self.v = StringVar()
        # 创建 Listbox 组件, 与 v 变量绑定
        self.lb = Listbox(topF, listvariable=self.v)
        self.lb.pack(side=LEFT, fill=Y, expand=YES)
        for item in range(20):
            self.lb.insert(END, str(item))
        # 创建 Scrollbar 组件,设置组件与 self.lb 的垂直滚动关联
        scroll = Scrollbar(topF, command=self.lb.yview)
        scroll.pack(side=RIGHT, fill=Y)
        # 设置 self.lb 的垂直滚动影响 scroll 滚动条
        self.lb.configure(yscrollcommand=scroll.set)
        # 为双击事件绑定事件处理方法
        self.lb.bind("<Double-1>", self.click)

    def click(self, event):
        from tkinter import messagebox
        # 获取 Listbox 当前选中项
        messagebox.showinfo(title=None, message=str(self.lb.curselection()))

root = Tk()
root.title('Listbox测试')
# 改变窗口图标
root.iconbitmap('images/logo.ico')
App(root)
root.mainloop()

  上面代码中第25行为 Listbox 的左键双击事件()绑定了事件处理方法;当双击 Listbox时,就会触发该对象的 click 方法;第30行代码则调用了 Listbox 的 curselection() 方法来获取当前选中项。运行这段代码,双击某个列表项,可看到如图二十三所示的界面。

图二十三   为双击事件绑定事件处理方法

Combobox 的用法很简单,支持的选项如下:

(1)、values选项:用于设置多个选项,例如值可以是列表或元素等。

(2)、state选项:支持 'readonly' 状态,表示 Combobox 的文本框不允许编辑,只能通过下拉列表框的列表项来改变。

(3)、textvariable选项:可与指定变量绑定后,通过该变量来获取或修改 Combobox 组件的值。

(4)、postcommand选项:用于指定事件处理函数或方法;当单击 Combobox 的下拉箭头时,就会触发 postcommand 选项指定的事件处理函数或方法。

关于 Combobox 组件的用法示例如下:

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        self.strVar = StringVar()
        # 创建 Combobox 组件
        self.cb = ttk.Combobox(self.master,
            textvariable=self.strVar, # 绑定到 self.strVar 变量
            postcommand=self.choose)    # 当用户单击下拉箭头时触发 self.choose 方法
        self.cb.pack(side=TOP)
        # 为 Combobox 配置多个选项
        self.cb['values'] = ('Python', 'HTML', 'Linux', 'Java')
        f = Frame(self.master)
        f.pack()
        self.isreadonly = IntVar()
        # 创建 Checkbutton,绑定到 self.isreadonly 变量
        Checkbutton(f, text='是否只读:',
                    variable=self.isreadonly,
                    command=self.change).pack(side=LEFT)
        # 创建 Button,单击该按钮时触发 setvalue 方法
        Button(f, text='绑定变量设置',
               command=self.setvalue).pack(side=LEFT)

    def choose(self):
        from tkinter import messagebox
        # 获取 Combobox 的当前值
        messagebox.showinfo(title=None, message=str(self.cb.get()))
    def change(self):
        self.cb['state'] = 'readonly' if self.isreadonly.get() else 'enable'
    def setvalue(self):
        self.strVar.set('人生苦短,我用Python')

root = Tk()
root.title('Combobox测试')
# 改变窗口图标
root.iconbitmap('images/logo.ico')
App(root)
root.mainloop()

  上面代码的第13行将 Combobox 组件绑定到 self.strVar 变量;第14行代码为 Combobox 的 command 绑定事件处理方法。第34行代码根据列表的值来确定 Combobox 是否允许编辑。运行这段代码,可看到图二十四所示的界面。

图二十四  Combobox 组件

 

7、 Spinbox 组件

该组件是有两个小箭头文本框,两个小箭头可上下调整该组件内的值,也可在文本框内输入内容作为该组件的值。

Spinbox 本质是一个列表框,类似于 Combobox,但 Spinbox 不会展开下拉列表供选择,只能用上下小箭头选择不同的项。

Spinbox 的选项列表:可通过from(由于是关键字,要写成from_)、to、increment选项来指定选项列表,也可通过 values 选项来指定多个列表项,该选项的值可以是 list 或 tuple。

Spinbox 的 textvariable 选项与指定变量绑定时,可通过该变量来获取或修改 Spinbox 组件的值。

Spinbox 的 command 选项用于指定事件处理函数或方法;当点击 Spinbox 的向上、向下箭头时,就会触发 command 选项指定的事件处理函数或方法。

Spinbox 的用法示例如下所示:

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        ttk.Label(self.master, text='指定from、to、increment').pack()
        #通过指定 from、to、increament 选项创建 Spinbox
        sb1 = Spinbox(self.master, from_=20,
                      to=100, increment=5)
        sb1.pack(fill=X, expand=YES)
        ttk.Label(self.master, text='指定values').pack()
        # 通过指定values 选项创建 Spinbox
        self.sb2 = Spinbox(self.master,
                values=('Python', 'HTML', 'Linux', 'Java'),
                command=self.press) # 通过 command 绑定事件处理方法
        self.sb2.pack(fill=X, expand=YES)
        ttk.Label(self.master, text='绑定变量').pack()
        self.intVar = IntVar()
        # 通过指定 values 选项创建 Spinbox,并为之绑定变量
        sb3 = Spinbox(self.master,
                      values=list(range(20, 100, 4)),
                      textvariable=self.intVar, # 绑定变量
                      command=self.press)
        sb3.pack(fill=X, expand=YES)
        self.intVar.set(33)     # 通过变量愀 Spinbox 的值

    def press(self):
        print(self.sb2.get())

root = Tk()
root.title('Combobox测试')
# 改变窗口图标
root.iconbitmap('images/logo.ico')
App(root)
root.mainloop()

  上面代码中第12~13行代码使用 from_、to、increment 选项创建了 Spinbox 组件;第17~19代码使用 values 选项创建了 Spinbox 组件,并为该组件的 command 选项指定了事件处理方法,在点击 Spinbox 组件的上下箭头调整值时,该选项指定的事件处理方法就会被触发;第24~27行代码在创建 Spinbox 时使用 textvariable 选项绑定变量,这样在程序中完全可通过绑定变量来访或修改 Spinbox 组件的值。运行这段代码,可看到如图二十五所示的界面。

图二十五   Spinbox组件

 

8、 Scale 和 LabeledScale 组件

Scale 组件是一个滑动条,可为滑动条设置最小值和最大值,也可设置滑动条每次调节的步长。支持的选项有下面这些:

(1)、from:设置最小值;

(2)、to:设置最大值;

(3)、resolution:设置滑动时的步长。

(4)、label:为 Scale 组件设置标签内容。

(5)、length:设置轨道的长度。

(6)、width:设置轨道的宽度。

(7)、troughcolor:设置轨道的背景色。

(8)、sliderlength:设置尚志的长度。

(9)、sliderrelief:设置滑块的立体样式。

(10)、showvalue:设置是否显示当前值。

(11)、orient:设置方向,该选项支持 VERTICAL 和 HORIZONTAL 两个值。

(12)、digits:设置有效数字至少要有几位。

(13)、variable:用于与变量进行绑定。

(14)、command:为 Scale 组件绑定事件处理函数或方法。

注意:ttk.Scale 的组件更接近操作系统本地的效果,但可支持的选项少。

关于 Scale 组件的功能选项和用法示例如下:

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        self.scale = Scale(self.master,
                           from_=-100,  # 设置最小值
                           to=100,      # 设置最大值
                           resolution=5, # 设置步长
                           label='Scale 示例',  # 设置标签内容
                           length=400,  # 设置轨道的长度
                           width=30,    # 设置轨道的宽度
                           troughcolor='lightblue', # 设置轨道的背景色
                           sliderlength=20, # 设置滑块的长度
                           sliderrelief=SUNKEN, # 设置滑块的立体样式
                           showvalue=YES,    # 设置显示当前值
                           orient=HORIZONTAL    # 设置水平方向
                           )
        self.scale.pack()
        # 创建一个 Frame 作为容器
        f = Frame(self.master)
        f.pack(fill=X, expand=YES, padx=10)
        Label(f, text='是否显示值:').pack(side=LEFT)
        i = 0
        self.showVar = IntVar()
        self.showVar.set(1)
        # 创建两个 Radiobutton 控制 scale 是否显示值
        for s in ('不显示', '显示'):
            Radiobutton(f, text=s, value=i, variable=self.showVar,
                        command=self.switch_show).pack(side=LEFT)
            i += 1
        # 创建一个 Frame 作为容器
        f2 = Frame(self.master)
        f2.pack(fill=X, expand=YES, padx=10)
        Label(f2, text='方向:').pack(side=LEFT)
        j = 0
        self.orientVar = IntVar()
        self.orientVar.set(0)
        # 创建两个 Radiobutton 控制 Scale 的方向
        for s in ('水平', '垂直'):
            Radiobutton(f2, text=s, value=j, variable=self.orientVar,
                        command=self.switch_orient).pack(side=LEFT)
            j += 1

    def switch_show(self):
        self.scale['showvalue'] = self.showVar.get()
    def switch_orient(self):
        # 根据单选钮的选中状态设置 orient 选项的值
        self.scale['orient'] = VERTICAL if self.orientVar.get() else HORIZONTAL

root = Tk()
root.title('Scale 测试')
App(root)
root.mainloop()

  上面代码中第10~22行创建了 Scale 组件,并且为该组件指定前面介绍的多个选项。第50行代码根据单选钮的选中状态来设置 Scale 组件的 showvalue 选项,该选项会控制 Scale 组件是否显示当前值;第53行代码根据单选钮的选中状态设置 Scale 是水平的还是垂直的,通过改变 orient 选项值为设置。代码运行效果如图二十六所示。

图二十六   Scale组件运行效果

Scale 也支持 variable 进行变量绑定,也支持使用 command 选项绑定事件处理函数或方法,这样在点击滑动条的滑块时,就会触发 command 绑定的事件处理方法,Scale 的事件处理方法还可能额外定义一个参数,用于获取 Scale 的当前值。示例如下:

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 定义变量
        self.doubleVar = DoubleVar()
        self.scale = Scale(self.master,
                           from_=-100,  # 设置最小值
                           to=100,      # 设置最大值
                           resolution=5, # 设置步长
                           label='Scale 示例',  # 设置标签内容
                           length=300,  # 设置轨道的长度
                           width=15,    # 设置轨道的宽度
                           orient=HORIZONTAL,    # 设置水平方向
                           digits=10,   # 设置10位有效数字
                           command=self.change, # 绑定事件处理方法
                           variable=self.doubleVar  # 绑定变量
                           )
        self.scale.pack()
        # 设置 Scalie 的当前值
        self.scale.set(20)
    # Scale 的事件处理方法可以接收到 Scale 的值
    def change(self, value):
        print(value, self.scale.get(), self.doubleVar.get())

root = Tk()
root.title('Scale 测试')
App(root)
root.mainloop()

  上面代码中,第29行使用了三种方式来获取 Scale 组件的值。分别是:

(1)、通过事件处理方法的参数来获取。

(2)、通过 Scale 组件提供的 get() 方法来获取。

(3)、通过 Scale 组件绑定的变量来获取。

这三种方式获取到的变量值都是一样的,但是 Scale 组件的 digits 选项为10,表示有效数字保留10位,所以在通过事件处理方法获取的值会有 10 位有效数字,如:-10.0000000。

ttk.LabeledScale 是平台化的滑动条,允许设置的选项少,可设置 from、to、compound 等几项,生成一是一个水平滑动条(不能变成垂直的),compound 用于控制滑动条的数值标签是显示在滑动条上方或下方。示例代码如下:

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        self.scale = ttk.LabeledScale(self.master,
                    from_=-100,     # 设置最小值
                    to=100,         # 设置最大值
                    compound=BOTTOM # 设置显示数值的标签在下方
                    )
        self.scale.value = -20
        self.scale.pack(fill=X, expand=YES)

root = Tk()
root.title('LabeledScale 测试')
App(root)
root.mainloop()

  上面代码中第10~14行创建了一个 LabeledScale 组件,该组件生成一个水平滑动条,默认数值标签显示在滑动条上方,通过compound 选项设置为BOTTOM,让数值标签显示在滑动条的下方。

 

9、Labelframe 组件

Labelframe 是 Frame 容器的改进版,它允许为容器添加一个标签,该标签可以是普通的文字,也可以是任意的GUI组件。

ttk.Labelframe 与 ttk.LabelFrame(注意 f 大小写)是完全相同的,出现两上是因为与 tkinter.LabelFrame 保持名字上的兼容。

Labelframe 要设置文字标签,只要为它指定 text 选项即可。关于 Labelframe 组件的用法示例如下:

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 创建 Labelframe 容器
        lf = ttk.Labelframe(self.master, text="请选择图书", padding=20)
        lf.pack(fill=BOTH, expand=YES, padx=10, pady=10)
        books = ['Python', 'HTML', 'Linux', 'Java']
        i = 0
        self.intVar = IntVar()
        # 使用循环创建多个 Radiobutton ,并放入 Labelframe 中
        for book in books:
            Radiobutton(lf, text='高级' + book + '编程',
                        value=i,
                        variable=self.intVar, command=self.click).pack(side=LEFT)
            i += 1
    def click(self):
        print(self.intVar.get())

root = Tk()
root.title('Labelframe 测试')
# 改变窗口图标
root.iconbitmap('images/logo.ico')
App(root)
root.mainloop()

  上面代码第11行创建一个 Labelframe 组件,并指定了 text 选项,该选项会作为容器的标签。接下来向 Labelframe 容器中添加4个 Radibutton。运行这段代码可看到如图二十七所示的界面。

图二十七  Labelframe组件

Labelframe 对标签定制的选项有:

(1)、labelwidget:设置任意GUI组件作为标签。

(2)、labelanchor:设置标签的位置。选项值是有12个,分别有用于控制标签位置。这12个选项如下:'e', 's', 'w', 'n', 'es', 'ws', 'en', 'wn', 'ne', 'nw', 'se', 'sw'。

对 Labelframe 标签进行定制的代码如下:

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 创建 Labelframe 容器
        self.lf = ttk.Labelframe(self.master, padding=20)
        self.lf.pack(fill=BOTH, expand=YES, padx=10, pady=10)
        # 创建一个显示图片的 Label
        bm = PhotoImage(file='images/z.png')
        lb = Label(self.lf, image=bm)
        lb.bm = bm
        # 将 Labelframe 的标题设为显示图片的 Label
        self.lf['labelwidget'] = lb
        # 定义代表 Labelframe 的标题位置的 12 个常量
        self.books = ['e', 's', 'w', 'n', 'es', 'ws', 'en', 'wn', 'ne', 'nw', 'se', 'sw']
        i = 0
        self.intVar = IntVar()
        # 使用循环创建多个 Radiobutton,并放入 Labelframe 中
        for book in self.books:
            Radiobutton(self.lf, text=book, value=i, command=self.change,
                        variable=self.intVar).pack(side=LEFT)
            i += 1
        self.intVar.set(9)

    def change(self):
        # 通过 labelanchor 选项改变 Labelframe 的标签的位置
        self.lf['labelanchor'] = self.books[self.intVar.get()]

root = Tk()
root.title('Labelframe 测试')
# 改变窗口图标
root.iconbitmap('images/logo.ico')
App(root)
root.mainloop()

  上面代码中第18行通过 labelwidget 选项定制了该 labelframe 的标签,选项值是图片的 Label,因此该 Labelframe 的标签就是一张图片。第32行代码根据单选钮的选中状态来调整 Labelframe 的标签位置。这段代码的运行效果如图二十八所示。

图二十八   定制Labelframe的标签

 

10、 Panedwindow 组件

Panedwindow 是一个管理窗口布局的容器,可添加多个子组件(不需要 pack、grid、place布局),为每个子组件划分一个区域,用户可用鼠标移动各区域的分隔线来改变各子组件的大小(不指定大小,则默认占满整个区域)。

ttk组件下的 ttk.Panedwindow 与 ttk.PanedWindow 功能是完全一样的,主要是为了与 tkinter.PanedWindow 兼容。

Panedwindow 是一个有特色的容器,自带布局,可通过 orient 选项指定水平或垂直方向,让容器中的各组件按水平或垂直排列。

在创建 Panedwindow 容器后,可操作子组件的方法有下面几个:

(1)、add(self, child, **kw):添加一个子组件。 (2)、insert(self, pos, child, **kw):在 pos 位置插入一个子组件。 (3)、remove(self, child):删除一个子组件,该子组件所在区域也被删除。

关于 Panedwindow 的添加、插入、删除子组件的示例如下所示:

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 创建 Style
        style = ttk.Style()
        style.configure("Py.TPanedwindow", background='darkgray', relief=RAISED)
        # 创建 Panedwindow 组件,通过 Style 属性设置分隔线
        pwindow = ttk.PanedWindow(self.master,
                    orient=VERTICAL, style="Py.TPanedwindow")
        pwindow.pack(fill=BOTH, expand=1)
        first = ttk.Label(pwindow, text='第一个标签')
        # 调用 add 方法添加组件,每个组件占一个区域
        pwindow.add(first)
        okBn = ttk.Button(pwindow, text="第二个按钮",
                # 调用 remove() 方法删除组件,该组件所在区域消失
                command=lambda : pwindow.remove(okBn))
        # 调用 add 方法添加组件,每个组件占一个区域
        pwindow.add(okBn)
        entry = ttk.Entry(pwindow, width=30)
        # 调用 add 方法添加组件,每个组件占一个区域
        pwindow.add(entry)
        # 调用 insert 方法插入组件
        pwindow.insert(1, Label(pwindow, text='插入的标签'))

root = Tk()
root.title('Labelframe 测试')
# 改变窗口图标
root.iconbitmap('images/logo.ico')
App(root)
root.mainloop()

  上面代码中第11、12行创建了 ttk.Style 对象,该对象用于管理 ttk 组件的样式,这样 ttk 组件可通过 style 选项复用 ttk.Style 管理的样式。这里使用 ttk.Style 为 ttk.Panedwindow 指定样式后才能看到 ttk.Panedwindow 容器内的分隔线(默认看不见)。

第19行代码调用 add() 方法为 Panedwindow 容器添加子组件;第22行代码调用了 remove() 方法删除 Panedwindow 容器中的子组件;第29行代码调用了 insert() 方法向 Panedwindow 容器中添加了子组件。

运行上面这段代码会看到如图二十九所示的界面。在图二十九中单击【1图】的【第二个按钮】将会删除 Panedwindow 组件中的该按钮,该按钮所占的区域也随之消失,此时会看到【2图】所示的界面。

图二十九  Panedwindow 组件示例

Panedwindow 组件只能水平和垂直排列,显得有些局限。但是 Panedwindow 组件是可以嵌套的,可以实现功能丰富的界面。下面代码在水平 Panedwindow 中嵌套垂直 Panedwindow。

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 创建 Style
        style = ttk.Style()
        style.configure("Py.TPanedwindow", background='red', relief=RAISED)
        # 创建第一个 Panedwindow 组件,通过 style 属性设置分隔线
        pwindow = ttk.PanedWindow(self.master,
                orient=HORIZONTAL, style="Py.TPanedwindow")
        pwindow.pack(fill=BOTH, expand=YES)
        left = ttk.Label(pwindow, text="左边标签", background='pink')
        pwindow.add(left)
        # 创建第二个 Panedwindow 组件,该组件的方向是垂直的
        rightwindow = PanedWindow(pwindow, orient=VERTICAL)
        pwindow.add(rightwindow)
        top = Label(rightwindow, text='右上标签', background='lightgreen')
        rightwindow.add(top)
        bottom = Label(rightwindow, text="右下标签", background='lightblue')
        rightwindow.add(bottom)

root = Tk()
root.title('Panedwindow 测试')
# 改变窗口图标
root.iconbitmap('images/logo.ico')
App(root)
root.mainloop()

  上面代码中第14、15行创建了一个水平分布的 Panedwindow 容器,在该容器中先添加一个 Label 组件;第20行代码创建一个垂直分布的 Panedwindow容器,该容器被添加到第一个 Panedwindow 容器中,这样就形成了嵌套,从而实现功能更丰富的界面。运行结果界面如图三十所示。

图三十  Panedwindow嵌套

 

11、OptionMenu 组件

该组件可构建一个带菜单的按钮,该菜单可在按钮的四个方向上展开,展开方向可通过 direction 选项控制。要使用 OptionMenu,只要调用它的如下构造函数即可: __init__(self, master, variale, value, *values, **kwargs)

其中,master 参数的作用与 Tkinter 组件一样,指定将该组件放入哪个容器中。其它参数含义如下:

(1)、variable:指定该按钮上的菜单与哪个变量绑定。

(2)、value:指定默认选择菜单中的哪一项。

(3)、values:Tkinter 将收集为此参数传入的多个值,为每个值创建一个菜单项。

(4)、kwargs:用于为 OptionMenu 配置选项。除了前面几个选项外,还可通过 direction 选项控制菜单的展开方向。

关于 OptionMenu 的示例如下,示例中通过单选钮控制 OptionMenu 中菜单的展开方向。

from tkinter import *
from tkinter import ttk

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        self.sv = StringVar()
        # 创建一个 OptionMenu 组件
        self.om = ttk.OptionMenu(self.master,
                    self.sv, # 绑定变量
                    'Python', # 设置初始选中值
                    'Kotlin', # 以下多个值用于设置菜单项
                    'HTML',
                    'Linux',
                    'Java',
                    'Python',
                    'JavaScript',
                    command=self.print_option)  # 绑定事件处理方法
        self.om.pack()
        # 创建 Labelframe 容器
        lf = ttk.Labelframe(self.master, padding=20, text='请选择菜单方向')
        lf.pack(fill=BOTH, expand=YES, padx=10, pady=10)
        # 定义代表 Labelframe 的标签位置的 12 个常量
        self.directions = ['below', 'above', 'left', 'right', 'flush']
        i = 0
        self.intVar = IntVar()
        # 使用循环创建多个 Radiobutton,并放入 Labelframe 中
        for direct in self.directions:
            Radiobutton(lf, text=direct,
                        value=i,
                        command=self.change,
                        variable=self.intVar).pack(side=LEFT)
            i += 1
        self.intVar.set(9)

    def print_option(self, val):
        # 通过两个各方来获取 OptionMenu 选中的菜单项的值
        print(self.sv.get(), val)

    def change(self):
        # 通过 direction 选项改变 OptionMenu 中菜单的展开方向
        self.om['direction'] = self.directions[self.intVar.get()]

root = Tk()
root.title('OptionMenu 测试')
# 改变窗口图标
root.iconbitmap('images/logo.ico')
App(root)
root.mainloop()

  上面代码中第21行代码为 OptionMenu 的 command 选项绑定了 self.print_option 方法,这样在选择菜单中的不同菜单项时,都会触发 self.print_option 方法。该事件处理方法可以额外指定一个参数来获取目标菜单项上的值,例如 self.print_option 方法的定义所示。

第45行代码通过动态改变 OptionMenu 的 direction 选项值,可以动态改变按钮上菜单的展开方向。运行这段代码,选中下方的“left”单选钮,可看到如图三十一所示的界面。

图三十一  OptionMenu组件

 

六、对话框(Dialog)

 

对话框用于向用户生成某种提示信息,或者请求用户输入某些简单的信息。对话框有点类似于顶级窗口,但有下面两点需要注意:

(1)、对话框要依赖其它窗口,在创建对话框时需要指定 master 属性。

(2)、对话框有非模式(non-modal)和模式(modal)两种,当某个模式对话框被打开后,该模式对话框总是位于它依赖的窗口之上;在模式对话框被关闭之前,它依赖的窗口无法获得焦点。

 

1、普通对话框

Tkinter 在 simpledialog 和 dialog 模块下分别提供了 SimpleDialog 类和 Dialog 类,它们都可作为普通对话模框使用,使用方法相差不大。使用 simpledialog.SimpleDialog 创建对话框时,可指定的选项如下:

(1)、title:指定该对话框的标题。

(2)、text:指定该对话框的内容。

(3)、button:指定该对话框下方的几个按钮。

(4)、default:指定该对话框中默认第几个按钮得到焦点。

(5)、canncel:指定当用户通过对话框右上角的 X 按钮关闭对话框时,该对话框的返回值。

使用 dialog.Dialog 创建对话框,除可用 master 指定对话框的属主窗口外,还可通过 dict 来指定下面的选项:

(1)、title:指定该对话框的标题。

(2)、text:指定对话框的内容。

(3)、strings:指定该对话框下方的几个按钮。

(4)、default:指定该对话框中默认第几个按钮得到焦点。

(5)、bitmap:指定该对话框上的图标。

由上可知,simpledialog.SimpleDialog 和 dialog.Dialog 所支持的选项大同小异,区别只是 dialog.Dialog 需要使用 dict 来传入多个选项。下面代码分别使用 SimpleDialog 和 Dialog 来创建对话框。

from tkinter import *
from tkinter import ttk
from tkinter import simpledialog
from tkinter import dialog

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        self.msg = '《Python应用前景》,Python发展至今,应用的领域众多,'+\
            '目前最火的人工智能,云计算、大数据分析等领域都在使用Python做为基本工具。'+\
            '在最新的编程语言排行榜中,Python已经超越了C++,成为热门编程语言第三名。'
        # 创建两个按钮,并为之绑定事件处理方法
        ttk.Button(self.master, text='打开SimpleDialog',
                   command=self.open_simpledialog # 绑定 open_simpledialog 方法
                   ).pack(side=LEFT, ipadx=5, ipady=5, padx=10)
        ttk.Button(self.master, text='打开Dialog',
                   command=self.open_dialog # 绑定 open_dialog 方法
                   ).pack(side=LEFT, ipadx=5, ipady=5, padx=10)

    def open_simpledialog(self):
        # 使用 simpledialog.SimpleDialog 创建对话框
        d = simpledialog.SimpleDialog(self.master,  # 设置该对话框所属的主窗口
            title='SimpleDialog测试', # 标题
            text=self.msg,  # 内容
            buttons=["是", "否", "取消"],
            cancel=3,
            default=0   # 设置默认哪个按钮得到焦点
            )
        print(d.go())   # 通过 go() 方法获取返回值,即点击的是哪个按钮

    def open_dialog(self):
        # 使用 dialog.Dialog 创建对话框
        d = dialog.Dialog(self.master,  # 设置该对话框所属的窗口
            {'title': 'Dialog测试',   # 标题
             'text': self.msg,        # 内容
             'bitmap': 'question',    # 图标
             'default': 0,            # 设置默认选中项
             # strings 选项用于设置按钮
             'strings': ('确定', '取消', '退出')})
        print(d.num)    # 通过 num 属性获取返回值,即点击的是哪个按钮

root = Tk()
root.title("对话框测试")
App(root)
root.mainloop()

  在上面代码中第25行使用 simpledialog.SimpleDialog 创建了对话框;第二行粗体字代码使用 dialog.Dialog 创建了对话框。对比这两行代码可知,创建两个对话框的代码相似,区别只是创建 dialog.Dialog 时需要使用 dict 传入选项。运行这段代码,单击图三十二界面上【1图】的“打开SimpleDialog”按钮可看到【2图】的效果,单击【1图】的“打开Dialog”按钮,可以看到【3图】的效果。

图三十二  SimpleDialog和Dialog对话框

在【3图】对话框的左边还显示了一个问号图标,这是Python 内置的 10 个位置之一,可以直接使用。共有如下几个常量可用于设置位置:

(1)、"error"

(2)、"gray75"

(3)、"gray50"

(4)、"gray25"

(5)、"gray12"

(6)、"hourglass"

(7)、"info"

(8)、"questhead"

(9)、"question"

(10)、"warning"

 

2、自定义模式、非模式对话框

可通过继承 Toplevel类实现定制模式和非模式行为的对话框。但是有两点注意事项:

(1)、继承 Toplevel 实现自定义对话框同样需要为对话框指定 master。

(2)、可调用 Toplevel 的 grab_set() 方法让该对话框变成模式对话框,否则就是非模式对话框。

关于自定义模式和非模式对话框的示例如下:

from tkinter import *
from tkinter import ttk
from tkinter import messagebox

# 自定义对话框类,继承 Toplevel
class MyDialog(Toplevel):
    # 定义构造方法
    def __init__(self, parent, title=None, modal=True):
        Toplevel.__init__(self, parent)
        self.transient(parent)
        # 设置标题
        if title: self.title(title)
        self.parent = parent
        self.result = None
        # 创建对话框的主体内容
        frame = Frame(self)
        # 调用 init_widgets 方法来初始化对话框界面
        self.initial_focus = self.init_widgets(frame)
        frame.pack(padx=5, pady=5)
        # 调用 init_buttons 方法初始化对话框下方的按钮
        self.init_buttons()
        # 根据 modal 选项设置是否为模式对话框
        if modal: self.grab_set()
        if not self.initial_focus:
            self.initial_focus = self
        # 为 WM_DELETE_WINDOW 协议使用 self.cancel_click 事件处理方法
        self.protocol("WM_DELETE_WINDOW", self.cancel_click)
        # 根据父窗口来设置对话框的位置
        self.geometry("+%d+%d" % (parent.winfo_rootx()+50,
                      parent.winfo_rooty()+50))
        print(self.initial_focus)
        # 让对话框获得焦点
        self.initial_focus.focus_set()
        self.wait_window(self)

    # 通过该方法来创建自定义对话框的内容
    def init_widgets(self, master):
        # 创建并添加Label
        Label(master, text='用户名', font=12, width=10).grid(row=1, column=0)
        # 创建并添加Entry,用于接受用户输入的用户名
        self.name_entry = Entry(master, font=16)
        self.name_entry.grid(row=1, column=1)
        # 创建并添加Label
        Label(master, text='密  码', font=12, width=10).grid(row=2, column=0)
        # 创建并添加Entry,用于接受用户输入的密码
        self.pass_entry = Entry(master, font=16)
        self.pass_entry.grid(row=2, column=1)

    # 通过该方法来创建对话框下方的按钮框
    def init_buttons(self):
        f = Frame(self)
        # 创建"确定"按钮,位置绑定self.ok_click处理方法
        w = Button(f, text="确定", width=10, command=self.ok_click, default=ACTIVE)
        w.pack(side=LEFT, padx=5, pady=5)
        # 创建"取消"按钮,位置绑定self.cancel_click处理方法
        w = Button(f, text="取消", width=10, command=self.cancel_click)
        w.pack(side=LEFT, padx=5, pady=5)
        self.bind("<Return>", self.ok_click)
        self.bind("<Escape>", self.cancel_click)
        f.pack()

    # 该方法可对用户输入的数据进行校验
    def validate(self):
        # 可重写该方法
        return True

    # 该方法可处理用户输入的数据
    def process_input(self):
        user_name = self.name_entry.get()
        user_passwd = self.pass_entry.get()
        messagebox.showinfo(message='用户名是: %s, 密码是: %s'
            % (user_name , user_passwd))

    def ok_click(self, event=None):
        print('确定')
        # 如果不能通过校验,让用户重新输入
        if not self.validate():
            self.initial_focus.focus_set()
            return
        self.withdraw()
        self.update_idletasks()
        # 获取用户输入数据
        self.process_input()
        # 将焦点返回给父窗口
        self.parent.focus_set()
        # 销毁自己
        self.destroy()

    def cancel_click(self, event=None):
        print('取消')
        # 将焦点返回给父窗口
        self.parent.focus_set()
        # 销毁自己
        self.destroy()

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()
    def initWidgets(self):
        # 创建2个按钮,并为之绑定事件处理函数
        ttk.Button(self.master, text='模式对话框',
            command=self.open_modal # 绑定open_modal方法
            ).pack(side=LEFT, ipadx=5, ipady=5, padx= 10)
        ttk.Button(self.master, text='非模式对话框',
            command=self.open_none_modal # 绑定open_none_modal方法
            ).pack(side=LEFT, ipadx=5, ipady=5, padx= 10)
    def open_modal(self):
        d = MyDialog(self.master, title='模式对话框') # 默认是模式对话框
    def open_none_modal(self):
        d = MyDialog(self.master, title='非模式对话框', modal=False)

root = Tk()
root.title("颜色对话框测试")
App(root)
root.mainloop()

  这里定义了一个父类为 Toplevel 的 MyDialog 类,该类是一个自定义对话框类,以后定义对话框,可以复用这个 MyDialog 类。该对话框的主体包含两个方法:

(1)、init_widgets:初始化对话框的主体界面组件。

(2)、init_buttons:初始化对话框下方的多个按钮组件。

在 MyDialog 类中的 ok_click、cancel_click 是为对话框按钮绑定的事件处理方法。这不是固定的,有几个按钮,就需要为几个按钮绑定事件处理方法。

第109行代码通过 MyDialog 创建模式对话框;第111行粗体字代码通过 MyDialog 创建非模式对话框。

运行这段代码,点击界面上的“模式对话框”按钮后,将弹出自定义对话框。在该对话框没有关闭时,主窗口将不能与用户交互,主窗口也不能获得焦点。如果点击的是“非模式对话框”按钮,也将弹出自定义对话框,由于这个对话框是非模式的,所以即使在该对话框没有关闭时,也可以与主窗口交互,主窗口可以获得焦点。(运行结果省略)

 

3、 输入对话框

在 simpledialog 模块下还有便捷的工具函数,可以方便的生成各种输入对话框。

(1)、askinteger:生成一个输入整数的对话框;

(2)、askfloat:生成一个输入浮点数的对话框;

(3)、askstring:生成一个输入字符串的对话框。

这三个工具函数的前两个参数分别指定对话框的标题和提示信息,后面的选项可以设置对话框的初始值、最大值和最小值。

关于 simpledialog 模块下的这三个工具函数用法示例如下:

from tkinter import *
from tkinter import ttk
from tkinter import simpledialog

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 创建3个按钮,并为之绑定事件处理函数
        ttk.Button(self.master, text='输入整数对话框',
            command=self.open_integer # 绑定open_integer方法
            ).pack(side=LEFT, ipadx=5, ipady=5, padx= 10)
        ttk.Button(self.master, text='输入浮点数对话框',
            command=self.open_float # 绑定open_float方法
            ).pack(side=LEFT, ipadx=5, ipady=5, padx= 10)
        ttk.Button(self.master, text='输入字符串对话框',
            command=self.open_string # 绑定open_string方法
            ).pack(side=LEFT, ipadx=5, ipady=5, padx= 10)
    def open_integer(self):
         # 调用askinteger函数生成一个让用户输入整数的对话框
        print(simpledialog.askinteger("猜数字", "猜一个下期彩票开奖数字:",
            initialvalue=20, minvalue=1, maxvalue=33))
    def open_float(self):
        # 调用askfloat函数生成一个让用户输入浮点数的对话框
        print(simpledialog.askfloat("猜重量", "猜一猜某人体重多少公斤:",
            initialvalue=27.3, minvalue=10, maxvalue=70))
    def open_string(self):
        # 调用askstring函数生成一个让用户输入字符串的对话框
        print(simpledialog.askstring("猜名字", "猜一猜某人叫什么名字:",
            initialvalue='Michael'))

root = Tk()
root.title("输入对话框测试")
App(root)
root.mainloop()

  在这段代码中第23、24行代码生成输入整数的对话框;第27、28行代码生成输入浮点数的对话框;第31、32行代码生成输入字符串的对话框。askinteger()、askfloat() 和 askstring() 这三个函数会返回在对话框中输入的数据,这三个函数的返回值传递给了print() 函数,这样就可以打印出在对话框中输入的内容。如果输入的内容不符合这三个函数的限制规则,就会看到错误提示。(运行结果省略)

 

4、文件对话框

用于生成文件对话框的工具函数由 filedialog 模块提供。这些工具函数有些返回所选择文件的路径,有些直接返回用户所选择文件的输入/输出流。

(1)、askopenfile():生成打开单个文件的对话框,返回所选择文件的文件流,在程序中可通过该文件流来读取文件内容。

(2)、askopenfiles():生成打开多个文件的对话框,返回多个所选择文件的文件流组成的列表,在程序中可通过这些文件流来读取文件内容。

(3)、askopenfilename():生成打开单个文件的对话框,返回所选择文件的文件路径。

(4)、askopenfilenames():生成打开多个文件的对话框,返回多个所选文件的文件路径组成的元组。

(5)、asksaveasfile():生成保存文件的对话框,返回所选择文件的文件输出流,在程序中可通过文件输出流向文件写入数据。

(6)、asksaveasfilename():生成保存文件的对话框,返回所选择文件的文件路径。

(7)、askdirectory():生成打开目录的对话框。

上面这些用于生成打开文件的对话框的工具函数支持如下选项:

(1)、defaultextension:指定默认扩展名。当用户没有输入扩展名时,系统会默认添加选项指定的扩展名。

(2)、filetypes:指定在该文件对话框中能查看的文件类型。该选项值是一个序列,可指定多个文件类型。可用“*”指定浏览全部文件。

(3)、initialdir:指定初始打开的目录。

(4)、initialfile:指定所选择的文件。

(5)、parent:指定该对话框的属主窗口。

(6)、title:指定对话框的标题。

(7)、multiple:指定是否允许多选。

(8)、mustexist:指定是否只允许打开已存在的目录。

关于文件对话框的各工具函数用法示例如下:

from tkinter import *
from tkinter import ttk
from tkinter import filedialog

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 创建7个按钮,并为之绑定事件处理函数
        ttk.Button(self.master, text='打开单个文件',
            command=self.open_file # 绑定open_file方法
            ).pack(side=LEFT, ipadx=5, ipady=5, padx= 10)
        ttk.Button(self.master, text='打开多个文件',
            command=self.open_files # 绑定open_files方法
            ).pack(side=LEFT, ipadx=5, ipady=5, padx= 10)
        ttk.Button(self.master, text='获取单个打开文件的文件名',
            command=self.open_filename # 绑定open_filename方法
            ).pack(side=LEFT, ipadx=5, ipady=5, padx= 10)
        ttk.Button(self.master, text='获取多个打开文件的文件名',
            command=self.open_filenames # 绑定open_filenames方法
            ).pack(side=LEFT, ipadx=5, ipady=5, padx= 10)
        ttk.Button(self.master, text='获取保存文件',
            command=self.save_file # 绑定save_file方法
            ).pack(side=LEFT, ipadx=5, ipady=5, padx= 10)
        ttk.Button(self.master, text='获取保存文件的文件名',
            command=self.save_filename # 绑定save_filename方法
            ).pack(side=LEFT, ipadx=5, ipady=5, padx= 10)
        ttk.Button(self.master, text='打开路径',
            command=self.open_dir # 绑定open_dir方法
            ).pack(side=LEFT, ipadx=5, ipady=5, padx= 10)

    def open_file(self):
        # 调用askopenfile方法获取单个打开的文件,注意返回的是文件流
        print(filedialog.askopenfile(title='打开单个文件',
            filetypes=[("文本文件", "*.txt"), ('Python源文件', '*.py')], # 只处理的文件类型
            initialdir='e:/')) # 初始目录
    def open_files(self):
        # 调用askopenfiles方法获取多个打开的文件,返回的是多个文件流
        print(filedialog.askopenfiles(title='打开多个文件',
            filetypes=[("文本文件", "*.txt"), ('Python源文件', '*.py')], # 只处理的文件类型
            initialdir='e:/')) # 初始目录
    def open_filename(self):
        # 调用askopenfilename方法获取单个文件的完整文件名(路径+文件名)
        print(filedialog.askopenfilename(title='打开单个文件',
            filetypes=[("文本文件", "*.txt"), ('Python源文件', '*.py')], # 只处理的文件类型
            initialdir='e:/')) # 初始目录
    def open_filenames(self):
        # 调用askopenfilenames方法获取多个文件的完整文件名(路径+文件名)列表
        print(filedialog.askopenfilenames(title='打开多个文件',
            filetypes=[("文本文件", "*.txt"), ('Python源文件', '*.py')], # 只处理的文件类型
            initialdir='e:/')) # 初始目录
    def save_file(self):
        # 调用asksaveasfile方法保存单个文件
        print(filedialog.asksaveasfile(title='保存文件',
            filetypes=[("文本文件", "*.txt"), ('Python源文件', '*.py')], # 只处理的文件类型
            initialdir='e:/')) # 初始目录
    def save_filename(self):
        # 调用asksaveasfilename方法获取多个文件的路径名
        print(filedialog.asksaveasfilename(title='保存文件',
            filetypes=[("文本文件", "*.txt"), ('Python源文件', '*.py')], # 只处理的文件类型
            initialdir='e:/')) # 初始目录
    def open_dir(self):
        # 调用askdirectory方法打开目录
        print(filedialog.askdirectory(title='打开目录',
            initialdir='e:/')) # 初始目录
root = Tk()
root.title("文件对话框测试")
App(root)
root.mainloop()

  上面代码中第34行~67行代码是 filedialog 模块下不同函数的示例。运行这段程序可看到如图三十三所示的界面,点击界面上的按钮就进入到文件对话框。在文件对话框中,只能选择浏览文本文件和 Python源文件,这就是通过 filetypes 选项设置的结果;另外对话框默认打开的目录是 E:/根目录,这是 initialdir 选项设置的效果。

图三十三  filedialog模块函数使用示例

 

5、颜色选择对话框

colorchooser 模块提共有颜色选择对话框的 askcolor() 工具函数,该工具函数的可用选项有:

(1)、parent:指定该对话框的属主窗口。

(2)、title:指定该对话框的标题。

(3)、color:指定该对话框初始选择的颜色。

颜色选择对话框的功能和用法示例如下:

from tkinter import *
from tkinter import ttk
from tkinter import colorchooser

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 创建一个按钮,并为之绑定事件处理方法
        ttk.Button(self.master, text="选择颜色",
                   command=self.choose_color    # 绑定 choose_color 方法
                   ).pack(side=LEFT, ipadx=5, ipady=5, padx=10)

    def choose_color(self):
        # 调用 askcolor 函数获取所选中的颜色
        print(colorchooser.askcolor(parent=self.master, title='选择画笔颜色',
                                    color='pink'))  # 初始颜色

root = Tk()
root.title("颜色对话框测试")
App(root)
root.mainloop()

  上面代码第18、19行调用了 askcolor() 函数生成颜色选择对话框。运行这段代码,点击【选择颜色】按钮,就能打开颜色选择对话框。颜色选择对话框与系统平台有关,不同的平台看到颜色选择对话框是不同的。当选定颜色点击【确定】按钮后,askcolor() 函数就返回颜色的RGB值或十六进制表示的值。在控制台输出的颜色是下面这样的:((64.25, 0.0, 128.5), '#400080')

 

6、消息框

messagebox 模块下有大量工具函数可生成各种消息框,消息框结构可分为上下两部分,上面部分又分为左右两部分。其中上部分的左部分是图标区域,右部分是消息信息。下部分是按钮区域。在调用 messagebox 的工具函数时,只要设置提示区的字符串即可,图标区的图标、按钮区的按钮都有默认设置。

下面两个选项可定制图标和按钮区域:

(1)、icon:定制图标的选项。支持的选项值有:"error"、"info"、"question"、"warning"。

(2)、type:定制按钮的选项。支持的选项值有:"abortretryignore"(取消、重试、忽)、"yesno"(是、否)、"yesnocancel"(是、否、取消)。

下面代码示例了 messagebox 各工具函数的用法,同时通过两组单选钮动态选择不同的 icon 和 type 选项的效果。

from tkinter import *
from tkinter import ttk
from tkinter import messagebox as msgbox

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        #-----------创建第1个Labelframe,用于选择图标类型-----------
        topF = Frame(self.master)
        topF.pack(fill=BOTH)
        lf1 = ttk.Labelframe(topF, text='请选择图标类型')
        lf1.pack(side=LEFT, fill=BOTH, expand=YES, padx=10, pady=5)
        i = 0
        self.iconVar = IntVar()
        self.icons = [None, "error", "info", "question", "warning"]
        # 使用循环创建多个Radiobutton,并放入Labelframe中
        for icon in self.icons:
            Radiobutton(lf1, text = icon if icon is not None else '默认',
            value=i,
            variable=self.iconVar).pack(side=TOP, anchor=W)
            i += 1
        self.iconVar.set(0)
        #-----------创建第二个Labelframe,用于选择按钮类型-----------
        lf2 = ttk.Labelframe(topF, text='请选择按钮类型')
        lf2.pack(side=LEFT,fill=BOTH, expand=YES, padx=10, pady=5)
        i = 0
        self.typeVar = IntVar()
        # 定义所有按钮类型
        self.types = [None, "abortretryignore", "ok", "okcancel",
            "retrycancel", "yesno", "yesnocancel"]
        # 使用循环创建多个Radiobutton,并放入Labelframe中
        for tp in self.types:
            Radiobutton(lf2, text= tp if tp is not None else '默认',
            value=i,
            variable=self.typeVar).pack(side=TOP, anchor=W)
            i += 1
        self.typeVar.set(0)
        #-----------创建Frame,用于包含多个按钮来生成不同的消息框-----------
        bottomF = Frame(self.master)
        bottomF.pack(fill=BOTH)
        # 创建8个按钮,并为之绑定事件处理函数
        btn1 = ttk.Button(bottomF, text="showinfo",
            command=self.showinfo_clicked)
        btn1.pack(side=LEFT, fill=X, ipadx=5, ipady=5,
            pady=5, padx=5)
        btn2 = ttk.Button(bottomF, text="showwarning",
            command=self.showwarning_clicked)
        btn2.pack(side=LEFT, fill=X, ipadx=5, ipady=5,
            pady=5, padx=5)
        btn3 = ttk.Button(bottomF, text="showerror",
            command=self.showerror_clicked)
        btn3.pack(side=LEFT, fill=X, ipadx=5, ipady=5,
            pady=5, padx=5)
        btn4 = ttk.Button(bottomF, text="askquestion",
            command=self.askquestion_clicked)
        btn4.pack(side=LEFT, fill=X, ipadx=5, ipady=5,
            pady=5, padx=5)
        btn5 = ttk.Button(bottomF, text="askokcancel",
            command=self.askokcancel_clicked)
        btn5.pack(side=LEFT, fill=X, ipadx=5, ipady=5,
            pady=5, padx=5)
        btn6 = ttk.Button(bottomF, text="askyesno",
            command=self.askyesno_clicked)
        btn6.pack(side=LEFT, fill=X, ipadx=5, ipady=5,
            pady=5, padx=5)
        btn7 = ttk.Button(bottomF, text="askyesnocancel",
            command=self.askyesnocancel_clicked)
        btn7.pack(side=LEFT, fill=X, ipadx=5, ipady=5,
            pady=5, padx=5)
        btn8 = ttk.Button(bottomF, text="askretrycancel",
            command=self.askretrycancel_clicked)
        btn8.pack(side=LEFT, fill=X, ipadx=5, ipady=5,
            pady=5, padx=5)
    def showinfo_clicked(self):
        print(msgbox.showinfo("Info", "showinfo测试.",
            icon=self.icons[self.iconVar.get()],
            type=self.types[self.typeVar.get()]))
    def showwarning_clicked(self):
        print(msgbox.showwarning("Warning", "showwarning测试.",
            icon=self.icons[self.iconVar.get()],
            type=self.types[self.typeVar.get()]))
    def showerror_clicked(self):
        print(msgbox.showerror("Error", "showerror测试.",
            icon=self.icons[self.iconVar.get()],
            type=self.types[self.typeVar.get()]))
    def askquestion_clicked(self):
        print(msgbox.askquestion("Question", "askquestion测试.",
            icon=self.icons[self.iconVar.get()],
            type=self.types[self.typeVar.get()]))
    def askokcancel_clicked(self):
        print(msgbox.askokcancel("OkCancel", "askokcancel测试.",
            icon=self.icons[self.iconVar.get()],
            type=self.types[self.typeVar.get()]))
    def askyesno_clicked(self):
        print(msgbox.askyesno("YesNo", "askyesno测试.",
            icon=self.icons[self.iconVar.get()],
            type=self.types[self.typeVar.get()]))
    def askyesnocancel_clicked(self):
        print(msgbox.askyesnocancel("YesNoCancel", "askyesnocancel测试.",
            icon=self.icons[self.iconVar.get()],
            type=self.types[self.typeVar.get()]))
    def askretrycancel_clicked(self):
        print(msgbox.askretrycancel("RetryCancel", "askretrycancel测试.",
            icon=self.icons[self.iconVar.get()],
            type=self.types[self.typeVar.get()]))

root = Tk()
root.title("消息框测试")
App(root)
root.mainloop()

  上面代码中首先创建两组单选钮选择图标类型(通过 icon 选项改变)和按钮类型(通过 type 选项改变)。接下来第77~108行代码是调用函数生成不同消息框的关键代码。运行这段代码可看到图三十四所示的界面。

图三十四   生成消息框的程序界面

在图三十四的左边可选择图标类型,右边可选择按钮类型。例如在左边选择"error",右边选择"abortretryignore",再单击"showinfo"按钮,可看图三十五所示的消息框。

图三十五  定制的消息框

showinfo() 函数默认生成的消息框图标是一个感叹号,并且下方也只有一个按钮。但是在图三十五中看到通过 showinfo() 函数生成的消息框被改变了,这就是因为指定了 icon 和 type 选项的缘故。

这些消息框返回的内容可以在控制台的输出中看到。消息框返回的是单击的按钮,例如单击“中止”按钮,消息框就返回"abort"字符串;单击“重试”,消息框就返回"retry"字符串等等。

 

七、菜单

 

Tkinter 为菜单提供了 Menu 类。该类即可代表菜单条,也可代表菜单,还可代表上下文菜单(右键菜单)。所以,Menu 类可以搞定所有菜单相关的内容。

要创建菜单需要调用 Menu 的构造方法,创建完菜单后有下面的方法可添加菜单项。

(1)、add_command():添加菜单项。

(2)、add_checkbutton():添加复选框菜单项。

(3)、add_radiobutton():添加单选钮菜单项。

(4)、add_separator():添加菜单分隔条。

上面4个方法中的前面3个都用于添加菜单项,这3个方法都支持下面的选项:

(1)、label:指定菜单项的文本。

(2)、command:指定为菜单项绑定的事件处理方法。

(3)、image:指定菜单项的图标。

(4)、compound:指定在菜单项中图标位于文字的哪个方位。

菜单的使用方法有两种:

(1)、在窗口上方通过菜单条管理菜单。

(2)、通过鼠标右键触发右键菜单(上下文菜单)。

 

1、窗口菜单

菜单设置为窗口的菜单条,通过窗口的 menu 选项即可。例如下面这行代码:

self.master['menu'] = menubar

如果要将菜单添加到菜单条中,或者添加为子菜单,则调用 Menu 的 add_cascade() 方法。为窗口添加菜单示例如下:

from tkinter import *
from tkinter import ttk
from tkinter import messagebox as msgbox

class App:
    def __init__(self, master):
        self.master = master
        self.init_menu()

    # 创建菜单
    def init_menu(self):
        # 创建menubar,它被放入self.master中
        menubar = Menu(self.master)
        self.master.filenew_icon = PhotoImage(file='images/filenew.png')
        self.master.fileopen_icon = PhotoImage(file='images/fileopen.png')
        # 添加菜单条
        self.master['menu'] = menubar
        # 创建file_menu菜单,它被放入menubar中
        file_menu = Menu(menubar, tearoff=0)
        # 使用add_cascade方法添加file_menu菜单
        menubar.add_cascade(label='文件', menu=file_menu)
        # 创建lang_menu菜单,它被放入menubar中
        lang_menu = Menu(menubar, tearoff=0)
        # 使用add_cascade方法添加lang_menu菜单
        menubar.add_cascade(label='选择语言', menu=lang_menu)
        # 使用add_command方法为file_menu添加菜单项
        file_menu.add_command(label="新建", command = None,
            image=self.master.filenew_icon, compound=LEFT)
        file_menu.add_command(label="打开", command = None,
            image=self.master.fileopen_icon, compound=LEFT)
        # 使用add_command方法为file_menu添加分隔条
        file_menu.add_separator()
        # 为file_menu创建子菜单
        sub_menu = Menu(file_menu, tearoff=0)
        # 使用add_cascade方法添加sub_menu子菜单
        file_menu.add_cascade(label='选择性别', menu=sub_menu)
        self.genderVar = IntVar()
        # 使用循环为sub_menu子菜单添加菜单项
        for i, im in enumerate(['男', '女', '保密']):
            # 使用add_radiobutton方法为sub_menu子菜单添加单选菜单项
            # 绑定同一个变量,说明它们是一组
            sub_menu.add_radiobutton(label=im, command=self.choose_gender,
                variable=self.genderVar, value=i)
        self.langVars = [StringVar(), StringVar(), StringVar(), StringVar()]
        # 使用循环为lang_menu菜单添加菜单项
        for i, im in enumerate(('Python', 'Kotlin','Swift', 'Java')):
            # 使用add_add_checkbutton方法为lang_menu菜单添加多选菜单项
            lang_menu.add_checkbutton(label=im, command=self.choose_lang,
                onvalue=im, variable=self.langVars[i])
    def choose_gender(self):
        msgbox.showinfo(message=('选择的性别为: %s' % self.genderVar.get()))
    def choose_lang(self):
        rt_list = [e.get() for e in self.langVars]
        msgbox.showinfo(message=('选择的语言为: %s' % ','.join(rt_list)))

root = Tk()
root.title("菜单测试")
root.geometry('400x200')
# 禁止改变窗口大小
root.resizable(width=False, height=False)
App(root)
root.mainloop()

  上面代码第13行将 Menu 设置为窗口的 menu 选项,这样菜单变成了菜单条;第21行、第25行代码调用 add_cascade() 方法添加菜单,这就为菜单条添加了两个主菜单。接下来调用 add_command 方法为 file_menu 添加多个子菜单项。第36行代码调用 file_menu的 add_cascade() 方法再次为 file_menu 添加子菜单。

第42、43行代码调用 add_radiobutton() 方法添加多个单选菜单项,并且为这些单选菜单项都绑定了一个变量,因此它们是一组的;第48、49行代码调用 add_checkbutton() 方法添加多个多选菜单项,每个多选菜单都有单独的值,因此它们都需要绑定一个变量。运行上面这段代码,可以看到如图三十六所示的界面。

图三十六  生成菜单

由于为单选菜单、多选菜单都绑定了事件处理方法,因此单击这些菜单,将会弹出消息框提示用户的选择。下面代码是一个功能更全面的菜单示例,该示例中还添加了工具栏。由于 Tkinter 没有提供工具栏组件,这里以 Frame 来实现工具栏,以 Button 实现工具栏上的按钮。代码如下:

from tkinter import *
from tkinter import ttk
from collections import OrderedDict

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        # 初始化菜单、工具条用到的图标
        self.init_icons()
        # 调用init_menu初始化菜单
        self.init_menu()
        # 调用init_toolbar初始化工具条
        self.init_toolbar()
        #---------------------------------
        # 创建、添加左边的Frame容器
        leftframe = ttk.Frame(self.master, width=40)
        leftframe.pack(side=LEFT, fill=Y)
        # 在左边窗口放一个Listbox
        lb = Listbox(leftframe, font=('Courier New', 20))
        lb.pack(fill=Y, expand=YES)
        for s in ('Python', 'Ruby', 'Swift', 'Kotlin', 'Java'):
            lb.insert(END, s)
        # 创建、添加右边的Frame容器
        mainframe = ttk.Frame(self.master)
        mainframe.pack(side=LEFT, fill=BOTH, padx=5)
        text = Text(mainframe, width=40, font=('Courier New', 16))
        text.pack(side=LEFT, fill=BOTH)
        scroll = ttk.Scrollbar(mainframe)
        scroll.pack(side=LEFT,fill=Y)
        # 设置滚动条与text组件关联
        scroll['command'] = text.yview
        text.configure(yscrollcommand=scroll.set)
    # 创建菜单
    def init_menu(self):
        '初始化菜单的方法'
        # 定义菜单条所包含的3个主菜单
        menus = ('文件', '编辑', '帮助')
        # 定义菜单数据
        items = (OrderedDict([
                # 每项对应一个菜单项,后面元组第一个元素是菜单图标,
                # 第二个元素是菜单对应的事件处理函数
                ('新建', (self.master.filenew_icon, None)),
                ('打开', (self.master.fileopen_icon, None)),
                ('保存', (self.master.save_icon, None)),
                ('另存为...', (self.master.saveas_icon, None)),
                ('-1', (None, None)),   # 添加分隔线
                ('退出', (self.master.signout_icon, None)),
                ]),
            OrderedDict([('撤销',(None, None)),
                ('重做',(None, None)),
                ('-1',(None, None)),
                ('剪切',(None, None)),
                ('复制',(None, None)),
                ('粘贴',(None, None)),
                ('删除',(None, None)),
                ('选择',(None, None)),
                ('-2',(None, None)),
                # 二级菜单
                ('更多', OrderedDict([
                    ('显示数据',(None, None)),
                    ('显示统计',(None, None)),
                    ('显示图表',(None, None))
                    ]))
                ]),
            OrderedDict([('帮助主题',(None, None)),
                ('-1',(None, None)),
                ('关于', (None, None))]))
        # 使用Menu创建菜单条
        menubar = Menu(self.master)
        # 为窗口配置菜单条,也就是添加菜单条
        self.master['menu'] = menubar
        # 遍历menus元组
        for i, m_title in enumerate(menus):
            # 创建菜单
            m = Menu(menubar, tearoff=0)
            # 添加菜单
            menubar.add_cascade(label=m_title, menu=m)
            # 将当前正在处理的菜单数据赋值给tm
            tm = items[i]
            # 遍历OrderedDict,默认只遍历它的key
            for label in tm:
                print(label)
                # 如果value又是OrderedDict,说明是二级菜单
                if isinstance(tm[label], OrderedDict):
                    # 创建子菜单、并添加子菜单
                    sm = Menu(m, tearoff=0)
                    m.add_cascade(label=label, menu=sm)
                    sub_dict = tm[label]
                    # 再次遍历子菜单对应的OrderedDict,默认只遍历它的key
                    for sub_label in sub_dict:
                        if sub_label.startswith('-'):
                            # 添加分隔条
                            sm.add_separator()
                        else:
                            # 添加菜单项
                            sm.add_command(label=sub_label,image=sub_dict[sub_label][0],
                                command=sub_dict[sub_label][1], compound=LEFT)
                elif label.startswith('-'):
                    # 添加分隔条
                    m.add_separator()
                else:
                    # 添加菜单项
                    m.add_command(label=label,image=tm[label][0],
                        command=tm[label][1], compound=LEFT)
    # 生成所有需要的图标
    def init_icons(self):
        self.master.filenew_icon = PhotoImage(file='images/filenew.png')
        self.master.fileopen_icon = PhotoImage(file='images/fileopen.png')
        self.master.save_icon = PhotoImage(file='images/save.png')
        self.master.saveas_icon = PhotoImage(file='images/saveas.png')
        self.master.signout_icon = PhotoImage(file='images/signout.png')
    # 生成工具条
    def init_toolbar(self):
        # 创建并添加一个Frame作为工具条的容器
        toolframe = Frame(self.master, height=20, bg='lightgray')
        toolframe.pack(fill=X) # 该Frame容器放在窗口顶部
        # 再次创建并添加一个Frame作为工具按钮的容器
        frame = ttk.Frame(toolframe)
        frame.pack(side=LEFT) # 该Frame容器放在容器左边
        # 遍历self.master的全部数据,根据系统图标来创建工具栏按钮
        for i, e in enumerate(dir(self.master)):
            # 只处理属性名以_icon结尾的属性(这些属性都是图标)
            if e.endswith('_icon'):
                ttk.Button(frame, width=20, image=getattr(self.master, e),
                    command=None).grid(row=0, column=i, padx=1, pady=1, sticky=E)
root = Tk()
root.title("菜单测试")
# 禁止改变窗口大小
root.resizable(width=False, height=True)
App(root)
root.mainloop()

  在这段代码中图形界面的菜单没有写死,可根据 items 变量(第42~70行)自动生成,这个变量中的每个 OrderedDict 代表一个菜单,它的每个 key-value 对代表一个菜单项,其中key 是菜单文本,value 是一个元组,元组的第一个元素是菜单图标,第二个元素是为菜单绑定的事件处理函数。

如果需要改变图形界面中的菜单时,只需要修改 items 变量(第42~70行)代码即可。这里只生成了二级菜单,Tkinter 可以生成三级菜单,但需要做进一步处理。

这个程序还会自动生成工具栏(或者工具条),只要为 self.master 添加以 _icon 结尾的属性,就会自动把它们添加为工具条上的按钮。运行这段代码,可看到图三十七所示的界面。

图三十七   菜单和工具栏

 

2、右键菜单

创建菜单后,为目标组件的右键单击事件绑定处理函数,当点击鼠标右键时,调用菜单的 post() 方法即可在指定位置弹出右键菜单。示例如下:

from tkinter import *
from tkinter import ttk
from collections import OrderedDict

class App:
    def __init__(self, master):
        self.master = master
        self.initWidgets()

    def initWidgets(self):
        self.text = Text(self.master, height=12, width=60,
                         foreground='darkgray',
                         font=('微软雅黑', 12),
                         spacing2=8,    # 设置行间距
                         spacing3=12)   # 设置段间距
        self.text.pack()
        st = '《Python应用前景》,Python发展至今,应用的领域众多,'+\
            '目前最火的人工智能,云计算、大数据分析等领域都在使用Python做为基本工具。'+\
            '在最新的编程语言排行榜中,Python已经超越了C++,成为热门编程语言第三名。\n'
        self.text.insert(END, st)
        # 为 Text 组件的右键单击事件绑定处理函数
        self.text.bind('<Button-3>', self.popup)
        # 创建 Menu 对象,准备作为右键菜单
        self.popup_menu = Menu(self.master, tearoff=0)
        self.my_items = (OrderedDict([('超大', 16), ('大', 14), ('中', 12),
                                      ('小', 10), ('超小', 8)]),
                         OrderedDict([('红色', 'red'), ('绿色', 'green'), ('蓝色', 'blue')]))
        i = 0
        for k in ['字体大小', '颜色']:
            m = Menu(self.popup_menu, tearoff=0)
            # 添加子菜单
            self.popup_menu.add_cascade(label=k, menu=m)
            # 遍历 OrderedDict 的 key(默认遍历的就是 key)
            for im in self.my_items[i]:
                m.add_command(label=im, command=self.handlerAdaptor(self.choose, x=im))
            i += 1

    def popup(self, event):
        # 在指定位置显示菜单
        self.popup_menu.post(event.x_root, event.y_root)

    def choose(self, x):
        # 如果选择修改字体大小的子菜单项
        if x in self.my_items[0].keys():
            # 改变字体大小
            self.text['font'] = ('微软雅黑', self.my_items[0][x])
        # 如果用选择修改颜色的子菜单项
        if x in self.my_items[1].keys():
            # 改变颜色
            self.text['foreground'] = self.my_items[1][x]

    def handlerAdaptor(self, fun, **kwds):
        return lambda fun=fun, kwds=kwds: fun(**kwds)

root = Tk()
root.title('右键菜单测试')
App(root)
root.mainloop()

  这段代码的第24~36行创建一个 Menu,并为之添加菜单项。第40行代码位于 Text 组件的右键单击事件的处理函数内,这行代码调用Menu 对象的 post() 方法弹出的右键菜单,这意味在Text组件内单击鼠标右键时,Text 组件就会弹出右键菜单。运行这段代码,在弹出框内单击右键可以看到图三十八所示的界面。

图三十八   右键菜单

 

八、 使用 Canvas 绘图

 

Tkinter 的 Canvas 组件可实现绘图。绘制的图形有直线、矩形、椭圆等几何图形,也可绘制图片、文字、UI组件(如Button)等。Canvas 可以重新改变 Tkinter 绘制的所有图形项(item)的属性,比如坐标、外观等。

 

1、Tkinter Canvas 的绘制功能

绘制功能分两步:第1步,创建并添加 Canvas 组件;第2步,调用该组件的方法来绘制图形。示例如下:

 1 from tkinter import *
 2 
 3 root = Tk()
 4 # 第1步,创建并添加 Canvas 组件
 5 cv = Canvas(root, background='white')
 6 cv.pack(fill=BOTH, expand=YES)
 7 # 第2步,调用该组件的方法来绘制图形
 8 cv.create_rectangle(30, 30, 200, 200,
 9                     outline='red',  # 边框颜色
10                     stipple='question',  # 填充的位图
11                     fill='red', # 填充颜色
12                     width=5)    # 边框宽度
13 cv.create_oval(240, 30, 330, 200,
14                outline='yellow',    # 边框颜色
15                fill='pink',     # 填充颜色
16                width=4)         # 边框宽度
17 root.mainloop()

这段代码首先创建并添加了 Canvas 组件,接下来第8行~第16行代码分别绘制了矩形和椭圆。运行这段代码,可以看到如图三十九所示的效果。

图三十九   简单的 Canvas 绘图

关于 Canvas 提供的绘制方法如下:

(1)、create_rectangle():绘制矩形。

(2)、create_oval():绘制椭圆(包括圆,圆是椭圆的特例)。

(3)、create_arc():绘制绵弧形。

(4)、create_bitmap():绘制位置。

(5)、create_image():绘制图片。

(6)、create_line():绘制直线。

(7)、create_polygon():绘制多边形。

(8)、create_text():绘制文字。

(9)、create_window():绘制组件。

Canvas 的坐标系统是绘图的基础,坐标系的点(0,0)位于 Canvas 组件的左上角,X轴水平向右延伸,Y轴垂直向下延伸。在绘制上面这些几何图形时,需要一些几何知识。下面对上面这些方法及选项做一些简短介绍。

(1)、create_line()绘制直线需要指定两个点的坐标,分别是起点和终点。

(2)、create_rectangle()绘制矩形需要指定两个点坐标,分别是矩形左上角和右下角坐标。

(3)、create_oval()绘制椭圆需要指定两个点坐标,分别作为左上角点和右下角点的坐标来确定一个矩形,这个方法负责绘制该矩形的内切椭圆。

(4)、create_arc()绘制圆弧,和 create_oval()的用法相似,因为圆弧是椭圆的一部分。因此同样需要指定左上角和右下角两个点的坐标,默认是绘制从3点(0)开始,逆时针旋转90度这段弧形。可通过start选项改变起始角度,通过extent改变转过的角度。

(5)、create_polygon()绘制多边形,需要指定多个点的坐标作为多边形的多个定点。

(6)、使用 create_bitmap()、create_image()、create_text()、create_window()等方法时,只要指定一个坐标点,用于指定目标元素的绘制位置即可。

绘制这些图形可指定的选项有:

(1)、fill:填充颜色。不指定则默认不填充。

(2)、outline:指定边框颜色。 (3)、width:边框宽度。不指定则默认宽度是1。

(4)、dash:边框虚线。只有一个整数时是指定虚线中线段的长度;使用(5,2,3)格式时,5表示虚线中线段的长度,2是间隔长度,3是虚线长度······依此类推。

(5)、stipple:使用位图平铺进行填充。该选项可与fill选项结合使用,fill选项用于指定位图的颜色。

(6)、style:只对create_arc()有效,弧的样式。选项值有:PIESLICE(扇形)、CHORD(弓形)、ARC(仅绘制弧)。

(7)、start:只对create_arc()有效,弧的起始角度。

(8)、extent:只对create_arc()有效,弧的角度。

(9)、arrow:绘制的直线两端是否有箭头。选项值有:NONE(两端无箭头)、FIRST(起始端有箭头)、LAST(结束端有箭头)、BOTH(两端有箭头)。

(10)、arrowshape:箭头开头。选项值是一个形如"20 20 10"的字符串,字符串中的三个整数依次指定填充长度、箭头长度、箭头宽度。

(11)、joinstyle:指定连接点的样式。仅对绘制直线和多向形有效。选项值有:METTER、ROUND、BEVEL选项值。

(12)、anchor:绘制的文字、GUI组件位置。仅对create_text()、create_window()方法有效。

(13)、justify:文字的对齐方式。仅对create_text()方法有效。选项有:CENTER、LEFT、RIGHT常量值。

下面示例使用不同的方法绘制不同的图形,这些图形分别使用不同的边框、不同的填充效果。

from tkinter import *

# 创建窗口
root = Tk()
root.title('绘制图形项')
# 创建并添加Canvas
cv = Canvas(root, background='white', width=830, height=600)
cv.pack(fill=BOTH, expand=YES)
columnFont = ('微软雅黑', 14)
titleFont = ('微软雅黑', 16, 'bold')
# 使用循环绘制文字
for i, st in enumerate(['默认', '指定边宽', '指定填充', '边框颜色', '位图填充']):
    cv.create_text((130 + i * 140, 18), text = st,
    font = columnFont,
    fill='gray',
    anchor = W,
    justify = LEFT)
# 绘制文字
cv.create_text(10, 60, text = '绘制矩形',
    font = titleFont,
    fill='magenta',
    anchor = W,
    justify = LEFT)
# 定义列表,每个元素的4个值分别指定边框宽度、填充色、边框颜色、位图填充
options = [(None, None, None, None),
    (4, None, None, None),
    (4, 'pink', None, None),
    (4, 'pink', 'blue', None),
    (4, 'pink', 'blue', 'error')]
# 采用循环绘制5个矩形
for i, op in enumerate(options):
    cv.create_rectangle(130 + i * 140, 50, 240 + i * 140, 90,
        width = op[0], # 边框宽度
        fill = op[1], # 填充颜色
        outline = op[2], # 边框颜色
        stipple = op[3]) # 使用位图填充
# 绘制文字
cv.create_text(10, 120, text = '绘制椭圆',
    font = titleFont,
    fill='magenta',
    anchor = W,
    justify = LEFT)
# 定义列表,每个元素的4个值分别指定边框宽度、填充色、边框颜色、位图填充
options = [(None, None, None, None),
    (4, None, None, None),
    (4, 'pink', None, None),
    (4, 'pink', 'blue', None),
    (4, 'pink', 'blue', 'error')]
# 采用循环绘制5个椭圆
for i, op in enumerate(options):
    cv.create_oval(130 + i * 140, 100, 240 + i * 140, 180,
        width = op[0], # 边框宽度
        fill = op[1], # 填充颜色
        outline = op[2], # 边框颜色
        stipple = op[3]) # 使用位图填充
# 绘制文字
cv.create_text(10, 220, text = '绘制多边形',
    font = titleFont,
    fill='magenta',
    anchor = W,
    justify = LEFT)
# 定义列表,每个元素的4个值分别指定边框宽度、填充色、边框颜色、位图填充
options = [(None, "", 'black', None),
    (4, "", 'black', None),
    (4, 'pink', 'black', None),
    (4, 'pink', 'blue', None),
    (4, 'pink', 'blue', 'error')]
# 采用循环绘制5个多边形
for i, op in enumerate(options):
    cv.create_polygon(130 + i * 140, 260, 185 + i * 140, 200, 240 + i * 140, 260,
        width = op[0], # 边框宽度
        fill = op[1], # 填充颜色
        outline = op[2], # 边框颜色
        stipple = op[3]) # 使用位图填充
# 绘制文字
cv.create_text(10, 300, text = '绘制扇形',
    font = titleFont,
    fill='magenta',
    anchor = W,
    justify = LEFT)
# 定义列表,每个元素的4个值分别指定边框宽度、填充色、边框颜色、位图填充
options = [(None, None, None, None),
    (4, None, None, None),
    (4, 'pink', None, None),
    (4, 'pink', 'blue', None),
    (4, 'pink', 'blue', 'error')]
# 采用循环绘制5个扇形
for i, op in enumerate(options):
    cv.create_arc(130 + i * 140, 280, 240 + i * 140, 350,
        width = op[0], # 边框宽度
        fill = op[1], # 填充颜色
        outline = op[2], # 边框颜色
        stipple = op[3]) # 使用位图填充
# 绘制文字
cv.create_text(10, 380, text = '绘制弓形',
    font = titleFont,
    fill='magenta',
    anchor = W,
    justify = LEFT)
# 定义列表,每个元素的4个值分别指定边框宽度、填充色、边框颜色、位图填充
options = [(None, None, None, None),
    (4, None, None, None),
    (4, 'pink', None, None),
    (4, 'pink', 'blue', None),
    (4, 'pink', 'blue', 'error')]
# 采用循环绘制5个弓形
for i, op in enumerate(options):
    cv.create_arc(130 + i * 140, 360, 240 + i * 140, 440,
        width = op[0], # 边框宽度
        fill = op[1], # 填充颜色
        outline = op[2], # 边框颜色
        stipple = op[3], # 使用位图填充
        start = 30, # 指定起始角度
        extent = 60, # 指定逆时针转过角度
        style = CHORD) # CHORD指定绘制弓
# 绘制文字
cv.create_text(10, 420, text = '仅绘弧',
    font = titleFont,
    fill='magenta',
    anchor = W,
    justify = LEFT)
# 定义列表,每个元素的4个值分别指定边框宽度、填充色、边框颜色、位图填充
options = [(None, None, None, None),
    (4, None, None, None),
    (4, 'pink', None, None),
    (4, 'pink', 'blue', None),
    (4, 'pink', 'blue', 'error')]
# 采用循环绘制5个弧
for i, op in enumerate(options):
    cv.create_arc(130 + i * 140, 410, 240 + i * 140, 480,
        width = op[0], # 边框宽度
        fill = op[1], # 填充颜色
        outline = op[2], # 边框颜色
        stipple = op[3], # 使用位图填充
        start = 30, # 指定起始角度
        extent = 60, # 指定逆时针转过角度
        style = ARC) # ARC指定仅绘制弧
# 绘制文字
cv.create_text(10, 460, text = '绘制直线',
    font = titleFont,
    fill='magenta',
    anchor = W,
    justify = LEFT)
# 定义列表,每个元素的5个值分别指定边框宽度、线条颜色、位图填充、箭头风格, 箭头形状
options = [(None, None, None, None, None),
    (6, None, None, BOTH, (20, 40, 10)),
    (6, 'pink', None, FIRST, (40, 40, 10)),
    (6, 'pink', None, LAST, (60, 50, 10)),
    (8, 'pink', 'error', None, None)]
# 采用循环绘制5个弧
for i, op in enumerate(options):
    cv.create_line(130 + i * 140, 450, 240 + i * 140, 500,
        width = op[0], # 边框宽度
        fill = op[1], # 填充颜色
        stipple = op[2], # 使用位图填充
        arrow = op[3], # 箭头风格
        arrowshape = op[4]) # 箭头形状
# 绘制文字
cv.create_text(10, 540, text = '绘制位图\n图片、组件',
    font = titleFont,
    fill='magenta',
    anchor = W,
    justify = LEFT)
# 定义包括create_bitmap, create_image, create_window三个方法的数组
funcs = [Canvas.create_bitmap, Canvas.create_image, Canvas.create_window]
# 为上面3个方法定义选项
items = [{'bitmap' : 'questhead'}, {'image':PhotoImage(file='images/logo.gif')},
    {'window':Button(cv,text = '单击我', padx=10, pady=5,
        command = lambda :print('按钮单击')),'anchor': W}]
for i, func in enumerate(funcs):
    func(cv, 230 + i * 140, 550, **items[i])
root.mainloop()

  上面这段代码使用了 create_xxx 方法的各种功能和用法,绘制了矩形、椭圆、多边形、扇形、弓形、弧、直线、位图、图片和组件等,在绘制不同的图形时可指定不同的选项,来实现不同的绘制效果。运行这段代码,可看到如图四十所示的效果。

图四十  使用Canvas绘制各种图形

下面用图形界面来实现五子棋游戏。在游戏中还需要根据鼠标动作来确定下棋坐标,因此为游戏界面的<Button-1>(单击左键)、<Motion>(移动鼠标)、<Leave>(鼠标移出)事件绑定事件处理函数。代码如下:

from tkinter import *
import random

BOARD_WIDTH = 535
BOARD_HEIGHT = 536
BOARD_SIZE = 15
# 定义棋盘坐标的像素值和棋盘数组之间的偏移距。
X_OFFSET = 21
Y_OFFSET = 23
# 定义棋盘坐标的像素值和棋盘数组之间的比率。
X_RATE = (BOARD_WIDTH - X_OFFSET * 2) / (BOARD_SIZE - 1)
Y_RATE = (BOARD_HEIGHT - Y_OFFSET * 2) / (BOARD_SIZE - 1)
BLACK_CHESS = "●"
WHITE_CHESS = "○"
board = []
# 把每个元素赋为"╋",代表无棋
for i in range(BOARD_SIZE) :
    row = ["╋"] * BOARD_SIZE
    board.append(row)
# 创建窗口
root = Tk()
# 禁止改变窗口大小
root.resizable(width=False, height=False)
# 修改图标
root.iconbitmap('images/logo.ico')
# 设置窗口标题
root.title('五子棋游戏')
# 创建并添加Canvas
cv = Canvas(root, background='white',
    width=BOARD_WIDTH, height=BOARD_HEIGHT)
cv.pack()
bm = PhotoImage(file="images/board.png")
cv.create_image(BOARD_HEIGHT/2 + 1, BOARD_HEIGHT/2 + 1, image=bm)
selectedbm = PhotoImage(file="images/selected.gif")
# 创建选中框图片,但该图片默认不在棋盘中
selected = cv.create_image(-100, -100, image=selectedbm)
def move_handler(event):
    # 计算用户当前的选中点,并保证该选中点在0~14之间
    selectedX = max(0, min(round((event.x - X_OFFSET) / X_RATE), 14))
    selectedY = max(0, min(round((event.y - Y_OFFSET) / Y_RATE), 14))
    # 移动红色选择框
    cv.coords(selected,(selectedX * X_RATE + X_OFFSET,
        selectedY * Y_RATE + Y_OFFSET))
black = PhotoImage(file="images/black.gif")
white = PhotoImage(file="images/white.gif")
def click_handler(event):
    # 计算用户的下棋点,并保证该下棋点在0~14之间
    userX = max(0, min(round((event.x - X_OFFSET) / X_RATE), 14))
    userY = max(0, min(round((event.y - Y_OFFSET) / Y_RATE), 14))
    # 当下棋点没有棋子时,才能下棋子,用户才能下棋子
    if board[userY][userX] == "╋":
        cv.create_image(userX * X_RATE + X_OFFSET, userY * Y_RATE + Y_OFFSET,
            image=black)
        board[userY][userX] = "●"
        while(True):
            comX = random.randint(0, BOARD_SIZE - 1)
            comY = random.randint(0, BOARD_SIZE - 1)
            # 如果电脑要下棋的点没有棋子时,才能让电脑下棋
            if board[comY][comX] == "╋": break
        cv.create_image(comX * X_RATE + X_OFFSET, comY * Y_RATE + Y_OFFSET,
            image=white)
        board[comY][comX] = "○"
def leave_handler(event):
    # 将红色选中框移出界面
    cv.coords(selected, -100, -100)
# 为鼠标移动事件绑定事件处理函数
cv.bind('<Motion>', move_handler)
# 为鼠标点击事件绑定事件处理函数
cv.bind('<Button-1>', click_handler)
# 为鼠标移出事件绑定事件处理函数
cv.bind('<Leave>', leave_handler)
root.mainloop()

  上面代码的第33行绘制了五子棋的棋盘,该棋盘是一张图片;第36行代码绘制选择框,当鼠标在棋盘上移动时,该选择框显示鼠标当前停留在哪个下棋点上。第42、43行调用了 Canvas 的 coords() 方法,该方法负责重设选择框的坐标。这是 Tkinter 绘图的特别之处:绘制好的每一个图形项都不是固定的,后面完全可能修改它们。所以第42、43行代码可以控制选择图片随着用户鼠标的移动而改变位置。

第52、53行代码根据鼠标单击来绘制黑色棋子,下的黑棋;第60、61行代码绘制白色棋子,下的白棋。在绘制黑色棋子和白色棋子时,也改变了底层代表棋盘状态的 board 列表的数据来记录下棋状态,从而在后面可以根据 board[] 列表来判断胜负(这里没有实现判断胜负的功能)。此外,还可加入人工智能,根据 board[] 列表来决定电脑的下棋点。运行这段代码,可看到如图四十一所示的效果。

图四十一  使用Canvas绘制五子棋

 

2、操作图形项的标签

使用 Canvas 绘制的图形并不完全是静态图形,每个图形项都是一个独立的对象,这些图形对象可以被动态修改、删除等操作。Canvas 以“堆叠”方式管理这些图形项,即先绘制的图形在下面,后绘制的图形在上面。如果两个图形在绘图区有重叠的部分,那么后绘制的图形就位于先绘制的图形上面,也就是遮挡了先绘制的图形。

要修改、删除这些图形项,需要先获得这些图形项的引用。有两种方式可获得图形项的引用。

(1)、通过图形项的 id,也就是 Canvas 执行 create_xxx() 方法的返回值。create_xxx()会依次返回1、2、3等整数作为图形项的id。

(2)、通过图形项的 tag(标签)。

在 Canvas 中调用 create_xxx() 方法绘图时,还可传入一个 tags 选项,该选项可以为所绘制的图形项(比如矩形、椭圆等)添加一个或多个 tag(标签)。Canvas 还允许调用方法为图形项添加 tag、删除 tag 等,这些 tag 也相当于该图形项的标识,在程序中完全可以根据 tag 来获取图形项。在 Canvas 中为图形项添加 tag 的方法有下面这些:

(1)、addtag_above(self, newtag, tagOrId):为tagOrId 对应图形项的上一个图形项添加新 tag。

(2)、addtag_all(self,newtag):为所有图形项添加新 tag。

(3)、addtag_below(self,newtag,tagOrId):为 tagOrId 对应图形项的下一个图形项添加新 tag。

(4)、addtag_closest(self,newtag,x,y):为与 x、y 点最近的图形项添加新 tag。

(5)、addtag_enclosed(self,newtag,x1,y1,x2,y2):为指定矩形区域内最上面的图形项添加新 tag。其中x1、y1是左上角坐标,x2、y2是右下角坐标。

(6)、addtag_overlapping(self,newtag,x1,y1,x2,y2):为与指定矩形区域重叠的最上面的图形项添加tag。

(7)、addtag_withtag(self,newtag,tagOrId):为 tagOrId 对应图形项添加新 tag。

Canvas 提供下面这个方法来删除图形项的 tag:

(1)、dtag(self,*args):删除指定图形项的 tag。

Canvas 获取图形项的所有 tag 的方法是:

(1)、gettags(self,*args):获取指定图形项的所有 tag。

Canvas 可根据 tag 来获取其对应的所有图形项:

(1)、find_withtag(self, tagOrId):获取 tagOrId 对应的所有图形项。

下面代码示例了上面这些方法的作用。

from tkinter import *

# 创建窗口
root = Tk()
root.title('操作标签')
# 创建并添加Canvas
cv = Canvas(root, background='white', width=620, height=250)
cv.pack(fill=BOTH, expand=YES)
# 绘制一个矩形框
rt = cv.create_rectangle(40, 40, 300, 220,
    outline='blue', width=2,
    tag = ('t1', 't2', 't3', 'tag4'))  # 为该图形项指定标签
# 访问图形项的id,也就是编号
print(rt) # 1
# 绘制一个椭圆
oval = cv.create_oval(350, 50, 580, 200,
    fill='yellow', width=0,
    tag = ('g1', 'g2', 'g3', 'tag4'))  # 为该图形项指定标签
# 访问图形项的id,也就是编号
print(oval) # 2
# 根据指定tag返回该tag对应的所有图形项
print(cv.find_withtag('tag4')) # (1, 2)
# 获取指定图形项的所有tag
print(cv.gettags(rt))  # ('t1', 't2', 't3', 'tag4')
print(cv.gettags(2))  # ('g1', 'g2', 'g3', 'tag4')
cv.dtag(1, 't1') # 删除id为1的图形项上名为t1的tag
cv.dtag(oval, 'g1') # 删除id为oval的图形项上名为g1的tag
# 获取指定图形项的所有tag
print(cv.gettags(rt))  # ('tag4', 't2', 't3')
print(cv.gettags(2))  # ('tag4', 'g2', 'g3')
# 为所有图形项添加tag
cv.addtag_all('t5')
print(cv.gettags(1))  # ('tag4', 't2', 't3', 't5')
print(cv.gettags(oval))  # ('tag4', 'g2', 'g3', 't5')
# 为指定图形项添加tag
cv.addtag_withtag('t6', 'g2')
# 获取指定图形项的所有tag
print(cv.gettags(1))  # ('tag4', 't2', 't3', 't5')
print(cv.gettags(oval))  # ('tag4', 'g2', 'g3', 't5', 't6')
# 为指定图形项上面的图形项添加tag, t2上面的就是oval图形项
cv.addtag_above('t7', 't2')
print(cv.gettags(1))  # ('tag4', 't2', 't3', 't5')
print(cv.gettags(oval))  # ('tag4', 'g2', 'g3', 't5', 't6', 't7')
# 为指定图形项下面的图形项添加tag, g2下面的就是rt图形项
cv.addtag_below('t8', 'g2')
print(cv.gettags(1))  # ('tag4', 't2', 't3', 't5', 't8')
print(cv.gettags(oval))  # ('tag4', 'g2', 'g3', 't5', 't6', 't7')
# 为最接近指定点的图形项添加tag,最接近360、90的图形项是oval
cv.addtag_closest('t9', 360, 90)
print(cv.gettags(1))  # ('tag4', 't2', 't3', 't5', 't8')
print(cv.gettags(oval))  # ('tag4', 'g2', 'g3', 't5', 't6', 't7', 't9')
# 为位于指定区域内(几乎覆盖整个图形区)的最上面的图形项添加tag
cv.addtag_closest('t10', 30, 30, 600, 240)
print(cv.gettags(1))  # ('tag4', 't2', 't3', 't5', 't8')
print(cv.gettags(oval))  # ('tag4', 'g2', 'g3', 't5', 't6', 't7', 't9', 't10')
# 为与指定区域内重合的最上面的图形项添加tag
cv.addtag_closest('t11', 250, 30, 400, 240)
print(cv.gettags(1))  # ('tag4', 't2', 't3', 't5', 't8')
print(cv.gettags(oval))  # ('tag4', 'g2', 'g3', 't5', 't6', 't7', 't9', 't10', 't11')
root.mainloop()

 

上面这段代码介绍了操作图形项 tag 的方法,并且在每次操作后在控制台有输出结果。可根据输出结果进一步理解 Canvas 是如何管理图形项的 tag。

 

3、操作图形项

获取了 Canvas 的图形项后, Canvas 还有很多的方法来操作这些图形项。下面这些方法是在“堆叠”图形项中查找图形项。

(1)、find_above(self, tagOrId):返回 tagOrId 对应图形项的上一个图形项。

(2)、find_all(self):返回全部图形项。

(3)、find_below(self, tagOrId):返回 tagOrId 对应图形项的下一个图形项。

(4)、find_closest(self,x,y):返回与 x、y点最接近的图形项。

(5)、find_enclosed(self,x1,y1,x2,y2):返回位于指定矩形区域内最上面的图形项。

(6)、find_overlapping(self,x1,y1,x2,y2):返回与指定矩形区域重叠的最上面的图形项。

(7)、find_withtag(self, tagOrId):返回 tagOrId 对应的全部图形项。

下面这些方法在“堆叠”图形项中移动图形项:

(1)、tag_lower(self, *args)|lower:将 args 的第一个参数对应的图形项移动“堆叠”的最下面。也可额外指定一个参数,代表移动到指定图形项的下面。*

(2)、tag_raise(self,*args)|lift:将 args 的第一个参数对应的图形项移到“堆叠”的最上面。也可额外指定一个参数,代表移动到指定的图形项的上面。

修改图形项的选项方法如下:

(1)、itemcget(self, tagOrId, option):获取 tagOrId 对应图形项的 option 选项值。

(2)、itemconfig(self, tagOrId, cnf=None,**kw):为 tagOrId 对应的图形项配置选项。

(3)、itemconfigure:这个方法与上一个方法完全相同。

下面方法可修改图形项的大小和位置。

(1)、coords(self,*args):重设图形项的大小和位置。*

(2)、move(self,*args):移动图形项,但不能改变大小。在图形项的x、y 基础上加上新的 mx、my 参数。 (3)、scale(self,*args):缩放图形项。该方法的 args 参数要传入4个值,其中前两个值指定缩放中心;后两个值指定x、y方向的缩放比。

下面方法可删除图形项或文字图形项(由 create_text方法创建的)中间的部分文字。

(1)、delete(self,*args):删除指定 id 或 tag 对应的全部图形项。

(2)、dchars(self,*args):删除文字图形项中间的部分文字。

关于图形项的操作示例如下:

from tkinter import *
from tkinter import colorchooser
import threading

# 创建窗口
root = Tk()
root.title('操作图形项')
# 创建并添加Canvas
cv = Canvas(root, background='white', width=400, height=350)
cv.pack(fill=BOTH, expand=YES)
# 该变量用于保存当前选中的图形项
current = None
# 该变量用于保存当前选中的图形项的边框颜色
current_outline = None
# 该变量用于保存当前选中的图形项的边框宽度
current_width = None
# 该函数用于高亮显示选中图形项(边框颜色在red、yellow之间切换)
def show_current():
    # 如果当前选中项不为None
    if current is not None:
        # 如果当前选中图形项的边框色为red,将它改为yellow
        if cv.itemcget(current, 'outline') == 'red':
            cv.itemconfig(current, width=2,
                outline='yellow')
        # 否则,将颜色改为red
        else:
             cv.itemconfig(current, width=2,
                outline='red')
    global t
    # 通过定时器指定0.2秒之后执行show_current函数
    t = threading.Timer(0.2, show_current)
    t.start()
# 通过定时器指定0.2秒之后执行show_current函数
t = threading.Timer(0.2, show_current)
t.start()
# 分别创建矩形、椭圆、和圆
rect = cv.create_rectangle(30, 30, 250, 200,
    fill='magenta', width='0')
oval = cv.create_oval(180, 50, 380, 180,
    fill='yellow', width='0')
circle = cv.create_oval(120, 150, 300, 330,
    fill='pink', width='0')
bottomF = Frame(root)
bottomF.pack(fill=X,expand=True)
liftbn = Button(bottomF, text='向上',
    # 将椭圆移动到它上面的item之上
    command=lambda : cv.tag_raise(oval, cv.find_above(oval)))
liftbn.pack(side=LEFT, ipadx=10, ipady=5, padx=3)
lowerbn = Button(bottomF, text='向下',
    # 将椭圆移动到它下面的item之下
    command=lambda : cv.tag_lower(oval, cv.find_below(oval)))
lowerbn.pack(side=LEFT, ipadx=10, ipady=5, padx=3)
def change_fill():
    # 弹出颜色选择框,让用户选择颜色
    fill_color = colorchooser.askcolor(parent=root,
        title='选择填充颜色',
        # 初始颜色设置为椭圆当前的填充色(fill选项值)
        color =  cv.itemcget(oval, 'fill'))
    if fill_color is not None:
        cv.itemconfig(oval, fill=fill_color[1])
fillbn = Button(bottomF, text='改变填充色',
    # 该按钮触发change_fill函数
    command=change_fill)
fillbn.pack(side=LEFT, ipadx=10, ipady=5, padx=3)
def change_outline():
    # 弹出颜色选择框,让用户选择颜色
    outline_color = colorchooser.askcolor(parent=root,
        title='选择边框颜色',
        # 初始颜色设置为椭圆当前的边框色(outline选项值)
        color = cv.itemcget(oval, 'outline'))
    if outline_color is not None:
        cv.itemconfig(oval, outline=outline_color[1],
            width=4)
outlinebn = Button(bottomF, text='改变边框色',
    # 该按钮触发change_outline函数
    command=change_outline)
outlinebn.pack(side=LEFT, ipadx=10, ipady=5, padx=3)
movebn = Button(bottomF, text='右下移动',
    # 调用move方法移动图形项
    command=lambda : cv.move(oval, 15, 10))
movebn.pack(side=LEFT, ipadx=10, ipady=5, padx=3)
coordsbn = Button(bottomF, text='位置复位',
    # 调用coords方法重设图形项的大小和位置
    command=lambda : cv.coords(oval, 180, 50, 380, 180))
coordsbn.pack(side=LEFT, ipadx=10, ipady=5, padx=3)
# 再次添加Frame容器
bottomF = Frame(root)
bottomF.pack(fill=X,expand=True)
zoomoutbn = Button(bottomF, text='缩小',
    # 调用scale方法对图形项进行缩放
    # 前面两个坐标指定缩放中心,后面两个参数指定横向、纵向的缩放比
    command=lambda : cv.scale(oval, 180, 50, 0.8, 0.8))
zoomoutbn.pack(side=LEFT, ipadx=10, ipady=5, padx=3)
zoominbn = Button(bottomF, text='放大',
    # 调用scale方法对图形项进行缩放
    # 前面两个坐标指定缩放中心,后面两个参数指定横向、纵向的缩放比
    command=lambda : cv.scale(oval, 180, 50, 1.2, 1.2))
zoominbn.pack(side=LEFT, ipadx=10, ipady=5, padx=3)
def select_handler(ct):
    global current, current_outline, current_width
    # 如果ct元组包含了选中项
    if ct is not None and len(ct) > 0:
        ct = ct[0]
        # 如果current对应的图形项不为空
        if current is not None:
            # 恢复current对应的图形项的边框
            cv.itemconfig(current, outline=current_outline,
                width = current_width)
        # 获取当前选中图形项的边框信息
        current_outline = cv.itemcget(ct, 'outline')
        current_width = cv.itemcget(ct, 'width')
        # 使用current保存当前选中项
        current = ct
def click_handler(event):
    # 获取当前选中的图形项
    ct = cv.find_closest(event.x, event.y)
    # 调用select _handler处理选中图形项
    select_handler(ct)
def click_select():
    # 取消为“框选”绑定的两个事件处理函数
    cv.unbind('<B1-Motion>')
    cv.unbind('<ButtonRelease-1>')
    # 为“点选”绑定鼠标点击的事件处理函数
    cv.bind('<Button-1>', click_handler)
clickbn = Button(bottomF, text='点选图形项',
    # 该按钮触发click_select函数
    command=click_select)
clickbn.pack(side=LEFT, ipadx=10, ipady=5, padx=3)
# 记录鼠标拖动的第一个点的x、y坐标
firstx = firsty = None
# 记录前一次绘制的、代表选择区的虚线框
prev_select = None
def drag_handler(event):
    global firstx, firsty, prev_select
    # 刚开始拖动时,用鼠标位置为firstx、firsty赋值
    if firstx is None and firsty is None:
        firstx, firsty = event.x, event.y
    leftx, lefty = min(firstx, event.x), min(firsty, event.y)
    rightx, righty = max(firstx, event.x), max(firsty, event.y)
    # 删除上一次绘制的虚线选择框
    if prev_select is not None:
        cv.delete(prev_select)
    # 重新绘制虚线选择框
    prev_select = cv.create_rectangle(leftx, lefty, rightx, righty,
        dash=2)
def release_handler(event):
    global firstx, firsty
    if prev_select is not None:
        cv.delete(prev_select)
    if firstx is not None and firsty is not None:
        leftx, lefty = min(firstx, event.x), min(firsty, event.y)
        rightx, righty = max(firstx, event.x), max(firsty, event.y)
        firstx = firsty = None
        # 获取当前选中的图形项
        ct = cv.find_enclosed(leftx, lefty, rightx, righty)
        # 调用select _handler处理选中图形项
        select_handler(ct)
def rect_select():
    # 取消为“点选”绑定的事件处理函数
    cv.unbind('<Button-1>')
    # 为“框选”绑定鼠标拖动、鼠标释放的事件处理函数
    cv.bind('<B1-Motion>', drag_handler)
    cv.bind('<ButtonRelease-1>', release_handler)
rectbn = Button(bottomF, text='框选图形项',
    # 该按钮触发rect_select函数
    command=rect_select)
rectbn.pack(side=LEFT, ipadx=10, ipady=5, padx=3)
deletebn = Button(bottomF, text='删除',
    # 删除图形项
    command=lambda : cv.delete(oval))
deletebn.pack(side=LEFT, ipadx=10, ipady=5, padx=3)
root.mainloop()

  上面代码画了三个图形,依次是:矩形、椭圆和圆。椭圆是第二个绘制的,所以矩形的上面、圆的下面,如图四十二中的1图所示。此时椭圆能挡住矩形,圆又能挡住椭圆。

上面代码中的第47行代码由第一个按钮的事件处理函数触发,这行代码会把椭圆移动到它上面的图形项(圆)之上,此时就会看到椭圆挡住圆,如图四十二中的2图所示。

图四十二  椭圆变化前和变化后的效果

如果单击两次上图中的“向下”按钮,会把椭圆移动到最下面,这是由第51行代码控制实现的,这时会看到椭圆被矩形、圆挡住的效果,如图四十三中的1图所示。

第60行代码调用 Canvas 的itemconfig() 方法改变椭圆的 fill 选项值,这样即可改变椭圆的填充颜色;第72行代码改变椭圆的 outline、width选项值,修改的是椭圆的边框。

第80行代码调用 move() 方法移动图形项。由于在该方法中传入的是两个正数参数,因此椭圆会不断的向下移动。多次单击“右下移动”按钮,会看到图四十三中的2图效果。

图四十三  改变椭圆的位置

第84行代码调用 coords() 方法设置图形项的大小和位置,由“位置复位”按钮触发。在调用该方法时传入的参数与创建椭圆时指定的位置信息完全相同,因此单击“位置复位”后,椭圆会再次回到原来的位置。

第92行、第97行代码调用 Canvas 的 scale() 方法缩放椭圆。第116行代码调用 Canvas 的find_closest()方法获取与指定点最接近的图形项;第155行代码调用 Canvas 的find_enclosed() 方法获取指定矩形区域内的图形项;第170行代码调用 Canvas 的 delete() 方法删除图形项。

 

4、为图形项绑定事件

Canvas 的 tag_bind() 方法可为指定图形项绑定事件处理函数或方法,这样图形项就可以响应用户动作。下面代码为矩形的单击事件绑定事件处理函数。

 1 from tkinter import *
 2 
 3 root = Tk()
 4 # 创建一个 Canvas,设置其背景为白色
 5 cv = Canvas(root, bg='white')
 6 cv.pack()
 7 # 创建一个矩形
 8 cv.create_rectangle(30, 30, 220, 150, width=8, tags=('r1', 'r2', 'r3'))
 9 
10 def first(event):
11     print('第一次的函数')
12 def second(event):
13     print('第二次的函数')
14 
15 # 为指定图形项的左键单击事件绑定事件处理函数
16 cv.tag_bind('r1', '<Button-1>', first)
17 # add 为 True 表示添加,否则表示替代
18 cv.tag_bind('r1', '<Button-1>', second, add=True)
19 root.mainloop()

上面代码第16行为 r1 对应的图形项的左键单击事件绑定事件处理函数,第18行依然为 r1 对应的图形项的左键单击事件绑定事件处理函数,其中 add 选项为 True 表示为该图形项再次添加一个事件处理函数,此时为该图形项的单击事件绑定了两个事件处理函数。如果add 选项为 False,则第二次添加的事件处理函数会取代第一次添加的事件处理函数。运行代码,单击图形项的边框就可看到后台的输出信息。

下面代码实现一个更完善的绘图程序,可绘制直线、矩形、椭圆、多边形。此外,还可通过左键来选中所绘的图形,也可通过鼠标右键拖动来移动图形项。代码如下:

from tkinter import *
from tkinter import ttk
from tkinter import colorchooser
import threading

class App:
    def __init__(self, master):
        self.master = master
        # 保存设置初始的边框宽度
        self.width = IntVar()
        self.width.set(1)
        # 保存设置初始的边框颜色
        self.outline = 'black'
        # 保存设置初始的填充颜色
        self.fill = None
        # 记录拖动时前一个点的x、y坐标
        self.prevx = self.prevy = -10
        # 记录拖动开始的第一个点的x、y坐标
        self.firstx = self.firsty = -10
        # 记录拖动右键来移动图形时前一个点的x、y坐标
        self.mv_prevx = self.mv_prevy = -10
        # item_type记录要绘制哪种图形
        self.item_type = 0
        self.points = []
        self.init_widgets()
        self.temp_item = None
        self.temp_items = []
        # 初始化选中的图形项
        self.choose_item = None
    # 创建界面组件
    def init_widgets(self):
        self.cv = Canvas(root, background='white')
        self.cv.pack(fill=BOTH, expand=True)
        # 为鼠标左键拖动事件、鼠标左键释放事件绑定处理函数
        self.cv.bind('<B1-Motion>', self.drag_handler)
        self.cv.bind('<ButtonRelease-1>', self.release_handler)
        # 为鼠标左键双击事件绑定处理函数
        self.cv.bind('<Double-1>', self.double_handler)
        f = ttk.Frame(self.master)
        f.pack(fill=X)
        self.bns = []
        # 采用循环创建多个按钮,用于绘制不同的图形
        for i, lb in enumerate(('直线', '矩形', '椭圆', '多边形', '铅笔')):
            bn = Button(f, text=lb, command=lambda i=i: self.choose_type(i))
            bn.pack(side=LEFT, ipadx=8,ipady=5, padx=5)
            self.bns.append(bn)
        # 默认选中直线
        self.bns[self.item_type]['relief'] = SUNKEN
        ttk.Button(f, text='边框颜色',
            command=self.choose_outline).pack(side=LEFT, ipadx=8,ipady=5, padx=5)
        ttk.Button(f, text='填充颜色',
            command=self.choose_fill).pack(side=LEFT, ipadx=8,ipady=5, padx=5)
        om = ttk.OptionMenu(f,
            self.width, # 绑定变量
            '1', # 设置初始选中值
            '0', # 以下多个值用于设置菜单项
            '1',
            '2',
            '3',
            '4',
            '5',
            '6',
            '7',
            '8',
            command = None)
        om.pack(side=LEFT, ipadx=8,ipady=5, padx=5)
    def choose_type(self, i):
        # 将所有按钮恢复默认状态
        for b in self.bns: b['relief'] = RAISED
        # 将当前按钮设置选中样式
        self.bns[i]['relief'] = SUNKEN
        # 设置要绘制的图形
        self.item_type = i
    # 处理选择边框颜色的方法
    def choose_outline(self):
        # 弹出颜色选择对话框
        select_color = colorchooser.askcolor(parent=self.master,
            title="请选择边框颜色", color=self.outline)
        if select_color is not None:
            self.outline = select_color[1]
    # 处理选择填充颜色的方法
    def choose_fill(self):
        # 弹出颜色选择对话框
        select_color = colorchooser.askcolor(parent=self.master,
            title="请选择填充颜色", color=self.fill)
        if select_color is not None:
            self.fill = select_color[1]
        else:
            self.fill = None
    def drag_handler(self, event):
        # 如果是绘制直线
        if self.item_type == 0:
            # 如果第一个点不存在(self.firstx 和 self.firsty都小于0)
            if self.firstx < -1 and self.firsty < -1:
                self.firstx, self.firsty = event.x, event.y
            # 删除上一次绘制的虚线图形
            if self.temp_item is not None:
                self.cv.delete(self.temp_item)
            # 重新绘制虚线
            self.temp_item = self.cv.create_line(self.firstx, self.firsty,
                event.x, event.y, dash=2)
        # 如果是绘制矩形或椭圆
        if self.item_type == 1 or self.item_type == 2:
            # 如果第一个点不存在(self.firstx 和 self.firsty都小于0)
            if self.firstx < -1 and self.firsty < -1:
                self.firstx, self.firsty = event.x, event.y
            # 删除上一次绘制的虚线图形
            if self.temp_item is not None:
                self.cv.delete(self.temp_item)
            leftx, lefty = min(self.firstx, event.x), min(self.firsty, event.y)
            rightx, righty = max(self.firstx, event.x), max(self.firsty, event.y)
            # 重新绘制虚线选择框
            self.temp_item = self.cv.create_rectangle(leftx, lefty, rightx, righty,
                dash=2)
        if self.item_type == 3:
            self.draw_polygon = True
            # 如果第一个点不存在(self.firstx 和 self.firsty都小于0)
            if self.firstx < -1 and self.firsty < -1:
                self.firstx, self.firsty = event.x, event.y
            # 删除上一次绘制的虚线图形
            if self.temp_item is not None:
                self.cv.delete(self.temp_item)
            # 重新绘制虚线
            self.temp_item = self.cv.create_line(self.firstx, self.firsty,
                event.x, event.y, dash=2)
        if self.item_type == 4:
            # 如果前一个点存在(self.prevx 和 self.prevy都大于0)
            if self.prevx > 0 and self.prevy > 0:
                self.cv.create_line(self.prevx, self.prevy, event.x, event.y,
                    fill=self.outline, width=self.width.get())
            self.prevx, self.prevy = event.x, event.y
    def item_bind(self, t):
        # 为鼠标右键拖动事件绑定处理函数
        self.cv.tag_bind(t, '<B3-Motion>', self.move)
        # 为鼠标右键释放事件绑定处理函数
        self.cv.tag_bind(t, '<ButtonRelease-3>', self.move_end)
    def release_handler(self, event):
        # 删除临时绘制的虚线图形项
        if self.temp_item is not None:
            # 如果不是绘制多边形
            if self.item_type != 3:
                self.cv.delete(self.temp_item)
            # 如果绘制多边形,将之前绘制的虚线先保存下来,以便后面删除它们
            else:
                self.temp_items.append(self.temp_item)
        self.temp_item = None
       # 如果是绘制直线
        if self.item_type == 0:
            # 如果第一个点存在(self.firstx 和 self.firsty都大于0)
            if self.firstx > 0 and self.firsty > 0:
                # 绘制实际的直线
                t = self.cv.create_line(self.firstx, self.firsty,
                    event.x, event.y, fill=self.outline, width=self.width.get())
                # 为鼠标左键单击事件绑定处理函数,用于选择被单击的图形项
                self.cv.tag_bind(t, '<Button-1>',
                    lambda event=event, t=t: self.choose_item_handler(event,t))
                self.item_bind(t)
        # 如果是绘制矩形或椭圆
        if self.item_type == 1 or self.item_type == 2:
            # 如果第一个点存在(self.firstx 和 self.firsty都大于0)
            if self.firstx > 0 and self.firsty > 0:
                leftx, lefty = min(self.firstx, event.x), min(self.firsty, event.y)
                rightx, righty = max(self.firstx, event.x), max(self.firsty, event.y)
                if self.item_type == 1:
                    # 绘制实际的矩形
                    t = self.cv.create_rectangle(leftx, lefty, rightx, righty,
                        outline=self.outline, fill=self.fill, width=self.width.get())
                if self.item_type == 2:
                    # 绘制实际的椭圆
                    t = self.cv.create_oval(leftx, lefty, rightx, righty,
                        outline=self.outline, fill=self.fill, width=self.width.get())
                # 为鼠标左键单击事件绑定处理函数,用于选择被单击的图形项
                self.cv.tag_bind(t, '<Button-1>',
                    lambda event=event, t=t: self.choose_item_handler(event,t))
                self.item_bind(t)
        if self.item_type != 3:
            self.prevx = self.prevy = -10
            self.firstx = self.firsty = -10
        # 如果正在绘制多边形
        elif(self.draw_polygon):
            # 将第一个点添加到列表中
            self.points.append((self.firstx, self.firsty))
            self.firstx, self.firsty = event.x, event.y
    def double_handler(self, event):
        # 只处理绘制多边形的情形
        if self.item_type == 3:
            t = self.cv.create_polygon(*self.points,
                outline=self.outline, fill="" if self.fill is None else self.fill,
                width=self.width.get())
            # 为鼠标左键单击事件绑定处理函数,用于选择被单击的图形项
            self.cv.tag_bind(t, '<Button-1>',
                lambda event=event, t=t: self.choose_item_handler(event,t))
            self.item_bind(t)
            # 清空所有保存的点数据
            self.points.clear()
            # 将self.firstx = self.firsty设置为-10,停止绘制
            self.firstx = self.firsty = -10
            # 删除所有临时的虚线框
            for it in self.temp_items: self.cv.delete(it)
            self.temp_items.clear()
            self.draw_polygon = False
    # 根据传入的参数t来选中对应的图形项
    def choose_item_handler(self, event, t):
        # 使用self.choose_item保存当前选中项
        self.choose_item = t
    # 定义移动图形项的方法
    def move(self, event):
        # 如果被选中图形项不为空,才可以执行移动
        if self.choose_item is not None:
            # 如果前一个点存在(self.mv_prevx 和 self.mv_prevy都大于0)
            if self.mv_prevx > 0 and self.mv_prevy > 0:
                # 移动选中的图形项
                self.cv.move(self.choose_item, event.x - self.mv_prevx,
                    event.y - self.mv_prevy)
            self.mv_prevx, self.mv_prevy = event.x, event.y
    # 结束移动的方法
    def move_end(self, event):
        self.mv_prevx = self.mv_prevy = -10
    def delete_item(self, event):
        # 如果被选中的item不为空,删除被选中的图形项
        if self.choose_item is not None:
            self.cv.delete(self.choose_item)
root = Tk()
root.title("绘图工具")
root.iconbitmap('images/logo.ico')
root.geometry('800x680')
app = App(root)
root.bind('<Delete>', app.delete_item)
root.mainloop()

  运行这段代码,可以在绘图界面中绘制各种图形,如图四十四所示。 (图片省略,图四十四 绘图工具)

 

5、 绘制动画

动画效果:使用一个定时器,周期性的改变界面上图形项的颜色、大小、位置等选项,看上去就是所谓的“动画”。

下面是一个简单的桌球游戏,通过这个游戏来了解 Canvas 绘制动画。游戏界面上有一个小球,该小球会在界面上滚动,遇到边界或用户挡板会反弹。该游戏涉及两个动画。

(1)、小球转动:小球转动是一个“逐帧动画”,需要循环显示多张转动的小球图片,这样就会看到小球转动的效果。

(2)、小球移动:只要改变小球的坐标就可以控制小球移动。

为了让用户控制挡板移动,还要为 Canvas 的向左箭头、向右箭头绑定事件处理函数。代码如下:

from tkinter import *
from tkinter import messagebox
import threading
import random
GAME_WIDTH = 500
GAME_HEIGHT = 680
BOARD_X = 230
BOARD_Y = 600
BOARD_WIDTH = 80
BALL_RADIUS = 9
class App:
    def __init__(self, master):
        self.master = master
        # 记录小球动画的第几帧
        self.ball_index = 0
        # 记录游戏是否失败的旗标
        self.is_lose = False
        # 初始化记录小球位置的变量
        self.curx = 260
        self.cury = 30
        self.boardx = BOARD_X
        self.init_widgets()
        self.vx = random.randint(3, 6) # x方向的速度
        self.vy = random.randint(5, 10) # y方向的速度
        # 通过定时器指定0.1秒之后执行moveball函数
        self.t = threading.Timer(0.1, self.moveball)
        self.t.start()
    # 创建界面组件
    def init_widgets(self):
        self.cv = Canvas(root, background='white',
            width=GAME_WIDTH, height=GAME_HEIGHT)
        self.cv.pack()
        # 让画布得到焦点,从而可以响应按键事件
        self.cv.focus_set()
        self.cv.bms = []
        # 初始化小球的动画帧
        for i in range(8):
            self.cv.bms.append(PhotoImage(file='images/ball_' + str(i+1) + '.gif'))
        # 绘制小球
        self.ball = self.cv.create_image(self.curx, self.cury,
            image=self.cv.bms[self.ball_index])
        self.board = self.cv.create_rectangle(BOARD_X, BOARD_Y,
            BOARD_X + BOARD_WIDTH, BOARD_Y + 20, width=0, fill='lightblue')
        # 为向左箭头按键绑定事件,挡板左移
        self.cv.bind('<Left>', self.move_left)
        # 为向右箭头按键绑定事件,挡板右移
        self.cv.bind('<Right>', self.move_right)
    def move_left(self, event):
        if self.boardx <= 0:
            return
        self.boardx -= 5
        self.cv.coords(self.board, self.boardx, BOARD_Y,
            self.boardx + BOARD_WIDTH, BOARD_Y + 20)
    def move_right(self, event):
        if self.boardx + BOARD_WIDTH >= GAME_WIDTH:
            return
        self.boardx += 5
        self.cv.coords(self.board, self.boardx, BOARD_Y,
            self.boardx + BOARD_WIDTH, BOARD_Y + 20)
    def moveball(self):
        self.curx += self.vx
        self.cury += self.vy
        # 小球到了右边墙壁,转向
        if self.curx + BALL_RADIUS >= GAME_WIDTH:
            self.vx = -self.vx
        # 小球到了左边墙壁,转向
        if self.curx - BALL_RADIUS <= 0:
            self.vx = -self.vx
        # 小球到了上边墙壁,转向
        if self.cury - BALL_RADIUS <= 0:
            self.vy = -self.vy
        # 小球到了挡板处
        if self.cury + BALL_RADIUS >= BOARD_Y:
            # 如果在挡板范围内
            if self.boardx <= self.curx <= (self.boardx + BOARD_WIDTH):
                self.vy = -self.vy
            else:
                messagebox.showinfo(title='失败', message='您已经输了')
                self.is_lose = True
        self.cv.coords(self.ball, self.curx, self.cury)
        self.ball_index += 1
        self.cv.itemconfig(self.ball, image=self.cv.bms[self.ball_index % 8])
        # 如果游戏还未失败,让定时器继续执行
        if not self.is_lose:
            # 通过定时器指定0.1秒之后执行moveball函数
            self.t = threading.Timer(0.1, self.moveball)
            self.t.start()
root = Tk()
root.title("弹球游戏")
root.iconbitmap('images/logo.ico')
root.geometry('%dx%d' % (GAME_WIDTH, GAME_HEIGHT))
# 禁止改变窗口大小
root.resizable(width=False, height=False)
App(root)
root.mainloop()

  上面代码中第26、27行代码通过线程启动一个定时器,定时器控制 moveball() 方法每隔 0.1 秒执行一次,而 moveball() 方法中对应的第80行代码用于改变小球的坐标,这样可以实现小球的移动效果;第82行代码用于改变小球的图片,实现小球滚动的效果。

 

九、小结

(1)、Tkinter GUI编程基本知识,以及基本概念。

(2)、Tkinter GUI的三种布局管理器。

(3)、Tkinter GUI编程的事件绑定机制(重点),使用 command 选项绑定事件处理函数,以及使用 bind() 方法绑定事件处理函数。

(4)、Tkinter GUI的常用组件,有按钮、文本框、对话框、菜单等。

(5)、Tkinter GUI中绘图,包括绘制几何图形和位图。通过桌球游戏实现动画效果。

posted @ 2020-01-13 15:04  远方那一抹云  阅读(3147)  评论(0编辑  收藏  举报