子类化QTableWidgetItem

Cell类这个类用作保存单元格的公式,并且它还重新实现了QTableWidgetem: :data()函数,Spreadsheet可以通过QTableWid-getItem::text()间接调用该函数,用它显示单元格公式的计算结果。

Cell类派生自QTableWidgetItem类。这个类被设计用于和Spreadsheet一起工作,但是它对类QTableWidglttem没有任何特殊的依赖关系,所以在理论上讲,它也可以用于任意的QTableWidget类中。

Cell.h

#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型值。

在声明cachedValue和cacheIsDirty变量时使用了C++的mutable关键字,这样就可以在const函数中修改这些变量。或者,在每次调用text()时,本应当重新计算这个值,但是这样做是不必要的,因为它的效率非常低下。

我们注意到,在该类的定义中并没有使用Q_ OBJECT宏。这是因为,Cell是一个普通的C++类,它没有使用任何信号或者槽。实际上,因为QTableWidgetItem不是从Q0bject派生而来的,所以就不能让Cell拥有信号和槽。为了使Qt的项(item)类的开销降到最低,它们就不是从QObject 派生的。如果需要信号和槽,可以在包含项的窗口部件中实现它们,或者在特殊情况下,可以通过对Q0bject进行多重继承的方式来实现它们。

Cell.cpp

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

在构造函数中,只需要将缓存设置为dirty。没有必要传递父对象,当用setItem()把单元格插入到一个QTableWidget中的时候,QTableWidget将会自动对其拥有所有权。

每个QTableWidgelItem都可以保存一些数据,最多可以为每个数据"角色"分配一个QVariant变量。最常用的角色是Qt::EditRole和Qt::DisplayRole。编辑角色用在那些需要编辑的数据上,而显示角色用在那些需要显示的数据上。

通常情况下,用于两者的数据是一样的,但在Cell类中,编辑角色对应于单元格的公式,而显示角色对应于单元格的值(对公式求值后的结果)。

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

当QTableWidget需要创建一个新的单元格时,例如,当用户在一个以前没有使用过的空白单元格中开始输人数据时,它就会调用clone()函数。传递给QTableWidget::settemPrototype()中的实例就是需要克隆的项。由于对于Cell 来讲,成员级的复制已经足以满足需要,所以在clone()函数中,只需依靠由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()一样,它也是一个简便函数,这次是重新获得该项的EditRole数据。

void Cell::setData(int role, const QVariant &value)
{QTableWidgetItem::setData(role, value);if (role == Qt::EditRole)setDirty();
}

如果有一个新的公式,就可以把cacheIsDitrty设置为true,以确保在下一次调用text()的时候可以重新计算该单元格。

尽管对Cell实例中的Spreadsheet::text()调用了text(),但在Cell中没有定义text()函数。这个text()函数是一个由QTableWidgetItem 提供的简便函数。这相当于调用data(Qt::DisplayRole).toString()。

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

调用setDirty()函数可以用来对该单元格的值强制进行重新计算。它只是简单地把cacheIsDirty设置为true,也就意味着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()函数是从QTableWidgetItemn中重新实现的。如果使用Qt::DisplayRole调用这个函数,那么它返回在电子制表软件中应该显示的文本;如果使用Qt: EditRole调用这个函数,那么它返回该单元格中的公式;如果使用Qt::TextAlignmentRole调用这个函数,那么它返回一个合适的对齐方式。在使用DisplayRale的情况下,它依靠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是通过引用(reference)方式传递的,由它来说明需要从哪里开始解析字符的位置。在调用evalExpression()之后,如果表达式解析成功,那么在位置pos处的字符应当是我们添加上的QChar::Null字符。如果在表达式结束之前解析失败了,那么可以把cachedValue设置为Invalid。

如果公式不是由单引号或者等号开始的,那么可以使用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++异常(exception)机制来报告错误。但是,如果你的编译器支持C++异常,那么这也不会妨碍你在自己的Qt程序中使用它们。

