🔥 本文专栏:Qt
🌸作者主页:努力努力再努力wz



💪 今日博客励志语录 :
风的方向由天气决定,但飞翔的姿态由你的翅膀决定。你不需要活成别人期待的样板,你只需要活成自己无可替代的限量版。
思维导图

引入
在此前的学习中,我们已经知道,一个图形化界面程序通常由多个界面元素组成,例如窗口、按钮、输入框、标签等。对于这些界面元素,Qt 并不是直接用一堆零散的数据来描述,而是通过对应的类来进行抽象和封装。
这些不同的组件虽然功能各不相同,但它们本质上都属于图形化界面中的可视化对象,因此也会具备一些共同能力,例如显示在界面上、设置位置和大小、接收用户操作、响应系统事件等。Qt 会将这些通用能力向上抽象,封装到一个公共的基类中,而 QWidget 就是 Qt 中非常核心的一个可视化组件基类。
也就是说,只要一个组件继承自 QWidget,它就具备了作为图形化界面元素的基本能力,包括可视化显示能力、几何属性管理能力,以及事件接收和事件处理能力。当然,一个组件具备显示能力,并不代表它一定会立刻显示出来,它还需要被创建出来,并通过 show() 或者依附于已经显示的父窗口,最终才能真正出现在屏幕上。
在前面的内容中,我们已经认识了 QWidget 的一些基础属性,例如用于控制组件是否可用的 enabled 属性,以及和组件位置、大小相关的 geometry 相关属性。同时,我们也借助顶层窗口认识了客户区和非客户区的概念,理解了一个窗口并不只有我们自己绘制和摆放组件的区域,还可能包含由操作系统管理的标题栏、边框等区域。
接下来,本文会继续围绕 QWidget 展开,进一步认识它提供的其他常见属性,并理解这些属性是如何影响组件的显示效果和交互行为的。
QWidget 常用属性详解:从显示外观到交互行为
窗口级透明度 windowOpacity:取值范围与边界处理
本文要介绍的第一个 QWidget 属性是窗口透明度属性,也就是 windowOpacity。
从作用上看,透明度属性主要影响的是窗口的显示效果,属于组件静态展示层面的属性,而不是动态交互层面的属性。也就是说,它不会改变组件是否能够接收事件,也不会改变组件内部的交互逻辑,而是影响窗口最终显示到屏幕上时的不透明程度。
对于窗口透明度,QWidget 提供了两个常用接口:
cpp
qreal windowOpacity() const;
void setWindowOpacity(qreal level);
其中,windowOpacity() 用来获取当前窗口的透明度,setWindowOpacity() 用来设置当前窗口的透明度。
这里还需要注意 windowOpacity() 接口的返回值类型。该接口返回的并不是直接写成 float 或 double,而是 Qt 中定义的 qreal 类型。
qreal 并不是 Qt 重新封装出来的一套浮点数类型,也不是一个新的类,而是 Qt 给浮点数类型取的一个统一别名。
cpp
using qreal = double;
通常情况下,我们可以简单将其理解为 double。Qt 在接口中使用 qreal,主要是为了在不同平台或不同编译配置下保持类型使用的一致性。
透明度的有效取值范围是 0.0 ~ 1.0。其中,1.0 表示窗口完全不透明,这也是窗口的默认状态;0.0 表示窗口完全透明。也就是说,数值越大,窗口越不透明;数值越小,窗口越透明。
但是这里还需要特别注意一点:windowOpacity 针对的是窗口级别的透明度,主要作用对象是顶层窗口,而不是普通子控件的局部透明度。
也就是说,当我们对一个顶层窗口调用 setWindowOpacity() 时,改变的是整个窗口最终显示到屏幕上的透明度。由于顶层窗口内部的按钮、标签、输入框等子组件,最终都会作为该窗口画面的一部分被绘制出来,所以当顶层窗口变得透明时,窗口内部的子组件也会一起呈现出透明效果。
这里并不是说每一个子组件自身的透明度属性都被单独修改了,而是顶层窗口作为一个整体被透明化处理了。因此,窗口内部所有已经绘制出来的内容,都会受到这个窗口整体透明度的影响。
虽然普通子组件也继承自 QWidget,语法上也可以调用 setWindowOpacity() 接口,但是该属性的语义是窗口透明度,而不是普通子控件的局部透明度。因此,对普通子组件调用 setWindowOpacity(),通常不会产生让该子组件单独变透明的效果。
另外,这里还需要注意,如果传入setWindowOpacity() 的数值超出了 0.0 ~ 1.0 的范围,并不会意味着透明度会继续按照数学比例变化。
从窗口系统的语义上看,透明度只有三个区间:当数值位于 0.0 ~ 1.0 之间时,表示从完全透明到完全不透明之间的不同透明程度;当数值大于等于 1.0 时,会被当成完全不透明;当数值小于等于 0.0 时,会被当成完全透明。
也就是说,如果我们传入的值是 1.2,窗口并不会变成"比完全不透明还要更不透明",而是仍然按照完全不透明处理;如果传入的值是 -0.2,窗口也不会出现"负透明度"这种效果,而是会按照完全透明处理。
因此,虽然越界值通常不会直接导致程序崩溃,但从接口语义上看,越界后的值会被归到边界效果上。为了让代码逻辑更加清晰,也为了避免依赖不同平台底层实现的细节,实际编写代码时仍然应该主动将透明度限制在 0.0 ~ 1.0 之间。
由于透明度本质上是一个浮点数,所以在对透明度进行递增或递减时,还需要注意浮点数运算误差的问题。例如,我们每次让透明度增加 0.1,从数学上看,得到的结果应该是按照十进制小数正常累加的结果;但是在计算机底层,浮点数并不一定能够精确表示所有十进制小数,因此实际计算结果可能和我们理想中的小数运算结果存在细微误差。
因此,在实际编写代码时,如果通过按钮控制窗口透明度增加或减少,最好不要完全依赖浮点数的精确计算结果,而是应该在设置透明度之前进行边界检查。比如透明度增加后如果超过 1.0,就将其修正为 1.0;透明度减少后如果小于 0.0,就将其修正为 0.0,从而保证传入 setWindowOpacity() 的值始终处于 0.0 ~ 1.0 的有效范围内。
cpp
qreal opacity = this->windowOpacity();
opacity += 0.1;
if (opacity > 1.0)
{
opacity = 1.0;
}
this->setWindowOpacity(opacity);
鼠标光标属性 cursor:从内置使用到自定义实现
组件级鼠标光标 cursor:内置光标使用与事件分发机制
认识了窗口透明度属性之后,接下来我们继续认识 QWidget 的另一个常见属性:cursor。
从作用上看,cursor 属性影响的是鼠标光标移动到某个组件区域时的显示形状。这个属性并不会改变组件本身的显示内容,也不会改变组件原有的交互逻辑(即信号与槽对应的响应行为),而是影响用户在操作时看到的鼠标形态。
在日常使用图形化界面程序时,我们其实经常能够感受到这个属性的存在。比如,当鼠标移动到普通窗口区域时,光标通常是箭头形状;而当鼠标移动到输入框区域时,光标往往会变成类似大写字母 I 的形状,用来提示用户当前区域可以进行文本输入。
鼠标的物理移动首先会被底层硬件设备感知到,并最终通过中断的方式被 CPU 感知到,CPU 切换为内核态,然后硬件会按照自身的设备协议向操作系统上报原始输入数据。不同输入设备的底层协议并不完全相同,例如普通鼠标、蓝牙鼠标、触摸板、触摸屏等设备,它们上报数据的方式和数据格式都可能存在差异。
驱动程序的作用,就是负责和具体的硬件设备打交道。它会按照对应设备的硬件协议解析这些原始输入数据,从中得到更有意义的信息,例如鼠标在 X/Y 方向上的移动距离、鼠标按键状态、滚轮变化等。
text
鼠标:X/Y 相对位移、按键状态、滚轮变化
触摸板:手指位置、滑动方向、多指操作、点击状态
触摸屏:触点坐标、触点数量、按压状态
但是,不同设备对应的驱动程序解析出来的数据,仍然可能带有各自设备的特点。如果窗口系统或者 GUI 框架直接面对这些设备差异,那么上层处理逻辑就会变得非常复杂。因此,操作系统通常会通过输入子系统对这些输入信息进行统一整理,把它们整理成系统能够统一处理的输入事件。例如:
text
鼠标移动事件
鼠标按下事件
鼠标释放事件
滚轮事件
触摸事件
键盘按键事件
除了对事件类型进行划分之外,每一个输入事件内部还会按照统一的数据格式来组织具体的输入信息,例如:
text
X 方向移动了多少
Y 方向移动了多少
哪个按键被按下
哪个按键被释放
滚轮滚动了多少
经过输入子系统处理之后,后续的窗口系统就不需要关心底层设备具体是普通鼠标、触摸板还是其他输入设备,也不需要直接解析硬件协议,而是可以基于统一的输入事件继续处理。例如,窗口系统可以根据鼠标当前所在的位置,判断该事件属于哪个顶层窗口,并将事件分发给对应的图形化程序。
对于 Qt 程序来说,当鼠标事件被分发到程序之后,Qt 会进一步根据鼠标坐标判断它当前位于哪个 QWidget 组件区域内,并结合该组件当前生效的 cursor 属性,决定此时屏幕上应该显示哪一种鼠标光标形状。
所以可以简单概括为:驱动程序负责解析具体硬件协议,输入子系统负责统一输入事件格式,窗口系统负责定位和分发事件,而 Qt 则会在接收到事件后,将其进一步封装成具体的事件对象,分发到具体的 QWidget 对象中。
c
QMouseEvent
QWheelEvent
QEnterEvent
QHoverEvent
和前面介绍的 windowOpacity 不同,cursor 属性并不是只针对顶层窗口对象。由于普通子组件同样继承自 QWidget,因此普通按钮、输入框、标签等子组件也可以单独设置自己的鼠标光标形状。也就是说,我们可以让鼠标移动到不同组件区域时,显示不同的光标形状。
对于鼠标光标属性,QWidget 提供了几个常用接口:
cpp
QCursor cursor() const;
void setCursor(const QCursor &);
void unsetCursor();
其中,cursor() 用来获取当前组件的鼠标光标对象,setCursor() 用来设置当前组件的鼠标光标形状,unsetCursor() 则用来取消当前组件自身设置的光标属性。
虽然 setCursor() 的参数类型是 QCursor,但是在实际使用时,我们通常可以直接传入 Qt 已经提供好的光标枚举值。例如:
cpp
this->setCursor(Qt::PointingHandCursor);
这里的 Qt::PointingHandCursor 表示手型光标,常用于按钮、链接等具有点击含义的区域。
Qt 内置了很多常见的鼠标光标形状,例如箭头光标、文本输入光标、等待光标、十字光标、手型光标等。通过这些内置光标,我们就可以根据不同组件的交互语义,设置更加合适的鼠标显示效果。常见的搭配场景包括:
- 按钮、超链接等可点击区域,使用
Qt::PointingHandCursor(手型光标); - 文本编辑区域,使用
Qt::IBeamCursor(文本输入光标); - 用于调整窗口尺寸或拖拽手柄的区域,使用
Qt::SizeHorCursor、Qt::SizeVerCursor等方向性光标; - 程序正在执行耗时操作时,可以使用
Qt::WaitCursor(等待光标)来提示用户当前需要等待。
另外,如果某个组件没有单独设置 cursor 属性,那么它通常会沿用父组件的光标设置;如果父组件也没有特殊设置,那么默认情况下会使用普通的箭头光标。也就是说,cursor 属性同样存在一定的继承效果。
因此,cursor 属性可以理解为:当鼠标移动到某个 QWidget 区域时,用来控制鼠标光标显示形状的属性。它主要用于增强界面的交互提示,让用户能够通过鼠标形状的变化,感知当前区域可能支持的操作。
这里需要注意,setCursor() 并不是在鼠标每次移动时才被调用的函数,而是用来提前设置当前组件的鼠标光标属性。也就是说,调用 setCursor() 后,本质上是修改当前 QWidget 对象中维护的 cursor 属性。
当鼠标移动到某个 Qt 窗口区域内时,操作系统窗口系统会先将鼠标事件分发给对应的 Qt 程序。Qt 程序在接收到鼠标事件后,会根据鼠标当前所在的位置判断它具体落在哪一个 QWidget 组件区域内,然后结合该组件当前生效的 cursor 属性,通知底层窗口系统切换为对应的鼠标光标形状。
最终,Qt 会通过底层平台接口通知窗口系统更新当前屏幕上显示的鼠标光标。也就是说,setCursor() 负责设置组件的光标属性,而真正的光标显示更新,则是在鼠标进入或位于该组件区域时,由 Qt 和底层窗口系统配合完成的。
这里可以顺便补充一点:虽然鼠标移动是一种比较频繁的输入行为,但这并不意味着鼠标每发生一个极小的物理位移,系统都会完整执行一次从硬件到 Qt 组件处理的全过程。
在实际系统中,鼠标设备会按照一定频率上报移动变化,而不是对连续移动过程中的每一个细微位置都单独上报一次。操作系统接收到这些输入信息之后,会根据鼠标当前所在的位置,将相关事件交给对应的图形化程序处理。
对于 Qt 程序来说,当鼠标移动到某个窗口区域内时,Qt 会根据鼠标坐标判断它当前位于哪个 QWidget 组件区域,并结合该组件的 cursor 属性,决定当前应该显示哪一种鼠标光标形状。
因此,鼠标移动事件虽然比较频繁,但它不是以无限细粒度直接作用到 Qt 程序上的。操作系统和 Qt 框架会按照图形界面程序的处理方式完成这类基础输入事件的处理,所以不会因为鼠标频繁移动就导致程序无法正常运行。
Qt Designer 设置与自定义鼠标光标:从资源加载到尺寸缩放
除了通过 C++ 代码设置组件的鼠标光标形状之外,我们也可以直接在 Qt Designer 中进行设置。
在 Qt Designer 中选中某个组件后,可以在右侧属性栏中找到 cursor 属性。通过这个属性,我们可以直接从 Qt 已经内置好的光标形状中选择对应类型。比如普通箭头光标、文本输入光标、手型光标、等待光标等,都可以通过可视化属性面板直接完成设置。

