☰
Qt 的原理及使用(7)——Q组件:从 QWidget 到自定义控件的完整实践
2026/10/2 6:04:48 网站建设 项目流程

1. 从 QWidget 说起:为什么每个 Qt 控件都绕不开它

如果你刚开始写 Qt 桌面程序,大概率会遇到一个疑问:QPushButton、QLabel、QLineEdit 这些控件到底是怎么组织起来的?它们之间有没有共同的“祖先”?答案是有的,而且这个祖先就是 QWidget。理解 QWidget,是理解整个 Qt Q组件体系的第一步,也是从“拖控件”进阶到“写自定义控件”的必经之路。

QWidget 是 Qt 中所有用户界面对象的基类。它本身可以是一个窗口,也可以是一个嵌入到其他窗口里的子控件。换句话说,你在界面上看到的每一个按钮、标签、输入框,甚至整个主窗口,本质上都是一个 QWidget 或者 QWidget 的子类。它负责的事情包括:绘制自己、响应鼠标和键盘事件、管理几何位置、处理焦点、承载布局等。

我习惯把 QWidget 理解成一块“空白画布”。它默认什么都不画,但提供了完整的交互框架。你继承它之后,可以决定这块画布长什么样、怎么响应点击、怎么和外界通信。Qt 内置的那些控件,其实就是官方在这块画布上画好了各种图案,并封装了常用行为。

从继承关系上看,大致是这样的:

QObject └── QPaintDevice └── QWidget ├── QLabel ├── QPushButton ├── QLineEdit ├── QFrame │ └── QAbstractScrollArea └── ...

QObject 提供了信号槽机制和对象树管理,QPaintDevice 提供了绘制能力,QWidget 把这两者结合起来,再加上事件系统,就成了所有可视控件的根基。这个结构决定了 Qt 控件的两个核心特征:一是可以通过信号槽互相通信,二是可以通过父子关系自动管理内存和生命周期。

对于桌面端 GUI 开发来说,掌握 QWidget 的意义在于:当内置控件不能满足需求时,你可以自己继承 QWidget,重写绘制和事件处理,做出完全自定义的组件。比如一个圆形的状态指示灯、一个带刻度的滑块、一个可以拖拽排序的列表项,这些都可以通过子类化 QWidget 实现。

这一篇会从 QWidget 的常用属性讲起,然后梳理内置组件的使用方式,最后给出一个完整的自定义控件示例,并说明如何编译运行验证。过程中会涉及窗口透明度、光标、字体、提示、焦点策略这些实际开发中高频用到的点。

2. QWidget 常用属性与内置组件实战:窗口透明度、光标、字体、提示与焦点策略

QWidget 提供了一大批属性方法,覆盖外观、交互、状态等各个方面。与其死记硬背,不如按使用场景来分类。下面挑几个最常用的属性,结合可运行的代码说明。

2.1 窗口透明度 windowOpacity

windowOpacity()返回当前窗口的不透明度,类型是 float,范围 0.0 到 1.0。0.0 表示全透明,1.0 表示完全不透明。setWindowOpacity(float n)用来设置这个值。

这个属性只对顶层窗口生效,对嵌入的子控件无效。一个典型用法是做淡入淡出效果,或者做一个半透明的悬浮面板。

在界面上放两个按钮,objectName 分别为pushButton_add和pushButton_sub,然后在 widget.cpp 里写槽函数:

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); }

运行后点击减号,窗口逐渐变透明,能透过窗口看到后面的内容;点击加号又慢慢恢复。控制台会打印每次的 opacity 值。

这里有个坑要注意:C++ 的 float 遵循 IEEE 754 标准,运算会有精度误差。所以1 - 0.1的结果并不是精确的 0.9,而是 0.90000004 之类。如果你用==去比较浮点数,很容易出问题。判断边界时用>=和<=更稳妥。

2.2 光标 cursor

cursor()返回当前控件的 QCursor 对象,setCursor(const QCursor&)设置鼠标悬停在该控件上时显示的光标形状。另外QGuiApplication::setOverrideCursor()可以设置全局光标,会覆盖所有控件的单独设置。

在 Qt Designer 里,选中按钮后直接在右侧属性编辑区把 cursor 改成“等待”,运行后鼠标悬停就能看到沙漏形状。用代码设置也很简单:

#include <QPushButton> Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); QPushButton* button = new QPushButton(this); button->resize(100, 50); button->move(100, 100); button->setText("这是一个按钮"); button->setCursor(QCursor(Qt::WaitCursor)); }

