4.2. Создание класса-потомка от QTable.

Начнем создание нашего класса Spreadsheet с оформления файла заголовка:

#ifndef SPREADSHEET_H #define SPREADSHEET_H #include <qstringlist.h> #include <qtable.h> class Cell; class SpreadsheetCompare; Заголовочный файл начинается с опережающего описания классов Cell и SpreadsheetCompare.

Рисунок 4.1. Дерево наследования классов Spreadsheet и Cell.


Атрибуты ячейки в QTable, такие как текст и выравнивание, хранятся в элементе QTableItem. В отличие от QTable, класс QTableItem не является виджетом и предназначен исключительно для хранения данных. Класс Cell порожден от QTableItem. В дополнение к атрибутам родительского класса, он имеет возможность хранить формулу вычисления содержимого ячейки.

Мы подробно обсудим реализацию класса Cell в последнем разделе этой главы.

class Spreadsheet : public QTable { Q_OBJECT public: Spreadsheet(QWidget *parent = 0, const char *name = 0); void clear(); QString currentLocation() const; QString currentFormula() const; bool autoRecalculate() const { return autoRecalc; } bool readFile(const QString &fileName); bool writeFile(const QString &fileName); QTableSelection selection(); void sort(const SpreadsheetCompare &compare); Класс Spreadsheet является потомком класса QTable.

В Главе 3, при разработке MainWindow, мы уже использовали некоторые из публичных методов Spreadsheet. Например, мы вызывали clear() из MainWindow::newFile(). Кроме того были использованы некоторые функции, унаследованные от QTable, например setCurrentCell() и setShowGrid().

public slots: void cut(); void copy(); void paste(); void del(); void selectRow(); void selectColumn(); void selectAll(); void recalculate(); void setAutoRecalculate(bool on); void findNext(const QString &str, bool caseSensitive); void findPrev(const QString &str, bool caseSensitive); signals: void modified(); Spreadsheet предоставляет несколько слотов, которые реализуют функциональность пунктов меню Edit, Tools и Options. protected: QWidget *createEditor(int row, int col, bool initFromCell) const; void endEdit(int row, int col, bool accepted, bool wasReplacing); Дополнительно он перекрывает реализацию ряда виртуальных функций QTable, которые вызываются, когда пользователь изменяет значение в ячейке. Это необходимо для поддержки формул в ячейках. private: enum { MagicNumber = 0x7F51C882, NumRows = 999, NumCols = 26 }; Cell *cell(int row, int col) const; void setFormula(int row, int col, const QString &formula); QString formula(int row, int col) const; void somethingChanged(); bool autoRecalc; }; В приватной секции мы определили три константы, четыре функции и одну переменную. class SpreadsheetCompare { public: bool operator()(const QStringList &row1, const QStringList &row2) const; enum { NumKeys = 3 }; int keys[NumKeys]; bool ascending[NumKeys]; }; #endif Заголовочный файл завершается определением класса SpreadsheetCompare. Мы опишем его, когда коснемся реализации метода Spreadsheet::sort().

Теперь перейдем к рассмотрению реализации каждой из функций:

#include <qapplication.h> #include <qclipboard.h> #include <qdatastream.h> #include <qfile.h> #include <qlineedit.h> #include <qmessagebox.h> #include <qregexp.h> #include <qvariant.h> #include <algorithm> #include <vector> using namespace std; #include "cell.h" #include "spreadsheet.h" Мы подключили заголовочные файлы классов, использующихся в приложении, а так же стандартные заголовки C++: <algorithm> и <vector>. Директива using namespace импортирует все имена из пространства std в глобальное пространство имен, что позволяет использовать сокращенную форму записи: stable_sort() и vector<?> вместо полной формы: std::stable_sort() и std::vector<?>. Spreadsheet::Spreadsheet(QWidget *parent, const char *name) : QTable(parent, name) { autoRecalc = true; setSelectionMode(Single); clear(); } В конструкторе устанавливается режим выборки строк в QTable -- Single. Это означает, что в таблице может существовать только одна выделенная область ячеек, в каждый конкретный момент времени. void Spreadsheet::clear() { setNumRows(0); setNumCols(0); setNumRows(NumRows); setNumCols(NumCols); for (int i = 0; i < NumCols; i++) horizontalHeader()->setLabel(i, QChar('A' + i)); setCurrentCell(0, 0); } Функция clear() вызывается для инициализации таблицы, в конструкторе и в MainWindow::newFile().

