【学习笔记】C++ GUI Qt4 项目之SpreadSheet源码合集(Qt5可运行版本)


详细的源码打包下载地址:https://download.csdn.net/download/King_why_love/12518707
也可以关注我的微信公众号: 不止编程,回复 SpreadSheet获取。


SpreadSheet(电子表格程序)主要包含书中第二、三、四章节内容。
1、SpreadSheet是一个Excel操作封装类,使用起来比其他的更为方便。
2、修正了书中SpreadSheet几个错误问题。
3、压缩包里面包含SpreadSheet的源码详细注释,以及编译打包后的文件。
4、使用QtCreator Qt5.6.3版本编译通过。
在这里插入图片描述

1 项目运行结果

主界面

在这里插入图片描述
在这里插入图片描述

2 项目结构

源码目录

源码目录

项目结构图

在这里插入图片描述

3 项目源码

3.1 main.cpp

#include <QApplication>

#include "mainwindow.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    MainWindow mainWin;
    mainWin.show();
    return app.exec();
}

3.2 MainWindow类

//mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

class QAction;
class QLabel;
class FindDialog;
class Spreadsheet;

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{//MainWindow类定义为QMainWindow类的子类。
 //由于类MainWindow提供了自己的信号和槽,所以它包含了Q_OBJECT宏
    Q_OBJECT

public:
    MainWindow();
    ~MainWindow();

protected:
    void closeEvent(QCloseEvent *event);
    //closeEvent()函数是QWidget类中的一个虚函数,当用户关闭窗口时,这个函数会被自动调用。
    //类MainWindow中重新实现了它,这样就可以向用户询问一个标准问题“Do you want to save yourchanges?" ,并且可以把用户的一些偏好设置保存到磁盘中。

private slots:
    void newFile();
    void open();
    bool save();
    bool saveAs();
    void find();
    void goToCell();
    void sort();
    void about();
    /*
     * 像File->New和Help->About这样的菜单项,在MainWindow中会被实现为私有槽。
     * 除了save()槽和saveAs( )槽返回一
     * 个bool值以外,绝大多数的槽都把void作为它们的返回值。
     * 当槽作为一个信号的响应函数而被执行时,就会忽略这个返回值;
     * 但是当把槽作为函数来调用时,其返回值对我们的作用就和调用任何一个普通的C++函数时的作用是相同的。
     */
    void openRecentFile();
    void updateStatusBar();
    void spreadsheetModified();

private:
    Ui::MainWindow *ui;
    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;
    QMenu *selectSubMenu;
    QMenu *toolsMenu;
    QMenu *optionsMenu;
    QMenu *helpMenu;
    QToolBar *fileToolBar;
    QToolBar *editToolBar;
    QAction *newAction;
    QAction *openAction;
    QAction *saveAction;
    QAction *saveAsAction;
    QAction *exitAction;
    QAction *cutAction;
    QAction *copyAction;
    QAction *pasteAction;
    QAction *deleteAction;
    QAction *selectRowAction;
    QAction *selectColumnAction;
    QAction *selectAllAction;
    QAction *findAction;
    QAction *goToCellAction;
    QAction *recalculateAction;
    QAction *sortAction;
    QAction *showGridAction;
    QAction *autoRecalcAction;
    QAction *aboutAction;
    QAction *aboutQtAction;
};

#endif // MAINWINDOW_H
//mainwindow.cpp
#include <QtWidgets>//包含了在子类中所要用到的所有Qt类的定义

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "finddialog.h"
#include "gotocelldialog.h"
#include "sortdialog.h"
#include "spreadsheet.h"


MainWindow::MainWindow():ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    spreadsheet = new Spreadsheet;
    setCentralWidget(spreadsheet);
    //创建一个Spreadsheet窗口,并设置为中央窗口部件
    //Spreadsheet类是QTableWidget类的一个子类,并且也具有一些电子制表软件的功能

    createActions();
    createMenus();
    createContextMenu();
    createToolBars();
    createStatusBar();
    //创建主窗口中的其余部分

    readSettings();
    //读取这个应用程序存储的一些设置

    findDialog = 0;
    //我们把findDialog指针初始化为空(null)指针。在第一次调用MainWindow::find()函数时,将会创建该FindDialog对象。

    setWindowIcon(QIcon(":/images/icon.png"));
    //把窗口图标设置为icon.png
    //资源机制
    setCurrentFile("");
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::closeEvent(QCloseEvent *event)
{
    /* 当用户单击File->Exit或者单击窗口标题栏中的关闭按钮时,将会调用QWidget::close()槽。
     * 该槽会给这个窗口部件发射一个”close“事件。通过重新实现QWidget::closeEvent()函数就可以中途截取对这个主窗口的关闭操作,并且可以确定到底是不是真的要关闭这个窗口。
     * 如果存在未保存的更改并且用户选择了Cancel, 就会“忽略”这个关闭事件并且让这个窗口不受该操作的影响。
     * 一般情况下,我们会接受这个事件,这会让Qt隐藏该窗口。
     * 也可以调用私有函数writeSettings()来保存这个应用程序的当前设置。
     * 当最后一个窗口关闭后,这个应用程序就结束了。
     * 如果需要,通过把QApplication的quitOnLastWindowClosed属性设置为false,可以禁用这种行为。
     * 在这种情况下,该应用程序将会持续保持运行,直到调用QApplication:: quit()函数,程序才会结束。
     */
    if (okToContinue()) {
        writeSettings();
        event->accept();
    } else {
        event->ignore();
    }
}

void MainWindow::newFile()
{
    /* 当用户点击File->New菜单项或者单击工具栏上的New按钮时,就会调用newFile()槽。
     * 如果存在还没有被保存的信息, okToContinue( )私有函数就会弹出对话框:“Do you wantto save your changes?"。
     * 如果用户选择Yes或者No(保存文档应该选择Yes),这个函数会返回true;
     * 如果用户选择Cancel,它就返回false。 Spreadsheet::clear()函数会清空电子制表软件中的全部单元格和公式。
     * setCurentFile()私有函数会更新窗口的标题,以说明正在编辑的是一个没有标题的文档,它还会设置curFile私有变量并且更新最近打开文件的列表。
     */
    if (okToContinue()) {
        spreadsheet->clear();
        setCurrentFile("");
    }
}

void MainWindow::open()
{
    /* open()槽对File->Open做出响应。就像newFile()一样,它首先调用okToContiniue()函数来处理任何没有被保存的变化。
     * 然后它使用方便的QFileDialog::getOpenFileName()静态函数从用户那里获得一个新的文件名。
     * 这个函数会弹出一个文件对话框,让用户选择一个文件,并且返回这个文件名——或者,如果用户单击了Cancel按钮,则返回一个空字符串。
     */
    if (okToContinue()) {
        /* 传递给QFileDialog::getOpenFileName()函数的第一个参数是它的父窗口部件。
         * 用于对话框和其他窗口部件的这种父子对象关系意义并不相同。
         * 对话框通常都拥有自主权,但是如果它有父对象,那么在默认情况下,它就会居中放到父对象上。
         * 一个子对话框也会共用它的父对象的任务栏。
         * 第二个参数是这个对话框应当使用的标题。
         * 第三个参数告诉它应当从哪一级目录开始,在这个例子中就是当前目录。
         * 第四个参数指定了文件过滤器。文件过滤器(filter)由一个描述文本和一个通配符组成。
         * 如果除了要支持Spreadsheet本地文件格式以外,还需要支持采用逗号分隔的数据文件和Lotus 1-2-3 文件,就应当使用如下的文件过滤器:
         * tr( "Spreadsheet files (* . sp)\n"
         * "Comma-separated values files (* . csv)\n"
         * "Lotus 1-2-3 files (* .wk1 * . wks)")
         */
        QString fileName = QFileDialog::getOpenFileName(this,
                                   tr("Open Spreadsheet"), ".",
                                   tr("Spreadsheet files (*.sp)"));
        if (!fileName.isEmpty())
            //loadFile()私有函数是在open()中得到调用的,它用来载入文件。
            //我们让它成为一个独立的函数,是因为会在载入最近打开的文件中使用同样的功能。
            loadFile(fileName);
    }
}

bool MainWindow::save()
{
    /* save()槽对File->Save做出响应。
     * 如果因为这个文件是之前打开的文件或者它是一个已经保存过的文件,这样已经有了一个名字,
     * 那么save()函数就会用这个名字调用saveFile()函数;
     * 否则,它只是简单地调用saveAs()函数。
     */
    if (curFile.isEmpty()) {
        return saveAs();
    } else {
        return saveFile(curFile);
    }
}

bool MainWindow::saveAs()
{
    /* saveAs()槽对File->Save As做出响应。
     * 调用QFileDialog::getSaveFileName()函数来从用户那里得到一个文件名。
     * 如果用户单击了Cancel,则返回false,这将会使这个结果向上传递给它的调用者[save()或者okToContinue()]。
     * 如果给定的文件已经存在, geSaveFileName( )函数将会要求用户确认是否需要覆盖该文件。
     * 但通过给getSveFileNarne() 函数传递一个QFileDialog::DontConfirmOverwite附加参数,则可以改变这一行为。
     */
    QString fileName = QFileDialog::getSaveFileName(this,
                               tr("Save Spreadsheet"), ".",
                               tr("Spreadsheet files (*.sp)"));
    if (fileName.isEmpty())
        return false;

    return saveFile(fileName);
}

void MainWindow::find()
{
    //创建非模态对话框时,通常会把它的信号连接到能够对用户的交互做出响应的那些槽上。
    if (!findDialog) {
        findDialog = new FindDialog(this);
        connect(findDialog, SIGNAL(findNext(const QString &,
                                            Qt::CaseSensitivity)),
                spreadsheet, SLOT(findNext(const QString &,
                                           Qt::CaseSensitivity)));
        connect(findDialog, SIGNAL(findPrevious(const QString &,
                                                Qt::CaseSensitivity)),
                spreadsheet, SLOT(findPrevious(const QString &,
                                               Qt::CaseSensitivity)));
    }
    /* Find对话框是一个可以让用户在电子制表软件中搜索文本的窗口。当用户单击Edit->Find时,就会调用find( )槽来弹出Find对话框。
     * 这时,就可能出现下列几种情形:
     * ●这是用户第一次调用Find对话框。
     * ●以前曾经调用过Find对话框,但用户关闭了它。.
     * ●以前曾经调用过Find对话框,并且现在它还是可见的。
     * 如果Find对话框还不曾存在过,就可以创建它并且把它的findNext()信号和findPrevious()信号与Spreadsheet中相对应的那些槽连接起来。
     * 本应该在MainWndow的构造函数中创建这个对话框,但是推迟对话框的创建过程将可以使程序的启动更加快速。
     * 还有,如果从来没有使用到这个对话框,那么它就决不会被创建,这样可以既节省时间又节省内存。
     * 然后,调用show()、raise()和activateWindow()来确保窗口位于其他窗口之上并且是可见的和激活的。
     * 只调用show()就足以让一个隐藏窗口变为可见的、位于最上方并且是激活的,
     * 但是也有可能是在Find对话框窗口已经是可见的时候又再次调用了它,在这种情况下,show()调用可能什么也不做,
     * 那么就必须调用raise()和activateWindow()让窗口成为顶层窗口和激活状态。
     */
    findDialog->show();
    findDialog->raise();
    findDialog->activateWindow();
}

void MainWindow::goToCell()
{
    /* 如果对话框被接受,函数QDialog::exec()可返回一个true值(QDialog::Accepted),否则就会返回一个false值(QDialog::Rected)。
     * 可以回想一下,当初在第2章利用Qt设计师创建Go to Cell对话框时,就曾经把OK连接到accept(),把Cancel连接到reject()。
     * 如果用户选择0K,就把当前单元格的值设置成行编辑器中的值。
     */
    GoToCellDialog dialog(this);
    if (dialog.exec()) {
        QString str = dialog.getLineEdit();
        spreadsheet->setCurrentCell(str.mid(1).toInt() - 1,
                                    str[0].unicode() - 'A');
    }
    /* QTable::setCurentCell()函数需要两个参数:一个行索引和一个列索引。
     * 在Spreadsheet 应用程序中,单元格A1就是单元格(0,0),单元格B27就是单元格(26,1)。
     * 为了从函数QLineEdit::text()返回的QString中获得行索引,可以使用QString::mid()来提取行号(这个函数将返回一个从字符串的开始直到末尾位置的子字符串),
     * 然后使用QString::toInt()把它转换成一个整数值,并且把该值再减去1。
     * 对于列号,则可以用这个字符串中第一个字符的大写数值减去字符‘A'的数值而得到。
     * 我们知道,该字符串将具有正确的格式,因为为对话框创建了一个QRegExpValidator检验器,只有满足一个字符后面再跟至多三位数字格式的字符串才能让OK按钮起作用。
     * goToCell()函数与目前着到的所有代码都有些不同,因为它在堆栈中创建了一个作为变量的窗口部件(一个GoToCellDialog)。
     * 虽然多使用了一行代码,但是换来了不需要使用new和delete的简便。
     * 由于在使用完一个对话框(或者菜单)后,通常就不再需要它了,所以在堆栈中创建对话框(和上下文菜单)是一种常见的编程模式,并且对话框会在作用域结束后自动销毁掉。
     */
}

