第六章 界面优化 —— QSS 样式表与 2D 绘图完全解析

本文梳理了 Qt 界面优化的两大核心方向:QSS(Qt 样式表)美化和 2D 绘图系统。全文涵盖 QSS 语法体系、选择器系统、盒模型、伪状态、子控件以及 QPainter 绘图 API、坐标变换、绘图设备等全部知识点,力求让读者全面掌握 Qt 界面美化与自定义绘制技术。


第一部分:QSS 样式表

一、QSS 概述

1.1 什么是 QSS?

QSS(Qt Style Sheets)是 Qt 框架中用于定义界面样式的机制,其设计思想直接借鉴自 Web 前端的 CSS(Cascading Style Sheets,层叠样式表)。如果你有 CSS 基础,学习 QSS 将非常快;即使没有,QSS 的语法也远比纯 C++ 代码设置样式要直观和高效。

QSS 与 CSS 的关系:

维度CSSQSS
应用领域HTML 网页Qt 桌面应用
选择器丰富的 CSS 选择器支持 CSS2 的大部分选择器
属性font-size, color, margin…几乎同名同义
盒模型content → padding → border → margin完全相同
伪状态:hover, :focus, :active:hover, :pressed, :checked…
不支持—不支持 CSS 动画、flexbox、grid 等高级特性

为什么 QSS 借鉴 CSS? CSS 作为 Web 前端领域的样式定义语言,经过二十多年的发展已经非常成熟。HTML 通过 CSS 将"内容"和"表现"彻底分离的设计思想,同样适用于 GUI 应用——通过 QSS,开发者可以将界面的外观样式与业务逻辑代码解耦,使代码更清晰、更易维护。

1.2 QSS 的核心价值

在没有 QSS 的时代,设置 Qt 控件样式需要在 C++ 代码中逐一调用 setFont()、setPalette()、setStyle() 等方法,代码冗长且难以全局调整。有了 QSS:

  • 样式集中管理:一套 QSS 可以控制整个应用的风格
  • 代码与外观分离:换肤只需替换 QSS 文件,无需修改 C++ 逻辑
  • 设计师友好:UI 设计师可以直接参与 QSS 编写
  • 高效灵活:几行 QSS 代码就能实现数百行 C++ 的样式效果

二、QSS 基本语法

2.1 语法规则

QSS 的语法与 CSS 完全一致:

选择器 {
    属性: 值;
    属性: 值;
}

组成部分说明:

  • 选择器(Selector):指定要应用样式的控件类型,如 QPushButton
  • 属性(Property):要设置的样式属性名称,如 color、font-size
  • 值(Value):属性的取值,如 red、20px
  • 声明块:用花括号 { } 包裹,多条声明用分号 ; 分隔

基础示例:

/* 将所有 QPushButton 的文字设为红色 */
QPushButton { color: red; }

/* 多行写法更清晰 */
QPushButton {
    color: red;
    font-size: 20px;
}

2.2 QSS 的三种应用方式

方式一:通过 C++ 代码直接设置

在代码中调用 setStyleSheet() 方法:

// 为特定控件设置样式
ui->pushButton->setStyleSheet("QPushButton { color: red; }");
方式二:从外部 QSS 文件加载

将样式写在独立的 .qss 文件中,程序启动时加载:

QFile file(":/style.qss");
file.open(QFile::ReadOnly);
QString style = file.readAll();
a.setStyleSheet(style);
file.close();
方式三:在 Qt Designer 中可视化设置

在 Qt Designer 中右键控件 → “改变样式表”,在弹出窗口中直接编辑 QSS 代码并可实时预览效果。


三、QSS 的应用方式

3.1 作用于单个控件

最直接的方式,调用控件自身的 setStyleSheet() 方法。注意:样式只会作用于该控件本身,不影响其他同类控件。

Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget)
{
    ui->setupUi(this);

    // 只有这一个按钮变成红色文字
    ui->pushButton->setStyleSheet("QPushButton { color: red; }");
}

3.2 作用于全局

在 QApplication 上调用 setStyleSheet(),样式将作用于整个应用程序的所有控件:

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

    // 全局样式:所有 QPushButton 都将是红色文字
    a.setStyleSheet("QPushButton { color: red; }");

    Widget w;
    w.show();
    return a.exec();
}

注意:全局样式如果写了 QWidget { color: red; },Qt 默认不会继承到子控件上,需要加 .QWidget(类选择器)才能只作用于 QWidget 本身而不影响子类。这是因为 Qt 的样式继承规则与 CSS 有所不同——子控件不会自动继承父类控件的样式。

3.3 样式优先级与层叠规则

当同一个控件被多处 QSS 规则命中时,Qt 按以下优先级决定最终生效的样式(从低到高):

级别 1:QApplication 全局样式(qApp->setStyleSheet)
    ↓ 可被覆盖
级别 2:父控件样式(parentWidget->setStyleSheet)
    ↓ 可被覆盖
级别 3:控件自身样式(widget->setStyleSheet)
    ↓ 可被覆盖
级别 4:选择器优先级(ID > Class > Type > *)
    ↓ 可被覆盖
级别 5:同优先级中,后定义的覆盖先定义的

示例:验证层级覆盖

// main.cpp —— 全局样式
a.setStyleSheet("QPushButton { color: red; }");

// widget.cpp —— 控件自身样式(优先级更高)
ui->pushButton->setStyleSheet("QPushButton { color: green; }");
// 结果:按钮显示为绿色(控件自身样式覆盖了全局样式)