Qt 内置的光标形状由Qt::CursorShape枚举定义,常用的有:

枚举值含义
Qt::ArrowCursor标准箭头
Qt::CrossCursor十字
Qt::WaitCursor沙漏/等待
Qt::IBeamCursor文本输入竖线
Qt::PointingHandCursor手型,常用于链接
Qt::ForbiddenCursor禁止
Qt::OpenHandCursor张开的手
Qt::ClosedHandCursor握紧的手

如果要自定义光标,可以用 QPixmap 加载图片,再构造 QCursor:

#include <QPixmap> Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); QPixmap pixmap(":/huaji.png"); pixmap = pixmap.scaled(64, 64); QCursor cursor(pixmap, 2, 2); this->setCursor(cursor); }

这里(2, 2)是“热点”坐标,也就是鼠标点击时实际生效的位置。图片需要先加入 qrc 资源文件,前缀设为/。热点一般设在图片的视觉中心,否则点击位置会偏移。

2.3 字体 font

font()返回 QFont 对象,setFont(const QFont&)设置字体。QFont 的常用属性包括 family(字体家族,如“微软雅黑”“仿宋”)、pointSize(字号)、weight(粗细,0 到 99)、bold(加粗)、italic(倾斜)、underline(下划线)、strikeOut(删除线)。

Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); ui->label->setText("这是一段文本"); QFont font; font.setFamily("仿宋"); font.setPointSize(20); font.setBold(true); font.setItalic(true); font.setUnderline(true); font.setStrikeOut(true); ui->label->setFont(font); }

运行后 label 上的文字会变成仿宋、20 号、加粗、倾斜、带下划线和删除线。实际项目里不建议同时开这么多效果,这里只是为了演示属性。

需要注意的是,字体家族名要和系统里已安装的字体匹配。如果写了系统没有的字体名,Qt 会回退到默认字体,不会报错,但效果和预期不符。跨平台开发时,最好用QFontDatabase查询可用字体,或者用通用家族名如sans-serif。

2.4 提示 toolTip

setToolTip()设置鼠标悬停时的提示文字,setToolTipDuration()设置提示显示时长,单位毫秒。toolTip 只是给用户看的,代码里一般不需要读取。

Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); ui->pushButton_yes->setToolTip("这个是 yes 按钮"); ui->pushButton_yes->setToolTipDuration(3000); ui->pushButton_no->setToolTip("这个是 no 按钮"); ui->pushButton_no->setToolTipDuration(10000); }

运行后鼠标停在按钮上会弹出提示,时间到后自动消失。toolTip 支持简单的 HTML 格式,比如<b>加粗</b>、<br>换行,做复杂提示时很有用。

2.5 焦点策略 focusPolicy

焦点指的是当前能接收键盘输入的那个控件。比如你在输入框里打字,那个输入框就拥有焦点。setFocusPolicy(Qt::FocusPolicy)决定控件如何获取焦点。

Qt::FocusPolicy的取值:

  • Qt::NoFocus:不接收键盘焦点,无法通过 Tab 或鼠标选中。
  • Qt::TabFocus:只能通过 Tab 键获取焦点。
  • Qt::ClickFocus:只能通过鼠标点击获取焦点。
  • Qt::StrongFocus:Tab 和鼠标点击都可以,这是默认值。
  • Qt::WheelFocus:在 StrongFocus 基础上增加鼠标滚轮获取焦点,很少用。

在界面上放四个 QLineEdit,默认都是 StrongFocus,可以用鼠标点击或 Tab 切换。把第二个改成 NoFocus,它就完全无法被选中,也就无法输入。改成 TabFocus,只能 Tab 选中,鼠标点不进去。改成 ClickFocus,只能鼠标点,Tab 跳过它。

这个属性对输入框、单选框、复选框这类需要键盘交互的控件特别重要。做自定义控件时,如果你希望它能被 Tab 聚焦,记得设置合适的 focusPolicy,并在paintEvent里根据hasFocus()画出焦点框,否则用户用键盘操作时会迷失位置。

2.6 内置组件与布局嵌套

Qt 内置组件大致分几类:显示类(QLabel、QProgressBar)、输入类(QLineEdit、QTextEdit、QSpinBox)、按钮类(QPushButton、QCheckBox、QRadioButton)、容器类(QGroupBox、QTabWidget、QScrollArea)、列表类(QListWidget、QTableWidget、QTreeWidget)。

