Qt学习日志-第四章

来源:互联网 发布:java读取hdfs文件内容 编辑:程序博客网 时间:2024/05/22 17:16

实现程序的功能

在前面两章中我们解释了怎么创建spreadsheet程序的UI界面。在这章中,我们将完成这个程序的功能。还有,我们将看到怎么加载文件和保存文件,怎么将数据保存在内存中,怎么实现剪切板的操作,怎么让QTableWidget增加对公式的支持。

 

中央组件(TheCentral Widget

QMainWindow的中央区域可以被任何组件(widget)占据。这里是一些可能占据中央区域的组件:

1.      使用标准的Qt组件。

一个标准的Qt组件比如QTableWidget或者QTextEdit可以被用来作为一个中央组件。这种情况下,程序的一些功能,比如加载和保存文件,必须在其他地方实现(比如,可以在QMainWindow的子类中)。

2.      使用一个定制的组件

一些特殊功能的应用程序常常需要在一个定制的组件上显示一些数据。例如,一个图标编辑器的程序,则有一个IconEditor组件作为它的中央组件。第5章中我们会解释怎么在Qt中编写一个定制的组件。

3.      使用一个空白的组件和一个布局管理器(layout manager

有时候程序的中央区域被许多组件占据着。可以这样来实现这个功能,使用一个QWidget类来作为其他组件的父组件,并且使用布局管理器来对这些子组件进行设定大小和排列位置。

4.      使用切分窗口(splitter

当多个组件需要占据中央区域时,另一个方法是使用QSplitterQSplitter可以对它的子组件进行水平的或者垂直的排列,有一个splitter句柄可以让用户进行大小控制。Splitter可以包含任何组件,包括其他的splitters

5.      使用MDI区域

如果一个程序使用MDI,则中央区域被一个QMdiArea组件占据着,每个MDI窗口都是这个组件的一个子窗口。

 

Layouts, splitters MDI区域可以跟标准Qt组件或者定制组件绑定起来。我们会在第6章中对这些类进行更加深入的探讨。

spreadsheet程序来说,一个QTableWidget子类被用来作为中央组件。QTableWidget类已经提供了spreadsheet程序需要的大部分的功能,但是这个类不支持剪贴板操作,也不支持公式运算,比如”=A1+A2+A3”。我们会在Spreadsheet类中实现这些功能。

 

继承QTableWidget

Spreadsheet类继承于QTableWidget,如下图所示。

QTableWidget类是一个有效的代表二维散列数组网格。它会在其指定的区域内显示任何用户滚动到的单元格。当用户在一个空的单元格内输入一些文本,QTableWidget会自动创建一个QTableWidgetItem项来保存这些文本。

QTableWidget继承于QTableView(其中一个模式/视图类,我们会在第10章中会更加详细的讲解)。另一个具有很多功能的表格类是QicsTable。可以从下列网址获得:

http://www.ics.com

让我们开始实现Spreadsheet类,下面是头文件:

#ifndef SPREADSHEET_H
#define SPREADSHEET_H
 
#include <QTableWidget>
 
class Cell;
class SpreadsheetCompare;

 

Cell类和SpreadsheetCompare类的前向申明。

QTableWidget单元格的属性,例如文本以及对齐等,被保存在QTableWidgetItem中。跟QTableWidget类不同,QTableWidgetItem不是一个组件类;它是一个纯数据类。Cell类继承于QTableWidgetItem类,会在这一章的最后一部分进行讲解。

 

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()来清除spreadsheet。我们也使用了从QTableWiget类继承而来的一些函数,比如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();

 

Spreadsheet类提供了很多槽来执行EditToolsOptions菜单中的一些操作,并提供了一个信号modified(),来通知修改已经发生了。

private slots:
    
void somethingChanged();

我们定义一个私有槽用于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;

};

在类的私有区域,我们申明了三个常量,四个函数和一个变量。

 

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

                    
const QStringList &row2) const;

 
    
enum { KeyCount = 3 };

    
int keys[KeyCount];

    
bool ascending[KeyCount];

};
 
#endif

这里定义了一个SpreadsheetCompare类。我们会在讲解Spreadsheet::sort()函数时对这个类进行详细讲解。

 

现在我们来看看具体的实现:

 

#include <QtGui>
 