层叠规则(与 CSS 相同):

  • 同一个属性在多个规则中定义时,优先级高(更具体)的规则生效
  • 优先级相同时,后定义的覆盖先定义的
  • ID 选择器(#objectName)比类型选择器(QPushButton)优先级更高

3.4 从文件加载 QSS

将 QSS 写在独立文件中是推荐的做法。具体步骤如下:

第 1 步:新建 Qt 资源文件 resource.qrc,添加前缀 /,将 style.qss 文件添加到资源中。

第 2 步:编写 style.qss 文件内容:

QPushButton {
    color: red;
}

第 3 步:在 main.cpp 中编写加载函数并调用:

// 从 Qt 资源文件中加载 QSS
QString loadQSS()
{
    QFile file(":/style.qss");           // 打开资源文件
    file.open(QFile::ReadOnly);           // 以只读方式打开
    QString style = file.readAll();       // 读取全部内容
    file.close();                          // 关闭文件
    return style;
}

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

    // 加载并应用 QSS
    const QString &style = loadQSS();
    a.setStyleSheet(style);

    Widget w;
    w.show();
    return a.exec();
}

扩展封装:实际项目中可以封装一个工具函数:

void setStyleSheetFromFile(const QString &path)
{
    QFile file(path);
    if (file.open(QFile::ReadOnly)) {
        qApp->setStyleSheet(file.readAll());
        file.close();
    }
}

3.5 Qt Designer 设置 QSS

在 Qt Designer 中也可以直接编写 QSS:

  1. 选中目标控件(或空白区域选中 Widget)
  2. 右键 → “改变样式表…”
  3. 在弹出的编辑器中编写 QSS 代码,点击 OK 确认
  4. Qt Designer 会实时预览样式效果(部分属性可能需要运行程序才能看到效果)

背后的原理:在 Qt Designer 中设置的样式最终会写入 .ui 文件(XML 格式)中,对应控件的 <property name="styleSheet"> 标签。编译时 uic 工具会将其转换为 C++ 的 setStyleSheet() 调用。

四种设置方式的应用场景对比:

方式优点适用场景
代码中 setStyleSheet灵活,可动态修改需要运行时动态改变样式
全局 setStyleSheet统一风格,一改全改应用级别主题切换
qss 文件加载样式与代码分离,方便维护大型项目,需要换肤功能
Qt Designer 设置可视化,所见即所得快速原型设计

四、QSS 选择器详解

选择器是 QSS 的核心,它决定了样式规则应用于哪些控件。QSS 支持 CSS2 规范中的绝大部分选择器类型。

4.1 基本选择器

选择器类型语法说明示例
通配选择器*匹配所有控件* { color: red; }
类型选择器ClassName匹配该类及其子类的实例QPushButton { color: red; }
类选择器.ClassName只匹配该类本身的实例,不包括子类.QPushButton { color: red; }
ID 选择器#objectName匹配 objectName 为指定值的控件#pushButton_2 { color: red; }
后代选择器A B匹配 A 内部的所有 B(含嵌套多级)QDialog QPushButton { color: red; }
子选择器A > B只匹配 A 的直接子控件 BQDialog > QPushButton { color: red; }
并集选择器A, B, C匹配 A、B、C 中的任意一个(或关系)QPushButton, QLabel, QLineEdit { color: red; }
属性选择器A[prop="val"]匹配具有指定属性值的控件QPushButton[flat="false"] { color: red; }

各选择器详细示例:

(1)通配选择器 *
// 设置所有控件文字为红色
a.setStyleSheet("* { color: red; }");
(2)类型选择器 vs 类选择器
// 类型选择器:QPushButton 和它的子类都会被匹配
a.setStyleSheet("QPushButton { color: red; }");

// 类选择器:只有 QPushButton 自身被匹配,子类不受影响
a.setStyleSheet(".QPushButton { color: red; }");

注意:QSS 中的类选择器 .QPushButton 依赖于控件拥有对应的 CSS 类名,这与 C++ 的继承机制有所不同。

(3)ID 选择器
// 假设 UI 中有三个按钮,objectName 分别为 pushButton、pushButton_2、pushButton_3
QString style = "";
style += "QPushButton { color: yellow; }";      // 所有按钮默认黄色
style += "#pushButton { color: red; }";           // 第一个按钮覆盖为红色
style += "#pushButton_2 { color: green; }";       // 第二个按钮覆盖为绿色
a.setStyleSheet(style);
(4)后代选择器 vs 子选择器
// 后代选择器:QDialog 内部所有 QPushButton(不管嵌套多少层)都生效
a.setStyleSheet("QDialog QPushButton { color: red; }");

// 子选择器:只有 QDialog 的直接子控件 QPushButton 才生效
a.setStyleSheet("QDialog > QPushButton { color: red; }");
(5)并集选择器
// 让按钮、标签、输入框同时应用同一套样式
a.setStyleSheet("QPushButton, QLabel, QLineEdit { color: red; }");

选择器优先级(从高到低):ID 选择器 > 类选择器 > 类型选择器 > 通配选择器。多个选择器组合时,越具体的优先级越高。

4.2 子控件选择器 (Sub-Controls)

有些复杂控件(如 QComboBox、QSpinBox、QProgressBar)由多个"子控件"组合而成。QSS 通过 :: 语法来访问这些子控件,实现对控件内部细节的样式定制。

语法格式:

ClassName::sub-control {
    property: value;
}

常用子控件:

子控件所属控件说明
::down-arrowQComboBox、QSpinBox下拉箭头/增减箭头的图标
::indicatorQCheckBox、QRadioButton勾选框/单选按钮的指示器
::chunkQProgressBar进度条的前进块
::itemQMenuBar、QMenu、QListView菜单栏/菜单/列表的单个项
::separatorQMenu菜单中的分隔线

示例 1:自定义 QComboBox 的下拉箭头

// 使用自定义图片替换默认的下拉箭头
QString style = "";
style += "QComboBox::down-arrow { image: url(:/down.png); }";
a.setStyleSheet(style);

示例 2:自定义 QProgressBar 的进度块颜色

// 在 Qt Designer 中选中 QProgressBar,设置 styleSheet
QString style = "QProgressBar::chunk { background-color: #FF0000; }";
ui->progressBar->setStyleSheet(style);

注意:QProgressBar 的 alignment 属性在 Qt 某些版本中存在 bug,可能导致文字居中无效。建议通过 QSS 补充设置或使用自定义绘制来规避。