这些组件都继承自 QWidget,所以前面讲的属性它们都有。实际开发中,很少单独放一个控件,而是用布局管理器把它们组织起来。常用的布局有 QVBoxLayout(垂直)、QHBoxLayout(水平)、QGridLayout(网格)、QFormLayout(表单)。

布局可以嵌套。比如一个设置面板,外层是垂直布局,里面每一行是一个水平布局,左边标签右边输入框。嵌套布局能让界面自适应窗口大小,比手动 setGeometry 靠谱得多。

QVBoxLayout* mainLayout = new QVBoxLayout(this); QHBoxLayout* row1 = new QHBoxLayout(); row1->addWidget(new QLabel("用户名:")); row1->addWidget(new QLineEdit()); QHBoxLayout* row2 = new QHBoxLayout(); row2->addWidget(new QLabel("密码:")); row2->addWidget(new QLineEdit()); mainLayout->addLayout(row1); mainLayout->addLayout(row2); mainLayout->addWidget(new QPushButton("登录"));

这段代码创建了一个两行输入加一个按钮的界面,窗口缩放时控件会跟着调整。布局的父子关系由addLayout和addWidget自动建立,不需要手动 delete。

3. 自定义控件:继承 QWidget 并重写绘制与事件

内置组件够用的时候,直接用就好。但当需求变成“画一个圆形的状态灯”“做一个带刻度的旋钮”“实现一个可拖拽的卡片”时,就需要自定义控件了。自定义控件的标准做法是继承 QWidget,重写paintEvent负责绘制,重写鼠标或键盘事件负责交互,通过信号槽对外通信。

下面做一个完整的例子:一个圆形的状态指示灯,支持设置颜色,点击时切换开关状态并发出信号。

3.1 头文件 StatusLight.h

#ifndef STATUSLIGHT_H #define STATUSLIGHT_H #include <QWidget> #include <QColor> class StatusLight : public QWidget { Q_OBJECT public: explicit StatusLight(QWidget *parent = nullptr); void setColor(const QColor& color); QColor color() const; bool isOn() const; signals: void toggled(bool on); protected: void paintEvent(QPaintEvent* event) override; void mousePressEvent(QMouseEvent* event) override; QSize sizeHint() const override; private: QColor m_color; bool m_on; }; #endif // STATUSLIGHT_H

这里用到了Q_OBJECT宏,它让类支持信号槽。只要类里声明了 signals 或 slots,就必须加这个宏,否则 moc 不会生成元对象代码,编译会报 undefined reference to vtable 之类的错误。

3.2 实现文件 StatusLight.cpp

#include "StatusLight.h" #include <QPainter> #include <QMouseEvent> StatusLight::StatusLight(QWidget *parent) : QWidget(parent) , m_color(Qt::gray) , m_on(false) { setFixedSize(40, 40); setCursor(Qt::PointingHandCursor); setToolTip("点击切换状态"); } void StatusLight::setColor(const QColor& color) { m_color = color; update(); } QColor StatusLight::color() const { return m_color; } bool StatusLight::isOn() const { return m_on; } void StatusLight::paintEvent(QPaintEvent* event) { Q_UNUSED(event); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); QRect rect = this->rect().adjusted(2, 2, -2, -2); if (m_on) { painter.setBrush(m_color); painter.setPen(QPen(m_color.darker(150), 2)); } else { painter.setBrush(Qt::lightGray); painter.setPen(QPen(Qt::gray, 2)); } painter.drawEllipse(rect); } void StatusLight::mousePressEvent(QMouseEvent* event) { if (event->button() == Qt::LeftButton) { m_on = !m_on; update(); emit toggled(m_on); } QWidget::mousePressEvent(event); } QSize StatusLight::sizeHint() const { return QSize(40, 40); }

几个关键点:

paintEvent里用 QPainter 绘制。setRenderHint(QPainter::Antialiasing)开启抗锯齿,圆形边缘才平滑。adjusted(2, 2, -2, -2)把绘制区域往里缩 2 像素,避免边框被裁掉。

mousePressEvent里判断左键,切换状态,调用update()触发重绘,然后emit toggled(m_on)发出信号。update()是异步的,Qt 会在合适时机调用paintEvent,不要手动调paintEvent。

sizeHint()返回建议尺寸,布局管理器会参考它。配合setFixedSize可以固定大小。