#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会自动创建QTableWidgetItem来保存这些文本数据。在我们的程序中,我们希望Cell被创建来替代上面的QTableWidgetItem。我们可以在构造函数中调用setItemPrototype()函数来达到这个目的。这样每次需要一个新项时,QTableWidget内部会克隆Cell

在构造函数中,我们又设置了选择模式为 QAbstractItemView::ContiguousSelection来允许选中单个矩形区域。我们连接了表格组件的itemChanged()信号和私有槽somethingChanged(),这样确保当用户编辑一个单元格时,somethingChanged()槽被调用。最后我们调用clear()来重新设置表格大小并且设置列的头信息。

 

void Spreadsheet::clear()
{
    
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);

}

clear()函数在Spreadsheet构造函数中被调用来初始化spreadsheet。这个函数也在MainWindow::newFile()函数中被调用。

我们本来可以直接用QTableWidget::clear()来清除所有的项及任何选中的区域,但是这里我们需要让每一列的头部保持当前的大小。所以,我们先把表格的大小设置成0x0,清除整个spreadsheet,包括头部。然后我们设置表格的大小为ColumnCount xRowCount (26 x 999)并且用QTableWidgetItems类设置水平头部(包含列名称AB,,, Z)。我们不需要设置垂直的头部标签,因为这些是默认设置为1,2,,,,,999。最后我们把当前单元格设置为A1

 

QTableWidget有多个子组件组成。在最上面有一个水平的QHeaderView,在左边有一个QHeaderView,以及两个QScrollBar。中间的区域被一个特殊的组件占据着,这个组件我们叫作viewportQTableWidget在这个组件上来画单元格。我们可以调用QTableViewQbstractScrollArea类中的一些函数来得到这些子组件。QAbstractScrollArea提供一个可滚动的viewport和两个滚动条,滚动条可以被打开也可以被关闭。我们在第6章中对QScrollArea子类会进行详细的讲解。

 

Cell *Spreadsheet::cell(int row, int column) const
{
    
return static_cast<Cell *>(item(row, column));

}

cell()私有函数根据给定的行和列返回一个Cell对象。这个函数类似于QTableWidget::item()用来返回QTableWidgetItem指针。

 

QString Spreadsheet::text(int row, int column) const
{
    
Cell *c = cell(row, column);

    
if (c) {

        
return c->text();

    
} else {

        
return "";

    
}

}

私有函数text()根据给定的行和列返回文本。如果cell()返回null指针,说明这个单元格时空的,我们返回一个空字符串。

 

QString Spreadsheet::formula(int row, int column) const
{
    
Cell *c = cell(row, column);

    
if (c) {

        
return c->formula();

    
} else {

        
return "";

    
}

}

formula()函数返回此单元格的公式。很多情况下,公式和文本是一样的;比如,公式“Hello”即是字符串“Hello”,因此当用户在一个单元格中输入“Hello”并点击确认,单元格会显示文本“Hello”。但是也有一些例外:

1. 如果公式是一个数组。比如,公式是1.50,则被认为是一个双精度值1.5,在spreadsheet中就以右对齐方式显示1.5

2. 如果公式以单引号开头,则剩余部分被当作字符串。例如,公式’”12345”,则被当作是字符串“12345.

3. 如果公式是以一个等号=开头的,则被视为一个代数公式。例如,如果单元格A1包含“12”,单元格A2包含“6”,则公式“=A1+A2”被认为是18

 

保存数据项

spreadsheet程序中,每个非空的单元格都被当作独立的QTableWidgetItem对象保存在内存中。保存数据项这一方法也被用于QListWidgetQTreeWidget类,对应的被保存在QListWidgetItemQTreeWidgetItem对象中。

