Python GUI Cookbook —— 创建 GUI 窗体并添加 Widgets

开发我们的第一个 Python GUI 程序

创建第一个 Python GUI 程序

使用 Python 内置的 tkinter 模块,仅需 4 行代码就能创建一个 GUI。

下面是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3

import tkinter as tk

# Create instance
window = tk.Tk()

# Add a title
window.title("My First Python GUI")

# Start GUI
window.mainloop()

防止窗口大小被调整

使用 tkinter 创建的 GUI 默认是能够调整大小的,但有时候我们希望窗体保持特定的大小,因此需要学习如何防止用户调整 GUI 大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3

import tkinter as tk

# Create instance
window = tk.Tk()

# Add a title
window.title("My First Python GUI")

# Disable resizing the GUI by passing in False/False
window.resizable(False, False)

# Enable resizing x-dimension, disable y-dimension
# window.resizable(True, False)

# Start GUI
window.mainloop()

在窗体中添加 label

label 是一种简单的 widget,它能够向窗体中添加值(value),能够解释其他 widgets 的目的,提供额外的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python3

import tkinter as tk
from tkinter import ttk

# Create instance
window = tk.Tk()

# Add a title
window.title("My First Python GUI")

# Add a Label
ttk.Label(window, text = 'A Label').grid(column=0, row=0)

# Start GUI
window.mainloop()

这里我们从 tkinter 包导入了一个独立模块 ttk。ttk 模块有一些高级 widgets 能够让我们的 GUI 看起来更好看。ttk 代表 themed tk。

  • 在这个实例中我们使用了 ttk.Label 构造器设置文本属性(text property)
  • 使用了网格布局管理器(grid layout manager)
  • 可以发现窗体突然变小了好多,这是因为我们在窗体中添加了 widget,触发了优化,尽量使用小空间显示 widget(s)。

创建按钮并改变其文本属性

这里我们将添加一个 button widget,并使用该按钮(button)改变其他 widget 的属性。我们将学习 Python GUI 的回调函数(callback function)和事件处理机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env python3

import tkinter as tk
from tkinter import ttk

# Create instance
window = tk.Tk()

# Add a title
window.title("My First Python GUI")

# Adding a Label that will get modified
a_label = ttk.Label(window, text = 'A Label')
a_label.grid(column=0, row=0)


# Button click Event Function
def click_me():
action.configure(text='** I have been Clicked! **')
a_label.configure(foreground='red')
a_label.configure(text='A Red Label')

# Adding a Button
action = ttk.Button(window, text='Click Me!', command=click_me)
action.grid(column=1, row=0)

# Start GUI
window.mainloop()

GUIs 是事件驱动的。点击按钮产生一个事件。我们将事件产生时会发生的事与回调函数绑定。通过 ttk.Button widget 的 command 属性调用它,注意调用时不需要圆括号,只需使用名字 click_me

文本框 Text Box widgets

在 tkinter 中,只有一行的 textbox widget 被称为 Entry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/env python3

import tkinter as tk
from tkinter import ttk

# Create instance
window = tk.Tk()

# Add a title
window.title("My First Python GUI")

# Adding a Label that will get modified
a_label = ttk.Label(window, text = 'A Label')
a_label.grid(column=0, row=0)


# Modified Button click Event Function
def click_me():
action.configure(text='Hello ' + name.get())

# Changing our Label
ttk.Label(window, text='Enter a name: ').grid(column=0, row=0)

# Adding a Text Box Entry widget
name = tk.StringVar()
name_entered = ttk.Entry(window, width=12, textvariable=name)
name_entered.grid(column=0, row=1)

# Adding a Button
action = ttk.Button(window, text="Click Me!", command=click_me)
action.grid(column=1, row=1)

# Start GUI
window.mainloop()

在 tkinter 中,我们需要声明 name 变量为 tk.StringVar()。因为 tkinter 不是 Python。我们只是能够在 Python 中使用它,但它们并不是同种语言。

给 widget 设置焦点,禁用 widgets

只需调用 focus() 方法就能给一个 widget 设置焦点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/env python3

import tkinter as tk
from tkinter import ttk

# Create instance
window = tk.Tk()

# Add a title
window.title("My First Python GUI")