Собственно очистка производится за счет изменения размера таблицы до (0 X 0), после чего восстановливается ее первоначальный размер (26 X 999). Затем выполняется заполнение меток столбцов: "A", "B", ..., "Z" (номера столбцов 1, 2, ..., 26, соответственно) и перемещение курсора в ячейку A1.

Рисунок 4.2. Виджеты, составляющие QTable.


QTable состоит из нескольких подчиненных виджетов. В их число входят: горизонтальный заголовок -- QHeader, находящийся в верхней части; вертикальный заголовок -- QHeader, находящийся слева; полосы прокрутки -- QScrollBar справа и снизу. Центральную область занимает специальный виджет, который называется viewport, в котором QTable рисует сетку с ячейками. Доступ к подчиненным виджетам реализуется через функции QTable и его базового класса QScrollView. Например, в функции clear() мы обращались к горизонтальному заголовку через вызов QTable::horizontalHeader().

Хранение данных в виде отдельных объектов

В приложении Spreadsheet, все не пустые ячейки хранятся в памяти, в виде отдельных объектов QTableItem. Этот способ присущ не только QTable, такие классы, как QIconView, QListBox и QListView тоже хранят свои данные в виде отдельных элементов (QIconViewItem, QListBoxItem и QListViewItem, соответственно).

Классы элементов используются в Qt для хранения данных. Например, QTableItem может хранить такие данные, как строка, рисунок и указатель на таблицу QTable. Создавая класс, наследник от класса элемента, мы можем расширить его возможности и хранить в нем дополнительные данные, а за счет перекрытия виртуальных функций -- управлять этими данными.

Некоторые библиотеки, при реализации своих элементов хранения данных, предусматривают в них дополнительный указатель типа void, который может использоваться для присоединения к элементу дополнительных данных. Qt не следует этому примеру и не обременяет свои классы лишними указателями, которые могут не использоваться. Вместо этого, она дает программистам свободу создания дочерних классов и возможность подстраивать их под свои нужды. Если вам так необходим дополнительный указатель на внешнюю структуру данных, то вы с легкостью сможете добавить его в дочернем классе.

В QTable достаточно просто можно "обойти" механизм работы с элементами, путем повторной реализации низкоуровневых функций, таких как paintCell() и clearCell(). Если данные, отображаемые в QTable, уже находятся в памяти или во внешних структурах, то такой способ поможет избежать ненужного дублирования информации. За подробностями обращайтесь к статье "A Model/View Table for Large Datasets".

Ожидается, что Qt 4, в этом отношении, будет проявлять большую гибкость, чем Qt 3. Кроме того, Qt 4 вероятно предложит единственный унифицированный класс элемента данных, используемый всеми классами отображения, допуская показ одного и того же элементами различными способами.

---

QScrollView -- это единственный базовый класс для виджетов, которые могут отображать значительные объемы данных. Он предоставляет область просмотра и две полосы прокрутки, которые могут быть включены и выключены. Подробнее мы остановимся на этом в Главе 6.

