GUI编写技巧
技巧:
- 在“混合类”中提供常见的GUI操作
- 从数据结构模板中创建菜单和工具栏
- 为命令行工具添加GUI接口
- 将输入和输出流重定向到GUI组件
- 重新加载运行中的GUI回调处理程序
- 封装和自动化顶层窗口的接口
- 使用线程和队列避免GUI中的阻塞
- 根据需要从非GUI程序中弹出GUI窗口
- 用套接字和管道将GUI作为单独的程序实现添加
组件生成器函数:可以尽可能地将繁琐的组件设置放在函数中封装,使用时只需要调用函数即可完成设置。实际上这种方法可以用OOP来代替,那样可复用性更高!下面的代码演示了组件生成器函数的使用方法,完整的代码参见这里:
def frame(master, side=TOP, **kw):
"""
Generate a Frame widget with custom configurations.
"""
w = Frame(master)
w.pack(side=side, expand=YES, fill=BOTH)
if kw:
w.config(**kw)
return w
def main():
app = Tk()
f = frame(app)
app.mainloop()
- 混合类:在一个类中使用常用的方法,在任何需要的地方继承。混合类通常可以用来封装有用的工具,在需要的时候使用。下面给出一个简单的示例用法,完整示例参见这里:
class MixinDemo:
def info_box(self, title, text, **options):
return showinfo(title, text, **options)
def question(self, title, text, **options):
return askyesno(title, text, **options)
def quit(self):
ans = self.question('Quit?', 'Are you sure you want to quit?')
if ans:
exit()
class Test(Frame, MixinDemo):
def __init__(self, master=None, cnf={}, **kw):
super(Test, self).__init__(master, cnf, **kw)
self.pack()
Button(self, text='Info Box', command=lambda: self.info_box('info', 'test info')).grid(row=0, column=0)
Button(self, text='Question', command=lambda: self.question('question', 'test question')).grid(row=1, column=0)
Button(self, text='Quit', command=self.quit).grid(row=2, column=0)
- 菜单栏和状态栏实际上是比较常用的,但是在
tkinter
中创建它们却比较繁琐,重复性工作很多,所以可以在类中封装生成的方法,通过读取并解析固定格式的配置来自动生成。 - 重定向流到组件:这里需要使用的技巧就是实现两个类似文件的类(即,分别实现
write
,read
等接口),然后将命令行中的sys.stdin
,sys.stdout
,sys.stderr
重定向到要显示的组件(如Text
组件)即可。
class GuiConsole(Frame):
def __init__(self, master=None, cnf={}, **kw):
super(GuiConsole, self).__init__(master, cnf, **kw)
self.pack(fill=BOTH, expand=YES)
self.console = ScrolledText(self, font=('Source Code Pro', 12, 'normal'))
self.console.pack(side=TOP, fill=BOTH, expand=YES, padx=5, pady=5)
self.console.focus()
self.console.mark_set(INSERT, '1.0')
def clear(self):
self.console.delete('1.0', END)
def write(self, text):
text = '{}'.format(text)
self.console.insert(INSERT, text)
self.console.mark_set(INSERT, INSERT+'+{}c'.format(len(text)))
def writeline(self, text):
self.write('{}\n'.format(text))
def writelines(self, lines):
for line in lines:
self.writeline(line)
def read(self):
pass
def main():
import sys
app = Tk()
c = GuiConsole(app)
save_streams = sys.stdin, sys.stdout
sys.stdout = c
print('hello, world')
sys.stdin, sys.stdout = save_streams
app.mainloop()
动态重载回调处理器(仅作了解,需要时候深入):
- 即在GUI运行时修改GUI,这块很好理解,自己领悟!
importlib.reload
函数允许动态修改并且重载程序的模块,无须停止程序。这个特性特别适合开发需要很长事件才能重新启动的程序。对于连接到数据库或网络服务器的程序、初始化大型对象的程序、执行长期运行服务的程序,或者需要经过较多步骤才能重新触发回调的程序,都可以作为reload
的主要对象。它可以在开发周期中节省大量时间,并且使系统更加灵活- 相关的测试代码参见ch10/reload.py, ch10/actions.py,可以在测试中看到修改
actions.py
中的打印消息后,也会在按下按钮时打印最新修改后的结果。
GUI应用中,长期运行的操作必须在并行的线程中运行,这样才可以避免GUI因自我更新或响应新的用户请求而遭遇阻塞。长期运行的操作:时间密集型函数调用、服务器下载、阻塞输入/输出调用等。
- 多线程中,只使用主线程来更行显示,其他并行线程负责产生数据供主线程消费,这种是典型的生产者/消费者模型。
- 来看一个简单的示例,学习如何在GUI程序中使用多线程的生产者消费者模型。完整的代码参见ch10/queue-thread-gui.py,以及封装在类中的示例ch10/queue-thread-gui-class.py,以下是关键代码:
def producer(id_):
for i in range(4):
time.sleep(0.1)
data.put('[producer: id is {}, count is {}]\n'.format(id_, i))
def consumer(out):
try:
d = data.get(block=False)
except queue.Empty:
# out.writeline('No data in queue now.')
pass
else:
out.write(d)
out.after(100, lambda: consumer(out))
在队列中放置回调:通过将函数或其他任何可调用对象放置在队列中,生产者线程可以非常直接地告知GUI如何处理消息。所以,我们可以构建调用队列,等待主线程处理。相关的实例参见ch10/threadtools
可以通过进程间通信机制为非GUi脚本添加图形界面,比如利用管道、套接字、文件或者其他方式。