如果只是给某个组件设置一个固定的内置光标形状,那么使用 Qt Designer 会更加直观和方便。因为通过代码设置光标时,我们需要记住或者查询对应的枚举值,例如 Qt::PointingHandCursor、Qt::IBeamCursor 等;而在 Qt Designer 中,我们可以直接通过属性栏进行选择,不需要额外记忆这些枚举常量。
不过,代码方式也有自己的使用场景,如果 Qt 内置的光标形状无法满足需求,我们也可以使用自定义光标图片。
在使用自定义图片之前,通常会先通过 .qrc 文件将图片资源加入到 Qt 的资源系统中。需要注意的是,.qrc 文件本身并不是直接保存图片数据,而是用来描述项目中包含哪些资源文件,以及这些资源文件在 Qt 资源系统中的虚拟路径。
在程序构建时,rcc 工具会读取 .qrc 文件,并根据其中描述的资源信息生成对应的资源代码。这样程序运行时,就可以通过类似 :/cursor/my_cursor.png 这样的虚拟路径访问到对应的图片资源,而不需要依赖外部文件路径。
当我们通过资源路径创建 QPixmap 对象时,Qt 会根据这个虚拟路径找到对应的图片资源,并将图片文件内容加载成一个适合屏幕显示的 QPixmap 对象。
这里需要区分一下 QPixmap 和 QImage 的侧重点。QImage 更偏向于图像数据的读取、保存以及像素级处理,因此如果我们想直接访问或修改图片中的像素数据,通常更适合使用 QImage。而 QPixmap 更偏向于图像在界面上的显示,它的底层数据通常会按照窗口系统或图形系统更适合显示的方式进行组织。
也就是说,如果我们的目标是对图片进行像素级处理,那么应该优先考虑 QImage;而如果我们的目标是将图片显示到界面上,或者将图片作为图标、鼠标光标等界面资源使用,那么使用 QPixmap 会更加符合 Qt 的设计。
得到 QPixmap 对象之后,就可以用它构造一个 QCursor 对象。在自定义鼠标光标的场景中,QCursor 可以理解为一个鼠标光标对象。它会基于前面加载得到的 QPixmap 构造出来,其中 QPixmap 用来提供光标显示所需要的图片资源,而热点位置则是在构造 QCursor 时额外指定的热点位置
这里的热点位置可以理解为光标图片中的"真正指向点"。一张自定义光标图片可能有一定的宽度和高度,但系统并不会把整张图片都当成鼠标的位置,而是需要通过热点坐标确定图片中的哪一个像素点代表真正的鼠标位置。
因此,热点位置会影响两个方面:一方面,它决定了鼠标点击时真正生效的位置。例如箭头光标真正用于点击的位置通常是箭头尖端,而不是整张图片的中心;另一方面,它也会影响窗口系统和 Qt 对鼠标当前位置的判断。也就是说,判断鼠标是否进入某个窗口或组件区域时,依据的是热点位置对应的屏幕坐标,而不是整张光标图片覆盖到的区域。因此,在使用自定义光标图片时,如果有需要,也可以在构造 QCursor 时指定热点坐标。
cpp
// 通过 Qt 资源系统加载自定义鼠标光标图片
QPixmap pixmap(":/cursor/my_cursor.png");
// 使用 QPixmap 构造 QCursor 对象
// 后面两个参数分别表示热点位置的 x 坐标和 y 坐标
// 也就是说,真正发生点击的位置,对应到图片中的 (0, 0) 这个像素点
QCursor cursor(pixmap, 0, 0);
// 将自定义光标设置到当前组件上
this->setCursor(cursor);
最后,再调用 setCursor() 将这个 QCursor 对象设置到对应的 QWidget 组件上。这样当鼠标移动到该组件区域时,Qt 就会根据组件当前的 cursor 属性,让屏幕上显示我们自定义的鼠标光标形状。
其次需要注意的是,如果我们使用 .qrc 中加载的图片作为自定义鼠标光标,那么这张图片本身的尺寸会影响最终光标显示出来的大小。
也就是说,自定义光标并不会自动变成系统默认鼠标光标那样的合适尺寸。如果原始图片本身比较大,那么将它设置为鼠标光标之后,屏幕上显示出来的光标也可能会显得过大,从而影响界面的使用体验。因此,在使用自定义光标图片之前,通常需要先对图片进行适当缩放。
这里可以使用 QPixmap 提供的 scaled() 函数得到一张缩放后的图片。例如:
cpp
QPixmap pixmap(":/cursor/my_cursor.png");
// 将图片缩放到 32x32 范围内,并保持原始宽高比,避免图片变形
QPixmap scaledPixmap = pixmap.scaled(
32,
32,
Qt::KeepAspectRatio,
Qt::SmoothTransformation
);
// 使用缩放后的 QPixmap 构造自定义鼠标光标
QCursor cursor(scaledPixmap, 0, 0);
// 设置当前组件的鼠标光标
this->setCursor(cursor);
在这段代码中,32, 32 表示希望将图片缩放到指定的宽度和高度范围内;Qt::KeepAspectRatio 表示在缩放过程中保持原始图片的宽高比,避免图片被强行拉伸变形;Qt::SmoothTransformation 表示使用更加平滑的缩放方式,让缩放后的光标显示效果更加自然。
需要注意的是,scaled() 并不会直接修改原来的 QPixmap 对象,而是返回一个缩放后的新 QPixmap 对象。因此,在构造 QCursor 时,应该使用缩放后的 scaledPixmap,而不是原始的 pixmap。
所以,自定义鼠标光标的基本流程可以概括为:先通过资源路径加载图片,再根据需要对图片进行缩放,随后使用缩放后的 QPixmap 构造 QCursor 对象,最后通过 setCursor() 将其设置到对应的 QWidget 组件上。
字体属性 font:QFont 对象与静态/动态设置
根据上文,我们已经认识了如何改变鼠标光标的形状属性。除了能够改变鼠标光标的显示形状之外,QWidget 还允许我们改变组件中文本的显示效果,例如字体、字号、是否加粗、是否斜体等,这就和 font 属性有关。
font 属性主要用于控制组件中文本的字体表现。比如对于 QLabel、QPushButton、QLineEdit 这类会显示文本的组件来说,设置 font 属性后,就可以改变它们中文字的显示样式。需要注意的是,font 控制的是字体相关属性,例如字体族、字号、粗细、斜体、下划线等;而文本颜色、背景颜色、边框等效果并不属于 QFont 的职责范围,通常需要通过其他方式进行设置。
在代码中,如果想要设置组件的字体属性,通常需要先创建一个 QFont 对象。可以把 QFont 理解为一个字体属性集合对象,我们对字体的各种设置,例如字体名称、字号、是否加粗等,都可以先设置到这个 QFont 对象中,然后再通过 setFont() 一次性设置给对应组件。
例如:
cpp
QFont font;
// 设置字体名称
font.setFamily("Microsoft YaHei");
// 设置字号
font.setPointSize(14);
// 设置是否加粗
font.setBold(true);
// 设置到对应组件上
ui->pushButton->setFont(font);
这样做的好处是,我们不需要在组件上分别调用很多接口去修改字体的各个属性,而是先把字体相关设置统一封装到 QFont 对象中,最后再整体设置给组件。
这里还需要注意字体粗细这个属性。字体粗细虽然也可以从数值等级的角度理解,但它和前面介绍的透明度并不完全一样。透明度通常是 0.0 ~ 1.0 之间的连续浮点数,可以通过数值递增或递减来表现不同程度的透明效果;而在实际界面开发中,字体粗细通常不需要进行这么细粒度的连续调整,很多时候只需要区分"普通"和"加粗"两种状态即可。
因此,QFont 提供了一个更加直观的 setBold() 接口。该接口接收一个布尔值:当参数为 true 时,表示将字体设置为加粗;当参数为 false 时,表示取消加粗,恢复为普通粗细。
例如:
cpp
QFont font;
font.setPointSize(14);
font.setBold(true);
ui->label->setFont(font);
当然,如果确实需要更细粒度地控制字体粗细,也可以使用 setWeight() 设置具体的字重等级。但对于大多数普通界面文本来说,setBold() 已经足够表达"是否加粗"这一需求。
cpp
QFont::Thin // 100
QFont::Light // 300
QFont::Normal // 400
QFont::Medium // 500
QFont::Bold // 700
QFont::Black // 900
font.setWeight(QFont::Bold);
另外,对于 font 这类偏向静态界面显示效果的属性,通常也可以直接在 Qt Designer 中进行设置。在 Qt Designer 中选中某个组件后,可以在右侧属性栏中找到 font 属性,然后通过可视化面板设置字体、字号、是否加粗、是否斜体等信息。
如果某个按钮、标签或者输入框的字体样式在程序运行过程中不会发生变化,那么直接在 Qt Designer 中设置会更加直观和方便,也更容易观察界面的最终显示效果。

