Qt4_子类化QTableWidgetItem

这篇博客介绍了如何创建一个名为Cell的类,该类继承自QTableWidgetItem,用于存储和计算电子表格单元格中的公式。Cell类重写了data()函数以显示计算结果,并实现了公式解析,包括加减乘除运算以及单元格引用。解析器使用递归下降的方法处理表达式,能够处理单元格间的相互依赖。此外,Cell类还利用mutable关键字使得const函数能更新内部状态。
摘要由CSDN通过智能技术生成

子类化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(),另一个简单的扩展也可以把"+"好用字符串式的连接实现,这不需要更改代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阳光开朗男孩

你的鼓励是我最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值