Content Table

QSS QCalendarWidget

QCalendarWidget 是一个比较复杂的 widget,由几个 QToolButton, QSpinBox, QMenu, QTableView 等组成,Qt 的帮助文档里没有其 QSS 的相关文档,当要修改其样式的时候应该怎么办呢?

我们这里采用的方法是分析组成 QCalendarWidget 的 widget 的 className 和 objectName,然后 QSS 每个 widget,最终达到修改 QCalendarWidget 样式的目的。

QSS Subcontrol

普通的 QSS 和 CSS 没什么区别,难度不大,但除此之外,想要使用好 QSS,还必须得掌握好 subcontrol,这个在 CSS 里没有,是 Qt 独有的。

什么是 subcontrol?一个复杂的 widget 由多个部分组成,它们可以是一个 widget,也可以是逻辑上的部件,例如 QCheckBox 由 icon 和 text 两个部分组成,不仅可以定义 text 的样式,还可以定义 icon 相关的样式,icon 部分就是 QCheckBox 的 subcontrol ::indicator

在 Qt 的帮助文档里有所有 subcontrol 的说明,但是相信很多人看了还是不明白每个 subcontrol 具体是什么,这一节将使用可视化的方式标记出 subcontrol,介绍使用 QSS 自定义有 subcontrol 的常用 widget,这里的重心是怎么去 QSS subcontrol 而不是样式效果,复杂漂亮的界面需要大量的图片和更多的 QSS,这里不作介绍,以免陷入细节,掩盖本节主题。只要知道了原理,结合已经掌握的 QSS,找美工提供一套界面切图,就能很容易实现出来很专业效果了。

Subcontrol 的 QSS 和大多数 widget 的差不多,也支持盒子模型,可以自定义 color, background, background-color, background-image, border, padding, margin, width, height 等,也支持 Pseudo-States。

Subcontrol 的绘制位置由 subcontrol-origin、subcontrol-position, top, left 来指定,就先从这几个属性开始入手。

QSS 选择器

选择器决定了 style sheet 作用于哪些 widget,QSS 支持 CSS2 定义的所有选择器

QSS 的选择器有

  • 通用选择器 *
  • 类型选择器
  • 类选择器
  • ID 选择器
  • 属性选择器
  • 包含选择器
  • 子元素选择器
  • 伪类选择器
  • Subcontrol 选择器

很多时候,可以使用不同的选择器实现相同效果的样式,使用非常灵活。

盒子模型

每个 Widget 所在的范围都是一个矩形区域(无规则窗口也是一个矩形,只是有的地方是透明的,看上去不是一个矩形),像是一个盒子一样。QSS 支持盒子模型(Box Model),和 CSS 的盒子模型是一样的,由 4 个部分组成:content, padding, border, margin,也就是说,Widget 的矩形区域,用这 4 个矩形表示

  • content: 绘制内容的矩形区域(如绘制文本、图片),Qt 自带的 widget 都是在 content 区里绘制内容,这只是一个约定,只要你愿意,也可以在绘制到 padding, border, margin 区
  • padding: 内容区和边框之间的间隔
  • border: 边框,可视化的显示一个 widget 的逻辑范围,而不一定是 widget 所占矩形区域的实际大小
  • margin: 想像 widget 的矩形区域有一个隐形的边框,margin 就是 border 和这个隐形边框之间的间隔

QWidget 的 content, padding, border, margin 的矩形区域都是一样大的,也就是说,margin, border, padding 的值为 0,content 的矩形和 QWidget 的矩形一样大,但是 QPushButton 默认的 margin, border, padding 的值不为 0(可以试试 setFlat(true) 后再看看这几个值是什么)。

Margin,Border,Padding 都分为 4 个部分:上、右、下、左,它们的值可以不同:

加载 QSS

前一章节我们已经知道怎么写简单的 QSS 了,但是,应该把它们放在什么地方才能生效呢?