不过,如果字体样式需要根据程序运行状态动态变化,就更适合通过 C++ 代码来完成。例如,当用户点击某个按钮后,将某个提示文本设置为加粗;或者在程序进入某种状态后,动态改变某个标签的字号或字体样式。这类运行时变化的效果,就需要在代码中创建或修改 QFont 对象,然后再通过 setFont() 设置到对应组件上。
因此,font 属性的设置方式可以简单理解为:如果是固定的静态字体样式,使用 Qt Designer 更加直观方便;如果是运行过程中需要动态改变的字体效果,则更适合通过 C++ 代码来设置。
键盘焦点 focus:focusPolicy 策略与运行时状态
根据上文,我们已经认识了如何设置组件中文本显示的字体属性。接下来要认识的是另一个和组件交互行为密切相关的概念:焦点。
在正式介绍焦点之前,我们可以先从日常使用图形化界面的经验出发。比如,当我们用鼠标点击某一个输入框之后,接下来在键盘上输入的内容会进入这个输入框,而不会进入界面上的其他输入框。这个现象背后就和焦点机制有关。
所谓焦点,主要用来表示当前哪一个组件正在接收键盘输入。对于一个图形化界面程序来说,界面上可能同时存在多个输入框、按钮或其他组件,但是同一时刻,键盘输入通常需要有一个明确的接收对象。这个当前能够接收键盘输入的组件,就可以认为是当前拥有焦点的组件。
这个概念可以类比为 Linux 终端中的前台进程组。在一个终端会话中,键盘输入不会同时交给所有进程,而是会交给当前终端的前台进程组。
比如默认情况下,我们在终端中输入命令时,输入内容会被 bash 接收。bash 获取到用户输入的命令后,会先判断该命令是否属于内置命令。如果是内置命令,就由 bash 自己执行;如果不是内置命令,bash 通常会通过 fork() 创建子进程,并在子进程中通过 exec() 将其替换成目标程序。
如果这个外部命令以前台方式运行,那么 bash 会把该命令所在的进程组设置为终端的前台进程组。此时,终端的键盘输入就会转而交给这个前台进程组。比如执行 vim 后,键盘输入会进入 vim,而不是继续作为命令交给 bash 解析。等 vim 退出后,前台控制权再回到 bash,终端输入也重新由 bash 接收。
Qt 中的焦点机制也有类似的思想:一个界面中可能存在多个组件,但键盘输入通常只会交给当前拥有焦点的组件。也就是说,Linux 终端中通过"前台进程组"决定键盘输入交给谁,而 Qt 界面中则通过"焦点组件"决定键盘输入交给谁。
认识了焦点的基本概念之后,接下来就需要看一下 Qt 中关于焦点机制的常用接口。
首先,QWidget 提供了 setFocusPolicy() 接口,用来设置当前组件的焦点策略。所谓焦点策略,就是这个组件是否允许获得焦点,以及可以通过什么方式获得焦点。
常见的焦点策略有:
cpp
Qt::NoFocus // 不能获得焦点
Qt::TabFocus // 可以通过 Tab 键获得焦点
Qt::ClickFocus // 可以通过鼠标点击获得焦点
Qt::StrongFocus // 既可以通过 Tab 键获得焦点,也可以通过鼠标点击获得焦点
Qt::WheelFocus // 在 StrongFocus 基础上,还可以通过鼠标滚轮相关行为获得焦点
例如,如果我们希望某个输入框既可以通过鼠标点击获得焦点,也可以通过 Tab 键切换获得焦点,就可以这样设置:
cpp
ui->lineEdit->setFocusPolicy(Qt::StrongFocus);
如果我们希望某个组件不能获得键盘焦点,可以设置为:
cpp
ui->pushButton->setFocusPolicy(Qt::NoFocus);
除了设置焦点策略之外,Qt 还提供了 setFocus() 接口,用来主动让某个组件获得焦点。例如:
cpp
ui->lineEdit->setFocus();
这句代码的含义是:让 lineEdit 主动成为当前拥有键盘焦点的组件。这样后续键盘输入就会优先进入这个输入框。
如果我们想判断某个组件当前是否拥有焦点,可以调用 hasFocus():
cpp
if (ui->lineEdit->hasFocus())
{
qDebug() << "lineEdit 当前拥有焦点";
}
else
{
qDebug() << "lineEdit 当前没有焦点";
}
如果想从整个程序角度查询当前到底是哪一个组件拥有焦点,可以使用:
cpp
QWidget *widget = QApplication::focusWidget();
if (widget != nullptr)
{
qDebug() << "当前拥有焦点的组件是:" << widget;
}
这里的 QApplication::focusWidget() 返回的是当前拥有键盘焦点的 QWidget 对象。如果当前没有组件拥有焦点,则可能返回空指针。
另外,如果想让某个组件主动失去焦点,可以调用:
cpp
ui->lineEdit->clearFocus();
不过需要注意,clearFocus() 只是让当前组件失去焦点,并不一定代表整个界面之后就没有焦点组件。Qt 可能会根据窗口中的其他组件和焦点策略,将焦点转移给其他合适的组件。
下面给出一个简单示例:
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QApplication>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 设置 lineEdit 可以通过鼠标点击和 Tab 键获得焦点
ui->lineEdit->setFocusPolicy(Qt::StrongFocus);
// 设置 pushButton 不接收键盘焦点
ui->pushButton->setFocusPolicy(Qt::NoFocus);
// 主动让 lineEdit 获得焦点
ui->lineEdit->setFocus();
// 判断 lineEdit 当前是否拥有焦点
if (ui->lineEdit->hasFocus())
{
qDebug() << "lineEdit 当前拥有焦点";
}
// 查询当前整个程序中拥有焦点的 QWidget
QWidget *focusWidget = QApplication::focusWidget();
qDebug() << "当前焦点组件:" << focusWidget;
}
Widget::~Widget()
{
delete ui;
}