void MainWindow::sort()
{
    /* sort()函数中的代码使用了一种和goToCell()函数中用到的类似模式:
     * ● 在堆栈中创建对话框并且对其进行初始化。
     * ● 使用exec()弹出对话框。
     * ● 如果用户单击0K,就从对话框的各个窗口部件中提取并且使用这些用户输入的值。
     * setColumnRange()调用将那些可用于排序的列变量设置为选定的列。
     * 例如,使用图3.14中的选择,range.leftColumn()将返回值0,即‘A'+0=‘A’,并且range.rightColumn()将返回值2,即‘A' +2=‘C'。
     * compare对象储存了主键、第二键和第三键以及它们的排序顺序。(将会在下一章中看到SpreadsheetCompare类的定义)
     * 这个对象会由Spreadsheet::sort()使用,用于两行的比较。
     * keys数组存储了这些键的列号。
     * 例如,如果选择区域是从C2扩展到E5,那么列C的位置就是0。
     * ascending数组中按bool格式存储了和每一个键相关的顺序。
     * QComboBox::currentIndex()返回当前选定项的索引值,该值是一个从0开始的数。
     * 对于第二键和第三键,考虑到“None”项,我们从当前项减去1。
     */
    SortDialog dialog(this);
    QTableWidgetSelectionRange range = spreadsheet->selectedRange();
    dialog.setColumnRange('A' + range.leftColumn(),
                          'A' + range.rightColumn());

    if (dialog.exec()) {
        SpreadsheetCompare compare;
        compare.keys[0] =
              dialog.getPrimaryColumnComboIndex();
        compare.keys[1] =
              dialog.getSecondaryColumnComboIndex() - 1;
        compare.keys[2] =
              dialog.getTertiaryColumnComboIndex() - 1;
        compare.ascending[0] =
              (dialog.getPrimaryOrderComboIndex() == 0);
        compare.ascending[1] =
              (dialog.getSecondaryOrderComboIndex() == 0);
        compare.ascending[2] =
              (dialog.getTertiaryOrderComboIndex() == 0);
        spreadsheet->sort(compare);
    }
}

void MainWindow::about()
{
    //通过调用一个方便的静态函数QMessageBox::about(),就可以获得About对话框。
    //这个函数和QMessageBox::warming()的形式非常相似,只是它使用了父窗口的图标,而不是标准的“瞥告”图标。
    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."));
}

void MainWindow::openRecentFile()
{
    /* 当用户选择了一个最近打开的文件,就会调用openRecentFile()槽。
     * 只要有任何未保存的变化,就会调用okToContinue()函数,并且假定用户没有取消,还可以使用QObject::sender()查出是哪个特有动作调用了这个槽。
     * qobject_cast <T>()函数可在Qt的moc(meta-object comipiler, 元对象编译器)所生成的元信息基础上执行动态类型强制转换(dynamic cast)。
     * 它返回一个指向所需QObject子类的指针,或者是在该对象不能被转换成所需的那种类型时返回0。
     * 与标准C++的dynamic_ cast <T> ()不同,Qt的qobject_ cast <T> ()可正确地跨越动态库边界。
     * 在例子中,使用qobject _cast <T> ()把一个QObject指针转换成QAction指针。
     * 如果这个转换是成功的(应当是这样的),就可以利用从动作的data项中所提取的文件全名来调用loadFile()函数。
     * 顺便值得一提的是,由于知道这个发射器是一个QAction,如果使用static_cast <T> ()或者传统的C风格的数据类型强制转换代替原有的数据转换方式,这个程序应当仍然是可以运行的。
     * 请参见附录D中“类型转换”一节对不同C++数据类型强制转换的概述。
     */
    if (okToContinue()) {
        QAction *action = qobject_cast<QAction *>(sender());
        if (action)
            loadFile(action->data().toString());
    }
}

void MainWindow::updateStatusBar()
{
    //updatestatusBar()槽可以更新单元格定位指示器和单元格公式指示器。
    //只要用户把单元格光标移动到一个新的单元格,这个槽就会得到调用。
    locationLabel->setText(spreadsheet->currentLocation());
    formulaLabel->setText(spreadsheet->currentFormula());
}

void MainWindow::spreadsheetModified()
{
    //spreadsheeModified()槽把windowModified属性设置为true,用以更新标题栏。
    //这个函数也会更新位置和公式指示器,以便可以让它们反映事件的当前状态。
    setWindowModified(true);
    updateStatusBar();
}

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即Alt+N)、一个父对象(主窗口)、一个图标、一个快捷键和一个状态提示。
     * 大多数窗口系统都有用于特定动作的标准化的键盘快捷键。
     * 例如,在Windows、KDE和GNOME中,这个New动作就有一个快捷键Ctrl+N,而在MacOSX中则是Command+N。
     * 通过使用适当的QKeySequence::StandardKey枚举值,就可以确保Qt能够为应用程序在其运行的平台上提供正确的快捷键。
     * 把这个动作的tiggered()信号连接到主窗口的私有槽newFile()——将会在下一节实现它。
     * 这个连接可以确保在用户选择File New菜单项、选择工具栏上的New按钮或者按下Ctrl+N时,都可以调用newFile()槽。
     * 由于菜单中的Open、Save和SaveAs动作与New动作非常相似,
     * 所以将会直接跳到File菜单中的“recently opened files"(最近打开的文件)的部分。
     */

    openAction = new QAction(tr("&Open..."), this);
    openAction->setIcon(QIcon(":/images/open.png"));
    openAction->setShortcut(QKeySequence::Open);
    openAction->setStatusTip(tr("Open an existing spreadsheet file"));
    connect(openAction, SIGNAL(triggered()), this, SLOT(open()));

    saveAction = new QAction(tr("&Save"), this);
    saveAction->setIcon(QIcon(":/images/save.png"));
    saveAction->setShortcut(QKeySequence::Save);
    saveAction->setStatusTip(tr("Save the spreadsheet to disk"));
    connect(saveAction, SIGNAL(triggered()), this, SLOT(save()));

    saveAsAction = new QAction(tr("Save &As..."), this);
    saveAsAction->setStatusTip(tr("Save the spreadsheet under a new "
                                  "name"));
    connect(saveAsAction, SIGNAL(triggered()), this, SLOT(saveAs()));

    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动作与目前为止所看到的那些动作稍微有些不同。由于没有用于终止应用程序的标准化键序列,所以需要在这里明确指定键序列。
     * 另外一个不同之处是:我们连接的是窗口的close()槽,而它是由Qt提供的。
     */

    cutAction = new QAction(tr("Cu&t"), this);
    cutAction->setIcon(QIcon(":/images/cut.png"));
    cutAction->setShortcut(QKeySequence::Cut);
    cutAction->setStatusTip(tr("Cut the current selection's contents "
                               "to the clipboard"));
    connect(cutAction, SIGNAL(triggered()), spreadsheet, SLOT(cut()));

    copyAction = new QAction(tr("&Copy"), this);
    copyAction->setIcon(QIcon(":/images/copy.png"));
    copyAction->setShortcut(QKeySequence::Copy);
    copyAction->setStatusTip(tr("Copy the current selection's contents "
                                "to the clipboard"));
    connect(copyAction, SIGNAL(triggered()), spreadsheet, SLOT(copy()));

    pasteAction = new QAction(tr("&Paste"), this);
    pasteAction->setIcon(QIcon(":/images/paste.png"));
    pasteAction->setShortcut(QKeySequence::Paste);
    pasteAction->setStatusTip(tr("Paste the clipboard's contents into "
                                 "the current selection"));
    connect(pasteAction, SIGNAL(triggered()),
            spreadsheet, SLOT(paste()));

    deleteAction = new QAction(tr("&Delete"), this);
    deleteAction->setShortcut(QKeySequence::Delete);
    deleteAction->setStatusTip(tr("Delete the current selection's "
                                  "contents"));
    connect(deleteAction, SIGNAL(triggered()),
            spreadsheet, SLOT(del()));

    selectRowAction = new QAction(tr("&Row"), this);
    selectRowAction->setStatusTip(tr("Select all the cells in the "
                                     "current row"));
    connect(selectRowAction, SIGNAL(triggered()),
            spreadsheet, SLOT(selectCurrentRow()));

    selectColumnAction = new QAction(tr("&Column"), this);
    selectColumnAction->setStatusTip(tr("Select all the cells in the "
                                        "current column"));
    connect(selectColumnAction, SIGNAL(triggered()),
            spreadsheet, SLOT(selectCurrentColumn()));

    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()));
    //由于槽seletAll()是由QTableWidget的父类之一的QAbstractItemView提供的,所以就没有必要再去亲自实现它。

    findAction = new QAction(tr("&Find..."), this);
    findAction->setIcon(QIcon(":/images/find.png"));
    findAction->setShortcut(QKeySequence::Find);
    findAction->setStatusTip(tr("Find a matching cell"));
    connect(findAction, SIGNAL(triggered()), this, SLOT(find()));

    goToCellAction = new QAction(tr("&Go to Cell..."), this);
    goToCellAction->setIcon(QIcon(":/images/gotocell.png"));
    goToCellAction->setShortcut(tr("Ctrl+G"));
    goToCellAction->setStatusTip(tr("Go to the specified cell"));
    connect(goToCellAction, SIGNAL(triggered()),
            this, SLOT(goToCell()));

    recalculateAction = new QAction(tr("&Recalculate"), this);
    recalculateAction->setShortcut(tr("F9"));
    recalculateAction->setStatusTip(tr("Recalculate all the "
                                       "spreadsheet's formulas"));
    connect(recalculateAction, SIGNAL(triggered()),
            spreadsheet, SLOT(recalculate()));

    sortAction = new QAction(tr("&Sort..."), this);
    sortAction->setStatusTip(tr("Sort the selected cells or all the "
                                "cells"));
    connect(sortAction, SIGNAL(triggered()), this, SLOT(sort()));

    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)));
#if QT_VERSION < 0x040102
    // workaround for a QTableWidget bug in Qt 4.1.1
    connect(showGridAction, SIGNAL(toggled(bool)),
            spreadsheet->viewport(), SLOT(update()));