# Adding a Label that will get modified
a_label = ttk.Label(window, text = 'A Label')
a_label.grid(column=0, row=0)


# Modified Button click Event Function
def click_me():
action.configure(text='Hello ' + name.get())

# Changing our Label
ttk.Label(window, text='Enter a name:').grid(column=0, row=0)

# Adding a Text Box Entry widget
name = tk.StringVar()
name_entered = ttk.Entry(window, width=12, textvariable=name)
name_entered.grid(column=0, row=1)

# Adding a Button
action = ttk.Button(window, text="Click Me!", command=click_me)
action.grid(column=1, row=1)
action.configure(state='disabled') # Disable the Button Widget

name_entered.focus_set() # Place cursor into name Entry

# Start GUI
window.mainloop()

组合框 Combo box widgets

下拉式组合框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/env python3

import tkinter as tk
from tkinter import ttk

# Create instance
window = tk.Tk()

# Add a title
window.title("My First Python GUI")

# Adding a Label that will get modified
a_label = ttk.Label(window, text = 'A Label')
a_label.grid(column=0, row=0)


# Modified Button click Event Function
def click_me():
action.configure(text='Hello ' + name.get()+ ' ' + number_chosen.get())

# Changing our Label
ttk.Label(window, text='Enter a name:').grid(column=0, row=0)

# Adding a Textbox Entry widget
name = tk.StringVar()
name_entered = ttk.Entry(window, width=12, textvariable=name)
name_entered.grid(column=0, row=1) # column 0

# Adding a Button
action = ttk.Button(window, text="Click Me!", command=click_me)
action.grid(column=2, row=1) # change column to 2

ttk.Label(window, text='Choose a number:').grid(column=1, row=0)
number = tk.StringVar()
number_chosen = ttk.Combobox(window, width=12, textvariable=number)
number_chosen['values'] = (1, 2, 4, 42, 100)
number_chosen.grid(column=1, row=1) # Combobox in column 1
number_chosen.current(0)

name_entered.focus() # Place cursor into name Entry

# Start GUI
window.mainloop()

如果要限制用户,让他们只能选择程序中给出的选项,则要向构造器中传 Combobox 的 state 属性

1
2
3
4
5
6
7
8
9
10
11

[...]

number = tk.StringVar()
number_chosen = ttk.Combobox(window, width=12, textvariable=number, state='readonly')
number_chosen['values'] = (1, 2, 4, 42, 100)
number_chosen.grid(column=1, row=1) # Combobox in column 1
number_chosen.current(0)

[...]

创建具有不同初始状态的复选按钮

Checkbutton widgets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#!/usr/bin/env python3

import tkinter as tk
from tkinter import ttk

# Create instance
window = tk.Tk()

# Add a title
window.title("My First Python GUI")

# Adding a Label that will get modified
a_label = ttk.Label(window, text = 'A Label')
a_label.grid(column=0, row=0)


# Modified Button click Event Function
def click_me():
action.configure(text='Hello ' + name.get() + ' ' + number_chosen.get())

# Changing our Label
ttk.Label(window, text='Enter a name:').grid(column=0, row=0)

# Adding a Textbox Entry widget
name = tk.StringVar()
name_entered = ttk.Entry(window, width=12, textvariable=name)
name_entered.grid(column=0, row=1) # column 0

# Adding a Button
action = ttk.Button(window, text="Click Me!", command=click_me)
action.grid(column=2, row=1) # change column to 2

# Creating three checkbuttons
ttk.Label(window, text='Choose a number:').grid(column=1, row=0)
number = tk.StringVar()
number_chosen = ttk.Combobox(window, width=12, textvariable=number, state='readonly')
number_chosen['values'] = (1, 2, 4, 42, 100)
number_chosen.grid(column=1, row=1) # Combobox in column 1
number_chosen.current(0)

# Creating three checkbuttons
chVarDis = tk.IntVar()
check1 = tk.Checkbutton(window, text='Disabled', variable=chVarDis, state='disabled')
check1.select()
check1.grid(column=0, row=4, sticky=tk.W)

chVarUn = tk.IntVar()
check2 = tk.Checkbutton(window, text='UnChecked', variable=chVarUn)
check2.deselect()
check2.grid(column=1, row=4, sticky=tk.W)