完整子控件列表:Qt 官方文档中 “Qt Style Sheets Reference” → “List of Sub-Controls” 列出了所有可用的子控件。

4.3 伪状态选择器 (Pseudo-States)

伪状态用于描述控件在特定交互状态下的外观。例如:鼠标悬停、按钮被按下、获得焦点等。

语法格式:

ClassName:state {
    property: value;
}

常用伪状态:

伪状态说明示例
:hover鼠标悬停在控件上QPushButton:hover { color: green; }
:pressed控件被按下QPushButton:pressed { color: blue; }
:focus控件获得焦点QLineEdit:focus { border-color: blue; }
:enabled控件处于可用状态QPushButton:enabled { ... }
:disabled控件处于禁用状态QPushButton:disabled { color: gray; }
:checked控件处于选中状态(CheckBox/RadioButton)QCheckBox:checked { ... }
:unchecked控件处于未选中状态QCheckBox:unchecked { ... }
:selected列表项被选中QListView::item:selected { ... }
:read-only控件处于只读状态QLineEdit:read-only { ... }

取反伪状态:在伪状态前加 ! 表示"非":

QPushButton:!hover { color: red; }      /* 未悬停时红色 */
QPushButton:!pressed { color: black; }  /* 未按下时黑色 */

示例 1:按钮三态颜色切换

QString style = "";
style += "QPushButton { color: red; }";              // 默认:红色
style += "QPushButton:hover { color: green; }";       // 悬停:绿色
style += "QPushButton:pressed { color: blue; }";      // 按下:蓝色
a.setStyleSheet(style);

效果:按钮默认显示红色文字,鼠标悬停变绿,鼠标按下变蓝。


示例 2:通过 C++ 事件自定义伪状态行为

QSS 的伪状态由 Qt 内部事件驱动。如果我们自定义了某个控件的事件处理,可以模拟伪状态效果。以下示例通过重写 mousePressEvent、mouseReleaseEvent、enterEvent、leaveEvent 来实现更灵活的样式切换:

mypushbutton.h

#include <QPushButton>
#include <QMouseEvent>
#include <QEvent>

class MyPushButton : public QPushButton
{
public:
    MyPushButton(QWidget *parent);

protected:
    void mousePressEvent(QMouseEvent *e) override;
    void mouseReleaseEvent(QMouseEvent *e) override;
    void enterEvent(QEvent *e) override;
    void leaveEvent(QEvent *e) override;
};

mypushbutton.cpp

MyPushButton::MyPushButton(QWidget *parent) : QPushButton(parent)
{
    // 设置默认样式
    this->setStyleSheet("QPushButton { color: red; }");
}

void MyPushButton::mousePressEvent(QMouseEvent *e)
{
    this->setStyleSheet("QPushButton { color: blue; }");   // 按下变蓝
    QPushButton::mousePressEvent(e);
}

void MyPushButton::mouseReleaseEvent(QMouseEvent *e)
{
    this->setStyleSheet("QPushButton { color: green; }");  // 释放变绿
    QPushButton::mouseReleaseEvent(e);
}

void MyPushButton::enterEvent(QEvent *e)
{
    this->setStyleSheet("QPushButton { color: green; }");  // 进入变绿
    QPushButton::enterEvent(e);
}

void MyPushButton::leaveEvent(QEvent *e)
{
    this->setStyleSheet("QPushButton { color: red; }");    // 离开恢复红色
    QPushButton::leaveEvent(e);
}

直接使用 QSS 伪状态更优:上述 C++ 代码实现的效果完全可以用 QSS 的 :hover 和 :pressed 伪状态实现,且代码更简洁。仅在 QSS 无法满足需求时(如需要播放动画、触发其他逻辑等)才使用 C++ 事件方式。


五、QSS 属性与盒模型

QSS 支持丰富的属性,包括字体、颜色、背景、边框、内外边距等。详情可查阅 Qt 官方文档 “Qt Style Sheets Reference” 中的属性列表。

5.1 盒模型 (Box Model)

盒模型是 QSS 中最基础的布局概念,与 CSS 的盒模型完全一致。每个控件都视为一个矩形盒子,由内到外分为四层:

┌────────────────────────────────────────┐
│             margin(外边距)              │
│  ┌──────────────────────────────────┐  │
│  │         border(边框)             │  │
│  │  ┌────────────────────────────┐  │  │
│  │  │       padding(内边距)      │  │  │
│  │  │  ┌──────────────────────┐  │  │  │
│  │  │  │   content(内容区域)  │  │  │  │
│  │  │  │   文字/图标等          │  │  │  │
│  │  │  └──────────────────────┘  │  │  │
│  │  └────────────────────────────┘  │  │
│  └──────────────────────────────────┘  │
└────────────────────────────────────────┘

盒模型属性对照表:

QSS 属性说明
margin外边距,控件边框之外到父容器/相邻控件的距离。不影响 geometry
padding内边距,控件边框之内的留白空间
border-style边框样式:solid(实线)、dashed(虚线)、dotted(点线)、none(无边框)等
border-width边框宽度
border-color边框颜色
border边框简写:border: 宽度 样式 颜色; 例如 border: 5px solid red;

margin vs geometry:margin 是 QSS 层面的视觉间距,不会改变控件的 geometry() 返回值。控件在布局中的实际位置仍然由布局管理器决定。


示例 1:边框和内边距

// 给 QLabel 添加红色实线边框和左侧内边距
a.setStyleSheet("QLabel { border: 5px solid red; padding-left: 10px; }");

示例 2:margin 不改变 geometry

// widget.cpp 中创建一个按钮并设置 margin
QPushButton *btn = new QPushButton(this);
btn->setGeometry(0, 0, 100, 100);
btn->setText("hello");
btn->setStyleSheet("QPushButton { border: 5px solid red; margin: 20px; }");

