3. QWidget基础:从零开始理解Qt的“画布”
说实话,QWidget 是 Qt 里最基础、也最容易被忽视的一个类。我刚入行那会儿,总觉得它就是个空窗口,没什么好学的。直到有一次,我需要做一个完全自定义的仪表盘控件——没有现成的 QWidget 子类可用,所有绘制都得自己来。那时候我才意识到,不懂 QWidget 的底层机制,根本做不了真正的自定义控件。
这一章,我们就来把 QWidget 的底裤扒干净。我会从类层次结构讲起,再到窗口属性、事件系统,最后带你手写第一个 paintEvent。嗯,内容不少,但都是干货。
3.1 QWidget 的“家族树”
先看一张图,你就明白 QWidget 在整个 Qt 世界里的位置了。
看到没?QWidget 继承自 QObject 和 QPaintDevice。这意味着两件事:
- QObject 给了它信号槽、事件循环、对象树管理的能力
- QPaintDevice 给了它“被绘制”的能力——说白了,就是能往上面画东西
我个人习惯把 QWidget 想象成一张白纸。QObject 是这张纸的“行为逻辑”,QPaintDevice 是这张纸的“物理材质”。你想想看,没有材质你怎么画?没有逻辑你怎么响应鼠标?两者缺一不可。
3.2 窗口属性设置:别小看这几行代码
很多新手写 Qt 程序,上来就 new 一个 QWidget,然后 show() 就完事了。但实际项目中,窗口属性设置往往是第一个坑。
我举个例子。有一次,我需要做一个无边框的悬浮窗口。直接设置 setWindowFlags(Qt::FramelessWindowHint) 就完事了?没那么简单。你会发现窗口拖不动、点不到、甚至任务栏里都找不到它。
下面是我常用的几个属性设置,建议你记下来:
| 属性 | 作用 | 我的经验 |
|---|---|---|
setWindowTitle() |
设置标题栏文字 | 调试时一定要设,不然一堆窗口分不清 |
setWindowFlags() |
设置窗口类型/样式 | 无边框窗口记得配合 setAttribute(Qt::WA_TranslucentBackground) |
setAttribute() |
设置窗口属性(如透明、删除时释放) | Qt::WA_DeleteOnClose 我几乎每个窗口都用 |
setFixedSize() |
固定窗口大小 | 做自定义控件时常用,防止布局乱跑 |
setStyleSheet() |
设置样式表 | 能不用代码画边框就别用,QSS 快得多 |
setAttribute(Qt::WA_TranslucentBackground);
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
我曾经在这上面折腾了整整一下午,最后发现是顺序问题——先设 flags 再设 attribute 才有效。
3.3 事件系统入门:你的控件怎么“活”起来?
QWidget 本身是“死”的。你把它放在那里,它不会自己动。要让控件响应鼠标点击、键盘输入、窗口缩放,就得靠事件系统。
Qt 的事件系统说白了就是一个“分发-处理”模型:
- 系统产生事件(鼠标按下、键盘敲击等)
- Qt 把事件包装成 QEvent 对象
- 调用
QObject::event()进行分发 - 最终交给对应的虚函数处理(如
mousePressEvent())
你想想看,这就像公司里的流程:前台收到快递(事件),分发给对应部门(event() 分发),部门经理处理(具体的事件处理函数)。
最常见的几个事件处理函数:
paintEvent()—— 绘制控件外观mousePressEvent()—— 鼠标按下mouseMoveEvent()—— 鼠标移动resizeEvent()—— 窗口大小改变keyPressEvent()—— 键盘按键
3.4 paintEvent 初探:第一次亲手画控件
好了,终于到了最激动人心的部分——paintEvent。这是自定义控件的灵魂所在。
paintEvent 什么时候被调用?
- 窗口第一次显示时
- 窗口大小改变时
- 窗口被遮挡后重新暴露时
- 你手动调用
update()时
注意,千万不要直接调用 paintEvent()!正确的做法是调用 update(),Qt 会自动合并多次 update() 请求,避免频繁重绘。我见过有人在一个循环里连续调用 repaint(),结果 CPU 直接飙到 100%——嗯,那就是我刚开始学 Qt 时干的事。
下面是一个最简单的自定义控件,画一个带渐变色的圆:
// MyWidget.h
#include <QWidget>
class MyWidget : public QWidget
{
Q_OBJECT
public:
explicit MyWidget(QWidget *parent = nullptr);
protected:
void paintEvent(QPaintEvent *event) override;
};
// MyWidget.cpp
#include "MyWidget.h"
#include <QPainter>
#include <QLinearGradient>
MyWidget::MyWidget(QWidget *parent)
: QWidget(parent)
{
setFixedSize(200, 200);
}
void MyWidget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing); // 抗锯齿,必开
// 画背景
painter.fillRect(rect(), Qt::white);
// 创建渐变
QLinearGradient gradient(0, 0, 200, 200);
gradient.setColorAt(0.0, Qt::blue);
gradient.setColorAt(1.0, Qt::red);
// 画圆
painter.setBrush(gradient);
painter.setPen(Qt::NoPen);
painter.drawEllipse(20, 20, 160, 160);
}
运行这段代码,你会看到一个从蓝色渐变到红色的圆。虽然简单,但这就是自定义控件的起点。后面我们会在这个基础上,加入鼠标交互、动画效果、甚至完整的仪表盘。
3.5 避坑指南:我踩过的几个坑
- 坑1: 忘记调用 QWidget::paintEvent(event) 基类实现。如果你只是想在原有控件上加一点绘制,记得先调用父类的 paintEvent,否则原有内容会消失。
- 坑2: 在 paintEvent 外使用 QPainter。QPainter 只能在 paintEvent 内使用,其他地方画了也白画。
- 坑3: 窗口属性设置顺序。先 setWindowFlags,再 setAttribute,最后 show()。顺序错了,某些属性可能不生效。
- 坑4: 事件处理函数里忘记调用基类。比如重写 keyPressEvent 却不调用 QWidget::keyPressEvent,会导致 Tab 键切换焦点等功能失效。
嗯,这一章的内容就到这里。QWidget 虽然基础,但它是所有自定义控件的根基。把这一章吃透了,后面学 QPainter 高级绘制、自定义样式、动画框架都会轻松很多。
- QWidget 继承 QObject + QPaintDevice,兼具逻辑与绘制能力
- 窗口属性设置要注意顺序和组合
- 事件系统是控件“活起来”的关键
- paintEvent 是自定义绘制的入口,记住用 update() 触发重绘