- 文章包含个人倾向, 内容仅供参考, 请谨慎阅读
- 本文一共分为上下两篇, 上篇是 Python GUI 框架 (tkinter, PyQt, wxPython, etc.), 下篇是 Python Web 框架 + 前端技术实现 (Flask/Django/etc. + Electron/Remi/sofi/Carlo/etc.)
- 各小节按照 推荐度 降序排列
上篇: Python GUI 框架
PyQt5 / PySide2 (★★★★)
特性
- 使用 QML 可以轻松制作组件动画
QML 问题
- QML 的
implicitWidth/implicitHeight
,anchors & alignment
以及AbstractListModel/AbstractTableModel
是个人认为比较大的难点, 而且出现问题时难以排查定位, 我的大部分时间都用在修复这些错误上- 对于有
background
属性的对象, 比如Button
, 我们会迷惑究竟是在Button
中定义宽高, 还是在background
对象中定义宽高; Qt 助手上说应该在Button
中定义, 但是我在RowLayout
中对Button
对齐时遇到了 “怎么都对不齐” 的诡异现象, 后来在background
中定义隐式宽高解决了 - QML 的表格组件比较难用, 特别通过 Python 创建
AbstractTableModel
再绑到表格组件上 (因为我想把数据处理的逻辑放在 Python 中写), 我花了几天的时间还没有搞定
- 对于有
- 在 QML 中, 假设我们打印
item.chilren.length
显示 3 个子元素, 但同样的方法在 Python 中打印:len(item.children())
, 会发现变成了 4 个! 而且这多出来的一个在位置上是 “随机” 的, 究竟是谁也不得而知 (似乎是一个不可见的对象, 因为打印它的各项属性的值都是 None) - 父组件如果是圆角矩形, 则圆角区域的 clip 属性会失效 (如下图所示)
- 无法单一地对矩形的其中一个或几个角设置弧度
- QML 在导入模块命名空间时, 必须写版本号, 对于初学者来说, 有些库的版本号需要背记下来, 听起来有点荒唐. 我没找到版本号的具体规律是什么, 当然常用的模块的版本号是随着 Qt 发布的次版本号一同增长的, 比如 Qt 5.14 是 QtQuick 的 2.14, Qt 的 5.15 是 QtQuick 的 2.15, 也有某些模块是不遵循的, 只能查 Qt 助手了解 (好消息是听说 Qt 6.0 不用再写版本号了)
- QML 中支持 JavaScript, 但一些新语法尚不支持, 比如
for of
- 我在一次 Popup 窗口弹出动画中, 同时做了位移和尺寸变换动画, 发现居中的文字出现了较明显的 “颤动” 现象, 如下图所示
TODO - 假设 ListView 中子组件是 CheckBox (勾选框), 在视图中滚动 ListView, 会发现滚出视界的 CheckBox 在重新回到视界时, 其勾选状态有时候会丢失
Python 与 QML 组件通讯
- Python 调用 QML 对象的方法不太方便, 特别的, Python 修改
border.width
,border.color
,font.family
等二级属性时,QObject.setProperty()
方法无效, 只能通过QQmlProperty
来实现 (后者写起来比前者繁琐) - 在 Python 中, 无法修改 QML 对象的 anchors 属性
QSS
- QSS 无法定义 transitions 等动画相关的属性, 所以无法在 QSS 中定义组件的动画
PS: PyQt5 和 PySide2 的区别
除了网上常说的协议上的区别外, 我这里补充一些二者在具体用法上的区别 (主要根据我使用 PySide2 的经验):
- PyQt5 的信号与槽导入的名称是
pyqtSignal
,pyqtSlot
; PySide2 导入的名称是Signal
,Slot
- PyQt5 中的
QVariant
在 PySide2 中被移除了, 因此槽装饰器@pyqtSlot(QObject, result=QVariant)
应写为@Slot(QObject, result='QVariant')
(后者有点违和感) - PyQt5 和 PySide2 都可以调用 QML 中定义的函数, 但是调用方法都比较隐晦 (平时也很少用到); 另外, PyQt5 可以调用 QML 的含参函数, 但 PySide2 不能
- PySimpleGUIQt 是基于 PySide2 做了完整性测试, 作者说向 PyQt5 绑定时遇到了很多困难, 所以不能保证已安装 PyQt5 的用户在使用 PySimpleGUIQt 时是否有异常
- enaml 是基于 PyQt5 做了完整性测试的, 作者介绍说 QtPy 模块可以让 PyQt4, PyQt5 和 PySide2 统一调用方法, 写法上统一为 PySide2 那样子 (这是否说明这些作者更偏向于 PySide2?)
PySimpleGUI (★★★★)
简介
如果不考虑复杂的界面动效, PySimpleGUI 绝对是 Python 开发可视化界面的神器, 非常适合 短平快 的客户端编写.
在评测期间, 我对手中的几个项目都做了基于 PySimpleGUI 和 PySide2 的客户端, 个人感受是, PySimpleGUI 写界面的速度非常快, 几小时能写好的界面, 在 PySide2 中可能需要一两天的时间. 而且 PySimpleGUI 的界面外观虽然不够酷炫, 但至少看起来不难看. 这也是我推荐它的一大原因.
缺点
- 编辑框没有 text hint 属性 (妥协: 用
default_text
或tooltip
代替) - 编辑框没有内间距属性 (其
pad
参数相当于 web 的 margin, 真正的 padding 属性不存在) - tooltip 有时会出现提示文字 “抽搐” 的现象
Kivy (★★★☆)
问题1: TextInput 不支持文字居中显示
Stack Overflow 给出的回答解决了 text 的居中问题, 但没有解决 hint_text 无法居中, 我自己花了一些功夫造轮子解决了.
这件事给我带来的忧虑不是说官方为什么没有提供原生的方法, 而是当我寻找答案时, 网上对此的讨论出奇的少 (中文网站完全没有), 而且也没有完美地满足我的需求.
Kivy 的讨论度太低了, 注定后面的大部分的坑需要个人去踩, 这是我真正担忧的地方.
问题2: 自定义组件的引用问题
假设我的项目结构如下:
myproj
|-addressbar.kv
|-addressbar.py
|-home.kv
|-home.py
addressbar.py 中已经定义好了一个 AddressBar. 现在我想要 home.kv 中引用这个自定义组件, 该如何引用?
这个问题我没能解决. 我搜索网上的答案, 以及看了一些官方提供的 demo, 只得到了以下知识:
- 在单 kv 文件中定义所有组件 (包括自定义组件) 是可行的, 但如何导入其他 kv 文件的组件不知道
- 当我有大量组件分布在不同分文件中, 需要连接布局在一起时, 似乎只能在 py 中实现, 这与我理解的 “视图逻辑相分离” 思想有很大的差别. 因为我想单纯依靠 kv 文件完成所有布局活动
问题3: API 文档不好用
我喜欢 Kivy 将 API 独立为文档的做法, 但文档的内容写得无法让人满意.
当我想要知道 TextInput 有哪些属性可使用时, 我根本无法在 API 文档中弄清楚. 例如:
- TextInput 是否有 height 属性?
- TextInput 的排列方式是由 “align” 控制的吗? 以及如果要居中显示, 应该用 “center” 还是 “middle”? (PS: 答案是 “middle”, 但我并非通过 API 找到, 而是通过一些 demo 源码)
关于什么样的 API 才是好的 API 文档, 我建议了解一下 Adobe ExtendScript Toolkit 附带的 Object Module 文档, 虽然看起来很粗糙, 但它支持了我从零到完整开发一个脚本应用的整个过程. 我认为规范的格式和完整的属性列表是构造这类文档的基础与核心.
问题4: 默认不支持中文显示
当创建并显示一个 TextInput 控件时, 往里面输入中文, 会变成方块乱码.
关于解决方法是能够找到的 (比如 这篇文章), 但我不满意的地方是官方没有给出解决方式. 这也意味着官方对中文社区的支持力度还不够.
问题5: 缺少中文教程
我搜集了两个中译版的教程, 这两个教程都是对官方 Getting Started 的译文.
考虑到官方的 API 文档还没有翻译 (加上 API 内容写得也不够好), 虽然我希望有更多的资料支持我的开发, 但目前的结论是长期来看也只能啃生肉了.
enaml (★★★☆)
简介
enaml 是我在寻找 “能够在 QML 中写 Python” 时遇见的, 当然 enaml 自身能力远不止于此…TODO
问题1: 参考资料太少
除了官方文档给出的示例可供参考外, 网上鲜少有关于它的介绍和讨论.
问题2: 在 Style 中设置编辑框的文字对齐方式无效
# === test_view.enaml ===
from enaml.widgets.api import Field
from enaml.styling import StyleSheet, Style, Setter
enamldef MyField1(Field): # 编辑框
text = 'hello world'
StyleSheet:
Style:
Setter: # 我想让编辑框中的文字居中出现
field = 'text-align' # 这是文档中列出的受支持的字段
value = 'center' # 但是向这个字段赋值, 却是无效的
enamldef MyField2(Field): # 编辑框
text = 'hello world'
text_align = 'center' # 在属性中定义, 才有效
问题3: 暂不支持 Python 3.9
原因是 Python 3.9 似乎移除了内置的 bytecode/inst.py
模块的 ‘END_FINALLY’ 标志, 导致 enaml 启动时报错.
具体请参考: https://docs.python.org/3/whatsnew/3.8.html#cpython-bytecode-changes
Tkinter
体验时间: 2019年4月26日
问题1: 没有内置的表格组件
关于这个问题, 我通过 这篇文章 找到了一个对 Tkinter 的封装实现方案: tktable, 不过作者早在两年前就停止更新了. 我把源码下载下来, 运行后直接报错:
报错内容: _tkinter.TclError: invalid command name "table"
看了下报错源码位置, 好像是说 tktable 未安装导致报错, 不过实际上 tktable 不就是它自己吗? 没想明白, 暂时就放弃了.
问题2: 反直觉的列表元素类型
"""
来自: Listbox 列表部件 - 窗口 Tkinter | 莫烦Python - https://morvanzhou.github.io/tutorials/python-basic/tkinter/2-03-listbox/
"""
import tkinter
x = tkinter.StringVar()
x.set((1, 2, 3, 4))
# why set an array of int to a "StringVar"?
问题3: Listbox 不支持支持传入列表 (同样有违直觉)
报错内容: AttributeError: 'list' object has no attribute 'items'
appJar (★)
TODO
Atlas (☆)
问题1: 网络延迟
Atlas 运行后需要保持网络链接, 国内的连接不稳定, 导致任何交互动作的延迟都非常夸张.
例如官方的 hello world 示例, 点击下图的 Submit 或 Clear (清空文本框内容) 按钮时, 需要等待十多秒浏览器才会弹出对话框.
下篇: Python Web 框架与前端技术结合
Flask + Miniblink
特性 (Miniblink)
- 基于 chromium 最新版内核, 去除了所有多余的部件, 只保留最基本的排版引擎
- 内核体积极小 (~20mb), 且拥有完整的网页渲染功能
- 持续活跃的更新 (截止本文发表的最近更新于 2019.10.15)
注意事项: Miniblink 使用 Apache License 2.0 协议, 使用者需在项目发布文件中显式申明使用了 Miniblink.
问题1: 加载 node.dll 失败
# ./test.py
import ctypes
# 事先将 miniblink 的 node.dll 放在和 test.py 同一目录下
ctypes.cdll.LoadLibrary("node.dll")
# -> 报错: "OSError: [WinError 193] %1 不是有效的 Win32 应用程序"
根据 这个回答, 原因似乎是 64bit Python 运行 32bit DLL 的冲突引起. 本人没有继续进行测试.
参考
- https://github.com/ynyyn/Miniblink-Python-SimpleDemo
wuy
TODO
flaskwebgui
问题1: 进程管理问题
flaskwebgui 的进程管理似乎有问题, 会导致启动后 CPU 就被拉满. 并且 kill_servers() 方法也是无效的.
参考
Electron + Flask
体验时间
2019年3月 - 2019年5月.
问题1: Flask 默认实例化行为带来的项目结构管理的困扰
Flask 的相对路径有很多坑.
上图是我摸索得到的经验 (相关文章见 这里), 当我了解了 template_folder
, static_folder
, static_url_path
这三个参数该如何自定义后, 才摆脱了布局和资源路径找不到的困扰.
问题2: 糟糕的跳转体验
当我在输入框输入一个新路径并按下回车键, 如何更新本页面的列表元素的文件列表信息?
我需要在 <script>
中定位到这个输入框, 获取这个输入框的值, 把值利用 ajax 或者 window.location.href
激发一个 url 请求, 并携带该值作为参数, 等 Flask 解析携带的参数后, 再重定向到原页面, 但与此同时, 也要给原页面交递这个参数.
这个过程不但繁琐, 而且混入了 JS, jQuery 中的各种概念, 让一个基于 Python 的后端不再纯粹, 另外我也不知道它该如何实现局部的渲染, 当我尝试使用 Vue (这又是一个问题) 来局部渲染时, 新的路由问题和模块引用问题让整个逻辑变得更加混乱.
问题3: 缺乏资料
这个问题伴随着我尝试 Flask 的整个过程, 实践示例, 教程和书籍匮乏, 以及很多自己想问的问题找不到答案, 很多错误都是自己摸索, 尝试几天后自己总结的. 即便如此, 现在手上还有一些问题亟待解决, 但也仍未找到解决的线索, 让人感到非常挫败.
Flexx
项目地址: https://github.com/flexxui/flexx
体验时间
2019年3月24日.
问题1: IOLoop 错误
当我测试以下示例时, 运行报错:
from flexx import flx
class AAA(flx.Widget):
def __init__(self, *init_args, **kwargs):
super().__init__(*init_args, **kwargs)
flx.launch(AAA)
报错信息为 “type object ‘IOLoop’ has no attribute ‘_current’”.
经查找发现是 flx 使用了 tornado 模块的 IOLoop._current
, 而事实上在最新版的 tornado 模块中已经没有 _current
, 而是用 IOLoop.current()
取代.
也就是说我们在 flx.launch(AAA)
前加一行 IOLoop._current = IOLoop.current()
才能解决这个低级报错:
说实话 Flexx 官方没有解决这个问题, 可能是因为在 tornado 模块更新后没有去适配, 也可能是社区不活跃没有人反应问题, 也可能是已经停止维护更新了.
这是我不推荐使用 Flexx 的原因.
flybywire
简介
项目地址: https://github.com/thomasantony/flybywire
在了解 flybywire 之前, 不妨先了解一下 Sofi.
flybywire 的作者深受 Sofi 的启发, 但认为 Sofi 在 UI 的架构理念上显得有些过时 (或者说不够优雅?).
flybywire 的作者同时也深受 React 语言的影响, 认为响应式设计是一种深刻的变革 (关于响应式的理解可以见 这篇文章).
因此, flybywire 在承袭 Sofi 核心思想的同时, 将响应式设计加入到 UI 的渲染方案当中. 如果有了解过 MVVM (比如接触过 Vue), 相信你也会对 flybywire 的做法感兴趣:
问题1: pip 安装的库是过时的
通过 pip install flybywire
安装的库是过时的, 且无法运行.
请从 git 上克隆他的项目下来, 例如我在我的项目路径下使用 git clone https://github.com/thomasantony/flybywire.git
:
myprj/
|-flybywire/
|-examples/
|-flybywire/ # <- 将这个文件夹移动到 venv/Lib/site-packages/ 中
|-test/
|-.gitignore
|-.travis.yml
|-AUTHORS
|-ChangeLog
|-LICENSE
|-README.md
|-setup.cfg
|-setup.py
|-myapp/
|-venv/
|-Lib/
|-site-packages/
以及还需要安装一个 autobahn (这个可以 pip 安装):
pip install autobahn
问题2: 无法运行官方示例
是的, 当你安装成功后, 会发现启动后前端页面一直显示加载中… 暂不清楚是安装问题还是代码问题.
问题3: 没有教程文档
和 Sofi 一样, flybywire 没有教程文档, 这意味着新手很难上手这个框架.
CEFPython (cefpython3) (★★)
TODO
Carlo (★★)
项目地址: https://github.com/GoogleChromeLabs/carlo
TODO
Sofi
项目地址: https://github.com/tryexceptpass/sofi
介绍:
Sofi 的作者尝试将前端 HTML 和后端 Python 相结合, 利用 Websockets 的魔法让 HTML 元素向 Python 传讯以及 Python 操作 DOM 成为现实.
另外的, Sofi 内置了 Bootstrap 的渲染能力, 使默认绘制的前端控件外观现代和前卫. 从 Demo 中可以看到确实让人感到耳目一新, 有别于以往的 Python GUI 项目演示截图中复古的印象.
体验时间: 2019年5月9日
问题1: 性能表现似乎不太好
如图所示, 我在工作电脑上运行官方示例, 在连接到浏览器后, 会有一个明显的 HTML 加载延迟 (注: 在录制该动图时, 该延迟没有被捕捉到, 但在之前的几次运行中都有出现).
我不知道是这个官方示例写得不够好, 导致表现不佳, 还是电脑的问题 (不过自认为基本办公方面是没问题的). 评估 Sofi 的表现还需要更多的时间了解它.
问题2: 没有教程文档
Sofi 的 tutorials 文件还是空白的, 我估计他还没有写好文档, 但距今已经一年之久了, 不知道什么原因停止更新, 个人感觉有很大的可能是不会有下文了.
意思就是除了两个官方的 demo, 基本找不到可供参考的文档了.
Tornado (利用 Tornado Websocket 实现 Python 与前端通讯)
问题1: 由于继承了 WebSocketHandler 的自定义类无法实例化, 导致自定义类之间通讯受阻
假设我有一个 class AddressBar (地址栏) 和一个 class Filelist (文件列表), 这两个 class 都继承自 tornado.websocket.WebSocketHandler.
现在我想让地址栏在收到前端组件更新时, 随即调用 Filelist 中的 update_filelist() 方法…
TODO
附录
个人建议
根据个人使用的 Python GUI 框架 (很多都是浅尝辄止), 有以下经验可供参考:
- 尽量使用大公司的, 知名度高的. 大厂的 api docs 非常完善, 一般问题也能快速解决. 而个人开发者做的 sideproject, 有很多都停止更新了, 绝大多数缺乏教程文档, 遇到问题没人解答, 如果你的目的不是给一个单纯的小工具加一个 GUI, 不建议长期去依赖后者
了解一下 Python TUI?
TODO
参考
- Electron
- 有开发者抱怨 Electron 占用内存过多等缺点 https://www.reddit.com/r/node/comments/8rhwz7/making_a_very_small_application_is_electronnwjs/
- PyQt, PySide
- 这篇文章介绍了 PyQt 和 Pyside 的区别: https://www.e-education.psu.edu/geog489/node/2225
- Stack Overflow 上关于 PyQt 和 Pyside 差异的回答: https://stackoverflow.com/questions/6888750/pyqt-or-pyside-which-one-to-use
- Qt 官方对其区别的解释: https://wiki.qt.io/Differences_Between_PySide_and_PyQt
- Sofi & flybywire
- Sofi 的作者深刻体会到 Python 开发 GUI 方面的掣肘 (A Python Ate My GUI Series), 这促使他开发了 Sofi https://medium.com/@tryexceptpass/a-python-ate-my-gui-part-3-implementation-39fc105b6d81
- flybywire 的作者表示自己曾阅读过 Sofi 作者的文章, 对 Sofi 的理念表示赞赏, 但是认为 Sofi 的 UI 的架构理念可能有些过时 https://medium.com/@tantony/flybywire-declarative-guis-for-python-inspired-by-react-ad2131d4cbc1
- GUI 概论
- 关于 GUI 的十年架构演化之路 https://zhuanlan.zhihu.com/p/26799645