Qtitem类也可以作为数据容器单独使用,并不一定要跟那些组件绑定使用。比如,QTableWidgetItem已经保存了一些属性,包括字符串,字体,颜色和图标,以及一个指向QTableWidget对象的指针。这些item也可以保存一些数据(QVariant,包括登记的定制类型,通过继承这些item类我们可以提供额外的功能。

老的SDKitem类里面提供一个void指针来保存定制的数据。在Qt中,更常用的方法是调用setData()函数。但是如果实在需要一个void指针,我们可以继承一个item类,并且添加一个void指针的成员变量。

对那些更加复杂的数据处理,比如大块数据设置,复杂数据项,数据库集成以及多数据视图,Qt提供了一系列的模式/视图(model/view)类,来把数据从他们的UI中分离出来。我们会在第10章中讲解这部分内容。

 

把公式转换成值的任务在Cell类中完成。我们要清楚cell中显示的文本是公式计算的结果,不是公式本身。

void Spreadsheet::setFormula(int row, int column,
                             
const QString &formula)

{
    
Cell *c = cell(row, column);

    
if (!c) {

        
c = new Cell;

        
setItem(row, column, c);

    
}

   
 
c->setFormula(formula);

}

setFormula()私有函数用来对指定的单元格设置公式。如果单元格中已经有一个Cell对象,我们直接重用它,否则,我们创建一个新的Cell对象,并调用QTableWidget::setItem()来把它插入到表格中。最后我们调用Cell对象自己的setFormula()函数,如果单元格显示在屏幕上的话,这个函数会重画单元格。我们不用担心以后怎么删除Cell对象,QTableWidget会对单元格进行管理,它会在适当的时候自动删除它。

 

QString Spreadsheet::currentLocation() const
{
    
return QChar('A' + currentColumn())

           
+ QString::number(currentRow() + 1);

}

currentLocation()函数返回当前单元格的位置,返回的格式是列字母加上行号。

MainWindow::updateStatusBar()使用这个函数来得到单元格位置,并在状态栏上进行显示。

 

QString Spreadsheet::currentFormula() const
{
    
return formula(currentRow(), currentColumn());

}

currentFormula()函数返回当前单元格的公式。这个函数在MainWindow::updateStatusBar()中被调用。

 

void Spreadsheet::somethingChanged()
{
    
if (autoRecalc)

        
recalculate();

    
emit modified();

}

somethingChanged()私有槽用来重新计算整个spreadsheet,如果重新计算选项被打钩。同时它也发射modified()信号。

 

加载和保存

我们现在来实现spreadsheet程序的加载和保存功能,文件以定制的二进制格式保存。我们使用QFileQDataStream类,这两个类提供独立于平台的二进制I/O接口。

我们先来实现怎么写一个spreadsheet文件:

bool Spreadsheet::writeFile(const QString &fileName)
{
    
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);

 
    
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;

}

writeFile()函数在MainWindow::saveFile()中被调用,用来将文件写到磁盘上。如果成功的话返回true,错误将返回false

首先我们用指定的文件名创建一个QFile对象,然后调用open()打开这个文件。我们又创建了一个QDataStream对象对QFile对象进行操作,并用这个对象进行写数据。

在写数据之前,我们把鼠标光标改变为标准的等待光标,一旦数据写完了再恢复为原来的光标。在函数的最后,文件会在QFile的析构函数中被自动关闭。

 

QDataStream支持基本的C++类型,也支持许多Qt的类型。句法跟标准的C++ iostream类一样。例如:

out << x << y << z;

写变量x,y,z的值到out流中,

in >> x >> y >> z;

in流中读取值到x,y,z变量中。因为C++的整型值在不同的平台上可能会有不同的字节大小,如果我们用下列类型进行强制类型转换就比较安全。 qint8, quint8, qint16, quint16, qint32,quint32,qint64quint64.

 

Spreadsheet程序的文件格式非常简单。这个文件以一个32位数字开始,这个数字用来指示文件格式(MagicNumber,在spreadsheet.h文件中被定义为0x7F51C883,是一个随机数)。紧跟着一系列的数据块,每一块包含单个单元格的行,列和公式。为了节省空间,我们略去了空的单元格。具体格式如下:

 

QDataStream来表示精确的二进制数据。比如,quint16类型以大端模式保存为2个字节,而QString类型则保存为字符串长度,紧跟着Unicode字符。

Qt类型的二进制表示方法自从版本1.0以后就涉及到很多。将来的Qt版本发布中还会继续添加新的Qt类型。默认情况下,QDataStream使用最新的二进制格式的版本(Qt 4.3中视版本9),但是它能够读取就得版本。为了避免任何兼容性问题,如果程序将来用新的Qt版本进行重新编译时,我们会明确告知QDataStream使用版本9,不管我们正在编译的Qt是什么版本。(QDataStream::Qt_4_3就是一个等于9的常量。)

 