需要注意的是,setFocus() 并不适合在窗口尚未显示时立即验证效果。
在常见的 Qt 程序中,窗口的创建和显示通常是分开的。比如在 main.cpp 中,一般会先创建窗口对象,再调用 show() 显示窗口,最后进入事件循环:
cpp
Widget w;
w.show();
return a.exec();
也就是说,程序执行顺序通常是:
- 先执行
Widget构造函数; - 构造函数执行完成;
- 调用
w.show()显示窗口; - 进入事件循环。
因此,在 Widget 构造函数执行阶段,窗口通常还没有真正显示出来,也还没有成为可以接收键盘输入的活动窗口。这个时候即使调用了 setFocus(),也不一定能立刻让目标组件真正获得焦点,所以此时再调用 hasFocus() 或 QApplication::focusWidget(),可能得不到预期结果。
如果需要在窗口显示之后主动设置焦点,可以将 setFocus() 放到窗口显示之后执行,或者借助 QTimer::singleShot(0, ...) 将设置焦点的操作延后到事件循环开始之后再执行。
这里的重点是:构造函数阶段只是对象创建完成了,但窗口还没真正进入可接收输入的状态。
setFocusPolicy() 决定组件是否有资格获得焦点,以及可以通过什么方式获得焦点;setFocus() 用来主动让某个组件获得焦点;hasFocus() 用来判断某个组件当前是否已经拥有焦点;QApplication::focusWidget() 则可以从整个程序层面查询当前真正拥有焦点的组件。
所以关于焦点接口,可以先这样理解:
cpp
setFocusPolicy() // 设置组件获取焦点的策略
setFocus() // 主动让组件获得焦点
hasFocus() // 判断组件当前是否拥有焦点
clearFocus() // 让组件主动失去焦点
QApplication::focusWidget() // 查询当前拥有焦点的组件
不过需要注意的是,focus 和 focusPolicy 不是同一个概念。focus 表示的是组件当前是否已经拥有焦点,它更像是一个运行时状态;而 focusPolicy 表示的是组件是否允许获得焦点,以及可以通过什么方式获得焦点,它更像是一个策略设置。
也就是说,focusPolicy 是每个 QWidget 自己的属性,用来说明这个组件能不能获得焦点,以及它可以通过鼠标点击、Tab 键切换等哪种方式获得焦点。而"当前焦点到底在哪个组件上",则不是由某一个组件自己单独决定的,而是由 Qt 在更高层统一维护的运行时状态。
这是因为同一时刻键盘输入通常只能交给一个明确的组件。如果每个组件都各自维护一份"自己是否拥有焦点"的状态,就可能出现多个组件都认为自己拥有焦点的混乱情况。因此,Qt 会在应用程序或窗口层面记录当前真正拥有键盘焦点的组件。当键盘输入到来时,再将键盘事件分发给这个组件。
对于某个具体组件来说,可以通过 hasFocus() 判断自己当前是否拥有焦点;而对于整个 Qt 程序来说,也可以通过 QApplication::focusWidget() 查询当前真正拥有键盘焦点的组件。
因此,可以简单理解为:focusPolicy 是组件自己的能力声明,而当前焦点对象是 Qt 框架维护的运行时状态。前者决定组件是否有资格获得焦点,以及通过什么方式获得焦点;后者决定当前键盘输入真正应该交给哪个组件。
组件获得焦点的方式有很多,其中一种常见方式就是鼠标点击。当我们物理点击鼠标时,底层硬件会感知到这次输入行为,并通过驱动程序和操作系统输入子系统将其整理成鼠标输入事件。随后,窗口系统会根据鼠标点击的位置,将该事件分发给对应的图形化程序。对于 Qt 程序来说,Qt 在收到鼠标点击事件之后,会继续根据鼠标坐标判断这次点击落在哪一个 QWidget 组件区域内,并将鼠标事件交给对应组件处理。
这里需要注意,鼠标点击事件并不等价于直接触发槽函数。槽函数之所以会被调用,是因为某个组件在处理鼠标事件之后,发出了对应的信号,并且我们提前通过 connect() 将这个信号和槽函数建立了连接。
对于焦点机制来说,鼠标点击事件进入组件后,Qt 会结合该组件的 focusPolicy 判断它是否允许通过鼠标点击获得焦点。如果该组件允许通过点击获得焦点,那么这次点击就可能导致当前键盘焦点切换到该组件上。比如当我们点击一个输入框时,输入框获得焦点,后续键盘输入就会进入这个输入框。
而对于按钮这类组件来说,鼠标点击事件除了可能影响焦点之外,还会被按钮自身的事件处理逻辑继续处理。当按钮判断这次按下和释放构成一次有效点击时,才会发出 clicked() 信号,进而调用与之连接的槽函数。
因此,鼠标点击事件进入 Qt 之后,可能产生不同层面的结果:一方面,它可能导致组件获得焦点;另一方面,它也可能触发组件自身的交互语义,例如按钮点击。焦点切换和信号槽调用是两个不同层面的机制,不能简单混为一谈。
工具提示 toolTip:提示文本与显示时长
接下来要认识的是另一个常见的 QWidget 属性:toolTip。
在正式介绍 toolTip 之前,我们可以先从日常使用图形化界面的经验出发。比如,当我们把鼠标悬停在某个按钮、图标或者输入框上时,界面上可能会弹出一个小的文本提示框,用来说明这个组件的作用或者使用方式。这个临时弹出的提示文本,就是工具提示,也就是 toolTip。
toolTip 的主要作用是帮助用户快速理解组件的功能。比如一个按钮上可能只显示了一个简单图标,如果用户不清楚这个图标代表什么,就可以通过鼠标悬停触发工具提示,从而看到更加具体的文字说明。

