Qt 常用控件精讲:QWidget 核心属性全解 ------ 从 geometry 到 qrc 与 QSS
写在前面
上一篇我们搭好了 Qt 的开发环境,也跑通了第一个窗口程序。但那个窗口是空的------除了标题栏什么也没有。真正要做出一个能用的图形界面,我们必须往窗口里放东西:按钮、输入框、标签、下拉框......这些"放进去的东西",Qt 里统称为控件。
我在这一篇里要解决的,就是三件事:
- 控件到底是什么,Qt 为什么要把界面元素抽象成控件,以及控件体系是怎么一步步演化成今天这个样子的;
- 所有控件的公共基类
QWidget到底给了我们什么------十来个最关键、最常用的属性,每一个都配一个能跑起来的小工程; - 那些看起来简单、实际很容易踩坑的细节:为什么只改坐标会把按钮拉变形、为什么窗口框架会让同一套坐标 API 算出两组不同的数字、为什么浮点数算出来的不透明度总差那么一点点、为什么样式写错了程序一声不响。
我给自己定的标准是:每一个属性都要讲清"是什么 → 为什么这样设计 → 怎么用 → 容易错在哪里"这四步,代码一行一行跟下来,不跳步。
知识框架总览
先把这一篇要覆盖的内容整体摆出来。下面这张图从"概念"走到"资源与样式",编号和我后面的章节顺序完全对应,复习的时候可以直接拿它当索引。