QDataStream功能很丰富,可以用于QFile, 也可以用于QBuffer QProcess,QTcpSocket, QUdpSocket或者QSslSocketQt也提供QTextStream类用于代替QDataStream类读写文本文件。第12章我们会深入的讲解这些类,也会描述一些不同的方法来处理不同的QDataStream版本。

 

bool Spreadsheet::readFile(const QString &fileName)
{
    
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;

}

readFile()函数跟writeFile()函数很相似。我们使用QFile来读取文件,这次我们使用QIODevice::ReadOnly而不是QIODevice::WriteOnly。然后我们设置QDataStream版本为9.读取的格式必须总是跟写的格式保持一致。

 

如果文件开头有正确的magicnumber,我们调用clear()来清空spreadsheet中所有的单元格,然后把数据读到单元格里面。因为文件只包含那些非空单元格的数据,这样很有可能有些单元格没有设置到,我们必须确保在读入数据之前所有单元格被清空。

 

编辑菜单的实现

我们现在可以实现编辑菜单的对应的槽函数来。

 

void Spreadsheet::cut()
{
    
copy();

    

del
();
}

cut()槽对应Edit|Cut。实现很简单,剪切跟拷贝类似,只是拷贝后再进行删除。

 

void Spreadsheet::copy()
{
    
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);

}

copy()槽对应Edit|Copy。这个函数遍历当前选中的单元格(如果没有指定区域就是当前单元格)。每个选中的单元格的公式被添加到QString,每一行用行分隔符’/n’分开,每一列用tab字符’/t’。如下图所示:

 

系统的剪切板在Qt中用静态函数QApplication::clipboard()来得到。调用QClipboard::setText()把文本添加到剪切板上,任何支持纯文本的程序可以使用剪切板上的文本。我们这个tab和行分隔符的格式被很多程序相兼容,包括微软的Excel

QTableWidget::selectedRanges()函数返回一个选中的列表。我们知道这个函数肯定只返回一个选中的区域,因为我们在构造函数中设置了QAbstractItemView::ContiguousSelection模式。为了方面,我们定义了selectedRange()函数来返回选中的区域:

QTableWidgetSelectionRange Spreadsheet::selectedRange() const
{
    
QList<QTableWidgetSelectionRange> ranges = selectedRanges();

    
if (ranges.isEmpty())

        
return QTableWidgetSelectionRange();

    
return ranges.first();

}

如果存在一个选中的区域,我们只要返回列表的第一个(也是唯一一个)成员。任何情况下必须都有一个被选中的区域,因为ContiguousSelection模式认为当前的单元格属于被选中的区域。

 

void Spreadsheet::paste()
{
    
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();

}

paste()槽对应Edit|Paste。我们从剪切板上取得文本,然后调用静态函数QString::split()来把字符串打断成QStringList。每一行变成列表中的一个字符串。

接下来,我们来确认拷贝区域。行数就是QStringList中字符串的数量;列数就是第一行中tab字符的数量再加上1.如果只有一个单元格被选中,我们就使用那个单元格作为粘帖区域的左上角。否则,我们使用当前选中的区域作为粘帖的区域。

 

为了执行粘帖,我们遍历所有的行,然后通过使用QString::split()把每一行拆分成所有单元格,这次使用tab字符’/t’作为拆分符。

 

void Spreadsheet::del()
{
    
QList<QTableWidgetItem *> items = selectedItems();

    
if (!items.isEmpty()) {

        
foreach (QTableWidgetItem *item, items)

            
delete item;

        
somethingChanged();

    
}

}

del()槽对应Edit|Delete。如果有选中的项,这个函数删除它们,然后调用somethingChanged() 使用delete删除Cell对象就足以清楚单元格。当QTableWidgetItems被删除时QTableWidget会知道并且自动重画自己。如果我们对那些删除的单元格调用cell()会返回null指针。

 

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

}
 
void Spreadsheet::selectCurrentColumn()
{
    
selectColumn(currentColumn());

}

selectCurrentRow()selectCurrentColumn()函数对应Edit|Select|RowEdit|Select|Column菜单选项。实现依赖于QTableWidgetselectRow()selectColumn()函数。我们不需要实现Edit|Select|All背后的功能,因为那是QTableWidget的继承函数QAbstractItemView::selectAll()提供。

 

