第4章 实现程序的功能(中央物件)

发布: 2008-05-29 22:14







 



  • 中央物件

  • 创建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()清空电子表格中的所有单元格,并且读取单元格数据。因为文件仅包含非空单元格的数据,并且不是电子表格中所有的单元格都会填入数据,因而我们必须保证在读取之前清空所有的单元格。





 



原文: http://qtchina.tk/?q=node/124

Powered by zexport