4. QWidget与窗口系统:一切UI的起点
说实话,QWidget 是整个 Qt 界面体系的基石。你看到的每个按钮、每个对话框、每个输入框,本质上都是 QWidget 的子类。我刚开始学 Qt 时,总觉得这玩意儿就是个「空壳子」,后来才发现——正是这个空壳子,撑起了整个图形界面世界。
这一章,咱们就把它彻底搞明白。
4.1 QWidget 基类:窗口的「基因」
QWidget 是所有用户界面对象的基类。它定义了窗口最基本的属性:位置、大小、可见性、焦点、样式等等。说白了,你看到的任何 UI 组件,都是从 QWidget 派生出来的。
核心事实:QWidget 本身就是一个「空窗口」。它可以独立存在,也可以作为其他组件的容器。
我习惯把 QWidget 比作一块画布。你可以在上面画按钮、画标签、画表格。但画布本身也有自己的属性——它有多大?在屏幕哪个位置?是否可见?这些就是 QWidget 要管的事。
// 创建一个最简单的窗口
QWidget *window = new QWidget();
window->setWindowTitle("我的第一个窗口");
window->resize(400, 300);
window->show();
嗯,就这么几行代码,一个窗口就出来了。你可能会问:「它怎么这么简单?」其实,Qt 在背后做了大量工作——事件循环、绘制系统、窗口管理器交互……这些都被封装在 QWidget 里了。
4.2 窗口属性:控制窗口的「长相」
窗口属性决定了窗口的外观和行为。我列几个最常用的,你感受一下:
| 属性 | 类型 | 说明 |
|---|---|---|
| windowTitle | QString | 窗口标题栏的文字 |
| windowIcon | QIcon | 窗口左上角的图标 |
| windowOpacity | qreal | 窗口透明度(0.0~1.0) |
| windowState | Qt::WindowState | 最大化、最小化、全屏等状态 |
| geometry | QRect | 窗口的位置和大小(不含标题栏) |
| frameGeometry | QRect | 窗口的位置和大小(含标题栏和边框) |
这里有个坑,我当年踩过。geometry() 和 frameGeometry() 的区别,很多人搞混。简单说:geometry 是「画布」的大小,frameGeometry 是「画框」的大小。你设置窗口大小时,如果用了 resize(),它设置的是 geometry,不是 frameGeometry。
注意:在窗口显示之前,frameGeometry() 返回的值可能不准确。因为窗口管理器还没给窗口分配边框和标题栏。我曾经在构造函数里用 frameGeometry() 做计算,结果窗口显示后全乱了。
4.3 窗口坐标系统:屏幕上的「定位」
Qt 的坐标系统,说白了就是一套定位规则。原点在左上角,X 轴向右,Y 轴向下。这个和很多图形系统一样,没什么特别的。
但有一点要注意:坐标是相对于父窗口的。如果你有一个按钮放在窗口里,按钮的坐标 (10, 20) 表示它距离窗口左上角 10 像素、距离顶部 20 像素。不是屏幕坐标。
// 获取全局坐标
QPoint globalPos = widget->mapToGlobal(QPoint(0, 0));
// 从全局坐标映射到本地坐标
QPoint localPos = widget->mapFromGlobal(globalPos);
这两个函数我经常用。比如做右键菜单时,需要把鼠标的全局坐标转换成窗口内的坐标。你想想看,如果不用 mapToGlobal,菜单可能会弹出到屏幕外面去。
4.4 父子对象关系:Qt 的「家族树」
这是 Qt 最巧妙的设计之一。每个 QWidget 都可以有父对象和子对象。父对象负责管理子对象的生命周期——父对象被销毁时,所有子对象也会被自动销毁。
我刚开始做项目时,总觉得手动 delete 对象很麻烦。后来发现,只要把对象挂到父对象上,Qt 会自动帮你清理。这大大减少了内存泄漏的风险。
QWidget *parent = new QWidget();
QPushButton *btn = new QPushButton("点击我", parent); // btn 的父对象是 parent
// 当 parent 被销毁时,btn 也会被自动销毁
父子关系还影响坐标系统。子对象的坐标是相对于父对象的。如果你移动父对象,所有子对象会跟着移动。这个特性在做复杂界面时特别有用——你只需要移动一个容器,里面的所有组件都跟着走。
小技巧:可以用 parent() 函数获取父对象,用 children() 获取所有子对象列表。调试时,我经常用 dumpObjectTree() 打印整个对象树,一目了然。
4.5 窗口类型与标志:定制窗口的「身份」
窗口类型决定了窗口在窗口管理器中的行为。Qt 提供了几种预定义的窗口类型:
- Qt::Widget:普通窗口,默认类型。如果它有父对象,就嵌入到父对象中;如果没有父对象,就独立显示。
- Qt::Window:独立窗口,有标题栏和边框。无论有没有父对象,它都独立显示。
- Qt::Dialog:对话框,通常用于模态交互。
- Qt::Popup:弹出式窗口,点击其他区域会自动关闭。
- Qt::Tool:工具窗口,通常比主窗口小,没有任务栏图标。
窗口标志则更精细地控制窗口行为。比如:
// 创建一个无边框窗口
QWidget *window = new QWidget();
window->setWindowFlags(Qt::Window | Qt::FramelessWindowHint);
// 创建一个始终置顶的工具窗口
QWidget *tool = new QWidget();
tool->setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
这里我要提醒你:窗口标志的组合效果,有时候和你想的不一样。我曾经想做一个无边框的对话框,用了 Qt::Dialog | Qt::FramelessWindowHint,结果窗口显示不出来。后来才发现,某些窗口管理器对无边框对话框有限制。解决办法是改用 Qt::Window 代替 Qt::Dialog。
4.6 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张图:
这张图把本章的核心知识点串起来了。你看,QWidget 在中心,四个方向分别对应窗口属性、坐标系统、父子关系和窗口类型。每个分支下面还有具体的细节。我建议你把这个图记在脑子里,以后遇到窗口相关的问题,先想想它属于哪个分支。
4.7 避坑指南
最后,分享几个我实际项目中踩过的坑:
- 窗口显示位置不对:我曾经在构造函数里用 move() 设置窗口位置,结果窗口显示在屏幕左上角。后来发现,是因为窗口还没被窗口管理器处理,move() 被忽略了。解决办法是在 show() 之后调用 move(),或者用 QTimer::singleShot 延迟执行。
- 父子对象导致的内存问题:如果你手动 delete 了一个子对象,而父对象不知道,父对象销毁时会再次 delete 它,导致崩溃。所以,要么完全交给 Qt 管理,要么在删除子对象后调用 removeChild()。
- 窗口标志冲突:某些窗口标志组合在一起会产生奇怪的效果。比如 Qt::Popup 和 Qt::FramelessWindowHint 一起用,在某些 Linux 桌面环境下会导致窗口无法获得焦点。我建议你每次设置窗口标志后,都实际测试一下。
嗯,这一章的内容就到这里。QWidget 虽然基础,但它是你构建复杂界面的根基。把这一章吃透了,后面的内容你会学得轻松很多。