void Spreadsheet::findNext(const QString &str, Qt::CaseSensitivity cs)
{
    
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();

}

findNext()槽从当前光标右边的单元格开始遍历,向右移动直到最后一列,然后继续从下一行的第一列开始,这样一直遍历下去,直到文本被找到或者已经搜索到本文档的最后一个单元格。例如,如果当前单元格是C24, 我们搜索D24, E24, …,Z24,然后A25, B25,C25, …, Z25,直到Z999

如果我们找到了匹配的文本,就清除当前选中,把光标移到匹配的单元格上,并激活当前窗口。如果没有找到匹配的文本,我们让程序发出蜂鸣声来表示搜索失败。

 

void Spreadsheet::findPrevious(const QString &str,
                               
Qt::CaseSensitivity cs)

{
    
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();

}

findPrevious()槽类似于findNext(),除了它是向后遍历,最后终止于单元格A1.

 

 

实现其它的一些菜单

我们现在来实现ToolsOptions菜单的那些槽函数。这些菜单显示如下:

 

void Spreadsheet::recalculate()
{
    
for (int row = 0; row < RowCount; ++row) {

        
for (int column = 0; column < ColumnCount; ++column) {

            
if (cell(row, column))

                
cell(row, column)->setDirty();

        
}

    
}

    
viewport()->update();

}

recalculate()槽对应于菜单项Tools|Recalculate。当需要时也会被Spreadsheet自动调用。

我们遍历所有的单元格,每个单元格调用setDirty()来标记每个单元格都需要重新计算。下次QTableWidget调用单元格的text()函数来得到单元格的值时,这个值会被重新计算。

 

然后我们调用viewportupdate()函数来重画整个spreadsheetQTableWidget的重画代码会在每个可见的单元格上调用text()来得到需要显示的值。因为我们对每个单元格都调用的setDirty(),这样调用text()将会使用一个新的计算的值。对值的计算也许会要求那些不可见的单元格进行重新计算,这样在viewport中就可以显示正确的文本。计算的工作是在Cell类中完成的。

 

void Spreadsheet::setAutoRecalculate(bool recalc)
{
    
autoRecalc = recalc;

    
if (autoRecalc)

        
recalculate();

}

setAutoRecalculate()槽对应于菜单项Options|Auto-Recalculate。如果这个功能被打开,我们立即重新计算这个spreadsheet来确保这个spreadsheet是最新的;过后,recalculate()会自动被somethingChanged()调用。

 

我们不需要为菜单项Options|ShowGrid添加任何代码,因为QTableWidget已经有一个setShowGrid()槽,这个槽继承于QTableView。现在只剩下Spreadsheet::sort(),这个函数在MainWindow::sort()被调用:

 

void Spreadsheet::sort(const SpreadsheetCompare &compare)
{
    
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();

}

这个函数根据compare对象中的键值和排序次序(升序或降序)来对选中的行进行排序。我们用QStringList来表示每一行的数据,把选中的行保存在这个字符串列表中。我们使用QtqStableSort()算法,为了简便我们按公式而不是按值排序。整个过程用下图来表示。我们会在第11章中对Qt的标准算法和数据结构进行讲解。

 

 

qStableSort()函数接受一个起始迭代器,一个末尾迭代器和一个比较函数。这个比较函数接受2个参数(两个QStringList),如果第一个参数比第二个参数小则返回true,反之返回false。我们传递给qStableSort()的是compare对象,并不是一个真正的函数,但是我们这里可以这样用,稍后我们对它进行讲解。

qStableSort()调用以后,我们把数据重新添加到表格中,取消选中,并调用somethingChanged()

 

spreadsheet.h文件中,SpreadsheetCompare类定义如下:

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

                    
const QStringList &row2) const;

 
    
enum { KeyCount = 3 };

    
int keys[KeyCount];

    
bool ascending[KeyCount];

};

SpreadsheetCompare类很特殊,因为这个类实现了()运算符。这样就允许我们像函数一样来使用这个类。这种类叫做函数对象(functionobjects),或函数因子(functors)。为了加深了解函数对象,我们给一个简单的例子:

class Square
{
public:
    
int operator()(int x) const { return x * x; }

}

Square类提供了一个函数, operator() (int),返回参数的平方值。这里我们不是取名一个叫做compute(int)的函数,而是直接实现这个操作符(),这样我们就可以像函数一样来使用Square对象:

Square square;

int y = square(5);

// y equals 25

 

现在我们来看看SpreadsheetCompare的例子:


QStringList row1, row2;

SpreadsheetCompare compare;

...

if (compare(row1, row2)) {

    // row1 is lessthan row2

}

compare对象就像compare()函数一样来使用。另外,这个对象可以访问所有的排序键值和次序,因为这些是这个对象的成员变量。

 

另一个可选的方案是把键值和排序次序值保存在全局变量中,然后用一个compare()函数。然而,使用全局变量来进行通讯代码不是很雅观,也可能导致隐含的bug.当我们需要跟模板函数打交道时(比如qStableSort()),使用函数对象将非常有用。

下面是比较函数的实现,用来比较spreadsheet中的两行:

bool SpreadsheetCompare::operator()(const QStringList &row1,
    
                                
const QStringList &row2) const

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

}

如果第一行比第二行小,这个操作符返回true;反之,返回falseQStableSort()函数使用这个函数的返回值来进行排序。

SpreadsheetCompare对象中的键值和排序数组是在MainWindow::sort()函数中初始化的。每一个键都保存着一个列索引,或者-1 (“空值”)

 

我们按照顺序比较每一行中对应的单元格。一旦发现不同,就返回truefalse值。如果两行都相同,那么返回falseqStableSort()函数使用排序前的顺序解决这个问题。如果排序前的顺序是row1row2之前,且经比较相等,在结果中row1还是在row2之前。这就是qStableSort()和qSort()之间的不同,也就是qStableSort()qSort()更稳定。

 

我们现在已经完成了Spreadsheet类。在下一节中我们要来实现Cell类。这个类用来保存单元格的公式,并且重新实现了QTableWidgetItem::data()函数,Spreadsheet通过QTableWidgetItem::text()函数间接调用这个函数,来显示单元格公式的计算结果。

 

 

继承QTableWidgetItem

Cell类继承于QTableWidgetItem类。这个类在Spreadsheet中可以很好的工作,而且没有任何对Spreadsheet的依赖,即完全独立,所以理论上可以被用于任何QTableWidget中。这里是头文件:

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

};
 
#endif

Cell类扩展了QTableWidgetItem类的功能,添加了两个私有变量:

cachedValue 暂存着单元格的值,保存类型为QVariant

cacheIsDirty 如果暂存的值不是最新的,这个值为true

 

我们这里使用QVariant是因为一些单元格的值是double,而一些是QString类型的。

 

变量cachedValuecacheIsDirty的申明中添加了C++关键字mutable。这样做的目的是允许我们在const函数中修改它们的值。另一种方法是,我们可以在每次调用text()时重新计算这两个变量的值,但是这样效率不高。

 

注意在这个类的定义中没有Q_OBJECT宏。Cell类是一个纯C++类,没有任何信号和槽。事实在,因为QTableWidgetItem类不是继承于QObject,我们不能在这个类中使用信号和槽。Qt中所有的item类都不是继承于QObject类,来保证最低限度的系统开销。如果确实需要信号和槽,可以在包含这些itemwidget中实现,或者可以使用多继承的方法,让其继承于QObject

 

这里是cell.cpp的开始部分:

#include <QtGui>
 
#include "cell.h"
 
Cell::Cell()
{
    
setDirty();

}

在构造函数中,我们仅仅设置缓存为脏。这里不需要传递一个父参数;当单元格用setItem()函数被插入到QTableWidget时,QTableWidget会自动接管这个类。

每一个QTableWidgetItem类可以保存一些数据,每一个QVariant都以一种角色保存一类数据。最常用的角色是Qt::EditRoleQt::DisplayRole。编辑角色被用来那些可以编辑的数据,而显示角色则只能用来显示。常常这两种角色的数据是相同的,但是在Cell类中编辑角色对应于单元格的公式,而显示角色对应于单元格的值(公式的值)。

 

QTableWidgetItem *Cell::clone() const
{
    
return new Cell(*this);

}

QTableWidget需要创建一个新的单元格时,clone()函数被调用。比如,当用户开始输入一个以前没有用过的单元格时。传给QTableWidget::setItemPrototype()函数的实例就是克隆的item。我们用C++默认的拷贝构造函数来创建一个新的Cell实例。

 

