第4章 实现程序的功能(创建QTableWidgetItem子类)

发布: 2008-05-29 23:07

创建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的开始部分:



#include <QtGui>

#include "cell.h"

Cell::Cell()

{

  setDirty();

}

在构造函数中,我们仅需要把缓存设为脏数据。这里不需要传递一个父物件。因为这个单元格被用setItem()插入到QTableWidget中,QTableWidget会自动获得对它的拥有权。

每个QTableWidgetItem 都能保存一些数据,每个数据“角色”为一个QVariant。最常用的角色是Qt::EditRole 和 Qt::DisplayRole。编辑角色表示要被编辑的数据,显示角色表示要被显示的数据。通过他们俩是一样的数据,但是在单元格中,编辑角色对应于单元格的公式,显示角色对应于单元格的值(对公式求值的结果)。

QTableWidgetItem *Cell::clone() const

{

  return new Cell(*this);

}



在QTableWidget要创建一个新的单元格的时候,clone()函数将被调用。例如,当用户向一个从未使用过的空单元格中输入时文本时。传递到QTableWidget::setItemPrototype()的实例是克隆出来的。因为对Cell来说成员级拷贝就足够了,所以我们就在clone()函数中使用C++自动创建的默认拷贝构造函数来创建新的Cell实例。



void Cell::setFormula(const QString &formula)

{

  setData(Qt::EditRole, formula);

}



setFormula()函数设置单元格的公式。它是调用使用编辑角色的setData()的一个简单方便的函数。它在Spreadsheet::setFormula()中被调用。

formula()函数在Spreadsheet::formula()中被调用。像setFormula()一样,它也是一个方便函数,不过这次是获取项的EditRole数据。



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

{

  QTableWidgetItem::setData(role, value);

  if (role == Qt::EditRole)

  setDirty();

}



如果我们有一个新的公式,我们把cacheIsDirty设为true来保证单元格的值在下次调用text()的时候被重新计算。

虽然我们在Spreadsheet::text()中调用Cell实例的text()方法,但其实Cell中并没有定义text()函数。该text()函数是一个由QTableWidgetItem提供的便利函数。它等坐于调用 data(Qt::Display-Role).toString()。



void Cell::setDirty()

{

  cacheIsDirty = true;

}



setDirty()函数调用用于强制重新计算单元格的值。它只需简单地把cacheIsDirty 为true,这意味着cacheValue不是最新的了。重算过程直到必要的时候才被执行。





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中的函数的重新实现。如果使用Qt::DisplayRole它会返回显示在电子表格中的文本,如果使用Qt::EditRole调用则会返回公式,如果使用Qt::TextAlignmentRole调用则返回一个合适的对齐方式。在DisplayRole情况下,它依靠value()来计算相应单元格的值。如果此值无效(因为公式是错误的)我们就返回“####”。

用在data()中的Cell::value()返回一个QVariant。一个QVariant能存储不同类型的值,如double和QString,并提供了一些函数把它转换为其他类型。例如,调用一个存储了double型值的变量的toString()将会生成一个表示该double值的字符串。使用默认构造函数构造的QVariant是一个无效变量。



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()返回单元格的值。如果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()开始吧:



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()。否则,该表达式是由单个项组成的,那么我们就返回它的值作为整个表达式的值。在我们有了前两个项的值后,我们计算该操作的结果,这依赖于当前的操作符。如果两个项都是double类型的,我们的计算结果也是double类型的。否则,我们把结果设定为无效的。

我们像这样一直继续下去直到没有更多的项。这工作的很正常,因为加减是左结合的。这就是说"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()非常相似,但它处理的是乘法和除法。在evalTerm()中仅有的一个比较巧妙的是我们要避免除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() 和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/129

Powered by zexport