所谓 GUI 界面,归根结底,就是一堆组件的叠加。我们创建了一个窗口,把按钮放在上面,把图标放在上面,从而成为一个界面。组件的位置在放置时尤为重要。为了使窗口能够按照我们需要的方式渲染,我们必须指定组件放在哪里。这涉及到组件定位的机制。Qt 提供绝对定位和布局定位两种组件定位机制。
顾名思义,绝对定位是最原始的定位方法:给出该组件的坐标和长宽值。这样,Qt 知道把组件放在哪里,如何设置组件的大小。但这样做的一个问题是,如果用户改变了窗口的大小,如点击最大化按钮或使用鼠标拖动窗口的边缘,绝对定位的组件将没有响应。这也很自然,因为你没有告诉我 Qt,当窗口变化时,组件是否需要更新以及如何更新。例如,在最大化时,如果需要自动更新组件,Word 总是放大稿纸区域,拉长工具栏——编写相应的函数来响应这些变化。或者,更简单的方法是禁止用户改变窗口大小。但这不是长远之计。
针对这种变化的需求,Qt 为解决这个问题提供了另一种机制-布局。只需将组件放入某种布局,布局由专门的布局管理器管理。当需要调整大小或位置时,Qt 使用相应的布局管理器进行调整。以下是一个例子:
// !!! Qt 5 int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget window; window.setWindowTitle("Enter your age"); QSpinBox *spinBox = new QSpinBox(&window); QSlider *slider = new QSlider(Qt::Horizontal, &window); spinBox->setRange(0, 130); slider->setRange(0, 130); QObject::connect(slider, &QSlider::valueChanged, spinBox, &QSpinBox::setValue); void (QSpinBox:: *spinBoxSignal)(int) = &QSpinBox::valueChanged; QObject::connect(spinBox, spinBoxSignal, slider, &QSlider::setValue); spinBox->setValue(35); QHBoxLayout *layout = new QHBoxLayout; layout->addWidget(spinBox); layout->addWidget(slider); window.setLayout(layout); window.show(); return app.exec(); }
这个例子还是值得解释的。我们可以先看看操作结果:
当我们拖动窗口时,我们可以看到组件自动变化:
本代码中介绍了两个新组件:QSpinBox
和QSlider
。QSpinBox
输入框只能输入数字,步进按钮上下箭头。QSlider
它是带滑块的滑杆。我们可以从上面的截图中清楚地区分这两个组件。当我们创建这两个组件的例子时,我们使用它们setRange()
设置函数的范围。由于我们的窗口标题是Enter your age(输入年龄) range(范围)设置为 0 到 130 应该够了。
有趣的部分在下面connect()
函数。我们已经知道了connect()
因此,我们写了函数的使用
QObject::connect(slider, &QSlider::valueChanged, spinBox, &QSpinBox::setValue);
将 slider 的valueChanged()
信号同 spinBox 的setValue()
函数连接。这是我们熟悉的。然而,当我们直接写作时
QObject::connect(spinBox, &QSpinBox::valueChanged, slider, &QSlider::setValue);
编译器会报错:
no matching function for call to 'QObject::connect(QSpinBox*&, <unresolved overloaded function type>, QSlider*&, void (QAbstractSlider::*)(int))
这是怎么回事?从错误信息可以看出,编译器认为QSpinBox::valueChanged
是一个 overloaded 函数。让我们看看QSpinBox
的文档发现,QSpinBox
有两个信号:
- void valueChanged(int)
- void valueChanged(const QString &)
当我们使用&QSpinBox::valueChanged
编译器指针时,编译器不知道应该取哪个函数(记住我们之前介绍过的,已经 moc 预处理后,signal 它也是一个普通的函数。)地址,所以报告错误。解决方案很简单,编译器不能确定哪个函数吗?然后我们显式地指定了一个函数。方法是创建一个函数指针,指定为 int:
void (QSpinBox:: *spinBoxSignal)(int) = &QSpinBox::valueChanged;
然后我们把这个函数指针作为指针 signal,与 QSlider 函数连接:
QObject::connect(spinBox, spinBoxSignal, slider, &QSlider::setValue);
避免编译错误。
仔细观察这两个connect()
它们实际上完成了双向数据绑定。当然,对于 Qt 我们可以放心使用自己的信号函数。但是,如果是自己的信号,就要注意避免无限循环!
我们创建了下面的代码QHBoxLayout
对象。显然,这是一个布局管理器。然后将这两个组件添加到布局管理器中,并将布局管理器设置为窗口。这些代码似乎是合乎逻辑的,应该很容易理解。此外,布局管理器聪明地做出了正确的行为:保持QSpinBox
宽度不变,自动拉伸QSlider
的宽度。
Qt 为我们选择提供了几种布局管理器:
QHBoxLayout
:按照水平方向从左到右布局;QVBoxLayout
:从上到下按垂直方向布局;QGridLayout
:布局在网格中,类似于 HTML 的 table;QFormLayout
:根据表格布局,每行前面有一段文本,文本后面有一个组件(通常是输入框),类似 HTML 的 form;QStackedLayout
:层叠布局允许我们根据几个组件进行分层布局 Z 轴向堆叠可以形成一页一页的导向效果。
当然,我们也可以使用它 Qt 4 编译上正如你应该想到的,我们必须编译上述代码connect()
函数修改:
// !!! Qt 4 int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget window; window.setWindowTitle("Enter your age"); QSpinBox *spinBox = new QSpinBox(&window); QSlider *slider = new QSlider(Qt::Horizontal, &window); spinBox->setRange(0, 130); slider->setRange(0, 130); QObject::connect(slider, SIGNAL(valueChanged(int)), spinBox, SLOT(setValue(int))); QObject::connect(spinox, SIGNAL(valueChanged(int)),
slider, SLOT(setValue(int)));
spinBox->setValue(35);
QHBoxLayout *layout = new QHBoxLayout;
layout->addWidget(spinBox);
layout->addWidget(slider);
window.setLayout(layout);
window.show();
return app.exec();
}
这里我们强调一下,上面的代码在 Qt 5 中同样可以编译通过。不过,我们减少了使用函数指针指定信号的步骤。也就是说,在 Qt 5 中,如果你想使用 overloaded 的 signal,有两种方式可供选择:
- 使用 Qt 4 的
SIGNAL
和SLOT
宏,因为这两个宏已经指定了参数信息,所以不存在这个问题; - 使用函数指针显式指定使用哪一个信号。
有时候,使用 Qt 4 的语法更简洁。但是需要注意的是,Qt 4 的语法是没有编译期错误检查的。这也是同 Qt 5 的信号槽新语法不同之处之一。