const QRect &rect = btn->geometry();
qDebug() << rect;  // 输出仍然是 (0, 0, 100, 100),margin 未改变 geometry

六、QSS 实战案例

6.1 按钮美化

目标效果:圆角按钮,带默认背景色,按下时背景色改变。

QPushButton {
    font-size: 20px;
    border: 2px solid #8f8f91;
    border-radius: 15px;
    background-color: #dadbde;
}

QPushButton:pressed {
    background-color: #f6f7fa;
}

关键属性解释:

属性说明
font-size设置按钮文字大小
border-radius设置圆角半径(值越大越圆,等于高度一半时为胶囊形)
background-color设置按钮背景色
:pressed按下时的样式覆盖

效果:按钮默认是浅灰色圆角背景,按下时背景变为更浅的亮灰色,形成"被按下"的视觉反馈。

6.2 QCheckBox 美化

目标效果:使用自定义图片替换 QCheckBox 默认的勾选框,支持未选/已选 × 悬停/按下共 6 种状态。

准备素材:准备 6 张 PNG 图片,对应不同状态:

图片文件对应状态
checkbox-unchecked.png未勾选,默认
checkbox-unchecked_hover.png未勾选,鼠标悬停
checkbox-unchecked_pressed.png未勾选,鼠标按下
checkbox-checked.png已勾选,默认
checkbox-checked_hover.png已勾选,鼠标悬停
checkbox-checked_pressed.png已勾选,鼠标按下

将图片添加到 resource.qrc。

QSS 代码:

QCheckBox {
    font-size: 20px;
}

QCheckBox::indicator {
    width: 20px;
    height: 20px;
}

QCheckBox::indicator:unchecked {
    image: url(:/checkbox-unchecked.png);
}

QCheckBox::indicator:unchecked:hover {
    image: url(:/checkbox-unchecked_hover.png);
}

QCheckBox::indicator:unchecked:pressed {
    image: url(:/checkbox-unchecked_pressed.png);
}

QCheckBox::indicator:checked {
    image: url(:/checkbox-checked.png);
}

QCheckBox::indicator:checked:hover {
    image: url(:/checkbox-checked_hover.png);
}

QCheckBox::indicator:checked:pressed {
    image: url(:/checkbox-checked_pressed.png);
}

关键点解析:

知识点说明
::indicatorQCheckBox 的子控件——那个小方框
:unchecked未勾选状态
:checked已勾选状态
:hover鼠标悬停在 indicator 上
:pressed鼠标在 indicator 上按下
width / height设置 indicator 的尺寸(影响 image 的渲染尺寸)
image使用图片替换默认的绘制,图片会根据 width/height 缩放

6.3 QRadioButton 美化

QRadioButton 的美化方式与 QCheckBox 几乎完全相同,也是通过 ::indicator 子控件配合 :checked / :unchecked 伪状态来实现。

重要提醒:QRadioButton 是互斥的,同一组 QRadioButton 需要放在同一个父容器中才能自动互斥。如果放在不同的 QWidget 中,则不会互斥!

/* 使用后代选择器 + QWidget 来限定样式范围 */
QWidget QRadioButton {
    font-size: 20px;
}

QWidget QRadioButton::indicator {
    width: 20px;
    height: 20px;
}

QWidget QRadioButton::indicator:unchecked {
    image: url(:/radio-unchecked.png);
}

QWidget QRadioButton::indicator:unchecked:hover {
    image: url(:/radio-unchecked_hover.png);
}

QWidget QRadioButton::indicator:unchecked:pressed {
    image: url(:/radio-unchecked_pressed.png);
}

QWidget QRadioButton::indicator:checked {
    image: url(:/radio-checked.png);
}

QWidget QRadioButton::indicator:checked:hover {
    image: url(:/radio-checked_hover.png);
}

QWidget QRadioButton::indicator:checked:pressed {
    image: url(:/radio-checked_pressed.png);
}

QCheckBox vs QRadioButton 样式对比:两者的 QSS 写法完全一致,唯一的区别在于逻辑行为——QCheckBox 允许多选,QRadioButton 在同一父容器中互斥。

6.4 编辑框美化

目标效果:暗色主题风格的 QLineEdit。

QLineEdit {
    border-width: 1px;
    border-radius: 10px;
    border-color: rgb(58, 58, 58);
    border-style: inset;
    padding: 0 8px;
    color: rgb(255, 255, 255);
    background: rgb(100, 100, 100);
    selection-background-color: rgb(187, 187, 187);
    selection-color: rgb(60, 63, 65);
}

属性解析:

属性说明
border-width边框宽度
border-radius圆角半径
border-color边框颜色
border-style边框样式:inset 为内凹效果
padding内边距:上下 0,左右 8px
color文字颜色(前景色)
background背景颜色
selection-background-color选中文字时的背景色(高亮色)
selection-color选中文字时的文字颜色

6.5 QListView 美化

目标效果:列表项悬停时显示渐变背景,选中时显示带边框的渐变背景。

QListView::item:hover {
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
                        stop: 0 #FAFBFE, stop: 1 #DCDEF1);
}

QListView::item:selected {
    border: 1px solid #6a6ea9;
    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
                        stop: 0 #6a6ea9, stop: 1 #888dd9);
}

qlineargradient 渐变详解:

qlineargradient 是 QSS 提供的线性渐变函数,通过指定起点和终点坐标以及颜色停止点来定义一个渐变。

语法:

qlineargradient(x1: 起点x, y1: 起点y, x2: 终点x, y2: 终点y,
                stop: 位置 color, stop: 位置 color, ...)

渐变方向对照表:

方向参数效果
从上到下x1:0, y1:0, x2:0, y2:1垂直渐变
从左到右x1:0, y1:0, x2:1, y2:0水平渐变
从左上到右下x1:0, y1:0, x2:1, y2:1对角线渐变

stop 参数说明:

  • stop: 0 表示渐变的起始位置(0%)
  • stop: 1 表示渐变的结束位置(100%)
  • 可以添加更多 stop 点,如 stop: 0 #fff, stop: 0.5 #888, stop: 1 #000

