使用对话框
在本节中,我们将解释如何在Qt中使用对话框,如果创建并初始化他们以及运行它们,并响应用户的行为。我们将使用第2章中创建的”Find”,”Go-to-Cell”和排序对话框。我们还将创建一个简单的“About”对话框。
图3.12 电子表格程序的“Find”对话框
我们以“Find”对话框为开始。因为我们想让用户随意的在主窗口和”Find”对话框间切换,所以”Find”对话框必须是非模态的。非模态的窗口是程序中与其他窗口无关的窗口。
在模态窗口创建之后,他们都有自己的信号和槽连接以响应用户的交互。
void MainWindow::find()
{
if (!findDialog) {
findDialog = new FindDialog(this);
connect(findDialog, SIGNAL (findNext(const QString &,
Qt::CaseSensitivity)),
spreadsheet, SLOT (findNext(const QString &,
Qt::CaseSensitivity)));
connect(findDialog, SIGNAL(findPrevious(const QString &,
Qt::CaseSensitivity)),
spreadsheet, SLOT(findPrevious(const QString &,
Qt::CaseSensitivity)));
}
findDialog->show();
findDialog->activateWindow();
}
“Find”对话框是一个能让用户在电子表格中搜索文本的窗口。当用户点击”Edit|Find”以弹出”Find”对话框时find()槽被调用。这时,可能出现下面几种情况:
这是用户首次调用“查找”对话框。
“查找”对话框以前调用过,但用户已经将它关闭了。
“查找”对话框以前调用过,并且现存还在。
如果“查找”对话框不存在,我们创建它并把它的findNext()和findPrevious()信号连接到相应的Spreadsheet槽上。我们本该在MainWindow的构造函数中创建该对话框,但延迟创建会使启动更快。也就是,如果从没有使用过该对话框,它就不会被创建,即可以节省时间,还能节省内存。
接下来我们调用show()和activeWindow()以保证该窗口是可见的、活动的。一个单独的show()调用对于让隐藏的窗口可见并成为活动窗口是有效的,但“查找”对话框在它被调用的时候就已经存在了,这种情况下show()不做任何事情,使用activeWindow()击活窗口是有必要的。我们的另一选择或许可写为:
if (findDialog->isHidden()) {
findDialog->show();
} else {
findDialog->activateWindow();
}
这是兼顾两种情况的编程方式。
我们现存来看一下“转到单元格“对话框。我们想让用户弹出它,使用它,关闭它,但不能与程序中的其他窗口切换。这就是产”转到单元格“对话框必须是模态的。模态窗口是一种在调用的时候能够弹出并阻塞应用程序,在窗口关闭前阻止任何与原窗口的交互和处理的窗口。前面用到的文件对话框和消息框都是模态的。
图3.13 电子表格的“转到单元格“对话框
通过show()调用的对话框是非模态的(除非预先调用setModel()使它变成模态的)。通过exec()调用的则是模态的。
void MainWindow::goToCell()
{
GoToCellDialog dialog(this);
if (dialog.exec()) {
QString str = dialog.lineEdit->text().toUpper();
spreadsheet->setCurrentCell(str.mid(1).toInt() - 1,
str[0].unicode() - 'A');
}
}
如果对话框被接受QDialog::exec()函数返回真值(QDialog::Accepted),否则会返回假值(QDialog::Rejected)。回忆一下第2章中用Qt设计师创建的“转到单元格“对话框,我们曾把OK按钮连接了到accept(),把Cancel连接到了reject()。如果用户点击了OK,我们把当前单元格设置为行编辑框中指定的值。
QTableWidget::setCurrentCell()函数需要两个参数:一个行号和一个列号。在电子表格程序中,单元格A1是单元格(0,0),单元格B27是单元格(26,1)。要从QLineEdit::text()返回的字符串中获取行号,可以使用QString::mid()(它返回从开始位置到字符串未尾的子字符串)解析出行号,使用QString::toInt()把它转换为int类型后再减去1。对于列号,我们使用字符串第一个字符的大写形式的整数值减去’A’的整数值即可。由于我们为对话框创建的QregExpValidator能检验用户的输入,仅当输入为一个字母后跟最多3个数字的时候OK按钮才被使能,所以我们能确信获得的字符串格式是正确的。
goToCell()函数与我们到现存为止所看到的代码不同,它在内部创建了一个位于栈空间上的物件(一个GoToCellDialog实例)。由于仅有一行的额外代价,我们只需要简单的使用new和delete了:
void MainWindow::goToCell()
{
GoToCellDialog *dialog = new GoToCellDialog(this);
if (dialog->exec()) {
QString str = dialog->lineEdit->text().toUpper();
spreadsheet->setCurrentCell(str.mid(1).toInt() - 1,
str[0].unicode() - 'A');
}
delete dialog;
}
在栈空间上创建模态对话框(以及QWidget::contextMenuEvent()实现中的上下文菜单)是一种常用的编程模式,这是因为这些对话框对使用完后就不再需要了,正好在当前域的最后手工销毁它。
现在我们转向排序对话框。排序对话框是一个能让用户按照指定的列对选中的区域排序的模态对话框。图3.14展示一个排序的例子,其中列B是一级排序关键字,列B是二级排序关键字(两列都是按照升序)。
图3.14 排序电子表格的选中区域
void MainWindow::sort()
{
SortDialog dialog(this);
QTableWidgetSelectionRange range = spreadsheet->selectedRange();
dialog.setColumnRange('A' + range.leftColumn(),
'A' + range.rightColumn());
if (dialog.exec()) {
SpreadsheetCompare compare;
compare.keys[0] =
dialog.primaryColumnCombo->currentIndex();
compare.keys[1] =
dialog.secondaryColumnCombo->currentIndex() - 1;
compare.keys[2] =
dialog.tertiaryColumnCombo->currentIndex() - 1;
compare.ascending[0] =
(dialog.primaryOrderCombo->currentIndex() == 0);
compare.ascending[1] =
(dialog.secondaryOrderCombo->currentIndex() == 0);
compare.ascending[2] =
(dialog.tertiaryOrderCombo->currentIndex() == 0);
spreadsheet->sort(compare);
}
}
sort()中的代码遵循类似goToCell()的模式。
在栈上创建对话框并初始化它。
使用exec()弹出该对话框。
如果用户点击OK,我们从用户在对话框物件中输入中抽出值并使用它。
setColumnRange() 调用设置对选定列进行排序的可用列。例如,使用图3.14中的选择区域,range.leftColumn()值为0,表示为’A’+0=’A’,range.rightColumn()值为2,表示为’A’+2=’C’。
compare对象存储了一级,二级和三级排序关键字以及排序顺序。(我们将在下一章查看spreadsheetCompare类的定义)。该对象被Spreadsheet::sort() 使用来对两行进行比较。Keys数组存储了所有键的列号。例如,如果选定区域是从C2到E5,则C的位置为0。acending数组存储了与每个键的排序顺序相应的bool值。QComboBox::current-Index()返回当前选定的项的索引位置,从0开始。对于二级和三级键,我们从当前项减掉1以解释“None”项。
sort()函数可以做这些工作,但这有点脆弱。它假设排序对话框以一种带有复选框和“None”项的特有的方式实现。这意味着如果我们重新设计排序对话框,我们还需要重写这些代码。虽然这种方法对于只在一个地方使用的对话框还能胜任,但是如果该对话框用在几个地方的话势必造成维护的恶梦。
一个更健壮的方法是通过让SortDialog自己创建一个SpreadsheetCompare对象使它更巧妙。简化的MainWindow::sort()如下:
void MainWindow::sort()
{
SortDialog dialog(this);
QTableWidgetSelectionRange range = spreadsheet->selectedRange();
dialog.setColumnRange('A' + range.leftColumn(),
'A' + range.rightColumn());
if (dialog.exec())
spreadsheet->performSort(dialog.comparisonObject());
}
这种方法导致组件间的松藕合,并且对于在多个地方调用的对话框这几乎总是正确的选择。
一个更激进的方法在初始化SortDialog对象的时候传递指向Spreadsheet对象的指针过来,并允许该对话框直接操作Spreadsheet。这让SortDialog更难通用了,因为它仅能工作在特定类型的物件上,但这通过除去了SortDialog::setColumnRange()函数进下简化了代码。MainWindow::sort()函数现存变成了:
void MainWindow::sort()
{
SortDialog dialog(this);
dialog.setSpreadsheet(spreadsheet);
dialog.exec();
}
这种方法首先反映出:与调用者需要该对话框的内部知识相反,该对话框调用者提供的数据结构的内部知识。这种方法在对话框需要实事应用修改时非常有用。但是在像第一种方法的调用者代码很脆弱,第三种方法会在数组结构改变后崩溃。
一些开发人员仅选择一种使用对话框的方法并一直使用。这有精通和简单的好处因为他们的对话框用法总是同一模式,但同时也失去了他们没有使用的方法的好处。实际上,具体使用的方法应该取决于每个对话框的低层需求。
我们将以关于对话框结束本节。我们要像做“查找”和“转到”对话框一样创建一个自定义对话框,用其描述程序的信息。但由于大多数关于对话框是高度程序化的,所有Qt提供了一种简单的解决方案。
void MainWindow::about()
{
QMessageBox::about(this, tr("About Spreadsheet"),
tr("<h2>Spreadsheet 1.1</h2>"
"<p>Copyright © 2006 Software Inc."
"<p>Spreadsheet is a small application that "
"demonstrates QAction, QMainWindow, QMenuBar, "
"QStatusBar, QTableWidget, QToolBar, and many other "
"Qt classes."));
}
该关于对话框通过调用静态函数QMessageBox::about()获得。这个函数非常像QMessageBox::warning(),除了它使用父窗口的图标而不是标准的“warning”。
图3.15 关于电子表格
到现在为止我们已经使用了QMessageBox 和 QfileDialog中的几个便利的静态函数。这些函数创建一个对话框,初始化它,并调用它的exec()。还可以像任何其他物件一样创建QMessageBox 或QfileDialog物件并显式地调用它的exec()或者show()函数,尽管这不够便利。
保存设置
在MainWindow的构造函数中,我们调用readSettings()加载程序存储的设置。类似地,在closeEvent()中,我们调用writeSettings()来保存当前设置。这两个函数最后两个需要实现的成员函数。
void MainWindow::writeSettings()
{
QSettings settings("Software Inc.", "Spreadsheet");
settings.setValue("geometry", geometry());
settings.setValue("recentFiles", recentFiles);
settings.setValue("showGrid", showGridAction->isChecked());
settings.setValue("autoRecalc", autoRecalcAction->isChecked());
}
writeSettings()函数保存主窗口的几何参数(位置和大小),最近打开的文件列表,显示网格和自动重算选项。
默认情况下,Qsettings在平台相关的特定保存程序设置。在Windows上,它使用系统注册表。在Unix上,它在文本文件中存储数据。在Mac OS X上,它使用Core Foundation Preference API。
它的构造函数的参数指定组织名字和程序名字。这些信息用于以平台相关的方式查找设置数据的位置。
Qsettings以健值对的形式存储设置。键与文件系统中的路径相似。子键可以用类路径语法指定(如findDialog/matchCase),或者使用beginGroup() 和 endGroup():
settings.beginGroup("findDialog");
settings.setValue("matchCase", caseCheckBox->isChecked());
settings.setValue("searchBackward", backwardCheckBox->isChecked());
settings.endGroup();
值可以是int,bool ,double,QString, QStringList,或者任何其他QVariant支持的类型,当然也包括已经注册过的自定义类型。
void MainWindow::readSettings()
{
QSettings settings("Software Inc.", "Spreadsheet");
QRect rect = settings.value("geometry",
QRect(200, 200, 400, 400)).toRect();
move(rect.topLeft());
resize(rect.size());
recentFiles = settings.value("recentFiles").toStringList();
updateRecentFileActions();
bool showGrid = settings.value("showGrid", true).toBool();
showGridAction->setChecked(showGrid);
bool autoRecalc = settings.value("autoRecalc", true).toBool();
autoRecalcAction->setChecked(autoRecalc);
}
readSettings()函数加入由writeSettings()存储的设置。当没有有效设置时,value()函数的第二个参数用于给定一个默认值。该默认值仅在程序首次运行时用到。由于没有给最近的文件列表一个默认值作为第二个参数,在程序首次运行时它将被设为一个空表。
Qt 提供了一个与QWidget::geometry()对应的QWidget::setGeometry()函数,但在X11平台上它不总如我们预料的行为工作,这是由于许多窗口管理器的限制。由于这个原因,我们使用move()和resize()取而代之。(详见http://doc.trolltech.com/4.1/geometry.html)
我们在MainWindow中的所有安排,将QSettings相差的代码放在readSettings()和writeSettings()中,这仅是诸多可选方法的一种。QSettings 对象可在代码的任何地方,程序运行过程中任何时刻创建、查询或者修改设置。
我们现存已经完成了Spreadsheet的MainWindow的实现。在随后的小节中,我们将讨论如何修改该电子表格程序使之能处理多个文档,以及如何实现一个启动欢迎窗口。我们将在下一章中完成包括处理公式和排序在内的所有功能。
多文档程序
我们现存准备编写电子表格程序的main()函数:
include <QApplication>
include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow mainWin;
mainWin.show();
return app.exec();
}
这个main()函数与我们以前所写的有所不同。我们不再使用new而是在栈上创建MainWindow的变量实例。MainWindow实现将在函数结束时自动销毁。
使用上面的main()函数,电子表格程序提供了一个主窗口并且同一时刻只能处理一个文档。如果我们想山同时修改多个文档,我们必须启动电子表格程序的多个实例。但是对用户来说这不如一个提供了多个主窗口的单一实例程序方便,就像一个浏览器的实现能同时提供多个浏览窗口一样。
我们要修改电子表格程序以便它能处理多文档。首先,我们需要一个稍微不同的“文件”菜单:
“文件|新建”创建一个新的空文档主窗口而不是重用已有的主窗口。
“文件|关闭“仅关闭当前的主窗口。
“文件|退出“关闭所有的窗口。
在老版的“文件“菜单中是没有”关闭”选项的,因为这和退出一样。
图3.16 新的“文件”菜单
下面是新的main()函数:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow *mainWin = new MainWindow;
mainWin->show();
return app.exec();
}
使用了多窗口,我们现在要用new创建MainWindow,因为而后可能在编辑完成后用delete删除主窗口以节省内存。
下面是新MainWindow::newFile()槽:
void MainWindow::newFile()
{
MainWindow *mainWin = new MainWindow;
mainWin->show();
}
我们简单的创建一个新的MainWindow实例。看起来有点奇怪的是我们没有保存该新窗口的任何指针,但这不是问题,因为Qt为我们跟踪所有的窗口。
下面是关闭和退出的操作:
void MainWindow::createActions()
{
...
closeAction = new QAction(tr("&Close"), this);
closeAction->setShortcut(tr("Ctrl+W"));
closeAction->setStatusTip(tr("Close this window"));
connect(closeAction, SIGNAL(triggered()), this, SLOT(close()));
exitAction = new QAction(tr("E&xit"), this);
exitAction->setShortcut(tr("Ctrl+Q"));
exitAction->setStatusTip(tr("Exit the application"));
connect(exitAction, SIGNAL(triggered()),
qApp, SLOT(closeAllWindows()));
...
}
QApplication::closeAllWindows()槽将关闭程序的所有窗口,除非它们中的一个丢弃了了关闭事件。这的确是我们这里需要的行为。我们不必担心未保存的改变,因为无论任何时候关闭一个窗口,这都会在MainWindow::closeEvent()中进行处理。
看起来我们已经完成了该程序处理多窗口的功能。很遗憾的,这里有一个潜在的问题:如果用户一直创建和关闭主窗口,可能最终将耗尽机器内存。这是因为我们在newFile()中不断的创建MainWindow物件但从没有删除它们。当用户关闭一个主窗口的时候,它的默认行为是隐藏它,因为它还一直在内存中。主窗口多了之后将产生问题。
解决方法是在构造函数中设置Qt::WA_DeleteOnClose属性。
MainWindow::MainWindow()
{
...
setAttribute(Qt::WA_DeleteOnClose);
...
}
这会告诉Qt在关闭它之后同时把它删除。Qt::WA_DeleteOnClose属性仅是可设的能影响QWidget的行为的诸多标记之一。
内存泄漏不是我要处理的仅有的问题。我们老版程序的设计包括一个隐含假设:我们仅有一个主窗口。在多窗口情况下,每个主窗口都有它自己的打开的文件列表。明示的,最近打开的文件列表应该是整个程序共有的。我们可以把recentFiles声明为静态变量,因而整个程序嚘它的一份实例,这就简单的解决该问题。但是我们必须确保在任何地方调用updateRecentFileActions()更新文件菜单的时候,我们必须调用所有窗口的这一方法。下面是实现代码:
foreach (QWidget *win, QApplication::topLevelWidgets()) {
if (MainWindow *mainWin = qobject_cast<MainWindow *>(win))
mainWin->updateRecentFileActions();
}
以上代码使用Qt的foreach(在第11章说明)构造函数迭代程序的所有窗口并调用所有MainWindow类型的物件的updateRecentFileActions()。类似的代码也可用于“显示网格”和“自动重计算”项,或者用于确保同一文件不被加载两次。
每个主窗口提供一个文档的程序叫做SDI(单文档界面)应用程序。在Windows平台上一个常用的替代选择是MDI(多文档界面),这种程序有单一的主窗口,然而它的中央区域能管理多个文档窗口。Qt能用于在所有它支持的平台创建上创建SDI和MDI应用程序。图3.17展示使用两种方法的电子表格 程序。MID将在第6章中说明(布局管理)。
图3.17 SID对MDI
欢迎窗口
许多程序在启动的时候都有一个欢迎窗口。一些开发才使用欢迎窗口来掩盖缓慢的启动过程,而另一些人则用它来满足市场需要。使用QSplashScreen类给Qt程序添加一个欢迎窗口非常容易。
QSplashScreen类在主窗口显示之前显示一幅图像。它还能在图像上显示信息以告诉用户程序的初始化过程。一般情况下,欢迎窗口的代码在main()中调用QApplication::exec()之前。
接下来是一个使用QSplashScreen的程序的main()函数的例子,该程序在启动的时候会加载模块并建立网络连接。
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QSplashScreen *splash = new QSplashScreen;
splash->setPixmap(QPixmap(":/images/splash.png"));
splash->show();
Qt::Alignment topRight = Qt::AlignRight | Qt::AlignTop;
splash->showMessage(QObject::tr("Setting up the main window..."),
topRight, Qt::white);
MainWindow mainWin;
splash->showMessage(QObject::tr("Loading modules..."),
topRight, Qt::white);
loadModules();
splash->showMessage(QObject::tr("Establishing connections..."),
topRight, Qt::white);
establishConnections();
mainWin.show();
splash->finish(&mainWin);
delete splash;
return app.exec();
}
我们现存已经完成了电子表格程序的用户界面。在下一章中,我们将实现电子表格程序的核心功能。
图3.18 欢迎窗口
|