PyQt5 使用 QSettings
虽然写的是 PyQt5 中遇到的问题,PyQt4 中也可能会遇到类似问题。
中秋假期在家继续改进之前写的小工具,第一件事把设置数据的存取从直接读写 yaml 文件改成了使用 QSettings
。什么是 QSettings?摘一段 Qt 文档的翻译。
QSettings 类提供平台无关的持久化应用程序设置。
用户通常期待应用程序在不同会话中保留其设置(窗口大小和位置、设置项等等)。这些信息在 Windows 上经常存储在注册表中,在 Mac OS X 和 iOS 上则保存在 plist 文件中。在 Unix 系统上,由于缺少标准,许多应用程序(包括 KDE 程序)使用 INI 文本文件(来存储设置)。
QSettings 是围绕这些技术提供的一个抽象层,让你使用一种可移植的方式来存储和恢复应用程序设置。它同时支持自定义存储格式。
也就是说 QSettings 是用于保存应用程序设置项(任何你希望保存的、除大文件之外的其他数据)的一个类,在不同平台上可以使用不同的存储(也可以使用统一的存储,如果可用的话),并且存储格式是可扩展的。
QSettings 的特点大概主要是这么几点:
- 支持的数据类型很多。只要能用
QVariant
包起来的,都可以存储,例如QImage
和 Python 的datetime
对象。 - 存储隔离。你可以指定使用哪种存储,但一般在使用的时候无需关心存储细节,只需要调用
QSettings.sync()
,你的设置就保存好了。 - 可以多个线程、多个进程同时使用同一个存储位置。在多线程时,每个 QSettings 对象会立即获得来自其他使用同一个存储位置的 QSettings 对象的更新;在多进程时,Qt 使用文件锁和冲突合并算法来保证存储数据的完整性,并且每次 sync 之后可以获得来自其他进程的修改。
在 Mac OS X 上,plist 文件存储在 ~/Library/Preferences/{BUNDLE_IDENTIFIER}.{APPLICATION_NAME}.plist
。其中 BUNDLE_IDENTIFIER
是软件的 ID,在最终分发的 .app
目录中 Contents/Info.plist
有这个值,一般设置为开发团队域名的反写;APPLICATION_NAME
是 QApplication.applicationName()
的值,可以通过 QApplication.setApplicationName
来设置。对于 plist 不支持的一些 QVariant 类型,会直接存储二进制数据。注意:如果以源代码模式运行,BUNDLE_IDENTIFIER
将是 org.python.python
。注意:本段所写内容只是其中常见的一种情况,实际上并不完整;关于设置数据具体存储的位置,请参考 Qt 文档的这里。
在 Windows 上,默认 QSettings 使用注册表来存储数据。所有数据存储在 HKEY_CURRENT_USER\SOFTWARE\{ORGANIZATION_NAME}\{APPLICATION_NAME}
下,其中 ORGANIZATION_NAME
是 QApplication.organizationName()
的值,可以通过 QApplication.setOrganizationName
来设置。每个 group 是一个文件夹,以每个末端 key 为键,值有两种类型:REG_SZ
和 REG_BINARY
,前者是字符串类型,后者是二进制类型。不支持直接存储的一些 QVariant 类型值会存储为二进制类型。注意:如果组织名和软件名缺少任意一个,设置将不能保存。
我已经不再继续使用桌面 Linux,因此没有 Linux 平台相关的知识可以分享。
要使用 QSettings,最简单的例子是构造一个 QSettings 对象,直接向其中写入一对键值。
|
|
要取出值,使用 .value()
方法,同时可以提供一个可选的默认值。
|
|
前面说存储的时候提到了 group 的概念,什么叫做 group 呢?一个应用程序的设置通常有很多个部分,例如界面相关的设置、软件更新相关的设置、网络相关的设置等等。其实这些就是 group。大家想必都见过 INI 文件,在 INI 文件中我们可以这么写:
|
|
其中每个 []
就是开始了一个新的组。在 QSettings 中我们同样可以使用 group,并且可以嵌套。
|
|
上面这段代码会插入两个 key,完整的 key 是 UserInterface/window-width
和 UserInterface/window-height
。当我们调用 .beginGroup('UserInterface')
,我们实际上进入了 UserInterface
这个组中,此时设置或获取键值,拿到的是这个组中的数据。当我们调用 .endGroup()
,我们就从当前组(就是在此之前离 .endGroup()
最近的一个 .beginGroup()
调用)中回退到上一层。如果后续需要进行其他读写,在进入一个组之后,一定记得调用 .endGroup()
退出当前组。
在写入数据之后,所有的修改都处在内存中。要将修改保存到实际的存储端,调用 .sync()
方法。要删除一个键,使用 .remove()
。
以上就是 QSettings 的基本使用介绍。
除此之外,使用 PyQt5 时,可能会遇到与 QSettings 存储相关的一个问题。例如,延续上文中的代码示例,我们向设置中写入一个布尔值。
|
|
然后我们测试一下使用这个值的代码块是否可以正常工作。
|
|
看起来是可以的。那么我们修改一下设置,把调试模式关掉看看。
|
|
如果你是在 Mac 上测试这段代码,那么肯定没有问题。如果你是在 Windows 上测试这段代码(注意:由于大部分 Qt 功能需要预先初始化一个 QApplication 对象并进入事件循环,所有的代码基本无法在交互模式下运行),那么就会发现调试模式关不掉了!这是怎么回事?我们看看取回来的值的类型和实际的值。
|
|
如果是在 Mac 上,这里 debug 是 bool 类型;如果在 Windows 上使用默认的注册表存储,这里返回的类型是字符串,我们设置的 False
在存储的时候被写入为了 false
。为什么会这样?
前面说过,在 Mac 上 QSettings 默认存储为 plist 文件。plist 格式中支持直接存储布尔类型,因此在读写时可以保持类型不变。而注册表(以及其他可能的存储)不支持直接存储布尔类型,布尔值在存储时转换为了字符串,在读取时,由于数据本身没有类型信息,Qt 也只能以字符串类型返回,因此造成这种看似不科学的现象。更详细的讨论在这里。
对于 QVariant 包裹的值,这个问题不存在。因为 QVariant 不仅包含值,也包含了值的类型信息;它在存储时,是直接以二进制方式存储到文件(或注册表)的,在读取时可以准确还原为 QVariant 类型。
那么我们如何解决这个问题呢?Qt 的文档中,QSettings.value()
方法只有两个参数,但在 PyQt5 中这个方法有额外的 type
参数,可以指定以某种类型来读取数据。参考 PyQt 的发布通知。