void Cell::setFormula(const QString &formula)
{
    
setData(Qt::EditRole, formula);

}

setFormula()函数设置单元格的公式。这个函数就是简单的调用setData()函数,制定为编辑角色。它在Spreadsheet::setFormula()函数中被调用。

 

QString Cell::formula() const
{
    
return data(Qt::EditRole).toString();

}

formula()函数在Spreadsheet::formula()中被调用。就像setFormula(),它只是个方便函数,取得itemQt::EditRole数据。

 

void Cell::setData(int role, const QVariant &value)
{
    
QTableWidgetItem::setData(role, value);

    
if (role == Qt::EditRole)

        
setDirty();

}

如果我们有一个新的公式,我们设置cacheIsDirtytrue,来确保在下次调用text()函数时单元格被重新计算。

 

Cell类中没有定义text()函数,尽管我们在Spreadsheet::text()中调用了Cell实例的text()函数。Text()函数由QTableWidgetItem类提供;等效于调用data(Qt::DisplayRole).toString()

 

void Cell::setDirty()
{
    
cacheIsDirty = true;

}

setDirty()函数用来强制重新计算单元格的公式。通过设置cacheIsDirtytrue,意味着cachedValue已经不再是最新的了。程序会在必要的时候执行重新计算的操作。

 

QVariant Cell::data(int role) const
{
    
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);

    
}

}

data()函数是对QTableWidgetItem类中的data()函数的重新实现。如果用Qt::DisplayRole调用,返回显示的文本;如果用Qt::EditRole调用则返回公式;如果用Qt::TextAlignmentRole调用则返回合适的对齐方式。对于DisplayRole情况,根据value()函数得到的值来显示相关的内容。如果值无效(公式),我们返回####

Cell::value()返回QVariantQVariant可以用来存储不同的类型,例如doubleQString,并且提供了可以转换为相应类型的函数。例如,一个保存了double类型的QVariant调用toString()函数将产生double的字符串形式。QVariant以默认构造函数构建,初始值是invalid.

 

