第4章 实现程序的功能(实现编辑菜单)

发布: 2008-06-02 23:54

实现编辑菜单


我们现在准备实现与程序的编辑菜单相应的槽。
[code type="cpp-qt"]
void Spreadsheet::cut()
{
copy();
del();
}
[/code]
cut()槽对应于”Edit|cut”。它的实现非常简单,因为剪切与拷贝加删除一样。

图4.4 电子表格程序的编辑菜单

[code type="cpp-qt"]
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);
}
[/code]
copy()槽对应于”Edit|Copy”。它迭代当前的选中的单元格(如果没有选中任何单元格,它简单地处理当前单元格)。每个选中的单元格的标准添加到一个Qstring中,行之间以换行符分隔,列则以tab符分隔。
在Qt中通过QApplication::clipboard()静态函数获得系统剪贴板。通过调用QClipboard::setText(),就能把该文本放到剪贴板上,对于本程序和其他支持普通文本的程序都有效。我们使用tab和换行符的格式能被许多程序理解,包括Micro Excel。
图4.5 拷贝选中的区域到剪贴板上


QTableWidget::selectedRanges()函数返回选中区域的单元格列表。我们知道不会多于一个,因为我们在构造函数中把选择模式设置为QAbstractItemView::ContiguousSelection了。为了方便,我们定义了可返回选中区域的selectedRange()函数。
[code type="cpp-qt"]
QTableWidgetSelectionRange Spreadsheet::selectedRange() const
{
QList ranges = selectedRanges();
if (ranges.isEmpty())
return QTableWidgetSelectionRange();
return ranges.first();
}
[/code]
如果根本就没有选中单元格(好象原文的意思是这个,但描述却与之相反),我们简单地返回第一个(仅有的)即可。没有选中区域的情况应该是不存在的,因为ContiguousSelection会把当前的单元格作为选中的看待。但为了我们的程序在没有当前单元格的情况下能避免这一可能的bug,我们处理了这种情况。
[code type="cpp-qt"]
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();
}
[/code]
paste()槽对应于”Edit|Paste”。我们从剪贴板获取文本并调用静态函数QString::split()把它拆为一个QStringList。每行都变成列表中的一个字符串。
接下来,我们检测拷贝区域的维度。行数就是QStringList中字符串元素的个数。列数就是每一行中tab字符的个数加1。如果只有一个选中的单元格,我们把它当作粘贴区域左上角的单元格。否则,我们使用当前选中区域作为粘贴区域。
为执行粘贴操作,我们迭代所有的行并把每一个都使用QString::split()拆为单元格,但这回我们使用tab作为分隔符。图4.6阐明了这些步骤。

图4.6 把剪贴板上的文本粘贴到电子表格中

[code type="cpp-qt"]
void Spreadsheet::del()
{
foreach (QTableWidgetItem *item, selectedItems())
delete item;
}
[/code]
del()槽对应于”Edit|Delete”菜单项。它使用delete操作符删除选中区域所有的Cell对象来清空这些单元格就足够了。QTableWidget会关注在什么时候它的QTableWidgetItem被删除了,并且如果这些项中有任何一个是可见的,它将自动重绘自己。如果我们用一个被删除的单元格的坐标调用cell(),它将返回一个空指针。
[code type="cpp-qt"]
void Spreadsheet::selectCurrentRow()
{
selectRow(currentRow());
}
void Spreadsheet::selectCurrentColumn()
{
selectColumn(currentColumn());
}
[/code]
selectCurrentRow()和selectCurrentColumn()函数对应于“Edit|Select|Row ” 和 “Edit|Select|Column“菜单项。它们的实现依赖于QTableWidget的 selectRow() 和 selectColumn()函数。我们不需要实现” Edit|Select|All“背后的功能,因为它已经从QTableWidget的QAbstractItemView::selectAll()函数继承下来了。
[code type="cpp-qt"]
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();
}
[/code]
findNext()槽迭代从光标右侧的单元格开始直到最后一列单元格为止,然后继续从下一行第一列开始等待,直到然后直到找到查询文本或者直到最后一个单元格。例如,如果当前单元格是C24,我们搜索D24, E24, …, Z24, 而后是 A25, B25, C25, …, Z25等等,直到 Z999。如果没有发现匹配项,我们清空当前的选择,把光标移动到匹配的单元格,击活包含Spreadsheet的窗口。如果没有发现匹配项,我们让程序发声表示搜索结束但没有成功。
[code type="cpp-qt"]
{
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();
}
[/code]