#endif
    /* Show Grid是一个复选(checkable)动作。复选动作在菜单中显示时会带一个复选标记,并且在工具栏中它可以实现成切换(toggle)按钮。
     * 当启用这个动作时,Spreadsheet组件就会显示一个网格。
     * 我们用Spreadsheet组件的默认值来初始化这个动作,这样它们就可以从一开始就同步起来。
     * 然后,把Show Grid动作的toggled(bool)信号和Spreadsheet组件的setShowGrid(bool)槽连接起来,这个槽继承自QTableWidget。
     * 一旦把这个动作添加到菜单或者工具栏中,用户就可以对网格的显示与否进行切换了。
     * Show Grid动作和AutoRecalculate动作是相互独立的两个复选动作。通过QActionGroup类的支持,Qt也可以支持相互排斥的动作。
     */

    autoRecalcAction = new QAction(tr("&Auto-Recalculate"), this);
    autoRecalcAction->setCheckable(true);
    autoRecalcAction->setChecked(spreadsheet->autoRecalculate());
    autoRecalcAction->setStatusTip(tr("Switch auto-recalculation on or "
                                      "off"));
    connect(autoRecalcAction, SIGNAL(toggled(bool)),
            spreadsheet, SLOT(setAutoRecalculate(bool)));

    aboutAction = new QAction(tr("&About"), this);
    aboutAction->setStatusTip(tr("Show the application's About box"));
    connect(aboutAction, SIGNAL(triggered()), this, SLOT(about()));

    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动作,通过访问qApp全局变量,我们可以使用QApplication对象的aboutQt()槽。
}

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窗口部件,并且会把它添加到菜单栏中。
     * QMainWindow::menuBar()函数返回一个指向.QMenuBar的指针。
     * 菜单栏会在第一次调用menuBar()函数的时候就创建出来。
     * 从创建File菜单开始,然后再把New、Open、Save和SaveAs动作添加进去。
     * 插入一个间隔器(separator),可以从视觉上把关系密切的这些项放在一起。
     * 使用一个for循环从recentFileActions数组中添加一些动作(最初是隐藏起来的),然后在最后添加一个exitAction动作。
     * 我们已经让一个指针指向了这些间隔器中的某一个。
     * 这样就可以允许隐藏(如果没有最近文件的话)或者显示那个间隔器,因为不希望出现在两个间隔器之间什么都没有的情况。
     */

    editMenu = menuBar()->addMenu(tr("&Edit"));
    editMenu->addAction(cutAction);
    editMenu->addAction(copyAction);
    editMenu->addAction(pasteAction);
    editMenu->addAction(deleteAction);
    //现在来创建Edit菜单,就像在File菜单中所做的那样使用QMenu::addMenu()函数添加各个动作,并且在希望出现子菜单的地方使用OMenu::addMenu()函数添加子菜单。
    //一个子菜单与它所属的菜单一样,也是一个QMenu。

    selectSubMenu = editMenu->addMenu(tr("&Select"));
    selectSubMenu->addAction(selectRowAction);
    selectSubMenu->addAction(selectColumnAction);
    selectSubMenu->addAction(selectAllAction);

    editMenu->addSeparator();
    editMenu->addAction(findAction);
    editMenu->addAction(goToCellAction);

    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窗口部件都可以有一个与之相关联的QActions列表。
     * 要为该应用程序提供一个上下文菜单,可以将所需要的动作添加到Spreadsheet窗口部件中,并且将那个窗口部件的上下文菜单策略设置为一个显示这些动作的上下文菜单。
     * 当用户在一个窗口部件上单击鼠标右键,或者是在键盘上按下一个与平台相关的按键时,就可以激活这些上下文菜单。
     */
    /* 一种更为高级的提供上下文菜单方法是重新实现QWidget::contextMenuEvent()函数,
     * 创建一个QMenu窗口部件,在其中添加所期望的那些动作,并且再对该窗口部件调用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工具栏。就像菜单-样,工具栏也可以有多个间隔器。
}

void MainWindow::createStatusBar()
{
    /* 随着菜单和工具栏的完成,已经为设置Spreadsheet应用程序的状态栏做好了准备。
     * 在程序的普通模式下,状态栏包括两个状态指示器:当前单元格的位置和当前单元格中的公式。
     * 状态栏也用于显示状态提示和其他一些临时消息。见下图给出了各种情况下的状态栏。
     */
    locationLabel = new QLabel(" W999 ");
    locationLabel->setAlignment(Qt::AlignHCenter);
    locationLabel->setMinimumSize(locationLabel->sizeHint());
    /* 状态栏指示器是一些简单的QLabel,可以在任何需要的时候改变它们的文本。
     * 当把这些QLabel添加到状态栏的时候,它们会自动被重定义父对象,以便让它们成为状态栏的子对象。
     * 当QStatusBar摆放这些指示器窗口部件时,它会尽量考虑由QWidget::sizeHint()提供的每一个窗口部件的理想大小,然后再对那些可伸展的任意窗口部件进行伸展以填满全部可用空间。
     * 一个窗口部件的理想大小取决于这个窗口部件的内容以及改变内容时的变化大小。
     * 为了避免对定位指示器连续不断地重定义大小,设置它的最小尺寸大小为它所能包含的最大字符数(“W999")和一些空格的总大小。
     * 还把它的对齐方式设置为Qt::AlignHCenter,以便可以在水平方向上居中对齐它的文本。
     */

    formulaLabel = new QLabel;
    formulaLabel->setIndent(3);
    // 已经在formulaLabel中添加了一个缩进格式,以便让那些在它里面显示的文本能够与它的左侧边有一个小的偏移量。

    statusBar()->addWidget(locationLabel);
    statusBar()->addWidget(formulaLabel, 1);
    /* QMainWindow::statusBar()函数返回一个指向状态栏的指针。[在第一次调用statusBar()函数的时候会创建状态栏。]
     * 两个标签都有不同的空间需求。单元格定位指示器只需要非常小的空间,并且在重新定义窗口大小时,任何多余的空间都会分配给位于右侧的单元格公式指示器。
     * 这是通过在公式标签的QStatusBar::addWidge()调用中指定一个伸展因子1而实现的。位置指示器的默认伸展因子为0,这也就意昧着它不喜欢被伸展。
     */

    connect(spreadsheet, SIGNAL(currentCellChanged(int, int, int, int)),
            this, SLOT(updateStatusBar()));
    connect(spreadsheet, SIGNAL(modified()),
            this, SLOT(spreadsheetModified()));
    //在函数结尾的附近,把Spreadsheet的两个信号和MainWindow的两个槽, updateStatusBar()和spreadsheetModifed(),连接了起来。

    updateStatusBar();
    //该槽也可以作为一个普通函数而在createStatusBar()的最后用于初始化这些指示器。
    //因为Spreadsheet不会在一开始的时候就发射currentCellChanged()消息,所以还必需这样做。
}

void MainWindow::readSettings()
{
    /* readSettings()函数可以载人之前使用witeSttings()函数所保存的那些设置。
     * value()函数中的第二个参数可以在没有可用设置的情况下指定所需的默认值。
     * 在应用程序第一次运行时,使用的就是这些默认值。
     * 由于没有为形状或者最近打开文件列表指定第二个参数,所以在第一次运行时,窗口会使用任意但是却合理的大小和位置,而最近文件列表会是一个空表。
     * 在redSetings()和writettings()中使用与QSettings相关的全部代码为MainWindow所选择的布置方案,都只是许多可用方案中的一种而已。
     * 可以在应用程序执行期间的任何时候和程序代码中的任何地方,随时随地创建一个Qetings对象,用它查询或者修改一些设置。
     */
    QSettings settings("Software Inc.", "Spreadsheet");

    restoreGeometry(settings.value("geometry").toByteArray());

    recentFiles = settings.value("recentFiles").toStringList();
    updateRecentFileActions();

    bool showGrid = settings.value("showGrid", true).toBool();
    showGridAction->setChecked(showGrid);

    bool autoRecalc = settings.value("autoRecalc", true).toBool();
    autoRecalcAction->setChecked(autoRecalc);
}

void MainWindow::writeSettings()
{
    /* writettings()函数保存了主窗口的几何形状(位置和尺寸大小)、最近打开文件列表以及Show Grid和Auto Recalculate选项的设置值。
     * 默认情况下,QStings会存储应用程序中与特定平台相关的一些设置信息。
     * 在Windows系统中,它使用的是系统注册表;在UNIX系统中,它会把设置信息存储在文本文件中;在MacOSx中,它会使用Core Foundation Preferences 的应用程序编程接口。
     * 构造函数的参数说明了组织的名字和应用程序的名字。采用与平台相关的方式,可以利用这一信息查找这些设置所在的位置。
     * QSettings把设置信息存储为键值对( key-value pair) 的形式。键(key)与文件系统的路径很相似。
     * 可以使用路径形式的语法(例如, findDialog/matchCase)来指定子键(subkey)的值,或者也可以使用beginGroup()和endGroup()的形式
     * 值(value)可以是一个int、bol double、QString、QStingList或者是QVariant所支持的其他任意类型,包括那些已经注册过的自定义类型。
     */
    QSettings settings("Software Inc.", "Spreadsheet");

    settings.setValue("geometry", saveGeometry());
    settings.setValue("recentFiles", recentFiles);
    settings.setValue("showGrid", showGridAction->isChecked());
    settings.setValue("autoRecalc", autoRecalcAction->isChecked());
}

bool MainWindow::okToContinue()
{
    //在okToContinue()函数中,会检测windowModifed属性的状态。如果该属性的值是true,就显示一个如下图所示的消息框。这个消息框包含一个Yes按钮、一个No按钮和一个Cancel按钮。
    //QMessageBox::warning(parent, title, message, buttons);
    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;
}

bool MainWindow::loadFile(const QString &fileName)
{
    /* 我们使用Spreadsheet::readFile()函数从磁盘中读取文件。
     * 如果载入成功,会调用setCurentFile()函数来更新这个窗口的标题;
     * 否则,Spreadsheet::readFile()将会通过一个消息框把遇到的问题通知给用户。
     * 在通常情况下,让底层组件来报告错误消息是一个不错的习惯,这是因为它们可以提供准确的错误细节信息。
     * 在上述两种情况下,都会在状态栏中显示一个消息2秒(2000毫秒),这样可以通知用户应用程序正在做什么。
     */
    if (!spreadsheet->readFile(fileName)) {
        statusBar()->showMessage(tr("Loading canceled"), 2000);
        return false;
    }

    setCurrentFile(fileName);
    statusBar()->showMessage(tr("File loaded"), 2000);
    return true;
}

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;
}

void MainWindow::setCurrentFile(const QString &fileName)
{
    /* 在setCurrentFile()中,对保存正在编辑的文件名的curFile私有变量进行了设置。
     * 每个QWidget都有一个windowModifed属性,如果该窗口的文档存在没有保存的变化,则应当把它设置为true,否则应当将其设置为falsle。
     * 在Mac OS X下,未保存的文档是通过窗口标题栏上关闭按钮中的一个点来表示的;在其他平台下,则是通过文件名字后跟一个星号来表示的。
     * Qt会自动处理这一行为,只要始终让windowModified属性保持为当前最新状态,并且当需要显示星号的时候,把“[ * ]"标记放在窗口的标题栏上即可。
     * 如果存在文件名,就需要更新应用程序的最近打开文件列表recentFiles。
     * 在把这个文件名显示在标题栏中之前,需要使用strippedName()函数移除文件名中的路径字符,这样可以使文件名看起来更友好一些。
     * 可以调用removeAll()从列表中移除任何已经出现过的文件名,从而避免该文件名的重复。
     * 然后,可以调用prepend()把这个文件名作为文件列表的第一项添加进去。
     * 在更新了文件列表之后,可以调用私有函数updateRecentFileActions()更新File菜单中的那些条目。
     */
    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::arg()函数将会使用自己的参数替换最小数字的“%n”参数,并且会用它的参数返回结果“%n”字符和最终的结果字符串。
     * 在本例中, arg()被用于两个“%n”参数中。第一个arg()调用会替换参数“%1" ,第二个arg()调用则会替换参数“%2”。
     * 如果文件名是budget.sp并且没有载入翻译文件,那么结果字符串将是“budget. sp[*]- Spreadsheet”。
     * 这本应更简单地写作如下代码:setWindowTitle(shownName + tr("[*] - Spreadsheet"));但使用arg()函数可以为翻译人员提供更多的灵活性。
     */
    setWindowTitle(tr("%1[*] - %2").arg(shownName)
                                   .arg(tr("Spreadsheet")));
}

void MainWindow::updateRecentFileActions()
{
    /* 使用一个Java风格的迭代器,可以移除任何不再存在的文件。一些文件或许已经在前面的会话中使用过,但在此之前还没被删除掉。
     * recentFiles 变量的类型是QStringList(QString型列表)。
     * 第11章会详细说明一些像QStringList一样的容器类,其中将会说明它们与C++标准模板库(StandardTemplate Library ,STL)之间的关系,也会说明Qt的Java风格迭代器类的用法。
     */
    QMutableStringListIterator i(recentFiles);
    while (i.hasNext()) {
        if (!QFile::exists(i.next()))
            i.remove();
    }

    /* 然后,再遍历一次文件列表,这一次使用数组风格的索引形式。
     * 对于每一个文件,创建一个由一个与操作符、一位数字(j+ 1)、一个空格和该文件名(不带路径)组成的字符串。
     * 我们要为使用这种文本设置相应的动作。
     * 例如,如果第一个文件是C:\My Documnents\tab04.sp,那么第一个动作的文本将会是“&1 tab04.sp”。
     * 如下图给出了recentFileActions 数组和菜单的最终结果之间的对应关系。
     */
    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());
    /* 每一个动作都可以带一个与之相关的QVariant型data项。QVariant类型可以保存许多C++和Qt型变量,第11章将说明这一点。
     * 这里,将文件的全名保存在动作的data项中,以便随后可以方便地找到它。还要将这个动作设置为可见。
     * 如果有比最新文件更多的文件动作;那么只需隐藏那些多余的动作即可。
     * 最后,如果至少还存在一个最近打开的文件,那么就应该把间隔器设置为可见。
     */
}

QString MainWindow::strippedName(const QString &fullFileName)
{
    //在把这个文件名显示在标题栏中之前,需要使用strippedName()函数移除文件名中的路径字符,这样可以使文件名看起来更友好一些。
    return QFileInfo(fullFileName).fileName();
}

//mainwindow.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>400</width>
    <height>300</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralWidget"/>
  <widget class="QMenuBar" name="menuBar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>400</width>
     <height>26</height>
    </rect>
   </property>
  </widget>
  <widget class="QToolBar" name="mainToolBar">
   <attribute name="toolBarArea">
    <enum>TopToolBarArea</enum>
   </attribute>
   <attribute name="toolBarBreak">
    <bool>false</bool>
   </attribute>
  </widget>
  <widget class="QStatusBar" name="statusBar"/>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <resources/>
 <connections/>