3.3 在主窗口中使用

#include "widget.h" #include "ui_widget.h" #include "StatusLight.h" #include <QVBoxLayout> #include <QLabel> Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui->setupUi(this); StatusLight* light = new StatusLight(this); light->setColor(Qt::green); QLabel* label = new QLabel("状态:关闭", this); connect(light, &StatusLight::toggled, this, [label](bool on) { label->setText(on ? "状态:开启" : "状态:关闭"); }); QVBoxLayout* layout = new QVBoxLayout(this); layout->addWidget(light, 0, Qt::AlignCenter); layout->addWidget(label, 0, Qt::AlignCenter); }

这里用 lambda 连接信号,捕获 label 指针更新文字。运行后点击圆形指示灯,颜色在灰色和绿色之间切换,下方标签同步变化。

3.4 编译配置

如果用 qmake,在 .pro 文件里加上:

QT += widgets SOURCES += main.cpp widget.cpp StatusLight.cpp HEADERS += widget.h StatusLight.h

如果用 CMake,在 CMakeLists.txt 里:

cmake_minimum_required(VERSION 3.16) project(StatusLightDemo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) find_package(Qt6 REQUIRED COMPONENTS Widgets) add_executable(StatusLightDemo main.cpp widget.cpp StatusLight.cpp StatusLight.h widget.h ) target_link_libraries(StatusLightDemo PRIVATE Qt6::Widgets)

CMAKE_AUTOMOC ON很重要,它让 CMake 自动处理 Q_OBJECT 宏需要的 moc 编译。如果忘了开,链接时会报 vtable 相关的错误。

4. 验证请求与运行结果:从编译到交互的完整检查

代码写完之后,需要实际编译运行来验证。这一步不能省,因为 Qt 的很多问题(比如信号槽没连上、绘制不显示、moc 没生成)只有跑起来才能发现。

4.1 编译步骤

用 qmake 的话,在项目目录下执行:

qmake StatusLightDemo.pro make

Windows 上用 MinGW 的话是mingw32-make,用 MSVC 的话是nmake。用 CMake 的话:

cmake -B build -DCMAKE_PREFIX_PATH=/path/to/Qt/6.x.x/gcc_64 cmake --build build

CMAKE_PREFIX_PATH指向 Qt 安装目录,具体路径根据你的环境调整。如果 Qt 已经加入系统 PATH,可以省略这个参数。

4.2 预期运行结果

程序启动后,窗口中央有一个灰色圆形,下方文字显示“状态:关闭”。鼠标移到圆形上,光标变成手型,并弹出“点击切换状态”的提示。点击圆形,它变成绿色,文字变成“状态:开启”。再点一次,恢复灰色和“状态:关闭”。

如果圆形边缘有锯齿,检查setRenderHint(QPainter::Antialiasing)是否生效。如果点击没反应,检查mousePressEvent是否被正确重写,以及Q_OBJECT宏和 AUTOMOC 配置。

4.3 用调试输出验证信号

在 lambda 里加一行 qDebug,可以确认信号确实发出了:

connect(light, &StatusLight::toggled, this, [label](bool on) { qDebug() << "StatusLight toggled:" << on; label->setText(on ? "状态:开启" : "状态:关闭"); });

运行后点击,控制台会打印StatusLight toggled: true和StatusLight toggled: false。这是验证信号槽连接最直接的方式。

4.4 结合窗口透明度做联动

把前面讲的 windowOpacity 和自定义控件结合,可以做一个有趣的效果:点击指示灯时,同时改变窗口透明度。

connect(light, &StatusLight::toggled, this, [this, label](bool on) { label->setText(on ? "状态:开启" : "状态:关闭"); this->setWindowOpacity(on ? 1.0 : 0.7); });

开启时窗口完全不透明,关闭时变成 70% 不透明度。这样就把属性设置和自定义控件串起来了,也验证了信号槽的灵活性。

5. 常见报错与排查:undefined reference、moc 缺失、控件不显示

自定义控件和 QWidget 开发中,有几类错误出现频率特别高。下面按现象、原因、解决方式整理。

5.1 undefined reference to vtable for StatusLight

这是最典型的错误,几乎每个刚写 Q_OBJECT 的人都会遇到。原因通常是:类里声明了 Q_OBJECT 宏,但没有执行 moc 处理,或者 moc 生成的代码没有参与链接。

排查步骤:

第一,确认头文件里确实写了Q_OBJECT。注意它必须写在类声明的第一行,不能写在注释里,也不能写在 namespace 外面。

第二,确认构建系统开启了 moc。qmake 默认会处理,但如果你手动写了 Makefile 就可能漏掉。CMake 需要set(CMAKE_AUTOMOC ON)。

第三,如果改了头文件后报这个错,先执行一次 clean 再重新构建。有时候旧的 moc 文件残留会导致链接混乱。

make clean qmake make

CMake 的话删掉 build 目录重新来:

rm -rf build cmake -B build cmake --build build

5.2 控件不显示或绘制空白

自定义控件继承 QWidget 后,如果没有重写paintEvent,默认什么都不画,所以是空白的。这是正常行为,不是 bug。要显示内容,必须重写paintEvent并用 QPainter 绘制。

另一个常见原因是控件尺寸为 0。QWidget 默认尺寸可能是 0x0,如果没放进布局也没手动 resize,就看不见。解决办法是setFixedSize、resize,或者重写sizeHint并放进布局。

还有一种情况是绘制了但颜色和背景一样。比如在白色背景上画白色圆,自然看不见。调试时可以先用醒目的颜色,确认绘制逻辑生效后再改回来。

5.3 信号槽连接失败

如果点击控件后槽函数没执行,先检查 connect 的返回值。可以在连接后加判断:

bool ok = connect(light, &StatusLight::toggled, this, [label](bool on) { label->setText(on ? "状态:开启" : "状态:关闭"); }); qDebug() << "connect result:" << ok;

返回 false 说明连接失败。常见原因是信号或槽的签名不匹配,比如信号带 bool 参数,lambda 却没写参数,或者参数类型对不上。用函数指针语法(&StatusLight::toggled)时,编译器会做类型检查,比旧的 SIGNAL/SLOT 宏更安全。

如果运行时提示QObject::connect: Cannot connect (nullptr)::toggled(bool) to ...,说明发送者指针是空的,检查控件是否已经 new 出来。

5.4 焦点相关的问题

自定义控件默认的 focusPolicy 是Qt::NoFocus,也就是说它不会接收键盘焦点。如果你希望它能被 Tab 选中,需要setFocusPolicy(Qt::StrongFocus)。同时,在paintEvent里根据hasFocus()画一个焦点框,否则用户看不出当前焦点在哪。

if (hasFocus()) { painter.setPen(QPen(Qt::blue, 2, Qt::DashLine)); painter.drawRect(rect.adjusted(-2, -2, 2, 2)); }

这段代码在控件获得焦点时画一个蓝色虚线框,提升键盘操作的可访问性。

5.5 内存与父子关系

Qt 的对象树机制会自动管理内存:父对象销毁时,所有子对象也会被销毁。所以new QPushButton(this)之后不需要手动 delete。但如果你 new 了一个没有父对象的控件,就要自己负责释放,否则会内存泄漏。

布局里的控件会自动设置父子关系,所以layout->addWidget(button)之后,button 的父对象就是布局所在的窗口。这一点在排查“控件找不到父对象”的问题时很有用。

6. 把 QWidget 体系用起来:从内置组件到自定义控件的实践路径

走到这里,QWidget 的核心用法基本覆盖了。回头看,整个 Q组件体系其实就三层:QObject 提供信号槽和对象树,QPaintDevice 提供绘制能力,QWidget 把两者结合并加上事件系统。内置控件是官方封装好的常用组合,自定义控件是你按需组合这三层能力。

实际开发中,我的建议是:能用内置组件就用内置组件,它们经过充分测试,样式和交互都符合平台习惯。只有当内置组件确实无法满足需求时,才考虑自定义。自定义时优先继承现有的相近控件,比如做一个特殊按钮就继承 QPushButton,只重写需要改的部分,而不是从 QWidget 从零开始。

信号槽是贯穿始终的通信方式。自定义控件通过 signals 对外通知状态变化,通过 slots 接收外部指令。这种松耦合设计让控件可以独立开发和测试,也方便在不同界面里复用。

如果你在练习过程中遇到编译或运行问题,可以对照第 5 节的排查清单逐项检查。需要查阅 Qt 官方文档时,可以从接入文档入手,把环境配置和 API 调用方式理清楚。模型对话入口适合快速验证一些 API 用法和参数含义,Coding Plan 则更适合需要长期写 Qt 项目、反复调试编译的场景。把工具用顺手,自定义控件的开发效率会明显提升。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询