图里有三种颜色:蓝色是纯概念,绿色是动手写代码的部分,橙色是必须记住的易错点。底部那条橙色横条列了四个最典型的坑------术语拼写、坐标与宽高、浮点精度、资源路径,它们会分散在后面的章节里各自展开。
目录
文章目录
- [Qt 常用控件精讲:QWidget 核心属性全解 ------ 从 geometry 到 qrc 与 QSS](#Qt 常用控件精讲:QWidget 核心属性全解 —— 从 geometry 到 qrc 与 QSS)
-
- 写在前面
- 知识框架总览
- 目录
- [1. 控件到底是什么](#1. 控件到底是什么)
-
- [1.1 先看没有控件的年代](#1.1 先看没有控件的年代)
- [1.2 控件:一次封装,到处复用](#1.2 控件:一次封装,到处复用)
- [2. 控件体系的三代演进](#2. 控件体系的三代演进)
-
- [2.1 Qt 的控件处在什么位置](#2.1 Qt 的控件处在什么位置)
- [3. QWidget:所有控件的公共基类](#3. QWidget:所有控件的公共基类)
-
- [3.1 为什么先讲它](#3.1 为什么先讲它)
- [3.2 从哪里能看到这些属性](#3.2 从哪里能看到这些属性)
- [3.3 顺带一提:无障碍属性](#3.3 顺带一提:无障碍属性)
- [4. enabled:控件的可用与禁用](#4. enabled:控件的可用与禁用)
-
- [4.1 它描述什么](#4.1 它描述什么)
- [4.2 相关 API](#4.2 相关 API)
- [4.3 第一个小工程:直接把按钮禁掉](#4.3 第一个小工程:直接把按钮禁掉)
- [4.4 第二个小工程:运行中来回切换](#4.4 第二个小工程:运行中来回切换)
-
- 代码导航
- 关于"转到槽"和函数命名
- [关于 clicked 信号的两个版本](#关于 clicked 信号的两个版本)
- 运行现象
- [5. geometry:控件的位置与尺寸](#5. geometry:控件的位置与尺寸)
- [6. window frame:两套坐标原点](#6. window frame:两套坐标原点)
-
- [6.1 窗口框架是操作系统给的](#6.1 窗口框架是操作系统给的)
- [6.2 于是就有了两套坐标原点](#6.2 于是就有了两套坐标原点)
- [6.3 小工程:亲手量出两套坐标的差距](#6.3 小工程:亲手量出两套坐标的差距)
- [7. windowTitle:窗口标题](#7. windowTitle:窗口标题)
-
- [7.1 基本用法](#7.1 基本用法)
- [7.2 一个容易踩的坑:它只对顶层窗口有效](#7.2 一个容易踩的坑:它只对顶层窗口有效)
- [7.3 关于"没有报错"这件事](#7.3 关于"没有报错"这件事)
- [8. windowIcon 与 qrc 资源机制](#8. windowIcon 与 qrc 资源机制)
-
- [8.1 windowIcon:窗口图标](#8.1 windowIcon:窗口图标)
- [8.2 第一版:用绝对路径加载图片](#8.2 第一版:用绝对路径加载图片)
- [8.3 绝对路径为什么不行](#8.3 绝对路径为什么不行)
- [8.4 第二版:相对路径](#8.4 第二版:相对路径)
- [8.5 第三版:qrc 资源机制](#8.5 第三版:qrc 资源机制)
- [8.6 工程代码](#8.6 工程代码)
- [9. windowOpacity:窗口不透明度与浮点精度](#9. windowOpacity:窗口不透明度与浮点精度)
-
- [9.1 基本用法](#9.1 基本用法)
- [9.2 工程代码](#9.2 工程代码)
- [9.3 两个值得深挖的问题](#9.3 两个值得深挖的问题)
- [10. cursor:鼠标光标](#10. cursor:鼠标光标)
- [11. font:字体](#11. font:字体)
- [12. toolTip:悬停提示](#12. toolTip:悬停提示)
- [13. focusPolicy:焦点策略](#13. focusPolicy:焦点策略)
- [14. styleSheet:样式表与颜色表示](#14. styleSheet:样式表与颜色表示)
-
- [14.1 从"样式"说起](#14.1 从"样式"说起)
- [14.2 在界面设计器里写样式](#14.2 在界面设计器里写样式)
- [14.3 用代码写样式:日夜模式切换](#14.3 用代码写样式:日夜模式切换)
- [14.4 计算机里的颜色是怎么表示的](#14.4 计算机里的颜色是怎么表示的)
- [14.5 用取色器查颜色](#14.5 用取色器查颜色)
- [14.6 两件要注意的事](#14.6 两件要注意的事)
- [14.7 关于颜色的一个补充](#14.7 关于颜色的一个补充)
- [15. 边界、限制与常见误区](#15. 边界、限制与常见误区)
- [16. 总结](#16. 总结)
下面这份文字列出了本篇的全部章节,顺序就是我学习和拆解的顺序:
-
- 控件到底是什么
-
- 控件体系的三代演进
-
- QWidget:所有控件的公共基类
-
- enabled:控件的可用与禁用
-
- geometry:控件的位置与尺寸
-
- window frame:两套坐标原点
-
- windowTitle:窗口标题
-
- windowIcon 与 qrc 资源机制
-
- windowOpacity:窗口不透明度与浮点精度
-
- cursor:鼠标光标
-
- font:字体
-
- toolTip:悬停提示
-
- focusPolicy:焦点策略
-
- styleSheet:样式表与颜色表示
-
- 边界、限制与常见误区
-
- 总结
1. 控件到底是什么
1.1 先看没有控件的年代
要理解控件为什么会出现,最好的办法是先看它出现之前是什么样。
在最早期的图形界面开发里,根本没有"控件"这个概念 。那时候你要在屏幕上显示一个按钮,就真的得自己把它画出来。逻辑是这样的:显示器在操作系统眼里就是一块画布,操作系统提供一批绘图 API,你在这块画布上画点、画线、画矩形、画三角形,或者往某个区域里填颜色。
我顺着这个思路想一遍"画一个窗口"要几步:
- 先画一个矩形,这是窗口主体;
- 在它上面再叠一个矩形,这是标题栏;
- 在标题栏左边画一个小图标,右边画三个小方块当最小化、最大化、关闭按钮;
- 关闭按钮里再画两条斜线,这才像个叉;
- 然后在窗口内部再画几个矩形当按钮,画几个框当输入框。
一个完整的图形界面,就是这么一条一条画出来的。

上面这台文曲星电子词典就是那个年代的产品。它有一个叫 LAVA 的开发平台,很多开发者基于它写过不少程序甚至游戏。它的屏幕非常简单,程序能做的也就是"在屏幕上绘制文字、绘制形状,再基于形状做交互"。
这种开发方式的问题很明显:低效、重复、易错。你要画十个按钮,就得写十遍画按钮的代码;按钮换个颜色,所有地方都要跟着改。
1.2 控件:一次封装,到处复用
有了上面的背景,"控件"这个概念就很好理解了。
控件的英文原词是 Widget ,字面意思是"小部件、小组件"。Qt 里这个类就叫 QWidget。它不指某一个具体的东西,而是界面上各种元素的统称:
- 一个按钮是一个控件;
- 一个输入框是一个控件;
- 一行文字标签是一个控件;
- 甚至连最外层的窗口本身,也是一个控件。
顺带说一句发音:Widget 读作 /ˈwɪdʒɪt/,"维吉特",不要读成"维给特"。geometry 读作 /dʒiˈɒmətri/,重音在第二个音节。这类术语读错,在面试里很容易和面试官不在一个频道上------这一点我在第 5 节里还会细说。
控件的价值在于"站在巨人的肩膀上,而不是从头发明轮子"。 你要一个按钮,不需要自己去画矩形、处理鼠标按下抬起、处理高亮状态,直接用 Qt 已经写好的 QPushButton 就行。

打开 Qt Creator 任意一个工程的界面文件(.ui 文件),左侧那一长条就是 Qt 内置好的控件箱。上面这张图里标注出来的这些------按钮、单选框、树形视图、下拉框、列表视图、单行输入框、多行输入框、滚动条------全都可以直接拖到界面上使用。
所以这一篇的任务就很明确了:把这些控件(以及它们共同继承的属性)挑出最典型的一批,逐个说清怎么用。
2. 控件体系的三代演进
控件不是 Qt 发明的,它是整个 GUI 开发领域的通用概念。按成熟度,大致可以分成三个阶段。
第一阶段:完全没有控件。 就是上一节说的,全靠绘图 API 手工绘制。代码繁琐,几乎没有复用性,改一点点东西就要大改。
第二阶段:有了控件,但很简单、数量有限。 最典型的代表是早期的 HTML。HTML 里有很多标签,每个标签在浏览器里渲染出不同的控件效果:
<img>标签渲染成一张图片;<a>标签渲染成一个链接;<input>标签渲染成一个输入框;<button>标签渲染成一个按钮。
这种方式确实把"复用"这件事做起来了,但标签数量少、样式原始,做出来的界面在观感上很粗糙。
第三阶段:完整的控件体系。 控件数量足、覆盖场景广,而且每个控件都提供多种样式供你选。早期的 MFC、VB、C++ Builder、Qt、Delphi,后来的 Android SDK、JavaFX,以及前端的各种 UI 库,都属于这一阶段。

上面这张图截自 Element 这个前端界面框架的按钮页面。可以看到同一个"按钮",它有默认、主要、成功、信息、警告、危险这么多语义色,还有朴素、圆角、带图标等各种形态,甚至还有好看的交互动画。这就是成熟控件体系的样子。
2.1 Qt 的控件处在什么位置
这里要说一句实话:Qt 内置控件的默认外观,大概处在第二阶段和第三阶段之间。 控件数量是够的,但颜值比上面这种现代化的控件体系要逊色一些。
不过这并不意味着 Qt 做不出好看的界面,因为 Qt 提供了别的办法:
- 样式表(QSS):Qt 参考了前端的 CSS,做出了自己的一套样式机制,可以通过类似 CSS 的写法去改控件的显示效果。这一篇的第 14 节会先用起来,后面还有专门章节细讲。
- QDesign Studio:Qt 近几年推出的界面设计工具,对标现代化的界面体系,用它能做出业界领先档次的界面。但它是收费的,而且价格不便宜,所以本篇不涉及。
还有一点值得说:颜值这件事,其实主要不是程序员的责任。 正规公司里会有专门的美工(也叫设计、UED),他们负责决定用什么字体、多大字号、什么颜色、要不要加粗。程序员拿着设计稿去实现就行。小公司可能没有美工,那就参考别人做好的程序或网页------第 11 节我会讲怎么用浏览器的开发者工具去"抄"别人的字体设置。
3. QWidget:所有控件的公共基类
3.1 为什么先讲它
Qt 里所有控件都继承自 QWidget。按钮是 QPushButton,输入框是 QLineEdit,标签是 QLabel------它们的共同父类都是 QWidget。
这意味着一件很重要的事:QWidget 里的属性,是所有控件共享的通用部分。 我把 QWidget 弄明白了,再去学具体控件时,那些继承下来的能力就不用重新学了。
3.2 从哪里能看到这些属性
在 Qt Creator 里选中任意一个控件,右侧属性编辑器就会列出它当前可调的所有属性,而且可以直接在这里改,改完界面立刻变。

上面这张图里能看到的这些,就是接下来要逐个讲的东西:objectName、enabled、geometry、windowTitle、cursor、focusPolicy、toolTip、styleSheet......
但我不需要把每一个属性都记住。 这就好比给你一本词典,你不需要把里面的词全背下来,用到的时候再查。这一篇只挑最常用、最重要的一批讲透,剩下的用到时去查官方文档即可。
3.3 顺带一提:无障碍属性
属性列表里有两个平时基本用不到、但值得知道的属性:accessibleName 和 accessibleDescription。
它们服务于辅助技术 ,最典型的是屏幕阅读器。屏幕阅读器能把屏幕上的内容读出来,视障用户就是靠它使用电脑和手机的。Windows 系统里自带的屏幕阅读器叫"讲述人"。
也就是说,如果你给按钮设置了 accessibleName,视障用户用屏幕阅读器操作到那个按钮时,听到的就是你设置的这段文字。正常人用不到这两个属性,但对视障用户来说是刚需。
顺带说一个可能超出直觉的事实:视障人士不但能用电脑,还能写代码、当程序员。B 站上就有讲盲人程序员的视频,两位出镜的盲人敲代码非常熟练,和普通程序员没什么区别。这件事对我们的意义是------连看不见的人都能把编程做下来,我们遇到的问题基本都只是"还没弄懂",不是"做不到"。
4. enabled:控件的可用与禁用
4.1 它描述什么
enabled 描述一个控件当前是否处于可用状态 。和它相对的概念是禁用。
禁用的含义有三层,必须一起记住:
- 控件不再接受任何用户输入事件。鼠标点上去没反应,键盘怎么按也没用;
- 外观上通常显示为灰色,让人一眼看出"这里点不了";
- 如果一个控件被禁用,它的子控件也会一起被禁用。
第 3 点尤其重要。Qt 里的控件是以对象树的方式组织的,每个控件可以有一个父控件,也可以有若干子控件。把树上某个节点禁用,这个节点下面整棵子树都会被禁用------用一句话概括就是"一荣俱荣,一损俱损"。

4.2 相关 API
这一节涉及两个方法:
isEnabled():获取控件当前的可用状态。返回值是bool类型(布尔类型,取值只有true和false两种)。true表示可用,false表示禁用。setEnabled(bool):设置控件的可用状态。参数传true表示可用,传false表示禁用。
方法的命名很直白:is 开头的是查询,set 开头的是设置。这个规则在 Qt 里是通用的。
4.3 第一个小工程:直接把按钮禁掉
先写一个最简版本:在构造函数里创建一个按钮,然后把它设成禁用。
文件名:QWidget_1.pro
qmake
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
widget.cpp
HEADERS += \
widget.h
FORMS += \
widget.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
文件名:main.cpp
cpp
#include "widget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
文件名:widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void handle();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton* button = new QPushButton(this);
button->setText("按钮");
// 按钮就处于禁用状态了.
button->setEnabled(false);
connect(button, &QPushButton::clicked, this, &Widget::handle);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handle()
{
qDebug() << "handle";
}
文件名:widget.ui
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
</widget>
<resources/>
<connections/>
</ui>
代码导航
先看 .pro 文件。 这是 Qt 的工程描述文件,qmake 这个构建工具读它来决定怎么编译整个工程:
QT += core gui:声明本工程依赖 Qt 的core(核心非界面模块)和gui(底层绘图模块);greaterThan(QT_MAJOR_VERSION, 4): QT += widgets:如果 Qt 主版本号大于 4,就再追加widgets模块。这是因为 Qt 4 里控件类和gui是在一起的,从 Qt 5 开始控件被拆到了独立的widgets模块,这行写法让同一份工程文件在新旧版本下都能用;CONFIG += c++11:启用 C++11 标准;DEFINES += QT_DEPRECATED_WARNINGS:让编译器对"已废弃的 API"给出警告;SOURCES += ...、HEADERS += ...、FORMS += ...:分别列出源文件、头文件、界面文件。每行末尾的反斜杠\表示"下一行接续本行"。
再看 main.cpp,这是整个程序的入口。 它只有四步:
QApplication a(argc, argv);创建应用程序对象。它是 Qt 程序的"总管",负责消息循环、事件分发等。argc和argv就是main函数收到的命令行参数,原样转交给它;Widget w;在栈上 创建我们自己的窗口对象。Widget就是widget.h里定义的那个类;w.show();让窗口显示出来。注意这一句不能少------只创建对象不加这一句,窗口不会出现在屏幕上;return a.exec();进入事件循环。exec()会一直阻塞在这里,不断接收并分发鼠标、键盘、绘制等消息,直到窗口被关闭才返回。
最后看 widget.cpp 的构造函数。 构造函数是对象被创建时自动执行的函数,适合放"程序一启动就要生效"的设置:
ui->setupUi(this);这一句把.ui文件描述出来的界面搭到当前窗口上。ui是一个指向Ui::Widget对象的指针,Ui::Widget是 Qt 的设计工具根据widget.ui自动生成的类;QPushButton* button = new QPushButton(this);在堆上 创建一个按钮对象,返回值是指针。构造函数参数this表示"这个按钮的父控件是当前窗口"。传父控件有双重好处:按钮会被自动加到窗口上并显示;窗口销毁时会自动连同按钮一起销毁,不用我们手动delete;button->setText("按钮");设置按钮上显示的文字;button->setEnabled(false);把按钮设为禁用;connect(button, &QPushButton::clicked, this, &Widget::handle);把按钮的clicked信号连到当前窗口的handle槽函数上。
关于 connect,它有四个参数,顺序是"谁发信号 → 发什么信号 → 谁来接 → 怎么接":
- 第 1 个
button:信号的发送者; - 第 2 个
&QPushButton::clicked:发送哪个信号。&类名::成员名是 C++ 里"取成员地址"的写法,这里的含义是"按钮被点击时发出的那个信号"。clicked这个信号在按钮被"按下再松开"(一次完整的点击)时发出; - 第 3 个
this:接收者,也就是当前窗口对象; - 第 4 个
&Widget::handle:收到信号后执行哪个函数。handle是我们自己在widget.h里声明、在widget.cpp里定义的普通成员函数。
QPushButton 和 QDebug 这两个头文件是显式包含进来的:QPushButton 用于创建按钮,QDebug 用于 qDebug() 打印。
运行现象
编译运行后,窗口里会出现一个灰色的按钮。这时候点它,没有任何反应 ------哪怕 connect 已经把槽函数连上了,槽函数也不会执行。
这就直接验证了禁用最核心的语义:禁用和有没有连接槽函数没有关系,只要处于禁用状态,控件就不接收输入。
4.4 第二个小工程:运行中来回切换
上面那种写法是"一开机就禁用",属于静态设置。更常见的是在程序运行过程中切换。做法是再加一个按钮,点它就切换第一个按钮的禁用状态。
工程文件 QWidget_2.pro 和 main.cpp 与上一个逐字相同,不再重复粘贴。
文件名:widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_clicked();
void on_pushButton_enable_clicked();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_clicked()
{
qDebug() << "执行了槽函数";
}
void Widget::on_pushButton_enable_clicked()
{
// 切换第一个按钮的禁用状态.
// 1. 先获取到第一个按钮当前的可用状态
bool enable = ui->pushButton->isEnabled();
if (enable) {
ui->pushButton->setEnabled(false);
} else {
ui->pushButton->setEnabled(true);
}
}
文件名:widget.ui
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
<widget class="QPushButton" name="pushButton">
<property name="geometry">
<rect>
<x>310</x>
<y>140</y>
<width>241</width>
<height>61</height>
</rect>
</property>
<property name="text">
<string>按钮</string>
</property>
</widget>
<widget class="QPushButton" name="pushButton_enable">
<property name="geometry">
<rect>
<x>310</x>
<y>300</y>
<width>241</width>
<height>61</height>
</rect>
</property>
<property name="text">
<string>切换可用状态</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>
代码导航
先看 widget.ui。 这个文件是 Qt Creator 的界面设计器生成的 XML 描述,正文里出现的两个按钮都在这里:
- 第一个按钮的
name是pushButton,文字是"按钮"; - 第二个按钮的
name是pushButton_enable,文字是"切换可用状态"。
<widget class="QPushButton" name="..."> 里的 name 属性就是后面要讲的 objectName 。<property name="geometry"> 里的 <rect> 记录控件的位置和尺寸,<property name="text"> 里的 <string> 记录显示文字。
objectName 是这一节最关键的细节,必须单独讲。
在同一个界面里,不同控件的 objectName 必须不同 。原因在于 Qt 的元编程 机制:Qt 的设计工具会读取 widget.ui,自动生成一个头文件 ui_widget.h,里面把界面上每个控件都创建好,并且用它们各自的 objectName 作为成员变量名。所以我们才能在代码里写:
ui->pushButton拿到第一个按钮对象;ui->pushButton_enable拿到第二个按钮对象。
界面上创建的控件终究要在代码里参与逻辑,而代码要操作某个控件,第一步就得先拿到它------这就是 objectName 存在的意义。
在属性编辑器里拖控件时,Qt 会自动生成名字,规律是**"控件类型 + 下划线 + 序号"**:第一个按钮叫 pushButton,第二个叫 pushButton_2,第三个叫 pushButton_3,依次往后排。
⚠️ 易错点:别用自动生成的数字名字。 一个界面上控件一多,pushButton_2、pushButton_3 到底谁是谁根本分不清。正确做法是手动改成有描述性的名字,比如这一节用的 pushButton_enable,一眼就知道它是"切换可用状态"的那个按钮。
另外改属性的时候一定要先确认自己选中了哪个控件 。如果鼠标点到了空白处,右侧属性编辑器显示的就是整个窗口的属性,这时候改 objectName 改的是窗口而不是按钮。
再看 widget.h。 两个槽函数被放在 private slots: 下面:
private表示只能在类内部访问;slots是 Qt 特有的关键字,用来标记"这是槽函数"。槽函数本质上就是普通的成员函数,加上这个标记后,它才能被connect连接、被信号触发。
最后看 widget.cpp 里的两个槽函数。
on_pushButton_clicked() 只做一件事:qDebug() << "执行了槽函数";。qDebug() 是 Qt 提供的调试输出函数,<< 是流插入运算符,把右侧的内容送进输出流,最终打印到 Qt Creator 下方的应用程序输出窗口。
on_pushButton_enable_clicked() 是核心逻辑,三步:
bool enable = ui->pushButton->isEnabled();先把第一个按钮当前的可用状态读出来,存进一个bool变量;if (enable) { ui->pushButton->setEnabled(false); }如果当前是可用,就设成禁用;else { ui->pushButton->setEnabled(true); }否则(当前是禁用)就设回可用。
这样就实现了"来回切换":点一下变灰,再点一下恢复。
关于"转到槽"和函数命名
这两个槽函数名不是随便起的,而是 Qt Creator 的"转到槽"功能自动生成的,命名规则是 on_<objectName>_<信号名>:
on_pushButton_clicked=on+pushButton+clicked,意思是"pushButton的clicked信号触发的槽";on_pushButton_enable_clicked同理。
只要名字符合这个规则,Qt 在 setupUi() 里就会自动建立连接 ,不需要我们再手写 connect。这也是为什么 widget.cpp 的构造函数里只有一句 ui->setupUi(this); 就够用了。
关于 clicked 信号的两个版本
clicked 信号其实有两个重载版本 :一个不带参数,一个带一个 bool 参数。带参数的那个,bool 值表示"按钮当前是否处于被勾选的状态"。
对 QPushButton 来说,"是否被勾选"这件事是没有意义的,所以这里用无参数版本 即可。后面遇到像复选框那种本身就有"勾选/未勾选"两种状态的控件,才会用到带 bool 的版本。
运行现象
程序启动后,第一个按钮是可用状态,点它会打印"执行了槽函数"。点"切换可用状态",第一个按钮立刻变灰,再怎么点都没反应;再点一次,它又恢复正常。如此往复。
💡 提示: 记住一句话------"禁用就是变灰,变灰就是怎么点都没用"。判断一个控件的状态时,先看它是不是灰的。
5. geometry:控件的位置与尺寸
5.1 它是四个属性的统称
geometry 这个词的本意是"几何"。在 QWidget 里,它不是单一属性,而是四个属性的统称:
x:横坐标;y:纵坐标;width:宽度;height:高度。
x 和 y 合起来表示控件左上角那个点 在坐标系中的位置;width 和 height 表示控件的尺寸。位置 + 尺寸,合起来就叫 geometry。

5.2 坐标系长什么样
Qt 的坐标系有一个和数学课上不一样的地方,必须先说清楚。

- 原点是父控件的左上角 ,不是屏幕左上角,也不是窗口左上角。也就是说,控件的位置是相对于它的父控件来算的;
- X 轴向右增加;
- Y 轴向下增加------这一条和数学里的习惯相反,也是新手最容易记混的地方。
5.3 相关 API
geometry():获取控件的位置和尺寸,返回值是一个QRect对象。QRect就是"矩形",它内部包含x、y、width、height四份数据;setGeometry(const QRect&):传入一个完整的QRect,一次性把位置和尺寸都改掉;setGeometry(int x, int y, int w, int h):直接传四个分量,效果一样。两种写法随便挑,哪个方便用哪个;move(int x, int y):只改位置,不动尺寸。
💡 提示: QRect 和后面会遇到的 QPoint(表示一个点)都属于"小对象"------成员少、占空间小。C++ 里传参数时,大对象通常要传指针或引用以避免拷贝开销,但这种小对象直接用值传递就好,拷贝的代价可以忽略,代码还更直观。
5.4 小工程:用四个方向键挪动一个按钮
这一节要做一个"遥控器":界面上有一个 target 按钮,另外四个按钮分别是 up、down、left、right,点一下就让 target 朝对应方向移动 5 个像素。
这一节的工程叫 QWidget_3。它的工程文件 QWidget_3.pro 和 main.cpp 都与前面第 4 节那份逐字相同,不再重复粘贴,下面直接从界面和代码看起。
文件名:widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_up_clicked();
void on_pushButton_down_clicked();
void on_pushButton_left_clicked();
void on_pushButton_right_clicked();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
文件名:widget.ui
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
<widget class="QPushButton" name="pushButton_target">
<property name="geometry">
<rect>
<x>290</x>
<y>90</y>
<width>211</width>
<height>81</height>
</rect>
</property>
<property name="text">
<string>target</string>
</property>
</widget>
<widget class="QPushButton" name="pushButton_up">
<property name="geometry">
<rect>
<x>340</x>
<y>340</y>
<width>111</width>
<height>71</height>
</rect>
</property>
<property name="text">
<string>up</string>
</property>
</widget>
<widget class="QPushButton" name="pushButton_down">
<property name="geometry">
<rect>
<x>340</x>
<y>480</y>
<width>111</width>
<height>71</height>
</rect>
</property>
<property name="text">
<string>down</string>
</property>
</widget>
<widget class="QPushButton" name="pushButton_left">
<property name="geometry">
<rect>
<x>230</x>
<y>410</y>
<width>111</width>
<height>71</height>
</rect>
</property>
<property name="text">
<string>left</string>
</property>
</widget>
<widget class="QPushButton" name="pushButton_right">
<property name="geometry">
<rect>
<x>450</x>
<y>410</y>
<width>111</width>
<height>71</height>
</rect>
</property>
<property name="text">
<string>right</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>
widget.ui 里那五个 <widget class="QPushButton"> 就是界面上的五个按钮,它们的 name 分别是 pushButton_target、pushButton_up、pushButton_down、pushButton_left、pushButton_right。后面四个方向键的槽函数命名,就是照 on_<objectName>_clicked 规则生成的。
第一版:直接改 QRect 的分量
我一开始的想法很自然:既然目标按钮的几何信息是一个 QRect,那我把 QRect 的 y 减掉 5,再把它设回去,不就往上移了吗。
cpp
void Widget::on_pushButton_up_clicked()
{
// 获取到 target 本身的 geometry.
QRect rect = ui->pushButton_target->geometry();
qDebug() << rect;
// 第一版:改 QRect 的分量,再整体设回去
rect.setY(rect.y() - 5);
ui->pushButton_target->setGeometry(rect);
}
这一段有三个新东西:
rect.y():读取QRect当前的纵坐标。注意x、y、width、height在QRect里是私有成员 ,不能直接写rect.y,必须通过它提供的公开成员函数去读写;rect.setY(int):把QRect的纵坐标设成新值;ui->pushButton_target->setGeometry(rect):把改完的QRect重新设回按钮。
这里顺便厘清一个术语:成员函数 和方法是同一个东西,只是叫法不同。C++ 圈子里两种说法都有人用,Java 圈子里基本只叫"方法"。
运行起来点 up,按钮确实动了------但同时你也会发现,它变高了。多按几下,按钮被拉得越来越长。
为什么会变形
原因在于:QRect 描述的是一块矩形区域,它是"左上角坐标 + 宽高"这样一个整体。如果我只动左上角、不改宽高,那么这个矩形就是被"拉"了,而不是被"挪"了。
举两个方向看就很清楚:
- 把左上角往上移 5 像素,矩形的下边缘没动,于是高度增加了 5;
- 把左上角往左移 5 像素,矩形的右边缘没动,于是宽度增加了 5。

用一句话总结这张图:平移要保持宽高不变,只改坐标就会变成拉伸。
第二版:改成真正的平移
知道了问题所在,改法就明确了:X 和 Y 按方向加减,宽和高原样传回去。
所以这一节工程的最终形态用的是 setGeometry 的第二个重载版本------一次传四个分量。
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_up_clicked()
{
// 获取到 target 本身的 geometry.
QRect rect = ui->pushButton_target->geometry();
qDebug() << rect;
// rect.setY(rect.y() - 5);
// ui->pushButton_target->setGeometry(rect);
ui->pushButton_target->setGeometry(rect.x(), rect.y() - 5, rect.width(), rect.height());
}
void Widget::on_pushButton_down_clicked()
{
QRect rect = ui->pushButton_target->geometry();
qDebug() << rect;
// rect.setY(rect.y() + 5);
// ui->pushButton_target->setGeometry(rect);
ui->pushButton_target->setGeometry(rect.x(), rect.y() + 5, rect.width(), rect.height());
}
void Widget::on_pushButton_left_clicked()
{
QRect rect = ui->pushButton_target->geometry();
qDebug() << rect;
// rect.setX(rect.x() - 5);
// ui->pushButton_target->setGeometry(rect);
ui->pushButton_target->setGeometry(rect.x() - 5, rect.y(), rect.width(), rect.height());
}
void Widget::on_pushButton_right_clicked()
{
QRect rect = ui->pushButton_target->geometry();
qDebug() << rect;
// rect.setX(rect.x() + 5);
// ui->pushButton_target->setGeometry(rect);
ui->pushButton_target->setGeometry(rect.x() + 5, rect.y(), rect.width(), rect.height());
}
代码导航
四个槽函数的骨架完全一样,只有变化的那个分量和方向不同:
on_pushButton_up_clicked:y减 5,即往上移;on_pushButton_down_clicked:y加 5;on_pushButton_left_clicked:x减 5;on_pushButton_right_clicked:x加 5。
以 up 为例,执行顺序是:
QRect rect = ui->pushButton_target->geometry();取出目标按钮当前的矩形。这里rect是一个局部变量,出了函数就销毁;qDebug() << rect;把矩形打印出来观察。这一句很有用:Qt 内置的这些值类型对象基本都能被qDebug()直接打印 ,std::cout就不行。这也是我一直用qDebug()而不是std::cout的原因------它和 Qt 自己的类配合得更好;ui->pushButton_target->setGeometry(rect.x(), rect.y() - 5, rect.width(), rect.height());把新的四个分量交回去:x不变、y减 5、宽高不变。这一句执行完,按钮才真正在界面上移动。
顺便说一句:这份最终形态的源码里,第一版那两行代码并没有被删掉,而是以注释的形式留在原地------
cpp
// rect.setY(rect.y() - 5);
// ui->pushButton_target->setGeometry(rect);
保留被替换掉的旧写法是很有用的习惯:它能让人一眼看出这一处曾经怎么实现、后来为什么要改。上面正文里的"第一版"和"第二版",就是从这段代码的实际演进顺序推出来的。
关于包含头文件,这里有个实用原则值得记住:先把代码写出来,看编译器认不认得这个名字。认不得,再包含对应的头文件。 Qt 里很多类型(比如 QRect)已经被别的头文件间接包含了,不需要显式再包含。这一节用到的 qDebug() 需要 <QDebug>,因为在 widget.cpp 里显式写了这一句。
运行现象
运行后点 up,按钮整体往上走,尺寸不变 。在输出窗口里能看到 QRect(...) 的打印,前两个数字是 x 和 y,后两个是宽和高,单位都是像素。
把代码里的 5 改成更大的数字,移动速度就变快;写成方向相反的正负号,方向就反过来。这四个槽函数完全可以自己试着调一调。
5.5 进阶小工程:会逃跑的按钮
为了让 geometry 和随机数用得更熟一点,再加一个有点意思的练习:界面上有一个标签和一个"残忍拒绝"按钮,点这个按钮,它就随机跳到窗口的另一个位置。
这个练习放在 QWidget_4 工程里。它的 QWidget_4.pro 和 main.cpp 依旧与前面逐字相同,有改动的只有 widget.h、widget.cpp、widget.ui 三份。
文件名:widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_accept_clicked();
void on_pushButton_reject_clicked();
void on_pushButton_reject_pressed();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 设置随机种子. 使用时间戳作为随机种子.
srand(time(0));
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_accept_clicked()
{
ui->label->setText("女神女神, 快来嘴儿一个~, mua~");
}
void Widget::on_pushButton_reject_clicked()
{
// 这个就不要了.
}
void Widget::on_pushButton_reject_pressed()
{
// 如果女神点击了这个按钮, 就把这个按钮给挪走.
// 可以通过生成随机数的方式, 来确定按钮的新的位置.
// 获取到当前程序窗口的尺寸.
int width = this->geometry().width();
int height = this->geometry().height();
// 重新生成按钮的位置.
int x = rand() % width;
int y = rand() % height;
// 移动按钮的位置.
ui->pushButton_reject->move(x, y);
}
文件名:widget.ui
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>280</x>
<y>150</y>
<width>401</width>
<height>71</height>
</rect>
</property>
<property name="text">
<string>女神女神, 你做我女票好不好?</string>
</property>
</widget>
<widget class="QPushButton" name="pushButton_accept">
<property name="geometry">
<rect>
<x>170</x>
<y>350</y>
<width>171</width>
<height>61</height>
</rect>
</property>
<property name="text">
<string>欣然同意</string>
</property>
</widget>
<widget class="QPushButton" name="pushButton_reject">
<property name="geometry">
<rect>
<x>440</x>
<y>350</y>
<width>171</width>
<height>61</height>
</rect>
</property>
<property name="text">
<string>残忍拒绝</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>
代码导航
widget.ui 里有一个 QLabel 和两个 QPushButton:
QLabel直译是"标签",是一个只用来显示一段文本、不允许编辑 的控件。这一点和QLineEdit、QPlainTextEdit不同------后两者都能编辑。这里它的文字是"女神女神, 你做我女票好不好?";- 两个按钮的
objectName分别是pushButton_accept和pushButton_reject。
widget.h 里声明了三个槽函数,正好对应三种不同粒度的鼠标事件:clicked、pressed,以及被留下来的 clicked。
widget.cpp 里第一个要点是构造函数里多了一句:
cpp
// 设置随机种子. 使用时间戳作为随机种子.
srand(time(0));
rand()是 C 标准库里的函数,能生成一个随机的整数,取值范围很大。但它是伪随机------并不是数学意义上的真随机,而是通过一组数学变换算出来的一串"看起来没什么关联"的整数。既然是算出来的,那给它不同的起始参数,就会得到不同的序列;srand(unsigned)就是用来设置这个起始参数的,这个参数叫随机种子;time(0)返回当前的时间戳。时间戳是计算机表示时间的一种基本方式:以 1970 年 1 月 1 日 0 时 0 分 0 秒为基准时刻,算出当前时刻与基准时刻相差的秒数(也可以是毫秒、微秒),得到一个很大的整数。
把当前时间戳当种子,好处是每次运行程序种子都不同,得到的随机序列自然就不同。如果不设种子,每次运行拿到的序列完全一样------按钮每次都会跳到同一个位置,那就没意思了。
第二个要点是三个槽函数的分工:
on_pushButton_accept_clicked():调用ui->label->setText(...)把标签文字换掉。setText是修改文本的方法,和创建标签时在.ui里写死的初始文字是两回事------这是"运行中改内容";on_pushButton_reject_clicked():留空不写。因为它已经被更"灵敏"的pressed版本取代了;on_pushButton_reject_pressed():核心逻辑在这里。
pressed 槽函数分三步:
int width = this->geometry().width();和int height = this->geometry().height();先拿到当前窗口的宽和高。this指的就是当前这个窗口对象;int x = rand() % width;和int y = rand() % height;生成新的坐标。这里的%是取余运算符 :任何整数对width取余,结果一定落在0到width - 1之间。这一步是保证按钮不会跑到窗口外面去的关键 ------如果直接拿rand()的原始结果当坐标,那多半会跳到窗口外;ui->pushButton_reject->move(x, y);把按钮移到新位置。因为只是挪位置、不改尺寸,用move比用setGeometry更直接------用setGeometry还得先取出原来的宽高再传回去,麻烦。
关于三种鼠标信号
同样是"鼠标操作按钮",Qt 提供了好几个粒度不同的信号:
pressed:鼠标按下时触发,不需要松开;released:鼠标松开时触发;clicked:按下再松开(一次完整点击)才触发。
一开始用的是 clicked,必须"按一下再松开"按钮才会跑,手感偏慢。换成 pressed 之后,鼠标一按下去按钮立刻就跑了。
💡 提示: 想让反应更灵敏还可以继续做------只要鼠标移到 按钮上按钮就跑,连点都不用。这需要用到 Qt 的事件机制 (mouseMoveEvent),属于后面章节的内容,这里先把 pressed 用起来就够了。
运行现象
点"欣然同意",标签文字立刻变成"女神女神, 快来嘴儿一个~, mua~"。点"残忍拒绝",按钮随机跳到窗口内的新位置;换成 pressed 之后,鼠标按下去还没松手它就跑了。
⚠️ 提示: 这个程序拿来练 geometry、move、随机数都很好,但不要真的拿它去表白 。原因不是程序写得不好,而是"表白"这件事本身就不靠谱------表白是胜利的宣言 ,不是冲锋的号角。八字还没一撇就冲上去喊"做我女朋友",多半是把人吓跑。关系已经很熟、彼此心照不宣的时候再开口,那才叫宣言。
💡 顺带说个找工作时会遇到的真实问题: 你投的是 C++ 方向的岗位,面试时公司却问你"能不能转 Java"。这怎么答?分两种情况------
- 手里一个 offer 都没有:不管转什么,答"OK"。答"不 OK"等于直接拒绝,前面技术题答得再好也没用;
- 手里已经有 offer,而且新公司给的更好:当然也是"OK"。
道理很朴素:学技术最终是为了把事做成、把钱挣到。职业生涯很长,学 C++ 的这段时间放在整条时间线上并不算什么,路子别走窄。
6. window frame:两套坐标原点
6.1 窗口框架是操作系统给的
前面讲 geometry 时说"坐标以父控件为参照"。如果我们操作的是最外层窗口,那么它的父参照就有点特殊了。
先看现象:你运行任何一个图形界面程序,窗口主体之上都会多出一条标题栏,四周还会多出一圈很窄的边框。这一整套东西(标题栏 + 边框)叫做 window frame ,也就是窗口框架。
窗口框架不属于你的程序,它是操作系统自带的。 只要你运行一个图形界面程序,系统就会自动给你生成这么一套。当然也有办法把它隐藏掉------比如全屏游戏,整个屏幕都是游戏画面,就是因为窗口框架被去掉了。

上面这张图就是这一节的起点:外面那圈灰色的框架是操作系统给的,里面浅蓝色的区域是窗口本体(也叫客户区),再里面那个白色小矩形才是按钮。
6.2 于是就有了两套坐标原点
问题来了:假设窗口里有个按钮,你要算这个按钮的位置,原点该取哪里?
- 取窗口本体的左上角?还是
- 取窗口框架的左上角?
这两种参考体系在 Qt 里同时存在。 Qt 为位置和尺寸提供了很多组 API,其中一部分以窗口本体左上角为原点(不考虑窗口框架),另一部分以窗口框架左上角为原点(考虑窗口框架)。

这张图把两套体系标得非常清楚:
geometry()、setGeometry():不考虑窗口框架。它们算位置时以窗口本体左上角为原点;取宽高时也不包含外面那圈边框;frameGeometry()、setFrameGeometry():考虑窗口框架。它们以窗口框架左上角为原点;x()、y()、pos()、size()、width()、height()这一批:也都属于"包含窗口框架"的体系。
实际用起来,掌握 geometry 和 frameGeometry 这一对基本就够了,其余的在需要时再查。
6.3 小工程:亲手量出两套坐标的差距
要真正记住这件事,最好的办法是亲手把两组数字打出来对比一下。这个工程的思路是:创建一个按钮,点它就打印 geometry() 和 frameGeometry() 的结果。
工程目录是 QWidget_5,QWidget_5.pro 与 main.cpp 同样是那一份,无需重复粘贴。
文件名:widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void handle();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
widget.cpp 和 widget.ui 这两个文件在写法上有一个特殊之处,要分成两版来看。
第一版:在构造函数里打印
第一版的做法很直接------构造函数里直接对窗口对象调这两组 API,把结果打印出来:
cpp
// 此处直接针对 widget 对象来使用 geometry 和 frameGeometry . 观察区别.
QRect rect1 = this->geometry();
QRect rect2 = this->frameGeometry();
qDebug() << rect1;
qDebug() << rect2;
this->geometry() 中的 this 就是当前窗口对象。两个 QRect 变量分别接住两组结果,然后逐个打印。
运行结果出了问题:两组数字完全一样。 那前面讲了半天的窗口框架、两套原点,岂不是废话?
不是废话,是时机不对。
关键点在于:这段代码放在构造函数 里。构造函数执行的时候,这个窗口对象正在被构造 ------换句话说,它还没有被添加到窗口框架里 。一个窗口最终显示出来要经过两步:先把对象构造出来,再把它挂到窗口框架上。而"挂到窗口框架上"这件事,发生在构造过程之后。
构造函数阶段还没有窗口框架,自然就看不到它的影响,所以两组数字相同。
第二版:挪到按钮的槽函数里
既然问题出在"太早",那就把打印搬到更晚的时机------用户点击按钮的时候。
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QPushButton>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 此处直接针对 widget 对象来使用 geometry 和 frameGeometry . 观察区别.
// QRect rect1 = this->geometry();
// QRect rect2 = this->frameGeometry();
// qDebug() << rect1;
// qDebug() << rect2;
QPushButton* button = new QPushButton(this);
button->setText("按钮");
button->move(100, 100);
connect(button, &QPushButton::clicked, this, &Widget::handle);
}
Widget::~Widget()
{
delete ui;
}
void Widget::handle()
{
QRect rect1 = this->geometry();
QRect rect2 = this->frameGeometry();
qDebug() << rect1;
qDebug() << rect2;
}
文件名:widget.ui
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
</widget>
<resources/>
<connections/>
</ui>
代码导航
构造函数里现在做三件事:
QPushButton* button = new QPushButton(this);创建按钮,父对象是当前窗口;button->setText("按钮");设置按钮文字;button->move(100, 100);把按钮放到距离窗口本体左上角 (100, 100) 像素的位置;connect(button, &QPushButton::clicked, this, &Widget::handle);把按钮的clicked信号连到Widget::handle上。
handle() 槽函数里就是那两句打印。虽然代码和第 4 节那个 handle() 长得像,但含义完全不同------这一次它打印的是窗口已经挂上框架之后的两组数字。
运行程序,点一下按钮,输出窗口里会给出两组明显不同的 QRect:
geometry()的宽高比frameGeometry()小。因为外面还套着一圈窗口框架:宽度上左右各多了一个像素的边框,所以一共多 2 个像素;高度上多得多,因为标题栏本身就占了几十个像素;- 左上角的位置也差了一点。横坐标方向上差 1 个像素,正是左侧边框的宽度;纵坐标方向上差得更多,主要就是标题栏的高度。
还有一个细节要注意: 这两组数字必须让窗口正常显示之后再取,而且窗口大小不同、系统主题不同,具体数值都会变。所以不要去背具体数字,要记住的是"用哪套原点"。
💡 总结这条规则: 以后凡是涉及宽度、高度、位置的计算,先问自己一句------我现在用的是哪套坐标系的原点? 是窗口本体左上角,还是窗口框架左上角?想清楚这个,很多"数字对不上"的问题就自己解释清楚了。
7. windowTitle:窗口标题
7.1 基本用法
windowTitle 表示窗口的标题。相关 API 有两个:
windowTitle():获取当前标题,返回QString(Qt 的字符串类型);setWindowTitle(const QString&):设置标题,参数就是新的标题字符串。
7.2 一个容易踩的坑:它只对顶层窗口有效
这两个方法定义在 QWidget 里,而 QWidget 是个很宽泛的概念------理论上任何控件都是 QWidget 。但 windowTitle 只对顶层窗口(也就是真正作为窗口显示出来的那个控件)有效。
如果你给窗口里的某个按钮、某个输入框设置标题,那是没有任何效果的。
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
this->setWindowTitle("这是窗口标题");
QPushButton* button = new QPushButton(this);
button->setText("按钮");
button->setWindowTitle("通过按钮设置窗口标题");
}
Widget::~Widget()
{
delete ui;
}
文件名:widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
文件名:widget.ui
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
</widget>
<resources/>
<connections/>
</ui>
这个工程的文件构成和第 4 节非常接近:工程文件 QWidget_6.pro、main.cpp 都与前面逐字相同;widget.h 和 widget.ui 也和第 4 节里 QWidget_1 工程中的同名文件基本一致,只是没有了那个多余的 handle() 声明和槽函数。widget.cpp 只改了构造函数。
代码导航
构造函数里三句话:
this->setWindowTitle("这是窗口标题");给自己设置标题。this就是当前窗口对象,它确实是顶层窗口,所以这一句会生效------运行后标题栏上显示的就是"这是窗口标题";QPushButton* button = new QPushButton(this);创建一个按钮;button->setText("按钮");设置按钮文字;button->setWindowTitle("通过按钮设置窗口标题");给按钮设置"窗口标题"。
运行后你会发现:窗口标题正常变了,按钮上的文字是"按钮",而那句给按钮设置的标题完全没有效果------标题栏没有变化。
7.3 关于"没有报错"这件事
这里有个值得讨论的细节:给按钮设置 windowTitle 属于不合适的操作,但它既不生效、也不报任何错。
从我自己的角度,这种设定其实不太科学。设想实际开发中的场景:界面比较复杂,你误以为某个控件就是窗口,于是给它设置了标题。程序不报错,你也以为功能实现了,实际上没有。这个 bug 就这么埋下去,等到上线才被发现。
所以更好的设计是:写出这种不合理的代码时,应该给出明确的提示。
这一点可能刚学编程的时候不容易接受------那会儿一写代码冒出两三个报错,特别恼火。但代码写多了、为 bug 买过单之后就会明白:报错本身是好事。 它让我们尽早发现问题、尽早改掉,避免拖到后面造成更严重的后果。
💡 给自己的编码建议: 关键的用户操作逻辑要做校验;出现不该发生的情况时及时给出提示,不要让程序"将错就错"地跑下去。
8. windowIcon 与 qrc 资源机制
8.1 windowIcon:窗口图标
windowIcon 表示窗口的图标。运行程序时,这个图标会出现在两个地方:窗口左上角,以及任务栏上的程序按钮。
相关 API:
windowIcon():获取图标,返回值是QIcon对象。QIcon就是 Qt 对"图标"这个概念做的封装;setWindowIcon(const QIcon&):设置图标。
和 windowTitle 一样,这两个 API 也只对顶层窗口有效 。而且实际开发中我们几乎只用 setWindowIcon:程序启动就把提前做好的 logo 设置上去,让界面看起来更正式;获取图标这件事基本不会用到。
8.2 第一版:用绝对路径加载图片
设置图标需要先准备一张图片。有了图片,就得让程序知道它在哪。最直觉的做法是直接写死一个绝对路径。
cpp
// 第一版:直接把图片放在 D 盘根目录,用绝对路径加载
QIcon icon("d:/rose.jpg");
this->setWindowIcon(icon);
这里有两个必须讲清的点。
第一个是路径分隔符。 上面写的是正斜杠 /。那能不能写反斜杠 \?不能。
因为反斜杠在 C++ 字符串里是转义字符 的引导符,它和后面的字符会组合成一个特殊字符。d:\rose.jpg 里的 \r 会被编译器看成回车符 (回车符的作用是让光标回到本行的行首),于是这整段字符串就变成了 d: + 回车符 + ose.jpg------路径彻底不对,图片根本加载不到。
正确的写法有两种:
- 用正斜杠:
"d:/rose.jpg"; - 用双反斜杠 :
"d:\\rose.jpg"。两个反斜杠才表示一个真正的反斜杠字符。
C++11 之后还多了第三种更省事的写法------原始字符串(raw string) ,写法是 R"(...)",它的作用是让字符串里不包含任何转义字符,所有字符都按字面意思理解。所以路径可以原样写:
cpp
// 原始字符串:R"(...)" 里的反斜杠不会被当成转义字符
QIcon icon(R"(d:\rose.jpg)");
第二个是要不要 new。 这一段里 QIcon 对象是直接定义在栈 上的,没有用 new。这和前面创建按钮时的做法不同,原因有两条:
QIcon本身是一个很小的对象 。它的用途就是被设置到某个QWidget上,设置过去之后,原来这个QIcon对象释放不释放不影响图标最终显示。而按钮不一样:按钮如果提前释放了,界面上就什么都显示不出来了;QIcon不支持对象树机制 ,也就是说你无法给它指定一个父对象。既然挂不到对象树上,那new出来还得自己手动delete,反而更麻烦。
所以这里的结论是:直接在栈上创建就行,函数一结束它自己就释放了。
还有一点:用 QIcon 需要包含对应头文件 <QIcon>。Qt 里封装的这些类都会提供一个同名头文件------需要哪个类,就包含哪个类对应的头文件,规则非常统一。
8.3 绝对路径为什么不行
写完第一版,运行是能出图标的。但如果把那张图片改名、或者挪到别的位置,代码不动,再运行------
程序不报错,但图标变回了默认的样子。
这就是绝对路径的根本问题:你写的程序最终是要发布到用户的电脑上的,而你无法保证用户电脑上也有和你开发机完全一样的路径。
举几个很现实的例子:
- 你把图片放在 D 盘,但用户电脑只有一个 C 盘;
- 用户把 D 盘上那个文件夹挪走了;
- 用户不小心把图片删了。
只要出现其中任何一种,图标就没了。
8.4 第二版:相对路径
比绝对路径好一些的是相对路径 。相对路径的含义是:以某个给定目录为基准 去找目标文件,通常以 . 或 .. 开头。
.表示当前目录;..表示当前目录的上一级目录。
举例说明:假设基准目录是 D:/,
- 给相对路径
./rose.jpg,含义就是在基准目录D:/下直接找rose.jpg; - 给相对路径
./image/rose.jpg,含义就是在基准目录下先找到image目录,再在里面找rose.jpg。
相对路径的好处是:只要图片和可执行程序的相对位置是固定的,用户机器上的绝对路径长什么样都无所谓。
但它仍然有一个解决不了的问题:它没法防止文件被弄丢。 你按相对路径的结构把程序和图片一起打包发给用户,用户一不小心把图片删了,图标照样没。
8.5 第三版:qrc 资源机制
Qt 给出的最终答案是 qrc 机制 。它从根本上把上面两个问题一起解决了:既不需要保证路径在用户机器上存在,也不需要担心图片被弄丢。

这张图是 qrc 的完整链路,我按步骤走一遍。
第一步,往工程里新建一个资源描述文件,后缀是 .qrc。 在 Qt Creator 里是"新建文件",选 Qt 分类下的 Qt Resource File。文件名随便起(这里叫 resource.qrc),但不要带中文和特殊符号------这是 Qt 工程的一个不成文规律,路径和文件名尽量都用英文。
第二步,在资源编辑器里创建一个前缀,再把图片添加到这个前缀下面。
- "前缀"(prefix)可以理解成一个虚拟目录。这个目录并没有在你的电脑上真实存在,是 Qt 自己抽象出来的;
- 为什么要虚拟目录?因为 qrc 机制本质上就是把图片的二进制数据转成 C++ 代码,代码最终会变成一个巨大的
char数组。为了在代码里方便地访问到这张图片,Qt 就抽象出了这些虚拟目录; - 创建前缀之后,
Add Files按钮才可用(创建前缀之前它是灰的)。点击它选择图片文件即可。
这里有个强制约束:添加的图片必须位于 resource.qrc 文件所在目录,或者该目录的子目录中。 如果你直接去别的盘选一张图,会报"无效"的错误。所以正确做法是先把图片拷贝到工程目录里,再添加。
第三步到第五步是编译时自动完成的。 编译工程时,Qt 会根据 qrc 里的描述找到图片内容,把图片的二进制数据 提取出来,转成一份 C++ 源代码,里面就是一个巨大的 char 数组;这份源码再和其他源文件一起被编译进最终的可执行文件。图片从此就成了 exe 的一部分。
想亲眼看到这个过程,可以打开构建目录,会看到一个自动生成的文件 qrc_resource.cpp------它是 Qt 生成的,不是我们写的,里面的内容就是那张图片每一个字节的数据。
这套机制的好处和代价:
- 好处:路径不再需要存在,图片也不会被弄丢,发布到任何机器上都能正常显示。图片、字体这类小资源用它非常合适;
- 代价:它放不了太大的资源。 如果你把几个 GB 的视频塞进 qrc,编译时要把这几个 GB 全部转成 C++ 代码,编译时间会急剧上涨,编译出来的 exe 体积也会暴涨,后续加载运行都会变慢。
值得一提的是,这种资源管理机制并非 Qt 独有,Android 里的 Resources 和 AssetManager 也是类似的效果。
8.6 工程代码
QWidget_7.pro 相对前面几节的工程文件有改动 ------多了一行 RESOURCES。
文件名:QWidget_7.pro
qmake
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
widget.cpp
HEADERS += \
widget.h
FORMS += \
widget.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
RESOURCES += \
resource.qrc
文件名:resource.qrc
xml
<RCC>
<qresource prefix="/">
<file>rose.jpg</file>
</qresource>
</RCC>
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QIcon>
#include <QDir>
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 设置图标
// QIcon icon("d:/rose.jpg");
QIcon icon(":/rose.jpg");
this->setWindowIcon(icon);
}
Widget::~Widget()
{
delete ui;
}
代码导航
先看 .pro 的改动。 相比前面的工程文件,末尾多了两行:
RESOURCES += \后面的反斜杠表示续行;resource.qrc就是被加进来的资源描述文件。
这一行的作用是告诉 qmake:"这个工程有一个资源文件要一起处理。"少了它,编译器就不会去处理 resource.qrc,资源自然也进不了 exe。
再看 resource.qrc。 它是 XML 格式:
<RCC>是根节点,全称是 Qt Resource Compiler,也就是处理资源文件的那个工具;<qresource prefix="/">表示资源前缀是/。这一项和前面在资源编辑器里创建的前缀完全对应;<file>rose.jpg</file>表示这个前缀下管理着rose.jpg这个文件。
最后看 widget.cpp 的最终版本。 第一版那句绝对路径的写法已经被替换掉了:
cpp
// 设置图标
QIcon icon(":/rose.jpg");
this->setWindowIcon(icon);
关键在于那个冒号。当代码里要访问一个由 qrc 管理的文件时,路径必须以冒号开头 ,Qt 看到这个前缀就知道"这个要去 qrc 里找"。冒号后面接的就是前缀 + 文件名 :前缀是 /,文件名是 rose.jpg,合起来就是 :/rose.jpg。
在资源编辑器里创建的前缀叫什么名字,代码里就写什么名字------这是一个必须对齐的约定,写错了就找不到资源。
运行后,窗口左上角和任务栏上的图标都会变成这张图片。
💡 提示: 去哪儿找图标素材?推荐阿里巴巴矢量图标库(iconfont.cn)。它是阿里维护的一个开源站点,里面有海量图标可以免费下载,下载前还能改颜色、选格式(PNG、SVG 等)。自己做的图标也可以上传上去共享给其他人。
9. windowOpacity:窗口不透明度与浮点精度
9.1 基本用法
windowOpacity 用来控制窗口的不透明度。
相关 API:
windowOpacity():获取当前的不透明度数值,返回float类型,取值范围是0.0到1.0;setWindowOpacity(float):设置不透明度。
数值的含义要特别注意:1.0 表示完全不透明,0.0 表示完全透明。
所以严格说,这个属性叫"不透明度 "比叫"透明度"更贴切------数值越大越不透明。
半透明效果曾经非常流行。Windows Vista 主打的"毛玻璃效果"就是基于半透明再做一些处理做出来的。不过 Vista 当年口碑一般,主要原因是那个年代的硬件资源太紧张------CPU 只有两核、内存普遍 1G 到 2G,界面特效再好看也会把系统拖卡,属于生不逢时。后面口碑更好的 Windows 7 就吸取了这个教训,把特效做得更克制、运行更流畅。
9.2 工程代码
这个工程在界面上放两个按钮,一个增加不透明度、一个减少。
工程名是 QWidget_8,QWidget_8.pro 和 main.cpp 与前面几节逐字相同,这里只给出有改动的三份文件。
文件名:widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_add_clicked();
void on_pushButton_sub_clicked();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_add_clicked()
{
float opacity = this->windowOpacity();
if (opacity >= 1.0) {
return;
}
qDebug() << opacity;
opacity += 0.1;
this->setWindowOpacity(opacity);
}
void Widget::on_pushButton_sub_clicked()
{
float opacity = this->windowOpacity();
if (opacity <= 0.0) {
return;
}
qDebug() << opacity;
opacity -= 0.1;
this->setWindowOpacity(opacity);
}
文件名:widget.ui
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
<widget class="QPushButton" name="pushButton_add">
<property name="geometry">
<rect>
<x>210</x>
<y>190</y>
<width>93</width>
<height>28</height>
</rect>
</property>
<property name="text">
<string>+</string>
</property>
</widget>
<widget class="QPushButton" name="pushButton_sub">
<property name="geometry">
<rect>
<x>360</x>
<y>190</y>
<width>93</width>
<height>28</height>
</rect>
</property>
<property name="text">
<string>-</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>
代码导航
widget.ui 里两个按钮的 objectName 是 pushButton_add(文字 +)和 pushButton_sub(文字 -)。
widget.h 里声明了两个槽函数,都放在 private slots: 下。
widget.cpp 里两个槽函数的结构完全对称,以"增加"为例:
float opacity = this->windowOpacity();先把当前值读出来;if (opacity >= 1.0) { return; }如果已经达到上限就直接返回 。return在这里的作用是提前结束函数,后面的代码不会再执行;qDebug() << opacity;把当前值打印出来,方便观察;opacity += 0.1;加 0.1。+=是复合赋值运算符,a += b等价于a = a + b;this->setWindowOpacity(opacity);把改完的值设回去。
第 5 步特别容易漏。改局部变量的值只是改了你手里的副本,必须重新设置回窗口才会真正生效 ------这和第 5 节里 geometry 改完要 setGeometry 是同一个道理。
"减少"那个槽函数逻辑一样,只是判断条件换成 opacity <= 0.0,运算是 -= 0.1。每次增减的步长(这里是 0.1)可以自己调整。
运行现象
程序启动时窗口是完全不透明的。点几下"减少",窗口越来越透明,能透过它看到桌面上的内容;一直减下去,窗口会彻底"消失"。如果这时候找不到"增加"按钮了,把程序关掉重新运行即可。
9.3 两个值得深挖的问题
跑完这个程序,有两个问题值得单独说清楚。
问题一:不透明度变化并不精确
仔细观察打印出来的数值会发现:每次明明是 += 0.1 或 -= 0.1,但结果并不是精确的 0.9、0.8、0.7,而是一串像 0.898、0.796 这样"很接近但不等于"的数字。
原因在 IEEE 754 标准------它是浮点数在内存中的存储规范。

这个标准规定,浮点数要用二进制科学计数法 来表示。就像十进制里写 1.23 × 10²,二进制里写的是 1.xxx × 2ⁿ。一个浮点数被分成三部分:
- 符号位:一位,决定这个数是正还是负。所以浮点数里会存在"正零"和"负零"这种情况;
- 指数部分:决定小数点在哪,也就是 2 的多少次方;
- 有效数字部分:存的是小数点后面那一段。因为二进制非零数的整数部分固定是 1,这个 1 就不存进内存了,只存小数部分。
问题出在有效数字部分。它是二进制 的小数,每一位的权重从左到右依次是 0.5、0.25、0.125......每往右一位就再除以 2。
- 二进制小数
0.1(读作"零点一",二进制)= 0.5 - 二进制小数
0.01= 0.25 - 二进制小数
0.001= 0.125 - 所以二进制小数
0.101= 0.5 + 0.125 = 0.625
可以看到,用这套权重能精确凑出来的只有 0.5、0.25、0.125、0.625、0.75 这类数。像 0.1 这样的十进制小数,无论怎么组合都凑不出正好相等的值,只能凑出一个非常接近的数。
除非有效数字部分无限长 ,才能无限逼近。但 float 和 double 的有效数字长度都是有限的,所以误差必然存在。这就是不透明度每次只变化"大约 0.1"的根本原因。
需要强调:这不是 C++ 独有的问题。 C、C++、Java、Python、Go 这些主流语言用的都是这套体系(IEEE 754)。它的优势是占用空间小、运算速度快,CPU 在设计时就对这类运算做了专门优化;代价就是有些小数无法精确表示。
⚠️ 易错点:千万不要把两个浮点数直接用 == 比较。 比如 0.1 + 0.2 == 0.3 这种写法,结果很可能是 false,因为两边都有微小误差。
正确的做法是做差,判断差的绝对值是否小于预期的误差范围。
💡 提示: 涉及到需要精确计算的场景------最典型的就是算钱 ------一定要慎重使用 float 和 double。不同语言有不同的解决方案,比如 Java 里有 BigDecimal,C# 里有 decimal,它们用别的内存模型实现精确表示,代价是速度和空间上都更贵一些。
问题二:那个边界判断到底要不要写
上面代码里有一句 if (opacity >= 1.0) { return; }。实际上不写也能跑 ------因为 setWindowOpacity 内部也会做范围判断,超过 1.0 的值根本设不进去。
那为什么还要写?这涉及一个很重要的编程思想:防御性编程。
有一个很贴切的比喻叫"防御性驾驶":哪怕别人开车出错,只要你自己严格按规范来,别人犯错对你造成的伤害也会被大幅降低。比如系安全带------严格系好安全带、按时做年检、检查安全气囊,真的出了事故,这些措施能尽可能减少你受到的伤害。
编程上是一样的道理。一个大型项目通常要拆成很多模块,由不同的人分别负责,模块之间通过 API(也就是函数、类)互相调用。站在调用方的角度,你不确定对方提供的接口会不会处理非法参数;站在实现方的角度,你也不知道调用方会不会做校验。 而写调用的人和写实现的人,往往是两拨人,彼此并不知道对方做了什么。
针对这个问题,有一本叫《代码大全》的书给出的结论是:双重判定(double check)。

- 调用方:不确定对方传进来的指针是否合法,所以自己先挡一层;
- 实现方:不知道调用方会不会做校验,所以自己进函数再挡一层。
你可能会觉得两边都判有点重复、代码显得臃肿。但如果两边是两拨人写的,任何一方出现失误没有判定,都不会造成严重后果------这就是双重判定的意义。反过来,如果这个函数从定义到调用全是你一个人写的,那确实没必要判两遍。
💡 提示: 《代码大全》值得推荐。它不讲某一种语言的语法,也不讲数据结构和算法,而是讲"怎样才能把代码写好"这件跨语言通用的事。什么时候读效果最好?有一定工作经验、真正为了 bug 买过单之后再去读,收获会大得多------因为那时候你会真正理解它为什么那么说。
10. cursor:鼠标光标
10.1 光标是什么
鼠标在屏幕上移动时,指针的形状是会变的:默认是一个箭头;放到文本上会变成竖线状(像个大写字母 I);程序卡住时会变成转圈的圆圈,或者沙漏。
我们自己的程序也可以做到:让鼠标指向不同控件、不同区域时,显示不同的光标。
10.2 相关 API
这一节涉及三个方法,作用范围一层比一层大:
cursor():获取当前控件的光标,返回QCursor对象。QCursor是 Qt 对"光标"这个概念做的封装。这个属性是QWidget级别 的,也就是说同一个界面里不同的控件可以设置成不同的光标;setCursor(const QCursor&):设置当前控件的光标。只在鼠标悬停在这个控件上时生效;QApplication::setOverrideCursor(const QCursor&):设置全局 光标。注意这里的"全局"指的是这个程序范围内的全局 ,不是整个系统级别的------它只影响当前运行的这个程序,鼠标移到别的程序上还是正常的。所以它比逐个控件设置要省事,但也会覆盖掉setCursor设置的局部效果。
⚠️ 易错点: 一定要分清"光标在这个控件上生效"和"光标在这个窗口上生效"。给窗口设置光标,鼠标放到这个窗口的任何位置都会变;给按钮设置光标,只有鼠标在按钮上时才变。设置之前先确认自己操作的是哪个对象。
10.3 使用 Qt 内置的光标形状
先看用内置形状的版本。
文件名:widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QCursor cursor(Qt::WaitCursor);
// ui->pushButton->setCursor(cursor);
this->setCursor(cursor);
}
Widget::~Widget()
{
delete ui;
}
文件名:widget.ui
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="cursor">
<cursorShape>ArrowCursor</cursorShape>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
<widget class="QPushButton" name="pushButton">
<property name="geometry">
<rect>
<x>260</x>
<y>200</y>
<width>301</width>
<height>151</height>
</rect>
</property>
<property name="cursor">
<cursorShape>ArrowCursor</cursorShape>
</property>
<property name="text">
<string>按钮</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>
代码导航
widget.ui 里有一个按钮 pushButton,另外根节点上还多了一项:
xml
<property name="cursor">
<cursorShape>ArrowCursor</cursorShape>
</property>
ArrowCursor 就是普通的箭头,是默认值。这一项的存在说明:光标可以在界面设计器里直接改,不必写代码。
widget.cpp 的构造函数里只有两句:
cpp
QCursor cursor(Qt::WaitCursor);
this->setCursor(cursor);
QCursor cursor(Qt::WaitCursor);构造一个光标对象。参数Qt::WaitCursor是一个枚举值,表示"等待"形状(也就是沙漏/转圈那种);this->setCursor(cursor);把它设置到当前窗口上。
注意这里同样是在栈上创建 QCursor,不用 new。 理由和第 8 节里的 QIcon 一样:对象小,而且它不支持对象树机制,设置过去之后原来的对象释放不释放不影响显示。
代码里还留了一行注释掉的写法:
cpp
// ui->pushButton->setCursor(cursor);
这一句是把光标设置到按钮 上,而不是窗口上。两者的效果范围完全不同:设到按钮上,只有鼠标悬停在按钮上时才变;设到窗口上,整个窗口范围内都变。想验证这一点,把注释打开、把下面那行注释掉再运行对比即可。
在属性编辑器里改
除了写代码,更省事的方式是直接在 Qt Creator 右侧属性编辑器里改:
- 选中控件(注意选中的是按钮还是窗口,效果范围不同);
- 在属性列表里往下翻,找到
cursor属性; - 点开它,Qt 会列出一大堆内置光标形状供选:默认箭头、上箭头、十字、等待、文本光标、垂直/水平大小调整、打开的手、关闭的手、手势类光标等等。
Qt::CursorShape 这个枚举里的取值远不止界面上列出的那些,代码里可以直接用任意一个。
10.4 用自定义图片做光标
内置的这些光标形状,"经典"是客气说法------直白点说就是有点跟不上现在的审美。好在 Qt 留了扩展空间:允许用自定义图片来设置光标。
做法和设置窗口图标很像:先准备一张图片,导入工程(同样用 qrc 管理),然后在代码里访问到它,基于它构造出光标对象并设置。
文件名:QWidget_10.pro
qmake
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
widget.cpp
HEADERS += \
widget.h
FORMS += \
widget.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
RESOURCES += \
resource.qrc
文件名:resource.qrc
xml
<RCC>
<qresource prefix="/">
<file>basketball.png</file>
</qresource>
</RCC>
文件名:widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 访问到图片文件
QPixmap pixmap(":/basketball.png");
pixmap = pixmap.scaled(100, 100);
// 构造光标对象
QCursor cursor(pixmap, 10, 10);
// 把光标设置进去
this->setCursor(cursor);
}
Widget::~Widget()
{
delete ui;
}
文件名:widget.ui
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
</widget>
<resources/>
<connections/>
</ui>
代码导航
QWidget_10.pro 和前面 QWidget_7 工程里那份一样,末尾多了 RESOURCES 那两行,把 resource.qrc 加了进来。resource.qrc 的结构也和 QWidget_7 工程里的一致,只是管理的文件换成了 basketball.png。
widget.h 和 widget.ui 与前面 QWidget_9 工程中的同名文件一致(其中 widget.h 和 QWidget_6 工程里的那份也是逐字相同的),这里不再重复粘贴。
widget.cpp 的构造函数里三步走:
第一步,把图片读进来。
cpp
QPixmap pixmap(":/basketball.png");
QPixmap 是 Qt 里表示图片的类之一(常见的还有别的几个,但 QPixmap 功能最丰富,用得最多)。参数是资源路径,冒号开头表示去 qrc 里找------规则和第 8 节完全一样。同样是在栈上创建。
第二步,按需缩放。
cpp
pixmap = pixmap.scaled(100, 100);
scaled(int w, int h) 把图片缩放成指定的宽高。
⚠️ 易错点:scaled 不会修改原对象,而是返回一个新的图片副本。 所以必须把返回值接住------直接写 pixmap.scaled(100, 100); 是没用的,必须写成 pixmap = pixmap.scaled(100, 100);。
缩放时如果只给一个方向的比例,或者宽高比和原图不一致,图片可能会被拉扁。想让图片"等比例缩放"可以用 scaled 的另一个重载版本,传入宽高和一个 Qt::KeepAspectRatio 之类的方式参数。这里为了简单就直接指定了 100×100。
第三步,构造光标并设置。
cpp
QCursor cursor(pixmap, 10, 10);
this->setCursor(cursor);
QCursor(const QPixmap&, int hotX, int hotY) 这个重载版本接收三个参数:
- 第一个是图片;
- 后两个是热点坐标 (hot spot)。热点指的是鼠标真正"点击生效"的那个位置。
为什么要热点?因为图片是一整块,而鼠标点击本质上是一个点。默认情况下,点击生效的位置是图片的左上角 。但有时候我们希望点击点在图片中间------比如用一个圆形图标当光标,显然中心点才是应该响应点击的地方。这时候就用后两个参数指定:以图片左上角为原点 (0, 0),找到 (10, 10) 这个位置作为真正的点击点。
运行后,鼠标移到窗口上就变成这张图片;按一下左键,生效的位置就是热点那个坐标。
10.5 关于素材
同样可以用阿里巴巴矢量图标库找素材。搜索关键词(比如"篮球")会列出很多种,选中一个还能在线改颜色,改完按需下载 PNG 或其他格式,全部免费。有设计能力的同学也可以自己做图标上传上去,共享给其他人。
11. font:字体
11.1 它是一组属性的集合
font 严格来说不是单一属性,而是一组和字体相关的属性的集合。
相关 API:
font():获取当前控件的字体信息,返回QFont对象;setFont(const QFont&):设置字体信息。
QFont 这个类包含的属性,比前面介绍过的 QRect、QIcon 都要多一些。常用的有这么几个:
family:字体家族,也就是我们平时说的"用什么字体"。比如楷体、宋体、微软雅黑、仿宋;pointSize/pixelSize:字体大小;weight:字体粗细。用数值表示,Qt 里的取值范围是 0 到 99,数值越大越粗;bold:是否加粗 。它其实是一个便捷属性------设为true相当于把weight设成 75,设为false相当于设成 50。日常我们基本只用这个,不去纠结 98 和 99 之间差多少(肉眼也看不出来);italic:是否倾斜;underline:是否带下划线;strikeOut:是否带删除线,也就是一条从文字中间穿过去的线。
关于 family 多说一句:像毕业论文这种文档,对字体、字号、行间距、哪里换行、哪里空格都有严格的格式要求,差一点就会被打回重做。界面上的字体选择本质上和这是一回事,只是没那么严格。
关于粗细还要提醒一句:加粗与否并不是单纯的"是/否"二元概念 ,weight 允许你设置具体的粗细值。只是实际开发中我们更常用 bold 这个开关式的属性,很少去精细调节数值。
11.2 单位:像素
字体大小的单位是像素(pixel)。像素在计算机里是一个非常常见的单位,但凡涉及尺寸和位置,基本都用它。
11.3 在界面设计器里设置
第一种方式是在 Qt Creator 里直接改。
文件名:widget.ui
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>180</x>
<y>150</y>
<width>341</width>
<height>111</height>
</rect>
</property>
<property name="font">
<font>
<family>仿宋</family>
<pointsize>30</pointsize>
<weight>75</weight>
<italic>true</italic>
<bold>true</bold>
<underline>true</underline>
<strikeout>true</strikeout>
</font>
</property>
<property name="text">
<string>这是一段文本!</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>
文件名:widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
代码导航
widget.ui 里有一个 QLabel,它的 font 属性被完整地配置了一遍:
<family>仿宋</family>:字体家族是仿宋;<pointsize>30</pointsize>:字号 30;<weight>75</weight>:粗细 75,对应"加粗";<italic>true</italic>:倾斜;<bold>true</bold>:加粗;<underline>true</underline>:下划线;<strikeout>true</strikeout>:删除线。
widget.h 和 widget.cpp 都是最朴素的形态------只有一个构造函数,构造函数里只有 ui->setupUi(this); 一句。也就是说,这个工程的字体效果完全是在界面设计器里配出来的,代码一行都没写。 这套 widget.cpp 后面几个工程还会再用到。
实时预览
在界面上选中这个 QLabel,右侧属性列表里往下翻能找到一个 font 属性,展开它就能修改字体家族、字号、粗细、斜体、下划线、删除线等等。
Qt Creator 的界面设计器支持实时预览------你改哪个属性,界面上立刻显示效果,不需要运行程序就能看到结果。这一点用起来非常方便。
属性列表里还有一个"反锯齿"选项。这里不展开,它属于界面优化的范畴,对画面显示的平滑程度有一定帮助。
11.4 用代码设置
界面设计器改起来方便,但它解决不了一个问题:不够灵活。 如果需要在程序运行过程中动态修改字体属性,就只能靠代码。
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QLabel>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QLabel* label = new QLabel(this);
label->setText("这是一段文本");
// 创建字体对象
QFont font;
font.setFamily("仿宋");
font.setPixelSize(30);
font.setBold(true);
font.setItalic(true);
font.setUnderline(true);
font.setStrikeOut(true);
// 把 font 对象设置到 label 中
label->setFont(font);
}
Widget::~Widget()
{
delete ui;
}
文件名:widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
QWidget_12.pro、main.cpp 与前面的工程逐字相同;widget.ui 与 QWidget_1 工程中的那份一致(都是只有窗口本身、没有任何子控件),不再重复粘贴。
代码导航
构造函数里做了两件事。
第一件,创建一个标签并设置文字。
cpp
QLabel* label = new QLabel(this);
label->setText("这是一段文本");
QLabel 是"标签"控件的类名。这里在堆上创建,父对象传 this,所以它会自动显示在窗口上,并且由窗口负责销毁。因为这个工程用到了 QLabel 这个类,所以 widget.cpp 里显式包含了 <QLabel>。
这个时候运行程序,标签会出现在窗口左上角,文字比较小------因为还没设置字体。
第二件,构造字体对象、配置属性、设置到标签上。
cpp
// 创建字体对象
QFont font;
font.setFamily("仿宋");
font.setPixelSize(30);
font.setBold(true);
font.setItalic(true);
font.setUnderline(true);
font.setStrikeOut(true);
// 把 font 对象设置到 label 中
label->setFont(font);
逐句看:
QFont font;在栈上创建一个字体对象。理由和QIcon、QCursor一样:对象不大,也不需要挂到对象树上;font.setFamily("仿宋");设置字体家族;font.setPixelSize(30);按像素 设置字号为 30。注意方法名是setPixelSize而不是setPointSize------两者都是设置大小的方式,但单位不同:pixelSize的单位是像素,pointSize的单位是点。像素是"屏幕上占多少个像素点",点是排版行业传统的字号单位。在屏幕显示场景里用像素更直观;font.setBold(true);、font.setItalic(true);、font.setUnderline(true);、font.setStrikeOut(true);依次打开加粗、倾斜、下划线、删除线;label->setFont(font);最后必须把配置好的字体对象设置到标签上,否则前面所有设置只是改了一个没人使用的局部变量。
运行现象
运行后,标签里的文字会同时具备仿宋、30 像素、加粗、倾斜、下划线、删除线这六项特征。
你还可以进一步扩展:在界面上加几个按钮,每个按钮负责切换某一个字体属性,就能做出运行时动态改字体的效果。
💡 提示:字体属性具体怎么选,是一个"审美问题",不是"技术问题"。 正式项目里这类决定通常由美工/设计来做。如果所在环境没有美工,可以参考别人做好的网页:在浏览器里打开目标页面,右键选择"检查",打开开发者工具,用左上角那个选择元素的小箭头点一下想参考的文字,就能在右侧看到它用的字号、字体、颜色等具体值,照着调就行。开发者工具里还有很多有用的功能,值得自己多摸索。
12. toolTip:悬停提示
12.1 它解决什么问题
界面一复杂,按钮一多,"哪个按钮是干什么的"就不那么直观了。于是很多程序都提供了这样一个机制:鼠标悬停在控件上时弹出一小段提示,告诉你它是做什么的。
这就是 toolTip。相关 API 有两个:
setToolTip(const QString&):设置提示内容;setToolTipDuration(int):设置提示的持续时间 ,单位是毫秒。
toolTip 是给用户看的,代码里一般不需要获取它。这一点从 API 设计上也能看出来:Qt 提供了 toolTip() 用于获取,但实际开发中几乎用不到------提示信息都是往里设置的。这个"is/get 取、set 设"的命名规律在 Qt 里是一贯的。
12.2 关于时间单位
setToolTipDuration 的参数单位是毫秒,这个要记牢:
- 1 秒 = 1000 毫秒
- 1 毫秒 = 1000 微秒
⚠️ 易错点:时间单位是 1000 进制,不是 1024 进制。 国际单位制就是老老实实的 1000 进制;1024 进制是我们熟悉的 KB、MB、GB、TB 那一套,那是计算机特有的存储单位。两者完全是两回事,不要混。
如果你希望提示一直显示、不自动消失,把持续时间设成一个非常大的数字即可。
12.3 工程代码
这一节的工程是 QWidget_13,QWidget_13.pro 和 main.cpp 与前面的工程逐字相同,不再重复粘贴。
文件名:widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 设置这两个按钮的 toolTip
ui->pushButton_yes->setToolTip("这是一个 yes 按钮");
ui->pushButton_yes->setToolTipDuration(3000);
ui->pushButton_no->setToolTip("这是一个 no 按钮");
ui->pushButton_no->setToolTipDuration(7000);
}
Widget::~Widget()
{
delete ui;
}
文件名:widget.ui
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
<widget class="QPushButton" name="pushButton_yes">
<property name="geometry">
<rect>
<x>150</x>
<y>250</y>
<width>93</width>
<height>28</height>
</rect>
</property>
<property name="text">
<string>yes</string>
</property>
</widget>
<widget class="QPushButton" name="pushButton_no">
<property name="geometry">
<rect>
<x>440</x>
<y>250</y>
<width>93</width>
<height>28</height>
</rect>
</property>
<property name="text">
<string>no</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>
代码导航
widget.ui 里有两个按钮,objectName 分别是 pushButton_yes(文字 yes)和 pushButton_no(文字 no)。
widget.h 和第 11 节里那份一样,只有构造函数。
widget.cpp 的构造函数里四句话,分成两组:
cpp
// 设置这两个按钮的 toolTip
ui->pushButton_yes->setToolTip("这是一个 yes 按钮");
ui->pushButton_yes->setToolTipDuration(3000);
ui->pushButton_no->setToolTip("这是一个 no 按钮");
ui->pushButton_no->setToolTipDuration(7000);
- 前两句给
yes按钮设置提示文字和持续时间。3000毫秒也就是 3 秒; - 后两句给
no按钮设置,7000毫秒也就是 7 秒。
ui->pushButton_yes 这一套写法能成立,还是靠 objectName 生成出来的成员变量,规则和第 4 节讲的一样。
运行现象
把鼠标停在 yes 按钮上,会弹出"这是一个 yes 按钮",3 秒后自动消失;停在 no 按钮上,提示显示 7 秒。
再说设置方式的选择
这类属性既可以在界面设计器的属性列表里直接改,也可以写代码改。实际开发中怎么方便怎么来就行------可以全部在界面上配,也可以全部用代码写,还可以一部分界面配、一部分代码写,都行。
不过有一个原则要记住:属于"程序一启动就要呈现"的效果,设置代码放在构造函数里最合适。这一点前面几个工程都体现出来了。
13. focusPolicy:焦点策略
13.1 什么是"焦点"
焦点(focus) 描述的是"当前的键盘输入会送到哪个控件"。
这件事在键盘操作上体现得特别明显。假设界面上有一个输入框:
- 你必须先选中这个输入框,接下来键盘上按下的字才会输入进去;
- 如果你选中的是别的控件、别的窗口,那么键盘输入就不会进入这个输入框。
所以焦点的含义就是:哪个控件拿到了焦点,接下来的操作就针对哪个控件。

即时战略类游戏(RTS,比如星际争霸、魔兽争霸)是理解焦点的好例子。这类游戏要控制很多个单位,基本操作逻辑是"先选中单位,再下达命令":
- 选中单位的过程,就是让单位获取到焦点;
- 选中之后,下方会显示出这个单位可以释放的技能和可以执行的命令(移动、攻击、原地待命等);
- 如果什么都没选中,那一块就是空的,你无法进行任何操作。
控件和这是一回事。
13.2 焦点还与一件事有关
焦点机制在现实世界里还有一个很有意思的应用:线上笔试的反作弊。
现在很多公司招聘时会先发一个笔试链接,点开就是网页,直接在线做题。你难免会想:不会的题能不能切出去搜一下?
答案是可以切,但平台一定会知道。原理就是窗口焦点:
- 你在人家网页里做题时,那个网页是始终持有焦点的状态;
- 你一旦切到搜索引擎或者别的程序,网页立刻就能感知到失去焦点;
- 服务器就能收集到这些状态变化,用来衡量你是否涉嫌作弊。
而且这只是判定因素之一。平台通常还会结合摄像头画面、麦克风录音以及各种行为特征综合给你打一个作弊风险分。现在这类平台的反作弊机制已经相当成熟,你不会收到任何警告,但后台会有非常详细的记录:哪一刻、做了什么动作、当时摄像头的截图、当时的声音,全都留档。
⚠️ 重要提醒: 笔试分数低,仍有很大机会通过(因为别人也不一定高);但一旦被判定作弊,就是百分之百通过不了,甚至可能被企业直接拉黑。这属于诚信红线,千万不要去试。
13.3 focusPolicy 的取值
一个控件获取焦点通常有两条途径:鼠标点击 ,和键盘上的 Tab 键 。focusPolicy 就是用来影响这两条途径的。
相关 API:
focusPolicy():获取当前策略,返回Qt::FocusPolicy枚举类型;setFocusPolicy(Qt::FocusPolicy):设置策略。
Qt::FocusPolicy 的常见取值:
Qt::NoFocus:控件不接收键盘焦点。不管鼠标点击还是按 Tab,都拿不到焦点。有些控件本来就不该被"选中",比如用来显示文本的标签,用这个值正合适;Qt::TabFocus:只能通过 Tab 键获取焦点;Qt::ClickFocus:只能通过鼠标点击获取焦点;Qt::StrongFocus:Tab 键和鼠标点击都可以 。这是默认值,也是最符合日常操作习惯的;Qt::WheelFocus:比StrongFocus再多一个"鼠标滚轮可以获得焦点"的途径,是较新版本新增的,日常很少用。
13.4 工程代码
工程名是 QWidget_14,QWidget_14.pro、main.cpp 与前面的工程逐字相同。
文件名:widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
文件名:widget.ui
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
<widget class="QLineEdit" name="lineEdit">
<property name="geometry">
<rect>
<x>180</x>
<y>70</y>
<width>361</width>
<height>51</height>
</rect>
</property>
</widget>
<widget class="QLineEdit" name="lineEdit_2">
<property name="geometry">
<rect>
<x>180</x>
<y>160</y>
<width>361</width>
<height>51</height>
</rect>
</property>
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
</widget>
<widget class="QLineEdit" name="lineEdit_3">
<property name="geometry">
<rect>
<x>180</x>
<y>250</y>
<width>361</width>
<height>51</height>
</rect>
</property>
</widget>
<widget class="QLineEdit" name="lineEdit_4">
<property name="geometry">
<rect>
<x>180</x>
<y>340</y>
<width>361</width>
<height>51</height>
</rect>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>
代码导航
这个工程在界面上放了四个单行输入框 (QLineEdit),objectName 依次是 lineEdit、lineEdit_2、lineEdit_3、lineEdit_4。
widget.h 和第 11 节那份一致,widget.cpp 也和第 11 节里 QWidget_11 工程中的那份逐字相同------都只有构造函数和一句 ui->setupUi(this);。这一属性的效果全部靠界面属性完成,代码不用改。
widget.ui 里有一个细节值得注意:lineEdit_2(也就是第二个输入框)身上多了一项属性:
xml
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
这说明焦点策略可以直接在设计器的属性列表里改 。其他三个输入框没有这一项,用的就是默认值 StrongFocus。
实验过程与现象
先不改任何属性,直接运行:
- 程序启动时,第一个输入框拿到焦点------它里面有一个闪烁的光标,外围还有一圈淡蓝色的边框提示;
- 鼠标依次点击第二个、第三个、第四个输入框,每个都能正常获取焦点并输入内容;
- 按 Tab 键,焦点从第一个跳到第二个,再跳到第三个、第四个,然后绕回第一个;按 Shift + Tab 则是反向切换。
也就是说,默认情况下鼠标点击和 Tab 键都能切换焦点。
接下来把第二个输入框的 focusPolicy 改成 NoFocus,再运行:
- 鼠标点第二个输入框,点不动,拿不到焦点;第三、第四个照常;
- 按 Tab 键,焦点直接从第一个跳到第三个,第二个被跳过了;再按还在第一、第三、第四之间循环。
再把第二个输入框改成 TabFocus:
- 鼠标点它,依然点不动;
- 但按 Tab 键可以让它获取焦点。
最后改成 ClickFocus:
- 鼠标点击可以获取焦点;
- 但按 Tab 键时,焦点从第一个直接跳到第三个,第二个被跳过。
三个取值的效果,这样亲手跑一遍就非常清楚了。
💡 提示: 大部分情况下用默认策略就够了,只有特定需求才需要改。这一节真正要记住的其实不是这个属性本身,而是**"焦点"这个概念**------它在图形界面开发里非常关键:先确定焦点在哪,才知道你的输入去了哪里。
14. styleSheet:样式表与颜色表示
14.1 从"样式"说起
styleSheet 这个属性,作用是通过类似 CSS 的方式去设置控件的样式。
这里出现了两个需要先解释的词:CSS 和样式。
样式,描述的是"界面具体长什么样"。 打个比方:你跟朋友描述一个刚见到的人,说"大高个、大长腿、长头发、乌黑的眼睛、樱桃小嘴、高鼻梁、白皙的皮肤"------这一串形容词就是"样式"。样式就是你用来描述外观的那组特征。
放到界面上,样式就是:用什么字体、多大字号、什么颜色、有没有边框、背景是什么色......
CSS 的全称是"层叠样式表",是网页开发里用来设置网页样式的一套规范。它发展了很多年,功能非常丰富,能做出非常好看的页面。
问题来了:网页是界面,客户端程序也是界面,CSS 这套东西看起来挺好用,能不能搬到客户端上来?
Qt 就是干了这件事。它把 CSS 参考了过来,做出了自己的一套,叫 QSS(Qt Style Sheet)------你叫 CSS,我姓 Q,毕竟我本来就叫 Qt。
要说明的是:QSS 相比 CSS 在功能上还是缺失不少的,但即使如此,它已经能帮我们完成相当多的效果,属于很实用的东西。这一节先初步用一下,建立直观认识,后面还有专门章节细讲它的完整用法。
14.2 在界面设计器里写样式
先看用界面设计器的方式。
先看 QWidget_15 工程------QWidget_15.pro 和 main.cpp 与前面一致,界面上只有一个 QLabel,样式全部写在设计器里。
文件名:widget.ui
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>240</x>
<y>180</y>
<width>321</width>
<height>91</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true">font-family: '微软雅黑';
font-size: 30px;
font-style: italic;
color: green;</string>
</property>
<property name="text">
<string>这是一段文本</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
文件名:widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
代码导航
widget.ui 里有一个 QLabel,它身上多了一项 styleSheet 属性:
xml
<property name="styleSheet">
<string notr="true">font-family: '微软雅黑';
font-size: 30px;
font-style: italic;
color: green;</string>
</property>
注意到这个 <string> 标签带了 notr="true"。这是 XML 里的一种属性写法,含义是"这段字符串不需要翻译"------因为它是样式代码,不是给人看的界面文字,翻译工具不应该动它。
样式内容本身用了四条声明,语法格式是"键值对":
font-family: '微软雅黑';:字体家族是微软雅黑。值里有空格时用单引号括起来;font-size: 30px;:字号 30 像素 ,px是像素的单位后缀;font-style: italic;:字形为倾斜;color: green;:文字颜色是绿色。
这套语法的两条分隔规则必须记牢:
- 键和值之间用冒号
:分隔; - 键值对和键值对之间用分号
;分隔。
⚠️ 易错点:所有标点都必须是英文半角。 用了中文全角的冒号或分号,样式不会生效。
widget.h 和第 11 节里那份一致,widget.cpp 也和第 11 节里 QWidget_11 工程中的那份逐字相同------都只有构造函数和一句 ui->setupUi(this);。也就是说这个工程的样式完全是在设计器里配的。
操作方式
在 Qt Creator 里选中这个 QLabel,右侧属性列表往下翻,能看到 styleSheet 属性。点击它右侧的按钮会弹出一个编辑框,在里面直接写样式;也可以在属性上右键,选择"改变样式表",效果一样。
和字体属性一样,样式编辑同样支持实时预览------写完点确定,界面上立刻变,不用运行程序。
14.3 用代码写样式:日夜模式切换
界面设计器配样式虽然方便,但同样解决不了"运行时动态修改"的需求。接下来看用代码实现一个典型的场景:日间模式 / 夜间模式切换。
文件名:widget.h
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void on_pushButton_light_clicked();
void on_pushButton_dark_clicked();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
文件名:widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_light_clicked()
{
// 设置窗口的样式
this->setStyleSheet("background-color: rgb(240, 240, 240);");
// 设置输入框的样式
ui->plainTextEdit->setStyleSheet("background-color: white; color: black;");
// 设置按钮的样式
ui->pushButton_light->setStyleSheet("color: black;");
ui->pushButton_dark->setStyleSheet("color: black;");
}
void Widget::on_pushButton_dark_clicked()
{
// 设置窗口的样式
this->setStyleSheet("background-color: black;");
// 设置输入框的样式
ui->plainTextEdit->setStyleSheet("background-color: black; color: white;");
// 设置按钮的样式
ui->pushButton_light->setStyleSheet("color: white;");
ui->pushButton_dark->setStyleSheet("color: white;");
}
文件名:widget.ui
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Widget</string>
</property>
<widget class="QPlainTextEdit" name="plainTextEdit">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>781</width>
<height>511</height>
</rect>
</property>
</widget>
<widget class="QPushButton" name="pushButton_light">
<property name="geometry">
<rect>
<x>90</x>
<y>550</y>
<width>93</width>
<height>28</height>
</rect>
</property>
<property name="text">
<string>日间模式</string>
</property>
</widget>
<widget class="QPushButton" name="pushButton_dark">
<property name="geometry">
<rect>
<x>570</x>
<y>550</y>
<width>93</width>
<height>28</height>
</rect>
</property>
<property name="text">
<string>夜间模式</string>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>
QWidget_16.pro 和 main.cpp 与前面的工程逐字相同,不再重复粘贴。
代码导航
widget.ui 里有三个控件:
QPlainTextEdit,objectName是plainTextEdit,占了窗口大部分面积,用来显示和输入文字;- 两个
QPushButton,objectName是pushButton_light(文字"日间模式")和pushButton_dark(文字"夜间模式")。
widget.h 里声明了两个槽函数,对应两个按钮的点击。
widget.cpp 里两个槽函数结构对称,以日间模式为例,分三块设置:
cpp
// 设置窗口的样式
this->setStyleSheet("background-color: rgb(240, 240, 240);");
// 设置输入框的样式
ui->plainTextEdit->setStyleSheet("background-color: white; color: black;");
// 设置按钮的样式
ui->pushButton_light->setStyleSheet("color: black;");
ui->pushButton_dark->setStyleSheet("color: black;");
this->setStyleSheet(...)给窗口 设置样式,background-color是背景颜色。这里的值rgb(240, 240, 240)是一种颜色写法,具体含义马上在第 14.4 节讲;ui->plainTextEdit->setStyleSheet(...)给输入框 设置样式。除了背景色还设了color,也就是文字颜色;- 后面两句给两个按钮设置文字颜色。
夜间模式那个槽函数完全对称,只是把颜色反过来:窗口背景设为黑色,输入框背景设为黑色、文字设为白色,两个按钮的文字都设为白色。
⚠️ 易错点:按钮文字颜色一定要跟着背景一起改。 如果夜间模式下把窗口背景设成黑色、却忘了把按钮文字改成白色,按钮文字就变成了黑底黑字,直接"消失"了。
为什么窗口背景不设成纯白
这里有一个非常细致的坑。
日间模式最初写的是 background-color: white;(纯白)。但如果你把程序重新启动一次、和点过日间模式之后的界面放在一起对比,会发现两者不一样:
- 程序刚启动时,窗口背景是"白里透着一点灰";
- 点过日间模式之后,窗口背景变成了纯白。
也就是说,最初那个默认背景色并不是纯白。造成这个差别的原因,以及怎么查到它到底是什么颜色,需要先弄懂"计算机里的颜色是怎么表示的"。
14.4 计算机里的颜色是怎么表示的

先说一个前提:世界上有多少种颜色? 从计算机的角度看,是无数种。同样是"红",也能分出枣红、大红、豆沙红等等一大堆------就像口红的色号一样,虽然都叫红,但看上去差别明显。
计算机用 RGB 来表示颜色:
R是 red,红色;G是 green,绿色;B是 blue,蓝色。
这三种颜色按不同比例混合,就能构成不同的颜色。
⚠️ 易错点:别和美术课上学的"三原色"搞混。 美术课上的红黄蓝是颜料的三原色 ;这里的红绿蓝是光的三原色,两者不是一个体系。屏幕上显示的图像靠的是像素发光再照进眼睛,处理的是光,所以用的是光的三原色。
可以这样理解:屏幕上的一个像素,就是一个包含 RGB 三种颜色的小灯泡。 一个小灯泡能表示一种颜色,无数个小灯泡拼在一起,就构成了复杂绚烂的画面。
每个分量用一个字节(byte)表示。 一个字节不考虑符号时能表示 0 到 255 的整数,用十六进制写就是 0x00 到 0xFF。所以:
- 红色分量是 255,就是"红色拉满";是 0,就是"红色完全没有";
- 绿色、蓝色分量同理。
于是就有了两种等价的写法,两种在代码里都能用:
- 十进制写法 :
rgb(255, 0, 255)。意思是 R = 255(红色拉满)、G = 0(绿色没有)、B = 255(蓝色拉满); - 十六进制写法 :
#FF00FF。这里两个字符一组,一共三组分别对应 R、G、B。FF就是 255(十六进制的 FF 等于十进制的 255),所以FF表示红色拉满;00表示绿色没有;第三个FF表示蓝色拉满。
两种写法表达的是同一个颜色,区别只是进制不同。
继续补充: CSS 和 QSS 里还可以直接用一个单词来设置颜色,比如 white、black、red、green、blue、yellow 这些常见的颜色名,都能直接用。用单词写更省事,但它只能表达有限的几种颜色,前面那种默认背景色就只能用数字写出来。
14.5 用取色器查颜色
回到那个问题:窗口的默认背景色到底是多少?
答案要靠取色器。取色器是能读出屏幕上任意一点颜色值的工具。
Photoshop 里内置了取色器,但不必为了这个去装一个 PS。QQ 自带的截图功能就内置了取色器,而且几乎人人都有。用法:
- 用快捷键唤起 QQ 截图;
- 鼠标移动到你关心的位置,截图工具的提示框里会实时显示光标所在点的颜色值;
- 按
C可以直接把色号复制走。
用它去取窗口的背景色,读数是 RGB(240, 240, 240)。这正是 Qt 窗口默认的背景色------比纯白(255, 255, 255)稍暗一点,所以看起来"白里透灰"。
把日间模式里的 white 换成 rgb(240, 240, 240),重新运行,再点日间模式,背景就不会突变了,日夜切换变得自然。
💡 提示:取色器是个非常实用的工具。 看到别人界面里有个颜色很好看,想知道它到底是多少------拿取色器往上一取就得到了,完全不需要去猜。
14.6 两件要注意的事
第一件:样式里的属性名从哪查。
font-family、font-size、background-color、color 这些键名不是凭感觉写的。完整的支持列表在 Qt 的官方文档里------搜索 Qt Style Sheets 就能找到对应章节,后面讲到 QSS 时会专门展开。
第二件:拼写错误不会报错,样式会静默失效。
⚠️ 最容易踩的坑:样式字符串里的单词拼错了,没有任何报错,但样式就是不起作用。
比如把 background-color 写成了 backround-color,程序照常编译、照常运行,界面却没有任何变化。你盯着代码看半天也找不出问题,因为编译器根本不会检查这个字符串里的内容------它在你眼里只是一段普通的文本。
所以写完样式后如果发现"没生效",第一件事就是回头逐字检查属性名和值的拼写,再检查分号、冒号是不是英文半角。
💡 提示: 日常写代码时,相当一部分问题其实就是单词拼错了。这部分没有智能提示、没有拼写检查,只能靠细心。
14.7 关于颜色的一个补充
RGB 是一个比较常见的简化模型。如果从事专业的美术工作,还会关注一些更复杂的指标:
- 色域:显示器能表现的颜色范围;
- 色深:每个颜色分量的精度;
- 色准:显示器显示的颜色和真实颜色的接近程度。
做设计的人对显示器有专门要求,就是因为如果显示器和实际输出之间有色差,在电脑上配好的颜色,实际做出来就变了样。这类高规格显示器价格不便宜,普通开发者没必要追求------给你一台也未必看得出差别,专业人士则能明显区分。
不过理解 RGB 这个表示方法,在编程的很多领域都会用到。
15. 边界、限制与常见误区
把这一节当成一张"检查清单",写代码卡住的时候回来对一对。
一、禁用状态只和 enabled 有关。
按钮变灰点不动,不是 因为没连槽函数。就算 connect 连好了、槽函数写对了,控件处于禁用状态时依然不会响应任何输入。而且禁用会沿对象树向下传递,把父控件禁用,整棵子树一起失效。
二、windowTitle 和 windowIcon 只对顶层窗口有效。
给窗口内部的普通控件设置这两个属性不会生效------既不生效也不报错。写完之后发现标题或图标没变,先确认自己操作的对象是不是顶层窗口。
三、只改 X / Y 会拉伸控件,不会平移。
QRect 是"左上角 + 宽高"的整体。只改坐标,尺寸就跟着变。要平移,就用 setGeometry 的四个分量版本,把原来的宽高原样传回去。
四、构造函数里取不到窗口框架。
geometry() 和 frameGeometry() 在构造函数里得到的结果是相同的,因为当时窗口对象还没挂上窗口框架。想看到差异,必须放到界面显示之后的时机------比如某个按钮的槽函数里。另外,具体差多少像素取决于系统主题和窗口大小,不要去背数字,要记住的是用的是哪套原点。
五、位置和尺寸的单位都是像素,时间单位是 1000 进制。
字体大小、坐标、宽高的单位都是像素。setToolTipDuration 用的是毫秒,1 秒 = 1000 毫秒,不是 1024。
六、浮点数不要直接用 == 比较。
浮点数遵守 IEEE 754 标准,像 0.1 这样的十进制小数无法精确表示,加减之后必然有微小误差。正确做法是做差,判断差的绝对值是否小于可接受的误差范围。涉及金额等需要精确计算的场景,要慎重使用 float 和 double。
七、scaled 返回新副本,不修改原对象。
pixmap.scaled(w, h) 必须把返回值接住,写成 pixmap = pixmap.scaled(w, h);。直接调用而不接返回值,等于什么都没做。
八、qrc 不适合放大资源。
路径不需要文件真实存在、图片也不会丢,这是 qrc 的优势;但资源会被转成 C++ 代码一起编译进 exe,几个 GB 的视频会让编译时间和 exe 体积都急剧上涨。图片、字体这类小资源适合它。
九、添加进 qrc 的文件必须在 qrc 所在的同级目录或其子目录中。
否则添加时就会报"无效"。正确做法是先把文件拷到工程目录里,再添加。
十、路径分隔符不能用反斜杠。
"d:\rose.jpg" 里的 \r 会被当成回车符。用正斜杠 "d:/rose.jpg"、双反斜杠 "d:\\rose.jpg",或者原始字符串 R"(d:\rose.jpg)"。
十一、样式表拼写错误不会报错。
QSS 的属性名、值、分隔符全都在字符串里,编译器不检查内容。写错了样式静默失效------先查拼写,再查标点是不是英文半角。
十二、objectName 在同一个界面里必须唯一。
这个名字决定了代码里怎么拿到控件(ui->控件名)。用 Qt 自动生成的 pushButton_2、pushButton_3 在控件一多的时候极易混淆,建议改成有描述性的名字。
十三、setWindowOpacity 内部也做范围判断。
超出 0.0 到 1.0 的值设不进去,也不报错。所以边界判断从"必需"变成了"建议"------但按防御性编程的思路,建议还是写上。
16. 总结
这一节是整篇的收束。
我把 QWidget 里最常用的一批属性从头到尾走了一遍:从控件是什么 (界面元素的统称,一次封装到处复用)和控件体系的三代演进 (从手工绘图,到简单标签,再到完整控件体系)起步,先说清 QWidget 是所有控件的公共基类,再逐个展开它的核心能力------
- 位置与尺寸 这一组:
enabled控制可用与禁用(禁用会沿对象树向下传递),geometry用 x、y、width、height 四个分量描述位置和尺寸(只改坐标会拉伸,改全四个分量才是平移),window frame带来了两套坐标原点(geometry以窗口本体左上角为参考,frameGeometry以窗口框架左上角为参考); - 外观与交互 这一组:
windowTitle和windowIcon只对顶层窗口有效,windowOpacity用 0.0 到 1.0 的float控制不透明度,cursor可以换成内置形状或自定义图片(自定义时要设热点),font是一组字体属性的集合(家族、大小、粗细、倾斜、下划线、删除线),toolTip提供悬停提示并可以指定毫秒级的显示时长,focusPolicy决定控件通过什么途径获取焦点; - 资源与样式 这一组:从绝对路径的不可靠,到相对路径的有限改善,最后落到
qrc资源机制(图片被转成 C++ 数组编进 exe,路径和数据都不会丢),以及styleSheet用近似 CSS 的键值对语法改样式,顺带把颜色的 RGB 表示和十六进制写法一起理清。
更重要的是那些看起来小、实际最容易出问题 的地方:只改坐标导致控件被拉伸、构造函数里看不到窗口框架、浮点数算不准、样式拼错不报错、scaled 忘了接返回值、反斜杠被当成转义字符。这些坑我都实际跑过一遍,也都配了图。
走到这里,我们已经能给一个窗口配上位置、尺寸、标题、图标、字体、光标、提示和样式了。但窗口里放的还是按钮和标签这些最简单的控件。下一批要做的,就是把 Qt 内置的各种控件------按钮类、显示类、输入类、多元素控件、容器类控件------一个个用起来,再用布局管理器把它们自动排好,不再靠手算坐标去摆放。那一步做完,一个真正能用的界面就成形了。
标签: Qt、QWidget、常用控件、geometry、window frame、qrc 资源机制、styleSheet、QSS、focusPolicy、浮点数精度、C++