分享

qt Spreadsheet

 tianht 2015-04-17

创建main窗口

这一章会教会你如何用Qt创建main窗口。最后,你将会学会为应用程序建立完整的的UI界面,包括菜单,工具条,状态栏,以及一些程序设计到的对话框。

一个程序的main窗口提供框架,用户界面则建立在这个框架之上。这一章中我们会编写一个spreadsheet的应用程序,这个程序会用到第2章中创建的Find, Go To Cell,及Sort对话框。

 

在大多数GUI程序背后,会有一块代码来实现一些功能。例如,读写文件的代码,或者是处理显示在UI上的数据的代码。在第4章中,我们会知道怎么实现这些功能,还是用spreadsheet作为我们练习的例子。

 

继承QMainWindow

应用程序的主窗口是通过继承QMainWindow类来实现。我们在第2章中看到的很多创建对话框的技术跟建立主窗口也是相关的,因为QDialogQMainWindow都是从QWidget类继承的。

主窗口可能用Qt Designer来创建,但在这一章中,我们全部用纯手工编码来实现,这样我们就可以了解整个过程是如何实现的。如果你更喜欢用可视化的方法,可以参考Qt Designer在线手册中“Create Main Windows in Qt Designer”一章。

Spreadsheet程序主窗口的源代码有mainwindow.hmainwindow.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|NewHelp|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支持很多图像格式,包括BMPGIFJPEGPNGPNMSVGTIFF,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操作在WindowsKDEGNOME中的快捷键为Ctrl+N,在Mac OS X中为Command+N。通过使用合适的QKeySequence::StandardKey枚举量,我们可以确保Qt在应用程序所在的平台上提供正确的快捷键。

我们连接操作的triggered()信号和主窗口的私有newFile()槽函数。这个连接确保当用户选择File|New菜单,或点击工具栏中的New按钮,或按下Ctrl+N,这个newFile()槽函数会被调用。

OpenSaveSave 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, SaveSave 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,返回falseSpreadsheet::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, NoCancel

QMessageBox提供很多标准的按钮,并且自动设置一个按钮为默认按钮(当用户按下Enter键时执行),设置一个按钮为退出按钮(当用户按下Esc.我们也可以设置其他的按钮为默认按钮和退出按钮,也可以设置按钮显示的文本。

Warning()函数的调用看起来会觉得复杂,看看下面的语法定义:

QMessageBox::warning(parenttitlemessagebuttons);

 

除了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()来保存程序当前的设置。

当最后一个窗口被关闭时,应用程序结束。如果有需要,我们也可以禁止这个行为,只要设置QApplicationquitOnLastWindowClosed属性为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 &copy; 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程序的用户界面部分。在下一章,我们会完成整个程序的代码,实现其中一些核心的代码。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多