</ui>

3.3 Spreadsheet类

//spreadsheet.h
#ifndef SPREADSHEET_H
#define SPREADSHEET_H

#include <QTableWidget>

class Cell;
class SpreadsheetCompare;

/* 头文件是从Cell和SpreadsheetCompare 类的前置声明开始的。
 * QTableWidget单元格的属性,比如它的文本和对齐方式等,都存储在QTableWidgetItem中。
 * 与QTableWidget不同的是,QTableWidgetItem不是一个窗口部件类,而是一个纯粹的数据类。
 * Cell类派生自QTableWidgetltem,会在本章的最后一节对这个Cell类进行解释。
 */

class Spreadsheet : public QTableWidget
{
    Q_OBJECT
public:
    Spreadsheet(QWidget *parent = 0);

    bool autoRecalculate() const { return autoRecalc; }
    QString currentLocation() const;
    QString currentFormula() const;
    QTableWidgetSelectionRange selectedRange() const;
    void clear();
    bool readFile(const QString &fileName);
    bool writeFile(const QString &fileName);
    void sort(const SpreadsheetCompare &compare);
    /* 之所以把autoRecalculate()函数实现为内联函数,是因为无论自动重新计算的标识符生效与否,它都必须要有返回值。
     * 在第3章中,当实现MainWindow时,我们依赖于Spreadsheet中的一些公有函数。
     * 例如,我们从MainWindow::newFile()中调用clear()来重置电子制表软件。
     * 也使用了一些从QTableWidget中继承而来的函数,特别是setCurrentCell()和setShowGrid()。
     */

public slots:
    void cut();
    void copy();
    void paste();
    void del();
    void selectCurrentRow();
    void selectCurrentColumn();
    void recalculate();
    void setAutoRecalculate(bool recalc);
    void findNext(const QString &str, Qt::CaseSensitivity cs);
    void findPrevious(const QString &str, Qt::CaseSensitivity cs);

signals:
    void modified();

private slots:
    void somethingChanged();
    /* Spreadsheet提供了许多实现Edit、Tols和Options菜单中的动作的槽;
     * 并且它也提供了一个modified()信号,用来告知用户可能已经发生的任何变化。
     * 还定义了一个由Spreadsheet类内部使用的私有槽:
     */

private:
    enum { MagicNumber = 0x7F51C883, RowCount = 999, ColumnCount = 26 };

    Cell *cell(int row, int column) const;
    QString text(int row, int column) const;
    QString formula(int row, int column) const;
    void setFormula(int row, int column, const QString &formula);

    bool autoRecalc;
    //在这个类的私有段中,声明了3个常量、4个函数和1个变量。
};

class SpreadsheetCompare
{
public:
    bool operator()(const QStringList &row1,
                    const QStringList &row2) const;

    enum { KeyCount = 3 };
    int keys[KeyCount];
    bool ascending[KeyCount];
    //在这个头文件的最后,给出了SpreadsheetCompare类的定义。
    //当查看Spreaseet:sort()时,会解释这个类。

    /* SpreadsheetCompare类有些特殊,因为它实现了一个“( )"操作符。
     * 这样就允许把这个类像函数一样使用。
     * 把这样的类称为函数对象(function object) ,或者称为仿函数(functor)。
     * 为了理解仿函数是如何工作的,首先从一个简单的例子开始:
     *
     */
};

/*QStringList row1, row2;
 * SpreadsheetCompare compare;
 * ...
 * if (compare(row1, row2)) {
 * // row1 is less than row2
 * }
 *
 * 使用compare对象就像使用一个普通的compare()函数一样。另外,它的实现可以访问所有存储为成员变量的排序键和排序顺序。
 * 与此方案相同的另一种方法是,把这些排序键和排序顺序存储在全局成员变量中,并且使用一个普通的compare()函数。
 * 然而,在全局成员变量之间通信是一种并不提倡的做法,并且可能会产生一些莫名其妙的问题。
 * 作为像qStableSot()这样的模板函数的接口,仿函数是一种更为常用的做法。
 *
 */

#endif // SPREADSHEET_H
//spreadsheet.cpp
#include <QtWidgets>

#include "cell.h"
#include "spreadsheet.h"

Spreadsheet::Spreadsheet(QWidget *parent)
    : QTableWidget(parent)
{
    autoRecalc = true;

    setItemPrototype(new Cell);
    setSelectionMode(ContiguousSelection);

    connect(this, SIGNAL(itemChanged(QTableWidgetItem *)),
            this, SLOT(somethingChanged()));

    clear();
    /* 通常情况下,当用户在一个空单元格中输入一些文本的时候, QTableWidget将会自动创建一个QTableWidgtItem来保存这些文本。
     * 在电子制表软件中,我们想利用将要创建的Cell项来代替QTableWidgetItem。
     * 这可以通过在构造函数中调用setItemPrototype()来完成。
     * 实际上, QTableWidget会在每次需要新项的时候把所传递的项以原型的形式克隆出来。
     * 同样是在构造函数中,我们将选择模式设置为QAbstractemView::ContiguousSelection,从而可以允许简单矩形选择框方法。
     * 我们把表格窗口部件的ienChanged()信号连接到私有槽somethingChanged()上,
     * 这可以确保在用户编辑一个单元格的时候, somethingChanged()槽可以得到调用。
     * 最后,调用clear()来重新调整表格的尺寸大小并且设置列标题。
     */
}

QString Spreadsheet::currentLocation() const
{
    //currentLocation()函数返回当前单元格的位置,它是按照电子制表软件的通常格式,也就是一个列字母后跟上行号的形式来表示这个位置的值。
    //MainWindow::updateSatusBar()使用它把这个单元格的位置显示在状态栏上。
    return QChar('A' + currentColumn())
           + QString::number(currentRow() + 1);
}

QString Spreadsheet::currentFormula() const
{
    //curentFomula()函数返回当前单元格的公式。它是从MainWindow::upatetatusBar()中得到调用的。
    return formula(currentRow(), currentColumn());
}

QTableWidgetSelectionRange Spreadsheet::selectedRange() const
{
    /* 如果只有一个选择,则只需简单地返回第一个(并且也只有这一个)选择即可。
     * 没有选择的情况应该永远不会发生,因为ContigousSeletion模式至少可以把当前单元格当作是已经选中的选择。
     * 但是,为了避免使程序出现缺陷的可能性,还是需要对这种当前没有选中单元格的情况进行单独处理。
     */
    QList<QTableWidgetSelectionRange> ranges = selectedRanges();
    if (ranges.isEmpty())
        return QTableWidgetSelectionRange();
    return ranges.first();
}

void Spreadsheet::clear()
{
    //clear()函数是从Spreadsheet构造函数中得到调用的,用来初始化电子制表软件。它也会在MainWindow::newFile()中得到调用。
    /* 我们原本使用QTableWidget::clear()来清空所有项和任意选择,但是那样做的话,这些标题将会以当前大小的尺寸而被留下。
     * 相反的是,我们要把表格向下调整为0x0。这样就可以完全清空整个表格,包括这些标题。
     * 然后,重新调整表的大小为ColumnCount * RowCount(26 * 999),
     * 并且把QTableWidgetItem水平方向上的标题修改为列名“A","“B" ,.,“Z”。
     * 不需要设置垂直标题的标签,因为这些标签的默认值是“1”,“2”,.,”999“。后,把单元格光标移动到单元格A1处。
     * QTableWidget由多个子窗口部件构成。
     * 在它的顶部有一个水平的QHeaderView, 左侧有一个垂直的QHeaderView ,还有两个QScrollBar
     * 在它的中间区域被一个名为视口(viewport)的特殊窗口部件所占用, QTableWidget可以在它上面绘制单元格。
     * 通过从QTableView和QAbstraetScrollArea中继承的一些函数,可以访问这些不同的子窗口部件(参见图4.2)。
     * QAbstractSrollArea提供了一个可以滚动的视口和两个可以打开或关闭的滚动条。第6章将讲述QScrollArea子类。
     */
    setRowCount(0);
    setColumnCount(0);
    setRowCount(RowCount);
    setColumnCount(ColumnCount);

    for (int i = 0; i < ColumnCount; ++i) {
        QTableWidgetItem *item = new QTableWidgetItem;
        item->setText(QString(QChar('A' + i)));
        setHorizontalHeaderItem(i, item);
    }

    setCurrentCell(0, 0);
}

bool Spreadsheet::readFile(const QString &fileName)
{
    /* readFile()函数与writeFile() 函数非常相似。
     * 我们使用QFile读取一个文件,但这一次使用的是QIODevice::ReadOnly标记,而不是QIODevice::WriteOnly标记。
     * 然后,把QDataStream的版本设置为9。
     * 用于读取文件的格式必须总是与输出文件的格式相同。
     * 如果该文件在开始处具有正确的幻数(magic number),那么可以调用clear()来清空电子制表软件中的所有单元格,并且读入单元格中的数据。
     * 由于该文件中只包含那些非空单元格的数据,并且也不大可能重置电子制表软件中的每个单元格,所以必须确保在读入数据之前已经清空了所有的单元格。
     */
    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly)) {
        QMessageBox::warning(this, tr("Spreadsheet"),
                             tr("Cannot read file %1:\n%2.")
                             .arg(file.fileName())
                             .arg(file.errorString()));
        return false;
    }

    QDataStream in(&file);
    in.setVersion(QDataStream::Qt_4_3);

    quint32 magic;//用于匹配文件格式
    in >> magic;
    if (magic != MagicNumber) {
        QMessageBox::warning(this, tr("Spreadsheet"),
                             tr("The file is not a Spreadsheet file."));
        return false;
    }

    clear();

    quint16 row;
    quint16 column;
    QString str;

    QApplication::setOverrideCursor(Qt::WaitCursor);
    while (!in.atEnd()) {
        in >> row >> column >> str;
        setFormula(row, column, str);
    }
    QApplication::restoreOverrideCursor();
    return true;
}

bool Spreadsheet::writeFile(const QString &fileName)
{
    /* 从MainWindow::saveFile()中调用的writeFile()函数把文件输出到磁盘中。
     * 如果输出成功,它会返回true;如果出现错误,则返回false。
     * 我们使用给定的文件名创建一个QFile对象,并且调用open()打开这个用于输出的文件。
     * 我们也会创建一个QDataStream对象,由它操作这个QFile对象并且使用该对象输出数据。
     * 在输出数据之前,我们把这个应用程序的光标修改为标准的等待光标(通常是一个沙漏),并且一旦所有的数据输出完毕,就需要把这个应用程序的光标重新恢复为普通光标。
     * 在函数的最后,文件会由QFile对象的析构函数自动关闭。
     * QDataStream既可以支持C++基本类型,也可以支持多种Qt类型。该语法模仿了标准C++的<iostream>中的那些类的语法。
     * 例如:out << x << y << z;会把变量x、y和z输出到一个流中,
     * 而:in >> x >> y >> z;会从流中读出它们。
     * 因为C++的基本类型在不同平台上可能会有不同的大小,所以把这些变量强制转换成qint8、quint8、qint16、quint16、qint32、quint32、qint64以及quint64中的一个是最安全的做法,这样做可以确保它们能够获得应有的大小(按位计算)。
     */
    QFile file(fileName);
    if (!file.open(QIODevice::WriteOnly)) {
        QMessageBox::warning(this, tr("Spreadsheet"),
                             tr("Cannot write file %1:\n%2.")
                             .arg(file.fileName())
                             .arg(file.errorString()));
        return false;
    }

    QDataStream out(&file);
    out.setVersion(QDataStream::Qt_4_3);//Qt_4_3 = 9

    out << quint32(MagicNumber);

    QApplication::setOverrideCursor(Qt::WaitCursor);
    for (int row = 0; row < RowCount; ++row) {
        for (int column = 0; column < ColumnCount; ++column) {
            QString str = formula(row, column);
            if (!str.isEmpty())
                out << quint16(row) << quint16(column) << str;
        }
    }
    QApplication::restoreOverrideCursor();
    return true;
}