value()函数声明为const函数。我们不得不把cachedValue和cacheIsValid声明为mutable变量,以便编译器可以让我们在const函数中修政它们。当然,如果能够把value()声明为一个非const函数并且移除mutable关键字可能会更吸引人些,但是这将会导致无法编译,因为是从一个const函数的data()函数中调用value()的。

除了要解析这些公式外,现在已经完成了整个Spreadsheet应用程序。

evalExpression()函数返回一个电子制表软件表达式的值。表达式可以定义为:一个或者多个通过许多"+“或者”-“操作符分隔而成的项。这些项自身可以定义为:由”*“或者”/"操作符分隔而成的一个或者多个因子(factor)。通过把表达式分解成项,再把项分解成因子,就可以确保以正确的顺序来使用这些操作符了。

例如,"2xC5+D6"就是一个表达式,它由作为第一项的"2xC5"和作为第二项的"D6"构成。项"2*C5"是由作为第一个因子的"2"和作为第二个因子的"C5"组成的,而项"D6"则由一个单一的因子"D6"组成。一个因子可以是一个数(“2”)、一个单元格位置(“C5”),或者是一个在圆括号内的表达式,在它们的前面可以有负号。

如图,定义了电子制表软件表达式的语法。对于语法(表达式、顶和因子)中的每一个符号,都对应一个解析它的成员函数,并且函数的结构严格遵循语法。通过这种方式写出的解析器称为递归渐降解析器。

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值;否则,把结果设置为Invalid。

像前面那样继续操作,直到再没有更多的项为止。这样做可以正确地进行,因为加法和减法都是左相关(left-associative)的;也就是说,“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;
}

除了evalTemm()函数是处理乘法和除法这一点不同之外,它和evalExpression()都很相似。在evalTerm()中唯一的不同就是必须要避免除零,因为在一些处理器中这将是一个错误。尽管测试浮点数值是否相等通常并不明智,因为其中存在取舍问题,但是在这个防止除零的问题上,这样做相等性测试已经足够了。

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(),evalTerm()调用evalFactor(),evalFactor()则会再次调用evalExpression()。这就是在解析器中出现递归调用的地方。

如果该因子不是一个嵌套表达式,就提取下一个记号,它应当是一个单元格的位置,或者也可能是一个数字。如果这个记号匹配QRegExp,就把它认为是一个单元格引用并且对给定位置处的单元格调用value()。该单元格可能在电子制表软件中的任何一个地方,并且它可能会依赖于其他的单元格。这种依赖不是什么问题,它们只会简单地触发更多的value()调用和(对于那些"dirty"单元格)更多的解析处理,直到所有相关的单元格的值都得到计算为止。如果记号不是一个单元格的位置,那么就把它看作是一个数字。

如果单元格A1包含公式"=A1"时会发生什么呢?或者如果单元格A1包含公式"=A2"并且单元格A2包含公式"=A1"时又会发生什么呢?尽管还没有编写任何特定代码来检测这种循环依赖关系,但解析器可以通过返回一个无效的QVariant来完美地处理这一情况。之所以可以正常工作,是因为在调用evalExpression()之前,我们会在value()中把cacheIsDirty设置为false,把cachedValue设置为Invalid。如果evalExpression()对同一个单元格循环调用value(),它就会立即返回Invalid,并且这样就会使整个表达式等于Invalid。

我们就这样完成了公式的解析。也可以增加对因数的类型的定义,直接对它进行扩展处理表格预定义的函数,如 sum(),avg(),另一个简单的扩展也可以把"+"好用字符串式的连接实现,这不需要更改代码。