加载 QSS 有三种方式:

  1. Widget 的对象调用 setStyleSheet(qss) 函数加载 QSS,QSS 的作用域是 widget 自己和它的所有子 widget
  2. QApplication 的对象 setStyleSheet(qss) 函数加载 QSS,QSS 的作用域是整个程序里的所有 widget
  3. 在 Qt Designer 的 Change styleSheet... 打开的 QSS 编辑器中添加 QSS,在哪个 widget 上添加的,QSS 的作用域是那个 widget 自己和它的所有子 widget,其实和 1 是一样的,只不这里过是在 Qt Designer 里添加,不是我们自己手动写 C++ 代码添加而已。打开 ui 文件生成的代码(ui_xxxx.h),可以看到里面也是自动生成代码调用 setStyleSheet(qss) 添加 QSS 的,和我们写代码添加没有区别,只是在 Qt Designer 里添加的话,有时候方便一些,也可以实时看到 QSS 的效果

QSS 基础

如果你会 CSS,那么 QSS 对你来说将会非常简单,QSS 的语法和 CSS 的愈发非常相似,但也有些不同,有些 CSS 的东西在 QSS 里被去掉了,QSS 也加了些自己特有的东西,不过大多数还是差不多的,下面以修改 QLabel 的样式为例,学习 QSS 的基础语法。

QSS

Qt 提供的 widget 的默认外观很多时候都不符合项目的界面需求,必须要改,修改一个 widget 的外观(Look and Feel)有以下的方法:

  • 继承 Widget,然后在 paintEvent() 里绘制
  • 继承 QStyle
  • 使用 QSS(Qt Style Sheet)
  • 对于 item view 来说还有一种方式,还可以使用 Delegate

这几种方式里最简单灵活的是使用 QSS,虽然有人说 QSS 的效率低,具体有多低没测试过,但是在普通 PC 上从来没感觉出来,再说现在的硬件也不差这么点性能消耗,随便一个写的差点的函数的消耗就比这多的多,作为一个实用主义者,不追求理论上的效率完美,能满足需求的前提下什么好用用什么,QSS 就是修改 widget 外观的首选,什么效果不满意,修改一下 QSS 的文件就可以看到效果,甚至不需要重新编译、打包发布程序(如果把 QSS 放在文件中,并且实现动态加载 QSS)。

我们按下面的章节来介绍 QSS:

Perlin Noise

普通随机数算法生成的随机数真的很随意,不能做到在某个序列内是升序或者降序的,如果要生成某个范围内一序列的升序或者降序的随机数,例如用于游戏中生成随机地图,渲染海洋等,可以使用 Noise 随机数生成算法,Flow Noise 是 Noise 算法的 Java 实现,其 github 地址为 https://github.com/flow/noise#documentation

Noise generation library for Java, based on the libnoise C++ library. It is used to generate coherent noise, a type of smoothly-changing noise. It can also generate Perlin noise, ridged multifractal noise, and other types of coherent noise. https://flowpowered.com/noise

Https PKIX

使用 Gradle 编译项目,在下载 jar 包时如果遇到 PKIX path building failed: sun.security.provider… 错误,则说明是 ssl 证书的问题,把证书加入到 JVM 的 $JAVA_HOME/jre/lib/security/cacerts 文件即可。

确定证书有问题的域名

1
2
3
4
5
6
7
8
9
> Could not resolve net.ltgt.gradle:gradle-errorprone-plugin:0.0.14.
> Could not get resource 'https://maven.eveoh.nl/content/repositories/releases/net/ltgt/gradle/gradle-errorprone-plugin/0.0.14/gradle-errorprone-plugin-0.0.14.pom'.
> Could not GET 'https://maven.eveoh.nl/content/repositories/releases/net/ltgt/gradle/gradle-errorprone-plugin/0.0.14/gradle-errorprone-plugin-0.0.14.pom'.
> sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

