用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里显示信息

TkinterMessage组件可以用来展示信息。

我们先暂时只在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赋到messagetextvariable属性上,这样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()

然后看看显示效果

screenshot

感觉按钮有点多余,可以删掉,标签也可以删掉

改代码为

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

显示效果

scrollbar

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

results matching ""

    No results matching ""