- 中央物件
- 创建QTableWidget的子类
- 加载和保存
- 实现“编辑” 菜单
- 实现其他菜单
- 创建QTableWidgetItem的子类
在前面两章,我们阐述了如何创建电子表格程序的用户界面。在本章,我们要编写低层的代码完成该程序开发。我们要做的工作是我们来看看如何加载和保存文件,如何在内存中存储数据,如何实现剪贴板的操作,如何给QtableWidget添加电子表格公式功能的支持。
中央物件
QMainWindow的中央区域可被任何类型的物件占据。下面是所有可能的集合:
1.使用一个标准的Qt物件。
像QTableWidget 或者 QTextEdit 一类的标准物件可以用在中央物件中。这种情况下,程序的功能,如加载和保存文件,必须在另处实现 (例如,在QMainWindow的子类中)。
2.使用一个自定义物件。
特定的应用程序经常需要在自定义物件中显示数据。例如,一个图标编辑程序可能要一个IconEditor物件作为它的中央物件。第5章中将阐述如何在Qt中编写自定义物件。
3.使用一个带布局管理的普通QWidget。
有时应用程序的中央区域被许多的物件占据。这可以通过使用一个QWidget作为所有其他物件的父窗口实现,并通过布局管理调用其子物件的大小和位置。
4.使用一个分隔条。
另一个把多个物件整合在一起的方法是用一个QSplitter。Qsplitter可把它的子物件水平或者垂直排列,并提供分隔手柄给用户调整它们的大小。分隔条可以包含所有类型的物件,当然也包括其他的分隔条。
5.使用MDI工作区。
如果程序使用了MDI,它的中央区域被一个Qworkspace物件占据,每个MDI窗口都是该物件的子物件。
布局,分隔条和MDI工作区都可以组合标准的Qt物件或者自定义物件。第6章将深入阐述这些类。
对于电子表格应用程序,一个QTableWidget的子类被用作中央物件。QTableWidget类已经提供了我们所需要的大多数电子表格功能,但是它不支持剪贴板操作和它不理解像"=A1+A2+A3"这样的电子表格公式。我们将在Spreadsheet类中实现这些没有的功能。
创建QTableWidget的子类
类Spreadsheet继承自QTableWidget。QTableWidget实际上是一个表示二维稀疏数组的网格。它会在指定的维度内显示用户希望显示的单元格。当用户在一个空单元格中输入一些文本的时候,QTableWidget自动创建一个QTableWidgetItem来存储这些文本。
让我们从相应的头文件开始实现Spreadsheet:
#ifndef SPREADSHEET_H
#define SPREADSHEET_H
#include <QTableWidget>
class Cell;
class SpreadsheetCompare;
头文件中包括Cell类和SpreadsheetCompare类的前置声明。
图 4.1 Spreadsheet 和 Cell类的继承树
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()来重置电子表格。我们还使用了一些从QTableWidget继承来的函数,如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提供了许多槽以实现编辑,工作和选项菜单中的操作,并且它还提供了一个信号,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项被创建作为代替。这通过setItemPrototype()调用在构造函数中实现。在内部,QTableWidget将在任何需要一个新项的时候克隆这个作为原类型传入的项实例。
在构造函数中,我们还把选择模式设为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);
}
在Spreadsheet的构造函数中调用clear()初始化该电子表格。它还在MainWindow::newFile()被调用。
我们本应该用QTableWidget::clear()清空所有的项和所有的选中区域,但标题应该还是他们当前的尺寸。相反地,我们可调整表的大小为0x0。这会清空整个电子表格,也包括标题。然后我们把表调用为ColumnCount x RowCount (26 x 999),并用包含列表为"A", "B", …, "Z"的QtableWidgetItems填充标题。我们不需要设置垂直标题的标签,因为这些默认设置为"1", "2", …, "999"。最后,我们把单元格光标移到A1。
QTableWidget由几个子物件组成。在它顶部有一个水平的QHeaderView,在它的左侧有一个QHeaderView,另外还有两个QScrollBar。中央区域由一个叫做视口的专用物件,QTableWidget在其上绘制所有单元格。不同的子物件可以通过继承自QTableView 和 QabstractScrollArea(见图4.2)的函数来访问。QabstractScrollArea提供一个可滚动视口和两个滚动条,它们都可关闭和开启。它的QscrollArea子类将在第6章说明。
图4.2 QTableWidget的组成物件
把数据当项存储
在电子表格程序中,每个非空单元格被存储的内存中的一个独立的QTableWidget对象中。把数据当项存储是一种还被用于QlistWidget和QtreeWidget的方法,它们分别适用于操作QListWidgetItem 和 QTReeWidgetItem。
Qt的项类可用在盒子外作为数据持有人。例如,QTableWidgetItem已经存储了一些属性,包括字符串内容,字体,颜色,图标和一个指向QTableWidget的指针。项本身还能保存数据(QVariant),包括已注册的自定义类型,并且通过派生项类的子类我们还能提供额外的功能。
为存储自定义数据其他工具箱会在它们的项类中提供一个void类型的指针。在Qt中,最自然的方法是使用带一个QVairiant参数的setData(),但如果需要一个void 指针,可通过派生一个项类并加入一个oid类型的成员变量实现。
对于更具挑战的数据处理需求,比如大数据集,复数项,数据库整合,多数据视图,Qt提供一系列把数据从他们的视图中分离出来的模型/视图类。这些将在第10章中说明。
Cell *Spreadsheet::cell(int row, int column) const
{
return static_cast<Cell *>(item(row, column));
}
私有函数cell()对于给出的行号和列号返回一个Cell对象。它几乎和QTableWidget::item()一样,除了它返回的是一个Cell指针而不是一个QTableWidgetItem的指针。
QString Spreadsheet::text(int row, int column) const
{
Cell *c = cell(row, column);
if (c) {
return c->text();
} else {
return "";
}
}
私有函数text()对于给出的单元格返回一相应的文本内容。如果cell()返回一个空指针,那么单元格是空的,我们返回一个空字符串即可。
QString Spreadsheet::formula(int row, int column) const
{
Cell *c = cell(row, column);
if (c) {
return c->formula();
} else {
return "";
}
}
formula()函数返回对应单元格公式。在许多类中,公式和文本是一样的。例如 ,公式“Hello”求值为字符串“Hello”,因此如果用户在一个单元格中输入“Hello”并按Enter,那个单元格就会显示文本“Hello”。但是也有几个例外:
如果公式是一个数字,它就被解释为数字。例如,公式"1.50" 求值为双精度值1.5,它在电子表格中表示为右端对齐的"1.5"。
如果该公式以一个单引号开始,公式的其他部分为解释为字符串。例如,公式" '12345"求值为"12345"。
如果公式以一个等于号('=')开始,则该公式被解释为一个算术公式。例如,如果单元格A1包含“12”,单元格A2包含“6”,那么公式"=A1+A2" 求值为18。
把一个公式转换成值的工作由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()把它插入到表中。最后,我们调用单元格自己的setFormula()函数,如果单元格在屏幕上的话,这会导致该单元格被重绘。
我们不需要担心随后需要删除该单元格。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()私有槽会重新计算整个电子表格。另外它还发出射出modified()信号。
加载和保存
现在要实现电子表格文件的自定义二进制格式加载与保存。我们将使用QFile和QDataStream还做这工作,它们两者都提供了平台无关的二进制I/O。
我们以写电子表格文件开始:
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_1);
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()打开它准备写入。我们还创建了一个操作QFile并向它写入数据的QDataStream对象。
就在我们写入数据前,我们把程序的光标修改为标准等待光标(通过是个大砂漏),并在所有数据都写入后恢复为正常光标。在函数的最后,文件在QFile的析构函数中自动关闭。
QdataStream支持基本的C++类型和许多Qt数据类型。它的语法遵循标准C++ <iostream>类的形式。如:
out << x << y << z;
把变量x,y和z写入到流中,而
in >> x >> y >> z;
从流中读取它们。因为C++基本数据类型char, short, int, long, 和 long long可能在不同的平台上有不同的长度,最安全的是把它们的值转换成qint8, quint8, qint16, quint16, qint32, quint32, qint64, 和 quint64中的一个,因为它们能保存它们的长度与它们期望一致(以位计)。
电子表格程序的文件格式非常简单。电子表格文件以一个表示版本的32位数字开始(魔法数字,在spreadsheet.h,定义为0x7F51C883,其实就是一个随机数)。然后是一系列的块,每个块包含一个行号,列号和公式。为节省空间,我们不定空单元格。
图4.3 电子表格文件格式
数据类型的准确二进制表示由QData-Stream决定。例如,一个quint16被按高字节序存储为两个字节,一个QString 存储为字符串的长度其中跟Unicode格式的字符。
Qt数据类型的二进制表示与Qt 1.0有很大的改进。很有可能在以后的Qt版中还会改进以保持改进现在数据类型或者新Qt类型的步伐。默认情况下,QdataStream会使用最新的二进制版本(在Qt4.1中为V7),但它也能被设置为读更老的版本。为避免程序使用新版本的Qt重新编辑后产生的兼容性问题,我们明确告诉QdataStream使用V7而不管我们所使用的Qt的版本。(QDataStream::Qt_4_1 是一种等价于V7的使得的常量)
QdataStream的功能非常丰富。它可用于QFile, Qbuffer, Qprocess ,QtcpSocket 或者QudpSocket。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_1);
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版本高为V7。读取格式必须总是与写入的格式一致。
如果文件开始有正确的魔法数字版本号,我们就使用clear()清空电子表格中的所有单元格,并且读取单元格数据。因为文件仅包含非空单元格的数据,并且不是电子表格中所有的单元格都会填入数据,因而我们必须保证在读取之前清空所有的单元格。
|