chVarEn = tk.IntVar()
check3 = tk.Checkbutton(window, text='Enabled', variable=chVarEn)
check3.select()
check3.grid(column=2, row=4, sticky=tk.W)

name_entered.focus() # Place cursor into name Entry
# Start GUI
window.mainloop()

将网格的 sticky 属性设置为 tk.W 意味着该 widget 向网格的西(west)面对齐。

使用单选按钮 radio button widgets

Radiobutton widgets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#!/usr/bin/env python3

import tkinter as tk
from tkinter import ttk

# Create instance
window = tk.Tk()

# Add a title
window.title("My First Python GUI")

# Adding a Label that will get modified
a_label = ttk.Label(window, text = 'A Label')
a_label.grid(column=0, row=0)


# Modified Button click Event Function
def click_me():
action.configure(text='Hello ' + name.get() + ' ' + number_chosen.get())

# Changing our Label
ttk.Label(window, text='Enter a name:').grid(column=0, row=0)

# Adding a Textbox Entry widget
name = tk.StringVar()
name_entered = ttk.Entry(window, width=12, textvariable=name)
name_entered.grid(column=0, row=1) # column 0

# Adding a Button
action = ttk.Button(window, text="Click Me!", command=click_me)
action.grid(column=2, row=1) # change column to 2

ttk.Label(window, text='Choose a number:').grid(column=1, row=0)
number = tk.StringVar()
number_chosen = ttk.Combobox(window, width=12, textvariable=number, state='readonly')
number_chosen['values'] = (1, 2, 4, 42, 100)
number_chosen.grid(column=1, row=1) # Combobox in column 1
number_chosen.current(0)

# Creating three checkbuttons
chVarDis = tk.IntVar()
check1 = tk.Checkbutton(window, text='Disabled', variable=chVarDis, state='disabled')
check1.select()
check1.grid(column=0, row=4, sticky=tk.W)

chVarUn = tk.IntVar()
check2 = tk.Checkbutton(window, text='UnChecked', variable=chVarUn)
check2.deselect()
check2.grid(column=1, row=4, sticky=tk.W)

chVarEn = tk.IntVar()
check3 = tk.Checkbutton(window, text='Enabled', variable=chVarEn)
check3.deselect()
check3.grid(column=2, row=4, sticky=tk.W)

# GUI callback function
def checkCallback(*ignoredArgs):
if chVarUn.get():
check3.configure(state='disabled')
else:
check3.configure(state='normal')
if chVarEn.get():
check2.configure(state='disabled')
else:
check2.configure(state='normal')

# trace the state of the two checkbutton
chVarUn.trace('w', lambda unused0, unused1, unused2 : checkCallback())
chVarEn.trace('w', lambda unused0, unused1, unused2 : checkCallback())

# Radiobutton Globals
COLOR1 = 'Blue'
COLOR2 = 'Gold'
COLOR3 = 'Red'

# Radiobutton Callback
def radCall():
radSel = radVar.get()
if radSel == 1:
window.configure(background=COLOR1)
elif radSel == 2:
window.configure(background=COLOR2)
elif radSel == 3:
window.configure(background=COLOR3)

# Create three Radiobuttons using one variable
radVar = tk.IntVar()

rad1 = tk.Radiobutton(window, text=COLOR1, variable=radVar,
value=1, command=radCall)
rad1.grid(column=0, row=5, sticky=tk.W, columnspan=3)

rad2 = tk.Radiobutton(window, text=COLOR2, variable=radVar,
value=2, command=radCall)
rad2.grid(column=1, row=5, sticky=tk.W, columnspan=3)

rad3 = tk.Radiobutton(window, text=COLOR3, variable=radVar,
value=3, command=radCall)
rad3.grid(column=2, row=5, sticky=tk.W, columnspan=3)


name_entered.focus() # Place cursor into name Entry
# Start GUI
window.mainloop()

使用滚动文本 scrolled text widgets

ScrolledText 要比简单的 Entry 大得多并且跨越多行。当文本大于 ScrolledText widget 的高度时,将自动启用垂直滚动条。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext

[...]

