第3章 Qt基础控件(上):QWidget、QLabel、QPushButton、QLineEdit、QTextEdit,信号与槽机制入门
3.1 一切从QWidget开始
Qt里有个「万物之源」——QWidget。说白了,你看到的每个窗口、每个按钮、每个输入框,本质上都是QWidget的子类。我刚开始学Qt时,以为QWidget就是个空窗口,后来才发现它承载了太多基础能力。
QWidget提供了什么?位置、大小、可见性、鼠标事件、键盘事件……几乎所有可视化组件的基础行为都在这。你想想看,一个按钮能响应点击,一个标签能显示文字,这些能力都是从QWidget继承来的。
核心要点:QWidget是所有用户界面对象的基类。它本身就是一个矩形区域,可以容纳子控件。
我个人习惯,每次新建Qt项目时,第一件事就是创建一个QWidget作为主窗口。虽然Qt提供了QMainWindow,但很多小工具用QWidget反而更轻量。
// 创建一个最简单的窗口
#include <QApplication>
#include <QWidget>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget window;
window.setWindowTitle("我的第一个Qt窗口");
window.resize(400, 300);
window.show();
return app.exec();
}
这段代码跑起来,你会看到一个400x300像素的空白窗口。嗯,这里要注意:resize()设置的是客户区大小,不包括标题栏。我在项目中遇到过有人把窗口设成800x600,结果标题栏一加,整个窗口超出了屏幕范围……
3.2 QLabel:不只是显示文字
QLabel,标签控件。很多人觉得它太简单了,不就是显示一行字吗?其实它的能力远不止于此。
QLabel可以显示:
- 纯文本(普通文字)
- 富文本(HTML格式)
- 图片(QPixmap)
- 动画(QMovie)
我曾经用QLabel做了一个简单的状态提示区,显示系统运行状态。一开始只用纯文本,后来发现用HTML可以加颜色、加粗、甚至加链接,用户体验瞬间提升了一个档次。
// QLabel的多种用法
QLabel *label1 = new QLabel("普通文本", this);
QLabel *label2 = new QLabel("<b>粗体文本</b>", this);
label2->setTextFormat(Qt::RichText); // 启用富文本
// 显示图片
QLabel *label3 = new QLabel(this);
QPixmap pixmap(":/images/icon.png");
label3->setPixmap(pixmap);
小技巧:QLabel默认不支持富文本,需要调用setTextFormat(Qt::RichText)。或者直接用setText()传入HTML字符串,Qt会自动识别。
还有一个坑:QLabel默认不会自动换行。如果你的文字太长,它会直接截断。解决办法是调用setWordWrap(true)。我记得有一次做数据采集软件的日志显示,忘了开自动换行,结果长日志只显示了一半,排查了半天才发现是这个问题。
3.3 QPushButton:点击的魔力
按钮,交互的核心。QPushButton是Qt里最常用的按钮控件。它的用法很简单,但背后的信号与槽机制才是精髓。
// 创建按钮
QPushButton *btn = new QPushButton("点击我", this);
btn->move(100, 100); // 移动到指定位置
// 连接信号与槽
connect(btn, &QPushButton::clicked, this, [=]() {
qDebug() << "按钮被点击了!";
});
这里用到了Lambda表达式。我个人习惯在简单场景下用Lambda,代码更紧凑。但如果逻辑复杂,还是建议单独写一个槽函数,方便维护。
QPushButton还有一些实用功能:
setEnabled(false):禁用按钮,灰色不可点击setIcon():给按钮加图标setShortcut():设置快捷键
避坑指南:我曾经在数据采集项目中,把「开始采集」按钮的快捷键设成了Ctrl+S,结果和「保存」功能冲突了。用户一按Ctrl+S,数据没保存,采集却开始了……后来我改成Ctrl+Shift+S才解决。快捷键一定要全局检查,避免冲突。
3.4 QLineEdit:单行输入的秘密
QLineEdit,单行文本输入框。看起来简单,但实际项目中坑不少。
常用功能:
setPlaceholderText():设置占位提示文字setEchoMode():设置回显模式(密码输入时用Password)setValidator():设置输入验证器(限制只能输入数字等)textChanged信号:文本变化时触发
// 带验证的输入框
QLineEdit *lineEdit = new QLineEdit(this);
lineEdit->setPlaceholderText("请输入IP地址");
// 只允许输入数字和点
QRegularExpressionValidator *validator =
new QRegularExpressionValidator(QRegularExpression("[0-9.]*"), this);
lineEdit->setValidator(validator);
// 监听输入变化
connect(lineEdit, &QLineEdit::textChanged, this, [=](const QString &text) {
qDebug() << "当前输入:" << text;
});
嗯,这里要注意:setValidator只是限制了用户输入,但不会阻止程序通过setText()设置非法内容。所以后端还是要做校验。我在项目中遇到过用户通过粘贴的方式绕过了验证器,后来加了一层textChanged的二次校验才搞定。
3.5 QTextEdit:多行文本的利器
QTextEdit,多行文本编辑器。和QLineEdit不同,它支持富文本、图片、表格等复杂内容。在数据采集系统中,我常用它来做日志显示或配置编辑。
// 创建多行文本编辑框
QTextEdit *textEdit = new QTextEdit(this);
textEdit->setPlainText("这是普通文本");
textEdit->setHtml("<h2>这是HTML标题</h2><p>这是段落</p>");
// 追加内容
textEdit->append("新的一行");
// 获取内容
QString plain = textEdit->toPlainText();
QString html = textEdit->toHtml();
性能提醒:QTextEdit在显示大量日志时,如果每行都调用append(),内存会暴涨。我建议限制最大行数,比如超过1000行时删除前面的内容。或者改用QPlainTextEdit,它更轻量。
还有一个实用技巧:QTextEdit可以设置只读模式。在数据采集系统中,历史数据展示区就应该设为只读,防止用户误修改。
textEdit->setReadOnly(true); // 设为只读
3.6 信号与槽:Qt的灵魂
终于到了这个核心概念。信号与槽,说白了就是「当A发生了某件事,自动通知B去做某件事」。这是Qt实现对象间通信的机制,也是Qt区别于其他框架的最大特色。
为什么需要信号与槽?你想想看,一个按钮被点击后,你需要更新状态栏、保存数据、刷新界面……如果每个地方都手动调用,代码会乱成一团。信号与槽让你可以「声明式」地定义这些关系。
核心理解:信号(Signal)是「事件通知」,槽(Slot)是「响应函数」。一个信号可以连接多个槽,一个槽也可以被多个信号触发。
基本语法:
// 方式一:使用函数指针(推荐,编译期检查)
connect(sender, &Sender::signalName, receiver, &Receiver::slotName);
// 方式二:使用Lambda
connect(sender, &Sender::signalName, this, [=]() {
// 处理逻辑
});
// 方式三:旧式字符串语法(不推荐,运行时检查)
connect(sender, SIGNAL(signalName()), receiver, SLOT(slotName()));
我个人强烈推荐方式一,编译期就能检查出信号和槽是否匹配。方式三在Qt5之后已经标记为过时了,但很多老项目还在用。
自定义信号与槽:
// 在类定义中声明
class DataCollector : public QObject
{
Q_OBJECT // 必须加这个宏
public:
explicit DataCollector(QObject *parent = nullptr);
signals:
void dataReceived(int value); // 信号,只需声明,不需实现
public slots:
void processData(int value); // 槽,需要实现
};
// 使用
DataCollector *collector = new DataCollector(this);
connect(collector, &DataCollector::dataReceived,
collector, &DataCollector::processData);
// 发射信号
emit collector->dataReceived(42);
避坑指南:我曾经在项目中忘记在类定义里加Q_OBJECT宏,结果信号与槽死活连不上。编译没问题,运行时就是没反应。排查了半天才发现是这个问题。记住:所有使用信号与槽的类,必须在头文件里加上Q_OBJECT。
信号与槽的几种连接方式:
| 连接方式 | 说明 | 适用场景 |
|---|---|---|
| 直接连接 | 信号发射时立即调用槽 | 同一线程 |
| 队列连接 | 信号发射后,进入事件循环再调用槽 | 不同线程 |
| 自动连接 | Qt自动判断使用直接还是队列 | 默认方式,推荐 |
还有一个细节:信号与槽的参数类型必须匹配,但槽的参数可以比信号少。比如信号发射int, QString,槽可以只接收int,多余的参数会被忽略。这个特性我在做数据采集时经常用——信号带了很多数据,但某个槽只需要其中一部分。
3.7 本章知识体系
下面这张图展示了本章的核心知识结构,我把它画成了SVG,方便你理解各个控件之间的关系:
从这张图可以看得很清楚:所有控件都继承自QWidget,而信号与槽机制则是连接这些控件的「桥梁」。没有信号与槽,控件之间就是孤立的;有了它,整个界面就「活」了起来。
好了,这一章的内容就到这里。记住:控件是骨架,信号与槽是血脉。两者结合,才能构建出真正可用的交互界面。