findPrevious()槽与findNext()相似,但它向后迭代并停在单元格A1。

实现其他菜单


我们现在实现工具和选项菜单所用的槽。
图4.7 电子表格程序的工具和选项菜单

[code type="cpp-qt"]
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();
}
[/code]

recalculate()槽对应于“Tools|Recalculate“。它会被Spreadsheet在必要的时候自动调用。
我们遍历所有单元格并对每个单元格调用setDirty()设置需要重算的标志。接下来QTableWidget调用Cell 的text()以获取要在电子表格中显示的值,这个值会被重新求值。
然后我们调用当前视口上调用update()来重绘整个电子表格。在QTableWidget中的重绘代码再对每个可见单元格调用text()以获取要显示的值。因为我们已经对每个单元格都调用了setDirty(),text()调用将会使用新计算出的值。计算过程可能需要不可见单元格也被重新计算以及级联式计算,直到当前视口中所有需要重新计算的所有单元格都被重新计算为止。该计算过程由Cell类执行的。
[code type="cpp-qt"]
void Spreadsheet::setAutoRecalculate(bool recalc)
{
autoRecalc = recalc;
if (autoRecalc)
recalculate();
}
[/code]
setAutoRecalculate()槽对应于“Options|Auto-Recalculate“菜单项。如果这一特征被激活,我们将立即重新计算整个电子表格以保证它的内容是最新的。以后,recalculate()会被somethingChanged()自动调用。
我们不需要实现“Options|Show Grid“了,因为QTableWidget已经有一个从它的基类QTable-View继承来的setShowGrid()槽。那么剩下的就只有Spreadsheet::sort()了,它在MainWindow::sort()中被调用:
[code type="cpp-qt"]
void Spreadsheet::sort(const SpreadsheetCompare &compare)
{
QList 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();
}
[/code]
排序执行于当前选中区域并且通过排序键和存储在compare对象中的排序顺序重排这些行。我们使用一个QStringList表示数据的每一行,并把这些选中的行当作行的列表。我们使用Qt的qStableSort()算法,简单其间,通过公式而不是通过值进行排序。Qt的标准算法和数据结构将在第11章中说明(窗口类)。
图4.8 把选中区域当作行的列表


qStableSort()函数接受一个初始迭代器,一个结束迭代器和一个比较函数。该比较函数是一个有两个参数(两个QStringList)的函数,它在第一个参数“小于“第二个参数时返回true,而其他情况时返回false。我们传递过来当比较函数的compare对象其实不是个函数,但它能当函数用,正如我们呆会见到的一样。

图4.9 排序后把数据放回到表格中


在执行完qStableSort()之后,我们把数据放回到表格中,清除选中区域,然后再调用somethingChanged()。
在文件spreadsheet.h中,SpreadsheetCompare类是这样定义的:
[code type="cpp-qt"]
class SpreadsheetCompare
{
public:
bool operator()(const QStringList &row1,
const QStringList &row2) const;
enum { KeyCount = 3 };
int keys[KeyCount];
bool ascending[KeyCount];
};
[/code]
SpreadsheetCompare比较特别,因为它实现了一个()操作符。这允许用户把该类当任务函数使用。这种类被叫叫做函数对象,或者叫仿函式。为理解仿函式是如何工作的,我们先来看个简单的例子:
[code type="cpp-qt"]
class Square
{
public:
int operator()(int x) const { return x * x; }
}
[/code]

Square类提供了一个函数,operator()(int),它将返回参数的平方。把它命名为operator()(int),而不是compute(),我们就能获得把Square类型的对象当作函数来使用的能力。

