Шаг 176.
Библиотека Qt.
Функциональность приложения. Реализации виджета Spreadsheet

    На этом шаге рассмотрим реализацию виджета Spreadsheet.

    В приложении Электронная таблица в качестве центрального виджета применяется некоторый подкласс класса QTableWidget. Класс QTableWidget уже обеспечивает большинство необходимых нам функций электронной таблицы, но он не может понимать формулы электронной таблицы вида "=А1+А2+А3" и не поддерживает операции с буфером обмена. Мы реализуем эти недостающие функции в классе Spreadsheet, который наследует QTableWidget.

    Класс Spreadsheet происходит от QTableWidget, как показано на рисунке 1.


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

    Виджет QTableWidget фактически является сеткой, представляющей двумерный разряженный массив. На нем отображается часть ячеек всей сетки, полученная при прокрутке изображения пользователем. При вводе пользователем текста в пустую ячейку QTableWidget автоматически создает элемент QTableWidgetItem для хранения текста. QTableWidget происходит от виджета QTableView, одного из классов модели/представления.

    Давайте начнем с реализации виджета Spreadsheet и сначала приведем заголовочный файл:

#ifndef SPREADSHEET_H
#define SPREADSHEET_H
#include <QTableWidget>
/*Заголовочный файл начинается с предварительных объявлений классов Cell и
SpreadsheetCompare. Такие атрибуты ячейки QTableWidget, как ее текст и выравнивание,
хранятся в QTableWidgetItem. В отличие от QTableWidget, класс QTableWidgetItem не
является виджетом - это обычный класс данных.
Класс Cell происходит от QTableWidgetItem*/
class Cell;
class SpreadsheetCompare;

class Spreadsheet : public QTableWidget
{
    Q_OBJECT
public:
    Spreadsheet(QWidget *parent = 0);
/*Функция autoRecalculate() реализуется как встроенная (inline), поскольку
она лишь показывает, задействован или нет режим автоматического перерасчета*/
    bool autoRecalculate() const { return autoRecalc; }
    QString currentLocation() const;
    QString currentFormula() const;
    QTableWidgetSelectionRange selectedRange() const;
/*Ранее мы опирались на использование некоторых открытых функций класса электронной
таблицы Spreadsheet при реализации MainWindow. Например, из MainWindow::newFile() мы
вызывали функцию сlеаr() для очистки электронной таблицы. Кроме того, мы вызывали
функции, унаследованные от QTableWidget, а именно setCurrentCell() и setShowGrid()*/
    void clear();
    bool readFile(const QString &fileName);
    bool writeFile(const QString &fileName);
    void sort(const SpreadsheetCompare &compare);
/*Класс Spreadsheet содержит много слотов, которые реализуют действия пунктов меню
Edit, Tools и Options*/
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);
/*и он содержит один сигнал, modified(),
для уведомления о возникновении любого изменения*/
signals:
    void modified();
/*определяем один закрытый слот, который используется внутри класса Spreadsheet*/
private slots:
    void somethingChanged();
//В закрытой секции этого класса мы объявляем
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;
};
/*Заголовочный файл заканчивается определением класса SpreadsheetCompare*/
class SpreadsheetCompare
{
public:
    bool operator()(const QStringList &row1,
                    const QStringList &row2) const;
    enum { KeyCount = 3 };
    int keys[KeyCount];
    bool ascending[KeyCount];
};
#endif

    Теперь мы рассмотрим реализацию конструктора:

#include <QtGui>
#include <QtWidgets>
#include "cell.h"
#include "spreadsheet.h"

Spreadsheet::Spreadsheet(QWidget *parent)
    : QTableWidget(parent)
{
    autoRecalc = true;
/*при вводе пользователем некоторого текста в пустую ячейку QTableWidget будет
автоматически создавать элемент QTableWidgetItem для хранения этого текста.
Вместо этого мы хотим, чтобы создавались элементы Cell.
Это достигается с помощью вызова в конструкторе функции setItemPrototype().
Всякий раз, когда требуется новый элемент, QTableWidget дублирует элемент,
переданный в качестве прототипа*/
    setItemPrototype(new Cell);
/*Кроме того, в конструкторе мы устанавливаем режим выделения области на значение
QAbstractItemView::ContiguousSelection, чтобы могла быть выделена только
одна прямоугольная область*/
    setSelectionMode(ContiguousSelection);
/*Мы соединяем сигнал itemChanged() виджета таблицы с закрытым слотом
somethingChanged(); это гарантирует вызов слота somethingChanged() при
редактировании ячейки пользователем*/
    connect(this, SIGNAL(itemChanged(QTableWidgetItem *)),
            this, SLOT(somethingChanged()));
/*мы вызываем clear() для изменения размеров таблицы и задания заголовков столбцов*/
    clear();
}

    Функция clear() вызывается из конструктора Spreadsheet для инициализации электронной таблицы. Она также вызывается из MainWindow::newFile(). Мы могли бы использовать QTableWidget::clear() для очистки всех элементов и любых выделений, но в этом случае заголовки имели бы текущий размер.

