这一节将说明如何在Qt中使用对话框——如何创建、初始化以及运行它们,并且对用户交互中的选择做出响应。本节将会使用在第2章中创建的Find、GotoCell和Sort对话框,也会创建一个简单的About 对话框。我们从如图3.12所示的Find对话框开始。由于希望用户能够在Spreadsheet窗口和Find对话框之间进行切换,所以Find对话框必须是非模态(modeless)的。非模态窗口就是运行在应用程序中对于任何其他窗口都独立的窗口。
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)));
}
/* Find对话框是一个可以让用户在电子制表软件中搜索文本的窗口。当用户单击Edit->Find时,就会调用find( )槽来弹出Find对话框。
* 这时,就可能出现下列几种情形:
* ●这是用户第一次调用Find对话框。
* ●以前曾经调用过Find对话框,但用户关闭了它。.
* ●以前曾经调用过Find对话框,并且现在它还是可见的。
* 如果Find对话框还不曾存在过,就可以创建它并且把它的findNext()信号和findPrevious()信号与Spreadsheet中相对应的那些槽连接起来。
* 本应该在MainWndow的构造函数中创建这个对话框,但是推迟对话框的创建过程将可以使程序的启动更加快速。
* 还有,如果从来没有使用到这个对话框,那么它就决不会被创建,这样可以既节省时间又节省内存。
* 然后,调用show()、raise()和activateWindow()来确保窗口位于其他窗口之上并且是可见的和激活的。
* 只调用show()就足以让一个隐藏窗口变为可见的、位于最上方并且是激活的,
* 但是也有可能是在Find对话框窗口已经是可见的时候又再次调用了它,在这种情况下,show()调用可能什么也不做,
* 那么就必须调用raise()和activateWindow()让窗口成为顶层窗口和激活状态。
*/
findDialog->show();
findDialog->raise();
findDialog->activateWindow();
}
现在看一下如图3.13所示的Go to Cell对话框。我们希望用户可以弹出、使用和关闭它,但是却不希望让这个窗口能够与应用程序中的其他窗口相互切换。也就是说,GotoCell对话框窗口必须是模态(modal)的。模态窗口就是-个在得到调用可以弹出并可以阻塞应用程序的窗口,从而会从调用发生开始起妨碍其他的任意处理或者交互操作,直到关闭该窗口为止。前面使用的文件对话框和消息框就是模态的。
如果对话框是通过show()调用的,那么它就是非模态对话框[除非此后又调用了setModal(),才会让它变为模态对话框]。但是,如果它是通过exec()调用的,那么该对话框就会是模态对话框。
void MainWindow::goToCell()
{
/* 如果对话框被接受,函数QDialog::exec()可返回一个true值(QDialog::Accepted),否则就会返回一个false值(QDialog::Rected)。
* 可以回想一下,当初在第2章利用Qt设计师创建Go to Cell对话框时,就曾经把OK连接到accept(),把Cancel连接到reject()。
* 如果用户选择0K,就把当前单元格的值设置成行编辑器中的值。
*/
GoToCellDialog dialog(this);
if (dialog.exec()) {
QString str = dialog.lineEdit->text().toUpper();
spreadsheet->setCurrentCell(str.mid(1).toInt() - 1,
str[0].unicode() - 'A');
}
/* QTable::setCurentCell()函数需要两个参数:一个行索引和一个列索引。
* 在Spreadsheet 应用程序中,单元格A1就是单元格(0,0),单元格B27就是单元格(26,1)。
* 为了从函数QLineEdit::text()返回的QString中获得行索引,可以使用QString::mid()来提取行号(这个函数将返回一个从字符串的开始直到末尾位置的子字符串),
* 然后使用QString::toInt()把它转换成一个整数值,并且把该值再减去1。
* 对于列号,则可以用这个字符串中第一个字符的大写数值减去字符‘A'的数值而得到。
* 我们知道,该字符串将具有正确的格式,因为为对话框创建了一个QRegExpValidator检验器,只有满足一个字符后面再跟至多三位数字格式的字符串才能让OK按钮起作用。
* goToCell()函数与目前着到的所有代码都有些不同,因为它在堆栈中创建了一个作为变量的窗口部件(一个GoToCellDialog)。
* 虽然多使用了一行代码,但是换来了不需要使用new和delete的简便
* 由于在使用完一个对话框(或者菜单)后,通常就不再需要它了,所以在堆栈中创建对话框(和上下文菜单)是一种常见的编程模式,并且对话框会在作用域结束后自动销毁掉。
*/
}
现在转到Sort对话框上。Sort对话框是一个模态对话框,它允许用户在当前的选定区域中使用给定的列进行排序。图3.14给出了一个排序的实例,用列B作为排序的主键,列A作为排序的第二键(两个都采用升序)。
void MainWindow::sort()
{
/* sort()函数中的代码使用了一种和goToCell()函数中用到的类似模式:
* ● 在堆栈中创建对话框并且对其进行初始化。
* ● 使用exec()弹出对话框。
* ● 如果用户单击0K,就从对话框的各个窗口部件中提取并且使用这些用户输入的值。
* setColumnRange()调用将那些可用于排序的列变量设置为选定的列。
* 例如,使用图3.14中的选择,range.leftColumn()将返回值0,即‘A'+0=‘A’,并且range.rightColumn()将返回值2,即‘A' +2=‘C'。
* compare对象储存了主键、第二键和第三键以及它们的排序顺序。(将会在下一章中看到SpreadsheetCompare类的定义)
* 这个对象会由Spreadsheet::sort()使用,用于两行的比较。
* keys数组存储了这些键的列号。
* 例如,如果选择区域是从C2扩展到E5,那么列C的位置就是0。
* ascending数组中按bool格式存储了和每一个键相关的顺序。
* QComboBox::currentIndex()返回当前选定项的索引值,该值是一个从0开始的数。
* 对于第二键和第三键,考虑到“None”项,我们从当前项减去1。
*/
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( )函数会完成这项工作,但是它显得稍有不足。它认为Sort 对话框是按照一种特定的方式来实现的,也就是像上面那样来处理组合框和“None”项。这就意味着,如果重新设计了Sort对话框,也许就需要重新编写这些代码。如果对话框只会从一个地方调用,那么这样的方式应该是足够了,但是如果对话框可能会在几个地方调用到,那么这种处理方式就等于打开了维护工作的梦魇之门。
//一种更为稳健的方法是让SortDialog类具有自适应性,这可以通过让它自己创建一个SpreadsheetCompare对象,然后使这个对象只能被它的调用者使用来做到这一点。这样就可以有效地简化MainWindow::sort( )函数:
void MainWindow::sort()
{
SortDialog dialog(this);
QTableWidgetSelectionRange range = spreadsheet->selectedRange();
dialog.setColumnRange('A' + range. leftColumn(),
'A' + range. rightColunn());
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();
}
相比而言,第一种方法中调用者需要知道与这个对话框相关的暗示信息,而第二种方法中的对话框需要知道由调用者所提供的与数据结构相关的暗示信息。在对话框需要作用于现场变化的地方,这种方法显得更为有用些。但是就像第一种方法中调用者的代码功能不足一样,如果数据结构发生了变化,则第三种方法也会失效。
一些开发者只会选用一种对话框处理方法并对其持之以恒。这有一个好处,就是能够精通和简练处理方法,因为所有的对话框都使用的是同一种处理模式,但是这也会失去对调用对话框时没有用到的那些其他有益处理方法。理想情况下,应根据每一个对话框的自身来选择应当使用的对话框处理方法。
我们将用About对话框来圆满结束这一节。可以创建-一个像Find或Go to Cell:对话框那样的自定义对话框来显示应用程序的有关信息,但是因为绝大多数About对话框都具有较为固定的格式,所以Qt提供了一种更为简单的解决方案。
void MainWindow::about()
{
//通过调用一个方便的静态函数QMessageBox::about(),就可以获得About对话框。
//这个函数和QMessageBox::warming()的形式非常相似,只是它使用了父窗口的图标,而不是标准的“瞥告”图标。
QMessageBox::about(this, tr("About Spreadsheet"),
tr("Spreadsheet 1.1
"
"Copyright © 2008 Software Inc."
"Spreadsheet is a small application that "
"demonstrates QAction, QMainWindow, QMenuBar, "
"QStatusBar, QTableWidget, QToolBar, and many other "
"Qt classes."));
}
到现在为止,已经使用了由QMessageBox和QFileDialog 提供的多个方便的静态函数。这些函数可以创建一个对话框,初始化它,并且可以对它调用exec()。当然,也可以像创建其他任意窗口部件一样创建QMessageBox或者QFileDialog窗口部件,然后再明确地对它调用exec()或者甚至是show( ),尽管这样的处理方式会显得有些不大方便。
在MainWindow的构造函数中,调用了readSettings()来载入应用程序存储的那些设置。与之相似的是,在closeEvent()中,调用writettings( )来保存这些设置。这两个函数是最后两个需要实现的MainWindow成员函数。
void MainWindow::writeSettings()
{
/* writettings()函数保存了主窗口的几何形状(位置和尺寸大小)、最近打开文件列表以及Show Grid和Auto Recalculate选项的设置值。
* 默认情况下,QStings会存储应用程序中与特定平台相关的一些设置信息。
* 在Windows系统中,它使用的是系统注册表;在UNIX系统中,它会把设置信息存储在文本文件中;在MacOSx中,它会使用Core Foundation Preferences 的应用程序编程接口。
* 构造函数的参数说明了组织的名字和应用程序的名字。采用与平台相关的方式,可以利用这一信息查找这些设置所在的位置。
* QSettings把设置信息存储为键值对( key-value pair) 的形式。键(key)与文件系统的路径很相似。
* 可以使用路径形式的语法(例如, findDialog/matchCase)来指定子键(subkey)的值,或者也可以使用beginGroup()和endGroup()的形式
* 值(value)可以是一个int、bol double、QString、QStingList或者是QVariant所支持的其他任意类型,包括那些已经注册过的自定义类型。
*/
QSettings settings("Software Inc.", "Spreadsheet");
settings.setValue("geometry", saveGeometry());
settings.setValue("recentFiles", recentFiles);
settings.setValue("showGrid", showGridAction->isChecked());
settings.setValue("autoRecalc", autoRecalcAction->isChecked());
}
void MainWindow::readSettings()
{
/* readSettings()函数可以载人之前使用witeSttings()函数所保存的那些设置。
* value()函数中的第二个参数可以在没有可用设置的情况下指定所需的默认值。
* 在应用程序第一次运行时,使用的就是这些默认值。
* 由于没有为形状或者最近打开文件列表指定第二个参数,所以在第一次运行时,窗口会使用任意但是却合理的大小和位置,而最近文件列表会是一个空表。
* 在redSetings()和writettings()中使用与QSettings相关的全部代码为MainWindow所选择的布置方案,都只是许多可用方案中的一种而已。
* 可以在应用程序执行期间的任何时候和程序代码中的任何地方,随时随地创建一个Qetings对象,用它查询或者修改一些设置。
*/
QSettings settings("Software Inc.", "Spreadsheet");
restoreGeometry(settings.value("geometry").toByteArray());
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);
}
现在已经完成了对Spreadsheet的MainWindow的实现。在后续的几节中,将会讨论如何修改Spreadsheet应用程序来让它可以处理多文档以及如何实现一个程序启动画面(splash screen)。将会在下一章中完成它的功能,包括公式和排序的处理。