第六章 界面优化 —— 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 的关系

维度 CSS QSS
应用领域 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):要设置的样式属性名称,如 colorfont-size
  • 值(Value):属性的取值,如 red20px
  • 声明块:用花括号 { } 包裹,多条声明用分号 ; 分隔

基础示例

/* 将所有 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 的直接子控件 B QDialog > 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-arrow QComboBox、QSpinBox 下拉箭头/增减箭头的图标
::indicator QCheckBox、QRadioButton 勾选框/单选按钮的指示器
::chunk QProgressBar 进度条的前进块
::item QMenuBar、QMenu、QListView 菜单栏/菜单/列表的单个项
::separator QMenu 菜单中的分隔线

示例 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 内部事件驱动。如果我们自定义了某个控件的事件处理,可以模拟伪状态效果。以下示例通过重写 mousePressEventmouseReleaseEvententerEventleaveEvent 来实现更灵活的样式切换:

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);
}

关键点解析

知识点 说明
::indicator QCheckBox 的子控件——那个小方框
: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 insetoutset 模拟按钮凸起的效果,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 子类,各有不同的用途:

特性 QPixmap QImage QPicture
定位 显示优化、平台相关 像素级操作、平台无关 绘图指令的记录与回放
像素访问 较慢,不推荐频繁操作像素 快,支持直接 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社区

更多推荐