继承类 QTableWidgetItem(Subclassing QTableWidgetItem)
类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 继承来的。为了保证最小的代价和高效,Qt 所有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()插入Cell 对象时,QTableWidget 自动得到它的所有权。
在QTableWidgetItem 中,每一个QVariant 都以一种“角色”保存一类数据。最常用的角色是 Qt::EditRole 和Qt::DisplayRole。Qt::EditRole 表示用来编辑网格中的数据, Qt::DisplayRole 只是显示数据。通常这两个角色中的数据都是一样的。但是在 Cell 中, Qt::EditRole 表示要编辑的公式,Qt::DisplayRole 表示网格要显示的值(公式计算的结果)。当需要一个新的网格时,QTableWidget 调用函数 clone(),例如,当用户在一个空的网格中输入数据。传递给 QTableWidget::setItemPrototype()就是由clone()得到的项目。我们使用了C++自动创建的Cell 的默认拷贝构造函数,这对于成员级别的拷贝已经足够了。
函数setFormula()设置网格的公式。它为调用Qt::EditRole 的setData()函数提供了方便。在Spreadsheet::setFormula()函数中调用了 setFormula()函数。在Spreadsheet::formula()中调用了函数Cell::formula()。和setFormula()一样,它也是一个方便函数,得到网格项的Qt::EditRole 数据。修改网格的数据setData()时,如果输了一个新的公式,那么将 cacheIsDirty 设置为true,以便在下一此调用text()时重新计算显示值。
尽管在Spreadsheet::text()中用了Cell,但在Cell 中没有定义text()。text()函数是QTableWidgetItem 提供的一个方便函数,等价于data(Qt::DisplayRole).toString()。 setDirty()用来强制计算网格的值,它只是将cacheIsDirty 为true,说明cachedValue 中的值需要更新,需要时要重新计算。
函数 data()重新进行了实现。如果用 Qt::DisplayRole 调用,返回显示的文本。如果用 Qt::EditRole 调用则返回公式。如果用Qt::TextAlignmentRole 调用,返回给你合适的对其方式。在 Qt::isplayRole 方式中,调用 value()得到计算的网格值。如果值无效,则显示字符串####。
在 data()中使用的 Cell::value()函数返回一个 QVariant 类型值。一个 QVariant 类型可以保存多种类型的数据,并且提供不同数据类型之间的转换。例如,在一个保存double 型的变量中调运女冠toString()则得到double 的字符串表示。QVariant 用一个“invalid”数据进行默认的初始化。
函数value()返回网格的显示值。如果 cacheIsDirty 为true,则需要重新计算。如果公式用单引号开头,如“’12344”,网格值为单引号后面的文本。如果公式由等号“=”开头,得到等号后面的字符串并删除其中所有的空格然后调用 evalExpression()计算表达式的值。参数 pos 是传引用。它表示表达式开始分解的字符串位置 。调用evalExpression()后,如果表达式解析成功,pos 的值应为我们附加的QChar::Null,否则失败,置cachedValue 为Invalid。如果公式不是由单引号或者等号开头,首先试着把公式转换为浮点数,如果转换成功,返回值就是得到的浮点数。否则直接返回公式。
给toDouble()一个bool 型的指针。我们能够区分返回值为 0.0 时是成功与否。如果转换失败,返回值为 0.0,但是bool 值为false。在我们不需要考虑转换成功与否的时候,返回 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”是第二项,只有一个因数。一个因数可以是一个数字,也可以是一个网格的位置,有时候前面还有一个一元减号。
表格表达式的语法结构。对于语义中的每一个符号(表达式,算式项,因数项),都有一个相应的成员函数进行解析,算式结构严格遵照语法。这种解析方式被称为递归解析。
先从evalExpression()开始。这个函数解析一个表达式。首先,调用evalTerm()得到第一个算式的值。如果接下来的符号为“+”或者“+――”那么继续调用evalTerm()。否则这个表达式就是由一个算式组成的,它的值就是表达式的值。当得到前两个算式的值后,我们根据操作符计算这两个算式的结果。如果两个算式都是 double 型的,结果也为double 型的。否则结果为Invalid。继续计算直到表达式中没有算式为止。因为加法和减法都是左结合的,所以计算的结果是正确的。函数evalTerm()函数和evalExpression()很像,只是它处理的是乘除法。还有一个不同地方就是必须避免除数为 0 的情况,在很多处理器上都是错误的算式。由于四舍五入的误差,一般
不用判断浮点数的值,测试是否等于 0.0 就可以了。evalFactor()有些复杂。首先我们判断因数前面是否有负号,然后判断是否有左括号,如果发现括号,那么就把括号内的内容作为一个表达式,调用evalExpression()。在解析括号内的表达式时,evalExpression()调用 evalTerm(),再调用 evalFactor()。这就是解析的递归部分 。如果因数不是一个内嵌的表达式,我们就得到它的下一个语法符号,它可能是一个网格的位置或者是一个数字。如果符号匹配QRegExp,则意味这它是一个网格位置,得到这个网格的值。这个网格应该在表格的某一个地方,它的值如果依赖其他的网格,会触发更多的对 value()的调用 ,对所有依赖的网格都解析。如果因数不是网格,那么把它看作一个数字。如果A1 的公式为“=A1”,或者A1 的公式为“=A2”,A2 的公式为“=A1”时该怎么办那?虽然我们没有代码检测这些圆形依赖关系,解析器也可以返回一个无效的 QVariant 值,因为在调用evalExpression()之前,我们以把cacheIsDirty 置为false,cachedValue 为Invalid。 如果evalExpression()不停的调用某一个网格的 value(),它会返回Invalid,整个表达式的值为Invalid。
我们就这样完成了公式的解析。也可以增加对因数的类型的定义,直接对它进行扩展处理表格预定义的函数,如sum(),avg(),另一个简单的扩展也可以把“+”好用字符串式的连接实现,这不需要更改代码。