Square square;
int y = square(5);

现在让我们看一个调用SpreadsheetCompare的例子:
[code type="cpp-qt"]
QStringList row1, row2;
QSpreadsheetCompare compare;
...
if (compare(row1, row2)) {
// row1 is less than row2
}
[/code]
Compre对象可被当作一个普通的compare()函数一样使用。另外,它的实现能访问所有的排序键和排序次序,他们都被当作成员变量存储了。
这个方案的替代方案是:在全局变量中存储排序键和排序次序并使用一个普通compare()函数。然而,通过全局变量通过是不雅的,而且可能导致微秒的bug。仿函式是一种面向像qStableSort()一样的模板函数的更强大的用法。
下面是用于比较电子表格两行的函数的实现:
[code type="cpp-qt"]
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;
}
[/code]

如果第一行小于第二行的时候该操作返回true,否则返回false。qStableSort()函数使用这个函数的结果执行排序。
SpreadsheetCompare对象的keys和ascending数组用于MainWindow::sort()函数(在第2章)。每个键持有一个列索引,或者是-1(“None“)。
我们对每个键按次序比较两行中对应的单元格。一旦我们发现不同的地方,我们返回一个合适true或者false值。如果所有的比较结果都相等,则返回false。qStableSort()对这种情况将使用排序前的顺序。即如果row1原来在row2之前并且其中一个并不比另一个小,那么结果中row1仍旧在row2之前。这就是qStableSort()区别于它的不稳定版本qSort()的地方。
我们现在已经完成了Spreadsheet类的编写。在下一节中,我们要回顾一下Cell类。这个类用于保存单元格公式并提供了一个被Spreadsheet 的QTableWidgetItem::text()函数间接调用的QTableWidgetItem::data()函数的实现,以显示对一个单元格公式求值的结果。

创建QTableWidgetItem子类


Cell类继承自QTableWidgetItem。这个类被设计用于Spreadsheet并能很好的工作,但它对该类没有特别的依赖,所以理伦上能用于所有QTableWidget。下面是它的头文件:
[code type="cpp-qt"]
#ifndef CELL_H
#define CELL_H
#include
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
[/code]
Cell类通过添加两个私有变量扩展了QTableWidgetItem:
cachedValue缓存单元格的值,把它当作Qvariant类型。
cacheIsDirty 如果缓存的值不是最新的则它是true。
因为一些单元格是double类型的值,而另一些是QString类型的值,所有我们使用了Qvariant。
cachedValue 和 cacheIsDirty变量是使用c++ mutable关键字声明的。这允许我们在常量型函数中修改这些变量。另一种选择是,我们在每次text()调用的时候重新计算 ,但这应该是不必要的麻烦。
注意在该类的定义中没有OBJECT宏。Cell是一个普通C++类,没有信号或者槽。事实上,因为QTableWidgetItem不是继承自QObject,在Cell中不能有信号和槽。Qt的项类并不是继承自QOIbject以保证他们的额外开支最少。如果需要信号和槽,他们可以在包含这些项的物件类中实现,或者,更特别地,可以与QObject一块使用多继承。
下面是cell.cpp的开始部分:
[code type="cpp-qt"]
#include
#include "cell.h"
Cell::Cell()
{
setDirty();
}
[/code]
在构造函数中,我们仅需要把缓存设为脏数据。这里不需要传递一个父物件。因为这个单元格被用setItem()插入到QTableWidget中,QTableWidget会自动获得对它的拥有权。
每个QTableWidgetItem 都能保存一些数据,每个数据“角色”为一个QVariant。最常用的角色是Qt::EditRole 和 Qt::DisplayRole。编辑角色表示要被编辑的数据,显示角色表示要被显示的数据。通过他们俩是一样的数据,但是在单元格中,编辑角色对应于单元格的公式,显示角色对应于单元格的值(对公式求值的结果)。
[code type="cpp-qt"]
QTableWidgetItem *Cell::clone() const
{
return new Cell(*this);
}
[/code]
在QTableWidget要创建一个新的单元格的时候,clone()函数将被调用。例如,当用户向一个从未使用过的空单元格中输入时文本时。传递到QTableWidget::setItemPrototype()的实例是克隆出来的。因为对Cell来说成员级拷贝就足够了,所以我们就在clone()函数中使用C++自动创建的默认拷贝构造函数来创建新的Cell实例。
[code type="cpp-qt"]
void Cell::setFormula(const QString &formula)
{
setData(Qt::EditRole, formula);
}
[/code]
setFormula()函数设置单元格的公式。它是调用使用编辑角色的setData()的一个简单方便的函数。它在Spreadsheet::setFormula()中被调用。
formula()函数在Spreadsheet::formula()中被调用。像setFormula()一样,它也是一个方便函数,不过这次是获取项的EditRole数据。
[code type="cpp-qt"]
void Cell::setData(int role, const QVariant &value)
{
QTableWidgetItem::setData(role, value);
if (role == Qt::EditRole)
setDirty();
}
[/code]
如果我们有一个新的公式,我们把cacheIsDirty设为true来保证单元格的值在下次调用text()的时候被重新计算。
虽然我们在Spreadsheet::text()中调用Cell实例的text()方法,但其实Cell中并没有定义text()函数。该text()函数是一个由QTableWidgetItem提供的便利函数。它等坐于调用 data(Qt::Display-Role).toString()。
[code type="cpp-qt"]
void Cell::setDirty()
{
cacheIsDirty = true;
}
[/code]
setDirty()函数调用用于强制重新计算单元格的值。它只需简单地把cacheIsDirty 为true,这意味着cacheValue不是最新的了。重算过程直到必要的时候才被执行。