void Spreadsheet::clear()
{
/*уменьшаем размер электронной таблицы до 0 х 0.
Это приводит к очистке всей электронной таблицы, включая заголовки*/
    setRowCount(0);
    setColumnCount(0);
//устанавливаем ее размер на ColumnCount x RowCount (26 х 999)
    setRowCount(RowCount);
    setColumnCount(ColumnCount);
/*заполняем строку горизонтального заголовка элементами QTableWidgetItem,
содержащими обозначения столбцов. Нам не надо задавать метки строк,
потому что по умолчанию строки обозначаются как "1", "2",..., "26"*/
    for (int i = 0; i < ColumnCount; ++i) {
        QTableWidgetItem *item = new QTableWidgetItem;
        item->setText(QString(QChar('A' + i)));
        setHorizontalHeaderItem(i, item);
    }
//перемещаем курсор на ячейку А1
    setCurrentCell(0, 0);
}

    QTableWidget содержит несколько дочерних виджетов. Сверху располагается горизонтальный заголовок QHeaderView, слева - вертикальный заголовок QHeaderView и две полосы прокрутки QScrollBar. В центральной области размещается специальный виджет, называемый областью отображения (viewport), в котором QTableWidget вычерчивает ячейки. Доступ к различным дочерним виджетам осуществляется с помощью функций, унаследованных от QTableView и QAbstractScrollArea (рисунок 2).


Рис.2. Виджеты, составляющие QTableWidget

    QAbstractScrollArea содержит перемещаемую область отображения и две полосы прокрутки, которые могут включаться и отключаться.

    Закрытая функция cell() возвращает для заданной строки и столбца объект Cell. Она работает почти так же, как QTableWidget::item(), но возвращает указатель на Cell, а не указатель на QTableWidgetItem.

Cell *Spreadsheet::cell(int row, int column) const
{
    return static_cast<Cell *>(item(row, column));
}

    Закрытая функция text() возвращает формулу заданной ячейки. Если сеll() возвращает нулевой указатель, то это означает, что ячейка пустая, и поэтому мы возвращаем пустую строку.

QString Spreadsheet::text(int row, int column) const
{
    Cell *c = cell(row, column);
    if (c) {
        return c->text();
    } else {
        return "";
    }
}

    Функция formula() возвращает формулу ячейки. Во многих случаях формула и текст совпадают; например, формула "Qt" соответствует строке "Qt", поэтому при вводе пользователем в ячейку строки "Qt" и нажатии клавиши Enter в ячейке отобразится текст "Qt".

QString Spreadsheet::formula(int row, int column) const
{
    Cell *c = cell(row, column);
    if (c) {
        return c->formula();
    } else {
        return "";
    }
}

    Но имеется несколько исключений:

  1. Если формула представлена числом, именно оно и будет отображаться. Например, формула "1.50" обозначает значение 1.5 типа double, которое отображается в электронной таблице как выровненное вправо значение "1.5".
  2. Если формула начинается с одиночной кавычки, остальная часть формулы интерпретируется как текст. Например, результатом формулы '12345 будет строка "12345".
  3. Если формула начинается со знака равенства ("="), то ее значение интерпретируется как арифметическое выражение. Например, если ячейка А1 содержит "12" и ячейка А2 содержит "6", то результатом формулы "=А1+А2" будет "18".

    В приложении Электронная таблица каждая непустая ячейка хранится в памяти в виде одного объекта QTableWidgetItem (элемент табличного виджета). Хранение данных в объектах типа "элемент" используется также виджетами QListWidget и QTreeWidget, которые работают с объектами QListWidgetItem и QTreeWidgetItem.

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

    Другие инструментальные средства предусматривают наличие в классах элементов указателя типа void для хранения пользовательских данных. В Qt используется более естественный подход с применением setData() для типа QVariant, однако если требуется иметь указатель void, это можно сделать просто путем создания подкласса для класса элемента, который будет содержать переменную-указатель на член типа void.

    Для данных, к которым предъявляются повышенные требования, например для больших наборов данных, для сложных элементов данных, для интеграции баз данных и для множественных представлений данных, Qt предоставляет набор классов "модель/представление", в которых данные отделены от их визуального представления.

    Задача преобразования формулы в значение выполняется классом Cell. Здесь следует иметь в виду, что отображаемый в ячейке текст соответствует значению, полученному в результате расчета формулы, а не является текстом самой формулы. Закрытая функция setFormula() задает формулу для указанной ячейки.

void Spreadsheet::setFormula(int row, int column,
                             const QString &formula)
{
/*Если ячейка уже имеет объект Cell, мы его повторно используем.
В противном случае мы создаем новый объект Cell и вызываем QTableWidget::setItem()
для вставки его в таблицу*/
    Cell *c = cell(row, column);
    if (!c) {
        c = new Cell;
        setItem(row, column, c);
    }
/*вызываем для этой ячейки функцию setFormula(),
что приводит к перерисовке ячейки, если она отображается на экране*/
    c->setFormula(formula);
}

    Нам не надо беспокоиться об удалении в будущем объекта Cell; QTableWidget является собственником ячейки и будет автоматически удалять ее содержимое в нужное время.

    Функция currentLocation() возвращает текущее положение ячейки, используя обычную форму представления ее координат в электронной таблице с обозначением буквой положения столбца, за которой идет номер строки. Функция MainWindow::updateStatusBar() использует ее для отображения положения ячейки в строке состояния.

QString Spreadsheet::currentLocation() const
{
    return QChar('A' + currentColumn())
           + QString::number(currentRow() + 1);
} 

    Функция currentFormula() возвращает формулу текущей ячейки. Она вызывается из функции MainWindow::updateStatusBar().

QString Spreadsheet::currentFormula() const
{
    return formula(currentRow(), currentColumn());
}    

    Закрытый слот somethingChanged() делает перерасчет всей электронной таблицы, если включен режим Auto-Recalculate (Автопересчет). Он также генерирует сигнал modified().

void Spreadsheet::somethingChanged()
{
    if (autoRecalc)
        recalculate();
    emit modified();
} 

    На следующем шаге рассмотрим загрузку и сохранение файла данных для приложения Электронная таблица.




Предыдущий шаг Содержание Следующий шаг