Qt4_子类化QTableWidgetItem相关推荐

  1. Qt4_子类化QTableWidget

    子类化QTableWidget 类Spreadsheet派生自QTableWidget,如图所示.QTableWidget是一组格子,可以非常有效地用来表达二维稀疏数组.它可以在规定的维数内显示用户滚 ...

  2. Qt4_子类化QMainWindow

    子类化QMainWindow 通过子类化QMainWindow,可以创建一个应用程序的主窗口. #ifndef MAINWINDOW_H #define MAINWINDOW_H#include &l ...

  3. 关于如何换肤、子类化的解决方案

    对于应用程序的换肤及子类化.下面是我尝试过一些方法,以在CAboutDlg中子类化其中的Button为例: 第一种:直接用现成的类 1.自己写一个类class CButtonXP : public C ...

  4. 实现不可变类如何禁止子类化?

    实现不可变类时要求禁止子类化.本文先讲禁止子类化的方式,最后解释为什么要禁止子类化. 如何禁止子类化 常用姿势 最简单的手段是将类声明为final,如String.Integer等常用的值类.但这样缺 ...

  5. 窗口子类化-实例应用

    所谓窗口子类化:改变一个已经存在的窗口实例的性质:消息处理与其他实例属性.   通常在SDK中所谓的窗口子类化就是改变一个窗口函数(如GetWindowLong()和SetWindowLong())通 ...

  6. 动态子类化CComboBox以得到子控件EDIT及LISTBOX

    动态子类化CComboBox以得到子控件EDIT及LISTBOX Joise.LI写于2004-4-6 ComboBox是比较常用的一个控件,有三种样式:CBS_SIMPLE(简单),CBS_DROP ...

  7. Windows C/C++编程窗口子类化学习

    也许你需要一个特殊的Edit来限制浮点数的输入,但是现有的Edit却并不能完成这项工作,因为它只能够单纯的限制大小写或者纯数字.当你在论坛上求救的时候,某个网友告诉你:"用子类化." ...

  8. Wilcox:子类化顶级窗口

    Wilcox:子类化顶级窗口 Wilcox:子类化顶级窗口 编码 说明 OurFrame() vs OurFrame(wxWindow* parent, ...) 管理控件 ID Wilcox:子类化 ...

  9. wxWidgets:子类化Subclassing WxControl

    wxWidgets:子类化Subclassing WxControl wxWidgets:子类化Subclassing WxControl wxWidgets:子类化Subclassing WxCon ...

最新文章

  1. Check failed: registry.count(type) == 1 (0 vs. 1) Unknown layer type: Python
  2. 积极开展网络营销的AI换脸软件短短几日经历了从爆红到下架
  3. 学习:多项式算法----FFT
  4. python语言画心_python语言还是java如何用python画爱心
  5. Oracle 索引相关
  6. 罗永浩宣布进军电商直播;微博回应用户数据泄露;Android 11 开发者预览版 2 发布 | 极客头条...
  7. Java经典编程题50道之四十二
  8. Leetcode91. Decode Ways
  9. python123词频统计之哈姆雷特_基于Yarn的Spark环境,统计哈姆雷特词频(1)-阿里云开发者社区...
  10. python中__future__包
  11. 华为盒子 原生android,手把手教你刷机把华为悦盒刷机为安卓网络机顶
  12. ANSYS Workbench网格划分具体数值方法
  13. 聊一聊云电脑、云游戏以及阿里云的“无影”
  14. msyql创建数据库并指定字符集
  15. 【混合编程】c++中调用python脚本
  16. 饿了么4年 + 阿里2年:研发路上的一些总结与思考
  17. matlab ols regress,计量经济学简单线性回归OLS的Matlab程序.pdf
  18. win10代理服务器没有响应解决方案
  19. echart 折线图设置y轴单位_Echarts自定义Y轴
  20. 注册表操作C/C++(实战实现程序自启动)

热门文章

  1. 联想小新一键恢复小孔_联想机海真香预警!多款轻薄本平板来袭 学生/办公别错过...
  2. aop springboot 传入参数_springboot用aop做参数校验
  3. vue 圆形 水波_vue 开发波纹点击特效组件
  4. mysql double 转 字符串_没想到!在MySQL数据库中的数据有这三种类型!
  5. jQuery - 获取内容和属性
  6. linux下各目录的作用
  7. 一文看懂Stacking!(含Python代码)
  8. cocos2dx-lua 笔记 方向控制 v2
  9. JAVA File转Byte[]
  10. li中浮动元素span等在IE和Firefox中的高度Bug