[code type="cpp-qt"]
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);
}
}
[/code]
data()函数是QTableWidgetItem中的函数的重新实现。如果使用Qt::DisplayRole它会返回显示在电子表格中的文本,如果使用Qt::EditRole调用则会返回公式,如果使用Qt::TextAlignmentRole调用则返回一个合适的对齐方式。在DisplayRole情况下,它依靠value()来计算相应单元格的值。如果此值无效(因为公式是错误的)我们就返回“####”。
用在data()中的Cell::value()返回一个QVariant。一个QVariant能存储不同类型的值,如double和QString,并提供了一些函数把它转换为其他类型。例如,调用一个存储了double型值的变量的toString()将会生成一个表示该double值的字符串。使用默认构造函数构造的QVariant是一个无效变量。
[code type="cpp-qt"]
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;
}
[/code]
私有函数value()返回单元格的值。如果cacheIsDirty为true,我们必须重新计算它的值。
如果公式以一个单引号开始(如,”’12345”),单引号占据位置0,而值则是公位置1到最后的字符串。
如果公式以一个等号(‘=’)开始,我们抽取从位置1开始的字符串并删除其中的空格。然后我们调用evalExpression()计算这个表达式的值。Pos参数以引用形式传递。它表示解析开始字符的位置。在完成evalExpression()调用后,如果解析是成功的,则在pos 处的字符应该是我们添加的QChar::null字符。如果解析在结束前失败,我们把cachedValue设为无效的。
如果公式即不以单引号开始,也不以等于号开始,我们试着使用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++的异常来报告失败。但在Qt程序中这并不会阻止你使用他们,假如你的编译器支持他们的话。

