First GUI

完成一个 GUI 程序,运行在图形界面,实现以下功能:

  • 输入城市名,获取对应的天气情况;
  • 点击 Help,获取帮助信息;
  • 点击 History,获取历史查询信息;
  • 点击 Quit,退出程序。

Tk 环境爬坑

Mac 和 Python 联合挖了个坑,想也没想就跳进去了。

首先这是 Mac OSX 内置 Tk Toolkit 版本自己的 bug,不能用输入法写入中文字符了,也有记载此 bug 的文档。那按理说更新了就好了。

如果你和我一样只是下载好 8.5.18 版本安装完,就会发现完全不起作用。

排查原因吧!首先要看看新版本是不是真的安装成功了,在 tkinter library 里发现有 TkVersion 这个值,可惜只是 8.5 没有小版本号。这时了解到原来 python 自带的 IDLE 就是 Tk 写的,打开就有 warning。

>>> WARNING: The version of Tcl/Tk (8.5.9) in use may be unstable.
Visit http://www.python.org/download/mac/tcltk/ for current information.

可以确定至少 python 目前引用到的依然是老版本的 Tk。那么了解下 Tk 安装的目标路径吧。

搜索的结果是 Tk 会装在两个路径下

  1. /Library/Frameworks
  2. /System/Library/Frameworks

而 Python 的引用行为描述是这样的

In either case, the dynamically linking occurs when tkinter (Python 3) or Tkinter (Python 2) is first imported (specifically, the internal _tkinter C extension module). By default, the macOS dynamic linker looks first in /Library/Frameworks for Tcl and Tk frameworks with the proper major version. This is the standard location for third-party or built from source frameworks, including the ActiveTcl releases. If frameworks of the proper major version are not found there, the dynamic linker looks for the same version in /System/Library/Frameworks, the location for Apple-supplied frameworks shipped with macOS. (Note, you should normally not modify or delete files in /System/Library.)

我也搞不清楚这个 first imported 是什么时候 ... 总之暴力一点的解决方案就是替换 System/Library/Frameworks 路径下的相应文件,上文表示不推荐,我也不愿意,这有点让人想起曾经在 Windows 下试图补充单个 DLL 文件来满足驱动环境的挫折,而且现在版本的 OSX 的权限问题也很麻烦。

最终通过重新安装 Python 的方式来绕过这个问题。由于之前不希望考虑太多环境问题,Python3 也只是简单用 brew 来安装的,这次直接上 pyenv 吧,可以指定路径使用特定 Python 版本确实很方便。

Tk Widget 和 OOP

Tkinter Library 使用了 OOP 风格组织代码和暴露接口,这貌似是 Python 编程范式的主流。

比如 Tk 是一个 Class

>>> import tkinter
>>> tkinter.Tk
<class 'tkinter.Tk'>

使用 REPL 中使用 help 函数可以传入一个 Class 来查看其规格

>>> help(tkinter.Tk)
Help on class Tk in module tkinter:

class Tk(Misc, Wm)
 |  Toplevel widget of Tk which represents mostly the main window
 |  of an application. It has an associated Tcl interpreter.
 |
 |  Method resolution order:
 |      Tk
 |      Misc
 |      Wm
 |      builtins.object
 |
 |  Methods defined here:
  ...

如上信息,可以了解到 Tk 继承了 Misc 和 Wm,即除了自己的方法和属性外,也可让 Tk 生成的对象拥有 Misc 和 Wm 对象的方法和属性。

我们选择 Text 部件(Widget)来显示命令输出的日志,Text 的继承信息是这样的:

class Text(Widget, XView, YView)
 |  Text widget which can display text in various forms.
 |
 |  Method resolution order:
 |      Text
 |      Widget
 |      BaseWidget
 |      Misc
 |      Pack
 |      Place
 |      Grid
 |      XView
 |      YView
 |      builtins.object

我们希望 Text 对用户来说是只读的,不允许显示光标并随意书写内容,可以通过切换 text.option(state=DISABLED) 来实现。输出日志函数的代码如下:

def print_log(textarea, text):
    textarea.config(state=NORMAL)
    textarea.insert(END, '%s\n' % text)
    textarea.config(state=DISABLED)

