概述
实现程序的功能
在前面两章中我们解释了怎么创建 spreadsheet 程序的 UI 界面。在这章中,我们将完成这个程序的功能。还有,我们将看到怎么加载文件和保存文件,怎么将数据保存在内存中,怎么实现剪切板的操作,怎么让 QTableWidget 增加对公式的支持。
中央组件( The Central Widget )
QMainWindow 的中央区域可以被任何组件( widget )占据。这里是一些可能占据中央区域的组件:
1. 使用标准的 Qt 组件。
一个标准的 Qt 组件比如 QTableWidget 或者 QTextEdit 可以被用来作为一个中央组件。这种情况下,程序的一些功能,比如加载和保存文件,必须在其他地方实现(比如,可以在 QMainWindow 的子类中)。
2. 使用一个定制的组件
一些特殊功能的应用程序常常需要在一个定制的组件上显示一些数据。例如,一个图标编辑器的程序,则有一个 IconEditor 组件作为它的中央组件。第 5 章中我们会解释怎么在 Qt 中编写一个定制的组件。
3. 使用一个空白的组件和一个布局管理器( layout manager )
有时候程序的中央区域被许多组件占据着。可以这样来实现这个功能,使用一个 QWidget 类来作为其他组件的父组件,并且使用布局管理器来对这些子组件进行设定大小和排列位置。
4. 使用切分窗口( splitter )
当多个组件需要占据中央区域时,另一个方法是使用 QSplitter 。 QSplitter 可以对它的子组件进行水平的或者垂直的排列,有一个 splitter 句柄可以让用户进行大小控制。 Splitter 可以包含任何组件,包括其他的 splitters 。
5. 使用 MDI 区域
如果一个程序使用 MDI ,则中央区域被一个 QMdiArea 组件占据着,每个 MDI 窗口都是这个组件的一个子窗口。
Layouts, splitters 和 MDI 区域可以跟标准 Qt 组件或者定制组件绑定起来。我们会在第 6 章中对这些类进行更加深入的探讨。
对 spreadsheet 程序来说,一个 QTableWidget 子类被用来作为中央组件。 QTableWidget 类已经提供了 spreadsheet 程序需要的大部分的功能,但是这个类不支持剪贴板操作,也不支持公式运算,比如 ”=A1+A2+A3” 。我们会在 Spreadsheet 类中实现这些功能。
继承 QTableWidget
Spreadsheet 类继承于 QTableWidget ,如下图所示。
QTableWidget 类是一个有效的代表二维散列数组网格。它会在其指定的区域内显示任何用户滚动到的单元格。当用户在一个空的单元格内输入一些文本, QTableWidget 会自动创建一个 QTableWidgetItem 项来保存这些文本。
QTableWidget 继承于 QTableView (其中一个模式 / 视图类,我们会在第 10 章中会更加详细的讲解)。另一个具有很多功能的表格类是 QicsTable 。可以从下列网址获得:
http://www.ics.com
让我们开始实现 Spreadsheet 类,下面是头文件:
#ifndef SPREADSHEET_H
#define SPREADSHEET_H
#include <QTableWidget>
class Cell;
class SpreadsheetCompare;
Cell 类和 SpreadsheetCompare 类的前向申明。
QTableWidget 单元格的属性,例如文本以及对齐等,被保存在 QTableWidgetItem 中。跟 QTableWidget 类不同, QTableWidgetItem 不是一个组件类;它是一个纯数据类。 Cell 类继承于 QTableWidgetItem 类,会在这一章的最后一部分进行讲解。
class Spreadsheet : public QTableWidget
{
Q_OBJECT
public:
Spreadsheet(QWidget *parent = 0);
bool autoRecalculate() const { return autoRecalc; }
QString currentLocation() const;
QString currentFormula() const;
QTableWidgetSelectionRange selectedRange() const;
void clear();
bool readFile(const QString &fileName);
bool writeFile(const QString &fileName);
void sort(const SpreadsheetCompare &compare);
autoRecalculate() 函数是一个内联函数,因为它只返回是否允许自动计算的一个标志。
在第 3 章中,我们在 MainWindow 类中使用类 Spreadsheet 类的几个公有函数。比如,我们在 MainWindow::newFile() 函数中调用 clear() 来清除 spreadsheet 。我们也使用了从 QTableWiget 类继承而来的一些函数,比如 setCurrentCell() 和 setShowGrid() 函数。
public slots:
void cut();
void copy();
void paste();
void
del
();
void selectCurrentRow();
void selectCurrentColumn();
void recalculate();
void setAutoRecalculate(bool recalc);
void findNext(const QString &str, Qt::CaseSensitivity cs);
void findPrevious(const QString &str, Qt::CaseSensitivity cs);
signals:
void modified();
Spreadsheet 类提供了很多槽来执行 Edit , Tools 和 Options 菜单中的一些操作,并提供了一个信号 modified() ,来通知修改已经发生了。
private slots:
void somethingChanged();
我们定义一个私有槽用于 Spreadsheet 类内部使用。
private:
enum { MagicNumber = 0x7F51C883, RowCount = 999, ColumnCount = 26 };
Cell *cell(int row, int column) const;
QString text(int row, int column) const;
QString formula(int row, int column) const;
void setFormula(int row, int column, const QString &formula);
bool autoRecalc;
};
在类的私有区域,我们申明了三个常量,四个函数和一个变量。
class SpreadsheetCompare
{
public:
bool operator()(const QStringList &row1,
const QStringList &row2) const;
enum { KeyCount = 3 };
int keys[KeyCount];
bool ascending[KeyCount];
};
#endif
这里定义了一个 SpreadsheetCompare 类。我们会在讲解 Spreadsheet::sort() 函数时对这个类进行详细讲解。
现在我们来看看具体的实现:
#include <QtGui>
#include "cell.h"
#include "spreadsheet.h"
Spreadsheet::Spreadsheet(QWidget *parent)
: QTableWidget(parent)
{
autoRecalc = true;
setItemPrototype(new Cell);
setSelectionMode(ContiguousSelection);
connect(this, SIGNAL(itemChanged(QTableWidgetItem *)),
this, SLOT(somethingChanged()));
clear();
}
通常当用户在一个空单元格中输入一些文本时, QTableWidget 会自动创建 QTableWidgetItem 来保存这些文本数据。在我们的程序中,我们希望 Cell 被创建来替代上面的 QTableWidgetItem 。我们可以在构造函数中调用 setItemPrototype() 函数来达到这个目的。这样每次需要一个新项时, QTableWidget 内部会克隆 Cell 。
在构造函数中,我们又设置了选择模式为 QAbstractItemView::ContiguousSelection 来允许选中单个矩形区域。我们连接了表格组件的 itemChanged() 信号和私有槽 somethingChanged() ,这样确保当用户编辑一个单元格时, somethingChanged() 槽被调用。最后我们调用 clear() 来重新设置表格大小并且设置列的头信息。
void Spreadsheet::clear()
{
setRowCount(0);
setColumnCount(0);
setRowCount(RowCount);
setColumnCount(ColumnCount);
for (int i = 0; i < ColumnCount; ++i) {
QTableWidgetItem *item = new QTableWidgetItem;
item->setText(QString(QChar('A' + i)));
setHorizontalHeaderItem(i, item);
}
setCurrentCell(0, 0);
}
clear() 函数在 Spreadsheet 构造函数中被调用来初始化 spreadsheet 。这个函数也在 MainWindow::newFile() 函数中被调用。
我们本来可以直接用 QTableWidget::clear() 来清除所有的项及任何选中的区域,但是这里我们需要让每一列的头部保持当前的大小。所以,我们先把表格的大小设置成 0x0, 清除整个 spreadsheet, 包括头部。然后我们设置表格的大小为 ColumnCount x RowCount (26 x 999) 并且用 QTableWidgetItems 类设置水平头部(包含列名称 A , B , ,,, Z )。我们不需要设置垂直的头部标签,因为这些是默认设置为 1,2,,,,,999 。最后我们把当前单元格设置为 A1 。
QTableWidget 有多个子组件组成。在最上面有一个水平的 QHeaderView ,在左边有一个 QHeaderView ,以及两个 QScrollBar 。中间的区域被一个特殊的组件占据着,这个组件我们叫作 viewport , QTableWidget 在这个组件上来画单元格。我们可以调用 QTableView 和 Q A bstractScrollArea 类中的一些函数来得到这些子组件。 QAbstractScrollArea 提供一个可滚动的 viewport 和两个滚动条,滚动条可以被打开也可以被关闭。我们在第 6 章中对 QScrollArea 子类会进行详细的讲解。
Cell *Spreadsheet::cell(int row, int column) const
{
return static_cast<Cell *>(item(row, column));
}
cell() 私有函数根据给定的行和列返回一个 Cell 对象。这个函数类似于 QTableWidget::item() 用来返回 QTableWidgetItem 指针。
QString Spreadsheet::text(int row, int column) const
{
Cell *c = cell(row, column);
if (c) {
return c->text();
} else {
return "";
}
}
私有函数 text() 根据给定的行和列返回文本。如果 cell() 返回 null 指针,说明这个单元格时空的,我们返回一个空字符串。
QString Spreadsheet::formula(int row, int column) const
{
Cell *c = cell(row, column);
if (c) {
return c->formula();
} else {
return "";
}
}
formula() 函数返回此单元格的公式。很多情况下,公式和文本是一样的;比如,公式“ Hello ”即是字符串“ Hello ”,因此当用户在一个单元格中输入“ Hello ”并点击确认,单元格会显示文本“ Hello ”。但是也有一些例外:
1. 如果公式是一个数组。比如,公式是 1.50 ,则被认为是一个双精度值 1.5 ,在 spreadsheet 中就以右对齐方式显示 1.5 。
2. 如果公式以单引号开头,则剩余部分被当作字符串。例如,公式 ’”12345” ,则被当作是字符串“ 12345 ” .
3. 如果公式是以一个等号 = 开头的,则被视为一个代数公式。例如,如果单元格 A1 包含“ 12 ”,单元格 A2 包含“ 6 ”,则公式“ =A1+A2” 被认为是 18 。
保存数据项
在 spreadsheet 程序中,每个非空的单元格都被当作独立的 QTableWidgetItem 对象保存在内存中。保存数据项这一方法也被用于 QListWidget 和 QTreeWidget 类,对应的被保存在 QListWidgetItem 和 QTreeWidgetItem 对象中。
Qt 的 item 类也可以作为数据容器单独使用,并不一定要跟那些组件绑定使用。比如, QTableWidgetItem 已经保存了一些属性,包括字符串,字体,颜色和图标,以及一个指向 QTableWidget 对象的指针。这些 item 也可以保存一些数据( QVariant ) , 包括登记的定制类型,通过继承这些 item 类我们可以提供额外的功能。
老的 SDK 在 item 类里面提供一个 void 指针来保存定制的数据。在 Qt 中,更常用的方法是调用 setData() 函数。但是如果实在需要一个 void 指针,我们可以继承一个 item 类,并且添加一个 void 指针的成员变量。
对那些更加复杂的数据处理,比如大块数据设置,复杂数据项,数据库集成以及多数据视图, Qt 提供了一系列的模式 / 视图( model/view )类,来把数据从他们的 UI 中分离出来。我们会在第 10 章中讲解这部分内容。
把公式转换成值的任务在 Cell 类中完成。我们要清楚 cell 中显示的文本是公式计算的结果,不是公式本身。
void Spreadsheet::setFormula(int row, int column,
const QString &formula)
{
Cell *c = cell(row, column);
if (!c) {
c = new Cell;
setItem(row, column, c);
}
c->setFormula(formula);
}
setFormula() 私有函数用来对指定的单元格设置公式。如果单元格中已经有一个 Cell 对象,我们直接重用它,否则,我们创建一个新的 Cell 对象,并调用 QTableWidget::setItem() 来把它插入到表格中。最后我们调用 Cell 对象自己的 setFormula() 函数,如果单元格显示在屏幕上的话,这个函数会重画单元格。我们不用担心以后怎么删除 Cell 对象, QTableWidget 会对单元格进行管理,它会在适当的时候自动删除它。
QString Spreadsheet::currentLocation() const
{
return QChar('A' + currentColumn())
+ QString::number(currentRow() + 1);
}
currentLocation() 函数返回当前单元格的位置,返回的格式是列字母加上行号。
MainWindow::updateStatusBar() 使用这个函数来得到单元格位置,并在状态栏上进行显示。
QString Spreadsheet::currentFormula() const
{
return formula(currentRow(), currentColumn());
}
currentFormula() 函数返回当前单元格的公式。这个函数在 MainWindow::updateStatusBar() 中被调用。
void Spreadsheet::somethingChanged()
{
if (autoRecalc)
recalculate();
emit modified();
}
somethingChanged() 私有槽用来重新计算整个 spreadsheet, 如果 ” 重新计算 ” 选项被打钩。同时它也发射 modified() 信号。
加载和保存
我们现在来实现 spreadsheet 程序的加载和保存功能,文件以定制的二进制格式保存。我们使用 QFile 和 QDataStream 类,这两个类提供独立于平台的二进制 I/O 接口。
我们先来实现怎么写一个 spreadsheet 文件:
bool Spreadsheet::writeFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
QMessageBox::warning(this, tr("Spreadsheet"),
tr("Cannot write file %1:/n%2.")
.arg(file.fileName())
.arg(file.errorString()));
return false;
}
QDataStream out(&file);
out.setVersion(QDataStream::Qt_4_3);
out << quint32(MagicNumber);
QApplication::setOverrideCursor(Qt::WaitCursor);
for (int row = 0; row < RowCount; ++row) {
for (int column = 0; column < ColumnCount; ++column) {
QString str = formula(row, column);
if (!str.isEmpty())
out << quint16(row) << quint16(column) << str;
}
}
QApplication::restoreOverrideCursor();
return true;
}
writeFile() 函数在 MainWindow::saveFile() 中被调用,用来将文件写到磁盘上。如果成功的话返回 true, 错误将返回 false 。
首先我们用指定的文件名创建一个 QFile 对象,然后调用 open() 打开这个文件。我们又创建了一个 QDataStream 对象对 QFile 对象进行操作,并用这个对象进行写数据。
在写数据之前,我们把鼠标光标改变为标准的等待光标,一旦数据写完了再恢复为原来的光标。在函数的最后,文件会在 QFile 的析构函数中被自动关闭。
QDataStream 支持基本的 C++ 类型,也支持许多 Qt 的类型。句法跟标准的 C++ iostream 类一样。例如:
out << x << y << z;
写变量 x,y,z 的值到 out 流中, 而
in >> x >> y >> z;
从 in 流中读取值到 x,y,z 变量中。因为 C++ 的整型值在不同的平台上可能会有不同的字节大小,如果我们用下列类型进行强制类型转换就比较安全。 qint8, quint8, qint16, quint16, qint32,quint32,qint64 和 quint64.
Spreadsheet 程序的文件格式非常简单。这个文件以一个 32 位数字开始,这个数字用来指示文件格式( MagicNumber ,在 spreadsheet.h 文件中被定义为 0x7F51C883 ,是一个随机数)。紧跟着一系列的数据块,每一块包含单个单元格的行,列和公式。为了节省空间,我们略去了空的单元格。具体格式如下:
由 QDataStream 来表示精确的二进制数据。比如, quint16 类型以大端模式保存为 2 个字节,而 QString 类型则保存为字符串长度,紧跟着 Unicode 字符。
Qt 类型的二进制表示方法自从版本 1.0 以后就涉及到很多。将来的 Qt 版本发布中还会继续添加新的 Qt 类型。默认情况下, QDataStream 使用最新的二进制格式的版本( Qt 4.3 中视版本 9 ),但是它能够读取就得版本。为了避免任何兼容性问题,如果程序将来用新的 Qt 版本进行重新编译时,我们会明确告知 QDataStream 使用版本 9 ,不管我们正在编译的 Qt 是什么版本。( QDataStream::Qt_4_3 就是一个等于 9 的常量。)
QDataStream 功能很丰富,可以用于 QFile, 也可以用于 QBuffer , QProcess,QTcpSocket, QUdpSocket 或者 QSslSocket 。 Qt 也提供 QTextStream 类用于代替 QDataStream 类读写文本文件。第 12 章我们会深入的讲解这些类 , 也会描述一些不同的方法来处理不同的 QDataStream 版本。
bool Spreadsheet::readFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::warning(this, tr("Spreadsheet"),
tr("Cannot read file %1:/n%2.")
.arg(file.fileName())
.arg(file.errorString()));
return false;
}
QDataStream in(&file);
in.setVersion(QDataStream::Qt_4_3);
quint32 magic;
in >> magic;
if (magic != MagicNumber) {
QMessageBox::warning(this, tr("Spreadsheet"),
tr("The file is not a Spreadsheet file."));
return false;
}
clear();
quint16 row;
quint16 column;
QString str;
QApplication::setOverrideCursor(Qt::WaitCursor);
while (!in.atEnd()) {
in >> row >> column >> str;
setFormula(row, column, str);
}
QApplication::restoreOverrideCursor();
return true;
}
readFile() 函数跟 writeFile() 函数很相似。我们使用 QFile 来读取文件,这次我们使用 QIODevice::ReadOnly 而不是 QIODevice::WriteOnly 。然后我们设置 QDataStream 版本为 9. 读取的格式必须总是跟写的格式保持一致。
如果文件开头有正确的 magic number ,我们调用 clear() 来清空 spreadsheet 中所有的单元格,然后把数据读到单元格里面。因为文件只包含那些非空单元格的数据,这样很有可能有些单元格没有设置到,我们必须确保在读入数据之前所有单元格被清空。
编辑菜单的实现
我们现在可以实现编辑菜单的对应的槽函数来。
void Spreadsheet::cut()
{
copy();
del
();
}
cut() 槽对应 Edit|Cut 。实现很简单 , 剪切跟拷贝类似,只是拷贝后再进行删除。
void Spreadsheet::copy()
{
QTableWidgetSelectionRange range = selectedRange();
QString str;
for (int i = 0; i < range.rowCount(); ++i) {
if (i > 0)
str += "/n";
for (int j = 0; j < range.columnCount(); ++j) {
if (j > 0)
str += "/t";
str += formula(range.topRow() + i, range.leftColumn() + j);
}
}
QApplication::clipboard()->setText(str);
}
copy() 槽对应 Edit|Copy 。这个函数遍历当前选中的单元格(如果没有指定区域就是当前单元格)。每个选中的单元格的公式被添加到 QString ,每一行用行分隔符 ’/n’ 分开,每一列用 tab 字符 ’/t’ 。如下图所示:
系统的剪切板在 Qt 中用静态函数 QApplication::clipboard() 来得到。调用 QClipboard::setText() 把文本添加到剪切板上,任何支持纯文本的程序可以使用剪切板上的文本。我们这个 tab 和行分隔符的格式被很多程序相兼容,包括微软的 Excel 。
QTableWidget::selectedRanges() 函数返回一个选中的列表。我们知道这个函数肯定只返回一个选中的区域,因为我们在构造函数中设置了 QAbstractItemView::ContiguousSelection 模式。为了方面,我们定义了 selectedRange() 函数来返回选中的区域:
QTableWidgetSelectionRange Spreadsheet::selectedRange() const
{
QList<QTableWidgetSelectionRange> ranges = selectedRanges();
if (ranges.isEmpty())
return QTableWidgetSelectionRange();
return ranges.first();
}
如果存在一个选中的区域,我们只要返回列表的第一个(也是唯一一个)成员。任何情况下必须都有一个被选中的区域,因为 ContiguousSelection 模式认为当前的单元格属于被选中的区域。
void Spreadsheet::paste()
{
QTableWidgetSelectionRange range = selectedRange();
QString str = QApplication::clipboard()->text();
QStringList rows = str.split('/n');
int numRows = rows.count();
int numColumns = rows.first().count('/t') + 1;
if (range.rowCount() * range.columnCount() != 1
&& (range.rowCount() != numRows
|| range.columnCount() != numColumns)) {
QMessageBox::information(this, tr("Spreadsheet"),
tr("The information cannot be pasted because the copy "
"and paste areas aren't the same size."));
return;
}
for (int i = 0; i < numRows; ++i) {
QStringList columns = rows[i].split('/t');
for (int j = 0; j < numColumns; ++j) {
int row = range.topRow() + i;
int column = range.leftColumn() + j;
if (row < RowCount && column < ColumnCount)
setFormula(row, column, columns[j]);
}
}
somethingChanged();
}
paste() 槽对应 Edit|Paste 。我们从剪切板上取得文本,然后调用静态函数 QString::split() 来把字符串打断成 QStringList 。每一行变成列表中的一个字符串。
接下来,我们来确认拷贝区域。行数就是 QStringList 中字符串的数量;列数就是第一行中 tab 字符的数量再加上 1. 如果只有一个单元格被选中,我们就使用那个单元格作为粘帖区域的左上角。否则,我们使用当前选中的区域作为粘帖的区域。
为了执行粘帖,我们遍历所有的行,然后通过使用 QString::split() 把每一行拆分成所有单元格,这次使用 tab 字符 ’/t’ 作为拆分符。
void Spreadsheet::del()
{
QList<QTableWidgetItem *> items = selectedItems();
if (!items.isEmpty()) {
foreach (QTableWidgetItem *item, items)
delete item;
somethingChanged();
}
}
del() 槽对应 Edit|Delete 。如果有选中的项,这个函数删除它们,然后调用 somethingChanged() 。 使用 delete 删除 Cell 对象就足以清楚单元格。当 QTableWidgetItems 被删除时 QTableWidget 会知道并且自动重画自己。如果我们对那些删除的单元格调用 cell ()会返回 null 指针。
void Spreadsheet::selectCurrentRow()
{
selectRow(currentRow());
}
void Spreadsheet::selectCurrentColumn()
{
selectColumn(currentColumn());
}
selectCurrentRow() 和 selectCurrentColumn() 函数对应 Edit|Select|Row 和 Edit|Select|Column 菜单选项。实现依赖于 QTableWidget 的 selectRow() 和 selectColumn() 函数。我们不需要实现 Edit|Select|All 背后的功能,因为那是 QTableWidget 的继承函数 QAbstractItemView::selectAll() 提供。
void Spreadsheet::findNext(const QString &str, Qt::CaseSensitivity cs)
{
int row = currentRow();
int column = currentColumn() + 1;
while (row < RowCount) {
while (column < ColumnCount) {
if (text(row, column).contains(str, cs)) {
clearSelection();
setCurrentCell(row, column);
activateWindow();
return;
}
++column;
}
column = 0;
++row;
}
QApplication::beep();
}
findNext() 槽从当前光标右边的单元格开始遍历,向右移动直到最后一列,然后继续从下一行的第一列开始,这样一直遍历下去,直到文本被找到或者已经搜索到本文档的最后一个单元格。例如,如果当前单元格是 C24, 我们搜索 D24, E24, …, Z24, 然后 A25, B25, C25, …, Z25, 直到 Z999 。
如果我们找到了匹配的文本,就清除当前选中,把光标移到匹配的单元格上,并激活当前窗口。如果没有找到匹配的文本,我们让程序发出蜂鸣声来表示搜索失败。
void Spreadsheet::findPrevious(const QString &str,
Qt::CaseSensitivity cs)
{
int row = currentRow();
int column = currentColumn() - 1;
while (row >= 0) {
while (column >= 0) {
if (text(row, column).contains(str, cs)) {
clearSelection();
setCurrentCell(row, column);
activateWindow();
return;
}
--column;
}
column = ColumnCount - 1;
--row;
}
QApplication::beep();
}
findPrevious() 槽类似于 findNext() ,除了它是向后遍历,最后终止于单元格 A1.
实现其它的一些菜单
我们现在来实现 Tools 和 Options 菜单的那些槽函数。这些菜单显示如下:
void Spreadsheet::recalculate()
{
for (int row = 0; row < RowCount; ++row) {
for (int column = 0; column < ColumnCount; ++column) {
if (cell(row, column))
cell(row, column)->setDirty();
}
}
viewport()->update();
}
recalculate() 槽对应于菜单项 Tools|Recalculate 。当需要时也会被 Spreadsheet 自动调用。
我们遍历所有的单元格,每个单元格调用 setDirty() 来标记每个单元格都需要重新计算。下次 QTableWidget 调用单元格的 text() 函数来得到单元格的值时,这个值会被重新计算。
然后我们调用 viewport 的 update() 函数来重画整个 spreadsheet 。 QTableWidget 的重画代码会在每个可见的单元格上调用 text() 来得到需要显示的值。因为我们对每个单元格都调用的 setDirty() ,这样调用 text() 将会使用一个新的计算的值。对值的计算也许会要求那些不可见的单元格进行重新计算,这样在 viewport 中就可以显示正确的文本。计算的工作是在 Cell 类中完成的。
void Spreadsheet::setAutoRecalculate(bool recalc)
{
autoRecalc = recalc;
if (autoRecalc)
recalculate();
}
setAutoRecalculate() 槽对应于菜单项 Options|Auto-Recalculate 。如果这个功能被打开,我们立即重新计算这个 spreadsheet 来确保这个 spreadsheet 是最新的;过后, recalculate ()会自动被 somethingChanged() 调用。
我们不需要为菜单项 Options|Show Grid 添加任何代码,因为 QTableWidget 已经有一个 setShowGrid() 槽,这个槽继承于 QTableView 。现在只剩下 Spreadsheet::sort() ,这个函数在 MainWindow::sort() 被调用:
void Spreadsheet::sort(const SpreadsheetCompare &compare)
{
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();
}
这个函数根据 compare 对象中的键值和排序次序(升序或降序)来对选中的行进行排序。我们用 QStringList 来表示每一行的数据,把选中的行保存在这个字符串列表中。我们使用 Qt 的 qStableSort() 算法,为了简便我们按公式而不是按值排序。整个过程用下图来表示。我们会在第 11 章中对 Qt 的标准算法和数据结构进行讲解。
qStableSort() 函数接受一个起始迭代器,一个末尾迭代器和一个比较函数。这个比较函数接受 2 个参数(两个 QStringList ),如果第一个参数比第二个参数小则返回 true, 反之返回 false 。我们传递给 qStableSort() 的是 compare 对象,并不是一个真正的函数,但是我们这里可以这样用,稍后我们对它进行讲解。
qStableSort() 调用以后,我们把数据重新添加到表格中,取消选中,并调用 somethingChanged() 。
在 spreadsheet.h 文件中, SpreadsheetCompare 类定义如下:
class SpreadsheetCompare
{
public:
bool operator()(const QStringList &row1,
const QStringList &row2) const;
enum { KeyCount = 3 };
int keys[KeyCount];
bool ascending[KeyCount];
};
SpreadsheetCompare 类很特殊,因为这个类实现了 () 运算符。这样就允许我们像函数一样来使用这个类。这种类叫做函数对象 (function objects) ,或函数因子( functors )。为了加深了解函数对象,我们给一个简单的例子:
class Square
{
public:
int operator()(int x) const { return x * x; }
}
Square 类提供了一个函数, operator() (int) ,返回参数的平方值。这里我们不是取名一个叫做 compute(int) 的函数,而是直接实现这个操作符(),这样我们就可以像函数一样来使用 Square 对象:
Square square;
int y = square(5);
// y equals 25
现在我们来看看SpreadsheetCompare的例子:
QStringList row1, row2;
SpreadsheetCompare compare;
...
if (compare(row1, row2)) {
// row1 is less than row2
}
compare对象就像compare()函数一样来使用。另外,这个对象可以访问所有的排序键值和次序,因为这些是这个对象的成员变量。
另一个可选的方案是把键值和排序次序值保存在全局变量中,然后用一个compare()函数。然而,使用全局变量来进行通讯代码不是很雅观,也可能导致隐含的bug.当我们需要跟模板函数打交道时(比如qStableSort()),使用函数对象将非常有用。
下面是比较函数的实现,用来比较spreadsheet中的两行:
bool SpreadsheetCompare::operator()(const QStringList &row1,
const QStringList &row2) const
{
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;
}
如果第一行比第二行小,这个操作符返回 true ;反之,返回 false 。 QStableSort() 函数使用这个函数的返回值来进行排序。
SpreadsheetCompare 对象中的键值和排序数组是在 MainWindow::sort() 函数中初始化的。每一个键都保存着一个列索引,或者 -1 (“ 空值 ”) 。
我们按照顺序比较每一行中对应的单元格。一旦发现不同,就返回 true 或 false 值。如果两行都相同,那么返回 false 。 qStableSort() 函数使用排序前的顺序解决这个问题。如果排序前的顺序是 row1 在 row2 之前,且经比较相等,在结果中 row1 还是在 row2 之前。这就是 qStableSort ()和 qSort() 之间的不同,也就是 qStableSort() 比 qSort() 更稳定。
我们现在已经完成了 Spreadsheet 类。在下一节中我们要来实现 Cell 类。这个类用来保存单元格的公式,并且重新实现了 QTableWidgetItem::data() 函数, Spreadsheet 通过 QTableWidgetItem::text() 函数间接调用这个函数,来显示单元格公式的计算结果。
继承 QTableWidgetItem
Cell 类继承于 QTableWidgetItem 类。这个类在 Spreadsheet 中可以很好的工作,而且没有任何对 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 。
cacheIsDirty 如果暂存的值不是最新的,这个值为 true 。
我们这里使用 QVariant 是因为一些单元格的值是 double ,而一些是 QString 类型的。
变量 cachedValue 和 cacheIsDirty 的申明中添加了 C++ 关键字 mutable 。这样做的目的是允许我们在 const 函数中修改它们的值。另一种方法是,我们可以在每次调用 text() 时重新计算这两个变量的值,但是这样效率不高。
注意在这个类的定义中没有 Q_OBJECT 宏。 Cell 类是一个纯 C++ 类,没有任何信号和槽。事实在,因为 QTableWidgetItem 类不是继承于 QObject ,我们不能在这个类中使用信号和槽。 Qt 中所有的 item 类都不是继承于 QObject 类,来保证最低限度的系统开销。如果确实需要信号和槽,可以在包含这些 item 的 widget 中实现,或者可以使用多继承的方法,让其继承于 QObject 。
这里是 cell.cpp 的开始部分:
#include <QtGui>
#include "cell.h"
Cell::Cell()
{
setDirty();
}
在构造函数中,我们仅仅设置缓存为脏。这里不需要传递一个父参数;当单元格用 setItem() 函数被插入到 QTableWidget 时, QTableWidget 会自动接管这个类。
每一个 QTableWidgetItem 类可以保存一些数据,每一个 QVariant 都以一种角色保存一类数据。最常用的角色是 Qt::EditRole 和 Qt::DisplayRole 。编辑角色被用来那些可以编辑的数据,而显示角色则只能用来显示。常常这两种角色的数据是相同的,但是在 Cell 类中编辑角色对应于单元格的公式,而显示角色对应于单元格的值(公式的值)。
QTableWidgetItem *Cell::clone() const
{
return new Cell(*this);
}
当 QTableWidget 需要创建一个新的单元格时, clone() 函数被调用。比如,当用户开始输入一个以前没有用过的单元格时。传给 QTableWidget::setItemPrototype() 函数的实例就是克隆的 item 。我们用 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() ,它只是个方便函数,取得 item 的 Qt::EditRole 数据。
void Cell::setData(int role, const QVariant &value)
{
QTableWidgetItem::setData(role, value);
if (role == Qt::EditRole)
setDirty();
}
如果我们有一个新的公式,我们设置 cacheIsDirty 为 true ,来确保在下次调用 text() 函数时单元格被重新计算。
Cell 类中没有定义 text() 函数,尽管我们在 Spreadsheet::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() 函数是对 QTableWidgetItem 类中的 data() 函数的重新实现。如果用 Qt::DisplayRole 调用,返回显示的文本;如果用 Qt::EditRole 调用则返回公式;如果用 Qt::TextAlignmentRole 调用则返回合适的对齐方式。对于 DisplayRole 情况,根据 value() 函数得到的值来显示相关的内容。如果值无效(公式),我们返回 #### 。
Cell::value() 返回 QVariant 。 QVariant 可以用来存储不同的类型,例如 double 和 QString ,并且提供了可以转换为相应类型的函数。例如,一个保存了 double 类型的 QVariant 调用 toString() 函数将产生 double 的字符串形式。 QVariant 以默认构造函数构建,初始值是 invalid.
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” ),则后面的值视为一个字符串。
如果公式以等号( ’=’ )开头,我们提取后面的字符串,删除里面的空格,然后调用 evalExpression() 计算这个表达式的值。 Pos 参数以引用形式传递,它指示了此函数需要解析的字符的起始位置。调用 evalExpression() 成功以后,位置 pos 的字符应该是 QChar::Null 字符。如果解析失败,我们设置 cacheValue 为 Invalid 。
如果公式即不是以单引号开头也不是以等号开头,我们尝试着把它转换为浮点值 toDouble(). 如果转换成功,我们把 cachedValue 设置为转换后的浮点值;否则,我们设置 cachedValue 为公式字符串。例如,公式为“ 1.50 ” , 则调用 toDouble ()成功, ok 为 true, 返回 1.5 ,而公式“ World Population ”,则调用 toDouble() 失败, ok 为 false ,返回为 0.0 。
通过传递给 toDouble() 一个指向 bool 类型的指针,我们能够知道返回 0.0 的结果是因为这个字符串本身就是 0.0 还是因为转换失败得到的。有时候转换失败得到的 0 值正是我们所需要的,这种情况我们不用传递一个 bool 的指针。考虑到性能和可移植性的原因, Qt 绝不使用 C++ 异常机制来报告一个错误。当然在 Qt 程序中你也可以使用 C++ 异常机制,只要编译器支持。
Value() 函数被声明为 const 。所以我们不得不把 cachedValue 和 cacheIsValid 声明为 mutable 变量,这样编译器允许我们在 const 函数中修改它们。当然我们也可以把 value() 函数申明为非 const 类型的,删除 mutable 关键字,但是这样还是会编译失败,因为我们在 data() 中调用 value() ,而 data() 函数是 const 类型的。
我们现在已经完成了 Spreadsheet 程序,除了一些解析公式没有实现。接下来的部分我们讲述 evalExpression() 和两个帮助函数 evalTerm() 和 evalFactor() 。代码有一点点复杂,我们把代码放在这里讲解使得这个程序完整。因为这部分代码与 GUI 编程无关,你可以略过这部分而直接读第 5 章。
evalExpression() 函数返回 spreadsheet 表达式的值。表达式被定义成一个或多个项组成,这些项之间由 ’+’ 或 ’-‘ 隔开,每一项由一个或多个因子组成,因子由 ’*’ 或 ’/’ 隔开。通过把表达式分解成项式,把项式分解成因子,我们确保操作符按照正确的优先级进行计算。
例如, “2*C5+D6” 表达式中, ”2*C5” 是第一项, D6 是第二项。项“ 2*C5 ”中“ 2 ”是第一个因子, C5 是第二个因子,项“ D6 ”则由单个因子 D6 构成。一个因子可以是一个数字(“ 2 ”),一个单元格位置( ”C5” ) , 或者是一个括号中的表达式,有时候还有一个一元减号。
Spreadsheet 表达式的语法由下图 4.10 定义。每个语法符号(表达式,项,因子)由一个对应的成员函数来解析,函数的结构则完全遵循这个语法结构。按照这种方法写出的解析器我们叫做递归解析器。
图 4.10 spreadsheet 表达式的语法结构图
让我们从 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() 一次;否则,说明这个表达式只含有一个项式,这种情况下这个项式就是整个表达式的值。我们得到 2 个项式以后,根据操作符计算值。如果两个项式都是 double 型的,则结果为 double ,否则,我们设置结构为 Invalid 。
继续按照这样计算,直到没有更多的项式。这个可以正确工作,因为加减法是按从左到右的优先级的,即 ”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() 很像,只是这个函数处理乘除运算。唯一需要注意的地方就是在除法中分母不能为 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() 。当解析一个括号表达式时, evalExpression() 调用 evalTerm() ,而 evalTerm() 又调用 evalFactor() , evalFactor() 会再次调用 evalExpression() 。这就是解析器中的递归调用。
如果因子中不是一个嵌套的表达式,我们提取下一个语法符号,它可能是一个单元格位置或一个数字。如果符号匹配 QRegExp ,我们认为这是一个单元格引用,调用这个单元格的 value() 函数。单元格可以是 spreadsheet 中的任意一个,也可以跟其他单元格具有依赖关系。依赖的存在不是一个问题;这些依赖关系的存在会触发更多的 value() 调用和解析,直到所有的依赖单元格的值都被计算出来。如果这个语法符号不是一个单元格位置,我们认为是一个数字。
下面这些情况会发生什么,如果单元格 A1 包含公式 ”=A1”, 又或者 A1 包含“ =A2 ”而 A2 包含 ”A1” 。尽管我们没有专门编写代码来检测这种圆形封闭依赖关系的存在,在解析器中这些情况会返回一个无效的 QVariant 值。这能正确工作因为我们在调用 evalExpression() 函数之前在 value() 函数中设置了 cacheIsDirty 为 false ,并且 cachedValue 为 Invalid 。如果 evalExpression() 函数在同一个单元格中递归调用 value() ,这个函数会立即返回 Invalid ,然后整个表达式被认为是 Invalid 。
我们现在已经完全完成了公式解析器的编码。可以很容易的扩展这部分代码来处理预定义的 spreadsheet 函数,比如 sum() 和 avg() ,只要扩展因子( Factor )的语法定义即可。另一个比较容易的扩展是用字符串操作数实现 ’+’ 操作符,这种方法不需要语法上的修改。
最后
以上就是平淡钥匙为你收集整理的Qt学习日志-第四章的全部内容,希望文章能够帮你解决Qt学习日志-第四章所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复