创建main窗口
这一章会教会你如何用Qt创建main窗口。最后,你将会学会为应用程序建立完整的的UI界面,包括菜单,工具条,状态栏,以及一些程序设计到的对话框。
一个程序的main窗口提供框架,用户界面则建立在这个框架之上。这一章中我们会编写一个spreadsheet的应用程序,这个程序会用到第2章中创建的Find, Go To Cell,及Sort对话框。
在大多数GUI程序背后,会有一块代码来实现一些功能。例如,读写文件的代码,或者是处理显示在UI上的数据的代码。在第4章中,我们会知道怎么实现这些功能,还是用spreadsheet作为我们练习的例子。
继承QMainWindow
应用程序的主窗口是通过继承QMainWindow类来实现。我们在第2章中看到的很多创建对话框的技术跟建立主窗口也是相关的,因为QDialog和QMainWindow都是从QWidget类继承的。
主窗口可能用Qt Designer来创建,但在这一章中,我们全部用纯手工编码来实现,这样我们就可以了解整个过程是如何实现的。如果你更喜欢用可视化的方法,可以参考Qt Designer在线手册中“Create Main Windows in Qt Designer”一章。
Spreadsheet程序主窗口的源代码有mainwindow.h和mainwindow.cpp两个文件组成。让我们先来看看头文件:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
class QAction;
class QLabel;
class FindDialog;
class Spreadsheet;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow();
protected:
void closeEvent(QCloseEvent *event);
我们定义了一个类MainWindow来继承QMainWindow类。包含Q_OBJECT宏,因为这个类要提供自己的信号和槽。
closeEvent()函数是类QWidget中的一个虚函数,当用户关闭窗口时这个函数被自动调用。它在MainWindow类中重新实现,这样在关闭窗口时我们就可以向用户询问“你想保存修改吗?”,以避免由于用户疏忽而丢失一些重要数据。
private slots:
void newFile();
void open();
bool save();
bool saveAs();
void find();
void goToCell();
void sort();
void about();
一些菜单选项,例如File|New和Help|About,在MainWindow作为私有槽函数实现。多数槽函数的返回值是void,只有save()和saveAs()返回bool类型。当一个槽函数作为响应一个信号而执行时,返回值是被忽略的,但是当我们将槽函数作为一个函数来调用时,调用者就可以根据这个返回值来做一些处理了。
void openRecentFile();
void updateStatusBar();
void spreadsheetModified();
private:
void createActions();
void createMenus();
void createContextMenu();
void createToolBars();
void createStatusBar();
void readSettings();
void writeSettings();
bool okToContinue();
bool loadFile(const QString &fileName);
bool saveFile(const QString &fileName);
void setCurrentFile(const QString &fileName);
void updateRecentFileActions();
QString strippedName(const QString &fullFileName);
主窗口还需要一些私有槽函数和私有函数来支持用户界面。
Spreadsheet *spreadsheet;
FindDialog *findDialog;
QLabel *locationLabel;
QLabel *formulaLabel;
QStringList recentFiles;
QString curFile;
enum { MaxRecentFiles = 5 };
QAction *recentFileActions[MaxRecentFiles];
QAction *separatorAction;
QMenu *fileMenu;
QMenu *editMenu;
...
QToolBar *fileToolBar;
QToolBar *editToolBar;
QAction *newAction;
QAction *openAction;
...
QAction *aboutQtAction;
};
#endif
除了一些私有的槽和函数,MainWindow类还有很多私有变量。我们会在用到它们的时候对它们进行详细解释。
现在我们来浏览一下实现代码:
#include <QtGui>
#include "finddialog.h"
#include "gotocelldialog.h"
#include "mainwindow.h"
#include "sortdialog.h"
#include "spreadsheet.h"
我们包含头文件<QtGui>,这个头文件里面包含了所有我们用的UI相关的类。我们也包含了其他一些第2章中完成的头文件。
MainWindow::MainWindow()
{
spreadsheet = new Spreadsheet;
setCentralWidget(spreadsheet);
createActions();
createMenus();
createContextMenu();
createToolBars();
createStatusBar();
readSettings();
findDialog = 0;
setWindowIcon(QIcon(":/images/icon.png"));
setCurrentFile("");
}
在构造函数中,我们开始创建一个spreadsheet widget,并把这个widget设置为主窗口的中心widget。中心widget占据了主窗口的中间位置。Spreadsheet类是QTableWidget类的子类,实现了一些spreadsheet的功能,比如对公式的支持。我们会在第3章中实现这些功能。
我们调用私有函数createActions(), createMenues, createContextMenu(), createToolBars()和createStatusBar()来设置主窗口的其他部分。我们也调用readSettings()函数来读取程序保存的一些设置。
我们初始化了一个findDialog指针为null。在第一次MainWindow::find()函数被调用的时候,我们会创建一个FindDialog对象。
在构造函数的最后,我们设置窗口的图标为icon.png, 这是一个png文件。Qt支持很多图像格式,包括BMP,GIF,JPEG,PNG,PNM,SVG,TIFF,XBM还有XPM。调用QWidget::setWindowIcon()来设置图标显示在窗口的左上角。很不幸,没有一个不依赖于平台的方法来设置桌面上显示的应用图标。对于特殊平台的方法在下面链接中有介绍。http://doc.trolltech.com/4.3/appicon.html.
GUI应用程序通常会用到很多图片。有几种方法可以为程序提供图片。最常用的方法如下:
a) 把图片保存在文件中,在运行时加载它们;
b) 在源代码中包含XPM文件。(这是因为XPM文件也是有效的C++文件。)
c) 使用Qt资源机制;
这里我们将使用Qt资源机制,因为这种方法比在运行时加载文件来得更方便,可以支持任何支持的文件格式。我们把图片保存在代码目录中images文件夹。
为了使用Qt的资源系统,我们必须创建一个资源文件,并把它加到项目文件.pro来告诉qmake这是一个资源文件。在这个例子中,我们把这个资源文件叫做spreadsheet.qrc,我们在.pro项目文件中加入如下一行:
RESOURCES = spreadsheet.qrc
打开资源文件,里面其实是简单的XML格式。我们列出一部分:
<RCC>
<qresource>
<file>images/icon.png</file>
...
<file>images/gotocell.png</file>
</qresource>
</RCC>
资源文件会被编译到应用程序中,所有我们不能丢失资源文件。当我们需要引用这个资源时,使用路径前缀 :/ (冒汗加斜线),比如我们在代码中指示图标的路径 :/images/icon.png。资源可是是任何类型的文件,而不单单包括图片,我们可以在大多数地方来使用这些资源文件。第12章中我们会更详细的讲解这方面的内容。
创建菜单和工具条
目前大多数GUI程序会提供菜单,上下文菜单和工具栏。菜单可以使得用户浏览程序,提供程序一些功能,而上下文菜单和工具条提供快速运行常用功能的捷径。下图显示了spreadsheet程序的菜单。
Qt中使用操作(action)的概念来简化菜单和工具条的编程。一个操作可以被添加到任何数量的菜单和工具条中。在Qt中创建菜单和工具条包含下列步骤:
1. 创建并设置操作。
2. 创建菜单,并把操作组装到菜单上。
3. 创建工具条,并把操作组装到工具条上。
在spreadsheet程序中,所有的操作在createActions()函数中创建:
void MainWindow::createActions()
{
newAction = new QAction(tr("&New"), this);
newAction->setIcon(QIcon(":/images/new.png"));
newAction->setShortcut(QKeySequence::New);
newAction->setStatusTip(tr("Create a new spreadsheet file"));
connect(newAction, SIGNAL(triggered()), this, SLOT(newFile()));
New操作有一个快速启动键New,一个父窗口(这里即是主窗口),一个图标,一个快捷键,还有一个状态提示。大多数窗口系统都为某些操作提供标准的键盘快捷键。例如这里New操作在Windows,KDE,GNOME中的快捷键为Ctrl+N,在Mac OS X中为Command+N。通过使用合适的QKeySequence::StandardKey枚举量,我们可以确保Qt在应用程序所在的平台上提供正确的快捷键。
我们连接操作的triggered()信号和主窗口的私有newFile()槽函数。这个连接确保当用户选择File|New菜单,或点击工具栏中的New按钮,或按下Ctrl+N,这个newFile()槽函数会被调用。
Open,Save和Save As操作跟New操作的实现非常相似,我们这里略过,直接来看看File菜单中”最近打开文件”部分:
...
for (int i = 0; i < MaxRecentFiles; ++i) {
recentFileActions[i] = new QAction(this);
recentFileActions[i]->setVisible(false);
connect(recentFileActions[i], SIGNAL(triggered()),
this, SLOT(openRecentFile()));
}
我们为每个recentFileActions数组成员添加一个操作。每个操作设置为隐藏,并被连接到openRecentFile()槽函数中。过会儿,我们会看到这个最近文件的操作时怎么变成可视的并使用。
exitAction = new QAction(tr("E&xit"), this);
exitAction->setShortcut(tr("Ctrl+Q"));
exitAction->setStatusTip(tr("Exit the application"));
connect(exitAction, SIGNAL(triggered()), this, SLOT(close()));
Exit操作跟上面一些操作有一些不一样。没有标准的按键来结束一个程序,所以这里我们明确的指定一个快捷键,Ctrl+Q。另外一个不同点是我们连接到了主窗口的close()槽函数,这个槽函数是由Qt提供的。
现在我们来看看Select All操作:
...
selectAllAction = new QAction(tr("&All"), this);
selectAllAction->setShortcut(QKeySequence::SelectAll);
selectAllAction->setStatusTip(tr("Select all the cells in the "
"spreadsheet"));
connect(selectAllAction, SIGNAL(triggered()),
spreadsheet, SLOT(selectAll()));
selectAll()槽函数是由QTableWidget类的其中一个祖先类QAbstractItemView提供的,所有我们不需要自己来实现它。
让我们再来看看Options菜单的Show Grid操作:
...
showGridAction = new QAction(tr("&Show Grid"), this);
showGridAction->setCheckable(true);
showGridAction->setChecked(spreadsheet->showGrid());
showGridAction->setStatusTip(tr("Show or hide the spreadsheet's "
"grid"));
connect(showGridAction, SIGNAL(toggled(bool)),
spreadsheet, SLOT(setShowGrid(bool)));
Show Grid是一个可检查的操作。可检查操作在菜单中有一个检查标记,在工具栏中就是一个开关(toggle)按钮.当操作打开时,spreadsheet组件显示一个网格。我们初始化操作为默认值,这样在启动时它们可以同步。然后我们连接Show Grid操作的toggled(bool)信号到spreadsheet组件的setShowGrid(bool)槽,这个槽函数是从QTableWidget类中继承。一旦这个操作被添加到菜单或工具条中,用户就可以开关这个网格了。
Show Grid和Auto-Recalculate操作是独立的可检查的操作。Qt也支持互斥操作,可以用QActionGroup类来实现。
...
aboutQtAction = new QAction(tr("About &Qt"), this);
aboutQtAction->setStatusTip(tr("Show the Qt library's About box"));
connect(aboutQtAction, SIGNAL(triggered()), qApp, SLOT(aboutQt()));
}
对于About Qt操作,我们使用QApplication对象的aboutQt()槽,通过qApp这个全局变量。这个弹出的对话框如下图显示。
现在我们已经创建了所有的操作,我们就可以建立一个菜单系统来包含它们:
void MainWindow::createMenus()
{
fileMenu = menuBar()->addMenu(tr("&File"));
fileMenu->addAction(newAction);
fileMenu->addAction(openAction);
fileMenu->addAction(saveAction);
fileMenu->addAction(saveAsAction);
separatorAction = fileMenu->addSeparator();
for (int i = 0; i < MaxRecentFiles; ++i)
fileMenu->addAction(recentFileActions[i]);
fileMenu->addSeparator();
fileMenu->addAction(exitAction);
在Qt中,菜单是QMenu的实例。addMenu()函数会用指定的文本创建一个QMenu widget,并把它添加到菜单栏中。QMainWindow::menuBar()函数返回一个指向QMenuBar的指针。第一次调用menuBar()时会创建一个菜单栏。
我们先创建一个File菜单,然后往它添加New, Open, Save和Save As操作。我们插入一个分隔条,使那些相近功能项尽量靠在一起。我们用一个for循环来添加recentFileActions数组的操作(这些操作起初是隐藏的),最后我们添加exitAction()操作。
我们保存了其中一个分隔条的指针。这个可以运行我们隐藏分隔条(当没有最近打开文件时)或显示它,因为我们想显示两条分隔条如果它们之间没有项目的话。
editMenu = menuBar()->addMenu(tr("&Edit"));
editMenu->addAction(cutAction);
editMenu->addAction(copyAction);
editMenu->addAction(pasteAction);
editMenu->addAction(deleteAction);
selectSubMenu = editMenu->addMenu(tr("&Select"));
selectSubMenu->addAction(selectRowAction);
selectSubMenu->addAction(selectColumnAction);
selectSubMenu->addAction(selectAllAction);
editMenu->addSeparator();
editMenu->addAction(findAction);
editMenu->addAction(goToCellAction);
现在我们创建Edit菜单,用QMenu::addAction()函数来添加操作,用QMenu::addMenu()函数来添加子菜单。子菜单也是QMenu类的对象。
toolsMenu = menuBar()->addMenu(tr("&Tools"));
toolsMenu->addAction(recalculateAction);
toolsMenu->addAction(sortAction);
optionsMenu = menuBar()->addMenu(tr("&Options"));
optionsMenu->addAction(showGridAction);
optionsMenu->addAction(autoRecalcAction);
menuBar()->addSeparator();
helpMenu = menuBar()->addMenu(tr("&Help"));
helpMenu->addAction(aboutAction);
helpMenu->addAction(aboutQtAction);
}
我们用同样的方法创建Tools, Options和Help菜单。在Options和Help菜单之间我们插入了一个分隔符。在Motif和CDE风格中,分隔符会Help菜单推到最右边;在其他风格中,分隔符是被忽略的。下图展示了两个风格:
void MainWindow::createContextMenu()
{
spreadsheet->addAction(cutAction);
spreadsheet->addAction(copyAction);
spreadsheet->addAction(pasteAction);
spreadsheet->setContextMenuPolicy(Qt::ActionsContextMenu);
}
任何Qt widget可以包含一系列的操作。为了给程序提供一个上下文菜单,我们把那些需要的操作添加到spreadsheet widget当中,并设置widget的上下文菜单显示策略。当用户右击widget或按下平台指定的快捷键时,上下文菜单就会被显示。看看spreadsheet上下文菜单如下:
另一种稍微复杂一点的创建右键菜单的方法是重新实现(重载)QWidget::contextMenuEvent()函数,方法是创建一个QMenu widget,绑定一些需要的操作,然后调用exec()。
void MainWindow::createToolBars()
{
fileToolBar = addToolBar(tr("&File"));
fileToolBar->addAction(newAction);
fileToolBar->addAction(openAction);
fileToolBar->addAction(saveAction);
editToolBar = addToolBar(tr("&Edit"));
editToolBar->addAction(cutAction);
editToolBar->addAction(copyAction);
editToolBar->addAction(pasteAction);
editToolBar->addSeparator();
editToolBar->addAction(findAction);
editToolBar->addAction(goToCellAction);
}
创建工具栏跟菜单方法很像。我们创建一个File工具条和Edit工具条。跟菜单一样,工具条也可以有分隔符。
设置状态栏
我们已经完成了菜单栏和工具栏,现在可以创建程序的状态栏里。在正常状态下,状态栏包含:当前cell的位置和当前cell的公式。状态栏也用来显示一些状态提示信息和其它一些暂时的信息。下图显示了一些状态下的显示情况。
MainWindow构造函数调用createStatusBar()来设置状态栏:
void MainWindow::createStatusBar()
{
locationLabel = new QLabel(" W999 ");
locationLabel->setAlignment(Qt::AlignHCenter);
locationLabel->setMinimumSize(locationLabel->sizeHint());
formulaLabel = new QLabel;
formulaLabel->setIndent(3);
statusBar()->addWidget(locationLabel);
statusBar()->addWidget(formulaLabel, 1);
connect(spreadsheet, SIGNAL(currentCellChanged(int, int, int, int)),
this, SLOT(updateStatusBar()));
connect(spreadsheet, SIGNAL(modified()),
this, SLOT(spreadsheetModified()));
updateStatusBar();
}
QMainWindow::statusBar()函数返回一个指向状态栏的指针。(状态栏是在第一次调用statusBar()函数时创建。)我们用QLabel来显示状态信息。我们为formulaLabel增加了一个缩进,这样文本就会稍稍靠右显示。当QLabel被添加到状态栏时,它们就会自动成为状态栏的子widget。
上图中显示了两个显示标签有不同的空间要求。指示cell位置的标签只需要一个很小的空间,当窗口被重设大小时,任何额外的空间必须分配给指示cell公式的标签。这是通过在调用QStatusBar::addWidget()函数时指定公式标签一个伸展因子1来实现的.位置指示有一个默认的伸展因子0,意味着它不会被伸展。
当QStatusBar布局指示widget时,默认会使用每个widget的理想的大小(QWidget::sizeHint()),然后伸展那些可以伸展的widget以填充那用的空间。一个widget的理想大小本身依赖于widget的内容,会随着我们改变它的内容而改变大小。为了避免平凡的改变位置标签的大小,我们设置它的最小尺寸足够容纳最大的文本(W999),并给予一定的额外空间。我们也设置对其方式为Qt::AlignHCenter,使文本显示在水平正中。
在函数的最后,我们连接了两个Spreadsheet的信号到MainWindow的两个槽函数:updateStatusBar()和spreadsheetModified()。
void MainWindow::updateStatusBar()
{
locationLabel->setText(spreadsheet->currentLocation());
formulaLabel->setText(spreadsheet->currentFormula());
}
updateStatusBar()槽函数更新cell位置和cell公式标签。当用户移动鼠标到一个新的celll时这个函数被调用。这个槽也作为一个普通函数在createStatusBar()的最后被调用来初始这个标签。这个很必要,因为spreadsheet在启动阶段是不会发射currentCellChanged()信号的。
void MainWindow::spreadsheetModified()
{
setWindowModified(true);
updateStatusBar();
}
spreadsheetModified()槽设置windowModified属性为true以更新标题栏。这个函数也更新状态栏中的位置和公式信息以反映当前的变化。
实现文件菜单
在这一部分我们来实现一些槽和私有函数以使File菜单正常工作,并管理最近曾打开的文件列表。
void MainWindow::newFile()
{
if (okToContinue()) {
spreadsheet->clear();
setCurrentFile("");
}
}
当用户点击File|New菜单选项或点击工具栏中的New按钮时,newFile()槽被调用。如果存在未保存的修改,okToContinue()函数会弹出一个对话框,询问用户是否需要保持修改。不管用户选择Yes还是No,这个函数都返回true;如果用户选择Cancel,返回false。Spreadsheet::clear()函数清楚所有cell和公式。setCurrentFile()私有函数更新窗口标题栏来指示一个没有命名的新闻当正在被编辑。
bool MainWindow::okToContinue()
{
if (isWindowModified()) {
int r = QMessageBox::warning(this, tr("Spreadsheet"),
tr("The document has been modified./n"
"Do you want to save your changes?"),
QMessageBox::Yes | QMessageBox::No
| QMessageBox::Cancel);
if (r == QMessageBox::Yes) {
return save();
} else if (r == QMessageBox::Cancel) {
return false;
}
}
return true;
}
okToContinue()函数中,我们坚持windowModified属性的状态。如果是true,我们显示消息框。如上图,这个消息框有3个按钮,Yes, No和Cancel。
QMessageBox提供很多标准的按钮,并且自动设置一个按钮为默认按钮(当用户按下Enter键时执行),设置一个按钮为退出按钮(当用户按下Esc).我们也可以设置其他的按钮为默认按钮和退出按钮,也可以设置按钮显示的文本。
Warning()函数的调用看起来会觉得复杂,看看下面的语法定义:
QMessageBox::warning(parent, title, message, buttons);
除了warning(),QMessageBox还提供了information(), question()和critical()函数,每一个函数都有自己独特的图标。图标显示如下:
void MainWindow::open()
{
if (okToContinue()) {
QString fileName = QFileDialog::getOpenFileName(this,
tr("Open Spreadsheet"), ".",
tr("Spreadsheet files (*.sp)"));
if (!fileName.isEmpty())
loadFile(fileName);
}
}
open()槽响应File|Open,如newFile(),首先调用okToContinue()处理未保存的修改。然后用静态函数QFileDialog::getOpenFileName()来从用户那边获取一个新的文件名。这个函数弹出一个文件对话框,让用户选择一个文件,返回文件名,或者空字符串如果用户点击Cancel的话。
QFileDialog::getOpenFileName()的第一个参数指定父widget。父-子关系用作对话框上跟其他的widgets有些不一样。对话框总是一个完整的窗口,但是如果它有父窗口的话,默认的会显示在父窗口的正中心,并且子对话框总是共享父窗口的任务栏。
第二个参数指定对话框的标题。第三个参数告诉对话框从哪个目录开始打开,我们这里打开当前文件夹。
第四个参数指定文件过滤器。一个文件过滤器有一些描述性文字和通配符组成。如果我们要在spreadsheet程序中支持comma-separated values文件和Lotus 1-2-3文件,那么我们要使用如下过滤器:
tr("Spreadsheet files (*.sp)/n"
"Comma-separated values files (*.csv)/n"
"Lotus 1-2-3 files (*.wk1 *.wks)")
loadFile()私有函数用来加载一个文件。我们在这里独立写成一个函数,因为我们在打开最近文件功能中还需要这个函数:
bool MainWindow::loadFile(const QString &fileName)
{
if (!spreadsheet->readFile(fileName)) {
statusBar()->showMessage(tr("Loading canceled"), 2000);
return false;
}
setCurrentFile(fileName);
statusBar()->showMessage(tr("File loaded"), 2000);
return true;
}
我们使用Spreadsheet::readFile()来从磁盘中读取一个文件。如果读取成功,我们调用setCurrentFile()来更新窗口标题;如果失败,Spreadsheet::readFile()已经用消息框通知用户读取文件失败了。通常情况下,让底层单元来通知一个错误消息是一个很好的编程习惯,这样可以提供一个比较精确的错误类型。
在上面两种情况中,我们都会在状态栏里显示信息,持续2秒,以告知用户程序正在进行的操作。
bool MainWindow::save()
{
if (curFile.isEmpty()) {
return saveAs();
} else {
return saveFile(curFile);
}
}
bool MainWindow::saveFile(const QString &fileName)
{
if (!spreadsheet->writeFile(fileName)) {
statusBar()->showMessage(tr("Saving canceled"), 2000);
return false;
}
setCurrentFile(fileName);
statusBar()->showMessage(tr("File saved"), 2000);
return true;
}
save()槽响应File|Save。如果文件已经有一个名字,可能这个文件以前打开过,或者已经被保存,save()函数调用saveFile()。还没有指定文件名的话,调用saveAs()。
bool MainWindow::saveAs()
{
QString fileName = QFileDialog::getSaveFileName(this,
tr("Save Spreadsheet"), ".",
tr("Spreadsheet files (*.sp)"));
if (fileName.isEmpty())
return false;
return saveFile(fileName);
}
saveAs()槽响应File|Save As。我们调用QFileDialog::getSaveFileName()来从用户那里得到一个文件名。如果用户点击Cancel,我们返回false,向上传递给它的调用者(save()或okToContinue())。
如果文件已经存在,getSaveFileName()函数会询问让用户确认他们是否想要覆盖已经存在的文件。这个行为可以通过传递一个额外的QFileDialog::DontConfirmOverride参数给getSaveFileName()函数来改变。
void MainWindow::closeEvent(QCloseEvent *event)
{
if (okToContinue()) {
writeSettings();
event->accept();
} else {
event->ignore();
}
}
当用户点击File|Exit时或者在窗口的标题栏中点击close按钮,QWidget::close()槽被调用。它会给widget发送一个‘close’事件。通过重新实现QWidget::closeEvent(),我们可以在关闭主窗口的时候做一些额外的事情,或者询问用户是否真的需要关闭窗口。
如果有未保存的修改,并且用户选择了Cancel,我们忽略这个时间,窗口不会被闭关。正常情况下,我们接受一个事件,Qt就是隐藏这个窗口。我们也调用了私有函数writeSettings()来保存程序当前的设置。
当最后一个窗口被关闭时,应用程序结束。如果有需要,我们也可以禁止这个行为,只要设置QApplication的quitOnLastWindowClosed属性为false,这一的话程序会一直保持运行直到我们调用QApplication::quit()函数。
void MainWindow::setCurrentFile(const QString &fileName)
{
curFile = fileName;
setWindowModified(false);
QString shownName = tr("Untitled");
if (!curFile.isEmpty()) {
shownName = strippedName(curFile);
recentFiles.removeAll(curFile);
recentFiles.prepend(curFile);
updateRecentFileActions();
}
setWindowTitle(tr("%1[*] - %2").arg(shownName)
.arg(tr("Spreadsheet")));
}
QString MainWindow::strippedName(const QString &fullFileName)
{
return QFileInfo(fullFileName).fileName();
}
在setCurrentFile()函数中,我们设置curFile私有变量来保存文件名。在标题栏中显示文件名之前,我们用函数strippedName()去掉文件的路径名,这样看起来更友好。
每个QWidget组件有一个windowModified属性,如果窗口中的文档包含未保存的修改,我们就要设置这个属性为true,否则设置为false。在Mac OS X系统中,如果文档未保存,则窗口的标题栏靠近关闭按钮的旁边会有一个点;在其他平台上,都是一个星号*,后跟文件名。这些平台依赖性的问题,Qt都会帮我们处理好,只要我们及时更新windowModified属性,并把标识符[*]放到标题栏适当位置。
这里我们传递给setWindowTitle()函数的字符串是:
tr("%1[*] - %2").arg(shownName)
.arg(tr("Spreadsheet"))
QString::arg()函数会用它的参数来代替参数%n。这里,arg()和两个%n一起使用。第一个arg()代替%1,第二个代替%2。如果文件名是budget.sp并且没有翻译文件没有被加载,那么最后显示的字符串时”budget.sp[*] – Spreadsheet”。另一种简单的写法是:
setWindowTitle(shownName + tr("[*] - Spreadsheet"));
但是如果我们以后要翻译成其它语言的话,使用arg()函数会更灵活。
如果文件名存在,我们将更新recentFiles数组,保存程序最近打开的文件列表。我们调用removeAll()删除列表中存在的指定文件名,以避免多份拷贝。然后我们调用prepend()函数把文件名加入到列带头部。更新这个列表以后,我们调用私有函数updateRecentFileActions()来更新File菜单的显示项。
void MainWindow::updateRecentFileActions()
{
QMutableStringListIterator i(recentFiles);
while (i.hasNext()) {
if (!QFile::exists(i.next()))
i.remove();
}
for (int j = 0; j < MaxRecentFiles; ++j) {
if (j < recentFiles.count()) {
QString text = tr("&%1 %2")
.arg(j + 1)
.arg(strippedName(recentFiles[j]));
recentFileActions[j]->setText(text);
recentFileActions[j]->setData(recentFiles[j]);
recentFileActions[j]->setVisible(true);
} else {
recentFileActions[j]->setVisible(false);
}
}
separatorAction->setVisible(!recentFiles.isEmpty());
}
首先我们用一个Java风格的迭代器删除任何已经不存在的文件。有些文件也许以前用过,但是已经被删除。recentFiles变量是QStringList类型的(QString列表)。第11章我们会详细接受容器类,比如QStringList,告诉我们这些容器类是怎么跟C++标准模板库(STL)联系在一起的,以及Qt Java风格迭代器类的用法。
然后我们再次遍历文件列表,这次用的是数组风格的索引。对于每一项,我们创建一个由&,数字(j+1),一个空格和文件名(没有路径)组成的字符串。我们设置相应的操作来使用这个字符串。例如,如果第一个文件是C:/My Documents/tab04.sp,第一个操作的显示文本为“&1 tab04.sp“.下图显示了相应的recentFileActions数组和对应的菜单。
每一个操作可以有一个对应的类型为QVariant的数据项。QVariant类型可以是很多C++和Qt类型的值。在第11章中我们会详细介绍这个类型。这里,我们把包含路径的文件名保存在操作的数据项中,这里稍后我们可以很容易取出来用。同时我们设置这个操作可见。
如果最近打开的文件列表中的文件数少于action(操作)数组,我们把那些多余的操作先隐藏。最后,如果至少存在一个最近打开的文件,我们设置分割线可见。
void MainWindow::openRecentFile()
{
if (okToContinue()) {
QAction *action = qobject_cast<QAction *>(sender());
if (action)
loadFile(action->data().toString());
}
}
当用户选择一个最近打开的文件,openRecentFile()槽被调用。okToContinue()函数用来判断是否有未保存的修改。假如用户没有cancel的话,我们调用QObject::sender()来得到具体的那个唤醒这个槽函数对应的操作(action)。
Qobject_cast<T>()函数根据moc工具产生的元-对象信息来动态的进行类型转换。这个函数返回一个所要求的QObject子类的对象指针,如果对象不能被转换为那个类型,那么返回0.跟标准的C++ dynamic_cast<T>()函数不一样,Qt的qobject_cast<T>()可以在各种动态库中正确的工作。在我们的例子中,我们用qobject_cast<T>()函数来强制把QObject指针转换为一个QAction指针。如果转换成功,我们调用loadFile()函数来打开一个文件。
顺便提一句,因为我们知道发送者是QAction,所以如果我们用static_cast<T>()或用传统的C风格转换程序也能正常工作。请参考附录D中类型转换部分来详细了解一个C++各种类型转换问题。
使用对话框
在这一部分,我们要解释一下在Qt中怎样使用对话框—怎么创建和初始化一个对话框,怎么运行对话框,并且解释怎么响应用户的操作。我们会利用到在第2章中创建的Find,Go to Cell和Sort对话框。
我们先来看看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->raise();
findDialog->activateWindow();
}
Find对话框允许用户在spreadsheet中搜索文本。当用户点击Edit|Find时会弹出Find对话框。存在下列几种情况:
a) 用户第一次执行这个操作。
b) Find对话框以前弹出过,但是用户已经关掉了。
c) Find对话框仍然弹出着。
如果Find对话框还没有被创建过,即第一种情况,我们创建它并且建立两个信号-槽连接。我们也可以在MainWindow的构造函数中建立好对话框,但是晚一点建立对话框可以使程序启动快一点。另外,如果对话框一直没有使用,也就一直不用创建,不但节省运行时间也节省了内存。
然后我们调用show(),raise()和activateWindow()来确保窗口可见,并在其他窗口的上面,且是活动的窗口。单独调用show()函数足以使一个隐藏的窗口可见,并在其它窗口上面且活动的,但是Find对话框有可能已经可见了,这种情况,show()函数就不做什么事,我们必须调用raise()和activateWindow()函数来使这个对话框处于其它窗口的上面并且激活这个对话框。我们也可以写成这样:
if (findDialog->isHidden()) {
findDialog->show();
} else {
findDialog->raise();
findDialog->activateWindow();
}
但是这样编程不是很好,就好比我们看到了两条道路通向同一条单行道。
现在我们来看看Go to Cell对话框。跟Find对话框不同,我们允许用户弹出它,使用它以及关闭此对话框,但是在这个对话框弹出期间,不允许用户切换到程序中其它窗口。这意味着Go to Cell对话框必须是模式对话框。一个模式窗口一旦弹出,将会阻塞程序,在此窗口关闭之前阻止任何对其它窗口的交互。以前使用的文件对话框和消息框都是模式对话框。
如果我们用show()函数来显示这个对话框,则此对话框是非模式的(除非我们之前先调用了setModal()函数来设置这个对话框为模式的);如果用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()函数返回true(QDialog::Accepted),否则返回false(QDialog::Rejected)。回想一下在第2章中我们用Qt Designer来创建Go to Cell对话框时,我们连接OK按钮跟对话框的accept()槽,Cancel按钮跟reject()槽。如果我们选择OK,我们把当前Cell值写入line editor中。
QTableWidget::setCurrentCell()函数需要两个参数:一个行索引,一个列索引。在spreadsheet程序中,cell A1是cell(0,0),而cell B27是cell(26, 1).为了得到行索引,我们使用QString::mid()(此函数返回从原有字符串指定的位置到末尾一个子字符串)函数从字符串中提取出行号,并用QString::toInt()转换成int类型,并减去1.对应列号,我们是这样得到的,取得字符串的大写首字母,减去A。我们知道字符串一定是这样一个格式,因为我们在创建对话框时用了QRegExpValidator正则表达式,只有当字符串是正确的格式(一个字母后跟最多三个数字)时,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;
}
我们通常在栈上创建一个模式对话框(或右键菜单context menus),因为通常我们使用以后就不需要它来,这样这个对话框在局部变量有效范围结束时会自动销毁。
我们现在转到Sort对话框。Sort对话框也是一个模式对话框,允许用户对当前选中的区域按指定列排序。下图显示了一个排序的例子,B列为主要排序键,A列为次要排序键(都是升序)。
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()中的遵循差不多的格式:
1. 在栈上创建对话框并初始化。
2. 用函数exec()弹出对话框。
3. 如果用户点击OK,我们提取用户输入的内容并利用这些内容进行排序。
setColumnRange()函数设置选中的用来排序的列。例如,上图中,range.leftColumn()得到的是0,rangge.rightColumn()得到的是2,这样‘A’+0 = ‘A’, ‘A’+2=‘C’.
compare对象保存着1,2,3个排序键值,以及他们排序的顺序。(我们会在下一章中看到对SpreadsheetCompare类的定义。)这个对象被用于Spreadsheet.sort()函数来比较两行。Keys数组保存着键值的列序号。例如,我们选中C2到E5的区域,则列C就是位置0.ascending数组保存着每个键值相关的次序。QCombBox::currentIndex()返回当前选中项的索引,从0开始。对于第2和第3个键值,我们从当前索引值减去1.
Sort()函数来做具体的排序工作,但是这里的设计不够健壮。这里假设Sort对话框的设计方式必须有CombBoxes和None项。这意味着一旦我们重新设计Sort对话框,我们也必须重写这部分代码。要是这个对话框只是在一个地方被用到,那样维护起来的话还算方便,但是这里已经打开了代码维护的噩梦,要是这个对话框被多次用到的话,维护起来将很不方便。
一个让程序更健壮的方法是,让SortDialog类自己来创建SpreadsheetCompare对象,在对话框类内部调用这个compare对象。这样的话可以简化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类的通用性变差,因为它只允许对特定的widget(这里是Spreadsheet)进行操作,这也不是我们所需要的,但这种方法大大简化了MainWindow::sort()函数的代码,SortDialog::setColumnRange()函数也可以去掉了。MainWindow::sort()函数将变成如下:
void MainWindow::sort()
{
SortDialog dialog(this);
dialog.setSpreadsheet(spreadsheet);
dialog.exec();
}
这个方法更第一个方法相比:第一个方法中调用者必须对所调用的对话框的数据结构比较了解,而这个方法反过来,被调用的对话框必须对调用者传递的数据结构有比较清楚的了解。这种方法多用于对话框需要频繁的修改的情况。但是正如第一种方法的缺点一样,这种方法的缺点是如果调用者(这里是spreadsheet对象)的数据结果变化了,被调用者(这里是SortDialog)也需要重新设计。
一些开发人员用到对话框时始终坚持使用一种方法。这对于代码的易读性是有好处的,因为所有的对话框的使用都遵循一个形式,但是也错过了使用其它的方法所带来的好处。理想的情况是,方法的选择必须根据特定的情况而定。
最后,我们来创建一个About对话框来结束这一部分。我们可以像创建Find对话框和Go to Cell对话框一样来定制一个About对话框,并显示一些程序的相关信息。由于About对话框格式基本上固定的,我们可以直接使用Qt提供的方法来创建。
void MainWindow::about()
{
QMessageBox::about(this, tr("About Spreadsheet"),
tr("<h2>Spreadsheet 1.1</h2>"
"<p>Copyright © 2008 Software Inc."
"<p>Spreadsheet is a small application that "
"demonstrates QAction, QMainWindow, QMenuBar, "
"QStatusBar, QTableWidget, QToolBar, and many other "
"Qt classes."));
}
我们调用静态函数QMessageBox::about()来直接创建一个About对话框。这个函数跟QMessageBox::warning()非常像,唯一的区别是,它使用父窗口的图标。而warning()函数会使用标准的警告图标。以上函数生成的对话框如下:
到目前为止我们已经使用了QMessageBox类和QFileDialog类提供的好几个静态函数。这些函数里面创建一个对话框,初始化对话框,并且调用exec()。我们也可以创建一个QMessageBox类或QFileDialog类,显式的调用exec()或show()来顶到一样的目的,但是这样不是很方便,我们平常也很少这样用。
保存设置
在MainWindow构造函数中,我们调用readSettings()来读取程序保存的设置。同样的,在closeEvent()函数中我们调用writeSettings()来保存设置。这两个函数是MainWindow类中最后两个需要实现的成员函数。
void MainWindow::writeSettings()
{
QSettings settings("Software Inc.", "Spreadsheet");
settings.setValue("geometry", saveGeometry());
settings.setValue("recentFiles", recentFiles);
settings.setValue("showGrid", showGridAction->isChecked());
settings.setValue("autoRecalc", autoRecalcAction->isChecked());
}
writeSettings()函数保存主窗口的几何(位置和尺寸),最近打开过的文件,以及显示网格和自动计算的选项。
默认情况下,QSettings把程序的设置保存在系统指定的位置。在Windows上,数据保存在系统注册表中;在Unix上,数据保存在一个文本文件中;在Mac OS X系统上,数据保存使用内核的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");
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);
}
readSettings()函数读取用writeSettings()函数保存的设置。value()函数的第二个参数指定默认值,万一没有可用的设置,那么函数就返回这个默认值。当程序第一次运行时,因为还没有对应的设置,那么程序就使用这些默认值。对于geometry和最近文件列表我们没有指定默认值,这样程序第一次运行时就会在任意位置按合理的尺寸进行显示,并且最近文件列表为空。
我们把所有QSettings相关的代码放到readSettings()和writeSettings()函数中的安排,只是许多方法中的一种。QSettings对象可以在程序运行中的任何时候任何地方进行创建,进而对一些设置进行读取和修改。
到目前为止我们已经完成了MainWindow类的所有成员函数。在这一章接下来的几个部分,我们会讨论怎么样修改Spreadsheet程序来使它处理多个文档,以及怎么创建一个欢迎界面(splash screen)。我们会在下一章完成这个应用程序的所有功能,包括公式的处理及排序。
多文档程序
我们现在来编写Spreadsheet程序的main()函数:
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow mainWin;
mainWin.show();
return app.exec();
}
这个main()函数跟我们以前写得有些不一样,我们在栈上创建一个MainWindow的实例。当函数结束时,这个实例会自动被销毁。
Spreadsheet程序只提供单个主窗口,一次只能处理一个文档。如果我们想同时编辑多个文档,就要为Spreadsheet程序创建多个程序实例。但是这样对用户来说不方便,还不如只提供单个程序实例多个主窗口,就像一个网页浏览器,只有一个浏览器的实例,但是可以有多个浏览窗口的实例。
我们俩修改spreadsheet程序使它可以处理多个文档。首先我们需要对File菜单做一个小小的修改:
File|New操作将创建一个新的主窗口,而不是重用当前的主窗口。
File|Close操作将关闭当前的主窗口。
File|Exit关闭所有窗口。
在以前的设计的File菜单中没有Close选项,因为跟Exit功能一样。新的File菜单如下图所示:
新的main()函数如下:
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow *mainWin = new MainWindow;
mainWin->show();
return app.exec();
}
我们需要创建多个主窗口,所以在main()函数中我们用new来动态创建一个主窗口,因为过后我们关闭主窗口的时候会用delete来销毁一个它,这样可以减少内存的消耗。
新的MainWindow::newFile()槽函数如下:
void MainWindow::newFile()
{
MainWindow *mainWin = new MainWindow;
mainWin->show();
}
很简单,我们创建一个MainWindow的实例。上面的函数看起来有点古怪,我们没有保存这个新创建的指针,在Qt中这不是一个问题,Qt会为我们追踪所有的窗口。
这些是Close和Exit的操作:
void MainWindow::createActions()
{
...
closeAction = new QAction(tr("&Close"), this);
closeAction->setShortcut(QKeySequence::Close);
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::closeAllWindow()槽用来关闭程序中的所有窗口,除非其中一个窗口拒绝关闭。这个正是我们需要达到的效果。我们不必担心未保存的一些修改因为当一个窗口被关闭时MainWindow::closeEvent()函数中会进行处理。
看起来我们做的修改已经可以让这个程序支持处理多个窗口。很不幸,一个隐藏的问题正潜伏着:如果用户不断的创建窗口,关闭窗口,机器最终将耗完内存。这是因为我们在newFile()函数中创建了一个MainWindow组件,但是我们从不删除它们。当用户关闭一个主窗口时,默认的行为是隐藏这个窗口,因此这个窗口其实还是内存中。当创建对各主窗口时,这个问题将显现出来。
方案是:在构造函数中设置Qt::WA_DeleteOnClose属性:
MainWindow::MainWindow()
{
...
setAttribute(Qt::WA_DeleteOnClose);
...
}
这个告诉Qt当窗口关闭时从内存中删除这个窗口。Qt中可以对QWidget设置很多标志来影响它的行为,Qt::WA_DeleteOnClose属性只是众多中的其中一个。
内存泄漏不是我们需要处理的唯一的问题。我们最初的程序设计包含了一个隐含的假设:我们只有一个主窗口。当我们需要支持多个主窗口时,每个主窗口都有自己最近打开的文件列表和自己的一些选项。显然,最近打开的文件列表应该对于整个程序而言的,也就是说所有的主窗口共享一个列表。这个很容易办到,我们只要把recentFiles设置成静态变量。我们必须确保不管哪个主窗口调用updateRecentFileActions()来更新File菜单,我们必须在所有的主窗口中调用它。下面的代码可以达到这个效果:
foreach (QWidget *win, QApplication::topLevelWidgets()) {
if (MainWindow *mainWin = qobject_cast<MainWindow *>(win))
mainWin->updateRecentFileActions();
}
上面的代码用到了foreach结构(我们会在第11章中讲到这个)来遍历程序中所有的窗口,注意只是遍历最上层的窗口(即这个窗口的父窗口没有),对所有的MainWindow类型的窗口调用updateRecentFileActions()函数。同样的代码可以被用来同步网格显示和自动计算选项,或确保同样的文件不会被加载两次。
只提供单个处理文档的程序叫做SDI程序(single document interface)。另一种就是多文档处理程序(MDI, multiple document interface),程序具有单个主窗口,管理着多个文档窗口。Qt在它支持的平台上可以用来创建SDI和MDI程序。下图中显示了spreadsheet程序的两种类型。我们在第6章中会详细解释MDI。
欢迎界面(Splash Screens)
很多程序在启动的时候会显示一个欢迎界面。一些开发人员用欢迎界面来掩盖程序启动的缓慢,而有些则是为了满足市场的需要。在Qt程序中增加一个欢迎界面非常的方便,只要用QSplashScreen类可以轻松实现。
QSplashScreen类在主窗口出现之前显示一张图片。它也可以在图片上显示一些信息来告知用户程序初始化的进程。通常情况下,欢迎界面相关的代码放在main()函数里,并在QApplication::exec()调用之前。
下面是一个使用QSplashScreen类的例子,欢迎界面中显示了整个初始化的进程,加载模块,建立网络连接。
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();
}
我们现在已经完成了spreadsheet程序的用户界面部分。在下一章,我们会完成整个程序的代码,实现其中一些核心的代码。