void Spreadsheet::sort(const SpreadsheetCompare &compare)
{
    /* 排序操作会对当前的选择进行,并且会根据存储在compare对象中的排序键和排序顺序重新排列这些行。
     * 我们使用一个QStringList来重新表示每一行数据,并且把该选择存储在一个行列表中。
     * 我们使用Qt的qStableSort()算法,并且根据公式而不是根据值来进行简单排序。
     * 这一过程如下图所示。第11章中会讲述Qt的标准算法和数据结构。
     * qStableSort()函数可以接受一个开始迭代器、一个终止迭代器和一个比较函数。
     * 这个比较函数是一个带两个参数(两个QStringList)的函数,并且如果第一个参数“小于”第二个参数,它就返回true,否则返回falsle。
     * 传递的作为比较函数的这个compare对象并不是一个真正的函数,但是它可以用作一个函数,将会很快看到这一点。
     * 在执行完qStableSort()之后,我们把数据移回到这个表中,接着清空这一选择,并且调用somethingChanged()函数。
     */
    QList<QStringList> rows;
    QTableWidgetSelectionRange range = selectedRange();
    int i;

    for (i = 0; i < range.rowCount(); ++i) {
        QStringList row;
        for (int j = 0; j < range.columnCount(); ++j)
            row.append(formula(range.topRow() + i,
                               range.leftColumn() + j));
        rows.append(row);
    }

    qStableSort(rows.begin(), rows.end(), compare);

    for (i = 0; i < range.rowCount(); ++i) {
        for (int j = 0; j < range.columnCount(); ++j)
            setFormula(range.topRow() + i, range.leftColumn() + j,
                       rows[i][j]);
    }

    clearSelection();
    somethingChanged();
}

void Spreadsheet::cut()
{
    //cut()槽可以对Edit->Cut菜单做出响应。由于Cut 的执行效果与Copy之后再加上一个Delete的执行效果相同,所以其实现代码很简单。
    copy();
    del();
}

void Spreadsheet::copy()
{
    /* copy()槽能够对Edit->Copy做出响应。
     * 它会遍历当前选择(如果没有明确的选择,那么就认为选择的只是当前单元格)。
     * 每一个选中单元格的公式都会被添加到一个QString中,行与行之间利用换行符“\n”分隔,列与列之间则以制表符“\t’来分隔。
     * 下面给出了这一实现方法的示意图。
     * 在Qt中,通过调用QApplication::clipboard()静态函数可以使用系统的剪贴板。
     * 通过调用QClipboard::setText(),就既可以在本应用程序中又可以在其他应用程序中使用放在剪贴板上的这些文本。
     * 这种使用制表符“\t”和换行符“\n"作为文件分隔符的形式可以被包括微软Excel在内的许多应用程序所支持。
     * 函数QTableWidget::selectedRanges()返回一个选择范围列表。我们知道由于在构造函数中已经将选择模式设置为QAbstractemView::ContiguousSelection,所以选择范围不可能再超过1。
     * 为方便起见,我们定义了一个selectedRange( )函数来返回这个选择范围。
     */
    QTableWidgetSelectionRange range = selectedRange();
    QString str;

    for (int i = 0; i < range.rowCount(); ++i) {
        if (i > 0)
            str += "\n";
        for (int j = 0; j < range.columnCount(); ++j) {
            if (j > 0)
                str += "\t";
            str += formula(range.topRow() + i, range.leftColumn() + j);
        }
    }
    QApplication::clipboard()->setText(str);
}

void Spreadsheet::paste()
{
    /* paste()槽对Edit->Paste菜单选项做出响应。
     * 我们从剪贴板中取回文本,并且调用静态函数QString::split()把这串字符变成一个QStringListo。
     * 每行都会变成这个列表中的一个字符串。
     * 接下来,需要求出复制区域的维数。
     * 行数就是QStringList 中字符串的个数;列数就是第一行中制表符“\t”字符的个数再加上1。
     * 如果只选中了一个单元格,就把这个单元格作为粘贴区域放在左上角;否则,就把当前选择作为要粘贴的区域。
     * 为了执行粘贴操作,我们遍历所有行并且再次使用QString::split()把它们分隔到每一个单元格中,但是这一次要把制表符“\t”当作分隔符。
     * 图4.6给出了这一过程中所使用的步骤。
     */
    QTableWidgetSelectionRange range = selectedRange();
    QString str = QApplication::clipboard()->text();
    QStringList rows = str.split('\n');
    int numRows = rows.count();
    int numColumns = rows.first().count('\t') + 1;

    if (range.rowCount() * range.columnCount() != 1
            && (range.rowCount() != numRows
                || range.columnCount() != numColumns)) {
        QMessageBox::information(this, tr("Spreadsheet"),
                tr("The information cannot be pasted because the copy "
                   "and paste areas aren't the same size."));
        return;
    }

    for (int i = 0; i < numRows; ++i) {
        QStringList columns = rows[i].split('\t');
        for (int j = 0; j < numColumns; ++j) {
            int row = range.topRow() + i;
            int column = range.leftColumn() + j;
            if (row < RowCount && column < ColumnCount)
                setFormula(row, column, columns[j]);
        }
    }
    somethingChanged();
}

void Spreadsheet::del()
{
    /* del()槽对Edit->Delete菜单选项做出响应。
     * 如果有选中的项,那么该函数就会删除它们并且调用somethingChanged()函数。
     * 对选择中的每一个Cell对象使用delete足以清空所有这些单元格。
     * 当删除QTFableWidget的QTableWidgetTtem的时候,QTableWidget就会注意到这一情况的发生,而如果这些项中有可见的任意项,QTableWidget将会自动对自己进行重绘。
     * 如果在一个已经删除过的单元格位置上又调用了cell(),那么该函数将会返回一个空指针。
     */
    QList<QTableWidgetItem *> items = selectedItems();
    if (!items.isEmpty()) {
        foreach (QTableWidgetItem *item, items)
            delete item;
        somethingChanged();
    }
}

void Spreadsheet::selectCurrentRow()
{
    selectRow(currentRow());
}

void Spreadsheet::selectCurrentColumn()
{
    selectColumn(currentColumn());
}
/* selectCurentRow()和selectCurrentColumn()对Edit->Select->Row和Edit->Select->Column菜单选项做出响应。
 * 这些实现分别依赖于QTableWidget的selectRow()和slectClum()函数。
 * 我们不必再去实现Edit->Select->All菜单选项的功能,因为该功能可以由QTableWidget从QAstactemView::selectAll()的函数中继承过来。
 */
void Spreadsheet::recalculate()
{
    /* recalculate()槽能够对Tools->Recalculate菜单选项做出响应。
     * 当必要时,它也会被Spreadsheet自动调用。
     * 我们遍历每一个单元格,并且对每一个单元格调用setDirty()把它们标记为需要重新计算。
     * 为了在电子制表软件中显示一个Cell对象的值,QTableWidget会再次对该对象调用text()以获得其值,从而使该值重新计算一次。
     * 然后,对这个视口调用update()来重新绘制整个电子制表软件。
     * QTableWidget 中的重绘代码就又会对每一个可见单元格调用text()来获得它们中要显示的值。
     * 因为在每一个单元格上都调用了setDirty() ,所以这些对text()的调用将会使用重新计算过的值。
     * 该计算可能需要重新计算那些不可见的单元格,这就会造成一个级联计算,直到每一个需要被重新计算的单元格能够在刚才刷新过的视口中重新得到计算,从而使它们也能够显示正确的文本。
     * 这一计算是由Cell类执行的。
     */
    for (int row = 0; row < RowCount; ++row) {
        for (int column = 0; column < ColumnCount; ++column) {
            if (cell(row, column))
                cell(row, column)->setDirty();
        }
    }
    viewport()->update();
}

void Spreadsheet::setAutoRecalculate(bool recalc)
{
    /* setAutoRecalculate()槽对Options->Auto-Recalculate菜单选项做出响应。
     * 如果启用了这个特性,则会立即重新计算整个电子制表软件以确保它是最新的,
     * 然后,recalculate()会自动在somethingChanged()中得到调用。
     * 因为QTableWidget已经提供了一个从QTableView 中继承而来的setShowGrid()槽,所以不需要再对Options->Show Grid菜单选项编写任何代码。
     * 所有要保留的东西就是Spreadsheet:sort(),它会以在MainWindow::sort()中得到调用:
     */
    autoRecalc = recalc;
    if (autoRecalc)
        recalculate();
}

void Spreadsheet::findNext(const QString &str, Qt::CaseSensitivity cs)
{
    /* findNext()槽会遍历单元格一遍 ,它从当前光标右侧的单元格开始遍历到这一行的最后一列,然后再从下一行的第一个单元格开始继续遍历,如此反复,直到找到所要查找的文本,或者是直到最后一个单元格为止。
     * 例如,如果当前的单元格是C24,那么就会搜索D24、E24、... 、Z24,然后再去搜索A25、B25、C25、... 、Z25;等等,一直遍历到Z999为止。
     * 如果找到了一个匹配项,那么就清空当前选择,把单元格光标移动到那个匹配的单元格上,并且让包含Spreasheet的窗口变成激活状态。
     * 如果没能找到匹配的单元格,那么就让应用程序发出 "哔"(beep)的一声来表明搜索已经结束,匹配没有成功。
     */
    int row = currentRow();
    int column = currentColumn() + 1;

    while (row < RowCount) {
        while (column < ColumnCount) {
            if (text(row, column).contains(str, cs)) {
                clearSelection();
                setCurrentCell(row, column);
                activateWindow();
                return;
            }
            ++column;
        }
        column = 0;
        ++row;
    }
    QApplication::beep();
}

void Spreadsheet::findPrevious(const QString &str,
                               Qt::CaseSensitivity cs)
{
    //fndPrevious()槽与findNext()槽相似,区别之处是它会向相反的方向遍历并且会在单元格A1处停下来。
    int row = currentRow();
    int column = currentColumn() - 1;

    while (row >= 0) {
        while (column >= 0) {
            if (text(row, column).contains(str, cs)) {
                clearSelection();
                setCurrentCell(row, column);
                activateWindow();
                return;
            }
            --column;
        }
        column = ColumnCount - 1;
        --row;
    }
    QApplication::beep();
}

void Spreadsheet::somethingChanged()
{
    //如果启用了"auto-recalculate"(自动重新计算) ,那么somethingChanged()私有槽就会重新计算整个电子制表软件。
    //它也会发射modified()信号。
    if (autoRecalc)
        recalculate();
    emit modified();
}

Cell *Spreadsheet::cell(int row, int column) const
{
    //cell()私有函数可以根据给定的行和列返回一个Cell对象。
    //它几乎和QTableWidget::item()函数的作用一样,只不过它返回的是一个Cell指针,而不是一个QTableWidgetItem指针。

    return static_cast<Cell *>(item(row, column));
}

void Spreadsheet::setFormula(int row, int column,
                             const QString &formula)
{
    /* setFormula()私有函数可以设置用于给定单元格的公式。
     * 如果该单元格已经有一个Cell对象,那么我们就重新使用它。
     * 否则,可以创建一个新的Cell对象并且调用QTableWidget::setItem()把它插入到表中。
     * 最后,调用该单元格自己的setFormula()函数,但如果这个单元格已经显示在屏幕上,那么就重新绘制它。
     * 我们不需要担心随后对这个Cell对象的删除操作,因为QTableWidget会得到这个单元格的所有权,并且会在正确的时候自动将其删除。
     */
    Cell *c = cell(row, column);
    if (!c) {
        c = new Cell;
        setItem(row, column, c);
    }
    c->setFormula(formula);
}

QString Spreadsheet::formula(int row, int column) const
{
    /* formula()函数返回给定单元格中的公式。在很多情况下,公式和文本是相同的。
     * 例如,公式“Hello"等价于字符串“Hello",所以如果用户在单元格中输入“Hello"并且按下回车键;
     * 那么该单元格就会显示文本“Hello"。但是还有一些例外的情况:
     * ● 如果公式是一个数字那么它就会被认为是一个数字。
     * 例如,公式“1.50”等价于双精度实数(double)的1.5,它在电子制表软件中会被显示为右对齐的“1.5”。
     * ● 如果公式以单引号开始,那么公式的剩余部分将会被认为是文本。
     * 例如,公式“12345"等价于字符串“12345”。
     * ● 如果公式以等号开始,那么公式将会被认为是一个算术公式。
     * 例如, 如果单元格A1包含“12”并且单元格A2包含“6”,那么公式“=A1+A2”就会等于18。
     * 把公式转换成值的任务是由Cell类完成的。
     * 这时,要记住的事情是显示在单元格内的文本是公式的结果,而不是公式本身。
     */
    Cell *c = cell(row, column);
    if (c) {
        return c->formula();
    } else {
        return "";
    }
}

QString Spreadsheet::text(int row, int column) const
{
    //tex()私有函数可以返回给定单元格中的文本。如果cell( )返回的是一个空指针,则表示该单元格是空的,因而返回一个空字符串。
    Cell *c = cell(row, column);
    if (c) {
        return c->text();
    } else {
        return "";
    }
}

