类Cell继承自QTableWidgetItem。这个类在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的形式保存单元格的值,这个值可能是double型,也可能是QString类型。
cacheIsDirty:如果保存的值不需要更新则置为true。
变量
catchedValue和
cacheIsDirty前声明了C++的关键字
mutable。这可以允许我们在常函数中修改这个变量值。我们也可以在每次调用text()时重新计算变量的值,但是这样毫无疑问效率会很差。
注意,类定义里面没有声明Q_OBJECT宏。Cell是一个纯粹的C++类,没有信号和槽。事实上,QTableWidgetItem也是一个纯粹C++类,而不是从QObject继承来的,因此类Cell中不能有信号槽。为了保证最小的代价和高效,Qt所有item类都不是从QObject继承的。如果需要信号和槽,可以在使用item类的控件中定义,或者使用带QObject类的多重继承。
下面是源文件:
#include
<
QtGui
>
#include
"
cell.h
"
Cell::Cell()
{
setDirty();
}
QTableWidgetItem
*
Cell::clone()
const
{
return
new
Cell(
*
this
);
}
void
Cell::setFormula(
const
QString
&
formula)
{
setData(Qt::EditRole, formula);
}
QString Cell::formula()
const
{
return
data(Qt::EditRole).toString();
}
void
Cell::setData(
int
role,
const
QVariant
&
value)
{
QTableWidgetItem::setData(role, value);
if
(role
==
Qt::EditRole)
setDirty();
}
void
Cell::setDirty()
{
cacheIsDirty
=
true
;
}
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);
}
}
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;
}
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;
}
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;
}
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;
}
在构造函数中,我们只是把存贮器设为“脏”。这里不需要传递一个父参数。因为用QTableWidget::setItem()往QTableWidget中插入Cell对象时,QTableWidget自动得到它的所有权。
每个QTableWidgetItem都保存一些数据,相当于每一个QVariant都以一种“角色”保存一类数据。最常用的角色是
Qt::EditRole和
Qt::DisplayRole。Qt::EditRole用于需要编辑的数据,Qt::DisplayRole用于需要显示的数据。通常这两个角色中的数据都是一样的。但是在Cell中,Qt::EditRole表示要编辑的公式,Qt::DisplayRole表示单元格要显示的值(公式计算的结果)。
当需要创建一个新的单元格时,QTableWidget调用函数clone(),例如,当用户在一个空的单元格(该单元格以前未被使用过)中开始输入数据时,传递给QTableWidget::setItemPrototype()的实例就是由clone()得到的项目。我们使用了C++自动创建的Cell的默认拷贝构造函数来创建新的Cell对象,这对于成员级别的拷贝已经足够了。
函数setFormula()设置单元格的公式。对于调用带Qt::EditRole的setData()函数它是个很方便的函数,在Spreadsheet::setFormula()函数中调用了该Cell::setFormula()函数。
在Spreadsheet::formula()中调用了函数Cell::formula()。和setFormula()一样,它也是一个方便函数,得到单元格的Qt::EditRole数据。
修改单元格的数据setData()时,如果输了一个新的公式,那么将
cacheIsDirty设置为
true,以便在下一次调用text()时重新计算显示值。
尽管在Spreadsheet::text()中在Cell对象上调用了
text(),但在Cell中没有定义text()。text()函数是
QTableWidgetItem提供的一个方便函数,等价于
data(Qt::DisplayRole).toString() 。
setDirty()用来强制计算单元格的值,它只是将cacheIsDirty为true,说明cachedValue中的值不再更新,需要时才重新计算。
函数data()重新进行了实现。如果用Qt::DisplayRole调用,返回显示的文本。如果用Qt::EditRole调用则返回公式。如果用Qt::TextAlignmentRole调用,返回给你合适的对齐方式。在Qt::DisplayRole方式中,调用value()得到计算的单元格值。如果值无效,则显示字符串####。
在data()中使用的Cell::value()函数返回一个QVariant类型值。一个QVariant类型可以保存多种类型的数据,并且提供不同数据类型之间的转换。例如,在一个保存double型的变量中调用toString()则得到double的字符串表示。QVariant用一个“invalid”数据进行默认的初始化。
私有函数value()返回单元格的显示值。如果cacheIsDirty为true,则需要重新计算。
如果公式用单引号开头,如“'12344”,单引号占用位置0,单元格值为从位置1开始到最后的字符串。
如果公式由等号“=”开头,得到从位置1开始等号后面的字符串并删除其中所有的空格,然后调用evalExpression()计算表达式的值。用引用传递参数pos。它表示表达式开始分解的字符串位置。调用evalExpression()后,如果表达式解析成功,
pos的值应为我们附加的QChar::Null,否则失败,置
cachedValue为
Invalid。
如果公式不是由单引号或者等号开头,首先试着用
toDouble()把公式转换为浮点数,如果转换成功,置
cachedValue为得到的浮点数。否则置
cachedValue为公式字符串。例如,公式为"1.50",调用
toDouble()后转换成功,得到值1.5,如果公式为"World Population",则转换失败,返回"0.0"。
给toDouble()一个bool型的指针。我们能够区分字符串的转换返回值为数字0.0还是转换不成功的标志。如果转换失败,返回值为0.0,但是bool值为false。在我们不需要传递bool指针的时候,转换失败返回0.0值还是有必要的。为了性能和可移植性,Qt不使用C++异常来报告失败的情况。但这不影响你在Qt中使用它们,只要你的编译器支持就可以。
我们声明value()为常函数,为了编译器允许在value()中改变cachedValue和cacheIsValid的值,我们不得不把这两个变量声明为mutable。如果把value()改为非常函数,那么metable关键字就可以去掉,但是因为我们在data()常函数中调用的value(),这样编译不会成功。
到目前为止我们已经完成了大部分Spreadsheet程序,还有一部分就是公式的解析。下面的部分介绍evalExpression()和两个有用的函数evalTerm()和evalFactor()。代码有些复杂,为了程序的完整性把它们包含了进来。这些代码和GUI编程没有任何关系,因此你也可以跳过直接阅读第五章。
函数evalExpression()返回表格表达式的值。一个表达式由一个或者多个项组成,这些项之间由“+”或者“+--”符号隔开。每一算式项由一个或者多个因子“*”或者“/”隔开。把表达式分解为加减法项,把加减法解析为乘除法因子,这样我们就能确保表达式按照正确的优先级进行计算
例如表达式“2*C5+D6
”,“
2*C5”是第一项,其中2是第一个因子,C5是第二个因子。“
D6”是第二项,只有一个因子。一个因子可以是一个数字,也可以是一个单元格的位置,有时候前面还有一个一元减号。
图4.10定义了表格表达式的语法结构。对于语义中的每一个符号(表达式,算式项,因子项),都有一个相应的成员函数进行解析,算式结构严格遵照语法。这种解析方式被称为递归解析。
图4.10 syntax diagram for spreadsheet expressions
先从evalExpression()开始。这个函数解析一个表达式。
首先,调用evalTerm()得到第一个算式项的值。如果接下来的符号为“+”或者“+――”那么继续调用evalTerm()。否则这个表达式就是由一个算式项组成的,它的值就是表达式的值。当得到前两个算式项的值后,我们根据操作符计算这两个算式的结果。如果两个算式都是double型的,结果也为double型的。否则结果为Invalid。继续计算直到表达式中没有算式项为止。因为加法和减法都是左结合的,所以计算的结果是正确的。
函数evalTerm()函数和evalExpression()很像,只是它处理的是乘除法。还有一个不同地方就是必须避免除数为0的情况,在很多处理器上都是错误的算式。由于四舍五入的误差,一般不用判断浮点数的值,测试是否等于0.0就可以了。
evalFactor()有些复杂。首先我们判断因子前面是否有负号,然后判断是否有左括号,如果发现括号,那么调用evalExpression()就把括号内的内容作为一个表达式。在解析括号内的表达式时,evalExpression()调用evalTerm(),再调用evalFactor(),它再调用evalExpression()。这就是解析的递归部分。
如果因子不是一个内嵌的表达式,我们就得到它的下一个语法符号,它可能是一个单元格的位置或者是一个数字。如果符号匹配QRegExp,则把它作为一个单元格的引用,在指定位置的单元格上调用value(),得到这个单元格的值。这个单元格应该在表格的任何地方,它的值如果依赖其他的单元格,会触发更多的对value()的调用,对所有依赖的单元格都解析。如果因子不是单元格位置,那么把它看作一个数字。
如果A1的公式为“=A1”,或者A1 的公式为“=A2”,A2的公式为“=A1”时该怎么办?虽然我们没有代码检测这些循环依赖关系,解析器也可以返回一个无效的QVariant值,因为在调用evalExpression()之前,我们以把cacheIsDirty置为false,cachedValue为Invalid。如果在同一个单元格上evalExpression()不停的value(),它会返回Invalid,整个表达式的值为Invalid。
我们就这样完成了公式的解析。也可以增加对因子的类型的定义,直接对它进行扩展处理表格预定义的函数,如sum(),avg(),另一个简单的扩展也可以把“+”好用字符串式的连接实现,这不需要更改代码。