3. QWidget基础:从零开始理解Qt的“画布”

说实话,QWidget 是 Qt 里最基础、也最容易被忽视的一个类。我刚入行那会儿,总觉得它就是个空窗口,没什么好学的。直到有一次,我需要做一个完全自定义的仪表盘控件——没有现成的 QWidget 子类可用,所有绘制都得自己来。那时候我才意识到,不懂 QWidget 的底层机制,根本做不了真正的自定义控件。

这一章,我们就来把 QWidget 的底裤扒干净。我会从类层次结构讲起,再到窗口属性、事件系统,最后带你手写第一个 paintEvent。嗯,内容不少,但都是干货。

3.1 QWidget 的“家族树”

先看一张图,你就明白 QWidget 在整个 Qt 世界里的位置了。

QObject QPaintDevice QWidget(核心画布) QFrame QLineEdit QPushButton QSlider QLabel QTextEdit QCheckBox QComboBox 图:QWidget 类层次结构(简化版)

看到没?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 的事件系统说白了就是一个“分发-处理”模型:

  1. 系统产生事件(鼠标按下、键盘敲击等)
  2. Qt 把事件包装成 QEvent 对象
  3. 调用 QObject::event() 进行分发
  4. 最终交给对应的虚函数处理(如 mousePressEvent())

你想想看,这就像公司里的流程:前台收到快递(事件),分发给对应部门(event() 分发),部门经理处理(具体的事件处理函数)。

最常见的几个事件处理函数:

  • paintEvent() —— 绘制控件外观
  • mousePressEvent() —— 鼠标按下
  • mouseMoveEvent() —— 鼠标移动
  • resizeEvent() —— 窗口大小改变
  • keyPressEvent() —— 键盘按键
重点: 事件处理函数默认是空的。你不重写它,控件就不会有任何反应。很多初学者以为继承 QWidget 后自动就有点击效果——不存在的,你得自己写。

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);
}
注意: paintEvent 里不要做耗时操作!不要 new 对象!不要调用 update()!否则会导致无限重绘,界面卡死。我曾经在 paintEvent 里加载了一张大图片,结果窗口一缩放就卡成幻灯片。

运行这段代码,你会看到一个从蓝色渐变到红色的圆。虽然简单,但这就是自定义控件的起点。后面我们会在这个基础上,加入鼠标交互、动画效果、甚至完整的仪表盘。

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() 触发重绘
可可精品资料库