用Tkinter编写交互日记系统
任务要求
- 在上周开发基础上, 完成 极简交互式日记的桌面版本
- 需求如下:
- 每次运行时合理的打印出过往的所有日记
- 一次接收输入一行日记
- 保存为本地文件
添加Label
就像学编程语言第一步教人输出"Hello World",使用Tkinter
的第一步可以尝试去显示一句"Hello World"
代码如下
import Tkinter as tk
root = tk.Tk() # 建立主窗口
myLabel = tk.Label(root,text="Hello World") # 添加'Hello world'标签
myLabel.pack() # 为标签安排在主窗口中的位置
root.mainloop() # 运行Tk程序
在第一句里我没有用from Tkinter import *
将Tkinter
的全部功能导入进来,而是设了个缩写tk,这是因为我想用这种方法让自己更清晰地去记忆哪些类和方法是Tkinter
里的。
然后,Label
的属性如果不需要改变的话,也可以将实例化和pack写在一句里,于是将代码改为
import Tkinter as tk
root = tk.Tk()
tk.Label(root,text="Hello World").pack()
root.mainloop()
添加输入框
接下来需要添加信息输入的功能,对应的Tkinter
组件是Entry
import Tkinter as tk
root = tk.Tk()
tk.Label(root,text="Hello World").pack()
message = tk.Entry(root) # 添加Entry组件
message.pack()
root.mainloop()
接收输入信息
添加了Entry之后需要考虑怎样接受输入的信息
- Entry有个实例方法get(),可以获得输入框里输入的信息
下面代码里先加一个Button
组件,使得按了之后能显示Entry
的内容
import Tkinter as tk
root = tk.Tk()
tk.Label(root,text="Hello World").pack()
message = tk.Entry(root)
message.pack()
def print_content(): # 先写一个打印entry内容的函数,然后赋到button的command属性上
print message.get()
tk.Button(root,text="print",command=print_content).pack()
root.mainloop()
这样每次按这个按钮,在python程序里就输出Entry里的信息。
删除输入框保留的信息
但是每次按按钮之后信息还是留在输入框里,我想做到每次按按钮同时清空输入框里的信息,这样好重新输入Entry有个delete()的实例方法可以做到这点。
所以在button触发的函数里填上一句message.delete(0,'end')
这里的两个参数代表着删除的起止位置。
代码为
import Tkinter as tk
root = tk.Tk()
tk.Label(root,text="Hello World").pack()
message = tk.Entry(root)
message.pack()
def print_content():
print message.get()
message.delete(0,'end')
tk.Button(root,text="print",command=print_content).pack()
root.mainloop()
在输入框添加默认信息
接着我还想在输入框里设置默认信息,好提示用户输入怎样形式的信息。
Entry类里有个实例方法是insert()可以做到这点
但是不太清楚里面参数怎么写,于是搜索
$ pydoc Tkinter.Entry.insert
Help on method insert in Tkinter.Entry:
Tkinter.Entry.insert = insert(self, index, string) unbound Tkinter.Entry method
Insert STRING at INDEX.
(END)
了解到除了需要实例这个参数外,还需要插入点和插入内容两个参数
代码
import Tkinter as tk
root = tk.Tk()
tk.Label(root,text="Hello World").pack()
message = tk.Entry(root)
message.insert(0,"Hi, what's up")
message.pack()
def print_content():
print message.get()
message.delete(0,'end')
tk.Button(root,text="print",command=print_content).pack()
root.mainloop()
用回车键输入信息
在输入框输入完信息之后,我本能的反应是按回车键而不是去按按钮。按回车来确认输入信息,这对应了现在人使用电脑的心智模型,也反映了人们对GUI功能的预期,所以我想达到按回车键跟按按钮同样的功能。
在网上简单浏览了一些教程之后,在代码里添加一句
root.bind('<Return>',print_content)
运行之后,按回车键报错
TypeError: print_content() takes no arguments (1 given)
再仔细查阅文档,发现这里按回车键提供了一个event参数。
于是用匿名函数lambda event:print_content()
来改写,运行成功。
更详细的描述可以用$ pydoc Tkinter.Tk.bind
查看
代码为
import Tkinter as tk
root = tk.Tk()
tk.Label(root,text="Hello World").pack()
message = tk.Entry(root)
message.insert(0,"Hi, what's up")
message.pack()
def print_content():
print message.get()
message.delete(0,'end')
tk.Button(root,text="print",command=print_content).pack()
root.bind('<Return>',lambda event:print_content()) # <Return>代表回车键
root.mainloop()
运用Tk的变量
Tkinter
有自己的变量类,通过使用类里的get()和set()实例方法来操作变量
例如StringVar类
$ pydoc Tkinter.StringVar
里面可以看到
__init__(self, master=None, value=None, name=None)
| Construct a string variable.
|
| MASTER can be given as master widget.
| VALUE is an optional value (defaults to "")
| NAME is an optional Tcl name (defaults to PY_VARnum).
|
| If NAME matches an existing variable and VALUE is omitted
| then the existing value is retained.
get(self)
| Return value of variable as string.
set(self, value)
| Set the variable to VALUE.
因此可以用StringVar类来改写上述的代码
import Tkinter as tk
root = tk.Tk()
tk.Label(root,text="Hello World").pack()
var = tk.StringVar(value="Hi, what's up") # value可以设置默认值
message = tk.Entry(root,textvariable=var) # 将textvariable绑定var
message.pack()
def print_content():
print var.get() # 获取var的值
var.set('') # 将var值清零
tk.Button(root,text="print",command=print_content).pack()
root.bind('<Return>',lambda event:print_content())
root.mainloop()
输入中文
前面的代码都是用英文输入进行测试的,如果使用中文就会报错。
于是先尝试加入magic comment
# coding=utf-8
结果很奇妙,在Sublime Text 3里直接运行输出中文时会报错
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
而在命令行运行则能成功输出中文。
上周日杭州C2T2时跟@haidao谈到过这个问题,haidao提示说可能是terminal和sublime text用了不同的编码来进行输出。
用下列代码来测试
import sys
print sys.stdin.encoding
print sys.stdout.encoding
print sys.getdefaultencoding()
在Sublime Text里直接运行得到的结果是
None
None
ascii
而在terminal里运行的结果是
UTF-8
UTF-8
ascii
看来确实是有差别。
加上下面的代码之后,在Sublime Text里也可以正常输出中文了
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
在Tk里显示信息
Tkinter
的Message
组件可以用来展示信息。
我们先暂时只在message里显示"Show",然后为了避免混淆,把之前的message
变量名改为text_input
代码改为
# coding=utf-8
import Tkinter as tk
root = tk.Tk()
tk.Label(root,text="Hello World").pack()
var = tk.StringVar(value="Hi, what's up")
text_input = tk.Entry(root,textvariable=var)
text_input.pack()
def print_content():
print var.get()
var.set('')
tk.Button(root,text="print",command=print_content).pack()
root.bind('<Return>',lambda event:print_content())
text_output = tk.Message(root,text='Show')
text_output.pack()
root.mainloop()
更新message信息:方法一
可以将原来的变量实例var赋到message
的textvariable
属性上,这样Entry
里的信息变化了,Message
里也跟着变化。
代码
# coding=utf-8
import Tkinter as tk
root = tk.Tk()
tk.Label(root,text="Hello World").pack()
var = tk.StringVar(value="Hi, what's up")
text_input = tk.Entry(root,textvariable=var)
text_input.pack()
def print_content():
print var.get()
var.set('')
tk.Button(root,text="print",command=print_content).pack()
root.bind('<Return>',lambda event:print_content())
text_output = tk.Message(root,textvariable=var) # 将textvariable设为var
text_output.pack()
root.mainloop()
更新message信息:方法二
或者也可以使用Message
类里的config
实例方法来改变显示的值,然后写进print_conent
方法里,这样每次按回车键就能在message里显示之前输入的值。
代码改为
# coding=utf-8
import Tkinter as tk
root = tk.Tk()
tk.Label(root,text="Hello World").pack()
var = tk.StringVar(value="Hi, what's up")
text_input = tk.Entry(root,textvariable=var)
text_input.pack()
def print_content():
text_output.config(text=var.get()) # 通过config更新message里要显示的信息
var.set('')
tk.Button(root,text="print",command=print_content).pack()
root.bind('<Return>',lambda event:print_content())
text_output = tk.Message(root,text='')
text_output.pack()
root.mainloop()
更新message信息:方法三
$ pydoc Tkinter.Message
| __getitem__ = cget(self, key)
| Return the resource value for a KEY given as string.
|
| __setitem__(self, key, value)
可以找到有__getitem__
和__setitem__
两个方法
在字典的取值和赋值中使用的方括号对应的就是这两个方法
+ dict[key] <==> dict.__getitem__(key)
+ dict[key] = value <==> dict.__setitem__(key,value)
因此,我们可以用方括号来替换Message.config语句
text_output['text'] = var.get()
具体代码为
# coding=utf-8
import Tkinter as tk
root = tk.Tk()
tk.Label(root,text="Hello World").pack()
var = tk.StringVar(value="Hi, what's up")
text_input = tk.Entry(root,textvariable=var)
text_input.pack()
def print_content():
text_output['text'] = var.get() # 将var的值赋给text_output的text属性
var.set('')
tk.Button(root,text="print",command=print_content).pack()
root.bind('<Return>',lambda event:print_content())
text_output = tk.Message(root,text='')
text_output.pack()
root.mainloop()
结合1w的代码
第一周代码alan_diary.py
# coding=utf-8
import os
def append_text(text):
f = open('alan_diary.log','a')
f.write(text+'\n')
f.close()
def get_text():
if not os.path.exists('alan_diary.log'):
append_text('')
else:
f = open('alan_diary.log')
text = f.read()
f.close()
return text
if __name__ == '__main__':
while True:
text_input = raw_input('What do you want to write today:\n> ')
if text_input.lower() in ['quit','q','exit']: break
append_text(text_input)
print "you diary:\n", get_text()
我第一周的代码alan_dairy.py
里有两个函数
- append_text(text)将text信息加进
alan_diary.log
里 - get_text()读取'alan_diary.log',然后将返回里面的内容
调用1w代码中这两个函数,2w的代码改为
# coding=utf-8
import Tkinter as tk
from alan_diary import *
root = tk.Tk()
tk.Label(root,text="Hello World").pack()
var = tk.StringVar(value="Hi, what's up")
text_input = tk.Entry(root,textvariable=var)
text_input.pack()
def update_text():
append_text(var.get()) # 将输入的信息更新到log里
text_output.config(text=get_text()) # 读取所有记录显示在message里
var.set('')
tk.Button(root,text="print",command=update_text).pack()
root.bind('<Return>',lambda event:update_text())
text_output = tk.Message(root,text='')
text_output.pack()
root.mainloop()
这样就基本实现了任务要求。
然后再稍微调整一下页面布局
# coding=utf-8
import Tkinter as tk
from alan_diary import *
root = tk.Tk()
root.title("Alan's diary")
root.geometry('400x400')
tk.Label(root,text="Welcome to Alan's diary",pady=20).pack()
var = tk.StringVar(value="What do you want to write today?")
text_input = tk.Entry(root, textvariable=var, width=36)
text_input.pack()
def print_content():
append_text(var.get())
text_output.config(text=get_text())
var.set('')
tk.Button(root,text="print",command=print_content).pack()
root.bind('<Return>',lambda event:print_content())
text_output = tk.Message(root,text='', width=360, pady=20)
text_output.pack()
root.mainloop()
然后看看显示效果
感觉按钮有点多余,可以删掉,标签也可以删掉
改代码为
# coding=utf-8
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
import Tkinter as tk
from alan_diary import *
def enter_and_print(event):
append_text(var.get())
text_output.config(text=get_text())
var.set('')
root = tk.Tk()
root.title("Alan's diary")
var = tk.StringVar(value="What do you want to write today?")
text_input = tk.Entry(root, textvariable=var, width=36, bd=5)
text_input.pack()
root.bind('<Return>',enter_and_print)
text_output = tk.Message(root,text='', width=360, pady=20)
text_output.pack()
root.mainloop()
使用Text组件
接着尝试使用Tkinter
中的Text
组件来替代Message
组件
要更新Text
里显示的信息,需要使用insert
方法
代码改为
# coding=utf-8
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
import Tkinter as tk
from alan_diary import *
def enter_and_print(event):
append_text(var.get())
text_output.delete(0.0,'end') # 先清空
text_output.insert(0.0,get_text())
var.set('')
root = tk.Tk()
root.title("Alan's diary")
var = tk.StringVar(value="What do you want to write today?")
text_input = tk.Entry(root, textvariable=var, width=36, bd=5)
text_input.pack()
root.bind('<Return>', enter_and_print)
text_output = tk.Text(root,width=40)
text_output.pack()
root.mainloop()
添加滚动条
可以通过Tkinter
中的Scrollbar
类来添加滚动条
s = tk.Scrollbar(root)
s.pack(side='right',fill='y')
之后要把滚动条的行为跟Text的显示联系到一起
s.config(command=text_output.yview)
text_output.config(yscrollcommand=s.set)
这样就添加好了滚动条,之后再调整一下布局就行
代码
# coding=utf-8
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
import Tkinter as tk
from alan_diary import *
def enter_and_print(event):
append_text(var.get())
text_output.delete(0.0,'end')
text_output.insert(0.0,get_text())
var.set('')
root = tk.Tk()
root.title("Alan's diary")
var = tk.StringVar(value="What do you want to write today?")
text_input = tk.Entry(root, textvariable=var, width=36, bd=5)
text_input.pack()
root.bind('<Return>', enter_and_print)
text_output = tk.Text(root,width=46)
text_output.pack(side='left',fill='y')
s = tk.Scrollbar(root)
s.pack(side='right',fill='y')
s.config(command=text_output.yview)
text_output.config(yscrollcommand=s.set)
root.mainloop()
显示效果
使用ScrolledText
其实更方便的方法是用Tkinter
的扩展包ScrolledText
,import之后像使用Text组件一样使用ScrolledText就行
代码改为
# coding=utf-8
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
import Tkinter as tk
from alan_diary import *
from ScrolledText import ScrolledText
def enter_and_print(event):
append_text(var.get())
text_output.delete(0.0,'end')
text_output.insert(0.0,get_text())
var.set('')
root = tk.Tk()
root.title("Alan's diary")
var = tk.StringVar(value="What do you want to write today?")
text_input = tk.Entry(root, textvariable=var, width=36, bd=5)
text_input.pack()
root.bind('<Return>', enter_and_print)
text_output = ScrolledText(root,width=46)
text_output.pack()
root.mainloop()