渐变实战对比:

/* 从上到下:白 → 黑 */
QWidget {
    background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop: 0 #fff, stop: 1 #000);
}

/* 从左到右:白 → 黑 */
QWidget {
    background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop: 0 #fff, stop: 1 #000);
}

6.6 菜单栏美化

目标效果:自定义 QMenuBar 和 QMenu 的外观。

QMenuBar {
    background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                              stop:0 lightgray, stop:1 darkgray);
    spacing: 3px;                          /* 菜单项之间的间距 */
}

QMenuBar::item {
    padding: 1px 4px;
    background: transparent;
    border-radius: 4px;
}

QMenuBar::item:selected {                   /* 选中(鼠标悬停) */
    background: #a8a8a8;
}

QMenuBar::item:pressed {                    /* 按下 */
    background: #888888;
}

QMenu {
    background-color: white;
    margin: 0 2px;                         /* 菜单边缘留白 */
}

QMenu::item {
    padding: 2px 25px 2px 20px;
    border: 3px solid transparent;
}

QMenu::item:selected {
    border-color: darkblue;
    background: rgba(100, 100, 100, 150);
}

QMenu::separator {
    height: 2px;
    background: lightblue;
    margin-left: 10px;
    margin-right: 5px;
}

关键子控件说明:

子控件说明
QMenuBar::item菜单栏上的每个菜单项
QMenuBar::item:selected菜单栏项被高亮(鼠标悬停)
QMenuBar::item:pressed菜单栏项被按下(菜单展开中)
QMenu::item下拉菜单中的每个选项
QMenu::item:selected下拉菜单选项被高亮
QMenu::separator下拉菜单中的分隔线

6.7 综合案例:登录界面美化

目标效果:创建一个带背景图片的精美登录界面,包含头像/Logo 区域、账号密码输入框、"记住密码"复选框和登录按钮。

第 1 步:UI 布局设计。

  • 拖入一个 QFrame 作为背景容器
  • 在 QFrame 内使用 QVBoxLayout 垂直布局
  • 添加 QLineEdit(账号输入框,设置 minimumHeight: 50)
  • 添加 QLineEdit(密码输入框,设置 minimumHeight: 50)
  • 添加 QCheckBox(“记住密码”,居左对齐)
  • 添加 QPushButton("登录"按钮)

第 2 步:准备背景图片及图标素材,添加到 resource.qrc。

第 3 步:编写 QSS 样式。

QFrame 背景设置:

QFrame {
    border-image: url(:/cat.jpg);
}

border-image vs background-image:

  • border-image:图片会按边框区域自动切分为九宫格并拉伸,能较好地适应不同尺寸
  • background-image:图片直接拉伸填充整个控件
  • 对于背景图推荐使用 border-image

QLineEdit 输入框样式:

QLineEdit {
    color: #8d98a1;
    background-color: #405361;
    padding: 0 5px;
    font-size: 20px;
    border-style: none;
    border-radius: 10px;
}

QCheckBox 复选框样式:

QCheckBox {
    color: white;
    background-color: transparent;    /* 透明背景,显示出 QFrame 的图片 */
}

QPushButton 登录按钮样式:

QPushButton {
    font-size: 20px;
    color: white;
    background-color: #555;
    border-style: outset;
    border-radius: 10px;
}

QPushButton:pressed {
    color: black;
    background-color: #ced1db;
    border-style: inset;
}

border-style: outset vs inset:outset 模拟按钮凸起的效果,inset 模拟按钮被按下的凹陷效果。配合 background-color 变化,可以营造出逼真的物理按钮感觉。

完整 QSS 汇总:

QFrame {
    border-image: url(:/cat.jpg);
}

QLineEdit {
    color: #8d98a1;
    background-color: #405361;
    padding: 0 5px;
    font-size: 20px;
    border-style: none;
    border-radius: 10px;
}

QCheckBox {
    color: white;
    background-color: transparent;
}

QPushButton {
    font-size: 20px;
    color: white;
    background-color: #555;
    border-style: outset;
    border-radius: 10px;
}

QPushButton:pressed {
    color: black;
    background-color: #ced1db;
    border-style: inset;
}

七、QSS 学习资源

QSS 能做的事情远不止本文所述。以下资源可供进一步学习:

  • Qt 官方文档:Qt Style Sheets Reference —— 所有支持的选择器、属性和伪状态列表
  • Qt 官方示例:Qt Style Sheets Examples —— 大量可直接使用的 QSS 代码片段
  • 开源 QSS 主题:QSS 主题仓库 —— 社区贡献的成品 QSS 主题(如黑色主题、Material 风格等)
  • 遇到样式不生效时:
    1. 检查选择器是否正确(控件类型、objectName)
    2. 检查是否存在优先级更高的规则覆盖
    3. 对于复杂控件(QComboBox、QSpinBox),内层子控件可能需要单独设置
    4. 某些属性(如 alignment)在特定 Qt 版本中存在 bug,可能需要使用 C++ 代码作为替代

第二部分:2D 绘图

八、2D 绘图概述

Qt 的 2D 绘图系统基于 QPainter 类,提供了类似"画家在画布上作画"的编程模型。核心架构由三大组件构成:

┌──────────────────────────────────────────────┐
│                 QPainter                      │
│              (画家 —— 执行绘制)              │
│                                              │
│   拿着 QPen(画笔)画轮廓线、边框               │
│   拿着 QBrush(画刷)填充内部区域               │
│   拿着 QFont 写文字                            │
│                                              │
│   在 QPaintDevice(绘图设备)上作画             │
│   ├── QWidget(窗口 / 控件)                  │
│   ├── QPixmap(位图,显示优化)               │
│   ├── QImage(位图,像素级操作)              │
│   └── QPicture(绘图指令记录与回放)          │
└──────────────────────────────────────────────┘

QPainter 的"画家"类比:

角色类说明
画家QPainter提供各种 drawXXX() 方法,执行具体的绘制操作
画布QPaintDevice绘制的"目标",QWidget、QPixmap、QImage 等都是画布
画笔QPen控制线条的颜色、宽度、样式(实线/虚线等)
画刷QBrush控制填充区域的颜色和纹理

8.1 paintEvent —— 绘图的入口

在 QWidget 上绘图,必须重写 paintEvent() 函数。Qt 在以下情况会自动调用 paintEvent():

  • 控件首次显示时
  • 窗口从遮档状态恢复时(从最小化恢复、被其他窗口挡住后重新露出)
  • 调用 update() 或 repaint() 手动触发重绘
// 手动触发重绘
update();    // 推荐:异步请求重绘,Qt 会合并多次调用,优化性能
repaint();   // 立即同步重绘(一般不推荐,除非需要即时反馈如动画)

重要区别:update() 不会立即执行重绘,而是向事件队列中放入一个绘制事件,Qt 会等待当前事件处理完毕后再统一重绘。如果连续多次调用 update(),Qt 会将其合并为一次重绘,避免不必要的性能浪费。


九、基本绘图操作

9.1 绘制直线

函数原型:

// 通过两个 QPoint 绘制
void QPainter::drawLine(const QPoint &p1, const QPoint &p2);

// 或通过四个坐标绘制
void QPainter::drawLine(int x1, int y1, int x2, int y2);

示例:

// 在 widget.h 中声明
protected:
    void paintEvent(QPaintEvent *event) override;

// 在 widget.cpp 中实现
void Widget::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);

    QPainter painter(this);  // 创建画家,this 就是画布

    // 从 (50, 50) 到 (200, 200) 画一条线
    painter.drawLine(50, 50, 200, 200);

    // 再画一条水平线
    painter.drawLine(QPoint(50, 100), QPoint(300, 100));
}

9.2 绘制矩形

函数原型:

void QPainter::drawRect(int x, int y, int width, int height);

参数说明:

参数说明
x矩形左上角的 X 坐标
y矩形左上角的 Y 坐标
width矩形宽度
height矩形高度

示例:

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);

    // 在 (100, 50) 位置画一个 200×150 的矩形
    painter.drawRect(100, 50, 200, 150);

    // 多个矩形叠加
    painter.drawRect(50, 50, 100, 100);
    painter.drawRect(150, 100, 150, 80);
}

9.3 绘制椭圆

函数原型:

void QPainter::drawEllipse(const QPoint &center, int rx, int ry);

参数说明:

参数说明
center椭圆的中心点
rx水平方向半径(X 轴半轴长度)
ry垂直方向半径(Y 轴半轴长度)

当 rx == ry 时,绘制的是正圆。

示例:

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);

    // 以 (200, 200) 为圆心,水平半径 100,垂直半径 60(椭圆)
    painter.drawEllipse(QPoint(200, 200), 100, 60);

    // 正圆:以 (100, 100) 为圆心,半径 80
    painter.drawEllipse(QPoint(100, 100), 80, 80);
}

9.4 绘制文字

核心 API:

void QPainter::drawText(const QPoint &position, const QString &text);
void QPainter::setFont(const QFont &font);

示例:

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);

    // 设置字体
    QFont font("微软雅黑", 24);
    font.setBold(true);            // 加粗
    font.setItalic(true);          // 斜体
    painter.setFont(font);

    // 在指定位置绘制文字
    painter.drawText(QPoint(100, 100), "Hello Qt!");

    // 换个字体再画
    QFont font2("宋体", 16);
    painter.setFont(font2);
    painter.drawText(QPoint(100, 200), "这是一段中文文字");
}

9.5 画笔 —— QPen

QPen 控制图形轮廓的绘制效果,包括线条颜色、宽度、样式等。

核心 API:

// 构造函数:指定颜色
QPen::QPen(const QColor &color);

// 设置线条宽度(像素)
void QPen::setWidth(int width);

// 设置线条样式
void QPen::setStyle(Qt::PenStyle style);

// 设置画刷风格的线条(如渐变线条)
void QPen::setBrush(const QBrush &brush);

Qt::PenStyle 常用样式:

样式说明
Qt::SolidLine实线
Qt::DashLine短划线 --------
Qt::DotLine点线 ........
Qt::DashDotLine点划线 -·-·-·-·
Qt::DashDotDotLine双点划线 -··-··-··
Qt::NoPen不绘制线条(无轮廓)

完整示例:

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);

    // 创建红色画笔,宽度 5px,实线
    QPen pen(Qt::red);
    pen.setWidth(5);
    pen.setStyle(Qt::SolidLine);
    painter.setPen(pen);

    // 用此画笔绘制矩形(轮廓为红色实线)
    painter.drawRect(50, 50, 200, 150);

    // 切换为蓝色虚线
    pen.setColor(Qt::blue);
    pen.setWidth(3);
    pen.setStyle(Qt::DashLine);
    painter.setPen(pen);

    // 用此画笔绘制椭圆
    painter.drawEllipse(QPoint(250, 200), 80, 80);
}

9.6 画刷 —— QBrush

QBrush 控制图形内部填充的绘制效果。

核心 API:

void QPen::setBrush(const QBrush &brush);
void QPainter::setBrush(const QBrush &brush);

Qt::BrushStyle 常用样式:

样式说明
Qt::NoBrush不填充(透明)
Qt::SolidPattern纯色填充
Qt::HorPattern水平线条填充
Qt::VerPattern垂直线条填充
Qt::CrossPattern十字交叉线填充
Qt::BDiagPattern斜线填充
Qt::Dense1Pattern ~ Qt::Dense7Pattern不同密度的点阵填充

