QSS介绍

  QSS(Qt Style Sheets),Qt的样式表,和CSS非常像的东西。QSS用于为Qt应用程序定义界面元素的视觉样式。通过使用QSS,开发者可以轻松地改变应用程序的外观,而不需要深入了解每个控件的绘制细节。

Box Model(盒模型)

  在了解QSS细节前需要知道QSS的基础模型盒模型,它是从CSS来的一个概念(毕竟QSS参考自CSS)。Qt的所有控件可以认为是处在一个盒子中,这个盒子包含4层,从外到内分别是margin(外边距)border(边框)padding(内边距),content(内容)。具体布局如下图:

  用Qt的QLabel演示一个具体的例子,这里有一个需要特别注意的,在Qt中如果控件是顶级窗口,那么它会忽略margin属性,margin所有被强制值设置为0。所以我们需要把QLabel放到里一个顶级控件里面,比如一个QWidget。

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    QWidget w;
    QGridLayout layout(&w);
    //layout里面4个边距都设置为0,避免干扰分析。
    layout.setContentsMargins(0, 0, 0, 0);

    QLabel label("Hello", &w);
    QString qss = "margin:10px;"
        "border:15px;"
        "border-style: solid;"
        "border-color: blue;"
        "padding: 30px;"
        "background-color: red;"
        "background-clip: content;"
        "color: black;";
    label.setStyleSheet(qss);
    layout.addWidget(&label);
    w.show();

    return QApplication::exec();
}

运行得到的样子:

  其中,白色这个框框就是margin区域,蓝色是border区域,他们都是10px,内层的白色区域就是padding区域,红色框框内就是content区域,这里有几点需要注意:

  1. marginpadding区域都只能指定尺寸,因为它们分别被称为(外边距、内边距,既然是距离那就只能用长度来衡量)。
  2. border区域除了可以指定尺寸外,还可以指定颜色和填充风格等其他属性,具体下面会详细介绍。
  3. background-color或者background-image会填充paddingcontent区域。

盒模型四个区域属性详细介绍

margin区域

  margin区域只能指定尺寸,指定方式有两种风格:

  • 各方向属性分别指定,分别指定4个方向的尺寸,属性分别叫margin-topmargin-bottommargin-leftmargin-right。:
margin-left:10px;
margin-ringht:15px;
margin-top:25px;
margin-bottom:12px;
  • 无方向属性连写格式,只用margin属性:
//一元指定
margin:10px;/*相当于4个方向统一指定为10px*/

//二元指定
margin:10px 20px;/*相当于分别对垂直(top、bottom)、水平(left、right)方向指定*/

//三元指定
margin:10px 20px 30px;/*按照从top开始的顺时钟方向指定,分别是top、right、bottom、left,缺失的取它对面的值,即这里为:10px 20px 30px 20px。其实二元指定也可以这么解释*/

//四元指定
margin:10px 20px 30px 40px;/*按照从top开始的顺时钟方向指定,分别是top、right、bottom、left*/

padding区域

  padding同margin。

border区域

  border区域是最为复杂的区域,它同时具有四方向尺寸属性、填充颜色属性、填充风格属性,还具有4个顶点的圆角效果。和margin属性类似,border属性也可以用具体的属性key指定值,也可以按照默认顺序,连续指定多个属性。详细的属性key包括:

border-{top,right,bottom,left}-{width,style,color}

这样的3段构成的属性,其中第一段border固定不变,后面两个段可以取花括号内的某一个值,任意组合,一共4x3=12个三段属性。

还可以是两段构成的属性:

border-{top,right,bottom,left}
border-{width,style,color}

  如果省略的是方向,那多个值对应到4个方向的规则参考margin属性的规则。例如:

border-width:5px;/*表示4个方向都为5px*/
border-color:red green;/*表示垂直方向颜色为red,水平方向为green*/
border-style:solid dotted solid dashed;/*按照top, right, bottom, left顺时钟顺序设置4中风格*/ 

  如果省略的是第三段,指定多个值的顺序为width,style,color。

border-left:10px solid red;

我实测Qt5.15好像只能省略颜色,其他省略则不起作用。

  如果指保留border一个段,那就是4个方向都相同的值:

border:width style color