const QVariant Invalid;
QVariant Cell::value() const
{
    
if (cacheIsDirty) {

        
cacheIsDirty = false;

 
        
QString formulaStr = formula();

        
if (formulaStr.startsWith('/'')) {

            
cachedValue = formulaStr.mid(1);

        
} else if (formulaStr.startsWith('=')) {

            
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 {

            
bool ok;

            
double d = formulaStr.toDouble(&ok);

            
if (ok) {

                
cachedValue = d;

            
} else {

                
cachedValue = formulaStr;

            
}

        
}

    
}

    
return cachedValue;

}

value()函数返回单元格的值。如果cacheIsDirtytrue,我们需要重新计算值。如果公式以单引号开头(如,’”12345”),则后面的值视为一个字符串。

如果公式以等号(’=’)开头,我们提取后面的字符串,删除里面的空格,然后调用evalExpression()计算这个表达式的值。Pos参数以引用形式传递,它指示了此函数需要解析的字符的起始位置。调用evalExpression()成功以后,位置pos的字符应该是QChar::Null字符。如果解析失败,我们设置cacheValueInvalid

如果公式即不是以单引号开头也不是以等号开头,我们尝试着把它转换为浮点值toDouble().如果转换成功,我们把cachedValue设置为转换后的浮点值;否则,我们设置cachedValue为公式字符串。例如,公式为“1.50,则调用toDouble()成功,oktrue,返回1.5,而公式“WorldPopulation”,则调用toDouble()失败,okfalse,返回为0.0

通过传递给toDouble()一个指向bool类型的指针,我们能够知道返回0.0的结果是因为这个字符串本身就是0.0还是因为转换失败得到的。有时候转换失败得到的0值正是我们所需要的,这种情况我们不用传递一个bool的指针。考虑到性能和可移植性的原因,Qt绝不使用C++异常机制来报告一个错误。当然在Qt程序中你也可以使用C++异常机制,只要编译器支持。

Value()函数被声明为const。所以我们不得不把cachedValuecacheIsValid声明为mutable变量,这样编译器允许我们在const函数中修改它们。当然我们也可以把value()函数申明为非const类型的,删除mutable关键字,但是这样还是会编译失败,因为我们在data()中调用value(),而data()函数是const类型的。

 

我们现在已经完成了Spreadsheet程序,除了一些解析公式没有实现。接下来的部分我们讲述evalExpression()和两个帮助函数evalTerm()evalFactor()。代码有一点点复杂,我们把代码放在这里讲解使得这个程序完整。因为这部分代码与GUI编程无关,你可以略过这部分而直接读第5章。

 

evalExpression()函数返回spreadsheet表达式的值。表达式被定义成一个或多个项组成,这些项之间由’+’’-‘隔开,每一项由一个或多个因子组成,因子由’*’’/’隔开。通过把表达式分解成项式,把项式分解成因子,我们确保操作符按照正确的优先级进行计算。

例如, “2*C5+D6”表达式中,”2*C5”是第一项,D6是第二项。项“2*C5”中“2”是第一个因子,C5是第二个因子,项“D6”则由单个因子D6构成。一个因子可以是一个数字(“2”),一个单元格位置(”C5”,或者是一个括号中的表达式,有时候还有一个一元减号。

 

Spreadsheet表达式的语法由下图4.10定义。每个语法符号(表达式,项,因子)由一个对应的成员函数来解析,函数的结构则完全遵循这个语法结构。按照这种方法写出的解析器我们叫做递归解析器。

                            4.10 spreadsheet表达式的语法结构图

 

让我们从evalExpression()函数开始讲解,这个函数解析一个表达式:

 

QVariant Cell::evalExpression(const QString &str, int &pos) const
{
    
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;

}

首先我们调用evalTerm()函数来得到第一个项的值。如果接下来的字符是’+’’-‘,我们继续调用evalTerm()一次;否则,说明这个表达式只含有一个项式,这种情况下这个项式就是整个表达式的值。我们得到2个项式以后,根据操作符计算值。如果两个项式都是double型的,则结果为double,否则,我们设置结构为Invalid

继续按照这样计算,直到没有更多的项式。这个可以正确工作,因为加减法是按从左到右的优先级的,即”1-2-3”就是”(1-2)-3”,而不是“1-2-3)”.

 

QVariant Cell::evalTerm(const QString &str, int &pos) const
{
    
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;

}
 

evalTerm()函数和evalExpression()很像,只是这个函数处理乘除运算。唯一需要注意的地方就是在除法中分母不能为0,在很多处理器上这是一个错误。由于四舍五入引起误差,一般不建议判断0的浮点数的值,只要判断是否等于0.0就可以了。

 

QVariant Cell::evalFactor(const QString &str, int &pos) const
{
    
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;

}
 

evalFactor()函数比以上两个函数稍微有点复杂。我们先判断这个因子是否为负。然后判断是否以左括号开头。如果是的话,我们认为括号里的内容为表达式,调用evalExpression()。当解析一个括号表达式时,evalExpression()调用evalTerm(),而evalTerm()又调用evalFactor()evalFactor()会再次调用evalExpression()。这就是解析器中的递归调用。

如果因子中不是一个嵌套的表达式,我们提取下一个语法符号,它可能是一个单元格位置或一个数字。如果符号匹配QRegExp,我们认为这是一个单元格引用,调用这个单元格的value()函数。单元格可以是spreadsheet中的任意一个,也可以跟其他单元格具有依赖关系。依赖的存在不是一个问题;这些依赖关系的存在会触发更多的value()调用和解析,直到所有的依赖单元格的值都被计算出来。如果这个语法符号不是一个单元格位置,我们认为是一个数字。

 

下面这些情况会发生什么,如果单元格A1包含公式”=A1”,又或者A1包含“=A2”而A2包含”A1”。尽管我们没有专门编写代码来检测这种圆形封闭依赖关系的存在,在解析器中这些情况会返回一个无效的QVariant值。这能正确工作因为我们在调用evalExpression()函数之前在value()函数中设置了cacheIsDirtyfalse,并且cachedValueInvalid。如果evalExpression()函数在同一个单元格中递归调用value(),这个函数会立即返回Invalid,然后整个表达式被认为是Invalid

 

我们现在已经完全完成了公式解析器的编码。可以很容易的扩展这部分代码来处理预定义的spreadsheet函数,比如sum()avg(),只要扩展因子(Factor)的语法定义即可。另一个比较容易的扩展是用字符串操作数实现’+’操作符,这种方法不需要语法上的修改。

 

 

原创粉丝点击