Qt界面优化详解
本文系统介绍了Qt界面优化的两大核心技术:QSS样式表和2D绘图系统。QSS部分详细讲解了其语法规则、应用方式(单控件/全局/文件加载/设计器设置)、选择器系统、盒模型、伪状态与子控件等核心概念,强调其借鉴CSS实现界面与逻辑分离的设计思想。2D绘图部分则重点解析QPainter绘图API、坐标变换和绘图设备等关键技术。全文旨在帮助开发者全面掌握Qt界面美化与自定义绘制能力,提升GUI应用的视觉表
第六章 界面优化 —— 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):要设置的样式属性名称,如
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:
- 选中目标控件(或空白区域选中 Widget)
- 右键 → “改变样式表…”
- 在弹出的编辑器中编写 QSS 代码,点击 OK 确认
- 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 内部事件驱动。如果我们自定义了某个控件的事件处理,可以模拟伪状态效果。以下示例通过重写 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);
}
关键点解析:
| 知识点 | 说明 |
|---|---|
::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-imagevsbackground-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: outsetvsinset: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 风格等)
- 遇到样式不生效时:
- 检查选择器是否正确(控件类型、objectName)
- 检查是否存在优先级更高的规则覆盖
- 对于复杂控件(QComboBox、QSpinBox),内层子控件可能需要单独设置
- 某些属性(如
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 ¢er, 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() 中使用:
步骤:
- 准备图片文件(如
image.png) - 在 Qt Creator 中双击打开
resource.qrc - 点击"添加" → “添加前缀”(如
/) - 点击"添加" → “添加文件”,选择图片文件
- 在代码中通过
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(指令记录与回放) |
学习建议:
-
QSS 优先于 C++:能用 QSS 实现的样式效果,不要用 C++ 硬编码。这不仅简化代码,还能利用 QSS 伪状态的自动切换能力。
-
善用 QSS 选择器优先级:全局样式写大框架,ID 选择器精准覆盖特殊控件,避免到处写重复的 QSS。
-
QPainter 的坐标变换是绘图中最容易出错的地方。记住每次变换都是累积的,不确定时就在操作前后加
save()/restore()配对。 -
QPixmap vs QImage 的选择:需要显示 → QPixmap;需要操作像素 → QImage;需要记录绘图过程 → QPicture。
-
建议动手完成 6.7 节的登录界面综合案例——它融合了 QSS 选择器、背景图片、输入框美化、按钮交互状态等多种技术,是检验 QSS 掌握程度的绝佳练习。
更多推荐


所有评论(0)