例如:

border:10px solid red;

实测,同样发现只能省略颜色,为啥呢?其实是width默认值为0,style默认值为none,color默认值为black。明显使用默认的width和style都会导致你看不到它。

border-style属于Border Style类型,有11种可选值,效果图如下:

  需要注意的是如果border-with为0,那么效果相当于border-stylenone。如果border-style:double;那么border-width >= 3才能看出效果,很好理解,一根线条的最小尺寸为1px,那么2根最少需要2px,而且还要中间至少1px的间隙,才能让人看出来,所以它要大于等于3。

border-color属于Brush类型,而Brush类型又是Color | Gradient | PaletteRole 中三种方式中选一种。

Color也有多种写法:

  • rgb(r, g, b)
  • rgba(r, g, b, a)
  • hsv(h, s, v)
  • hsva(h, s, v, a)
  • hsl(h, s, l)
  • hsla(h, s, l, a)
  • #rrggbb
  • Color Name(eg. red green blue black white)

Gradient渐变效果也有3种类型,三种渐变的具体含义见Qt二维图形绘制这篇文章的相关章节:

  • 线性渐变
    qlineargradient,使用时像调用函数一样,需要指定参数,格式为:

    qlineargradient(
    spread:[pad|repeat|reflect], 
    x1:[0-1], y1:[0-1], // 渐变轴线起始坐标(通过百分比计算)
    x2:[0-1], y2:[0-1], // 渐变轴线结束坐标(通过百分比计算)
    stop:[0-1] [color], // 插色点
    ...);
    

    spread含义是指渐变的扩散方式。默认可以不指定,为pad。下面看看3种效果,需要注意的是,要看出3种扩散方式的差异,需要注意插色点位置不要到尽头,即包含stop:1 color;这种插色点。以线性渐变为例:

    渐变方式 QSS示例 效果图
    pad background-color: qlineargradient(spread:pad,x1:0, y1:0.5, x2:0.5, y2:0.5,stop:0 red,stop:0.5 yellow); img
    repeat 同上,仅修改为spread:repeat img
    reflect 同上,仅修改为spread:reflect img

    接下来的2对坐标表示渐变的轴线,取值范围为0.0 ~ 1.0。表示它作用区域width或者height百分比位置。后面可以跟多个stop点,每个stop点语法为:stop:percent color 其中percent取值范围也是0.0 ~ 1.0 表示渐变轴线的百分比位置,一般stop组按照percent做升序排布,表示沿着渐变轴线的起始位置向结束位置渐变。每个stop点设置的颜色就是严格和stop的color值一样的,然后从stop1的颜色向下一个点stop2颜色渐变过渡,如此循环。下面为一个示例:

    border-width: 10px;
    border-style: solid;
    border-color: qlineargradient(x1:0.0, y1:0.5, x2:1, y2:0.5, stop:0 red, stop: 0.5 green, stop:1 blue);
    padding: 10px;
    

    效果:

    img

  • 径向渐变
    qradialgradient,同理也有如下格式:

    qradialgradient(
    cx:[0-1], cy:[0-1],   // 圆心
    radius:[0-1],         // 半径
    fx:[0-1], fy:[0-1],   // 焦点位置
    stop:[0-1] [color],   // 插色点
    ...)
    
  • 锥形渐变
    qconicalgradient,同理也有如下格式:

    qconicalgradient(
    cx:[0-1], cy:[0-1],   // 中心点
    angle:[0-360],        // 起始角度(0=12点方向)
    stop:[0-1] [color],   // 插色点
    ...);
    

要理解这些参数含义,最好还是看看Qt二维图形绘制

PaletteRole

  然后是4个顶点的圆角属性设置。首先圆角是啥?我的理解就是往这个直角里面内切一个椭圆的1/4。实现顶点圆润的效果,而不是一个尖锐的直角。具体示意图:

写法也有两种,一种是完整、具体的顶点:border-top-left-radiusborder-top-right-radiusborder-bottom-right-radiusborder-bottom-left-radius;

border-{top,bottom}-{left,right}-radius: width height;

with表示这个1/4椭圆的圆弧边的水平尺寸,height表示它的垂直尺寸。示例如下,如果只有一个值,表示width=height,是一个标准的圆的圆弧。

