3. Qt基础控件(上)

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,方便你理解各个控件之间的关系:

第3章:Qt基础控件与信号槽 QWidget(基类) QLabel(标签) QPushButton(按钮) QLineEdit(单行输入) QTextEdit(多行文本) 信号与槽机制 connect(sender, signal, receiver, slot)

从这张图可以看得很清楚:所有控件都继承自QWidget,而信号与槽机制则是连接这些控件的「桥梁」。没有信号与槽,控件之间就是孤立的;有了它,整个界面就「活」了起来。

好了,这一章的内容就到这里。记住:控件是骨架,信号与槽是血脉。两者结合,才能构建出真正可用的交互界面。