# Using a scrlled text control
scrol_w = 40
scrol_h = 3
scr = scrolledtext.ScrolledText(window, width=scrol_w, height=scrol_h, wrap=tk.WORD)
scr.grid(column=0, columnspan=3)

name_entered.focus() # Place cursor into name Entry
# Start GUI
window.mainloop()

  • 通过将 wrap 属性设置为 tk.WORD 来告诉 ScrolledText widget 通过单词来断行。 默认的是 tk.CHAR,以字符来断行。
  • 对于 ScrolledText widget ,将网格的 columnspan 属性设置为 3,能够让该 widget 横跨 3 列。默认情况下只有 1 列。

重构

可以发现在上面的代码中有很多冗余,这里我们将重构它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#!/usr/bin/env python3

import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext

# Create instance
window = tk.Tk()

# Add a title
window.title("My First Python GUI")

# Adding a Label that will get modified
a_label = ttk.Label(window, text = 'A Label')
a_label.grid(column=0, row=0)


# Modified Button click Event Function
def click_me():
action.configure(text='Hello ' + name.get() + ' ' + number_chosen.get())

# Changing our Label
ttk.Label(window, text='Enter a name:').grid(column=0, row=0)

# Adding a Textbox Entry widget
name = tk.StringVar()
name_entered = ttk.Entry(window, width=12, textvariable=name)
name_entered.grid(column=0, row=1) # column 0

# Adding a Button
action = ttk.Button(window, text="Click Me!", command=click_me)
action.grid(column=2, row=1) # change column to 2

ttk.Label(window, text='Choose a number:').grid(column=1, row=0)
number = tk.StringVar()
number_chosen = ttk.Combobox(window, width=12, textvariable=number, state='readonly')
number_chosen['values'] = (1, 2, 4, 42, 100)
number_chosen.grid(column=1, row=1) # Combobox in column 1
number_chosen.current(0)

# Creating three checkbuttons
chVarDis = tk.IntVar()
check1 = tk.Checkbutton(window, text='Disabled', variable=chVarDis, state='disabled')
check1.select()
check1.grid(column=0, row=4, sticky=tk.W)

chVarUn = tk.IntVar()
check2 = tk.Checkbutton(window, text='UnChecked', variable=chVarUn)
check2.deselect()
check2.grid(column=1, row=4, sticky=tk.W)

chVarEn = tk.IntVar()
check3 = tk.Checkbutton(window, text='Enabled', variable=chVarEn)
check3.deselect()
check3.grid(column=2, row=4, sticky=tk.W)

# GUI callback function
def checkCallback(*ignoredArgs):
if chVarUn.get():
check3.configure(state='disabled')
else:
check3.configure(state='normal')
if chVarEn.get():
check2.configure(state='disabled')
else:
check2.configure(state='normal')

# trace the state of the two checkbutton
chVarUn.trace('w', lambda unused0, unused1, unused2 : checkCallback())
chVarEn.trace('w', lambda unused0, unused1, unused2 : checkCallback())

# First, we change our Radiobutton globals variables into a list
colors = ['Blue', 'Gold', 'Red']

# We have also changed the callback function to be zero-based, using list
# instead of module-level global variables
# Radiobutton Callback
def radCall():
radSel = radVar.get()
if radSel == 1:
window.configure(background=colors[0]) # now zero-based and using list
elif radSel == 2:
window.configure(background=colors[1])
elif radSel == 3:
window.configure(background=colors[3])

# Create three Radiobuttons using one variable
radVar = tk.IntVar()

# Next we are selecting a non-existing index value for radVar
radVar.set(99)

# Now we are creating all three Radiobutton widgets within one loop
for col in range(3):
curRad = tk.Radiobutton(window, text=colors[col], variable=radVar,
value=col, command=radCall)
curRad.grid(column=col, row=5, sticky=tk.W)

# Using a scrlled text control
scrol_w = 40
scrol_h = 3
scr = scrolledtext.ScrolledText(window, width=scrol_w, height=scrol_h, wrap=tk.WORD)
scr.grid(column=0, columnspan=3)

name_entered.focus() # Place cursor into name Entry
# Start GUI
window.mainloop()

运行程序会发现和上面的一样,但代码更简洁清晰。

参考文献

  • Python GUI Programming Cookbook - Second Edition by Burkhard A. Meier