创建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./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()函数如下: { 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的操作: { ... 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程序的用户界面部分。在下一章,我们会完成整个程序的代码,实现其中一些核心的代码。
|
|