我们的第一个例子是用纯C++写一个查找对话框。我们将实现一个对话框类。为了能完成它,我们把它做成一个独立的组件,有自己的信号和槽。最终效果如下图:
源代码分布在这两个文件中:finddialog.h和finddialog.cpp。我们从finddialog.h开始。
- #ifndef FINDDIALOG_H
- #define FINDDIALOG_H
- #include <QDialog>
- class QCheckBox;
- class QLabel;
- class QLineEdit;
- class QPushButton;
第1,2行(后面的#endif)是保护头文件不被重复包含
第3行是包含了QDialog类的定义,在Qt中QDialog类继承了QWidget。
第5行到第7行为Qt类的前向声明(forward declarations),在实现代码中才真正使用这些类。一个前向声明告诉C++编译器某个类是存在的,但是没有给出这个类定义所有的细节,这些细节通常在类本身的头文件提供。我们稍后将对此讨论更多。
下一步,我们定义FindDialog类为QDialog类的子类:
- class FindDialog : public QDialog
- {
- Q_OBJECT
- public:
- FindDialog(QWidget *parent = 0);
在类定义的最开始的Q_OBJECT宏对于所有定义了信号或槽的类来说是必不可少的。
FindDialog类的构造函数是典型的Qt的Widget类的构造函数。parent形参描述了parent的widget。默认形参是个空指针,表示这个dialog没有parent。
- signals:
- void findNext(const QString &str, Qt::CaseSensitivity cs);
- void findPrevious(const QString &str, Qt::CaseSensitivity cs);
signals部分声明了两个信号,当用户点击"查找"按钮时就发送信号。如果"反方向查找选项"被选上,则dialog发送findPrevious()信号;否则发送findNext()信号。
signals关键字实际上是个宏。C++与处理器在编译器看到它前把它转换为标准的C++代码。Qt::CaseSensitivity是一个枚举类型,包含了Qt::CaseSensitive和Qt::CaseInsensitive。
- private slots:
- void findClicked();
- void enableFindButton(const QString &text);
- private:
- QLabel *label;
- QLineEdit *lineEdit;
- QCheckBox *caseCheckBox;
- QCheckBox *backwardCheckBox;
- QPushButton *findButton;
- QPushButton *closeButton;
- };
- #endif
在类的私有部分,我们声明了两个槽。为了实现这两个槽,我们需要访问这个dialog的大部分的child widgets,所以我们也声明了指向它们的指针。slots关键字像signals关键字一样也是一个宏,会被扩展C++编译器就可识别之。
为了这些私有变量,我们使用这些变量所属的类的前向声明。这样是可行的,因为它们都是指针,我们不需要在头文件中访问它们,所以编译器不需要完整的类定义。我们本可以包含对应的头文件,诸如<QCheck>,<QLabel>,等等。但是使用前向声明时在编译时可以稍微快些。
我们现在来看看finddialog.cpp,此文件包含了FindDialog类的实现。
- #include <QtGui>
- #include "finddialog.h"
首先,我们包含了<QtGui>头文件,此头文件包含了Qt的所有关于GUI的类。Qt由几个模块组成,每个模块都有自己的库。最重要的模块有QtCore,QtGui,QtNetwork,QtOpenGL,QtSql,QtSvg,和QtXml。<QtGui>头文件包含了QtCore模块的一部分类和QtGui模块所有的类的定义。包含这个头文件可以比分别包含单个的头文件节省时间。
在filedialog.h头文件中,我们本可以简单的包含<QtGui>头文件来取代包含<QDialog>头文件和使用QCheck,QLabel,QLineEdit和QPushButton类的前向声明。然后,包含一个头文件中包含一个很大的头文件是一种坏习惯,尤其是在更大的应用程序。
- FindDialog::FindDialog(QWidget *parent)
- : QDialog(parent)
- {
- label = new QLabel(tr("Find &what:"));
- lineEdit = new QLineEdit;
- label->setBuddy(lineEdit);
- caseCheckBox = new QCheckBox(tr("Match &case"));
- backwardCheckBox = new QCheckBox(tr("Search &backward"));
- findButton = new QPushButton(tr("&Find"));
- findButton->setDefault(true);
- findButton->setEnabled(false);
- closeButton = new QPushButton(tr("Close"));
第4行,我们传了parent形参到基本的构造函数中。然后我们创建这个子类widget。tr()函数调用标记它的形参,就是字符串字面值,要被翻译成其它的语言。这个函数在QObject类和所有包含有Q_OBJECT宏的子类声明。把用户可见的串都包在tr()函数中是很好的习惯,虽然你并不想马上把你的应用程序翻译成其它语言。翻译Qt应用程序在第17章中涉及。
在字符串字面值中,我们使用&来标识快捷键。例如,第9行创建了一个查找按钮,用于可以在支持快捷键的平台下按下ALT+F来激活这个按钮。&号也能用来控制焦点:第4行我们创建了一个带有快捷键的标签(ALT+W),然后在第6行我们设置这个标签的搭档(buddy)为文本框。一个widget的搭档就是当标签的快捷键被按下时接收焦点。所以当用户按下ALT+W时(标签的快捷键),焦点就会来到文本框(标签的搭档)。
在第10行,我们调用setDefault(true)函数来使dialog的默认按钮为找找按钮。默认按钮就是当用户按下回车键时被按下的按钮。在第11行我们禁用查找按钮。当一个widget被禁用,通常会变成灰色且不会响应用户的交互。
- connect(lineEdit, SIGNAL(textChanged(const QString &)),
- this, SLOT(enableFindButton(const QString &)));
- connect(findButton, SIGNAL(clicked()),
- this, SLOT(findClicked()));
- connect(closeButton, SIGNAL(clicked()),
- this, SLOT(close()));
只要文本框的文本变动时私有槽enableFindButton(const QString &)就会被调用。当用户点击查找按钮时私有槽findClicked()就会被调用。当用户点击Close按钮dialog就会关闭。close()槽继承自QWidget,默认的行为是从视觉上隐藏widget(并没有delete掉)。我们等会会看到enableFindButton()和findClicked()槽的代码。
因为QObject类是FindDialog类的其中一个祖先类,所以我们可以省略connect函数前的QObject前缀。
- QHBoxLayout *topLeftLayout = new QHBoxLayout;
- topLeftLayout->addWidget(label);
- topLeftLayout->addWidget(lineEdit);
- QVBoxLayout *leftLayout = new QVBoxLayout;
- leftLayout->addLayout(topLeftLayout);
- leftLayout->addWidget(caseCheckBox);
- leftLayout->addWidget(backwardCheckBox);
- QVBoxLayout *rightLayout = new QVBoxLayout;
- rightLayout->addWidget(findButton);
- rightLayout->addWidget(closeButton);
- rightLayout->addStretch();
- QHBoxLayout *mainLayout = new QHBoxLayout;
- mainLayout->addLayout(leftLayout);
- mainLayout->addLayout(rightLayout);
- setLayout(mainLayout);
下一步,我们使用布局管理者来布局子widgets。布局可以包含widgets和其它的布局。可以使用不同的组合来嵌套QHBoxLayout,QVBoxLayout,QGridLayout,可以建造出非常精密的对话框。
对于这个查找对话框,我们使用两个QHBoxLayout和两个QVBoxLayout,如下图所示。外层的布局为主布局;在第15行被安置到查找对话框中,主布局负责整个对话框的布局。其它3个布局为子布局。右下角的空格占用符。它用来在查找按钮和退出按钮的下面填充空白,来确保这些按钮占据它们的布局的顶端。
一个微妙的方面是布局管理者并不是widgets,而是从QLayout继承而来,相应的也继承自QObject。在上图中,widgets表示为实线围起,而布局被虚线包围起来,以此来强调它们之间的不同。在运行中的应用程序,布局是不可见的。
当子布局被加到父布局中时,子布局被自动重定父级。然后,当主布局安置到查找对话框中时,它就变成对话框的child,这个布局所有的widgets被重定父级变成对话框的child。这个父子继承层次描绘如下图:
- setWindowTitle(tr("Find"));
- setFixedHeight(sizeHint().height());
- }
最后,我们设置对话框的标题栏的文字,我们还设置窗口有一个固定的高度,因为窗口没有任何widgets来占据其它的垂直空间。QWidget::sizeHint()函数返回一个widget的理想的尺寸。
OK,这里我们把FindDialog的构造函数就过了一遍。既然我们使用new关键字创建了对话框的widgets和布局,看起来我们需要写一个析构函数来使用delete关键字释放我们创建的每一个widget和布局。但这不是必须的,因为当parent被销毁时Qt会自动销毁所有的自级,所有的widgets和布局都是FindDialog的后裔。
现在我们来看看对话框的槽:
- void FindDialog::findClicked()
- {
- QString text = lineEdit->text();
- Qt::CaseSensitivity cs =
- caseCheckBox->isChecked() ? Qt::CaseSensitive
- : Qt::CaseInsensitive;
- if (backwardCheckBox->isChecked()) {
- emit findPrevious(text, cs);
- } else {
- emit findNext(text, cs);
- }
- }
- void FindDialog::enableFindButton(const QString &text)
- {
- findButton->setEnabled(!text.isEmpty());
- }
当用户点击查找按钮时findClicked槽被调用。这会发出findPrevious()信号或findNext()信号,这取决于向后搜索选选项。emit关键字是Qt特有的;像Qt其它的扩展,它被C++预处理器转换成标准C++。
当用户何时该表文本框的文本时enableFindButton()槽会被调用。如果文本框中没有文字时查找按钮可用,反之禁用。
OK,我们现在创建一个main.cpp文件来测试我们的FindDialog widget:
- #include <QApplication>
- #include "finddialog.h"
- int main(int argc, char *argv[])
- {
- QApplication app(argc, argv);
- FindDialog *dialog = new FindDialog;
- dialog->show();
- return app.exec();
- }
现在运行程序,如果你的平台支持快捷键,核实快捷键ALT+W,ALT+C,ALT+B,和ALT+F能出发正确的行为。按下TAB来遍历所有的widgets。默认的TAB顺序是各个widgets创建的顺序。这可以通过调用QWidget::setTabOrder()来设置。
提供合理的TAB顺序和快捷键确保不想使用鼠标的用户能完全使用这个程序。完全的键盘控制也被有的打字员的欣赏。
在第3章我们在一个真实的程序中使用这个Find Dialog,到那时我们将findPrevious()和findNext()信号与一些槽连接起来。