border-top-left-radius: 10px;
border-top-left-radius: 10px 20px;

还一种是连写格式的border-radius

border-radius: width height;

width、height含义同上,这样表示4个顶点的圆弧都一样。这种写法好像不能单独指定某一个顶点的圆弧,只能统一指定。

conntent区域

  内容区域可设置的东西也比较多,主要是3个方面:

文本控制属性

属性 值示例 说明
color #ff0000
rgb(255,0,0)
red
文本颜色
font bold 14px "Arial" 字体系列、大小和样式
font-weight bold
normal
800
字体粗细
font-style normal
italic
oblique
字体样式
font-size 12px
1.2em
字体大小
text-align top
bottom
center
left
right
文本对齐方式
text-decoration underline
overline
line-through
none
文本装饰线

  首先便是文字的颜色color属性,就是一个简单的颜色值,然后是字体,它也有具体属性和连写格式。具体属性包括:

font-style

它用于设置文字风格:

效果
normal 正常
italic 斜体
oblique 斜体

看上去italicoblique都是斜体,那有什么区别呢?这就要看斜体的实现方式,有些字库里面自带了斜体风格的字体,那么用italic,有些字库里面没带,设置为italic会没效果,这时就要就要靠算法模拟出斜体,此时用oblique

font-weight

它用于设置文字线条的粗细。

font-size

字体大小,单位可以是像素px或者pt。

font-family

字体类型,可以通过fc-list查看系统有哪些可用的字体。

连写方式就是按照如下顺序:

font:style weight size family;

除size外,其他都可以省略,使用系统默认中。

然后是一些文字的布局和装饰属性:

text-align

文本对齐属性,支持top,bottomleft,right,center几种值或者合理的组合,比如 top left 表示左上对齐,并且需要注意该属性只对部分支持内部布局的控件有效,并非普适性的。

text-decoration

给文本加装饰线,可以指定一个或者多个值。

效果
underline 下划线
overline 上划线
line-through 中划线
none 无划线效果

背景样式属性

属性 值示例 说明
background-color #3498db
transparent
背景颜色
background-image url(:/images/bg.png) 背景图像
background-repeat repeat-x
no-repeat
repeat-y
repeat-xy
背景图像重复方式
background-position center
top left
50% 30%
背景图像位置
background-clip content
border
padding
背景绘制区域
background-origin content
padding
border
背景起始位置
background-attachment* fixed
scroll
*(Qt 5.12后支持)

下面是几个具体的属性效果图:

background-clip

该属性用于限定background-color的显示区域。

img

值分别是border(默认),padding,content。其中绿色虚线框就是pandding区域和border区域的界线。值为content时很好理解,只填充到content区域,padding区域是不填充的,但是border和padding的效果要非常仔细的观察,前者背景色渗透到了这根绿色界线上了,后者没渗透过去。

background-repeat

该属性仅在指定了正确的background-image时才有效。

img

这几个效果分别是repeat-xy(默认,Qt官方文档说是repeat,但是我实际测试repeat==no-repeat), no-repeat, repeat-x, repeat-y。注意只有图片尺寸小于content区域时才可以重复,否则没效果。

background-position

该属性仅在指定了正确的background-image,并且Background-repeat值不为repeat-xy时才有效。它的取值是Alignment类型,具有{ top | bottom | left | right | center }等值,或者它们的组合,如下图所示。

img

  • background-repeat:no-repeat;时,可以指定这个背景图片在这9个区域中的任意一个。
  • background-repeat:repeat-x;时, background-position的值仅垂直方向的top,center,bottom有作用。
  • background-repeat:repeat-y;时, background-position的值仅水平方向的left,center,right有作用。
  • background-repeat:repeat-xy;时, background-position的值将失去意义。

background-origin

该属性和background-clip类似,不过一个限定的是background-color,一个限定的是background-image

background-attachment

该属性主要针对继承自QAbstractScrollArea的类,表示滚动时,背景图片是固定(fixed)还是跟着滚动(scroll)。

尺寸和布局属性