传入参数 textarea 要求是一个 Text 实例,是这样生成的

t = Text()

在生成的实例 t 上我们可以调用 config 函数来改变 t 的状态,config 函数是从哪来的呢?

>>> t = Text()
>>> t.config
<bound method Misc.configure of <tkinter.Text object .!text>>

config 函数其实是在 Text 父类 Misc 上实现的,继承关系是 Misc -> BaseWidget -> Widget -> Text。这样做的好处是 Misc 的子类都会获得 config 来改变状态的能力(甚至连状态本身可能都是 Misc 所给予的),这就是类继承的能力。

为了验证,特意去看了 tkinter 的源码(这种源码都好长 ...)

https://github.com/python-git/python/blob/master/Lib/lib-tk/Tkinter.py#L1177-L1203

其实没有指望看懂,不过有几个奇怪的问题,比如 _configure 函数备注指明是 internal function,却没有用 __ 来把它写成一个真正的私有方法。实际上,继承 Misc 的类也会使用这个函数,大概是希望设计成只对调用类库的外部使用者屏蔽吧。

另外 Python 的私有方法也不是真私有啊,对外暴露的私有方法加了 _A 前缀,但并不完全阻止用户访问。这也是 Python 语言设计中为了其使用灵活性所设计的吧。

>>> class A:
...     def __haha():
...         print('a')
...
>>> a = A()
>>> a.__haha
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'A' object has no attribute '__haha'
>>> dir(a)
['_A__haha', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> a._A__haha
<bound method A.__haha of <__main__.A object at 0x10f32ab00>>

想这些早了点,还是完成功能为主。

「最小步骤调试」 for GUI

另外和 w1 时一样我们还是可以用「最小步骤调试」来快速验证函数是不是满足要求

>>> from tkinter import *
>>> def print_log(textarea, text):
...     textarea.config(state=NORMAL)
...     textarea.insert(END, '%s\n' % text)
...     textarea.config(state=DISABLED)
...

我们需要先准备好参数 textarea,Disabled 状态的 Text 部件

>>> t = Text()
>>> t.config(state=DISABLED)
>>> t.pack()

此时显示一个空白界面,鼠标在上面显示成编辑文本的图标,但是点击没反应,也不能编辑

>>> print_log(t, 'Don\'t Panic.')

就会看到界面上出现了这些字。

module 逻辑复用

鉴于 w1 的程序维持了最小步骤拆分,逻辑很容易复用,简单修改一下就可以拿来用了。

为了真正的不用复制粘贴使用 w1 的源码,这里(为了偷懒)暂时选择了 linux 软链接方式。

$ ln -s ../../Chap1/project/main.py weather.py

这样我在修改 w1 的 main.py 文件时,w2 引用到的就总是最新了。

不用硬链接是因为 git 不能保存硬链接的状态,别人 clone 仓库时会变成两个不相干的文件。

Python 的模块似乎并不建议从相对路径引入的方式,虽然我觉得那样很灵活。

为什么这个程序没有写成 OOP 风格

OOP 是如今大多数设计模式和软件架构的基础范式,无论如何也是尽快需要掌握的(不然大多源码都看不懂),不过反向的声音其实也不少。

OOP 对实现程序自身作用是集中管理复杂的变量,使之变成一个个具体可扩展的实例的属性,而这些属性和方法互相牵连,和现实世界的关系一样变得不够简单透明了。这是我认为 OOP 亲直觉,反思维和容易令程序变得复杂难懂的原因。(啊,也或许是现阶段我还没掌握到精髓吧,之后思路说不定会变)

w2 的程序没有写成 OOP 最主要的原因是,OOP 把状态和方法绑在一起,就不容易执行「最小步骤调试」了。以后碰到更需要面向对象的场景再来考虑吧。

不过 Python 的 class 还可以做更多有趣的事情,比如可以重载运算符,下面的例子把减法变成了加法

>>> class Number:
...     def __init__(self, start):
...         self.data = start
...     def __sub__(self, other):
...         return self.data + other
...
>>> n = Number(20)
>>> n - 10
30

感觉很危险,不过用法得当的话会写出风格不错的代码吧(?)。

results matching ""

    No results matching ""