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 实现的,远没有上面的一行简洁。当然,退求其次你可以给下拉菜单另加个确定按钮,函数连接到按钮上,按下按钮后去获取选项再调用函数。

点这里可以看另一个 __getitem__ 的解释,以及另一个例子