Cell *Spreadsheet::cell(int row, int col) const { return (Cell *)item(row, col); } Приватная функция cell() возвращает указатель на объект, находящийся на пересечении заданных строки и столбца. Это практически то же самое, что и QTable::item(), за исключением того, что она возвращает указатель на экземпляр класса Cell, а не QTableItem. QString Spreadsheet::formula(int row, int col) const { Cell *c = cell(row, col); if (c) return c->formula(); else return ""; } Функция formula() возвращает формулу для заданной ячейки. Если cell() вернет пустой указатель (ячейка отсутствует, т.е. пустая), то в качестве формулы возвращается пустая строка. void Spreadsheet::setFormula(int row, int col, const QString &formula) { Cell *c = cell(row, col); if (c) { c->setFormula(formula); updateCell(row, col); } else { setItem(row, col, new Cell(this, formula)); } } Функция setFormula() устанавливает формулу вычисления для заданной ячейки. Если объект хранения данных для ячейки уже существует, то формула записывается в этот объект и затем вызывается updateCell(), чтобы сообщить QTable о необходимости перерисовать ячейку (если она видна на экране). В противном случае создается новый объект Cell и вызывается QTable::setItem(), для вставки объекта в таблицу и перерисовки ячейки. Нам нет нужды беспокоиться об уничтожении Cell, поскольку QTable берет владение объектом на себя и сама удалит его, когда придет время. QString Spreadsheet::currentLocation() const { return QChar('A' + currentColumn()) + QString::number(currentRow() + 1); } Функция currentLocation() возвращает "адрес" ячейки, в обычном, для электронной таблицы, формате, где за символом, обозначающим столбец, следует номер строки. MainWindow::updateCellIndicators() использует эту функцию для отображения адреса текущей ячейки в строке состояния. QString Spreadsheet::currentFormula() const { return formula(currentRow(), currentColumn()); } Функция currentFormula() возвращает формулу для текущей ячейки. Она также вызывается из MainWindow::updateCellIndicators(). QWidget *Spreadsheet::createEditor(int row, int col, bool initFromCell) const { QLineEdit *lineEdit = new QLineEdit(viewport()); lineEdit->setFrame(false); if (initFromCell) lineEdit->setText(formula(row, col)); return lineEdit; } Функция createEditor() перекрывает реализацию в QTable. Она вызывается, когда пользователь начинает редактирование содержимого ячейки -- либо после щелчка мышью по ячейке, либо по нажатии на клавишу F2, либо когда пользователь просто начинает набирать текст. Назначение этой функции заключается в создании виджета-редактора, который будет отображаться поверх ячейки. Если функция вызывается по щелчку мыши или по нажатию на клавишу F2, то initFromCell получает значение true, в результате производится редактирование существующего содержимого ячейки, иначе -- прежние данные игнорируются.

createEditor() создает объект класса QLineEdit и записывает в него содержимое ячейки, если initFromCell содержит значение true. Мы выполнили повторную реализацию этой функции для того, чтобы показывать формулу ячейки вместо ее содержимого.

Объект QLineEdit создается как подчиненный области просмотра QTable. В свою очередь QTable берет на себя обязательство по установке размеров и положения QLineEdit на экране такими, чтобы они полностью совпадали с размерами и положением ячейки. Она так же берется автоматически уничтожить объект QLineEdit, когда необходимость в нем отпадет.

Рисунок 4.3. Редактирование ячейки за счет подстановки QLineEdit.


В большинстве случаев, формула и содержимое ячейки -- суть одно и то же. Например, формула "Hello" превращается в строку "Hello". Таким образом, если пользователь напечатает в ячейке слово "Hello" и нажмет Enter, то ячейка будет отображать слово "Hello". Однако тут есть ряд исключений: Действия по преобразованию формулы в значение выполняются классом Cell. Важное примечание: имейте ввиду, что текст, отображаемый в ячейке, это результат преобразования формулы, а не сама формула. void Spreadsheet::endEdit(int row, int col, bool accepted, bool wasReplacing) { QLineEdit *lineEdit = (QLineEdit *)cellWidget(row, col); if (!lineEdit) return; QString oldFormula = formula(row, col); QString newFormula = lineEdit->text(); QTable::endEdit(row, col, false, wasReplacing); if (accepted && newFormula != oldFormula) { setFormula(row, col, newFormula); somethingChanged(); } } Функция endEdit() перекрывает аналогичную в QTable. Она вызывается, когда пользователь завершает редактирование ячейки -- либо щелчком мыши по любой другой ячейке (что подтверждает внесенные изменения), либо нажатием на клавишу Enter (что так же подтверждает внесенные изменения), либо нажатием на клавишу Esc (что отвергает внесенные изменения). Основное назначение функции -- переместить содержимое компонента редактора в объект Cell, если внесенные изменения подтверждены.

Доступ к редактору выполняется через обращение к QTable::cellWidget(). Мы можем без опаски выполнить приведение типа к QLineEdit, поскольку создаваемый нами компонент редактора -- всегда QLineEdit.

Рисунок 4.4. Передача содержимого QLineEdit обратно в ячейку.


В теле функции вызывается, унаследованная от QTable, функция endEdit(), поскольку мы должны известить QTable об окончании редактирования. В качестве третьего аргумента ей передается false, для предотвращения модификации элемента таблицы, поскольку мы сами выполняем все необходимые действия по созданию и модификации элементов. Если "новая" формула отличается от "старой", то вызывается setFormula(), для записи формулы в объект класса Cell. Вслед за этим вызывается функция somethingChanged(). void Spreadsheet::somethingChanged() { if (autoRecalc) recalculate(); emit modified(); } Она выполняет пересчет всего содержимого таблицы, если установлен флаг Auto-recalculate, а затем выдает сигнал modified().