bool SpreadsheetCompare::operator()(const QStringList &row1,
                                    const QStringList &row2) const
{
    /* 如果第1行小于第2行,该仿函数就返回true; 否则,就返回false。
     * qStableSort()函数会使用这个函数的结果来执行排序操作。
     * SpreadsheetCompare对象的key与ascending数组和MainWindow::sort()函数(已经在第2章中给出过)一起配合使用。
     * 每个键都保存一个列索引,或者- 1(为“None”时)。
     * 我们按键顺序比较两行中相应的单元格条目。一旦发现有不同之处,就返回一个适当的true或者false值。
     * 如果所有的比较关系都证明两者是相等的,就返回false。
     * qStableSort()函数会使用这里给出的顺序来解决这种平局情形。如
     * 果一开始的时候row1在row2之前,并且它们都不“小于”对方,那么,在结果中rowl还在row2前面。
     * 这就是qStableSort()与它很相似的非稳定版本的qSort()函数之间的区别。
     */
    for (int i = 0; i < KeyCount; ++i) {
        int column = keys[i];
        if (column != -1) {
            if (row1[column] != row2[column]) {
                if (ascending[i]) {
                    return row1[column] < row2[column];
                } else {
                    return row1[column] > row2[column];
                }
            }
        }
    }
    return false;
}

3.4 Cell类

//cell.h
#ifndef CELL_H
#define CELL_H

#include <QTableWidgetItem>

class Cell : public QTableWidgetItem
{
public:
    Cell();

    QTableWidgetItem *clone() const;
    void setData(int role, const QVariant &value);
    QVariant data(int role) const;
    void setFormula(const QString &formula);
    QString formula() const;
    void setDirty();

private:
    QVariant value() const;
    QVariant evalExpression(const QString &str, int &pos) const;
    QVariant evalTerm(const QString &str, int &pos) const;
    QVariant evalFactor(const QString &str, int &pos) const;

    mutable QVariant cachedValue;
    mutable bool cacheIsDirty;
    /* 通过增加两个私有变量,Cell类对QTableWidgetItem进行了扩展:
     * ● cachedValue把单元格的值缓存为QVariant。
     * ● 如果缓存的值不是最新的,那么就把cacheIsDirty设置为true。
     * 之所以使用QVariant,是因为有些单元格是double型值,另外一些单元格则是QString型值。
     * 在声明cachedValue和cacheIsDirty变量时使用了C++的mutable关键字,这样就可以在const函数中修改这些变量。
     * 或者,在每次调用text()时,本应当重新计算这个值,但是这样做是不必要的,因为它的效率非常低下。
     * 我们注意到,在该类的定义中并没有使用Q_ OBJECT宏。这是因为,Cell是一个普通的C++类,它没有使用任何信号或者槽。
     * 实际上,因为QTableWidgetItem不是从Q0bject派生而来的,所以就不能让Cell拥有信号和槽。
     * 为了使Qt的项(item)类的开销降到最低,它们就不是从QObject 派生的。
     * 如果需要信号和槽,可以在包含项的窗口部件中实现它们,或者在特殊情况下,可以通过对Q0bject进行多重继承的方式来实现它们。
     *
     */
};

#endif
//cell.cpp
#include <QtWidgets>

#include "cell.h"

Cell::Cell()
{
    /* 在构造函数中,只需要将缓存设置为dirty。
     * 没有必要传递父对象,当用setItem()把单元格插入到一个QTableWidget中的时候,QTableWidget将会自动对其拥有所有权。
     * 每个QTableWidgelItem都可以保存一些数据,最多可以为每个数据“角色"分配一个QVariant变量。
     * 最常用的角色是Qt::EditRole和Qt::DisplayRole。
     * 编辑角色用在那些需要编辑的数据上,而显示角色用在那些需要显示的数据上。
     * 通常情况下,用于两者的数据是一样的,但在Cell类中,编辑角色对应于单元格的公式,而显示角色对应于单元格的值(对公式求值后的结果)。
     */
    setDirty();
}

QTableWidgetItem *Cell::clone() const
{
    /* 当QTableWidget需要创建一个新的单元格时,例如,当用户在一个以前没有使用过的空白单元格中开始输人数据时,它就会调用clone()函数。
     * 传递给QTableWidget::settemPrototype()中的实例就是需要克隆的项。
     * 由于对于Cell 来讲,成员级的复制已经足以满足需要,所以在clone()函数中,只需依靠由C++自动创建的默认复制构造函数就可以创建新的Cell 实例了。
     */
    return new Cell(*this);
}

void Cell::setData(int role, const QVariant &value)
{
    /* 如果有一个新的公式,就可以把cacheIsDitrty设置为true,以确保在下一次调用text()的时候可以重新计算该单元格。
     * 尽管对Cell实例中的Spreadsheet::text()调用了text() ,但在Cell中没有定义text()函数。
     * 这个text()函数是一个由QTableWidgetItem 提供的简便函数。这相当于调用data(Qt::DisplayRole).toString()。
     */
    QTableWidgetItem::setData(role, value);
    if (role == Qt::EditRole)
        setDirty();
}

QVariant Cell::data(int role) const
{
    /* data()函数是从QTableWidgetItemn中重新实现的。
     * 如果使用Qt::DisplayRole调用这个函数,那么它返回在电子制表软件中应该显示的文本;
     * 如果使用Qt: EditRole调用这个函数,那么它返回该单元格中的公式;
     * 如果使用Qt::TextAlignmentRole调用这个函数,那么它返回一个合适的对齐方式。
     * 在使用DisplayRale的情况下,它依靠value()来计算单元格的值。如果该值是无效的(由于这个公式是错误的),则返回“####”。
     * 在data()中使用的这个Cell::value()函数可以返回一个QVariant值。QVariant可以存储不同类型的值,比如double和QString,并且提供了把变量转换为其他类型变量的一些函数。
     * 例如,对一,个保存了double值的变量调用toString(),可以产生一个表示这个double值的字符串。
     * 使用默认构造函数构造的QVariant是一个“无效”变量。
     */
    if (role == Qt::DisplayRole) {
        if (value().isValid()) {
            return value().toString();
        } else {
            return "####";
        }
    } else if (role == Qt::TextAlignmentRole) {
        if (value().type() == QVariant::String) {
            return int(Qt::AlignLeft | Qt::AlignVCenter);
        } else {
            return int(Qt::AlignRight | Qt::AlignVCenter);
        }
    } else {
        return QTableWidgetItem::data(role);
    }
}

void Cell::setFormula(const QString &formula)
{
    //setFormula()函数用来设置单元格中的公式。它只是一个对编辑角色调用setData()的简便函数。
    //也可以从Spreadsheet: :setFormula()中调用它。
    setData(Qt::EditRole, formula);
}

QString Cell::formula() const
{
    //formula()函数会从Spreadsheet::formula()中得到调用。
    //就像setFormula()一样,它也是一个简便函数,这次是重新获得该项的EditRole数据。
    return data(Qt::EditRole).toString();
}

void Cell::setDirty()
{
    //调用setDirty()函数可以用来对该单元格的值强制进行重新计算。
    //它只是简单地把cacheIsDirty设置为true, 也就意味着cachedValue不再是最新值了。
    //除非有必要,否则不会执行这个重新计算操作。
    cacheIsDirty = true;
}

const QVariant Invalid;

QVariant Cell::value() const
{
    /* value()私有函数返回这个单元格的值。如果cacheIsDirty是true,就需要重新计算这个值。
     * value()函数声明为const函数。我们不得不把cachedValue和cacheIsValid声明为mutable变量,以便编译器可以让我们在const函数中修政它们。
     * 当然,如果能够把value()声明为一个非const函数并且移除mutable关键字可能会更吸引人些,
     * 但是这将会导致无法编译,因为是从一个const函数的data()函数中调用value()的。
     */
    if (cacheIsDirty) {
        cacheIsDirty = false;

        QString formulaStr = formula();
        //如果公式是由单引号开始的(例如,“'12345"), 那么这个单引号就会占用位置0,而值就是从位置1直到最后位置的一个字符串。
        if (formulaStr.startsWith('\'')) {
            cachedValue = formulaStr.mid(1);
        } else if (formulaStr.startsWith('=')) {
            /* 如果公式是由等号开始的,那么会使用从位置1开始的字符串,并且将它可能包含的任意空格全部移除。
             * 然后,调用evalExpression()来计算这个表达式的值。
             * 这里的参数pos是通过引用(reference)方式传递的,由它来说明需要从哪里开始解析字符的位置。
             * 在调用evalExpression()之后,如果表达式解析成功,那么在位置pos处的字符应当是我们添加上的QChar::Null字符。
             * 如果在表达式结束之前解析失败了,那么可以把cachedValue设置为Invalid。
             */
            cachedValue = Invalid;
            QString expr = formulaStr.mid(1);
            expr.replace(" ", "");
            expr.append(QChar::Null);

            int pos = 0;
            cachedValue = evalExpression(expr, pos);
            if (expr[pos] != QChar::Null)
                cachedValue = Invalid;
        } else {
            /* 如果公式不是由单引号或者等号开始的,那么可以使用toDouble()试着把它转换为浮点数。
             * 如果转换正常,就把cachedValue设置为结果数字;否则,把cachedValue设置为字符串公式。
             * 例如,公式“1.50”会导致toDouble()把ok设置为true并且返回1.5,
             * 而公式“World Population"则会导致toDouble()把ok设置为false并且返回0.0。
             * 通过给toDouble()一个bool指针,可以区分字符串转换中表示的是数字0.0还是表示的是转换错误(此时,仍旧会返回一个0.0,但是同时会把这个bool设置为false)。
             * 有时候,对于转换失败所返回的0值可能正是我们所需要的。
             * 在这种情况下,就没有必要再麻烦地传递一个bool指针了。
             * 考虑到程序的性能和移植性因素,Qt从来不使用C++异常(exception)机制来报告错误。
             * 但是,如果你的编译器支持C++异常,那么这也不会妨碍你在自己的Qt程序中使用它们。
             */
            bool ok;
            double d = formulaStr.toDouble(&ok);
            if (ok) {
                cachedValue = d;
            } else {
                cachedValue = formulaStr;
            }
        }
    }
    return cachedValue;
}

QVariant Cell::evalExpression(const QString &str, int &pos) const
{
    /* evalExpression()函数返回一个电子制表软件表达式的值。
     * 表达式可以定义为:一个或者多个通过许多“+”或者“-”操作符分隔而成的项。
     * 这些项自身可以定义为:由“*”或者“/”操作符分隔而成的一个或者多个因子(factor)。
     * 通过把表达式分解成项,再把项分解成因子,就可以确保以正确的顺序来使用这些操作符了。
     * 例如,“2*C5+D6”就是一个表达式,它由作为第一项的“2*C5"和作为第二项的“D6"构成。
     * 项“2*C5”是由作为第一个因子的“2”和作为第二个因子的“C5”组成的,而项“D6"则由一个单一的因子“D6”组成。
     * 一个因子可以是一个数(“2")、一个单元格位置(“C5"),或者是一个在圆括号内的表达式,在它们的前面可以有负号。
     *
     * 首先,调用evalTerm()得到第一项的值。
     * 如果它后面紧跟的字符是“.+"或者“”,那么就继续第二次调用evalTerm();
     * 否则,表达式就只包-一个单一项,并且把它的值作为整个表达式的值而返回。
     * 在得到前两项的值之后,根据操作符计算出这操作的结果。
     * 如果两项都求出一个double值,就把计算出的结果当作一个doubl 值;否则,把结果设置为Invalid。
     * 像前面那样继续操作,直到再没有更多的项为止。
     * 这样做可以正确地进行,因为加法和减法都是左相关(left-associative)的;
     * 也就是说,“1-2-3”的意思是“(1-2)-3”,而不是“1-(2-3)”。
     */
    QVariant result = evalTerm(str, pos);
    while (str[pos] != QChar::Null) {
        QChar op = str[pos];
        if (op != '+' && op != '-')
            return result;
        ++pos;

        QVariant term = evalTerm(str, pos);
        if (result.type() == QVariant::Double
                && term.type() == QVariant::Double) {
            if (op == '+') {
                result = result.toDouble() + term.toDouble();
            } else {
                result = result.toDouble() - term.toDouble();
            }
        } else {
            result = Invalid;
        }
    }
    return result;
}