Value()函数被声明为const类型。我们必须把cachedValue和cacheIsValid声明为mutable变量,这样编译器才能允许我们在常量型函数中修改他们。可能把value()改为非常量类型并去掉mutable关键字非常诱人,但这样就不能通过编译,因为我们从常量函数data()中调用了value()。
现在我们已经完成了电子表格程序的公式解析部分。本节其余部分包括evalExpression()和两个帮助函数evalTerm() 和 evalFactor()。因为这些代码与GUI编程无关,你可以放心地跳过它并在第5章继续阅读。
evalExpression()函数返回电子表格表达式的值。表达式被定义为一个或者多个由’+’或’+--‘(是不是应该为’-‘)操作符分隔的项。这些项被定义为一个或者多个由’*’或者’/’操作符分隔的因子。通过把表达式分解为项,把项分解为因子,我们保证操作符的正确优先级。
例如,"2*C5+D6"是一个表达式,"2*C5" 是它的第一个项, "D6" 是它的第二个项。项"2*C5"有第一个因子"2"和第二个因子"C5",而项"D6"只由一个因子"D6"组成。因子可以是一个数据("2"),一个坐标("C5"),或者一个括号中的表达式,该表达式前还可放单个减号。
图4.10定义了电子表格的句法。对语法中的每个符号(表达式,项和因子),都有一个相应的成员函数用于解析它们并且这些函数的结构与语法密切相差。写成这样的分析器被叫做向下递归分析器。
图4.10 电子表格的句法图


让我们从分析表达式的函数evalExpression()开始吧:
[code type="cpp-qt"]
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;
}
[/code]
首先,我们调用evalTerm()来获取第一个项的值。如果接下来的字符为’+’或者’+--‘,我们继续调用evalTerm()。否则,该表达式是由单个项组成的,那么我们就返回它的值作为整个表达式的值。在我们有了前两个项的值后,我们计算该操作的结果,这依赖于当前的操作符。如果两个项都是double类型的,我们的计算结果也是double类型的。否则,我们把结果设定为无效的。
我们像这样一直继续下去直到没有更多的项。这工作的很正常,因为加减是左结合的。这就是说"1-2-3" 意味着 "(1-2)-3", 而不是 "1-(2-3)"。
[code type="cpp-qt"]
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;
}
[/code]
evalTerm()函数与evalExpression()非常相似,但它处理的是乘法和除法。在evalTerm()中仅有的一个比较巧妙的是我们要避免除0问题,因为在某些处理器上这会产生一个错误。由于圆整错误,浮点数的相等比较是非常不明智的,但与0.0进行相等比较是来防止除0错误是安全的。
[code type="cpp-qt"]
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(
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;
}
[/code]
evalFactor()函数比evalExpression() 和evalTerm().稍微复杂一点。我们先要注意因子是否为负。然后看一下它是否以一个开括号开始。如果是,我们通过调用evalExpression()把括号中的内容当作一个表达式来计算。在分析一个被括起来的表达式的时候,evalExpression()再调用evalTerm(),它又会调用eval-Factor(),而它又会再调用evalExpression()。这就是分析器中递归指定。

如果因子不是一个嵌套的表达式,我们抽取下一个序列,它应该是个单元格坐标或者是个数字。如果这个序列匹配相应QRegExp,我们把它当作一个单元格引用并以给定的坐标调用它的value()函数。这个单元格可以是电子表格中的任何一个,并且它还可以依赖于其他的单元格。依赖不是问题。他们将会简单的触发更多的value()调用和(对“脏“单元格)更多的分析直到所有的附属单元都被求值。如果该序列不是一个单元格坐标,我们把它当作一个数字。
如果单元格A1包含公式"=A1"会发生什么呢?或者如果单元格A1包含"=A2"并且单元格A2又包含"=A1"又会发生什么呢。尽管我们没有编写任何特别的代码来检测循环依赖,分析器通过返回一个无效的QVariant值优美地处理了这些情况。这能够工作的很好,因为在我们调用evalExpression()之前已经把cacheIsDirty设置为false,并把cachedValue在value()中设置为无效的了。如果evalExpression()递归地对同一个单元格调用value(),它会立即返回无效值,并且整个表达式的值的计算结果也是无效的。
我们现在已经完成了公式分析器。它应该能通过扩展因子的定义讲法来简单地扩展为处理预定义的电子表格程序函数,像sum()" 和"avg()"。另一个简单的扩展方式是实现字符串的'+'操作符(作为连接用)。而这并不需要修改因子定义语法。


原文: http://qtchina.tk/?q=node/125

Powered by zexport