对于 toolTip 属性,QWidget 提供了对应的访问接口:
cpp
QString toolTip() const;
void setToolTip(const QString &);
其中,toolTip() 用来获取当前组件的提示文本,setToolTip() 用来设置当前组件的提示文本。
这里需要注意,setToolTip() 接收的是一个字符串参数,这个字符串就是工具提示框中要显示的内容。例如:
cpp
ui->pushButton->setToolTip("点击后切换到夜间模式");
ui->plainTextEdit->setToolTip("这里可以输入多行文本内容");
设置完成之后,当鼠标悬停在对应组件上时,Qt 就会在合适的位置显示这个提示文本。也就是说,setToolTip() 只是负责给组件设置提示内容,至于提示框什么时候显示、显示在什么位置,则由 Qt 的工具提示机制根据鼠标悬停状态自动处理。
除了设置提示内容之外,QWidget 还提供了 setToolTipDuration() 接口,用来设置工具提示框显示的持续时间:
cpp
void setToolTipDuration(int msec);
该接口接收一个整数参数,单位是毫秒。例如:
cpp
ui->pushButton->setToolTip("点击后切换到夜间模式");
// 设置 tooltip 显示 3000 毫秒,也就是 3 秒
ui->pushButton->setToolTipDuration(3000);
这里的 3000 表示提示框显示出来之后会停留大约 3 秒。
需要注意的是,setToolTipDuration() 控制的是提示框显示出来之后停留多久,而不是鼠标悬停多久之后才弹出提示框。如果将提示时间设置为 -1,则表示使用 Qt 的默认显示时长。也就是说,我们不主动指定提示框停留多久,而是交给 Qt 按照默认规则处理。
因此,关于 toolTip 属性,可以简单理解为:setToolTip() 用来设置提示框中显示的文本内容,setToolTipDuration() 用来设置提示框显示出来之后的停留时间。通过这两个接口,我们就可以给组件添加简单的悬浮说明,从而提升界面的可理解性和易用性。
另外,toolTip 也可以直接在 Qt Designer 中设置。选中某个组件后,可以在右侧属性栏中找到 toolTip 属性,然后填写对应的提示文本。如果提示内容是固定不变的,使用 Qt Designer 设置会更加直观;如果提示内容需要根据程序运行状态动态变化,则更适合通过 C++ 代码调用 setToolTip() 来修改。
组件样式 styleSheet:从 QSS 语法到主题切换实战
根据上文,我们已经认识了 Qt 中的焦点机制。接下来要认识的是组件的样式设置,也就是 styleSheet 属性。
在前面的内容中,我们已经学习过 font 属性。font 主要用于设置组件中文本的字体表现,例如字体名称、字号、是否加粗、是否斜体等。也就是说,font 更关注的是"文字本身应该以什么字体显示"。
而 styleSheet 则更偏向于控制组件整体的外观样式。例如组件的背景颜色、文本颜色、边框样式、圆角效果等,都可以通过 styleSheet 来进行设置。因此,可以简单理解为:font 主要负责文本字体相关的属性,而 styleSheet 则负责更广泛的组件外观样式设置。
设置组件外观样式的方式通常也有两种:一种是通过 C++ 代码设置,另一种是通过 Qt Designer 在属性栏中设置。
如果通过代码设置,就需要调用 setStyleSheet() 接口,并向其中传入一个样式表字符串。这个字符串使用的是 Qt Style Sheet,也就是 QSS。QSS 的语法风格借鉴了前端开发中的 CSS,但它并不是完整的 CSS,而是 Qt 用来描述组件外观的一套样式规则。
在最简单的情况下,QSS 字符串可以写成 属性名: 属性值; 的形式。例如:
cpp
ui->pushButton->setStyleSheet("background-color: red;");
这里的 background-color 表示要设置的样式属性,red 表示该属性对应的值。通过这种方式,我们就可以修改组件的背景颜色。
如果需要设置多个样式属性,也可以在同一个字符串中连续书写多个样式声明:
cpp
ui->pushButton->setStyleSheet(
"background-color: red;"
"color: white;"
"border-radius: 8px;"
);
在这段代码中,background-color 用来设置按钮的背景颜色,color 用来设置按钮中文本的颜色,border-radius 用来设置按钮边角的圆角效果。
除了通过代码设置之外,我们也可以直接在 Qt Designer 中选中组件,然后在右侧属性栏中找到 styleSheet 属性进行编辑。对于固定不变的静态样式来说,使用 Qt Designer 会更加直观;而如果样式需要根据程序运行状态动态变化,则更适合通过 C++ 代码来设置。
其次,在设置组件外观样式时,我们还可以设置组件中文本的颜色。在 QSS 中,文本颜色通常通过 color 属性进行设置。例如:
cpp
ui->label->setStyleSheet("color: red;");
这里的 red 表示红色。除了使用这种颜色名称之外,我们也可以使用更加精确的颜色表示方式,例如 RGB 颜色值。
之所以可以通过 RGB 表示颜色,是因为显示器最终是通过大量像素点发光来呈现图像的。每个像素点显示什么颜色,本质上可以由红、绿、蓝三个颜色通道共同决定。这里的红、绿、蓝就是光的三原色,也就是 RGB。
从像素数据的角度来看,一张图片本质上可以看成一个像素矩阵。矩阵中的每一个位置对应一个像素点,而每个像素点都需要记录自己的颜色信息。
在常见的 RGB 表示方式中,一个像素点通常可以使用 3 个字节来描述,其中每个字节分别对应红、绿、蓝三个颜色通道。由于一个字节可以表示 0 ~ 255 之间的数值,所以红、绿、蓝三个通道都可以用一个 0 ~ 255 的数值来表示其亮度强弱。数值越大,表示该颜色通道的光越强;数值越小,表示该颜色通道的光越弱。
例如,rgb(255, 0, 0) 表示红色,说明红色通道的亮度最大,而绿色和蓝色通道的亮度为 0;rgb(0, 0, 0) 表示黑色,说明三个颜色通道都不发光;rgb(255, 255, 255) 表示白色,说明红、绿、蓝三个颜色通道都达到最大亮度。
cpp
ui->label->setStyleSheet("color: rgb(255, 0, 0);"); // 红色
ui->label->setStyleSheet("color: rgb(0, 255, 0);"); // 绿色
ui->label->setStyleSheet("color: rgb(0, 0, 255);"); // 蓝色
ui->label->setStyleSheet("color: rgb(0, 0, 0);"); // 黑色
ui->label->setStyleSheet("color: rgb(255, 255, 255);"); // 白色
也就是说,如果只使用 red、blue 这类颜色名称,我们选择的是一些比较固定的颜色;而如果使用 rgb() 这种形式,就可以更加细粒度地控制红、绿、蓝三个通道的比例,从而得到更加丰富的颜色效果。
比如深蓝色和浅蓝色,本质上就可以通过调整 RGB 中各个通道的数值来实现:
cpp
ui->label->setStyleSheet("color: rgb(0, 0, 139);"); // 深蓝色
ui->label->setStyleSheet("color: rgb(135, 206, 250);"); // 浅蓝色
因此,styleSheet 可以理解为 Qt 提供的一套组件外观样式描述机制。我们可以通过它设置组件的背景颜色、文本颜色、边框、圆角等外观效果,从而让界面显示不再完全依赖默认样式,而是可以根据自己的需求进行定制。
为了更直观地理解 styleSheet 的作用,这里可以实现一个简单的日间模式和夜间模式切换效果。
在这个示例中,我们可以在界面中准备一个 QPlainTextEdit 文本编辑框,再准备两个按钮,分别用于切换日间模式和夜间模式。当点击日间模式按钮时,让窗口整体背景变成浅色,组件背景也变成浅色,同时文本颜色设置为深色;当点击夜间模式按钮时,让窗口整体背景变成深色,组件背景也变成深色,同时文本颜色设置为浅色。
这里需要注意,如果我们想要改变的是整个窗口以及窗口内部多个组件的整体显示效果,那么就不应该只对某一个组件调用 setStyleSheet()。比如如果只对 plainTextEdit 调用 setStyleSheet(),那么改变的只是这个文本编辑框本身的样式,而窗口背景和按钮样式并不会一起发生变化。
因此,这里可以直接对当前窗口对象调用 setStyleSheet():
cpp
this->setStyleSheet(...);
这样我们就可以在一个 QSS 字符串中,分别为窗口、文本编辑框和按钮设置样式。例如:
cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
connect(ui->dayButton, &QPushButton::clicked, this, &Widget::setDayMode);
connect(ui->nightButton, &QPushButton::clicked, this, &Widget::setNightMode);
// 默认先设置为日间模式
setDayMode();
}
Widget::~Widget()
{
delete ui;
}
void Widget::setDayMode()
{
this->setStyleSheet(
"QWidget {"
" background-color: rgb(245, 245, 245);"
" color: rgb(30, 30, 30);"
"}"
"QPlainTextEdit {"
" background-color: rgb(255, 255, 255);"
" color: rgb(30, 30, 30);"
" border: 1px solid rgb(200, 200, 200);"
"}"
"QPushButton {"
" background-color: rgb(235, 235, 235);"
" color: rgb(30, 30, 30);"
" border: 1px solid rgb(180, 180, 180);"
" border-radius: 6px;"
" padding: 6px 12px;"
"}"
);
}
void Widget::setNightMode()
{
this->setStyleSheet(
"QWidget {"
" background-color: rgb(30, 30, 30);"
" color: rgb(230, 230, 230);"
"}"
"QPlainTextEdit {"
" background-color: rgb(40, 40, 40);"
" color: rgb(230, 230, 230);"
" border: 1px solid rgb(80, 80, 80);"
"}"
"QPushButton {"
" background-color: rgb(55, 55, 55);"
" color: rgb(230, 230, 230);"
" border: 1px solid rgb(90, 90, 90);"
" border-radius: 6px;"
" padding: 6px 12px;"
"}"
);
}