> Could not resolve net.ltgt.gradle:gradle-errorprone-plugin:0.0.14.
> Could not get resource 'https://plugins.gradle.org/m2/net/ltgt/gradle/gradle-errorprone-plugin/0.0.14/gradle-errorprone-plugin-0.0.14.pom'.
> Could not GET 'https://plugins.gradle.org/m2/net/ltgt/gradle/gradle-errorprone-plugin/0.0.14/gradle-errorprone-plugin-0.0.14.pom'.
> plugins.gradle.org

上面的异常,显示在获取 maven.eveoh.nl 下的资源时 ssl 证书有问题,可以使用 SSLPoke.class 来确定是否这个域名的 ssl 是否有问题: 执行 java SSLPoke maven.eveoh.nl 443,输出 Successfully connected 说明 ssl 证书没问题,如抛出下面的异常则 ssl 证书有问题:

1
2
3
4
5
6
sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:387)
at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:292)
at sun.security.validator.Validator.validate(Validator.java:260)
at sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:324)
...

确认证书有问题的域名后,下载这个域名的证书,导入到 cacerts 文件就可以了,可以使用下面的 2 种方式下载证书:

  • 使用 openssl 下载证书
  • 使用 Firefox 下载证书

使用 openssl 下载证书

  1. 执行 openssl s_client -showcerts -connect maven.eveoh.nl:443 (域名和端口根据实际情况进行修改)

  2. 输出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    CONNECTED(00000005)
    depth=2 O = Digital Signature Trust Co., CN = DST Root CA X3
    verify return:1
    depth=1 C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
    verify return:1
    depth=0 CN = eveoh.nl
    verify return:1
    ---
    Certificate chain
    0 s:/CN=eveoh.nl
    i:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
    -----BEGIN CERTIFICATE-----
    MIIGkzCCBXugAwIBAgISA//ESDMD0/IsDv3NVGVcCnMKMA0GCSqGSIb3DQEBCwUA
    ...
    jKjosk5w1g==
    -----END CERTIFICATE-----
    1 s:/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
    i:/O=Digital Signature Trust Co./CN=DST Root CA X3
    -----BEGIN CERTIFICATE-----
    MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
    ...
    KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
    -----END CERTIFICATE-----

    subject (s:) 为申请证书的网站,issuer(i:) 为证书颁发者

  3. 保存证书:

    1
    2
    3
    4
    5
    -----BEGIN CERTIFICATE-----
    MIIGkzCCBXugAwIBAgISA//ESDMD0/IsDv3NVGVcCnMKMA0GCSqGSIb3DQEBCwUA
    ...
    jKjosk5w1g==
    -----END CERTIFICATE-----

    可能会有像上面这样格式的多个证书,我们只需要 eveoh.nl 的证书,因为 0 s:/CN=eveoh.nl,所以保存第一个为文本文件 need.crt

  4. 导入证书:

    1
    sudo keytool -importcert -keystore $JAVA_HOME/jre/lib/security/cacerts  -file need.crt -alias eveoh
  5. 输入 cert 文件的密码,默认的都是 changeit

使用 Firefox 下载证书

  1. 使用 Firefox 打开 https 的链接: https://repo1.maven.org

  2. 点击地址栏中的小锁图标

  3. 点击 Security Connection 右边的向右箭头

  4. More Information > Security > View Certificate > Details > Export

  5. 例如上面 cert 文件保存为 repo1.maven.org.crt

  6. 打开终端,导入 cert 文件

    1
    sudo keytool -importcert -keystore $JAVA_HOME/jre/lib/security/cacerts -file repo1.maven.org.crt -alias maven

    cacerts 文件的路径和 JRE/JDK 安装的路径有关

  7. 输入 cert 文件的密码,默认的都是 changeit

  8. 重启系统

删除证书

1
sudo keytool -delete -keystore $JAVA_HOME/jre/lib/security/cacerts -alias eveoh

显示证书

1
2
keytool -list -v -keystore $JAVA_HOME/jre/lib/security/cacerts
输入密码 changeit

通过查找 Alias name: yourAlias 查看证书是否导入成功。