QVariant Cell::evalTerm(const QString &str, int &pos) const
{
    /* 除了evalTemm()函数是处理乘法和除法这一点不同之外,它和evalExpression()都很相似。
     * 在evalTerm()中唯一的不同就是必须要避免除零,因为在一些处理器中这将是一个错误。
     * 尽管测试浮点数值是否相等通常并不明智,因为其中存在取舍问题,但是在这个防止除零的问题上,这样做相等性测试已经足够了。
     */
    QVariant result = evalFactor(str, pos);
    while (str[pos] != QChar::Null) {
        QChar op = str[pos];
        if (op != '*' && op != '/')
            return result;
        ++pos;

        QVariant factor = evalFactor(str, pos);
        if (result.type() == QVariant::Double
                && factor.type() == QVariant::Double) {
            if (op == '*') {
                result = result.toDouble() * factor.toDouble();
            } else {
                if (factor.toDouble() == 0.0) {
                    result = Invalid;
                } else {
                    result = result.toDouble() / factor.toDouble();
                }
            }
        } else {
            result = Invalid;
        }
    }
    return result;
}

QVariant Cell::evalFactor(const QString &str, int &pos) const
{
    /* evalFactor()函数比evalExpression()和evalTerm()函数都要复杂一些。
     * 它先从计算因子是否为负开始。然后,判断它是否是从左圆括号开始的。
     * 如果是,就先把圆括号内的内容作为表达式并通过调用evalExpression()来处理它。
     * 当解析到带圆括号的表达式时, evalExpression()调用evalTerm(), evalTerm()调用evalFactor(),evalFactor()则会再次调用evalExpression()。
     * 这就是在解析器中出现递归调用的地方。
     *
     * 如果该因子不是一个嵌套表达式,就提取下一个记号,它应当是一个单元格的位置,或者也可能是一个数字。
     * 如果这个记号匹配QRegExp,就把它认为是一个单元格引用并且对给定位置处的单元格调用value()。
     * 该单元格可能在电子制表软件中的任何一个地方,并且它可能会依赖于其他的单元格。
     * 这种依赖不是什么问题,它们只会简单地触发更多的value()调用和(对于那些“dirty"单元格)更多的解析处理,
     * 直到所有相关的单元格的值都得到计算为止。如果记号不是一个单元格的位置,那么就把它看作是一个数字。
     *
     * 如果单元格A1包含公式“=A1”时会发生什么呢?或者如果单元格A1包含公式“=A2”并且单元格A2包含公式“=A1"时又会发生什么呢?
     * 尽管还没有编写任何特定代码来检测这种循环依赖关系,但解析器可以通过返回一个无效的QVariant来完美地处理这一情况。
     * 之所以可以正常工作,是因为在调用evalExpression()之前,我们会在value()中把cacheIsDirty设置为false, 把cachedValue设置为Invalid。
     * 如果evalExpression()对同一个单元格循环调用value(),它就会立即返回Invalid, 并且这样就会使整个表达式等于Invalid。
     *
     */
    QVariant result;
    bool negative = false;

    if (str[pos] == '-') {
        negative = true;
        ++pos;
    }

    if (str[pos] == '(') {
        ++pos;
        result = evalExpression(str, pos);
        if (str[pos] != ')')
            result = Invalid;
        ++pos;
    } else {
        QRegExp regExp("[A-Za-z][1-9][0-9]{0,2}");
        QString token;

        while (str[pos].isLetterOrNumber() || str[pos] == '.') {
            token += str[pos];
            ++pos;
        }

        if (regExp.exactMatch(token)) {
            int column = token[0].toUpper().unicode() - 'A';
            int row = token.mid(1).toInt() - 1;

            Cell *c = static_cast<Cell *>(
                              tableWidget()->item(row, column));
            if (c) {
                result = c->value();
            } else {
                result = 0.0;
            }
        } else {
            bool ok;
            result = token.toDouble(&ok);
            if (!ok)
                result = Invalid;
        }
    }

    if (negative) {
        if (result.type() == QVariant::Double) {
            result = -result.toDouble();
        } else {
            result = Invalid;
        }
    }
    return result;
}

3.5 FindDialog类

//finddialog.h
#ifndef FINDDIALOG_H//防止对头文件的多重包含
#define FINDDIALOG_H

#include <QDialog>

//前置声明,编译过程更快
class QCheckBox;
class QLabel;
class QLineEdit;
class QPushButton;

class FindDialog : public QDialog
{
    Q_OBJECT//对于使用了信号与槽的类,这个宏是必需的

public:
    FindDialog(QWidget *parent = 0);//默认值为空指针,说明对话框没有父对象

signals:
    //单击Find按钮时对话框发射的两个信号,Qt::CaseSensitivity cs则是指输入的大小写要敏感
    void findNext(const QString &str, Qt::CaseSensitivity cs);
    void findPrevious(const QString &str, Qt::CaseSensitivity cs);

private slots:
    void findClicked();
    void enableFindButton(const QString &text);

private:
    QLabel *label;
    QLineEdit *lineEdit;
    QCheckBox *caseCheckBox;
    QCheckBox *backwardCheckBox;
    QPushButton *findButton;
    QPushButton *closeButton;
};

#endif
//finddialog.cpp
#include <QtWidgets>
/*Qt5中需要将#include <QtGui>换成#include<QtWidgets>,涉及到版本升级中模块变化,具体见帮助文档
 *这里的<QtWidgets>包含了我们需要用到的所有类,通过帮助文档搜索QtWidgets即可查看包含的类
 *在头文件中本可以仅加入这个<QtWidgets>包含,而不需要其他<QDialog>引用和前置声明,
 *然而在一个头文件中再包含一个这么大的头文件着实不是一种好的编程风格,尤其是对于比较大的工程项目更是如此。
 */

#include "finddialog.h"

FindDialog::FindDialog(QWidget *parent)
    : QDialog(parent)
{
    label = new QLabel(tr("Find &what:"));
    //tr函数调用是把它们翻译成其他语言的标记,在每一个用户可见的字符串使用tr()函数是个很好的习惯。
    //&符号用于表示快捷键,可以用来控制焦点,Alt+W选中
    lineEdit = new QLineEdit;
    label->setBuddy(lineEdit);
    //设置行编辑器为标签的伙伴,在按下标签的快捷键时焦点转移至伙伴部件即行编辑器中,仅适用于QLabel

    caseCheckBox = new QCheckBox(tr("Match &case"));
    backwardCheckBox = new QCheckBox(tr("Search &backward"));

    findButton = new QPushButton(tr("&Find"));
    //Alt+F激活按钮
    findButton->setDefault(true);
    //设置Find按钮成为对话框的默认按钮(按Enter键使用按钮)
    findButton->setEnabled(false);
    //禁用窗口部件,显示为灰色且不能进行交互操作

    closeButton = new QPushButton(tr("Close"));

    connect(lineEdit, SIGNAL(textChanged(const QString &)),
            this, SLOT(enableFindButton(const QString &)));
    //行编辑框中文本发生变化时,调用enableFindButton函数,使Find按钮可以使用
    connect(findButton, SIGNAL(clicked()),
            this, SLOT(findClicked()));
    //Find按钮调用findClicked函数
    connect(closeButton, SIGNAL(clicked()),
            this, SLOT(close()));
    //close按钮关闭对话框(发送QCloseEvent事件,可用于检查保存)
    //这里QObject是FindDialog的父对象之一,所以connect函数可以不用加QObject::前缀

    QHBoxLayout *topLeftLayout = new QHBoxLayout;//水平布局
    topLeftLayout->addWidget(label);
    topLeftLayout->addWidget(lineEdit);

    QVBoxLayout *leftLayout = new QVBoxLayout;//竖直布局
    leftLayout->addLayout(topLeftLayout);
    leftLayout->addWidget(caseCheckBox);
    leftLayout->addWidget(backwardCheckBox);

    QVBoxLayout *rightLayout = new QVBoxLayout;
    rightLayout->addWidget(findButton);
    rightLayout->addWidget(closeButton);
    rightLayout->addStretch();//分隔符(弹簧)

    QHBoxLayout *mainLayout = new QHBoxLayout;
    mainLayout->addLayout(leftLayout);
    mainLayout->addLayout(rightLayout);
    setLayout(mainLayout);
    //布局管理器摆放子窗口部件,布局不可见,具体布局见布局图。
    //布局派生于QLayout,因而派生自QObject。
    //将子布局对象添加到父布局对象时,子布局对象会自动重定义自己的父对象。父子层次关系见图

    setWindowTitle(tr("Find"));//设置窗口标题
    setFixedHeight(sizeHint().height());
    //设置窗口固定高度,sizeHint()会返回一个窗口部件所"理想"的尺寸大小

    //由于创建这个对话框中的窗口部件和布局时使用的时new,所以需要写一个能够调用delete的析构函数,以便可以删除所创建的每一个窗口部件和布局。
    //但是这样做并不是必需的,因为Qt在删除父对象时会自动删除其所属的所有子对象,也就是其中的所有窗口部件和子布局。
}

void FindDialog::findClicked()
{
    //获取行编辑器中的文本内容
    QString text = lineEdit->text();
    //检查是否需要判断大小写,Qt::CaseSensitive为需要,Qt::CaseInsensitive为不需要
    Qt::CaseSensitivity cs =
            caseCheckBox->isChecked() ? Qt::CaseSensitive
                                      : Qt::CaseInsensitive;
    //判断Search backward选项的取值,决定发射哪个信号
    if (backwardCheckBox->isChecked()) {
        emit findPrevious(text, cs);
    } else {
        emit findNext(text, cs);
    }
}

void FindDialog::enableFindButton(const QString &text)
{//用户改变行编辑器文本时会调用这个函数,若有文本则启用Find按钮。
    findButton->setEnabled(!text.isEmpty());
}

//Tab键顺序可以遍历对话框中的窗口部件,可以由QWidget::setTabOrder()函数改变顺序

3.6 GoToCellDialog类

//gotocelldialog.h
#ifndef GOTOCELLDIALOG_H
#define GOTOCELLDIALOG_H

#include <QDialog>

namespace Ui {
class GoToCellDialog;
}

class GoToCellDialog : public QDialog
{
    Q_OBJECT

public:
    explicit GoToCellDialog(QWidget *parent = 0);
    ~GoToCellDialog();

    QString getLineEdit();
private slots:
    void on_lineEdit_textChanged();

private:
    Ui::GoToCellDialog *ui;
};

#endif // GOTOCELLDIALOG_H

//gotocelldialog.cpp
#include "gotocelldialog.h"
#include "ui_gotocelldialog.h"

GoToCellDialog::GoToCellDialog(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::GoToCellDialog)
{
    ui->setupUi(this);//初始化窗体
    //可以直接访问Ui::MainWindow中的成员。
    //创建用户接口后,setupUi()函数还会自动将那些符合on_objectName_signalName()命名惯例的任意槽与相应的objectName的signalName()信号连接到一起。
    //connect(lineEdit,SIGNAL(textChaned(const QString &)),this,SLOT(on_lineEdit_textChanged()));

    QRegExp regExp("[A-Za-z][1-9][0-9]{0,2}");
    //设置一个检验器来限制输入的范围,Qt提供了三个内置检验器类:QIntValidator、QDoubleValidator和QRegExpValidator
    //让它带一个正则表达式,允许一个大写或者小写的字母,后面跟一个1~9范围的数字,后面再跟0个、1个或2个0~9的数字。(数字不能以0开头)
    ui->lineEdit->setValidator(new QRegExpValidator(regExp,this));
    //通过把this传递给QRegExpValidator的构造函数,使它成为MainWindow对象的一个子对象。这样以后就不用担心有关删除QRegExpValidator的事情了:当删除它的父对象时,它也会被自动删除
    //对象树机制具体见下文

    connect(ui->okButton,SIGNAL(clicked()),this,SLOT(accept()));
    connect(ui->cancelButton,SIGNAL(clicked()),this,SLOT(reject()));
    //OK按钮连接到QDialog的accept()槽,将Cancel按钮连接到reject()槽。
    //这两个槽都可以关闭对话框,但accept()槽可以将对话框返回的结果变量设置为QDialog::Accepted(其值为1),而reject()槽会把对话框的值设置为QDialog::Rejected(其值为0);
    //当使用这个对话框的时候,可以利用这个结果来判断用户是否单击了OK按钮,从而执行相应的动作。
}

void GoToCellDialog::on_lineEdit_textChanged()
{//使用检验器来判断行编辑器中内容的有效性,可以启用或禁用OK按钮
    ui->okButton->setEnabled(ui->lineEdit->hasAcceptableInput());
}

GoToCellDialog::~GoToCellDialog()
{
    delete ui;
}