这里的 QWidget { ... } 用来设置窗口以及普通组件的整体样式,比如窗口背景颜色和默认文本颜色;QPlainTextEdit { ... } 用来单独设置文本编辑框的背景颜色、文本颜色和边框;QPushButton { ... } 用来设置按钮的背景颜色、文本颜色、边框、圆角以及内边距。
需要注意的是,setStyleSheet() 接收的是一个完整的字符串。上面的代码虽然把 QSS 拆成了多行书写,但每一段本质上都是 C++ 字符串字面量。在 C++ 中,相邻的字符串字面量会在编译阶段自动拼接成一个完整字符串,所以最终传入 setStyleSheet() 的仍然是一个整体样式字符串。
这里还需要注意 setStyleSheet() 的作用范围。由于 QPushButton、QPlainTextEdit 等组件本身也继承自 QWidget,所以它们也可以调用 setStyleSheet() 设置样式。但是,如果对某一个具体子组件调用 setStyleSheet(),通常只会影响这个子组件自身,不能反向修改父窗口的样式,也不能直接修改它的兄弟组件样式。
而如果对父窗口调用 setStyleSheet(),则可以在父窗口的范围内,通过 QSS 选择器统一设置窗口自身以及内部子组件的样式。比如可以在同一个样式表中分别设置 QWidget、QPushButton、QPlainTextEdit 的背景颜色和文本颜色。
因此,可以简单理解为:子组件设置样式时,主要控制自己;父组件设置样式时,可以统一管理自己以及它内部子组件的显示效果。对于日间模式和夜间模式这种整体主题切换场景,更适合对顶层窗口调用 setStyleSheet(),然后在样式表中分别描述不同类型组件的样式。
另外,QSS 中的 color 属性一般表示文本颜色,而 background-color 表示背景颜色。例如:
cpp
"background-color: rgb(30, 30, 30);"
"color: rgb(230, 230, 230);"
前者表示将背景设置为深色,后者表示将文本设置为浅色。通过这种方式,我们就可以在槽函数中调用不同的 setStyleSheet() 代码,从而实现日间模式和夜间模式的动态切换。
在实际设置界面颜色时,如果我们不想手动凭感觉去写 RGB 数值,也可以借助取色器来获取颜色值。
所谓取色器,就是用来从屏幕上的某一个像素点读取颜色信息的工具。例如,我们可以使用 QQ 截图、微信截图、Snipaste 或者 PowerToys 中的 Color Picker 功能,将鼠标移动到某个界面区域,然后直接获取该位置对应的颜色值。
取色器通常会给出多种颜色表示方式,例如:
text
RGB: 245, 245, 245
HEX: #F5F5F5
对于 Qt 的 styleSheet 来说,这两种形式都可以使用。例如:
cpp
this->setStyleSheet("background-color: rgb(245, 245, 245);");
或者:
cpp
this->setStyleSheet("background-color: #F5F5F5;");
这样做的好处是,我们可以从已有的软件界面、设计图或者截图中直接提取颜色,而不是自己反复调整 RGB 数值。比如我们想实现一个比较柔和的日间模式背景色,就可以从某些常见软件的浅色背景中取色;如果想实现夜间模式,也可以从深色主题界面中提取合适的背景颜色和文本颜色。
因此,取色器可以理解为辅助我们确定颜色值的工具。它不会改变 styleSheet 的使用方式,只是帮助我们更方便地得到合适的 RGB 或十六进制颜色值,然后再将这些颜色值写入 QSS 字符串中。

结语
那么这就是本篇文章的全部内容,我会持续更新,希望你能够多多关注,如果本文有帮助到你的话,还请三连加关注,你的支持就是我创作的最大动力!