属性 类型 说明
width Lenth 表示内容区域的宽度,通常会被hintSize覆盖
height Lenth 表示内容区域的高度,通常会被hintSize覆盖
{min,max}-width Lenth 表示内容区域最大或最小宽度,会限定值只能在这个区间内
{min,max}-height Lenth 表示内容区域最大或最小高,会限定值只能在这个区间内
spacing Lenth 表示控件内部子元素之间的间隙距离,通常用于容器控件或者内部包含多个子控件的复合控件

如何使用QSS

  在Qt中使用QSS的方式大致分为两种,一种是控件自己调用它的setStyleSheet方法,另一种是加载全局的QSS文件。

  • 方式一:
QWidget w;
w.setStyleSheet("padding:10px;background-color:black");
  • 方式二:
void loadQss()
{
    QFile qssFile("../sytle.qss");
    if (qssFile.open(QFile::ReadOnly))
    {
        QByteArray qss = qssFile.readAll();
        qApp->setStyleSheet(qss);
        qssFile.close();
    }
}

特定对象的样式表的属性会覆盖全局的样式表对应的属性(合并效果),特定对象多次调用setStyleSheet出现的现象是后面的覆盖前面的(覆盖效果)。详细的对比为:

特性 全局样式表 (qApp->setStyleSheet) 局部样式表 (widget->setStyleSheet)
优先级 低(作为默认生效) 高(覆盖全局和其他继承样式)
作用范围 所有符合选择器的控件 单个控件及其子控件(依赖选择器逻辑)
属性继承 ✓ 未设置的属性可从全局继承 ⨯ 清除原始样式,需手动合并
维护灵活性 方便统一管理基础样式 灵活定制个别控件的特殊样式

QSS语法介绍

下面内容基本翻译摘抄自Qt官方文档内容QSS语法

样式规则

  QSS样式表由一系列样式规则组成。一个样式规则由选择器和声明组成。选择器指定了受该规则影响的控件;声明指定了应设置在控件上的属性。格式如下:

选择器:{
    属性1:值;
    属性2:值1 值2;
    ...
    }

示例:

QPushButton { color: red }

在上述样式规则中,QPushButton 是选择器,{ color: red } 是声明。该规则指定了 QPushButton 及其子类(例如,MyPushButton)应使用红色作为前景色(例如文字颜色)。

Qt样式表通常不区分大小写(即,color、Color、COLOR 和 cOloR 指的是同一个属性)。唯一的例外是类名、对象名和Qt属性名,这些是区分大小写的。

如果多个选择器拥有相同的声明,则可以共用一个声明,选择器之间用,逗号隔开:

QPushButton, QLineEdit, QComboBox { color: red }

上面等价于:

QPushButton { color: red }
QLineEdit { color: red }
QComboBox { color: red }

样式规则的声明部分是一系列 属性 : 值 对,用大括号 ({}) 包围,并用分号分隔。例如:

QPushButton { color: red; background-color: white }

选择器类型

到目前为止,所有示例都使用了最简单的选择器类型,即类型选择器。Qt 样式表支持CSS2中定义的所有选择器 。下表总结了最常用的选择器类型。

选择器 示例 说明
通配选择器 * 匹配所有部件
类型选择器 QPushButton 匹配 QPushButton 及其子类的实例。
属性选择器 QPushButton[flat="false"] 匹配不是 QPushButton 的 扁平 实例。
类选择器 .QPushButton 匹配 QPushButton 的实例,但不包括其子类。这等同于 *[class~="QPushButton"]
ID选择器 QPushButton#okButton 匹配所有对象名称为okButton,类型为QPushButton的实例。
后代选择器 QDialog QPushButton 匹配所有作为 QDialog 的后代(子代、孙代等)的 QPushButton 实例。
子选择器 QDialog > QPushButton 匹配所有直接属于 QDialog 的 QPushButton 实例。

通配选择器:

  通常用于公用的属性,例如背景颜色,字体,字体大小,前景颜色等。以保证UI界面风格统一。

类型选择器:

  就是直接用控件类名作为选择器名,匹配所有这种类型的实例以及它子类的实例。匹配子类是因为Qt的样式表子类如果未明确指定,那么会继承父类的。