QString GoToCellDialog::getLineEdit()
{
    return ui->lineEdit->text().toUpper();
}
//gotocelldialog.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>GoToCellDialog</class>
 <widget class="QDialog" name="GoToCellDialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>314</width>
    <height>147</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Go to Cell</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout_2">
   <item>
    <layout class="QVBoxLayout" name="verticalLayout">
     <item>
      <layout class="QHBoxLayout" name="horizontalLayout">
       <item>
        <widget class="QLabel" name="label">
         <property name="text">
          <string>&amp;Cell Location:</string>
         </property>
         <property name="buddy">
          <cstring>lineEdit</cstring>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QLineEdit" name="lineEdit"/>
       </item>
      </layout>
     </item>
     <item>
      <layout class="QHBoxLayout" name="horizontalLayout_2">
       <item>
        <spacer name="horizontalSpacer">
         <property name="orientation">
          <enum>Qt::Horizontal</enum>
         </property>
         <property name="sizeHint" stdset="0">
          <size>
           <width>40</width>
           <height>20</height>
          </size>
         </property>
        </spacer>
       </item>
       <item>
        <widget class="QPushButton" name="okButton">
         <property name="enabled">
          <bool>false</bool>
         </property>
         <property name="text">
          <string>OK</string>
         </property>
         <property name="default">
          <bool>true</bool>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QPushButton" name="cancelButton">
         <property name="text">
          <string>Cancel</string>
         </property>
        </widget>
       </item>
      </layout>
     </item>
    </layout>
   </item>
  </layout>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <tabstops>
  <tabstop>lineEdit</tabstop>
  <tabstop>okButton</tabstop>
  <tabstop>cancelButton</tabstop>
 </tabstops>
 <resources/>
 <connections/>
</ui>

3.7 SortDialog类

//sortdialog.h
#ifndef SORTDIALOG_H
#define SORTDIALOG_H

#include <QDialog>

namespace Ui {
class SortDialog;
}

class SortDialog : public QDialog
{
    Q_OBJECT

public:
    explicit SortDialog(QWidget *parent = 0);
    ~SortDialog();

    void setColumnRange(QChar first, QChar last);
    //根据电子制表软件中选择的列初始化这些组合框的内容,在可选的第二键和第三键的组合框选项中插入了“None”选项。

    int getPrimaryColumnComboIndex();
    int getSecondaryColumnComboIndex();
    int getTertiaryColumnComboIndex();
    int getPrimaryOrderComboIndex();
    int getSecondaryOrderComboIndex();
    int getTertiaryOrderComboIndex();


private:
    Ui::SortDialog *ui;
};

#endif // SORTDIALOG_H
//sortdialog.cpp
#include <QtWidgets>
#include "sortdialog.h"
#include "ui_sortdialog.h"
#include <QDebug>


SortDialog::SortDialog(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::SortDialog)
{
    ui->setupUi(this);
    //构造函数中隐藏了对话框中第二键和第三键这两个部分
    ui->secondaryGroupBox->hide();
    ui->tertiaryGroupBox->hide();
    //把窗体布局(布局名改为layout)的SizeConstraint设置为QLayout::SetFixedSize,这样使用户不能再重新修改这个对话框的窗体大小。
    //这里书上没有修改对话框栅格布局layoutName为layout,直接使用会报错,记得在ui中修改
    ui->layout->setSizeConstraint(QLayout::SetFixedSize);
    //这样一来,布局就会负责对话框重新定义大小的职责,并且也会在显示或者隐藏子窗口部件的时候自动重新定义这个对话框的大小,从而可以确保对话框总是能以最佳的尺寸显示出来。

    setColumnRange('A', 'Z');//组合框中内容设置为A到Z
}

void SortDialog::setColumnRange(QChar first, QChar last)
{
    ui->primaryColumnCombo->clear();
    ui->secondaryColumnCombo->clear();
    ui->tertiaryColumnCombo->clear();

    ui->secondaryColumnCombo->addItem(tr("None"));
    ui->tertiaryColumnCombo->addItem(tr("None"));

    //这里我的ui文件第一次出了问题,不知道错在哪里总是无法实现一致性效果,最后把样例的ui文件拿过来就没问题了。重新做了一遍可能是创建新的群组框时出现了错误
    ui->primaryColumnCombo->setMinimumSize(ui->secondaryColumnCombo->sizeHint());
    //给出了布局中的一个特殊习惯用语。QWidget::sizeHint()函数可以返回布局系统试图认同的"理想"大小。
    //这也解释了为什么不同的窗口部件或者具有不同内容的类似窗口部件通常会被布局系统分配给不同的尺寸大小。
    //对于这些组合框,这里指的是第二键组合框和第三键组合框,由于它们包含了一个”None”选项
    //所以它们要比只包含了一个单字符项目的主键组合框显得宽一些。
    //为了避免这种不一致性,需要把主键组合框的最小大小设置成第二键组合框的理想大小。

    QChar ch = first;
    while (ch <= last) {
        ui->primaryColumnCombo->addItem(QString(ch));
        ui->secondaryColumnCombo->addItem(QString(ch));
        ui->tertiaryColumnCombo->addItem(QString(ch));
        ch = ch.unicode() + 1;
    }
}

SortDialog::~SortDialog()
{
    delete ui;
}

int SortDialog::getPrimaryColumnComboIndex()
{
    return ui->primaryColumnCombo->currentIndex();
}

int SortDialog::getSecondaryColumnComboIndex()
{
    return ui->secondaryColumnCombo->currentIndex();
}

int SortDialog::getTertiaryColumnComboIndex()
{
    return ui->tertiaryColumnCombo->currentIndex();
}

int SortDialog::getPrimaryOrderComboIndex()
{
    return ui->primaryOrderCombo->currentIndex();
}

int SortDialog::getSecondaryOrderComboIndex()
{
    return ui->secondaryOrderCombo->currentIndex();
}

int SortDialog::getTertiaryOrderComboIndex()
{
    return ui->tertiaryOrderCombo->currentIndex();
}
//sortdialog.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>SortDialog</class>
 <widget class="QDialog" name="SortDialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>336</width>
    <height>340</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Sort</string>
  </property>
  <layout class="QGridLayout" name="layout">
   <item row="0" column="0">
    <widget class="QGroupBox" name="primaryGroupBox">
     <property name="title">
      <string>&amp;Primary Key</string>
     </property>
     <layout class="QGridLayout" name="gridLayout">
      <item row="0" column="0">
       <widget class="QLabel" name="label">
        <property name="text">
         <string>Column:</string>
        </property>
       </widget>
      </item>
      <item row="0" column="1">
       <widget class="QComboBox" name="primaryColumnCombo">
        <item>
         <property name="text">
          <string>None</string>
         </property>
        </item>
       </widget>
      </item>
      <item row="0" column="2">
       <spacer name="horizontalSpacer">
        <property name="orientation">
         <enum>Qt::Horizontal</enum>
        </property>
        <property name="sizeHint" stdset="0">
         <size>
          <width>28</width>
          <height>20</height>
         </size>
        </property>
       </spacer>
      </item>
      <item row="1" column="1" colspan="2">
       <widget class="QComboBox" name="primaryOrderCombo">
        <item>
         <property name="text">
          <string>Ascending</string>
         </property>
        </item>
        <item>
         <property name="text">
          <string>Descending</string>
         </property>
        </item>
       </widget>
      </item>
      <item row="1" column="0">
       <widget class="QLabel" name="label_2">
        <property name="text">
         <string>Order:</string>
        </property>
       </widget>
      </item>
     </layout>
    </widget>
   </item>
   <item row="0" column="1" rowspan="2">
    <layout class="QVBoxLayout" name="verticalLayout">
     <item>
      <widget class="QPushButton" name="okButton">
       <property name="text">
        <string>OK</string>
       </property>
       <property name="default">
        <bool>true</bool>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QPushButton" name="cancelButton">
       <property name="text">
        <string>Cancel</string>
       </property>
      </widget>
     </item>
     <item>
      <spacer name="verticalSpacer">
       <property name="orientation">
        <enum>Qt::Vertical</enum>
       </property>
       <property name="sizeHint" stdset="0">
        <size>
         <width>20</width>
         <height>13</height>
        </size>
       </property>
      </spacer>
     </item>
     <item>
      <widget class="QPushButton" name="moreButton">
       <property name="text">
        <string>More</string>
       </property>
       <property name="checkable">
        <bool>true</bool>
       </property>
      </widget>
     </item>
    </layout>
   </item>
   <item row="1" column="0">
    <spacer name="verticalSpacer_2">
     <property name="orientation">
      <enum>Qt::Vertical</enum>
     </property>
     <property name="sizeHint" stdset="0">
      <size>
       <width>20</width>
       <height>28</height>
      </size>
     </property>
    </spacer>
   </item>
   <item row="2" column="0">
    <widget class="QGroupBox" name="secondaryGroupBox">
     <property name="title">
      <string>&amp;Secondary Key</string>
     </property>
     <layout class="QGridLayout" name="gridLayout_2">
      <item row="0" column="0">
       <widget class="QLabel" name="label_3">
        <property name="text">
         <string>Column:</string>
        </property>
       </widget>
      </item>
      <item row="0" column="1">
       <widget class="QComboBox" name="secondaryColumnCombo">
        <item>
         <property name="text">
          <string>None</string>
         </property>
        </item>
       </widget>
      </item>
      <item row="0" column="2">
       <spacer name="horizontalSpacer_2">
        <property name="orientation">
         <enum>Qt::Horizontal</enum>
        </property>
        <property name="sizeHint" stdset="0">
         <size>
          <width>28</width>
          <height>20</height>
         </size>
        </property>
       </spacer>
      </item>
      <item row="1" column="0">
       <widget class="QLabel" name="label_4">
        <property name="text">
         <string>Order:</string>
        </property>
       </widget>
      </item>
      <item row="1" column="1" colspan="2">
       <widget class="QComboBox" name="secondaryOrderCombo">
        <item>
         <property name="text">
          <string>Ascending</string>
         </property>
        </item>
        <item>
         <property name="text">
          <string>Descending</string>
         </property>
        </item>
       </widget>
      </item>
     </layout>
    </widget>
   </item>
   <item row="3" column="0">
    <widget class="QGroupBox" name="tertiaryGroupBox">
     <property name="title">
      <string>&amp;Tertiary Key</string>
     </property>
     <layout class="QGridLayout" name="gridLayout_3">
      <item row="0" column="0">
       <widget class="QLabel" name="label_5">
        <property name="text">
         <string>Column:</string>
        </property>
       </widget>
      </item>
      <item row="0" column="1">
       <widget class="QComboBox" name="tertiaryColumnCombo">
        <item>
         <property name="text">
          <string>None</string>
         </property>
        </item>
       </widget>
      </item>
      <item row="0" column="2">
       <spacer name="horizontalSpacer_3">
        <property name="orientation">
         <enum>Qt::Horizontal</enum>
        </property>
        <property name="sizeHint" stdset="0">
         <size>
          <width>28</width>
          <height>20</height>
         </size>
        </property>
       </spacer>
      </item>
      <item row="1" column="0">
       <widget class="QLabel" name="label_6">
        <property name="text">
         <string>Order:</string>
        </property>
       </widget>
      </item>
      <item row="1" column="1" colspan="2">
       <widget class="QComboBox" name="tertiaryOrderCombo">
        <item>
         <property name="text">
          <string>Ascending</string>
         </property>
        </item>
        <item>
         <property name="text">
          <string>Descending</string>
         </property>
        </item>
       </widget>
      </item>
     </layout>
    </widget>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections>
  <connection>
   <sender>okButton</sender>
   <signal>clicked()</signal>
   <receiver>SortDialog</receiver>
   <slot>accept()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>258</x>
     <y>31</y>
    </hint>
    <hint type="destinationlabel">
     <x>112</x>
     <y>137</y>
    </hint>
   </hints>
  </connection>
  <connection>
   <sender>cancelButton</sender>
   <signal>clicked()</signal>
   <receiver>SortDialog</receiver>
   <slot>reject()</slot>
   <hints>
    <hint type="sourcelabel">
     <x>283</x>
     <y>65</y>
    </hint>
    <hint type="destinationlabel">
     <x>167</x>
     <y>152</y>
    </hint>
   </hints>
  </connection>
  <connection>
   <sender>moreButton</sender>
   <signal>toggled(bool)</signal>
   <receiver>secondaryGroupBox</receiver>
   <slot>setVisible(bool)</slot>
   <hints>
    <hint type="sourcelabel">
     <x>259</x>
     <y>183</y>
    </hint>
    <hint type="destinationlabel">
     <x>205</x>
     <y>248</y>
    </hint>
   </hints>
  </connection>
  <connection>
   <sender>moreButton</sender>
   <signal>toggled(bool)</signal>
   <receiver>tertiaryGroupBox</receiver>
   <slot>setVisible(bool)</slot>
   <hints>
    <hint type="sourcelabel">
     <x>276</x>
     <y>182</y>
    </hint>
    <hint type="destinationlabel">
     <x>206</x>
     <y>340</y>
    </hint>
   </hints>
  </connection>
 </connections>
</ui>
相关推荐
©️2020 CSDN 皮肤主题: 黑客帝国 设计师:白松林 返回首页