PyQt5 下拉菜单如何优雅地连接函数
近日接触到 PyQt,做点小工具在工作时方便用。发现用它做一些桌面应用相当给力,结合了 Python 的快速开发、Qt 的高运行效率。对普通程序员来说,如果 Python 还行的话,稍微花点时间读读文档就能立马掌握 PyQt,实在是相当划算的一项技能!平时一些自用的 Python 脚本用 PyQt 一封装,就能做成相当拿的上台面的桌面软件(还通杀 Windows、Linux、macOS 三平台),吹水装逼撩妹岂不信手拈来((雾雾雾
1. 问题
言归正传,关于 Python 的版本,就不多说啥了好吧,我用 Python 3.7 + PyQt5。本人在搜寻 PyQt 打包方案时,找到了挺不错的框架 fbs,这里也在此推荐。在观摩它的示例应用代码时,发现了一处很有悖 Python 常规的地方,如下:
class WidgetGallery(QDialog):
def __init__(self, parent=None):
super(WidgetGallery, self).__init__(parent)
styleComboBox = QComboBox()
styleComboBox.addItems(...)
# 选项改变的信号连接函数 self.changeStyle
styleComboBox.activated[str].connect(self.changeStyle)
...
def changeStyle(self, styleName):
...
这里的功能很简单,在下拉菜单一改变选项时,立即调用 self.changeStyle
函数,同时把新选项作为字符串传给 self.changeStyle
函数。注意非常古怪的这一行:styleComboBox.activated[str].connect(self.changeStyle)
,按理说 QComboBox
里的 activated
并不是什么字典类对象,应该只是一个普通 Qt 信号槽,并且再注意,方括号中 str
是 Python 保留字,是 Python 的 String 字符类,这是什么鬼(⊙_⊙)?没有耐心的话翻至下文的 4. 完整解释
2. PyQt 的黑魔法
苦于 Google 搜索会把特殊符号剔除,对程序员不怎么友好,花了些时间才在 reddit 找到一篇相关的问答,点此查看原文。下面我来概括解释一下,activated[str]
是 PyQt 里的一个黑魔法,原本 C++ 中的 QComboBox::activated
函数有两个重载形式(不信你来看文档):
void QComboBox::activated(int)
void QComboBox::activated(const QString)
作为弱类型的 Python 没有重载,就必须通过其他方法区分开这俩。重写个不同名的函数当然行得通,毕竟 PyQt 只是作为 Qt 这一 C++ 库的绑定层。那还有其他更“原汁原味”的办法嘛?比如说用列表或者字典方括号之类的办法,像这样 activated[0]
用“下标”指定出实际需要的对象?要知道 API/ABI 的改动是一件很痛苦的事情,绑定层同 C++ 库保持一致能减少头疼次数。
这里就轮到 Python 的魔法函数 __getitem__
上场了。通常情况下,比方说一个字典 a["key"]
这样取值的形式,实际上都是通过调用 a.__getitem__("key")
实现的 (或者更确切地讲,是 type(a).__getitem__(a, "key")
)。而 activated
的类中就定义了 __getitem__
方法,所以 activated[...]
其实真的是跟字典一样的形式,用键值对(或者说“下标”)的方法获取对象。
3. Python 对象
至于方括号里的 str
是什么鬼,打开 python 终端一试就知道了:
>>> str
<class 'str'>
>>> str is str
True
>>> isinstance(str, type)
True
str
本身是一个类,调用时实例化了,成为了对象,而且也是一个 type
类的对象。
4. 完整解释
activated
是一个带有 __getitem__
定义的对象,使用 str
对象作为 key:activated[str]
,这样可获取到读取字符串的 activated
(而不是另一个读取整数的 activated
)。当你改变下拉菜单的选项时,它会去调用刚刚连接的函数 self.changeStyle
,同时读取你新选中的选项,将其作为字符串参数传给 self.changeStyle
去使用。
5. 现学现用,举个栗子
如果你还不能理解的话,可以在 python 终端里试试下面这个例子,其中 __setitem__
是设置对象,是和 __setitem__
正好相反的魔法函数:
class School:
def __init__(self):
self.student_names = ["Anna", "Bob", "Caroline"]
self.student_scores = {
"Anna": 100,
"Bob": 90,
"Caroline": 80
}
def __getitem__(self, key):
if key is str:
return self.student_names
if key is dict:
return self.student_scores
if isinstance(key, int):
return self.student_names[key]
if isinstance(key, str):
return self.student_scores[key]
def __setitem__(self, key, data):
if isinstance(key, int):
self.student_names[key] = data
if isinstance(key, str):
self.student_scores[key] = data
接着继续如下执行,可以看到结果:
>>> s = School()
>>> s[str]
['Anna', 'Bob', 'Caroline']
>>> s[dict]
{'Anna': 100, 'Bob': 90, 'Caroline': 80}
>>> s[str][0] = "Amy" (这里实际上调用了 __getitem__ 而不是 __setitem__)
>>> s[str]
['Amy', 'Bob', 'Caroline']
>>> s[0] (这里调用了 __getitem__)
'Amy'
>>> s[0] = "Annie" (这里才调用了 __setitem__)
>>> s[str]
['Annie', 'Bob', 'Caroline']
>>> s["Bob"] (这里调用了 __getitem__)
90
>>> s["Annie"] = 99 (这里调用了 __setitem__)
>>> s[dict]
{'Anna': 100, 'Bob': 90, 'Caroline': 80, 'Annie': 99}
怎么样?是不是感觉这对函数非常黑魔法?竟然能把普通对象同时变成字典和列表(~ ̄▽ ̄)~
6. 回到问题
最后,关于如何优雅地给下拉菜单连接函数,activated[str].connect(function)
学会这一招了嘛?我见过用 lambda 实现的,远没有上面的一行简洁。当然,退求其次你可以给下拉菜单另加个确定按钮,函数连接到按钮上,按下按钮后去获取选项再调用函数。