属性选择器:

  所谓属性选择器,就是选择器后跟一个属性表达式,表达式为真,则匹配上。这里提到属性表达式前是一个选择器,因此它可以是任何选择器,甚至包括属性选择器,而上面表格中看上去好像只能是类型选择器,实际不对!因此下面的写法都正确:

*[flat="false"] { key:value;...;}
QPushButton[default="true"] { key:value;...;}
/*关于连续的属性表达式可以理解为表达式相与*/
QPushButton[default="true"][flat="false"]{ key:value;...;}
QPushButton#pbtn_ok[default="true"] { key:value;...;}
...
...

属性表达式中值通常以字符串形式书写,但是我发现属性为boolean类型是,可以省略双引号"。也不知道这鬼东西为什么这么混乱!!

属性又包括静态属性和动态属性。静态属性可以看类的头文件,以QPushButton为例:

class Q_WIDGETS_EXPORT QPushButton : public QAbstractButton
{
    Q_OBJECT

    Q_PROPERTY(bool autoDefault READ autoDefault WRITE setAutoDefault)
    Q_PROPERTY(bool default READ isDefault WRITE setDefault)
    Q_PROPERTY(bool flat READ isFlat WRITE setFlat)
}    

当然也要注意,该类祖先的静态属性也可以被它用来做属性表达式。例如已QPushButton为例,它有个老祖宗QObject,里面有声明了如下静态属性:

class Q_CORE_EXPORT QObject
{
    Q_OBJECT

    Q_PROPERTY(QString objectName READ objectName WRITE setObjectName NOTIFY objectNameChanged)
}

objectName静态属性,因此我们属性表达式可以使用对象名(也就是ID选择器中的对象id),因此它们两个可以达到一样的效果:

QPushButton[objectName="pbtn_ok"]{ }
QPushButton#pbtn_ok{ }

也可以通过Designer添加动态属性:

img

它实际是调用继承自QObject的方法。

bool setProperty(const char *name, const QVariant &value);

对于动态属性,需要注意的是属性改变后,可能需要强制重新加载样式。例如:

// 通过定时器反转test属性状态
connect(timer, &QTimer::timeout, this, [&]() {
    ui->pbtn->setProperty("test", !ui->pbtn->property("test").toBool());
    // 卸载之前样式
    ui->pbtn->style()->unpolish(ui->pbtn);
    // 重新加载样式
    ui->pbtn->style()->polish(ui->pbtn);
});

timer->start(1000);

属性表达式的操作符有多种:

操作符 含义 实例 详细介绍
[attr] 是否存在 [flat] 就是只要包含该属性即可,不关心属性的值
= 等于 [flat="true"] 精准匹配属性值
^= 以值开头 [myProperty^="te"] 了解正则表达式都清楚,^表示行匹配的开头处
$= 以值结尾 [myProperty^="st"] 了解正则表达式都清楚,$表示行匹配的结尾处
~= 包含列表值 [myProperty~="ok"] 属性值中如果包含多个以空格分开的字符串,那么只要任意单词匹配上就成功。
比如属性表达式为[myProperty~="ok"]时,"state ok"、"status ok", 都包含ok,都会匹配上,但是token匹配不上。
*= 包含子串 [myProperty~="es"] 包含子串如test state都会被st或者te子串匹配上
|= 连字符匹配 [myProperty|="lang"] 所谓连字符匹配就是这里例子中会匹配上lang,lang-en, lang-zh
但是不会匹配上language。看这个例子也知道它一般用于语言匹配,区域匹配等场景

Qt官方文档我只看到介绍了=~=,其他的几种我是从网上找到的例子,并且看了qcss解析的源代码后才确定支持这几种:

bool Parser::parseAttrib(AttributeSelector *attr)
{                   
    skipSpace();                                                       
    if (!next(IDENT)) return false;
    attr->name = lexem();
    skipSpace();    

    if (test(EQUAL)) {                  // [attr=value]
        attr->valueMatchCriterium = AttributeSelector::MatchEqual;
    } else if (test(INCLUDES)) {        // [attr~=value]
        attr->valueMatchCriterium = AttributeSelector::MatchIncludes;
    } else if (test(DASHMATCH)) {       // [attr|=value]
        attr->valueMatchCriterium = AttributeSelector::MatchDashMatch;
    } else if (test(BEGINSWITH)) {      // [attr^=value]
        attr->valueMatchCriterium = AttributeSelector::MatchBeginsWith;
    } else if (test(ENDSWITH)) {        // [attr$=value]
        attr->valueMatchCriterium = AttributeSelector::MatchEndsWith;
    } else if (test(CONTAINS)) {        // [attr*=value]
        attr->valueMatchCriterium = AttributeSelector::MatchContains;
    } else {        
        return next(RBRACKET);
    }               
                    
    skipSpace();    
                    
    if (!test(IDENT) && !test(STRING)) return false;
    attr->value = unquotedLexem();
                    
    skipSpace();    
    return next(RBRACKET);
}                   

相关源码,枚举定义位于qtbase/src/gui/text/qcssparser.cpp,qtbase/src/gui/text/qcssparser_p.h, qtbase/src/gui/text/qcssparser.cpp 里面。

需要注意的是,使用属性表达式时有几个小坑需要注意

  1. 选择器和属性表达式之间不要有空格,css QPushButton [flat="false]{ color:red;}
  2. 属性表达式如果为~=时,属性和操作符之间不能有空格。css QPushButton[myProperty ~= "hello" ]{color:red;}

我的测试验证版本是Qt5.15,发行版是Ubuntu22.04。这多半是一个bug。

类选择器

  类选择器就是严格匹配类,不包括它的子类。

ID选择器

  所谓ID就是指对象的objectName,每次就是仅针对objectName属性匹配,上面也介绍了,用属性选择器可以到达同样的效果。通常Id相同的话(即控件变量名),他们不会出现在同一个UI内,因为冲突了嘛, 举几个例子:

/*下面两种等效*/
#myId{color:red;}
*#myId{color:red;}
/*限定特定类型*/
QPushButton#myId{color:red;}
/*和属性选择器组合*/
QPushButton#commonid[flat="true"]{
    color:red;
}

后代选择器

  后代选择器Ancestors Descendants指的是具体对象的父子关系,而不是继承关系,理解这点非常重要。例如有如下自定义控件:

class MyButton : public QPushButton
{
    Q_OBJECT

public:
    explicit MyButton(QWidget *parent = nullptr);
    ~MyButton() override;
};

然后往界面中分别添加QPushButton和MyButton的按钮, 如果使用如下样式表

* {
    color: blue;
}

QPushButton MyButton{
    color: red;
}

最终的效果是这样:

img

没达到预期的效果,我原来的理解是Mybutton不是继承于QPushButton吗?这里按道理匹配得上。认真阅读,才发现,是Ancestors要和Descendants有血缘关系(这是针对对象),而不是继承关系(这是针对类)。甚至Descendants的类型都可以不是Ancestors的派生类型。
只要AncestorsDescendants的祖先对象即可。

例如如下例子:

#include <QApplication>
#include <QFile>
#include <QPushButton>
#include <QVBoxLayout>
#include <QLabel>

#include "mainwidget.h"

void loadQss()
{
    QFile qssFile("../sytle.qss");
    if (qssFile.open(QFile::ReadOnly))
    {
        QByteArray qss = qssFile.readAll();
        qApp->setStyleSheet(qss);
        qssFile.close();
    }
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    loadQss();

    QWidget w;
    QVBoxLayout layout(&w);

    QPushButton button("hello");
    layout.addWidget(&button);

    QLabel label("world");
    layout.addWidget(&label);

    w.show();

    return QApplication::exec();
}

往QWidget w分别添加了两个子对象button和label。然后使用如下样式表:

* {
    color: blue;
}

QWidget QPushButton{
    color: red;
}

最终效果:

img

所以该选择器含义是:所有属于QWidget对象的子对象的QPushButton对象匹配上。当然它也可以有如下写法:

/*省略或者通配祖先对象类型*/
QPushButton{color:red;}
* QPushButton{color:red;}

/*省略或者通配字子对象类型*/
QWidget {color:red;}
QWidget *{color:red;}

/*标准形式*/
QWidget QPushButton{
    color: red;
}
/*组合形式*/
QWidget QPushButton[flat="false"]{
    color: red;
}
posted @ 2025-06-18 17:32  thammer  阅读(210)  评论(0)    收藏  举报