【学习笔记】C++ GUI Qt4 第四章 4.5 实现其他菜单 和 4.6 子类化QTableWidgetItem
程序员文章站
2022-05-22 19:55:04
...
4.5 实现其他菜单
void Spreadsheet::recalculate()
{
/* recalculate()槽能够对Tools->Recalculate菜单选项做出响应。
* 当必要时,它也会被Spreadsheet自动调用。
* 我们遍历每一个单元格,并且对每一个单元格调用setDirty()把它们标记为需要重新计算。
* 为了在电子制表软件中显示一个Cell对象的值,QTableWidget会再次对该对象调用text()以获得其值,从而使该值重新计算一次。
* 然后,对这个视口调用update()来重新绘制整个电子制表软件。
* QTableWidget 中的重绘代码就又会对每一个可见单元格调用text()来获得它们中要显示的值。
* 因为在每一个单元格上都调用了setDirty() ,所以这些对text()的调用将会使用重新计算过的值。
* 该计算可能需要重新计算那些不可见的单元格,这就会造成一个级联计算,直到每一个需要被重新计算的单元格能够在刚才刷新过的视口中重新得到计算,从而使它们也能够显示正确的文本。
* 这一计算是由Cell类执行的。
*/
for (int row = 0; row < RowCount; ++row) {
for (int column = 0; column < ColumnCount; ++column) {
if (cell(row, column))
cell(row, column)->setDirty();
}
}
viewport()->update();
}
void Spreadsheet::setAutoRecalculate(bool recalc)
{
/* setAutoRecalculate()槽对Options->Auto-Recalculate菜单选项做出响应。
* 如果启用了这个特性,则会立即重新计算整个电子制表软件以确保它是最新的,
* 然后,recalculate()会自动在somethingChanged()中得到调用。
* 因为QTableWidget已经提供了一个从QTableView 中继承而来的setShowGrid()槽,所以不需要再对Options->Show Grid菜单选项编写任何代码。
* 所有要保留的东西就是Spreadsheet:sort(),它会以在MainWindow::sort()中得到调用:
*/
autoRecalc = recalc;
if (autoRecalc)
recalculate();
}
void Spreadsheet::sort(const SpreadsheetCompare &compare)
{
/* 排序操作会对当前的选择进行,并且会根据存储在compare对象中的排序键和排序顺序重新排列这些行。
* 我们使用一个QStringList来重新表示每一行数据,并且把该选择存储在一个行列表中。
* 我们使用Qt的qStableSort()算法,并且根据公式而不是根据值来进行简单排序。
* 这一过程如下图所示。第11章中会讲述Qt的标准算法和数据结构。
* qStableSort()函数可以接受一个开始迭代器、一个终止迭代器和一个比较函数。
* 这个比较函数是一个带两个参数(两个QStringList)的函数,并且如果第一个参数“小于”第二个参数,它就返回true,否则返回falsle。
* 传递的作为比较函数的这个compare对象并不是一个真正的函数,但是它可以用作一个函数,将会很快看到这一点。
* 在执行完qStableSort()之后,我们把数据移回到这个表中,接着清空这一选择,并且调用somethingChanged()函数。
*/
QList<QStringList> rows;
QTableWidgetSelectionRange range = selectedRange();
int i;
for (i = 0; i < range.rowCount(); ++i) {
QStringList row;
for (int j = 0; j < range.columnCount(); ++j)
row.append(formula(range.topRow() + i,
range.leftColumn() + j));
rows.append(row);
}
qStableSort(rows.begin(), rows.end(), compare);
for (i = 0; i < range.rowCount(); ++i) {
for (int j = 0; j < range.columnCount(); ++j)
setFormula(range.topRow() + i, range.leftColumn() + j,
rows[i][j]);
}
clearSelection();
somethingChanged();
}
//spreadsheet.h
class SpreadsheetCompare
{
public:
bool operator()(const QStringList &row1,
const QStringList &row2) const;
enum { KeyCount = 3 };
int keys[KeyCount];
bool ascending[KeyCount];
//在这个头文件的最后,给出了SpreadsheetCompare类的定义。
//当查看Spreaseet:sort()时,会解释这个类。
/* SpreadsheetCompare类有些特殊,因为它实现了一个“( )"操作符。
* 这样就允许把这个类像函数一样使用。
* 把这样的类称为函数对象(function object) ,或者称为仿函数(functor)。
* 为了理解仿函数是如何工作的,首先从一个简单的例子开始:
*
*/
};
/*QStringList row1, row2;
* SpreadsheetCompare compare;
* ...
* if (compare(row1, row2)) {
* // row1 is less than row2
* }
*
* 使用compare对象就像使用一个普通的compare()函数一样。另外,它的实现可以访问所有存储为成员变量的排序键和排序顺序。
* 与此方案相同的另一种方法是,把这些排序键和排序顺序存储在全局成员变量中,并且使用一个普通的compare()函数。
* 然而,在全局成员变量之间通信是一种并不提倡的做法,并且可能会产生一些莫名其妙的问题。
* 作为像qStableSot()这样的模板函数的接口,仿函数是一种更为常用的做法。
*/
/* 为了理解仿函数是如何工作的,首先从一个简单的例子开始:
* class Square :
* {
* pubtic:
* int operator()(int x) const { return x* x; }
* }
* Square类提供了一个函数, operator()(int)函数,它返回其参数的平方值。
* 通过把这个函数命名为operator( )(int),而不是将其命名为compute(int)之类的函数,就可以把一个类型Square的对象当作一个函数。
*
*Square square;
*int y = square(5);
*y equals 25
*/
//spreadsheet.cpp
bool SpreadsheetCompare::operator()(const QStringList &row1,
const QStringList &row2) const
{
/* 如果第1行小于第2行,该仿函数就返回true; 否则,就返回false。
* qStableSort()函数会使用这个函数的结果来执行排序操作。
* SpreadsheetCompare对象的key与ascending数组和MainWindow::sort()函数(已经在第2章中给出过)一起配合使用。
* 每个键都保存一个列索引,或者- 1(为“None”时)。
* 我们按键顺序比较两行中相应的单元格条目。一旦发现有不同之处,就返回一个适当的true或者false值。
* 如果所有的比较关系都证明两者是相等的,就返回false。
* qStableSort()函数会使用这里给出的顺序来解决这种平局情形。如
* 果一开始的时候row1在row2之前,并且它们都不“小于”对方,那么,在结果中rowl还在row2前面。
* 这就是qStableSort()与它很相似的非稳定版本的qSort()函数之间的区别。
*/
for (int i = 0; i < KeyCount; ++i) {
int column = keys[i];
if (column != -1) {
if (row1[column] != row2[column]) {
if (ascending[i]) {
return row1[column] < row2[column];
} else {
return row1[column] > row2[column];
}
}
}
}
return false;
}
现在已经完成了这个Spreadsheet类。在下一节中,将分析Cell类的代码。这个类用作保存单元格的公式,并且它还重新实现了QTableWidgetem: :data()函数,Spreadsheet可以通过QTableWid-getItem::text()间接调用该函数,用它显示单元格公式的计算结果。
4.6 子类化QTableWidgetItem
Cell类派生自QTableWidgetItem类。这个类被设计用于和Spreadsheet一起工作,但是它对类QTableWidglttem没有任何特殊的依赖关系,所以在理论上讲,它也可以用于任意的QTableWidget类中。这里给出的是Cell类的头文件:
//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;
/* 通过增加两个私有变量,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进行多重继承的方式来实现它们。
*
*/
};
#endif
//cell.cpp
#include <QtWidgets>
#include "cell.h"
Cell::Cell()
{
/* 在构造函数中,只需要将缓存设置为dirty。
* 没有必要传递父对象,当用setItem()把单元格插入到一个QTableWidget中的时候,QTableWidget将会自动对其拥有所有权。
* 每个QTableWidgelItem都可以保存一些数据,最多可以为每个数据“角色"分配一个QVariant变量。
* 最常用的角色是Qt::EditRole和Qt::DisplayRole。
* 编辑角色用在那些需要编辑的数据上,而显示角色用在那些需要显示的数据上。
* 通常情况下,用于两者的数据是一样的,但在Cell类中,编辑角色对应于单元格的公式,而显示角色对应于单元格的值(对公式求值后的结果)。
*/
setDirty();
}
QTableWidgetItem *Cell::clone() const
{
/* 当QTableWidget需要创建一个新的单元格时,例如,当用户在一个以前没有使用过的空白单元格中开始输人数据时,它就会调用clone()函数。
* 传递给QTableWidget::settemPrototype()中的实例就是需要克隆的项。
* 由于对于Cell 来讲,成员级的复制已经足以满足需要,所以在clone()函数中,只需依靠由C++自动创建的默认复制构造函数就可以创建新的Cell 实例了。
*/
return new Cell(*this);
}
void Cell::setFormula(const QString &formula)
{
//setFormula()函数用来设置单元格中的公式。它只是一个对编辑角色调用setData()的简便函数。
//也可以从Spreadsheet: :setFormula()中调用它。
setData(Qt::EditRole, formula);
}
QString Cell::formula() const
{
//formula()函数会从Spreadsheet::formula()中得到调用。
//就像setFormula()一样,它也是一个简便函数,这次是重新获得该项的EditRole数据。
return data(Qt::EditRole).toString();
}
void Cell::setData(int role, const QVariant &value)
{
/* 如果有一个新的公式,就可以把cacheIsDitrty设置为true,以确保在下一次调用text()的时候可以重新计算该单元格。
* 尽管对Cell实例中的Spreadsheet::text()调用了text() ,但在Cell中没有定义text()函数。
* 这个text()函数是一个由QTableWidgetItem 提供的简便函数。这相当于调用data(Qt::DisplayRole).toString()。
*/
QTableWidgetItem::setData(role, value);
if (role == Qt::EditRole)
setDirty();
}
void Cell::setDirty()
{
//调用setDirty()函数可以用来对该单元格的值强制进行重新计算。
//它只是简单地把cacheIsDirty设置为true, 也就意味着cachedValue不再是最新值了。
//除非有必要,否则不会执行这个重新计算操作。
cacheIsDirty = true;
}
QVariant Cell::data(int role) const
{
/* data()函数是从QTableWidgetItemn中重新实现的。
* 如果使用Qt::DisplayRole调用这个函数,那么它返回在电子制表软件中应该显示的文本;
* 如果使用Qt: EditRole调用这个函数,那么它返回该单元格中的公式;
* 如果使用Qt::TextAlignmentRole调用这个函数,那么它返回一个合适的对齐方式。
* 在使用DisplayRale的情况下,它依靠value()来计算单元格的值。如果该值是无效的(由于这个公式是错误的),则返回“####”。
* 在data()中使用的这个Cell::value()函数可以返回一个QVariant值。QVariant可以存储不同类型的值,比如double和QString,并且提供了把变量转换为其他类型变量的一些函数。
* 例如,对一,个保存了double值的变量调用toString(),可以产生一个表示这个double值的字符串。
* 使用默认构造函数构造的QVariant是一个“无效”变量。
*/
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
{
/* value()私有函数返回这个单元格的值。如果cacheIsDirty是true,就需要重新计算这个值。
* value()函数声明为const函数。我们不得不把cachedValue和cacheIsValid声明为mutable变量,以便编译器可以让我们在const函数中修政它们。
* 当然,如果能够把value()声明为一个非const函数并且移除mutable关键字可能会更吸引人些,
* 但是这将会导致无法编译,因为是从一个const函数的data()函数中调用value()的。
*/
if (cacheIsDirty) {
cacheIsDirty = false;
QString formulaStr = formula();
//如果公式是由单引号开始的(例如,“'12345"), 那么这个单引号就会占用位置0,而值就是从位置1直到最后位置的一个字符串。
if (formulaStr.startsWith('\'')) {
cachedValue = formulaStr.mid(1);
} else if (formulaStr.startsWith('=')) {
/* 如果公式是由等号开始的,那么会使用从位置1开始的字符串,并且将它可能包含的任意空格全部移除。
* 然后,调用evalExpression()来计算这个表达式的值。
* 这里的参数pos是通过引用(reference)方式传递的,由它来说明需要从哪里开始解析字符的位置。
* 在调用evalExpression()之后,如果表达式解析成功,那么在位置pos处的字符应当是我们添加上的QChar::Null字符。
* 如果在表达式结束之前解析失败了,那么可以把cachedValue设置为Invalid。
*/
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 {
/* 如果公式不是由单引号或者等号开始的,那么可以使用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程序中使用它们。
*/
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
{
/* evalExpression()函数返回一个电子制表软件表达式的值。
* 表达式可以定义为:一个或者多个通过许多“+”或者“-”操作符分隔而成的项。
* 这些项自身可以定义为:由“*”或者“/”操作符分隔而成的一个或者多个因子(factor)。
* 通过把表达式分解成项,再把项分解成因子,就可以确保以正确的顺序来使用这些操作符了。
* 例如,“2*C5+D6”就是一个表达式,它由作为第一项的“2*C5"和作为第二项的“D6"构成。
* 项“2*C5”是由作为第一个因子的“2”和作为第二个因子的“C5”组成的,而项“D6"则由一个单一的因子“D6”组成。
* 一个因子可以是一个数(“2")、一个单元格位置(“C5"),或者是一个在圆括号内的表达式,在它们的前面可以有负号。
*
* 首先,调用evalTerm()得到第一项的值。
* 如果它后面紧跟的字符是“.+"或者“”,那么就继续第二次调用evalTerm();
* 否则,表达式就只包一个单一项,并且把它的值作为整个表达式的值而返回。
* 在得到前两项的值之后,根据操作符计算出这操作的结果。
* 如果两项都求出一个double值,就把计算出的结果当作一个doubl 值;否则,把结果设置为Invalid。
* 像前面那样继续操作,直到再没有更多的项为止。
* 这样做可以正确地进行,因为加法和减法都是左相关(left-associative)的;
* 也就是说,“1-2-3”的意思是“(1-2)-3”,而不是“1-(2-3)”。
*/
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
{
/* 除了evalTemm()函数是处理乘法和除法这一点不同之外,它和evalExpression()都很相似。
* 在evalTerm()中唯一的不同就是必须要避免除零,因为在一些处理器中这将是一个错误。
* 尽管测试浮点数值是否相等通常并不明智,因为其中存在取舍问题,但是在这个防止除零的问题上,这样做相等性测试已经足够了。
*/
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
{
/* 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。
*
*/
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;
}