示例:

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);

    // ===== 设置画笔 =====
    QPen pen(Qt::black);
    pen.setWidth(3);
    painter.setPen(pen);

    // ===== 设置画刷 =====
    QBrush brush(Qt::green, Qt::SolidPattern);
    painter.setBrush(brush);

    // 绘制矩形:黑色边框 + 绿色填充
    painter.drawRect(50, 50, 200, 150);

    // 切换填充为十字交叉线
    brush.setStyle(Qt::CrossPattern);
    brush.setColor(Qt::yellow);
    painter.setBrush(brush);

    // 绘制椭圆:黑色边框 + 黄色十字交叉线填充
    painter.drawEllipse(QPoint(300, 200), 100, 80);
}

QPen 也有 setBrush():这意味着你可以创建渐变线条或纹理线条,而不仅仅是纯色线条。


十、绘图设备与资源加载

10.1 加载图片资源

将图片添加到 resource.qrc 后,即可在 paintEvent() 中使用:

步骤:

  1. 准备图片文件(如 image.png)
  2. 在 Qt Creator 中双击打开 resource.qrc
  3. 点击"添加" → “添加前缀”(如 /)
  4. 点击"添加" → “添加文件”,选择图片文件
  5. 在代码中通过 QPixmap 加载:
// 从资源文件加载
QPixmap pixmap(":/image.png");

// 或从磁盘文件加载
QPixmap pixmap("C:/Users/Pictures/image.png");

10.2 坐标平移 —— translate()

translate() 用于平移坐标系原点,之后所有绘制操作都将基于新的原点进行。

void QPainter::translate(const QPointF &offset);
void QPainter::translate(qreal dx, qreal dy);

示例:

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);

    // 原始坐标下画红线
    painter.setPen(Qt::red);
    painter.drawLine(0, 0, 100, 0);

    // 平移坐标系:X 右移 50px,Y 下移 50px
    painter.translate(50, 50);

    // 在新的坐标下画蓝线(实际从 (50,50) 画到 (150,50))
    painter.setPen(Qt::blue);
    painter.drawLine(0, 0, 100, 0);
}

坐标平移的意义:通过变换坐标系,可以将复杂的几何计算简化为基于原点的简单运算。例如绘制一个"车"的图案,可以先在原点附近绘制车身,然后 translate() 到另一个位置画车轮。

10.3 绘制图片 —— drawPixmap()

void QPainter::drawPixmap(const QRect &targetRect, const QPixmap &pixmap);
void QPainter::drawPixmap(int x, int y, const QPixmap &pixmap);

示例:

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);

    // 加载图片
    QPixmap pixmap(":/image.png");

    // 在 (50, 50) 处以原始尺寸绘制图片
    painter.drawPixmap(50, 50, pixmap);

    // 在 (50, 300) 处以 200×200 区域绘制(图片会被缩放)
    painter.drawPixmap(QRect(50, 300, 200, 200), pixmap);
}

10.4 旋转 —— rotate()

void QPainter::rotate(qreal angle);  // angle 单位为度

重要:rotate() 默认以坐标原点 (0, 0) 为旋转中心。通常需要先用 translate() 将旋转中心移到目标位置,然后 rotate(),最后在原点附近绘制。

示例:

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);

    // 1. 将坐标系原点平移到旋转中心
    painter.translate(200, 200);

    // 2. 旋转 45 度
    painter.rotate(45);

    // 3. 在新坐标系中以 (0, 0) 为中心绘制
    painter.setPen(Qt::red);
    painter.drawLine(0, 0, 100, 0);   // 实际上是从 (200,200) 出发的 45° 斜线

    // 再旋转 45 度(累计 90 度),绘制绿线
    painter.rotate(45);
    painter.setPen(Qt::green);
    painter.drawLine(0, 0, 100, 0);   // 实际上是从 (200,200) 出发的 90° 垂直线
}

十一、绘图状态管理

11.1 坐标系变换详解

变换的累积效应:translate() 和 rotate() 的调用会产生累积效应(每次变换都是在上一次变换后的坐标系基础上进行)。

示例:连续平移画图

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);

    // 第一次平移
    painter.translate(100, 50);
    painter.drawRect(0, 0, 50, 50);  // 实际绘制在 (100, 50)

    // 第二次平移(在第一次基础上再平移)
    painter.translate(100, 0);
    painter.drawRect(0, 0, 50, 50);  // 实际绘制在 (200, 50)
}

11.2 save() 与 restore()

由于变换会累积,多次变换后很难手动回退到之前的状态。Qt 提供了 save() 和 restore() 方法来保存和恢复完整的绘图状态(包括坐标变换、画笔、画刷、字体等所有设置)。

void QPainter::save();     // 将当前绘图状态压栈保存
void QPainter::restore();  // 从栈顶弹出最近一次保存的状态并恢复

工作原理:save() 和 restore() 采用了**栈(Stack)**的数据结构,遵循后进先出(LIFO)原则。

save()     →  将当前状态压入栈顶
restore()  →  从栈顶弹出状态并恢复到 QPainter

完整示例:

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);

    // 设置原始画笔
    painter.setPen(QPen(Qt::black, 2));
    painter.drawRect(10, 10, 80, 80);  // 黑色矩形

    // ===== 保存当前状态 =====
    painter.save();

    // 变换 + 改画笔绘制
    painter.translate(100, 0);
    painter.setPen(QPen(Qt::red, 5));
    painter.drawRect(10, 10, 80, 80);  // 红色矩形,在 (110, 10)

    // ===== 恢复状态 =====
    painter.restore();
    // 此时:坐标恢复为原始,画笔恢复为黑色 2px

    // 继续在原始坐标下绘制
    painter.drawRect(10, 110, 80, 80);  // 黑色矩形,在 (10, 110)
}

典型应用模式:在绘制复杂图形时,每绘制一个独立组件前都先 save(),绘制完成后 restore(),确保各组件的绘制逻辑互不干扰。


十二、三大绘图设备对比

Qt 提供了三种主要的 QPaintDevice 子类,各有不同的用途:

特性QPixmapQImageQPicture
定位显示优化、平台相关像素级操作、平台无关绘图指令的记录与回放
像素访问较慢,不推荐频繁操作像素快,支持直接 setPixel() / pixel()不支持(不存储像素数据)
显示性能高(经过硬件加速优化)较低(需要转换)不直接显示(回放渲染)
I/O 支持支持 save() / load()支持 save() / load()支持 save() / load()
透明度支持 Alpha 通道支持 Alpha 通道不适用
坐标系变换支持通过 QPainter支持通过 QPainter支持通过 QPainter
典型应用作为按钮、标签上的图标/图片显示图像处理(滤镜、像素修改)、截图矢量图形记录、画板回放

12.1 QPixmap

QPixmap 是为屏幕显示而优化的绘图设备:

// 创建 QPixmap 并在上面绘图
QPixmap pixmap(400, 300);
QPainter painter(&pixmap);
painter.drawRect(50, 50, 100, 100);
painter.end();

// 将 QPixmap 设置到 QLabel 上显示
ui->label->setPixmap(pixmap);

优势:显示速度快(尤其在 Windows 上通过 DirectX/OpenGL 加速)
劣势:像素访问慢,不适合逐像素修改

12.2 QImage

QImage 是为像素级图像处理而设计的设备:

核心方法:

void QImage::setPixel(const QPoint &position, QRgb value);
QRgb QImage::pixel(const QPoint &position) const;

完整示例:逐像素创建一张渐变色图片

void Widget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);

    // 创建一个 400×300 的 QImage
    QImage image(400, 300, QImage::Format_ARGB32);

    // 逐像素设置颜色:从左到右渐变
    for (int x = 0; x < image.width(); x++) {
        for (int y = 0; y < image.height(); y++) {
            // 根据 X 坐标计算红绿蓝分量
            int r = x * 255 / image.width();
            int g = y * 255 / image.height();
            int b = (x + y) * 255 / (image.width() + image.height());

            image.setPixel(x, y, qRgb(r, g, b));
        }
    }

    // 将 QImage 绘制到 Widget 上
    painter.drawImage(0, 0, image);
}

qRgb(r, g, b):将 RGB 三个分量(0-255)组合为一个颜色值,用于 setPixel()。

优势:像素级操作高效,跨平台一致
劣势:直接显示比 QPixmap 慢(需要格式转换),通常先操作完再转为 QPixmap 显示

12.3 QPicture

QPicture 是一种独特的绘图设备——它不存储像素,而是记录 QPainter 发出的每一条绘图指令。之后可以通过 QPainter 回放这些指令,重新渲染出画面。

核心用法:

// ===== 录制绘图指令 =====
QPicture picture;
QPainter recorder;
recorder.begin(&picture);            // 开始录制

recorder.setPen(Qt::red);
recorder.drawLine(0, 0, 100, 100);
recorder.drawRect(50, 50, 200, 100);
recorder.drawEllipse(QPoint(150, 150), 80, 60);

recorder.end();                      // 结束录制

// 保存录制结果
picture.save("drawing.pic");

// ===== 回放绘图指令 =====
QPicture pic2;
pic2.load("drawing.pic");

QPainter player(this);
player.drawPicture(0, 0, pic2);      // 在新画布上回放

QPicture 的类比理解:

  • 就像游戏录像(Replay):一场魔兽争霸 3 的比赛可能长达 60 分钟,但录像文件只有几 KB —— 因为它记录的不是画面帧,而是操作指令(选中单位、移动坐标、释放技能…)
  • QPicture 完全一样:它记录的是 drawLine()、drawRect() 等函数调用和参数,而不是生成位图

适用场景:

  • 矢量图形编辑器的保存/加载
  • 画板回放功能
  • 需要无损缩放的图形存储

begin() / end():当 QPainter 需要绑定到 QPicture 等非 QWidget 设备时,必须使用 begin(&device) 和 end() 配对。在 paintEvent() 中直接用 QPainter painter(widget) 构造的 QPainter 则会自动处理绑定。


总结

本章详细讲解了 Qt 界面优化的两大核心领域,知识体系回顾如下:

QSS 样式表核心要点

主题关键内容
语法选择器 { 属性: 值; }
应用方式控件自身 setStyleSheet / 全局 setStyleSheet / QSS 文件加载 / Qt Designer 设置
选择器通配、类型、类、ID、后代、子、并集、属性选择器 + 子控件(:😃 + 伪状态(😃
盒模型Content → Padding → Border → Margin 四层结构
实战按钮、CheckBox、RadioButton、LineEdit、ListView、菜单栏、登录界面共 7 个案例

2D 绘图核心要点

主题关键类/方法
绘图框架QPainter(画家)+ QPaintDevice(画布)+ QPen(画笔)+ QBrush(画刷)
基本图形drawLine、drawRect、drawEllipse、drawText + setFont
画笔设置QPen: setWidth、setStyle(6种线型)、setBrush
画刷设置QBrush: setStyle(12+种填充)、setColor
坐标变换translate(平移)、rotate(旋转),变换会累积
状态管理save()/restore() 栈式状态保存与恢复
绘图设备QPixmap(显示优化)、QImage(像素操作+setPixel)、QPicture(指令记录与回放)

学习建议:

  1. QSS 优先于 C++:能用 QSS 实现的样式效果,不要用 C++ 硬编码。这不仅简化代码,还能利用 QSS 伪状态的自动切换能力。

  2. 善用 QSS 选择器优先级:全局样式写大框架,ID 选择器精准覆盖特殊控件,避免到处写重复的 QSS。

  3. QPainter 的坐标变换是绘图中最容易出错的地方。记住每次变换都是累积的,不确定时就在操作前后加 save()/restore() 配对。

  4. QPixmap vs QImage 的选择:需要显示 → QPixmap;需要操作像素 → QImage;需要记录绘图过程 → QPicture。

  5. 建议动手完成 6.7 节的登录界面综合案例——它融合了 QSS 选择器、背景图片、输入框美化、按钮交互状态等多种技术,是检验 QSS 掌握程度的绝佳练习。


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