Qt 核心概念
cpp class MyClass : public QObject { Q_OBJECT // 必须的宏 public: explicit MyClass(QObject *parent = nullptr); };
### Q_OBJECT 宏与元对象编译器 (moc)
注意到类声明中的 `Q_OBJECT` 宏了吗?它不是可有可无的装饰。Qt 在标准 C++ 编译之前,会先用**元对象编译器 (Meta-Object Compiler, moc)** 处理头文件,为带有 `Q_OBJECT` 宏的类生成额外的 C++ 代码。这些代码实现了:
- 运行时类型信息(比 C++ RTTI 更详尽)
- 信号与槽机制
- 动态属性系统
**初学者注意**:每次为类添加 `Q_OBJECT` 宏后,都需要重新运行 qmake 或 CMake,确保 moc 重新生成对应实现文件,否则会出现链接错误。
### 对象所有权与对象树
Qt 通过父子关系来管理对象的生命周期。当你用一个 `parent` 参数构造一个 `QObject` 子对象时,该对象会被添加到父对象的“对象树”中。当父对象被销毁时,它会自动销毁所有子对象。
- **优点**:避免了大量手动 `delete`,极大降低内存泄漏风险。
- **注意**:如果你在一个栈对象上创建子对象,必须保证栈对象的析构顺序正确,或者在子对象析构前将其解除父子关系。通常推荐创建在堆上并指定 `parent`。
```cpp
QObject *parent = new QObject;
QObject *child = new QObject(parent);
delete parent; // child 会被自动析构
二、信号与槽 —— 对象间通信的灵魂
什么是信号与槽
信号与槽是 Qt 最核心的通信机制,用于替代传统的回调函数。它实现了松散耦合的对象间通信:一个对象发出信号,连接到此信号的一个或多个槽函数会被自动调用,而双方彼此不必知道对方的任何具体实现细节。
- 信号 (signal):在特定事件发生时被发射的函数声明,由 moc 生成实现。
- 槽 (slot):普通的 C++ 成员函数,可以经由信号调用,也能像一般函数般直接调用。
基本连接语法
在 Qt5 及以后,推荐使用新式信号槽语法,它具备编译期类型检查,避免了旧式基于字符串语法的运行时风险。
// 发送者:QPushButton
// 信号: clicked()
// 接收者:QApplication
// 槽: quit()
QObject::connect(button, &QPushButton::clicked,
qApp, &QApplication::quit);
当按钮被点击时,应用程序退出。连接可以跨线程、跨对象,乃至连接到 lambda 表达式。
一个完整示例
class Sender : public QObject
{
Q_OBJECT
public:
void doWork() { emit valueChanged(42); }
signals:
void valueChanged(int newValue);
};
class Receiver : public QObject
{
Q_OBJECT
public slots:
void onValueChanged(int value) {
qDebug() << "Received:" << value;
}
};
// 使用
auto sender = new Sender;
auto receiver = new Receiver;
QObject::connect(sender, &Sender::valueChanged,
receiver, &Receiver::onValueChanged);
sender->doWork(); // 输出 "Received: 42"
信号与槽的连接类型
默认情况下,连接类型为 Qt::AutoConnection:
- 若发送者和接收者在同一线程,则为直接连接(槽立即执行,类似同步调用)。
- 若在不同线程,则为队列连接(信号被放入接收者线程的事件队列,异步执行)。
你也可以显式指定 Qt::QueuedConnection 或 Qt::BlockingQueuedConnection(跨线程同步调用,仅在接收者线程不同于发送者线程时有效)。
三、事件系统与事件循环
事件驱动模型
Qt 应用程序是事件驱动的。当一个 Qt 程序启动后,QApplication::exec() 会启动主事件循环。事件循环持续从事件队列中取出事件,并分发到对应对象的事件处理函数中。
常见的事件类型包括:
- 用户输入(鼠标、键盘)
- 定时器事件
QTimerEvent - 重绘事件
QPaintEvent - 窗口状态变化事件
事件处理与重载
每个 QObject 都可以通过重写 event() 函数或具体的事件处理器来处理事件。例如,要让一个自定义 Widget 响应键盘按下,你可以重写 keyPressEvent(QKeyEvent *event)。
class MyWidget : public QWidget
{
protected:
void keyPressEvent(QKeyEvent *event) override {
if (event->key() == Qt::Key_Escape)
close();
else
QWidget::keyPressEvent(event); // 确保未处理的事件继续传播
}
};
事件过滤器
如果不想派生类,可以使用事件过滤器。调用 installEventFilter() 后,接收对象会首先将发送对象的事件交给过滤器的 eventFilter() 方法处理。
class Filter : public QObject {
protected:
bool eventFilter(QObject *obj, QEvent *event) override {
if (event->type() == QEvent::KeyPress) {
// 处理按键事件
return true; // 事件已消费,不再传递
}
return QObject::eventFilter(obj, event);
}
};
事件系统是比信号槽更底层的机制,一般只有在你需要精确控制事件流时才会直接使用。
四、布局管理 —— 自适应界面的基石
为何需要布局
手动设置坐标和大小来排列控件,无法应对窗口缩放或不同分辨率。Qt 的布局系统通过布局管理器自动调整子控件的位置和尺寸,保证界面始终美观。
常见的布局类
QHBoxLayout:水平排列控件QVBoxLayout:垂直排列控件QGridLayout:网格布局,类似表格QFormLayout:标签-字段对布局,适合设置界面
使用示例
QWidget *window = new QWidget;
QPushButton *btn1 = new QPushButton("One");
QPushButton *btn2 = new QPushButton("Two");
QPushButton *btn3 = new QPushButton("Three");
QVBoxLayout *layout = new QVBoxLayout(window);
layout->addWidget(btn1);
layout->addWidget(btn2);
layout->addWidget(btn3);
window->setLayout(layout);
window->show();
布局可以嵌套,如一个水平布局内可以放置一个垂直布局,以实现复杂界面结构。
尺寸策略与伸缩因子
每个 QWidget 都有 sizePolicy 属性,决定其在布局中的缩放行为。拉伸因子 (stretch factor) 则允许控件按比例占用额外空间。
layout->addWidget(btn1, 1); // 拉伸因子 1
layout->addWidget(btn2, 2); // 拉伸因子 2,将获得 btn1 两倍的额外空间
五、常用核心类简介
字符串:QString
QString 是 Qt 对 Unicode 字符串的封装,支持深拷贝、写时复制和丰富的操作函数。建议在所有 Qt 代码中统一使用 QString 而非 std::string,以获得最佳集成体验。
QString s = "Hello";
s.append(" Qt");
qDebug() << s.toUpper(); // "HELLO QT"
容器类
Qt 提供了类似 STL 但更轻量且支持隐式共享(写时复制)的容器,如 QList、QVector、QMap、QHash 等。对于大量数据或频繁访问的场景,QVector 通常比 QList 更高效。
QVector<int> numbers = {1, 2, 3};
numbers.append(4);
for (int n : numbers)
qDebug() << n;
文件与流
QFile 和 QDataStream 或 QTextStream 配合,使文件 I/O 非常直观。
QFile file("output.txt");
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream stream(&file);
stream << "Qt Core Concepts\n";
}